Rate limits

The default limit is 100 requests per second per account, burst-tolerant. Cross it and you get a 429 with a Retry-After header. Retries are safe when you pair them with idempotency keys — this page shows the pattern.

The limits

  • 100 requests/second per account, applied separately in test and live mode. Short bursts above the rate are absorbed; sustained excess is limited.
  • The limit counts requests, not payments — a payment that fails over across three providers is still one request.
  • Webhook deliveries to you and hosted checkout traffic don't count against your API limit.
  • Consistently need more? Talk to your Finscale representative about a raised limit before a traffic spike, not during one.

The 429 response

Rate-limited requests are rejected before they do any work — nothing was created, nothing moved. The envelope is the standard error model with type rate_limit_error:

429 Too Many Requests
# Retry-After: 2
{
  "error": {
    "type": "rate_limit_error",
    "code": "rate_limit_exceeded",
    "message": "Too many requests. Retry after the number of seconds given in the Retry-After header.",
    "doc_url": "https://docs.finscale.dev/errors/#rate_limit_error",
    "request_id": "req_7Hf3kQd2"
  }
}

The Retry-After header is the number of seconds to wait before retrying. Honor it — it's the server telling you exactly when capacity returns.

Retrying safely

Two rules make retries correct:

  1. Back off exponentially with jitter. Start from Retry-After, double per attempt, add randomness. A fleet of workers all retrying at the same instant re-creates the spike that caused the 429.
  2. Reuse the same Idempotency-Key. Then a retried POST can never double-create — if the first request actually got through, the replayed response comes back instead. This is what makes retrying a payment safe.
retry.js — backoff with jitter, same idempotency key
async function withRetries(request, maxAttempts = 5) {
  for (let attempt = 1; ; attempt++) {
    const res = await request(); // same Idempotency-Key every attempt
    if (res.status !== 429 || attempt === maxAttempts) return res;

    const retryAfter = Number(res.headers.get("Retry-After") ?? 1);
    const backoff = retryAfter * 1000 * 2 ** (attempt - 1);
    const jitter = Math.random() * 250;
    await new Promise((r) => setTimeout(r, backoff + jitter));
  }
}
Only 429 and 5xx are retryable A 400, 402, or 404 will fail identically every time — retrying burns your budget for nothing. Retry 429 (after Retry-After) and 5xx (with backoff); treat everything else as a bug or a business outcome. See Errors.

Staying under the limit

  • Consume webhooks instead of polling. Polling GET /v1/payments/{id} in a loop is the classic self-inflicted 429; the event arrives on its own.
  • Page at full width. Listing with limit=100 makes five requests do the work of twenty-five at the default page size. See Pagination.
  • Smooth batch jobs. Nightly reconciliation pulling thousands of objects should run at a fixed request rate below the limit, not as fast as the loop allows.
  • Keep live traffic ahead of batch. Run bulk backfills at reduced rate so checkout traffic never queues behind a script.