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

SituationResponseHeader
Over 120 requests in the minute429 rate_limitedRetry-After: <seconds>
Over 300 check starts in the hour429 rate_limitedRetry-After: <seconds>
A sixth check in flight409 concurrent_limitnone; wait for a slot
Transient failure on our side5xx internalnone; 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).

api.ts
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.

On this page