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.
| Status | Meaning | Retry? |
|---|---|---|
400 | The request was understood but a parameter or a body field is invalid. | No |
401 | The API key is missing or invalid. | No |
403 | The key is valid but this access is not allowed. | No |
404 | The resource does not exist in your organization, or the route does not exist. | No |
409 | The write conflicts with existing data, or the first request with the same `Idempotency-Key` is still running. | No |
413 | The request body is larger than 1 MB. | No |
422 | The `Idempotency-Key` was already used for a different request. | No |
429 | The key exceeded 30 requests per minute, or authentication failed too often. | After Retry-After |
500 | Something failed on our side. | With backoff |
503 | The service is temporarily unable to serve requests. | With backoff |
Server errors never include stack traces, SQL or internals in message.
Handling errors
- Check
response.okbefore readingdata. 4xxother than429: the request is wrong. Logmessageand theX-Request-Id, fix the request, and do not retry it unchanged. One exception: a409saying a request with yourIdempotency-Keyis in progress. Retry that one after a moment, with the same key.429: waitRetry-Afterseconds, then retry.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 anIdempotency-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"async function request(path, attempt = 0) {
const response = await fetch(`https://api-next.vitruve.fit/v1${path}`, {
headers: { 'X-API-Key': process.env.VITRUVE_API_KEY }
})
if (response.ok) return response.json()
const retryable = [429, 500, 503].includes(response.status)
if (!retryable || attempt >= 5) {
const body = await response.json()
throw new Error(`${response.status}: ${body.message} (request ${response.headers.get('x-request-id')})`)
}
const waitSeconds =
Number(response.headers.get('retry-after')) || Math.min(2 ** attempt, 30)
await new Promise((resolve) => setTimeout(resolve, waitSeconds * 1000))
return request(path, attempt + 1)
}import os
import time
import requests
def request(path, **params):
for attempt in range(6):
response = requests.get(
f"https://api-next.vitruve.fit/v1{path}",
params=params,
headers={"X-API-Key": os.environ["VITRUVE_API_KEY"]},
timeout=30,
)
if response.ok:
return response.json()
if response.status_code not in (429, 500, 503) or attempt == 5:
request_id = response.headers.get("X-Request-Id")
raise RuntimeError(
f"{response.status_code}: {response.json()['message']} (request {request_id})"
)
wait = int(response.headers.get("Retry-After", min(2**attempt, 30)))
time.sleep(wait)Status reference
Bad request
Do not retryThe 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-DDnor 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-Keyheader 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 datetimeInvalid path parameters: 'id': Invalid uuidInvalid query parameters: 'cursor': Invalid cursorInvalid request body: 'surname': RequiredInvalid request body: 'teamIds': Team not foundInvalid Idempotency-Key header: 1 to 255 visible ASCII characters
Unauthorized
Do not retryThe API key is missing or invalid.
Causes
- The
X-API-Keyheader 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 keyOrganization is not activePartner credentials required
Forbidden
Do not retryThe 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 subscriptionThe organization's plan doesn't allow this actionAthletes with their own Vitruve account manage their profile themselvesVitruve's library exercises can't be modified
Not found
Do not retryThe 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 foundInjury report not foundRoute not found
Conflict
Do not retryThe 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-Keywas 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 resourceResource already existsA request with this Idempotency-Key is in progress; retry laterThis request already completed; fetch the resource instead
Payload too large
Do not retryThe 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"
}Unprocessable content
Do not retryThe `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"
}Too many requests
Retry after waitingThe 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
403count 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
Internal server error
Retry with backoffSomething failed on our side.
Causes
- An unexpected error. Nothing in
messagereveals 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"
}Service unavailable
Retry with backoffThe 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-Keycould 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 unavailableIdempotency 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.