Skip to content

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.

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.

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.

  • Read Retry-After first. 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.

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.

Terminal window
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.