Rate limits & idempotency
Three protections, easy to conflate. The request rate bounds how often you may call the API at all. Traffic caps stop you placing calls faster than your account allows. And idempotency keys stop a retried request placing a second call by accident.
Request rate
Section titled “Request rate”Every account may make 3,000 API requests a minute, counted across every key it holds.
The count is per account, not per key. Creating a second key does not buy a second budget: both keys draw on the same 3,000.
Over the limit you get 429 with a Retry-After header saying how many seconds to wait:
// HTTP 429, Retry-After: 37{ "detail": "Too many API requests. The limit is 3000 a minute for this account. Retry in 37s, or slow the client down."}Unrecognised keys are counted against the address
Section titled “Unrecognised keys are counted against the address”A request carrying a key we do not recognise is counted against the calling address, not
against any account. Twenty unrecognised keys in a minute from one address, and that address gets
429 until the minute is out.
A caller whose key is valid never meets this limit. If you do see it, something on your side is sending a stale or truncated key. The usual cause is a revoked key still sitting in an old deployment.
Traffic caps
Section titled “Traffic caps”Every account has two independent limits on outbound call placement:
| Cap | Default | Refusal |
|---|---|---|
| Concurrency. Simultaneous live calls | 50 |
429, reason LIMIT.0429 |
| CPS. New calls started per second | 10 |
429, reason LIMIT.0430 |
Both are per-account and checked before a call is placed. A refused call is never dialled and never billed. If your traffic legitimately needs a higher ceiling, contact support: these are account settings, not hardcoded platform limits.
A cap refusal on POST /calls/originate returns the standard error body:
// HTTP 429{ "detail": "Concurrency cap reached (50 simultaneous calls)." }Distinguish “slow down, or raise my cap” (LIMIT.*) from “top up your wallet” (BILL.*, 402). They are never the same code, because the fix is different. See
Events & codes for the full table.
Handling a 429
Section titled “Handling a 429”- Read
Retry-Afterfirst. A request-rate refusal always carries it, so you never have to guess how long to wait. - Back off before retrying. An immediate retry against a CPS cap will just hit it again.
- For a concurrency cap, wait for a call to end (watch the
event stream for
call.completed/call.failed) rather than polling. - If you are consistently hitting a cap under normal traffic, that is a sign to ask us to raise it, not to build a bigger retry loop around it.
Idempotency
Section titled “Idempotency”Any endpoint that spends money or places a call accepts an Idempotency-Key header. Send the same
key on a retry. After a timeout, a dropped connection, anything. And you get back the
original response instead of a second call or a second charge.
curl https://api.sautipbx.com/api/calls/originate \ -H "Authorization: Bearer sk_live_..." \ -H "Idempotency-Key: order-4021" \ -d '{ "to": "+254711111111", "from": "+254709080010", "url": "https://yourapp.com/handle-call" }'- Scope. Keys are per-account. Two accounts can safely reuse the same key.
- Window. A key is remembered for 24 hours from its first use, then it is free to reuse for a new operation.
- In flight. If a request with that key is still being processed, a concurrent duplicate gets
409, not a second call. Retry shortly after. - Completed. Once the original request finished, a retry with the same key replays that exact response verbatim (same status, same body). It does not re-run the operation.
- Failed attempts do not burn the key. If the operation was refused (
402,429, a validation error), the key is freed immediately. A legitimate retry after fixing the problem is not blocked. - Pick a key that identifies the business action you are taking (an order id, a job id). Not a random value per HTTP attempt, or you lose the protection.