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

# Error Handling

> Conventional HTTP response codes with a structured error object containing code, message, and additional context.

The API uses conventional HTTP response codes. Error bodies come in three shapes depending on which layer rejects the request.

## Error response shapes

**Standard errors** (validation failures, missing resources, state conflicts, server errors) return a flat body with a machine-readable `code` and a `correlationId` you can quote to support. Some codes also include `details`. For `VALIDATION_ERROR` it is an array with one entry per rejected field, where `field` is the dotted path into the request body or parameters:

```json theme={null}
{
  "error": "Validation failed",
  "code": "VALIDATION_ERROR",
  "correlationId": "01JZWX3F9G2K4M6P8R0T2V4X6Y",
  "details": [
    { "field": "organization_id", "message": "Invalid UUID", "code": "invalid_format" }
  ]
}
```

**Authentication and permission errors** from the API-key layer (`401`, and `403` when the key lacks a scope) use a simpler two-field body with no `code`:

```json theme={null}
{
  "error": "API key required",
  "message": "Please provide an API key in the X-API-Key header"
}
```

**Rate-limit errors** (`429`) include `retryAfter` in the body and a `Retry-After` header:

```json theme={null}
{
  "success": false,
  "error": "Rate limit exceeded",
  "message": "Maximum 600 requests per 60 seconds",
  "code": "RATE_LIMIT_EXCEEDED",
  "statusCode": 429,
  "retryAfter": 3
}
```

## HTTP status codes

| Status | Meaning |
| - | - |
| `200` | **Success** - Request completed successfully. |
| `400` | **Bad Request** - Invalid parameters or malformed request. |
| `401` | **Unauthorized** - Missing or invalid API key. |
| `403` | **Forbidden** - API key lacks required permissions or access denied. |
| `402` | **Payment Required** - The wallet cannot cover the operation (`INSUFFICIENT_BALANCE`). Add funds and retry. |
| `404` | **Not Found** - Resource does not exist. |
| `409` | **Conflict** - Resource is in the wrong state for the request (for example scheduling a project that is not ready, requesting survey results for a broadcast project, or retrying an idempotent write that is still processing). |
| `422` | **Unprocessable** - The request was understood but can't be executed (for example reusing an `Idempotency-Key` with a different body, or scheduling with insufficient balance). |
| `429` | **Too Many Requests** - Rate limit exceeded. See [Rate Limits](/api-reference/rate-limits). |
| `500` | **Internal Server Error** - Something went wrong on our end. |

## Common error codes

| Code | Description |
| - | - |
| `VALIDATION_ERROR` | Request parameters or body failed validation. `details` is an array of `{ field, message, code }`, one per rejected field. |
| `ORG_ACCESS_DENIED` | Requested organization is not in your accessible hierarchy. |
| `ONBOARDING_INCOMPLETE` | `403`. The organization's 14-day onboarding grace window has passed and the business profile or funding step (see [Account Setup](/onboarding/account-setup)) is still incomplete. Returned on requests that create new resources: brands, campaigns, toll-free verifications, phone number purchases, email domains and senders, projects and project copies, and email campaigns and duplicates, including `POST /v1/projects`, `POST /v1/projects/{id}/copy`, and `POST /v1/email/campaigns`. `details` includes `missingSteps` (an array containing `profile`, `funding`, or both) and `onboardingUrl` (`/onboarding`). Nothing already running is affected, only new creates are blocked. |
| `ENTITLEMENT_REQUIRED` | `403`. The organization is not entitled to this feature; some features require enablement by Political Comms. Returned on every write (`POST`/`PUT`/`PATCH`/`DELETE`) under `/v1/email/*`. `details` includes `entitlement`, the entitlement key that is missing (`tendlc`, `toll_free`, `short_code`, `data_purchases`, `email`, `stripe_connect`, `phone_number_sharing`, or `sub_orgs`). Contact support to request enablement. |
| `SENDING_PAUSED` | `409`. Sending is paused for the organization or platform-wide, by Political Comms. Returned on `POST /v1/projects`, `POST /v1/projects/{id}/schedule`, `POST /v1/conversations/{id}/messages`, and `POST /v1/email/campaigns/{id}/schedule`. `details` includes `scope` (`organization` or `platform`). |
| `RATE_LIMIT_EXCEEDED` | Too many requests - wait for the `Retry-After` seconds before retrying. |
| `INVALID_STATE_TRANSITION` | Project is not in a state that allows this operation. `POST /v1/projects/{id}/schedule` accepts `ready` and `paused` projects. |
| `NOT_A_SURVEY` | The endpoint only applies to survey projects (`type: survey`). |
| `LIST_NOT_READY` | Contact list must finish importing before it can be analyzed. |
| `ANALYSIS_NOT_COMPLETE` | `409`. `GET /v1/contact-lists/{id}/download?type=analyzed` was requested before analysis finished. Poll `GET /v1/contact-lists/{id}` until `analysis.status` is `complete`, or subscribe to the `contact_list.analyzed` webhook. |
| `LIST_ANALYSIS_IN_PROGRESS` | `409`. `POST /v1/projects`, the project update endpoint, or `POST /v1/projects/{id}/schedule` referenced a contact list whose analysis is still running (`analysis.status` `processing`). Nothing is changed. `details.lists` is an array of `{ id, name, status }` for each list still in analysis. Wait for `analysis.status` `complete` on `GET /v1/contact-lists/{id}` or the `contact_list.analyzed` webhook, then retry the same request. |
| `INSUFFICIENT_BALANCE` | A wallet in the organization hierarchy cannot cover the scheduled send (`422` on schedule) or the list analysis (`402` on `POST /v1/contact-lists/{id}/analyze`, with nothing queued). This may be a **parent** organization rather than the one you called with - charges roll up, so every ancestor must cover its own share. `details` includes the shortfall plus `organizationId` and `organizationName` identifying which organization fell short, `actingOrganizationId` (the organization the call was made for) and `isAncestor` (`true` when the short wallet belongs to a parent of the acting organization). |
| `CONVERSATION_NOT_FOUND` | `404`. The conversation does not exist or belongs to an organization outside the key's hierarchy; the two are indistinguishable by design. |
| `CONTACT_OPTED_OUT` | `409`. The contact replied STOP; a reply to this conversation is refused before any charge. Final: do not retry. |
| `CONVERSATION_NOT_SENDABLE` | `409`. The conversation has no sending number bound to it, so nothing can reply from it. |
| `PROJECT_DELETED` | `409`. The project behind the conversation was deleted. |
| `PHONE_NUMBER_UNAVAILABLE` | `409`. The conversation's sending number was released or is inactive. |
| `SEND_ENQUEUE_FAILED` | `503`. A reply could not be handed to the delivery queue after billing; the charge was reversed and nothing was sent. Retry the same call. |
| `CARRIER_ESTIMATE_TIMEOUT` | `503`. The carrier throughput estimate for `GET /v1/projects/{id}/throughput` timed out and had no effect. Retry later. |
| `MIXED_PHONE_OWNERSHIP` | `400`. A project's `phone_number_ids` mixed numbers the organization owns with numbers shared with it, or shared numbers from two different campaigns or registrations. Select numbers from one source. |
| `PHONE_SHARE_REVOKED` | `409`. A number the project selects was shared with the organization and the share has been revoked (or the number released). Choose a different number. |
| `IDEMPOTENCY_CONFLICT` | The same `Idempotency-Key` is still processing - retry after the `Retry-After` seconds. |
| `IDEMPOTENCY_MISMATCH` | An `Idempotency-Key` was reused with a different request body - that's a client bug; use a fresh key for new requests. |
| `DATE_RANGE_TOO_LARGE` | Date range exceeds the maximum allowed (31 days). |
| `DATE_TOO_OLD` | Start date exceeds the look-back limit: 180 days for `/messages/stats` and `/ledger/*`, 90 days for `/projects/stats`. |
| `FUTURE_DATE_NOT_ALLOWED` | Date range may not end in the future. |
| `INVALID_DATE_FORMAT` | Date parameter must be in `YYYY-MM-DD` format. |

## Email error codes

Write endpoints under `/v1/email/*` require the email entitlement on the
organization and return `403 ENTITLEMENT_REQUIRED` without it. Reads are open.
These are the codes specific to the email surface.

| Code | Status | Description |
| - | - | - |
| `EMAIL_SENDING_PAUSED` | `409` | Sending is paused for this sender identity or organization. |
| `POOL_CAPACITY_EXHAUSTED` | `409` | The dedicated sending pool cap has been reached. Contact support. |
| `POOL_ALREADY_ACTIVE` | `409` | This organization already has a dedicated sending pool, so the request did nothing. |
| `DMARC_ROOT_DOMAIN_NOT_MANAGED` | `409` | Your organizational root domain publishes a DMARC record we do not manage, so we cannot stage the change. Edit that record yourself. |

`DMARC_ROOT_DOMAIN_NOT_MANAGED` is never resolved by retrying. Surface it to a
human.

Note that `401` responses and permission-scope `403` responses come from the authentication layer and carry no `code` field (see the shapes above).

## Retrying safely

* All `GET` endpoints are safe to retry - they have no side effects.
* For `POST` and `PATCH`, retry only on `429`, `500`, `502`, `503`, and `504` responses. Don't retry on `400`, `401`, `403`, or `404` - those indicate a problem with your request that won't be fixed by trying again.
* If you need stronger duplicate-write protection, send a unique `Idempotency-Key` header (16-200 printable ASCII characters; a UUID is recommended) on every write request. Retried calls with the same key within 24 hours return the cached response from the first call, marked with an `X-Idempotent-Replayed: true` response header. Reusing a key with a different body returns `422` (`IDEMPOTENCY_MISMATCH`).

## How agents should recover

Unattended integrations and AI agents should encode this recovery matrix rather than treating all failures alike:

| Status | Retry? | Recovery action |
| - | - | - |
| `400` | Never | Fix the request parameters. Check `code` for specifics. |
| `401` | Never | Credential is missing or revoked. Escalate to a human operator; keys are created in the dashboard, not over the API. |
| `403` | Never | Stay inside the key's organization hierarchy or request a key with the right scope. |
| `404` | Never | Verify the resource ID against a fresh list call. |
| `429` | After waiting | Sleep for the `Retry-After` seconds. Never retry tighter than once per second. |
| `500`-`504` | With backoff | Exponential backoff with jitter: 1 second base, double each attempt, 60 second cap, give up after 5 attempts. |

Before treating repeated `5xx` failures as your bug, check [status.politicalcomms.com](https://status.politicalcomms.com). A standalone machine-readable version of this playbook is published at [politicalcomms.com/errors.md](https://politicalcomms.com/errors.md).

## Reporting bugs

If you see a `500` response or a `code` that's not documented here, please email [support@politicalcomms.com](mailto:support@politicalcomms.com) with:

* Your request method and path
* The full response body, including the `correlationId` field (we use it to look up server-side logs; it is also sent as the `X-Correlation-ID` response header)
* Approximate timestamp (UTC is easiest)


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