Rate limiting
The ceilings a request passes, and how to keep batch work away from checkout.
A 429 is not a failure of the API. It is the API telling you that something in your code is
issuing requests faster than it needs to, and the only reason it ever reaches a customer is that the
noisy job and the checkout call share the same quota.
The quota table lives in Rate Limits. This page is about not needing it.
Three ceilings, checked in order
Every authenticated request passes three independent counters, each on a 60 second window. The first one that is full answers, and the body names which.
type | Counts | Scope |
|---|---|---|
team | Every request your team makes | Your plan's ceiling |
api_key | Every request made with one key | The same ceiling as the team |
endpoint | Requests touching payments, transfers, refunds or customers | Tighter, and scaled by plan |
Two consequences are worth internalising. Every key draws on the same team budget, so splitting
traffic across several keys adds no headroom. And the endpoint ceilings are the ones a burst hits
first: a loop over payments will trip endpoint long before it troubles team. On Enterprise,
none of the three is counted.
Requests that arrive without a recognised key are counted per IP at 30 per minute, with type
set to ip. That is what you see when a key is wrong rather than merely busy.
Read the ceiling, do not hardcode it
Every response carries your current team-level position. Those three headers are the truth, and they move when your plan does.
X-RateLimit-Limit: 360
X-RateLimit-Remaining: 357
X-RateLimit-Reset: 1748083260If a job of yours is anywhere near the limit, have it read X-RateLimit-Remaining and slow itself
down before it gets pushed. Reacting to the header costs nothing; reacting to the 429 costs a
round trip and a delay you did not choose.
Two shapes of 429
Most 429s come from the counters above and carry a type. A few routes have a second,
dedicated throttle on top, and those answer without one. The payout writes are the ones to know:
POST /transfers, POST /transfers/batch, POST /accounts/{id}/payouts, POST /beneficiaries, POST /beneficiaries/{id}/receivables and POST /beneficiaries/{id}/receivables/{receivable}/settle share a flat ceiling of 20 per minute per team and environment, independently of your
plan, because a payout executes with no human in the loop. A loop that creates a beneficiary and
then pays it counts twice.
{
"code": 429,
"status": "Too Many Requests",
"message": "Too many requests",
"type": "endpoint",
"retry_after": 24,
"retry_after_human": "00:00:24"
}
{
"code": 429,
"status": "Too Many Requests",
"message": "Too Many requests. Merchant limit : 360",
"retry_after": 24
}Read type when it is there, and treat its absence as a route-specific throttle rather than as a
malformed response. Both shapes carry retry_after in seconds. Only the first also sets the
Retry-After header.
Wait the time you were given
The delay is in the response. Guessing produces either a wasted wait or a second 429.
import { WajubRateLimitError } from '@wajub/node';
export async function withRateLimitRetry(fn, maxAttempts = 5) {
for (let attempt = 1; ; attempt++) {
try {
return await fn();
} catch (err) {
if (!(err instanceof WajubRateLimitError) || attempt >= maxAttempts) throw err;
const seconds = err.retryAfter ?? 2 ** attempt;
await new Promise((r) => setTimeout(r, seconds * 1000 + Math.random() * 500));
}
}
}The SDK reads Retry-After for you and puts it on err.retryAfter. The random tail matters when
several workers were throttled together: without it they all come back at the same instant and refill
the bucket they were waiting on.
Cap the attempts
A retry loop with no ceiling turns a ten minute incident into a queue full of jobs that have each been retrying for ten minutes. Stop after a handful, fail the job, and let your alerting see it.
Keep batch work away from checkout
Reconciliation sweeps, settlement exports, and any loop over
transfers or beneficiaries are where 429s are made.
They are also the easiest to fix, because nobody is waiting on them.
| Habit | Instead |
|---|---|
Promise.all over every id | A paced loop under the endpoint's ceiling, see Performance |
| A sweep every minute | A sweep every 15 minutes, over a wider window |
| Running the export at noon | Running it when your checkout is quiet |
| One key for everything | A separate restricted key for batch work, scoped to the reads it needs |
A separate key does not protect checkout on its own: every key draws on the same team budget and the same endpoint ceilings. It lets you revoke the job without touching checkout. Only pacing keeps a runaway export from spending the requests your checkout needs.
When you genuinely need more
If the ceiling is limiting real traffic rather than a loop, your plan's requests per minute is what moves. Contact support with the endpoint, the pattern, and the volume you expect. Raising a limit around a job that polls every payment every five seconds only delays the conversation.