Rate limits
What is actually capped, what is not capped at all, and the two behaviours that catch integrators.
Rate limiting is on. It is also narrower than you probably expect, and two of its behaviours will surprise you if you meet them in production rather than here.
The short version
The ordinary CRUD surface is not rate limited. /v1/tickets,
/v1/customers, /v1/companies, /v1/teams, /v1/macros and their siblings
have no rate limit of any kind, with an API key or with a session.
What is limited falls into three groups: unauthenticated ingress (the routes a stranger can reach), credential verification failures (an anti-brute-force budget), and a few authenticated routes that spend something irreversible — an email, a realm account, a model call.
Unlimited is not the same as unbounded
An endpoint with no rate limit is still bounded by the 4 MiB body cap, the 500-row page cap, and your database. "Not rate limited" means there is no request counter, not that you should page a million tickets in a tight loop.
What a refusal looks like
Every limit refuses the same way: 429, with headers and a body that both say
which budget you hit.
HTTP/2 429
retry-after: 56
ratelimit-limit: 10
ratelimit-remaining: 0
ratelimit-reset: 56
x-request-id: a4b32aa5-14e2-4952-b859-a059127aa677{
"scope": "csat",
"limit": 10,
"window_seconds": 60,
"error": "rate_limited",
"message": "too many csat requests; retry in 56s"
}Treat a 429 as "come back later", not "rejected". Nothing was written and
nothing was lost. The same request replayed after Retry-After is accepted. A
client that gives up on 429 turns a deferral into a lost ticket.
You cannot see your budget before you spend it
RateLimit-* headers are emitted only on the 429 itself. A successful
response carries none of them, so there is no way to watch your remaining
allowance and slow down in advance. Back off reactively, on the 429.
What is limited
Every window is a fixed 60-second window, not a sliding one. A fixed window admits up to twice the limit across a boundary; that is a known and accepted trade, not a bug.
Unauthenticated ingress
| Scope | Per 60s | Counted per | Routes |
|---|---|---|---|
ingest | 600 | workspace | POST /v1/ingest/email, POST /v1/ingest/{channel} |
webhook | 600 | integration | POST /v1/webhooks/{provider}/{integration_id} |
live_chat_session | 180 | live-chat session | the customer live-chat routes |
handoff | 120 | workspace | POST /v1/handoff |
widget | 30 | workspace + visitor address | POST /v1/contact-forms/{slug} |
csat | 10 | ticket | the CSAT survey routes |
preview_token | 60 | client address | GET /v1/help-centre/preview/{token} |
Credential verification failures
These count failures only. A successful verification is refunded, so normal traffic never accumulates against them.
| Scope | Failures per 60s | Counted per |
|---|---|---|
token_verify | 60 | client address |
api_key_verify | 60 | client address |
login_address | 50 | client address |
invite_accept, invite_claim | 20 | client address |
login_identity | 10 | email address (hashed) |
Authenticated routes that spend something
| Scope | Per 60s | Counted per | Why it is capped |
|---|---|---|---|
invite_issue | 20 | workspace | Each one sends mail; sending reputation is finite. |
auralis_chat | 10 | workspace | Each one is a model call. |
domain_claim | 10 | workspace | Each one touches DNS verification. |
A concurrency cap, not a rate limit
stream allows 8 concurrent live-chat event streams per session. The ninth
gets 429 with Retry-After: 5. It is a cap on simultaneous connections, not on
requests over time.
The two behaviours that catch integrators
These are the ones worth reading twice.
Every caller behind the same proxy shares one address budget
The address budgets are keyed on the peer address the runtime actually sees.
The runtime deliberately does not trust X-Forwarded-For here, because on this
path that header is supplied by the caller and trusting it would let anyone
reset their own budget.
The consequence is real: if your integration reaches the runtime through a shared proxy or NAT, every caller behind it shares one 60-failure verification budget. One misconfigured client looping on a bad credential can exhaust it for everyone on that address.
Exhausting the failure budget locks out valid credentials too
The budget is checked before the credential is verified. Once it is spent, a
correct key gets 429 as well:
# after 60 failed attempts from this address, with a valid key:
curl -sk -D- -o /dev/null \
-H "Authorization: Bearer $AURALIS_API_KEY" \
https://api.localhost/v1/tickets?limit=1
# HTTP/2 429 ratelimit-limit: 60 retry-after: 6It self-heals at the window boundary, and only failures accumulate. But it means
a retry loop on a bad credential is actively harmful — it does not just fail,
it takes your working clients down with it. Never retry a 401 without changing
the credential.
When the limiter cannot reach its counter
If the counter store is unreachable, the limiter fails open — the request is allowed and a warning is logged. Availability is preferred to enforcement.
Two scopes fail closed instead, answering 429: invite_issue and
invite_claim. Both spend something that cannot be taken back — sending
reputation, and an account on a shared identity realm — so the safe default
inverts.
Limits that are not rate limits
These bite far more often than the rate limiter does.
| Limit | Value | On breach |
|---|---|---|
| Request body | 4 MiB | 413 body_too_large |
| JSON nesting depth | 64 levels | 422 body_too_deep |
Page size (limit) | 500 max on /v1/tickets | 422 validation_error |
| Offset | 2,147,483,647 max | 422 validation_error |
| NUL byte in body or URL | not allowed | 400 invalid_text |
The body and depth guards apply to JSON content types only. A multipart upload bypasses them deliberately, because multipart legitimately carries NUL bytes and has no nesting to measure.
Two list endpoints have no default page size
GET /v1/customers and GET /v1/companies take limit as optional with no
default. Omit it and you get the entire workspace in one response — on a
workspace with 3,200 customers, that is 3,200 rows. Always send an explicit
limit on those two.
Handling limits well
- Read
Retry-Afterand honour it. Do not use a fixed sleep. - Back off on
429, and do not back off on401— fix the credential. - Prefer one request that pages than many that poll.
- Log
x-request-idon every failure.