Skip to content

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

Rate limits by plan
PlanSustainedBurstConcurrent
Sandbox (any plan)20 req/s4010
Flow Starter · Desk Starter10 req/s2510
Flow Growth · Vision Growth · Desk Growth50 req/s15040
Deploy Pro30 req/s8025
Deploy Scale100 req/s30080
EnterpriseNegotiatedNegotiatedNegotiated

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-specific rate limits
EndpointLimitWhy
POST /v1/orders/{id}/invoice5 req/sBounded by the government IRP, which is slower than we are
POST /v1/invoices/{id}/eway-bill/part-b5 req/sSame upstream constraint
POST /v1/qc-reports10 per hourReport generation is expensive; poll or use the webhook
POST /v1/webhook-endpoints/{id}/test20 per hourIt is a debugging tool, not a load generator
POST /v1/events/{id}/replay100 per hourProtects your own endpoint from a replay storm

Headers on every response

Response headers
HTTP/2 200
easarc-version: 2026-08-01
ratelimit-limit: 50
ratelimit-remaining: 47
ratelimit-reset: 1
x-request-id: req_01J9ZS7K4B2N
  • ratelimit-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

429 Too Many Requests
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.

backoff.js
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/orders on a timer. A webhook tells you the same thing at the moment it happens, and costs one request instead of thousands.
  • Use updated_since when you must poll. Syncing only what changed turns a full walk into a handful of rows.
  • Raise limit to 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,invoice beats three round trips.
  • Add jitter to scheduled jobs. A nightly sync at exactly 00:00 across all your workers is a self-inflicted burst.