> ## Documentation Index
> Fetch the complete documentation index at: https://docs.sendora.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Errors

> HTTP status codes and error response shapes the Sendora API returns.

## Error response format

Every error response is JSON, carries the HTTP status code, and includes a
`request_id` you can quote to support. The same value is returned in the
`X-Request-ID` response header. The human-readable message is a complete
sentence written for a person, and it sits in one of two fields depending on
where the error was raised.

Most errors use a `detail` field:

```json theme={"dark"}
{
  "detail": "We could not find this lead. It may have been deleted.",
  "request_id": "6f1c2a9e4b7d"
}
```

Errors raised by the application's own service layer use an `error` field with
the same shape:

```json theme={"dark"}
{
  "error": "We could not find this lead. It may have been deleted.",
  "request_id": "6f1c2a9e4b7d"
}
```

Read the message from `detail` when it is present, otherwise from `error`.
Some endpoints return an object in `detail` with a `message` field and extra
context.

<Warning>
  Match on the HTTP status code, never on the message text. Message wording is
  written for people and can change without notice. Treat it as display text.
</Warning>

Validation errors (422) keep a structured array in `detail`, one entry per
invalid field, and do not wrap it in a sentence:

```json theme={"dark"}
{
  "detail": [
    {
      "loc": ["body", "email"],
      "msg": "value is not a valid email address",
      "type": "value_error.email"
    }
  ]
}
```

## Status codes

The generated API reference declares applicable error codes on each operation.
An endpoint does not necessarily return every status in this table; use the
endpoint page as the source of truth for its error surface.

| Status | Name | When it happens |
| - | - | - |
| `200` | OK | Successful GET / PATCH |
| `201` | Created | Successful POST that created a resource |
| `202` | Accepted | Async operation queued (e.g., bulk enrich) |
| `204` | No Content | Successful DELETE |
| `400` | Bad Request | Malformed request (e.g., sending a voice message via the conversations API) |
| `401` | Unauthorized | Missing or invalid `X-API-Key` |
| `402` | Payment Required | The workspace is suspended for billing, the request would exceed the monthly lead limit, or a send would exceed the workspace's monthly limit for that channel |
| `403` | Forbidden | Key valid but the workspace lacks an entitlement, the feature is turned off or view-only for this workspace, the workspace has no active plan, the key's creator is no longer an active member or their current role does not allow the endpoint, or a widget origin is not allowed |
| `404` | Not Found | Resource doesn't exist or is not owned by the authenticated workspace |
| `409` | Conflict | Duplicate, idempotent re-creates return the existing resource |
| `410` | Gone | The workspace has been deleted |
| `422` | Unprocessable Entity | Validation error, see structured `detail` array |
| `429` | Too Many Requests | Rate limited, see [Rate Limits](/rate-limits) |
| `501` | Not Implemented | Feature exists but the specific operation isn't available yet |
| `502` | Bad Gateway | A connected service returned an error |
| `500` | Internal Server Error | Unexpected server error; retry only when the operation is safe |
| `503` | Service Unavailable | Temporary availability failure, including entitlement or billing checks that could not be read (honor `Retry-After`) |

## Retrying requests

Safe to retry: `GET` requests and a `DELETE` after confirming the resource state.
Treat `PUT`, `PATCH`, and `POST` as endpoint-specific: retry them only when the
endpoint page documents idempotency or you have verified that the first request
did not complete.

Use **exponential backoff** when retrying on `429` or `5xx`:

```python theme={"dark"}
import httpx, time

def request_with_retry(client, method, url, **kwargs):
    for attempt in range(5):
        resp = client.request(method, url, **kwargs)
        if resp.status_code == 429 or resp.status_code >= 500:
            time.sleep(2 ** attempt)
            continue
        return resp
    return resp
```

## Idempotency

Do not assume every write is idempotent. Use the documented idempotency field or
header where available. For campaign enrollment, inspect the response before
retrying; duplicate enrollment is reported as skipped rather than creating a
second enrollment.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.