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
| Scope | Ceiling per 60 seconds | Counted by |
|---|---|---|
| Unauthenticated | 30 | Calling IP, only for a call without a key |
| Account | Your plan: 120 on Pay as you go, 360 on Growth, 720 on Scale | Your account, across all its keys, sandbox and live apart |
| API key | The same figure as the account | The exact Authorization value |
| Endpoint | 30 payments, 20 transfers, 10 refunds, 50 customers, times your plan's figure divided by 120. Reading a transfer does not count in the transfers share | Account, environment and endpoint |
| Payout writes | 20 | POST /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:
X-RateLimit-Limit: 360
X-RateLimit-Remaining: 357
X-RateLimit-Reset: 1748083260X-RateLimit-LimitintegeroptionalX-RateLimit-RemainingintegeroptionalX-RateLimit-Resetunix timestampoptionalThese 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
typestringoptionalteam (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_afterintegeroptionalRetry-After header.retry_after_humanstringoptionalHH: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.
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.
Flatten your peaks
Reconciliation jobs, exports and bulk payouts should pace themselves rather than fire a
thousand requests and absorb the 429s. Reading X-RateLimit-Remaining and pausing when it
approaches zero costs less than retrying, and keeps your interactive traffic unaffected.