Error codes
Every HTTP status a partner request can get, the messages behind it, what causes it, whether to retry, and which errors should pause or disconnect a customer.
Errors use the standard HTTP status and the same envelope as successes, with ok: false and a message.
{
"ok": false,
"timestamp": 1738247125207,
"message": "Your plan no longer includes API access"
}The API has no machine-readable error code field. Branch on the HTTP status and, for 401 and 403, on message: it tells a revoked customer key apart from a problem on your side. Keep the X-Request-Id response header so we can trace a failure.
| Status | Meaning | Retry? |
|---|---|---|
400 | The request was understood but a parameter is invalid. | No |
401 | The customer key or your partner secret is missing or invalid. | No |
403 | The key is valid but this access is not allowed. | No |
404 | The resource does not exist in the customer’s organization, or the route does not exist. | No |
405 | Partners only read: the request used a method other than `GET`. | No |
429 | A customer 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.
Is it the customer or you?
Some errors concern one customer, others your whole platform. Handle them at the right level:
| Answer | Concerns | Action |
|---|---|---|
401 Invalid API key | One customer | Mark them disconnected and stop calling the key: retrying spends your secret's failed-authentication budget. |
401 Organization is not active | One customer | Stop syncing and delete the key. |
403 Your plan no longer includes API access | One customer | Pause them and retry once a day. |
401 Partner credentials required, 401 Invalid partner credentials | Your platform | Your secret is missing, wrong or revoked. |
403 Request origin not allowed for this integration | Your platform | You called from an address you did not register. |
403 This integration is not available | Your platform | Your integration is disabled. Contact Vitruve. |
405 Method not allowed | Your code | The request is not a GET. Partners only read. |
404 Route not found | Your code | The path is not one of the partner endpoints. |
429 Too many failed authentication attempts | Your platform | Your secret, or your address, spent its failed-authentication budget. Stop retrying keys that answer 401. |
A platform-level error with a valid customer key usually means every customer is affected: alert your team instead of disconnecting customers.
Handling errors
- Check
response.okbefore readingdata. 4xxother than429: do not retry the request unchanged. Act on the table above.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. Do not move your sync checkpoint.
# curl has a built-in retry for 429, 500 and 503
curl --retry 5 --retry-all-errors \
"https://api-integrations-next.vitruve.fit/v1/athletes" \
-H "X-API-Key: CUSTOMER_INTEGRATION_KEY" -H "X-Partner-Secret: YOUR_PARTNER_SECRET"async function request(path, attempt = 0) {
const response = await fetch(`https://api-integrations-next.vitruve.fit/v1${path}`, {
headers: { 'X-API-Key': customer.vitruveKey, 'X-Partner-Secret': process.env.VITRUVE_PARTNER_SECRET }
})
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-integrations-next.vitruve.fit/v1{path}",
params=params,
headers={"X-API-Key": customer.vitruve_key, "X-Partner-Secret": os.environ["VITRUVE_PARTNER_SECRET"]},
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 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.
What to do
Read message: it names each offending parameter. 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 cursor
Unauthorized
Do not retryThe customer key or your partner secret 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 request has no
X-Partner-Secret, or a secret that is unknown, revoked or issued to another partner. A customer key only works with the secret of the platform it was created for.
What to do
Send the customer’s integration key and your partner secret on every request. If the key itself is invalid, stop using it and ask the customer to create a new one: repeated 401s throttle your address.
Example
{
"ok": false,
"timestamp": 1738247125207,
"message": "Missing X-API-Key header"
}Other messages
Invalid API keyOrganization is not activePartner credentials requiredInvalid partner credentials
Forbidden
Do not retryThe key is valid but this access is not allowed.
Causes
- The customer's plan doesn't include export integrations anymore, or their subscription ended. Access follows the plan on every request.
- The request came from an IP address you did not register with Vitruve.
- Your platform’s integration is disabled.
What to do
Pause the customer and tell them when their plan is the cause. Otherwise call from a registered address.
Example
{
"ok": false,
"timestamp": 1738247125207,
"message": "Your plan no longer includes API access"
}Other messages
Request origin not allowed for this integrationThis integration is not available
Not found
Do not retryThe resource does not exist in the customer’s 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 workout is older than the history the customer’s plan includes.
- The path is wrong, or it is not one of the partner endpoints.
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 foundRoute not found
Method not allowed
Do not retryPartners only read: the request used a method other than `GET`.
Causes
- The request is a
POST,PATCH,PUTorDELETE. A partner never changes customer data.
What to do
Use GET. The Allow header lists the methods the API accepts.
Headers
Allow: `GET, HEAD`.
Example
{
"ok": false,
"timestamp": 1738247125207,
"message": "Method not allowed"
}Too many requests
Retry after waitingA customer 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. - Requests with your partner secret, from your registered addresses, failed authentication 600 times in the current minute, across every customer key.
- An address that is not registered, or that sent no valid secret, 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, the time, and the customer's organization id. Never include the customer key or your secret.