Skip to navigation

Error Handling

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

Error response format

Most API errors follow this structure:

{
"error": {
"message": "Human-readable error message",
"code": "ERROR_CODE",
"type": "error_type",
"details": []
},
"request_id": "3f1c2a6e-8b4d-4c1e-9f0a-2d7b5e6c8a91"
}
FieldDescription
messageHuman-readable description of the error
codeMachine-readable error code (see below)
typeError category (validation_error, not_found, etc.)
detailsArray of field-level validation errors (present on 422 validation errors)
request_idTop-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 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 409s. Robust error handling should check for both shapes.

HTTP status codes

CodeMeaningWhen it occurs
200OKSuccessful GET or POST request
201CreatedSuccessful POST /v1/contacts request (resource created)
202AcceptedAsynchronous iMessage provisioning order; wait for an assigned number
400Bad RequestInvalid request parameters or validation error
401UnauthorizedMissing or invalid API key
402Payment RequiredInsufficient balance for a paid action
403ForbiddenAccess, registration, conversation safety, or account restrictions
404Not FoundResource doesn’t exist or you don’t have access
409ConflictNumber limit reached, requested number unavailable, per-number concurrent-call limit, or DELIVERY_UNCONFIRMED (a message may already have been sent)
422Unprocessable EntityValidation error (invalid data format)
429Too Many RequestsRate limit or messaging cap exceeded (Retry-After header present on endpoint throttles)
500Internal Server ErrorServer error (see retry guidance below)
502Bad GatewayUpstream provider error (the operation may have executed; see retry guidance below)
503Service UnavailableTemporary service failure; a send may have executed. See retry guidance below.
504Gateway TimeoutAn 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.

{
"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 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.

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

CodeHTTP statusNext step
CONVERSATION_CONTENT_RESTRICTED422Start with plain text; wait for a written reply before links or media.
NEW_CONVERSATIONS_PAUSED403Pause new outreach and review engagement. Existing replies may continue.
REPLY_ONLY403Reply only to contact-initiated conversations.
ACCOUNT_REVIEW403Contact support; repeated sends will not clear the review.

See iMessage limits.

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 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 codeWhat happenedWhat to do
131047The 24-hour window is closed. The recipient hasn’t messaged you in over a daySend an approved template instead
131042Your WhatsApp Business Account has no valid payment methodAdd or repair the payment method in WhatsApp Manager; check Meta’s current pricing and account status
131026The recipient can’t receive messages. Often not a WhatsApp user, or blocked youDon’t retry. Fall back to SMS if you have consent
131051Unsupported message type for this recipientCheck the message shape against the WhatsApp guide
100Malformed request, usually a template whose variable shape doesn’t match how it was authoredPositional 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.

{
"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.

failureReasonWhat 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 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.

failureReasonWhat 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.

failureReasonWhat 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.

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.

failureReasonWhat it means
This number is not registered for US A2P 10DLC messaging. Register at 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 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.

failureReasonWhat 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

failureReasonWhat 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.

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.

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()
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:

ResponseAction
Endpoint throttle with Retry-AfterWait for the stated interval, then retry within a bounded policy.
RATE_LIMITEDSlow down; allow a bounded retry of this explicitly rejected request.
Outbound-call concurrency rejectionWait for an active call to finish before requesting another.
Daily-contact capWait for the daily window to reset.
Reply-required or inactive conversationWait for the recipient to reply.
Unknown 429Surface 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:

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)

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.

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.

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 });