Skip to content

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.

SurfaceLimitCounted perOn exhaustion
/v1/*300 requests / minuteWorkspace429 rate_limited
/v1/public/*30 requests / hourIP address429 rate_limited
/v1/search/*Also charged in creditsWorkspace402 payment_required
/v1/exports3 concurrent jobsWorkspace409 conflict

Response headers

Every response carries the state of the bucket it was counted against, so you can throttle before you are throttled.

Headers on every /v1 response
HTTP/1.1 200 OK
x-ratelimit-limit: 300
x-ratelimit-remaining: 287
x-ratelimit-reset: 41
x-request-id: req_8c0f1e4a9b
HeaderMeaning
x-ratelimit-limitSize of the bucket applied to this request.
x-ratelimit-remainingRequests left before the bucket is empty.
x-ratelimit-resetSeconds until the bucket is full again.
retry-afterSent only on a 429. Seconds to wait before retrying.
x-request-idQuote 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 }));