Rate limits
Every request is limited to 1,200 a minute per IP and 600 a minute per API key. Call routes also allow 10 requests in flight per workspace.
Every request is limited to 1,200 a minute per IP and 600 a minute per API key. Call routes also allow 10 requests in flight per workspace.
The Sautikit API limits how many requests one IP address and one API key can send in a minute. The same limits apply to the SautiDesk API, which is served by the same platform.
| Limit | Value | Counted per | When it is exceeded |
|---|---|---|---|
| Requests per IP | 1,200 a minute | Client IP address, across every route | 429 with code rate_limit.exceeded |
| Requests per key | 600 a minute | API key (the bearer token), from any IP | 429 with code rate_limit.exceeded |
| Requests in flight | 10 | Workspace, on the call routes | 429 with code ratelimit.concurrent_cap_reached |
A request counts against both the IP limit and the key limit, and is refused when either is spent. Windows are one minute long and reset together, so a client that uses a whole limit at the end of one window can use it again at the start of the next.
Some routes have a lower limit of their own, such as sign-in routes (10 to 20 a minute per IP) and GET /v1/public/pricing (60 a minute per IP). Requests from the platform's own PBX and carrier connections are not counted.
The call routes accept 10 requests at once per workspace: POST /v1/calls, POST /v1/calls/{call_id}/hangup, GET /v1/calls/destination-check, GET /v1/calls/{call_id}/recording, GET /v1/calls/{call_id}/control/events, POST /v1/calls/{call_id}/control/respond, call supervision, POST /v1/webrtc/token and conference commands. A request that is waiting for an event holds its slot until it returns. Every key and member of the workspace shares the same 10.
Every response from a limited route carries the limit that is closest to being spent:
| Header | Meaning |
|---|---|
RateLimit-Limit | The limit, in requests |
RateLimit-Remaining | Requests left in the current window |
RateLimit-Reset | Seconds until the window resets |
A 429 also carries Retry-After, in seconds.
Nothing. A refused request places no call, sends no message and debits nothing. For the in-flight limit it is refused before the idempotency lookup and before the wallet check, so retrying with the same Idempotency-Key is safe.
Wait the number of seconds in Retry-After, then retry.
async function withRetry(send, attempts = 5) {
for (let i = 0; i < attempts; i++) {
const res = await send();
if (res.status !== 429) return res;
const wait = Number(res.headers.get("retry-after") ?? 1);
await new Promise((r) => setTimeout(r, wait * 1000));
}
throw new Error("still rate limited after retries");
}
const res = await withRetry(() =>
fetch("https://api.sautikit.com/v1/calls", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.SAUTIKIT_API_KEY}`,
"Content-Type": "application/json",
"Idempotency-Key": "reminder-2026-11-11-0042",
},
body: JSON.stringify({
from: process.env.FROM_NUMBER,
to: ["+254720000001"],
}),
}),
);If you place many calls at once, such as a reminder run, send them through a queue that keeps fewer than 10 requests in flight and stays under 600 a minute, instead of retrying after each refusal.