Rate limits and quotas
There are two limits, and they are not the same thing. Both answer 429 , so branch on the error code , never on the status.
| Request throttle | Send allowance | |
|---|---|---|
| Counts | HTTP requests | Recipients you queue |
| Window | One minute | One day |
| Error code | request_throttled |
quota_exceeded |
| Clears | Within a minute | At your organisation's midnight |
| Protects | Our web servers | Your sending reputation, and your Xero budget |
One API call can queue thousands of emails; ten thousand reads queue none. Neither number substitutes for the other.
The request throttle
| Caller | Requests per minute |
|---|---|
| With a valid API key | 300 |
| Without one | 20 |
Counted per key, so two keys get their own allowances. The smaller anonymous allowance exists to blunt anyone guessing at tokens; a real integration never sees it.
When you exceed it:
{
"error": {
"code": "request_throttled",
"message": "Too many requests. Slow down and retry.",
"request_id": "a1b2c3d4-e5f6-4789-0abc-def123456789"
}
}
The response carries Retry-After in seconds. Wait that long, then continue.
Practical advice: 300 a minute is five a second, which is generous for normal work and easy to exceed with a tight polling loop. If you are waiting on a statement export, sleep a second or two between polls. If you are paging a large contact list, request page_size=200 rather than making four times as many calls at the default 50.
The send allowance
This counts recipients queued, not requests. Sending to 300 contacts uses 300 of your allowance, whether you do it in one call or three.
The ceiling is 5,000 recipients per day.
Sends from a test key count too. Testing is not a free send path — a rehearsal of 300 contacts spends 300 of your allowance. See Test keys.
Two response headers tell you where you stand on any send:
| Header | Meaning |
|---|---|
X-Quota-Limit |
Your daily ceiling |
X-Quota-Remaining |
What is left today |
Read them rather than sending a probe request.
When it runs out:
{
"error": {
"code": "quota_exceeded",
"message": "Today's send allowance for this organisation has been used.",
"request_id": "a1b2c3d4-e5f6-4789-0abc-def123456789"
}
}
Retrying will not help. Wait until tomorrow.
"Tomorrow" means your midnight
The allowance resets at midnight in your organisation's timezone, not UTC. GET /api/v1/organisation returns that timezone, and GET /api/v1/sync_status is a good place to confirm what we think the time is.
Nothing is queued if the whole request will not fit
If a send would take you over the ceiling, the entire request is rejected — we do not send to the first few hundred contacts and stop. That way a rejected send is a clean thing to retry tomorrow rather than a partial run you have to reconcile.
There is also a hard cap of 1,000 contacts per request, regardless of allowance. A larger list is rejected with validation_failed before any allowance is touched, so split big runs into batches — and give each batch its own Idempotency-Key .
A third thing that looks like a limit, and is not
A Xero rate-limit pause is neither of the above, and it does not block you at all.
If your organisation exhausts its own Xero budget, Statey stops syncing until Xero lets it resume. No endpoint here calls Xero, so everything keeps answering normally — what stops is your data being refreshed. You will see it as a freshness fact on every read:
"meta": {
"data_freshness": {
"sync_paused": true,
"retry_after": 3600
}
}
Nothing to back off from. Decide for yourself whether data frozen that long is fresh enough for what you are about to do — and if you are about to send statements, it usually is not.
Backing off properly
429 request_throttled → wait Retry-After, retry 429 quota_exceeded → stop until tomorrow 500 → exponential backoff, tell us if it persists
Anything that sends should carry an Idempotency-Key , so a retry after a timeout cannot email someone twice. See Idempotency.