Examples use fictional data. Every company, person, address and count in them is invented, and their domains use the reserved .example TLD.
Getting started
Rate limits
Two buckets, four headers, one retry strategy. Nothing here should surprise you at 3am.
The buckets
Limits are counted per workspace for authenticated traffic and per IP address for the public endpoints. A bucket refills continuously rather than resetting on the minute, so a well-behaved client running at the limit never sees a cliff.
| Surface | Limit | Counted per | On exhaustion |
|---|---|---|---|
/v1/* | 300 requests / minute | Workspace | 429 rate_limited |
/v1/public/* | 30 requests / hour | IP address | 429 rate_limited |
/v1/search/* | Also charged in credits | Workspace | 402 payment_required |
/v1/exports | 3 concurrent jobs | Workspace | 409 conflict |
Response headers
Every response carries the state of the bucket it was counted against, so you can throttle before you are throttled.
HTTP/1.1 200 OK
x-ratelimit-limit: 300
x-ratelimit-remaining: 287
x-ratelimit-reset: 41
x-request-id: req_8c0f1e4a9b| Header | Meaning |
|---|---|
x-ratelimit-limit | Size of the bucket applied to this request. |
x-ratelimit-remaining | Requests left before the bucket is empty. |
x-ratelimit-reset | Seconds until the bucket is full again. |
retry-after | Sent only on a 429. Seconds to wait before retrying. |
x-request-id | Quote this in a support request and we can find the exact call. |
Retrying politely
Retry 429 and 502; never retry 400, 401, 402 or 409 — those will fail identically forever. Honour retry-after when it is present and fall back to exponential backoff with jitter when it is not.
import { ApiClientError } from '@salvo/sdk';
async function withRetry(fn, attempts = 5) {
for (let i = 0; ; i++) {
try {
return await fn();
} catch (err) {
const retryable = err instanceof ApiClientError && [429, 502].includes(err.status);
if (!retryable || i >= attempts - 1) throw err;
const wait = Math.min(30_000, 2 ** i * 500) * (0.7 + Math.random() * 0.6);
await new Promise((r) => setTimeout(r, wait));
}
}
}
const res = await withRetry(() => salvo.search.people({ titles: ['Head of Events'], limit: 100 }));