> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://docs.agentphone.ai/llms.txt.
> For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://docs.agentphone.ai/_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](/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)                                                         |
| `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                                                                            |
| `404` | Not Found             | Resource doesn't exist or you don't have access                                                                   |
| `409` | Conflict              | Number limit reached, the requested number is unavailable, or a number is at its per-number concurrent-call limit |
| `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   | The server is temporarily unable to handle the request. Retry with exponential backoff.                           |
| `504` | Gateway Timeout       | The server did not receive a timely response from an upstream service. Retry with exponential backoff.            |

## Error codes

### `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. Provisioning a number requires at least \$3.00.

### 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](/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`.

Contacts who have messaged you before are not affected, so replies and re-engagement still go through. The cap resets daily and can be raised on request.

### `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, or contact support to enable first-touch sends on the line.

### `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 one in WhatsApp Manager. Session messages are free, so this usually surfaces on your first template send |
| `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 (`201`) 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 the only failure detail exposed on a message, so build your handling around these strings rather than a numeric code.

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

Delivery failures arrive asynchronously. Poll the message or subscribe to a [webhook](/documentation/guides/webhooks) rather than expecting them on the send response.

### Stop sending to this recipient

Retrying these will fail again and still costs you a message. 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`](/api-reference/numbers/lookup) before sending to avoid paying for these. A lookup costs a fraction of a message and returns line type and country.

### Retry later

Transient. Back off and resend.

| `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 and is still billed.

| `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, since every rejected message is still charged.

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

Only retry `429`s that retrying can actually clear: endpoint throttles (which carry a `Retry-After` header), call concurrency caps (which clear as active calls end), and the transient `RATE_LIMITED` code. The messaging-cap codes don't reset on retry, so surface them instead.

```python
import time, requests

NON_RETRIABLE_429 = {
    "CONVERSATION_STREAK_LIMIT", "CONVERSATION_AWAITING_REPLY", "CONVERSATION_INACTIVE",
    "OUTBOUND_LIMIT_REACHED", "NEW_CONVERSATION_LIMIT_REACHED",
}

def request_with_retry(url, headers, json=None, max_retries=3):
    for i in range(max_retries):
        response = requests.post(url, headers=headers, json=json)
        if response.status_code == 429:
            body = response.json()
            code = body.get("error", {}).get("code")
            if code in NON_RETRIABLE_429:
                # A retry won't clear these caps; handle them in your app logic
                raise Exception(body["error"]["message"])
            # Endpoint throttle (has Retry-After), call concurrency cap,
            # or transient RATE_LIMITED: wait, then retry
            time.sleep(int(response.headers.get("Retry-After", 30)))
            continue
        response.raise_for_status()
        return response.json()
    raise Exception("Max retries exceeded")
```

```javascript
const NON_RETRIABLE_429 = new Set([
  "CONVERSATION_STREAK_LIMIT", "CONVERSATION_AWAITING_REPLY", "CONVERSATION_INACTIVE",
  "OUTBOUND_LIMIT_REACHED", "NEW_CONVERSATION_LIMIT_REACHED",
]);

async function requestWithRetry(url, options, maxRetries = 3) {
  for (let i = 0; i < maxRetries; i++) {
    const response = await fetch(url, options);
    if (response.status === 429) {
      const body = await response.json().catch(() => ({}));
      const code = body.error?.code;
      if (NON_RETRIABLE_429.has(code)) {
        // A retry won't clear these caps; handle them in your app logic
        throw new Error(body.error?.message ?? "Messaging cap reached");
      }
      // Endpoint throttle (has Retry-After), call concurrency cap,
      // or transient RATE_LIMITED: wait, then retry
      const retryAfter = parseInt(response.headers.get("Retry-After") || "30");
      await new Promise(r => setTimeout(r, retryAfter * 1000));
      continue;
    }
    if (!response.ok) throw new Error(`Request failed: ${response.status}`);
    return response.json();
  }
  throw new Error("Max retries exceeded");
}
```

### Retry transient errors

For `429`, `500`, `502`, `503`, and `504` errors on **idempotent requests** (GETs, and retries of the same update), implement exponential backoff:

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

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

> **Warning**
>
> The API does not currently support idempotency keys. A `5xx` on a send (`POST /v1/messages`, `POST /v1/calls`) can occur after the operation has already executed, so blindly retrying can double-send or double-bill. Before retrying a failed send, confirm whether it went through (for example, list recent messages in the conversation).

> **Note**
>
> If you're using the official SDKs, retry logic is built in. The TypeScript SDK automatically retries on 408, 429, and 5xx errors with exponential backoff (default: 2 retries).