Rate limits

Per-merchant request quotas, response headers, and recommended retry strategies.

Implement exponential backoff in your integration from the start. Most client libraries support this out of the box — honour the Retry-After header rather than retrying at a fixed interval.

Merchant API rate limit

payzum enforces a per-merchant limit on all authenticated endpoints:

| Parameter | Value | |-----------|-------| | Window | 60 seconds (rolling) | | Max requests | 60 | | Limit key | Merchant ID (derived from the API key) | | Rate-limit header on 429 | Retry-After: 60 |

When the limit is exceeded the server responds immediately with:

HTTP/1.1 429 Too Many Requests
Retry-After: 60
Content-Type: application/json

{ "statusCode": 429, "code": "RATE_LIMIT_EXCEEDED", "message": "..." }

Back off for at least as many seconds as indicated by Retry-After before issuing the next request.

Shared budget across adapters

The /v1/* native API and the /legacy/* compatibility adapter share the same per-merchant rate-limit budget. If your integration uses both paths simultaneously — for example, the native API for invoice creation and the legacy adapter for older shopping-cart plugins — both sets of requests count against the single 60 rpm quota.

Buyer-facing invoice polling

The GET /v1/invoices/:id/status endpoint used by the hosted checkout and embeddable widget to poll invoice progress uses a separate limiter keyed on the client IP address. Polling traffic from buyers does not count against the merchant's 60 rpm budget.

async function fetchWithRetry(url: string, options: RequestInit, maxAttempts = 4): Promise<Response> {
  for (let attempt = 0; attempt < maxAttempts; attempt++) {
    const res = await fetch(url, options);
 
    if (res.status !== 429) return res;
 
    const retryAfter = Number(res.headers.get("Retry-After") ?? 60);
    const jitter = Math.random() * 1000;
    // exponential backoff: 60s, 120s, 240s … capped at Retry-After base
    const delay = retryAfter * 1000 * Math.pow(2, attempt) + jitter;
 
    if (attempt < maxAttempts - 1) await new Promise((r) => setTimeout(r, delay));
  }
  throw new Error("Rate limit: max retry attempts exceeded");
}

Do not retry 429 QUOTA_EXCEEDED with the same request. That error means the merchant has too many open invoices — retrying will keep hitting the same quota wall. Close or expire existing invoices first, then create new ones.