Rate limits
30 requests per minute per customer key, the failed-authentication budget of your secret, and how to back off on 429.
Two limits apply to partners. One is per customer, the other covers your whole platform.
| Limit | Counted per | Threshold | Answer |
|---|---|---|---|
| Requests | Customer key | 30 per minute | 429 Rate limit of 30 requests per minute exceeded |
| Failed authentication | Your partner secret | 600 failures per minute | 429 Too many failed authentication attempts |
Per customer key
Each customer key can send up to 30 requests per minute, counted in fixed one-minute windows across every server of the API. Every request that passes authentication counts, whatever it answers: a 403 costs the same as a 200. One large customer cannot slow down the others, and you can sync several customers in parallel.
Every response, successful or not, reports the key's current window:
| Header | Meaning |
|---|---|
RateLimit-Limit | Requests allowed per window: 30. |
RateLimit-Remaining | Requests left in the current window. |
RateLimit-Reset | Seconds until the window resets. |
Beyond the limit the API answers 429 with a Retry-After header holding the seconds to wait. Pause that customer only, and continue with the others.
Failed authentication, for your whole platform
Requests that fail authentication are counted per minute, and where they count depends on whether the request proves it is yours:
| Request | Counts against | Limit |
|---|---|---|
| One of your secrets, current or revoked, from one of your registered addresses | That secret | 600 failures a minute |
| No secret, an unknown one, or an unregistered address | The client address (IPv6 by its /64) | 30 failures a minute |
Past the limit, every request that counts against the same secret or address answers 429 Too many failed authentication attempts until the minute ends, whatever customer key it sends. A request that proves it is yours is never blocked by its address, so other traffic behind your addresses can't lock you out.
A sync that keeps retrying dead keys stops itself once it spends the 600. When a key answers 401, mark the customer disconnected and stop calling it. Fix a missing secret or an unregistered address first: those failures count against your address, with the lower limit.
When the limiter is unavailable
The API never serves a request it cannot count. If the rate-limit store is briefly unreachable, it answers 503 Rate limiter unavailable: retry with backoff.
Stay under the limits
- Use
limit=100on lists. It needs half the requests of the default page size. - Cache the catalogs once for all customers.
/metrics,/prescription-types,/units,/musclesand the global part of/exercisesare the same for everyone and change rarely. - Filter on the server. Use
fromanduserIdinstead of downloading everything and filtering yourself. - Keep each customer's requests sequential, and bound how many customers you sync in parallel.
Back off on 429
Wait Retry-After seconds, then continue where you stopped. Do not retry immediately: each retry also counts against the next window.
# 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)Filtering and search
Query parameter conventions shared by list endpoints: search, date ranges and time zones, booleans, enums, and strict validation.
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.