Guides
Rate limits
How many requests per minute each key allows, the X-RateLimit headers, what happens when you exceed the limit (429 and Retry-After) and how to retry properly.
Last reviewed: October 1, 2026
Per-key limit
Each API key has a requests-per-minute limit. The default is 60 requests per minute, and it can be adjusted per key to match your volume: if you need more, ask us to raise it and tell us what for.
- The limit is per key, not per IP address or per organisation: if you have two keys, each has its own.
- It's counted in calendar-minute windows: the counter resets at the start of every minute.
- Every request with a valid key and the required scope counts, including those that end in a validation error. Requests rejected with
401or403don't count. - The webhooks we send you don't count: they aren't your requests.
Headers
Every authenticated response includes:
| Header | Meaning |
|---|---|
X-RateLimit-Limit | Requests per minute your key allows. |
X-RateLimit-Remaining | Requests you have left in the current minute. |
HTTP/1.1 200 OK
X-Serad-Environment: production
X-RateLimit-Limit: 60
X-RateLimit-Remaining: 57When you exceed the limit
If you go over, the API responds 429 Too Many Requests with the Retry-After header (seconds until the next minute starts) and an RFC 9457 body:
HTTP/1.1 429 Too Many Requests
Content-Type: application/problem+json
Retry-After: 23
X-RateLimit-Limit: 60
X-RateLimit-Remaining: 0
{
"type": "/errors/provider-api/rate-limited",
"title": "Demasiadas peticiones",
"status": 429,
"detail": "Se ha superado el límite de la clave de API. Reintenta en 23 s",
"instance": "/api/integrations/v1/visits",
"timestamp": "2026-10-01T09:10:37.000Z"
}The rejected request has not been executed: if it was a create request, the visit hasn't been created. Wait as long as Retry-After says and repeat it (with the same Idempotency-Key).
Retry strategy
Not every error should be retried. The rule:
| Response | Retry? | How |
|---|---|---|
429 | Yes | Wait exactly Retry-After seconds. |
500, 502, 503, 504, network error, timeout | Yes | Exponential backoff with random jitter: 1 s, 2 s, 4 s, 8 s… up to a cap (for example, 5 attempts or 1 minute). |
409 idempotency-in-progress | Yes | A few seconds, with the same Idempotency-Key. |
400, 401, 403, 404, 409 (others) | No | It's a problem with the request: fix it. Retrying would give the same result. |
And always:
- Retry create requests with the same
Idempotency-Key(idempotency), or you could create duplicate visits. - Add random jitter to the waits: if many of your processes fail at the same time, they shouldn't all retry in the same second.
- Set a cap. If it's still failing after the retries, log the error, alert a person and, if the visit is urgent, have them call the dispatch centre (+34 611 673 280).
// Retry with exponential backoff, jitter and respect for Retry-After.
async function seradFetch(url: string, init: RequestInit, maxAttempts = 5): Promise<Response> {
for (let attempt = 1; ; attempt++) {
let response: Response | null = null;
try {
response = await fetch(url, { ...init, signal: AbortSignal.timeout(15_000) });
} catch {
// Network error or timeout: retried below.
}
const retryable = !response || response.status === 429 || response.status >= 500;
if (!retryable || attempt >= maxAttempts) {
if (!response) throw new Error('The Salud en Ruta API is not responding');
return response;
}
const retryAfter = Number(response?.headers.get('Retry-After'));
const backoff = Math.min(60_000, 1000 * 2 ** (attempt - 1));
const waitMs = response?.status === 429 && retryAfter > 0 ? retryAfter * 1000 : backoff * (0.5 + Math.random());
await new Promise((resolve) => setTimeout(resolve, waitMs));
}
}Something missing or not matching what you see? Email our technical team: info@saludenruta.es.
