Rate limits
How much you can call, how the budget is shared across your keys, and how to handle a 429 without making things worse.
Money-movement requests are limited per Integrator, per minute - not per key. Every key you hold draws on one shared budget, so issuing more keys does not buy more throughput. Test, sandbox and production keys share that budget too, so a load test with a test key can slow production. Run heavy tests outside your busy hours.
The budget covers the requests that move money: send-money, pay-bill and pay-bill-ext,
collect, disburse and payouts, and withdraw. Reads - lookups, lists, reference data,
reconciliation - are not counted against it, though the advice below still applies to them.
Your limit is set on your Integrator record. If it is not set explicitly, a platform default applies. Ask us if your volume needs a higher ceiling; that is a configuration change, not an engineering one.
Reading your headroom
Responses from the endpoints above carry the standard RateLimit-* headers:
| Header | Meaning |
|---|---|
RateLimit-Limit |
Requests allowed in the current window |
RateLimit-Remaining |
Requests left in it |
RateLimit-Reset |
Seconds until the window resets |
Log RateLimit-Remaining on a sample of responses. A slow drift toward zero is the warning you
get before a batch job starts failing at month end.
When you exceed it
You get 429 with a plain-text body:
Too many requests for this integrator. Please try again later.
Warning
This response is plain text, not the usual JSON envelope. A client that calls
response.json()unconditionally will throw a parse error on a429and may surface it as an unrelated bug. Check the status code before parsing.
Handling it
Node.js
async function callWithBackoff(request, attempt = 0) {
const response = await request();
if (response.status !== 429 || attempt >= 5) return response;
// Exponential backoff with jitter: 1s, 2s, 4s, 8s, 16s (±25%).
const base = 1000 * 2 ** attempt;
const wait = base * (0.75 + Math.random() * 0.5);
await new Promise(resolve => setTimeout(resolve, wait));
return callWithBackoff(request, attempt + 1);
}
Python
import random, time
def call_with_backoff(request, attempts: int = 5):
for attempt in range(attempts):
status, body = request()
if status != 429:
return status, body
# Exponential backoff with jitter: 1s, 2s, 4s, 8s, 16s (±25%).
time.sleep((2**attempt) * (0.75 + random.random() * 0.5))
return request()
PHP
<?php
function callWithBackoff(callable $request, int $attempts = 5): array
{
for ($attempt = 0; $attempt < $attempts; $attempt++) {
$result = $request();
if ($result['status'] !== 429) {
return $result;
}
// Exponential backoff with jitter: 1s, 2s, 4s, 8s, 16s (±25%).
usleep((int) ((2 ** $attempt) * (0.75 + mt_rand() / mt_getrandmax() * 0.5) * 1_000_000));
}
return $request();
}
Java
HttpResponse<String> callWithBackoff(Callable<HttpResponse<String>> request) throws Exception {
for (int attempt = 0; attempt < 5; attempt++) {
HttpResponse<String> response = request.call();
if (response.statusCode() != 429) return response;
// Exponential backoff with jitter: 1s, 2s, 4s, 8s, 16s (±25%).
long base = 1000L << attempt;
Thread.sleep((long) (base * (0.75 + Math.random() * 0.5)));
}
return request.call();
}
C#
async Task<HttpResponseMessage> CallWithBackoffAsync(Func<Task<HttpResponseMessage>> request)
{
for (var attempt = 0; attempt < 5; attempt++)
{
var response = await request();
if (response.StatusCode != HttpStatusCode.TooManyRequests) return response;
// Exponential backoff with jitter: 1s, 2s, 4s, 8s, 16s (±25%).
var baseMs = 1000 * Math.Pow(2, attempt);
await Task.Delay(TimeSpan.FromMilliseconds(baseMs * (0.75 + Random.Shared.NextDouble() * 0.5)));
}
return await request();
}
Retrying a 429 is safe - nothing was processed. If the original request was a write, keep the
same Idempotency-Key on the retry so you are protected if an earlier attempt did in fact get
through.
Caution
Do not retry immediately in a loop. A tight retry loop against a
429keeps the window saturated, so every client you have - including the ones doing useful work - stays blocked. Jittered exponential backoff exists to stop your own traffic from being its own outage.
Staying under the limit
- Use webhooks instead of polling. One webhook replaces however many reconcile calls you would have made waiting for the same outcome. This is the single biggest saving available.
- Reconcile on a backoff, not a fixed short interval - 10s, 30s, 2m, 5m, then every 15 minutes.
- Page with a date window.
from/toon a list endpoint beats walking every page of history each run. - Spread scheduled work. If a nightly job fires everything at 00:00, it competes with itself. Stagger batches and cap in-flight concurrency.
- Cache reference data. Banks and billers change rarely; fetch them on a schedule, not per transaction.
Other limits
Some flows carry their own narrower limits beyond the per-minute budget - for example, the payer-facing hosted checkout page limits how often one payer can submit. Those are independent of your Integrator budget and will not be hit by normal server-to-server traffic.
Sandbox keys, and production keys until your Integrator is verified, also have a per-transaction
amount cap. It is a 403, not a 429. See
Environments and key types.
Next
Errors and status codes - the complete catalogue, including which codes mean money definitely did not move.