> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://agentphone.to/docs/llms.txt.
> For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://agentphone.to/docs/_mcp/server.

# Error Handling

> HTTP status codes, error response format, and error handling best practices

The AgentPhone API uses standard HTTP status codes and returns detailed error information as JSON.

## Error response format

Most API errors follow this structure:

```json
{
  "error": {
    "message": "Human-readable error message",
    "code": "ERROR_CODE",
    "type": "error_type",
    "details": []
  },
  "request_id": "3f1c2a6e-8b4d-4c1e-9f0a-2d7b5e6c8a91"
}
```

| Field        | Description                                                                 |
| ------------ | --------------------------------------------------------------------------- |
| `message`    | Human-readable description of the error                                     |
| `code`       | Machine-readable error code (see below)                                     |
| `type`       | Error category (`validation_error`, `not_found`, etc.)                      |
| `details`    | Array of field-level validation errors (present on `422` validation errors) |
| `request_id` | Top-level id of this request, the same value as the `X-Request-Id` header   |

Every error body also carries a top-level `request_id`, the same value as the `X-Request-Id` response header. Quote it when you contact support or [submit feedback](/docs/feedback) about a call.

Some responses use a plain `{"detail": "..."}` body with no error code instead. This includes authentication errors (`401`/`403`), endpoint throttling (`429` with a `Retry-After` header), the outbound-call concurrency cap (`429`, no `Retry-After`), insufficient balance (`402`), and number-related `409`s. Robust error handling should check for both shapes.

## HTTP status codes

| Code  | Meaning               | When it occurs                                                                                                                                         |
| ----- | --------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `200` | OK                    | Successful GET or POST request                                                                                                                         |
| `201` | Created               | Successful `POST /v1/contacts` request (resource created)                                                                                              |
| `202` | Accepted              | Asynchronous iMessage provisioning order; wait for an assigned number                                                                                  |
| `400` | Bad Request           | Invalid request parameters or validation error                                                                                                         |
| `401` | Unauthorized          | Missing or invalid API key                                                                                                                             |
| `402` | Payment Required      | Insufficient balance for a paid action                                                                                                                 |
| `403` | Forbidden             | Access, registration, conversation safety, or account restrictions                                                                                     |
| `404` | Not Found             | Resource doesn't exist or you don't have access                                                                                                        |
| `409` | Conflict              | Number limit reached, requested number unavailable, per-number concurrent-call limit, or `DELIVERY_UNCONFIRMED` (a message may already have been sent) |
| `422` | Unprocessable Entity  | Validation error (invalid data format)                                                                                                                 |
| `429` | Too Many Requests     | Rate limit or messaging cap exceeded (`Retry-After` header present on endpoint throttles)                                                              |
| `500` | Internal Server Error | Server error (see retry guidance below)                                                                                                                |
| `502` | Bad Gateway           | Upstream provider error (the operation may have executed; see retry guidance below)                                                                    |
| `503` | Service Unavailable   | Temporary service failure; a send may have executed. See retry guidance below.                                                                         |
| `504` | Gateway Timeout       | An upstream response timed out; a send may have executed. See retry guidance below.                                                                    |

## Error codes

### `DELIVERY_UNCONFIRMED`

The message may already have been sent, but its outcome could not be confirmed. Returned with HTTP `409`. Do not automatically resend it. If a message ID or delivery update is available, use it to inspect the original attempt. Contact support if the outcome remains unknown. This is not a definitive delivery failure, and an absent history entry does not make resending safe.

### `VALIDATION_ERROR`

Request validation failed. Check the `details` field for specific field errors.

```json
{
  "error": {
    "message": "Validation error",
    "code": "VALIDATION_ERROR",
    "type": "validation_error",
    "details": [
      {
        "field": "country",
        "message": "Country must be a 2-letter ISO code",
        "type": "value_error"
      }
    ]
  }
}
```

### Number limit reached

Each account has a self-serve number limit (see `numbers.limit` in `GET /v1/usage`). `POST /v1/numbers` returns HTTP `409` with a plain `{"detail": "..."}` body when you hit the cap. [Contact us](https://calendar.app.google/kJQSTCyNYTphvCGm7) to increase your limit.

### Insufficient balance

Your balance is too low to complete a paid action. Returned as HTTP `402` with a plain `{"detail": "..."}` body; the message describes the balance requirement (for example, the minimum needed to provision a number and your current balance). Add funds from the Billing page or enable auto-recharge. The funding requirement depends on number type; iMessage uses a separate card subscription. See [billing](/docs/usage).

### Rate limiting (`429`)

`429` responses come in three flavors:

* **Endpoint throttles** return a plain `{"detail": "Too many requests. Please try again in N seconds."}` body with a `Retry-After` header. Wait that long, then retry.
* **Call concurrency caps** on `POST /v1/calls` return a plain `{"detail": "Concurrent outbound call limit reached (N). Please wait for an active call to end."}` body with no `Retry-After` header. This clears as your active calls end, so wait briefly and retry.
* **Messaging limits** return the error envelope with a specific code and no `Retry-After` header. `RATE_LIMITED` is transient; the cap codes (`CONVERSATION_STREAK_LIMIT`, `CONVERSATION_AWAITING_REPLY`, `CONVERSATION_INACTIVE`, `OUTBOUND_LIMIT_REACHED`, `NEW_CONVERSATION_LIMIT_REACHED`, described below) clear only when the recipient replies or a daily window resets, never on retry.

### `RATE_LIMITED`

Sending too fast. Returned with HTTP `429`. This one is transient: slow down and retry shortly. No `Retry-After` header is included.

### `PHONE_NUMBER_NOT_FOUND`

The requested phone number doesn't exist or you don't have access to it.

### `CONVERSATION_STREAK_LIMIT`

You've sent too many messages in a row to one contact without a reply. Returned with HTTP `429`.

Retrying will not clear this. The count only resets when the contact replies, and the limit applies to that one conversation, so your other threads keep sending. See [Messaging Rate Limits](/docs/documentation/reference/messaging-rate-limits) for details.

### `OUTBOUND_LIMIT_REACHED`

You've reached the daily cap for messaging contacts who have never messaged your line. Returned with HTTP `429`.

Replies do not consume the outbound-first daily quota, but other conversation and safety restrictions still apply. The standard ceiling is 50 new contacts/day/outbound line. Wait for the daily reset.

### `NEW_CONVERSATION_LIMIT_REACHED`

You've reached the daily cap for starting new conversations. Returned with HTTP `429`. Replies to existing conversations still go through, and the cap resets daily.

### `CONVERSATION_AWAITING_REPLY`

Up to 3 messages can be sent to a brand-new recipient before they respond. Returned with HTTP `429`. Retrying does not clear it; it unlocks when the recipient replies.

### `CONVERSATION_INACTIVE`

The conversation has been inactive for over 14 days and its single re-engagement message was already sent. Returned with HTTP `429`. It unlocks when the recipient replies.

### `INBOUND_ONLY`

This line can't send the first message to a new recipient. Returned with HTTP `422`. Ask the recipient to message you first. An inbound subscription does not include initiating new conversations; use an outbound line when that capability is needed.

### iMessage safety controls

| Code                              | HTTP status | Next step                                                                |
| --------------------------------- | ----------- | ------------------------------------------------------------------------ |
| `CONVERSATION_CONTENT_RESTRICTED` | `422`       | Start with plain text; wait for a written reply before links or media.   |
| `NEW_CONVERSATIONS_PAUSED`        | `403`       | Pause new outreach and review engagement. Existing replies may continue. |
| `REPLY_ONLY`                      | `403`       | Reply only to contact-initiated conversations.                           |
| `ACCOUNT_REVIEW`                  | `403`       | Contact support; repeated sends will not clear the review.               |

See [iMessage limits](/docs/documentation/reference/messaging-rate-limits#imessage).

### `WHATSAPP_NOT_ENABLED`

WhatsApp has been switched off for this account. Returned with HTTP `403`. WhatsApp is on by default for everyone, so this only appears if it was disabled manually. Email [founders@agentphone.to](mailto:founders@agentphone.to) to have it restored.

### WhatsApp send errors

WhatsApp sends are rejected by Meta rather than by a carrier, and the reason comes back in the error message. The ones worth handling in code:

| Meta code | What happened                                                                                | What to do                                                                                            |
| --------- | -------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------- |
| `131047`  | The 24-hour window is closed. The recipient hasn't messaged you in over a day                | Send an approved template instead                                                                     |
| `131042`  | Your WhatsApp Business Account has no valid payment method                                   | Add or repair the payment method in WhatsApp Manager; check Meta’s current pricing and account status |
| `131026`  | The recipient can't receive messages. Often not a WhatsApp user, or blocked you              | Don't retry. Fall back to SMS if you have consent                                                     |
| `131051`  | Unsupported message type for this recipient                                                  | Check the message shape against the WhatsApp guide                                                    |
| `100`     | Malformed request, usually a template whose variable shape doesn't match how it was authored | Positional templates take an array, named templates take an object                                    |

Template sends outside the window are the single most common WhatsApp failure. Check `capabilities.whatsappWindowExpiresAt` on the conversation before a free-form send rather than discovering it from a 422.

### Provider errors

`SMS_PROVIDER_ERROR`, `MESSAGE_PROVIDER_ERROR`, and `TELEPHONY_PROVIDER_ERROR` indicate a failure at an upstream messaging or telephony provider, usually returned with HTTP `502`. These are typically temporary, but the operation may have executed before the failure. See the retry guidance below before resending.

## Delivery failures (`failureReason`)

The errors above are **API** errors: your request was rejected. A message can also be accepted (`200`) and then fail later at the carrier. When that happens the message's `status` becomes `failed` and `failureReason` carries a plain-English explanation.

`failureReason` is human-readable diagnostic text, not a stable machine-code contract. Store the message ID and status, display the reason, and handle unknown reasons conservatively.

```json
{
  "id": "msg_...",
  "status": "failed",
  "failureReason": "The recipient has opted out of messages from this number."
}
```

Delivery failures arrive asynchronously. Inspect the original message in number or conversation message history. There is no documented customer delivery-status webhook or single-message GET endpoint; inbound `agent.message` events are not outbound delivery receipts.

### Stop sending to this recipient

Retrying these without correcting the cause will fail again and can incur additional charges. Remove the recipient or fix the number.

| `failureReason`                                            | What it means                                             |
| ---------------------------------------------------------- | --------------------------------------------------------- |
| The recipient has opted out of messages from this number.  | STOP was received. Do not message again from this number. |
| The recipient's number isn't valid or is disabled.         | Bad or disabled number.                                   |
| The recipient's number is inactive or invalid.             | Bad number.                                               |
| The recipient's number has been deactivated.               | Number no longer in service.                              |
| The recipient's number is unknown or no longer exists.     | Number not in the numbering plan.                         |
| The recipient's number isn't a valid mobile number.        | Landline or non-mobile.                                   |
| The recipient's number is a landline or can't receive SMS. | Landline.                                                 |
| The recipient's number blocks incoming messages.           | Recipient-side block.                                     |
| The recipient's number can't receive messages.             | Recipient-side block.                                     |
| The recipient's number isn't assigned to a carrier yet.    | Newly issued number, not yet routable.                    |
| Premium-rate numbers can't receive messages.               | Premium-rate destination.                                 |

Screen numbers with [`GET /v1/numbers/lookup`](/docs/documentation/guides/phone-numbers#look-up-a-number) before sending to avoid paying for these. A lookup costs a fraction of a message and returns line type and country.

### Retry later

These reasons suggest a transient problem. Inspect the original attempt’s status before deciding to send again; ambiguous outcomes can produce duplicates.

| `failureReason`                                                       | What it means                                  |
| --------------------------------------------------------------------- | ---------------------------------------------- |
| A temporary error occurred. Retry shortly.                            | Transient upstream error.                      |
| A carrier error occurred while submitting the message. Retry shortly. | Transient carrier error.                       |
| The carrier didn't acknowledge the message. Retry shortly.            | No carrier acknowledgement.                    |
| The recipient's phone is off or unreachable.                          | Handset unreachable. May succeed later.        |
| The recipient's phone is unreachable or turned off.                   | Handset unreachable.                           |
| The recipient's number could not be reached.                          | Temporary routing failure.                     |
| The recipient's carrier could not be reached.                         | Carrier unreachable.                           |
| The message queue for this number overflowed. Retry shortly.          | Send rate too high for this number. Slow down. |

### Wait, do not retry immediately

Volume and rate limits. A same-day retry on a **daily** limit fails again. Do not assume all failed messages have the same billing treatment; some confirmed non-billable failures receive a credit. See [failed messages and refunds](/docs/usage#failed-messages-and-refunds).

| `failureReason`                                                            | What it means                                                                                              |
| -------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------- |
| T-Mobile's daily volume limit for your registered brand was reached.       | Daily cap on your 10DLC brand. Resets the next day. Raising it requires brand vetting, so contact support. |
| This number reached its daily sending limit. Sending resumes tomorrow.     | Per-number daily cap.                                                                                      |
| Messages from this number were rate-limited by AT\&T. Retry shortly.       | Per-second/minute rate limit. Back off.                                                                    |
| Messages from this number were rate-limited by the carrier. Retry shortly. | Rate limit. Back off.                                                                                      |

> **Warning**
>
> The first two are **daily** limits and the second two are **rate** limits. Retrying within the same day only helps the rate limits. If you see the daily ones at volume, throttle rather than retry, to avoid repeat failed attempts and possible additional charges.

### Fix your configuration

These will keep failing until something is changed on your account.

| `failureReason`                                                                                                                                        | What it means                                     |
| ------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------- |
| This number is not registered for US A2P 10DLC messaging. Register at [https://agentphone.ai/register](https://agentphone.ai/register) to send US SMS. | Register before texting US numbers.               |
| This number isn't registered for US 10DLC messaging. Register at [https://agentphone.ai/register](https://agentphone.ai/register) to send US SMS.      | Same.                                             |
| There's a 10DLC registration issue at the carrier. Contact support.                                                                                    | Registration exists but is broken at the carrier. |
| The messaging campaign is inactive for this destination.                                                                                               | Campaign pending, rejected or suspended.          |
| Messaging to this destination country isn't enabled.                                                                                                   | Destination country not enabled on your account.  |
| This toll-free number isn't verified for messaging yet. Contact support.                                                                               | Toll-free verification required.                  |
| This toll-free number isn't enabled for messaging. Contact support.                                                                                    | Toll-free messaging not enabled.                  |
| This number isn't enabled for messaging. Contact support.                                                                                              | Number not provisioned for SMS.                   |
| The sending number isn't routable to the carrier. Contact support.                                                                                     | Routing misconfiguration.                         |
| The sending account is suspended. Contact support.                                                                                                     | Account suspended.                                |
| This number was blocked for policy violations. Contact support.                                                                                        | Number blocked.                                   |
| The message price exceeded the configured maximum. Contact support.                                                                                    | Price cap hit.                                    |

### Blocked as spam

Content or sender reputation. Repeated spam blocks damage your sender standing, so change the message rather than resending it.

| `failureReason`                                    | What it means                        |
| -------------------------------------------------- | ------------------------------------ |
| The message was blocked by spam filtering.         | Blocked before reaching the carrier. |
| The message was flagged as spam by the carrier.    | Carrier spam block.                  |
| The message was blocked by carrier spam filtering. | Carrier spam block.                  |
| The message was filtered by the carrier.           | Silently filtered.                   |
| The message was blocked by the carrier.            | Carrier block, no reason given.      |

### Content and routing

| `failureReason`                                                                      | What it means                                          |
| ------------------------------------------------------------------------------------ | ------------------------------------------------------ |
| The message was too long or invalid for the carrier.                                 | Shorten or split. Some carriers reject 10+ segments.   |
| The message content type isn't supported.                                            | Unsupported media or encoding.                         |
| The message expired before it could be sent.                                         | Expired in queue.                                      |
| The message expired after delivery retries.                                          | Carrier gave up retrying.                              |
| The message couldn't be routed after multiple attempts.                              | No route found.                                        |
| The recipient's number couldn't be routed to a carrier.                              | No route to destination.                               |
| Texting this destination isn't supported.                                            | Destination not supported.                             |
| The recipient's carrier or handset doesn't support this message.                     | Unsupported channel for that handset.                  |
| The carrier rejected the message.                                                    | Rejected with no specific reason.                      |
| The carrier could not deliver this message.                                          | Generic fallback when no specific reason is available. |
| The carrier failed to deliver the message.                                           | Carrier-side delivery failure.                         |
| The carrier reported an unknown delivery error.                                      | Unclassified carrier error.                            |
| The message couldn't be sent due to an account or formatting issue. Contact support. | Malformed request or account issue.                    |

> **Note**
>
> This list covers every reason currently emitted. New ones can appear as carriers change their reporting, so treat an unrecognised `failureReason` as non-retryable and log it rather than assuming it is safe to resend.

## Handling errors

### Check response status

Handle both body shapes: the error envelope and the plain `{"detail": "..."}` form.

```python
import requests

response = requests.post(url, headers=headers, json=data)
if not response.ok:
    body = response.json()
    message = body["error"]["message"] if "error" in body else body.get("detail", response.reason)
    print(f"API Error ({response.status_code}): {message}")
    raise Exception(message)
data = response.json()
```

```javascript
const response = await fetch(url, options);
if (!response.ok) {
  const body = await response.json().catch(() => ({}));
  const message = body.error?.message ?? body.detail ?? response.statusText;
  console.error(`API Error (${response.status}):`, message);
  throw new Error(message);
}
const data = await response.json();
```

### Handle rate limits

Use an explicit retry decision rather than retrying every `429`:

| Response                                | Action                                                                |
| --------------------------------------- | --------------------------------------------------------------------- |
| Endpoint throttle with `Retry-After`    | Wait for the stated interval, then retry within a bounded policy.     |
| `RATE_LIMITED`                          | Slow down; allow a bounded retry of this explicitly rejected request. |
| Outbound-call concurrency rejection     | Wait for an active call to finish before requesting another.          |
| Daily-contact cap                       | Wait for the daily window to reset.                                   |
| Reply-required or inactive conversation | Wait for the recipient to reply.                                      |
| Unknown `429`                           | Surface the error and investigate; do not assume it is transient.     |

A failed network request, timeout, or 5xx on a write is a different case: the action may have executed. Reconcile its outcome before any manual resend.

### Retry transient errors

For `429`, `500`, `502`, `503`, and `504` errors on **reads** (`GET` and `HEAD`), use bounded exponential backoff. Do not apply this policy to message sends, call creation, or other operations with side effects:

```python
from requests.adapters import HTTPAdapter
from urllib3.util.retry import Retry

session = requests.Session()
retry_strategy = Retry(
    total=3,
    backoff_factor=1,
    allowed_methods=frozenset(["GET", "HEAD"]),
    status_forcelist=[429, 500, 502, 503, 504]
)
adapter = HTTPAdapter(max_retries=retry_strategy)
session.mount("https://", adapter)
```

> **Warning**
>
> The API does not support customer-supplied idempotency keys. A timeout, connection loss, or `5xx` on `POST /v1/messages` or `POST /v1/calls` can occur after the operation has already executed. Treat the outcome as unknown and do not automatically resend. An empty conversation history is not proof that the original request did not execute: it may still be in flight, or its record may not have been saved. A new POST is a separate send and can produce a duplicate. Only retry when a specific response establishes non-acceptance, such as the endpoint throttles described above.

> **Note**
>
> The TypeScript SDK `agentphone@1.0.23` automatically retries HTTP 408, 429, and 5xx twice, including message and call creation. Set `maxRetries: 0` for sends, or on the client, to prevent this. Check the retry policy of your installed release; SDKs and versions do not all use the same defaults.

```typescript
import { AgentPhoneClient } from "agentphone";

const client = new AgentPhoneClient({
  token: process.env.AGENTPHONE_API_KEY,
  maxRetries: 0,
});

// Opt into bounded retries on an individual read when needed.
await client.calls.listCalls({}, { maxRetries: 2 });
```