---
title: Rate limits
description: >-
  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.
summary: >-
  Each IP address may send 1,200 requests a minute and each API key 600. Past a
  limit the API answers 429 with Retry-After. Call routes also allow 10 requests
  in flight per workspace.
date: 2026-11-11T00:00:00.000Z
updated: 2026-11-11T00:00:00.000Z
type: concept
---


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.

## Limits

| 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`](/developers/errors/rate_limit.exceeded) |
| Requests per key | 600 a minute | API key (the bearer token), from any IP | `429` with code [`rate_limit.exceeded`](/developers/errors/rate_limit.exceeded) |
| Requests in flight | 10 | Workspace, on the call routes | `429` with code [`ratelimit.concurrent_cap_reached`](/developers/errors/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.

### Requests in flight

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.

## Response headers

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.

## What a refused request costs

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.

## Retrying

Wait the number of seconds in `Retry-After`, then retry.

```js
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.

## Next steps

- See [rate_limit.exceeded](/developers/errors/rate_limit.exceeded) and [ratelimit.concurrent_cap_reached](/developers/errors/ratelimit.concurrent_cap_reached) for the error bodies.
- See [Calls API](/developers/api#calls) for the call routes.
