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

# Error handling

Expected API errors use a consistent JSON format and one of the documented HTTP
status codes. This includes requests to an unknown path and requests that use
an unsupported method on a known path. Unexpected `500` responses add a
`support_id`, as described under [500 Internal Server Error](#500-internal-server-error).

## Error response format

An expected error response follows this structure:

```json
{
  "code": "invalid_request",
  "message": "Human-readable error message"
}
```

* `code` — a stable, machine-readable class from a small closed set (see [Error Codes](#error-codes)). Branch on this. New codes are only added with a documented API change.
* `message` — a human-readable, English-only message whose phrasing may change. Use it for display and diagnostics.

Use the HTTP status code to determine whether a request succeeded. Any 2xx status
indicates success. Successful response bodies vary by endpoint and do not use a
common `success` field.

### Request validation errors

A request-validation response contains exactly one error. The `message` embeds
the failing path:

```json
{
  "code": "invalid_request",
  "message": "The \"to\" phone number must match E.164 format"
}
```

Fix that error and retry the request. If another field is invalid, the next
response identifies it. Validation responses do not include submitted values
or validator-specific fields such as `detail`, `errors`, `location`, or `value`.

## Error codes

For expected errors, the `code` field is the stable contract for programmatic
error handling. The closed set contains thirteen classes, mapped to these HTTP
status codes:

| Code                           | HTTP Status        | Meaning                                                                                                                                                                                                                                            |
| ------------------------------ | ------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `invalid_request`              | 400, 405, 415, 422 | The request was malformed or semantically invalid, including bad JSON, a missing or out-of-range parameter, an unsupported method on a known path, or a wrong or malformed `Content-Type`.                                                         |
| `invalid_authorization_header` | 400                | The `Authorization` header is missing or the Bearer credential is malformed.                                                                                                                                                                       |
| `payload_too_large`            | 413                | The request body exceeded the endpoint's size limit.                                                                                                                                                                                               |
| `result_set_too_large`         | 413                | A list endpoint's full matched set exceeds the system ceiling. Distinct from `payload_too_large` (oversized request body): here the *response* would be too large. Narrow a sessions query with filters; `limit` is applied only after this check. |
| `unauthorized`                 | 401                | The Bearer credential has a valid application identifier, but its API key is missing or invalid.                                                                                                                                                   |
| `forbidden`                    | 403                | Authenticated, but not permitted to act on this resource (e.g. the caller ID is not on the allow-list, or a per-app cap was reached). Acting on another app's resource is `404`, not `403`.                                                        |
| `not_found`                    | 404                | The requested path or addressed resource does not exist.                                                                                                                                                                                           |
| `conflict`                     | 409                | The request conflicts with the current state of the resource.                                                                                                                                                                                      |
| `recording_not_active`         | 409                | A room-recording stop addressed a room with no active Recording: a failed precondition, not a concurrency conflict. No recording command or lifecycle event is produced.                                                                           |
| `room_full`                    | 409                | A join was rejected because the room is at its member cap. A count cap, not a rate limit — no `Retry-After` header is sent; free a slot by removing a member. Distinct from `forbidden` (the per-app room-count cap on room create).               |
| `rate_limited`                 | 429                | A rate-limit bucket denied the request. The `Retry-After` response header gives the seconds to wait.                                                                                                                                               |
| `playback_source_error`        | 502                | A playback or DTMF prompt source could not be admitted. The `detail` object identifies the failure category and whether retrying may succeed.                                                                                                      |
| `unavailable`                  | 503, 529           | A required service is temporarily unavailable or the platform is overloaded. Use the HTTP status and operation documentation to determine how to retry.                                                                                            |

**The set is closed.** These thirteen expected-error codes are the complete list.
Branch on `code` for programmatic handling and fall back to the HTTP status
class for anything you do not special-case; do not pattern-match the `message`
string. Unexpected `500` responses use `internal_error` in a separate format.

## W3C trace context

The API supports the W3C Trace Context `traceparent` and `tracestate` request headers. It validates their W3C shape and ignores malformed values.

A valid incoming context is linked to a new server-owned trace with its own trace identifier.

Every response carries the server-owned `traceparent`. Log this response header
with your request context, and include it when you report a problem. The same
trace propagates through command processing and webhook delivery.

**Transport.** The API is machine-to-machine (M2M), server-to-server HTTP/JSON.
Send `Content-Type: application/json` on requests with a JSON body. Request-body
limits are documented under [413 Payload Too Large](#413-payload-too-large).

## HTTP status codes

### 400 Bad Request

The request is malformed or contains invalid parameters. This includes a missing
or malformed `Authorization` credential; a syntactically valid credential with
an invalid API key returns [401 Unauthorized](#401-unauthorized) instead.

| Error Message                                                                                                 | Cause                                                                      | Fix                                                                                               |
| ------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- |
| `Missing Authorization header. Expected format: Bearer <app_uuid>:<api_key>` (`invalid_authorization_header`) | The `Authorization: Bearer` header was not provided.                       | Include `Authorization: Bearer <app_uuid>:<api_key>` in every API request.                        |
| `Invalid Authorization header. Expected format: Bearer <app_uuid>:<api_key>` (`invalid_authorization_header`) | The Bearer credential is malformed or its `app_uuid` has an invalid shape. | Use the application identifier and API key provided by support.                                   |
| `The request body is not valid JSON` (`invalid_request`)                                                      | The JSON body could not be parsed.                                         | Ensure your request body is valid JSON with `Content-Type: application/json`.                     |
| `Operation-Id must be a lowercase RFC-4122 v4 UUID`                                                           | The `Operation-Id` request header is not a lowercase RFC-4122 v4 UUID.     | Send a lowercase v4 UUID in `Operation-Id`, or omit the header and let the platform generate one. |

### 401 Unauthorized

The Bearer credential has a valid application identifier, but its API key is
missing or invalid. Missing or malformed Bearer credentials return
`400 invalid_authorization_header` instead.

| Error Message     | Cause                                                             | Fix                                                                        |
| ----------------- | ----------------------------------------------------------------- | -------------------------------------------------------------------------- |
| `Missing API key` | The Bearer token does not include the API key after the colon.    | Include `Authorization: Bearer <app_uuid>:<api_key>` in every API request. |
| `Invalid API key` | The provided API key does not match the current key for this app. | Use the current key supplied by support.                                   |

### 403 Forbidden

You are authenticated, but the action is not permitted. A `403` indicates a policy or resource limit on a resource your application owns. A resource owned by another application returns `404` (see [404 Not Found](#404-not-found)).

| Error Message                                          | Cause                                                                                                                                                                   | Fix                                                                               |
| ------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------- |
| `Caller ID is not permitted for this application`      | The `from` on `POST /v1/sessions:dial` is not on this app's caller-ID allow-list.                                                                                       | Use a caller ID provisioned for the app, or ask support to update the allow-list. |
| `Maximum number of rooms reached for this application` | The per-app room cap was hit on `POST /api/v1/rooms`. This is a resource limit, not a rate limit. See [Resource Limits](/rate-limits#resource-limits-not-a-rate-limit). | Delete unused rooms to free capacity, or contact support.                         |
| `Forbidden`                                            | The recording URL is invalid or expired.                                                                                                                                | Use the URL from the latest recording lifecycle event before its `expires_at`.    |

### 404 Not Found

The requested path or resource does not exist. Sessions and rooms owned by another application also return `404`.

| Error Message                | Cause                                                                                       | Fix                                                           |
| ---------------------------- | ------------------------------------------------------------------------------------------- | ------------------------------------------------------------- |
| `No route matches this path` | The request path does not match a public API route.                                         | Use a path documented in the API reference.                   |
| `Session not found`          | No active session exists with the given UUID, including when the session has already ended. | Verify the session UUID and that the session is still active. |
| `Room not found`             | No room exists with the given room\_id, including when the room has been destroyed.         | Verify the room\_id and that the room still exists.           |

### 405 Method Not Allowed

The request path exists, but it does not accept the HTTP method you used.

| Error Message                       | Cause                                                              | Fix                                                        |
| ----------------------------------- | ------------------------------------------------------------------ | ---------------------------------------------------------- |
| `Method not accepted for this path` | The request used an unsupported method on a known public API path. | Use a method documented for the path in the API reference. |

### 409 Conflict

The request conflicts with the current state of the resource.

| Error Message                                                                                                                                            | Cause                                                                                                                                                                                                                           | Fix                                                                                                                                                                                                 |
| -------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `Session is still joining a conference room`                                                                                                             | The session is joining another room and does not yet have a member ID. (A session already fully joined to a different room is *not* an error: `POST /rooms/{room_id}/members` auto-leaves the prior room and joins the target.) | Wait for the [`room.member.joined`](/api/rooms#roommemberjoined) event for the in-progress join, then retry.                                                                                        |
| `Session is already muted / unmuted`                                                                                                                     | The mute state is already what you requested.                                                                                                                                                                                   | Check the current mute state before toggling. This is a state-conflict guard.                                                                                                                       |
| `Request already in progress`                                                                                                                            | A request with the same `Idempotency-Key` is still being processed.                                                                                                                                                             | Do not send another concurrent request. After the original request completes, retry the same request with the same key to retrieve its result. See the [Idempotency guide](/idempotency).           |
| `Another room media transition is in progress` / `Room recording stop is already in progress` / `Room recording state changed; retry` (`code: conflict`) | A room-recording stop overlapped another room-media transition or a concurrent recording lifecycle update.                                                                                                                      | Wait for the in-progress transition to finish, then retry against the room's current state.                                                                                                         |
| `Room is full` (`code: room_full`)                                                                                                                       | The room is at its member cap (system default 250 unless the application has its own cap). The member was not seated. This is a count cap, not a rate limit.                                                                    | Remove a member to free a slot, or ask support to raise the member cap. The response does not include a `Retry-After` header. See [Resource Limits](/rate-limits#resource-limits-not-a-rate-limit). |

### 422 Unprocessable Entity

The request was well-formed but could not be processed. A `422` uses
`code: invalid_request` for either of these conditions:

| Error Message                                     | Cause                                                                                                                                 | Fix                                                                                          |
| ------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------- |
| `The "to" phone number must match E.164 format`   | A Dial request failed schema validation. The response contains exactly one validation error and embeds the failing path in `message`. | Fix the named field and retry. If another field is invalid, the next response identifies it. |
| `Idempotency-Key reused with a different request` | The same key was sent with a different method, route, query, content type, or raw request body.                                       | Use a fresh key per distinct operation. See the [Idempotency guide](/idempotency).           |

Both conditions have the same `code`, so do not use `code` alone to select the
recovery action. Use the request context: correct a Dial validation failure, or
use a new idempotency key when you changed a request. Do not pattern-match the
human-readable `message`, because its phrasing may change.

### 413 Payload Too Large

Two distinct conditions return `413`, each with its own `code`. `payload_too_large` means the *request* body is too big; `result_set_too_large` means the *response* a list endpoint would return is too big. Branch on the `code` field to tell them apart.

For `payload_too_large`: the request body exceeds the per-route size limit. The default limit is 1 MiB; the per-session command route (`POST /v1/sessions/{uuid}/{command}`) has a 64 KiB limit.

For `result_set_too_large`: list endpoints (sessions, rooms, members) return all matching items by default. A full matched set above 50,000 items returns this error. A smaller `limit` does not avoid the check because it applies to the full matched set first. Narrow a sessions query with its filters. Room and member list endpoints have no narrowing filter.

| Error Message                                  | Cause                                                                                                                                                                                                                 | Fix                                                                                                                                                                                                                           |
| ---------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `Request body too large` (`payload_too_large`) | The request body exceeds the route's maximum size (1 MiB default; 64 KiB on session command routes).                                                                                                                  | Reduce the payload size. Command payloads should contain only small fields (DTMF strings, file URLs, flags). Do not embed audio or other large data inline.                                                                   |
| `result_set_too_large`                         | A list endpoint's full matched set exceeds the system ceiling of 50,000 items. Sessions return `Too many results to return; narrow your query using filters.`; rooms and members return `Too many results to return.` | For sessions, narrow the query with filters such as state or creation time. A smaller `limit` does not help. Rooms and members expose no narrowing filter or request-side workaround. This is unrelated to request-body size. |

### 415 Unsupported Media Type

The request explicitly supplies a malformed `Content-Type` header or a media
type other than `application/json`.

| Error Message                                    | Cause                                                                                                                                                                                       | Fix                                                            |
| ------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------- |
| `Unsupported Content-Type; use application/json` | The request explicitly supplies a malformed `Content-Type` header or a media type other than `application/json`. Charset parameters such as `application/json; charset=utf-8` are accepted. | Use `Content-Type: application/json` with a JSON request body. |

### 429 Too Many Requests

You have exceeded the rate limit. The error body contains only `code` and
`message`. Read the integer delay from the `Retry-After` response header, wait
at least that many seconds, and then retry. See [Rate Limits](/rate-limits) for
details.

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

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

| Error Message                                              | Cause                                                          | Fix                                                                                                        |
| ---------------------------------------------------------- | -------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------- |
| `Command rate limit exceeded. See Retry-After header.`     | A per-room/per-session per-command token bucket was exhausted. | Slow down and retry after the delay in the `Retry-After` response header. See [Rate Limits](/rate-limits). |
| `Application rate limit exceeded. See Retry-After header.` | The per-app command token bucket was exhausted.                | Slow down and retry after the delay in the `Retry-After` response header. See [Rate Limits](/rate-limits). |

### 500 Internal Server Error

An unexpected platform failure returns a top-level `code`, `message`, and opaque
`support_id`:

```json
{
  "code": "internal_error",
  "message": "An internal error occurred",
  "support_id": "err_0123456789abcdef0123456789abcdef"
}
```

Do not parse the support ID. Quote it when you contact support. The response
does not expose the technical cause or a vendor event identifier.

### 502 Bad Gateway

The platform could not admit a playback or DTMF prompt source. The response uses
`code: playback_source_error` and includes a `detail` object:

* `reason` classifies the failure as `unsupported`, `not_found`,
  `access_denied`, `timeout`, `unreachable`, `rejected`, `invalid_response`, or
  `preparation_failed`.
* `source_index` identifies the failing zero-based item when the request
  contains multiple URLs.
* `retryable` indicates whether retrying the same source later may succeed.
* `upstream_status` contains the source origin's HTTP status when one was
  received.

Use `detail.reason` and `detail.retryable` to decide whether to correct the
source or retry. The response never contains source URLs, credentials, raw
upstream response bodies, filesystem paths, or media-engine details.

### 503 Service Unavailable

`503` covers the temporary capacity and dependency failures below. Follow the
recovery action documented for the condition you receive.

| Error Message                     | Cause                                                                                                                        | Fix                                                                                  |
| --------------------------------- | ---------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------ |
| `Recording unavailable`           | The recording service could not admit the command.                                                                           | Retry with backoff. If repeated attempts fail, contact support.                      |
| `No source node available`        | No capacity was available to host the room, so no room was created.                                                          | This is a transient capacity condition. Retry with backoff.                          |
| `Source node unavailable`         | The infrastructure hosting the room could not be reached, so the room playback or room recording command was not dispatched. | Retry with backoff. If repeated attempts fail, contact support.                      |
| `Service temporarily unavailable` | The idempotency store is unavailable, so a request carrying an `Idempotency-Key` was **not** processed (fail-closed).        | Retry the same request with the same key. See the [Idempotency guide](/idempotency). |

### 529 Site Overloaded

The platform has no outbound capacity. The error body contains only `code` and
`message`. Read the integer delay from the `Retry-After` response header, wait
at least that many seconds, and then retry. `529` is a non-standard status code
for overload.

```http
HTTP/1.1 529 Site Overloaded
Retry-After: 5

{
  "code": "unavailable",
  "message": "overloaded. See Retry-After header."
}
```

| Error Message                         | Cause                                                  | Fix                                                                                                                       |
| ------------------------------------- | ------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------- |
| `overloaded. See Retry-After header.` | No outbound capacity is available to place a new call. | Wait for the duration in the `Retry-After` response header, then retry. Use exponential backoff after repeated responses. |

## Handle errors

* **Always check the HTTP status code first.** A 2xx status means the request was accepted. Any other status indicates an error.
* **Branch on the `code` field, not the message.** The `code` is a stable closed enum; the `message` string is human-readable and may change.
* **Capture the response `traceparent`.** Log the server-owned response value alongside your request context and quote it when reporting a problem to support.
* **Propagate W3C trace context.** Send a valid `traceparent` with the request and preserve `tracestate` when present.
* **Use the `message` field for display and diagnostics only.** Error messages are designed to be human-readable and actionable, but are not part of the programmatic contract.
* **Honor `Retry-After`.** For `429` and `529` responses, wait at least the number of seconds in the response header before retrying. Do not look for retry timing in the JSON body.
* **Back off after repeated retryable errors.** If another `429` or `529` follows a retry, honor its `Retry-After` header and increase your backoff. Use backoff for other documented temporary dependency failures and `Recording unavailable`. Contact support if repeated attempts fail.