Error codes

Every HTTP status the API returns, the messages behind it, what causes it and whether to retry.

Errors use the standard HTTP status and the same envelope as successes, with ok: false and a message.

{
  "ok": false,
  "timestamp": 1738247125207,
  "message": "Athlete not found"
}

The API has no machine-readable error code field. Branch on the HTTP status, read message to log or show what was wrong, and keep the X-Request-Id response header so we can trace a failure.

StatusMeaningRetry?
400The request was understood but a parameter or a body field is invalid.No
401The API key is missing or invalid.No
403The key is valid but this access is not allowed.No
404The resource does not exist in your organization, or the route does not exist.No
409The write conflicts with existing data, or the first request with the same `Idempotency-Key` is still running.No
413The request body is larger than 1 MB.No
422The `Idempotency-Key` was already used for a different request.No
429The key exceeded 30 requests per minute, or authentication failed too often.After Retry-After
500Something failed on our side.With backoff
503The service is temporarily unable to serve requests.With backoff

Server errors never include stack traces, SQL or internals in message.

Handling errors

  1. Check response.ok before reading data.
  2. 4xx other than 429: the request is wrong. Log message and the X-Request-Id, fix the request, and do not retry it unchanged. One exception: a 409 saying a request with your Idempotency-Key is in progress. Retry that one after a moment, with the same key.
  3. 429: wait Retry-After seconds, then retry.
  4. 5xx: retry with exponential backoff and a cap, for example 1 s, 2 s, 4 s, 8 s up to five attempts. Retry a create with an Idempotency-Key, so a retry never creates twice.
# curl has a built-in retry for 429, 500 and 503
curl --retry 5 --retry-all-errors \
  "https://api-next.vitruve.fit/v1/athletes" \
  -H "X-API-Key: YOUR_API_KEY"

Status reference

400

Bad request

Do not retry

The request was understood but a parameter or a body field is invalid.

Causes

  • A query parameter is out of range, has the wrong type or is not one of the allowed values.
  • A parameter name is misspelled or unknown. Unknown parameters are rejected rather than ignored, so a typo never returns unfiltered data.
  • A date is neither YYYY-MM-DD nor an ISO 8601 datetime.
  • A path id is not a UUID.
  • A cursor was edited or truncated, was served to another organization or for another list, or was issued before 2026-10.
  • A body field is missing, unknown, of the wrong type, or breaks a rule of the endpoint. A referenced id (athlete, team, exercise, metric, muscle) is not in your organization.
  • An Idempotency-Key header is empty, longer than 255 characters or not visible ASCII.

What to do

Read message: it names each offending parameter or field. Fix the request, do not retry it unchanged.

Example

{
  "ok": false,
  "timestamp": 1738247125207,
  "message": "Invalid query parameters: 'limit': Number must be less than or equal to 100"
}

Other messages

  • Invalid query parameters: 'status': Invalid enum value. Expected 'completed' | 'pending', received 'done'
  • Invalid query parameters: Unrecognized key(s) in object: 'userid'
  • Invalid query parameters: 'from': Invalid date: use YYYY-MM-DD or an ISO 8601 datetime
  • Invalid path parameters: 'id': Invalid uuid
  • Invalid query parameters: 'cursor': Invalid cursor
  • Invalid request body: 'surname': Required
  • Invalid request body: 'teamIds': Team not found
  • Invalid Idempotency-Key header: 1 to 255 visible ASCII characters
401

Unauthorized

Do not retry

The API key is missing or invalid.

Causes

  • The X-API-Key header is absent or misspelled.
  • The key is not a valid key, or it was revoked.
  • The organization that owns the key is no longer active.
  • The key is an integration key, created in Settings → Integrations for a partner platform. It only works together with that partner’s secret. Use a key from Settings → API Access instead.

What to do

Create or copy an API key in Settings → API Access and send it in X-API-Key.

Example

{
  "ok": false,
  "timestamp": 1738247125207,
  "message": "Missing X-API-Key header"
}

Other messages

  • Invalid API key
  • Organization is not active
  • Partner credentials required
403

Forbidden

Do not retry

The key is valid but this access is not allowed.

Causes

  • The organization's plan doesn't include API access anymore, or its subscription ended. Access follows the plan on every request.
  • A write without an active subscription, or one the plan doesn't include or whose limit is reached (for example the plan's athlete limit).
  • A write to a record the API can't change: an athlete who claimed their own Vitruve account, an exercise of Vitruve's library, or the organization's owner.

What to do

Check the plan in Settings → Plan. For a record the API can’t change, change it in the Vitruve app instead.

Example

{
  "ok": false,
  "timestamp": 1738247125207,
  "message": "Your plan no longer includes API access"
}

Other messages

  • The organization has no active subscription
  • The organization's plan doesn't allow this action
  • Athletes with their own Vitruve account manage their profile themselves
  • Vitruve's library exercises can't be modified
404

Not found

Do not retry

The resource does not exist in your organization, or the route does not exist.

Causes

  • The id is well-formed but belongs to another organization, was deleted, or is of another kind (for example a template id sent to /workouts/{id}).
  • The row is older than the history the plan includes (workouts, wellness questionnaires and injury reports).
  • A write to an injury report or a load-velocity profile of an athlete who left the organization.
  • The path is wrong, or the path does not take that method (for example POST /workouts).

What to do

Check the id with the matching list endpoint and the path against the reference.

Example

{
  "ok": false,
  "timestamp": 1738247125207,
  "message": "Athlete not found"
}

Other messages

  • Workout not found
  • Injury report not found
  • Route not found
409

Conflict

Do not retry

The write conflicts with existing data, or the first request with the same `Idempotency-Key` is still running.

Causes

  • The email belongs to another user, in any letter case.
  • A record the write refers to was changed or removed by a concurrent request.
  • A retry was sent while the first request with its Idempotency-Key was still running.
  • A retry of a create that already succeeded, whose answer was over 32 KB and so was not kept.

What to do

Look up the existing record before writing again. For a request still in progress, wait a moment and retry with the same Idempotency-Key.

Example

{
  "ok": false,
  "timestamp": 1738247125207,
  "message": "A user with email marta.ibanez@example.com already exists"
}

Other messages

  • Conflicts with a related resource
  • Resource already exists
  • A request with this Idempotency-Key is in progress; retry later
  • This request already completed; fetch the resource instead
413

Payload too large

Do not retry

The request body is larger than 1 MB.

Causes

  • The JSON body of a write is larger than 1 MB.

What to do

Send a smaller body.

Example

{
  "ok": false,
  "timestamp": 1738247125207,
  "message": "Payload too large"
}
422

Unprocessable content

Do not retry

The `Idempotency-Key` was already used for a different request.

Causes

  • The key was first sent, within the last hour, with another method, path or body. Keys belong to the API key that sent them.

What to do

Use a new key for every operation. Reuse a key only to retry the exact same request.

Example

{
  "ok": false,
  "timestamp": 1738247125207,
  "message": "This Idempotency-Key was already used for another request"
}
429

Too many requests

Retry after waiting

The key exceeded 30 requests per minute, or authentication failed too often.

Causes

  • More than 30 requests were sent with the same key in the current one-minute window, across every server. Requests answered 403 count too.
  • The same address failed authentication 30 times in the current minute. IPv6 addresses count by their /64.

What to do

Wait Retry-After seconds, then continue. Page with limit=100 and cache the catalogs to need fewer requests.

Headers

  • Retry-After: Seconds until the window resets.
  • RateLimit-Reset: Same value, present on every response past the key check.

Example

{
  "ok": false,
  "timestamp": 1738247125207,
  "message": "Rate limit of 30 requests per minute exceeded"
}

Other messages

  • Too many failed authentication attempts
500

Internal server error

Retry with backoff

Something failed on our side.

Causes

  • An unexpected error. Nothing in message reveals internals.
  • An analytics query failed while running.

What to do

Retry with backoff. If it persists, narrow the request (a shorter date range for analytics) or contact support with the X-Request-Id header value.

Example

{
  "ok": false,
  "timestamp": 1738247125207,
  "message": "Internal server error"
}
503

Service unavailable

Retry with backoff

The service is temporarily unable to serve requests.

Causes

  • The database or the rate-limit store was unreachable or timed out.
  • A query ran past the 15-second limit.
  • A create sent with an Idempotency-Key could not reach the idempotency store. Nothing was written.

What to do

Retry with exponential backoff. If a query keeps running past the limit, narrow it, for example with a shorter date range.

Example

{
  "ok": false,
  "timestamp": 1738247125207,
  "message": "Service temporarily unavailable"
}

Other messages

  • Rate limiter unavailable
  • Idempotency store unavailable

Reporting a problem

Write to support@vitruve.fit with the X-Request-Id of the failing request, the endpoint, and the time. Do not include your API key.

On this page