Skip to content
Loading keys…

Rate limiting

Quotas, X-RateLimit headers and the right back-off strategy.

Wajub throttles on several axes at once: your account, the individual key, and a few sensitive endpoints, or the calling IP when a request carries no key. A request needs to clear all of them. Whichever one trips first decides your 429.

Platform limits live elsewhere

This page covers request throttling. For upload sizes, webhook payload caps and account quotas, see Platform limits.

The limits

ScopeCeiling per 60 secondsCounted by
Unauthenticated30Calling IP, only for a call without a key
AccountYour plan: 120 on Pay as you go, 360 on Growth, 720 on ScaleYour account, across all its keys, sandbox and live apart
API keyThe same figure as the accountThe exact Authorization value
Endpoint30 payments, 20 transfers, 10 refunds, 50 customers, times your plan's figure divided by 120. Reading a transfer does not count in the transfers shareAccount, environment and endpoint
Payout writes20POST /transfers, POST /transfers/batch, POST /accounts/{id}/payouts, POST /beneficiaries, POST /beneficiaries/{id}/receivables and POST /beneficiaries/{id}/receivables/{receivable}/settle, together, per account and environment

Enterprise is not throttled by plan

An Enterprise account has no account, key or endpoint ceiling. The payout writes ceiling is the only one that applies whatever the plan.

Sandbox and live are counted apart. Each gets the whole of the ceilings above, so a load test run with a sandbox key never spends the minute of your live checkout. The API key row needs nothing for that: a sandbox key and a live key are two keys.

Read the limit, do not hardcode it

Your account quota depends on your plan and on any custom ceiling support has set for you. Rather than mirroring a table that can change under you, read it back from every response:

Response headers
X-RateLimit-Limit: 360
X-RateLimit-Remaining: 357
X-RateLimit-Reset: 1748083260
X-RateLimit-Limitintegeroptional
Your account ceiling for the current 60-second window.
X-RateLimit-Remainingintegeroptional
Requests left in that window before the account limit trips.
X-RateLimit-Resetunix timestampoptional
When the account counter starts over: the end of the current window, the same value on every call of that window.

These three headers describe the account counter only. The per-key, per-IP and per-endpoint counters are not exposed, so a 429 can arrive while X-RateLimit-Remaining still looks healthy. The type field in the body says which one it was.

No headers means no ceiling

An account on an unlimited plan gets no X-RateLimit-* headers at all, rather than a fake number. Treat their absence as "not throttled at the account level", never as zero.

What a 429 looks like

Response · 429 Too Many Requests
{
"code": 429,
"status": "Too Many Requests",
"message": "Too many requests",
"type": "team",
"retry_after": 42,
"retry_after_human": "00:00:42"
}
typestringoptional
Which limiter tripped: team (your account), api_key, ip (a call without a key), endpoint, or a named throttle such as payout_writes, beneficiary_verifications or checkout_process.
retry_afterintegeroptional
Seconds to wait. Also sent as the Retry-After header.
retry_after_humanstringoptional
The same delay as HH:MM:SS, for logs and support tickets.

Read type before you react. A team or api_key 429 means slow down everywhere; an endpoint one means only that resource is saturated and the rest of your traffic can continue; an ip one means the call went out without a key.

From a browser

A page calling the API with a public key can read every answer, the 429 included: it carries the CORS headers like any other response. The page's code can read Retry-After, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset and X-Wajub-Version, so it can wait the time the API asks for instead of guessing.

The tighter throttles, such as the one on payout writes, answer with the same body: type names the throttle (payout_writes), and retry_after and the Retry-After header give the real wait. Treat any 429 the same way: read retry_after, wait, retry.

Response · 429 on a payout write
{
"code": 429,
"status": "Too Many Requests",
"message": "Too many requests",
"type": "payout_writes",
"retry_after": 17,
"retry_after_human": "00:00:17"
}

Backing off

Honour Retry-After when it is there, fall back to exponential back-off with jitter when it is not. Jitter matters: without it, every worker throttled at the same second retries at the same second.

async function withRetry(send, max = 3) {
for (let attempt = 0; ; attempt++) {
  const res = await send();

  if (res.status !== 429 || attempt >= max) return res;

  const header = Number(res.headers.get('retry-after'));
  const wait = Number.isFinite(header) && header > 0
    ? header
    : 2 ** attempt * (0.5 + Math.random() * 0.5);

  await new Promise((r) => setTimeout(r, wait * 1000));
}
}

The PHP example reuses the same $options, and therefore the same idempotency key, so the retry cannot create a second payment. See Idempotency.

The SDK already retries

The official SDKs retry 429, 500, 502, 503 and 504 twice before raising, honouring Retry-After and otherwise backing off exponentially with jitter. Add your own retry loop only around a whole business operation, never around each call.

What did you think of this content?