> For clean Markdown of any page, append .md to the page URL. > For a complete documentation index, see https://docs.agentphone.ai/documentation/reference/error-handling/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). > HTTP status codes, error response format, and error handling best practices