Documentation
Rate limits
Limits are per account, not per key, and they are generous enough that a well-behaved integration will not meet them. If you are hitting them, something is usually polling that should be listening.
Quotas by plan
| Plan | Sustained | Burst | Concurrent |
|---|---|---|---|
| Sandbox (any plan) | 20 req/s | 40 | 10 |
| Flow Starter · Desk Starter | 10 req/s | 25 | 10 |
| Flow Growth · Vision Growth · Desk Growth | 50 req/s | 150 | 40 |
| Deploy Pro | 30 req/s | 80 | 25 |
| Deploy Scale | 100 req/s | 300 | 80 |
| Enterprise | Negotiated | Negotiated | Negotiated |
Limits are enforced with a token bucket. The bucket holds the burst figure and refills at the sustained rate, so a short spike is absorbed and a sustained flood is not. A request that would take the bucket below zero gets a 429 and consumes nothing.
Endpoints with their own, stricter limits
| Endpoint | Limit | Why |
|---|---|---|
| POST /v1/orders/{id}/invoice | 5 req/s | Bounded by the government IRP, which is slower than we are |
| POST /v1/invoices/{id}/eway-bill/part-b | 5 req/s | Same upstream constraint |
| POST /v1/qc-reports | 10 per hour | Report generation is expensive; poll or use the webhook |
| POST /v1/webhook-endpoints/{id}/test | 20 per hour | It is a debugging tool, not a load generator |
| POST /v1/events/{id}/replay | 100 per hour | Protects your own endpoint from a replay storm |
Headers on every response
HTTP/2 200
easarc-version: 2026-08-01
ratelimit-limit: 50
ratelimit-remaining: 47
ratelimit-reset: 1
x-request-id: req_01J9ZS7K4B2Nratelimit-limit— your sustained rate, in requests per second.ratelimit-remaining— tokens left in the bucket right now.ratelimit-reset— seconds until the bucket is full again.x-request-id— quote this if you write to support. It is the fastest way for us to find what happened.
When you are limited
HTTP/2 429
retry-after: 2
ratelimit-limit: 50
ratelimit-remaining: 0
ratelimit-reset: 2
{
"error": {
"type": "rate_limit_error",
"code": "rate_limited",
"message": "Too many requests. Retry after 2 seconds.",
"request_id": "req_01J9ZS8M2P7Q"
}
}Honour Retry-After. It is calculated from the actual state of your bucket, so it is always a better number than anything you would guess.
const MAX_ATTEMPTS = 6
export async function requestWithBackoff(url, init = {}, attempt = 1) {
const response = await fetch(url, init)
if (response.status !== 429) return response
if (attempt >= MAX_ATTEMPTS) throw new Error('Rate limited after 6 attempts')
// Retry-After is authoritative. Only fall back to our own curve
// if the header is somehow missing.
const retryAfter = Number(response.headers.get('Retry-After'))
const backoffMs = Number.isFinite(retryAfter)
? retryAfter * 1000
: Math.min(2 ** attempt * 250, 30_000)
// Jitter, so a fleet of workers does not retry in lockstep.
const jitter = Math.random() * 250
await new Promise((resolve) => setTimeout(resolve, backoffMs + jitter))
return requestWithBackoff(url, init, attempt + 1)
}How not to hit them at all
- Listen instead of polling. Almost every account we see near its limit is polling
GET /v1/orderson a timer. A webhook tells you the same thing at the moment it happens, and costs one request instead of thousands. - Use
updated_sincewhen you must poll. Syncing only what changed turns a full walk into a handful of rows. - Raise
limitto 100. The default of 25 means four times as many requests for the same data. - Expand rather than following identifiers. One request with
?expand=buyer,invoicebeats three round trips. - Add jitter to scheduled jobs. A nightly sync at exactly 00:00 across all your workers is a self-inflicted burst.