Rate limits and backoff
The per-key limits, the headers and codes that expose them, and one retry recipe that handles throttling, concurrency and transient failures.
- Requests
- 120 per minute per key
- Check starts
- 300 per hour per key
- In flight
- 5 checks per account
Every route counts toward the per-minute limit, polls included, so a tight polling loop over many checks is the usual way to hit it. Poll every few seconds, not continuously; the summary view is cheap but not free.
What the API answers
| Situation | Response | Header |
|---|---|---|
| Over 120 requests in the minute | 429 rate_limited | Retry-After: <seconds> |
| Over 300 check starts in the hour | 429 rate_limited | Retry-After: <seconds> |
| A sixth check in flight | 409 concurrent_limit | none; wait for a slot |
| Transient failure on our side | 5xx internal | none; retry with backoff |
One retry recipe
Retry on 429 (honoring Retry-After), on 5xx, and on a dropped connection. Do not retry 4xx other than 429: the request is wrong and will be wrong again. Retry a create call only with an Idempotency-Key (why).
export async function api(path: string, init: RequestInit & { attempts?: number } = {}) {
const { attempts = 4, ...rest } = init;
let delay = 1000;
for (let attempt = 1; ; attempt++) {
let res: Response | undefined;
try {
res = await fetch(`https://api.silvertext.com${path}`, {
...rest,
headers: {
Authorization: `Bearer ${process.env.SILVERTEXT_API_KEY}`,
'Content-Type': 'application/json',
...rest.headers,
},
});
} catch (err) {
if (attempt >= attempts) throw err; // network failure, retried below
}
if (res?.ok) return res.status === 202 || res.status === 200 ? res.json() : null;
const retryable = !res || res.status === 429 || res.status >= 500;
if (!retryable || attempt >= attempts) {
const body = await res?.json().catch(() => null);
throw new Error(body?.error?.message ?? `HTTP ${res?.status}`);
}
const retryAfter = Number(res?.headers.get('Retry-After'));
const wait = retryAfter > 0 ? retryAfter * 1000 : delay + Math.random() * 500;
await new Promise((r) => setTimeout(r, wait));
delay = Math.min(delay * 2, 30_000);
}
}Jitter matters: several workers backing off on the same schedule return in a wave and trip the limit again.
Concurrency is not a retry case
409 concurrent_limit will keep answering 409 until a check finishes. Keep a pool of in-flight ids, poll them, and submit the next document when one leaves the pool. Batch checking shows a five-wide pool.
Need more?
The rails are sized for real integrations, and spend is bounded by the credit balance rather than by these limits. If a product needs more than 300 checks an hour or more than five in flight, talk to us with the shape of the workload.