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": []
}
}
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)

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)
400Bad RequestInvalid request parameters or validation error
401UnauthorizedMissing or invalid API key
402Payment RequiredInsufficient balance for a paid action
404Not FoundResource doesn’t exist or you don’t have access
409ConflictNumber limit reached, the requested number is unavailable, or a number is at its per-number concurrent-call limit
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 UnavailableThe server is temporarily unable to handle the request. Retry with exponential backoff.
504Gateway TimeoutThe 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.

{
"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. 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 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 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 one in WhatsApp Manager. Session messages are free, so this usually surfaces on your first template send
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 (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.

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

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

Transient. Back off and resend.

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

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

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

Only retry 429s 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.

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

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)

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

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