> For complete lifecycle-event payload schemas and examples, use the canonical overview: Session /api/sessions; Room lifecycle, members, and room playback /api/rooms; Session playback /api/playback; Recording /api/recording; DTMF /api/keypad-input; Messages /api/messages; WebSocket Session /api/web-socket. Use /api/event-delivery for transport behavior. Endpoint pages name relevant events, but these overviews are the canonical references.

# Rate limits

The API uses [token-bucket](https://en.wikipedia.org/wiki/Token_bucket) rate
limiting. Each applicable limit allows a short burst followed by a sustained
request rate. Requests beyond an applicable limit receive
`429 Too Many Requests`.

## How Token Buckets Work

Each limit is a bucket with two numbers:

* **Capacity** — the maximum burst. The bucket starts full; each admitted operation consumes one token from each applicable operation bucket.
* **Refill / sec** — the sustained rate at which tokens are replenished.

If at least one token is available, the API accepts the request and consumes a
token. If the bucket is empty, the API rejects the request with a `429`. The
`Retry-After` response header tells you how many seconds to wait. An idle bucket
refills back to full, so you regain your full burst allowance after a quiet
period.

**Example:** a bucket with capacity `8` and refill `4/s` lets you send 8 requests back-to-back, then accepts roughly 4 more per second sustained. If you drain it, waiting one second restores 4 tokens.

## Limit Levels

Every admitted operation is checked against each applicable operation bucket.
Authentication failures do not consume a token. An idempotency replay returns
the stored response without re-executing or consuming a token.

| Level                   | Applies to                                                                                                                           | Enforced?   |
| ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------ | ----------- |
| **Per-command-in-call** | Each command type on a call, normally with its own bucket; playback seek and Recording mask/unmask controls share the `seek` bucket. | Yes — `429` |
| **Per-room**            | Room commands, aggregated across all members of a room, per command type.                                                            | Yes — `429` |
| **Per-app**             | App-global commands not tied to a session — room/conference creation and listings — aggregated per application.                      | Yes — `429` |

**Buckets are normally per command type.** At the per-command and per-room levels, command types normally have their *own* bucket. The exception is the shared `seek` group: playback seek and Recording mask/unmask controls draw from one `20 / 20` bucket. A burst of one of those commands can drain the budget available to the others; other command types use independent buckets.

## The 429 Response

When a hard limit is exceeded, the API returns `429 Too Many Requests`. Read the
integer delay from the `Retry-After` response header. The JSON body keeps the
uniform `code` and `message` error shape:

```http
HTTP/1.1 429 Too Many Requests
Retry-After: 1

{
  "code": "rate_limited",
  "message": "Command rate limit exceeded. See Retry-After header."
}
```

Branch on `code` (the stable enum, always `"rate_limited"` for a `429`), not
the human-readable `message` string, which may change. Wait at least the number
of seconds in `Retry-After`, then retry. Do not look for retry timing in the
JSON body. Include the `traceparent` response header when contacting support
about a request.

## Per-Command Limits

The table below lists each command's per-call bucket as
`capacity / refill-per-second`. The three commands in the shared `seek` group
draw from the same bucket rather than receiving a bucket apiece.

| Command                              | Capacity / Refill (per sec) | Notes                                                                                                                 |
| ------------------------------------ | --------------------------- | --------------------------------------------------------------------------------------------------------------------- |
| `playback/seek`                      | 20 / 20                     | Shares the `seek` bucket with Recording mask and unmask controls.                                                     |
| `playback/stop`                      | 20 / 20                     |                                                                                                                       |
| `playback/pause`                     | 20 / 20                     |                                                                                                                       |
| `playback/resume`                    | 20 / 20                     |                                                                                                                       |
| `playback/restart`                   | 20 / 20                     |                                                                                                                       |
| `recordings/{recording_uuid}/stop`   | 20 / 20                     |                                                                                                                       |
| `recordings/{recording_uuid}/mask`   | 20 / 20                     | Shares the `seek` bucket with playback seek and Recording unmask.                                                     |
| `recordings/{recording_uuid}/unmask` | 20 / 20                     | Shares the `seek` bucket with playback seek and Recording mask.                                                       |
| `playback/play`                      | 8 / 4                       | Plays files, a live stream, or silence.                                                                               |
| `dtmf/collect` (POST + DELETE)       | 4 / 0.8                     | The prompt-and-collect POST and cancel DELETE each have an independent bucket with the same capacity and refill rate. |
| `POST .../recordings`                | 4 / 1                       | Creates a Session Recording.                                                                                          |
| `room/mute` (`conference.mute`)      | 15 / 10                     |                                                                                                                       |
| `room/unmute` (`conference.unmute`)  | 15 / 10                     |                                                                                                                       |

Room-member volume handlers do not use per-command-in-call buckets. Room-member add and delete endpoints use only the per-room limits below.

### Per-Room Limits

Room commands are *additionally* checked against a per-room bucket that aggregates across all members of the room.

| Room command                                 | Capacity / Refill (per sec) | Scope                                      |
| -------------------------------------------- | --------------------------- | ------------------------------------------ |
| `POST /rooms/{room_id}/members`              | 100 / 30                    | per-room                                   |
| `DELETE /rooms/{room_id}/members/{uuid}`     | 100 / 30                    | per-room                                   |
| `POST /rooms/{room_id}/playback/pause`       | 15 / 10                     | per-room                                   |
| `POST /rooms/{room_id}/playback/seek`        | 15 / 10                     | per-room                                   |
| `POST /rooms/{room_id}/playback/volume`      | 15 / 10                     | per-room                                   |
| `POST /rooms/{room_id}/playback/stop`        | 15 / 10                     | per-room                                   |
| `POST /rooms/{room_id}/playback/play`        | 8 / 3                       | per-room                                   |
| `POST /rooms/{room_id}/playback/record`      | 8 / 3                       | per-room; start and stop share this bucket |
| `POST /rooms/{room_id}/playback/record/stop` | 8 / 3                       | per-room; start and stop share this bucket |

### Per-App Limits (App-Global Commands)

Creating a room and listing rooms or sessions are limited **per application**, aggregated across all requests. Each limit is shown as `capacity / refill-per-second`.

| Command                | Hard limit | Notes                              |
| ---------------------- | ---------- | ---------------------------------- |
| `POST /rooms` (create) | 10 / 2     | per-app — room/conference creation |
| `GET /sessions` (list) | 40 / 20    | per-app                            |
| `GET /rooms` (list)    | 40 / 20    | per-app                            |

## Resource Limits (Not a Rate Limit)

**This is a count cap, not a rate cap.** Everything above bounds how *fast* you may call; this bounds how *many* rooms exist at once. The two are independent: staying under the creation rate limit does not exempt you from the room-count cap, and vice versa.

Each application has a cap on the number of rooms it may have **concurrently live** at one time. The cap counts every room your app currently owns, including rooms that are still being torn down — a room frees its slot only once it is deleted *and* its teardown has finalized.

The default room cap is `100` concurrently live rooms. Contact support for
higher limit.

When creating a room would exceed the cap, `POST /api/v1/rooms` is rejected with
`403 Forbidden` — **not** a `429`. Because this is not a rate limit, the
response does not include a `Retry-After` header. The condition clears when you
delete a room and free a slot, not after a fixed delay.

```http
HTTP/1.1 403 Forbidden

{
  "code": "forbidden",
  "message": "Maximum number of rooms reached for this application"
}
```

### Room member cap

A second count cap bounds how *many members* may be in a single room at once. It is independent of the room-count cap above and of every rate limit. The default is `250` members per room. Contact support for higher limit.

When a join (`POST /api/v1/rooms/{room_id}/members`) would exceed the cap, it is
rejected with `409 Conflict` and `code: room_full` — **not** a `429` and **not**
the room-count `403`. The member is not seated. As with the room-count cap, this
is not a rate limit, so the response does not include a `Retry-After` header.
The condition clears when a member leaves and frees a slot, not after a fixed
delay.

```http
HTTP/1.1 409 Conflict

{
  "code": "room_full",
  "message": "Room is full"
}
```

The cap applies to members added through the join endpoint.

## Best Practices

* **Honor `Retry-After`.** On a `429`, wait at least the number of seconds in the `Retry-After` response header before retrying. The JSON body does not contain retry timing. After the burst capacity is exhausted, requests above the refill rate receive `429` responses.
* **Wait for webhooks.** For asynchronous operations, use the lifecycle webhook to detect completion instead of polling.
* **Batch playback files.** Send multiple URLs in one `playback/play` command with `type: files`.
* **Back off on repeated 429s.** If a retry receives another `429`, honor its new `Retry-After` response header and increase your backoff before retrying again.