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) | | Headers on 429 | Retry-After: 60, X-RateLimit-Limit: 60 |

When the limit is exceeded the server responds immediately with:

HTTP/1.1 429 Too Many Requests
Retry-After: 60
X-RateLimit-Limit: 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.

No remaining/reset counters

X-RateLimit-Remaining and X-RateLimit-Reset are not emitted — the platform's native rate limiter does not expose live counters. Your client cannot observe how much budget is left mid-window, so self-regulate against the fixed budget of 60 requests per minute (for example with a client-side token bucket) instead of reacting to headers.

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: 60 requests per minute keyed on the client IP address. Polling traffic from buyers does not count against the merchant's 60 rpm budget.

Its 429 response carries the canonical error envelope plus the legacy error slug rate_limited (see Errors — other surfaces).

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.