Every error is the same envelope: a machine code
to branch on, a human message, and a docs_url that
links to the code's row on this page.
error.json
{
"error": {
"code": "recipient_opted_out",
"message": "This recipient texted STOP. You cannot message them again unless they text START.",
"docs_url": "https://docs.handset.dev/errors.html#recipient_opted_out"
}
}
HTTP status carries the class of failure: 400/422
your request needs a change, 401/403 credentials,
404 no such resource (in your account — IDs are
account-scoped), 409 conflict, 429 slow down,
5xx us — retry.
Authentication & limits
missing_api_key
No Authorization header. Pass your key as a bearer token: Authorization: Bearer hs_live_….
malformed_authorization
The header isn't Bearer <key>. Check the scheme and whitespace.
invalid_api_key
Unknown, malformed, or revoked key. Check it in the console; test and live are different keys.
account_suspended
The account is suspended. Contact support to restore access.
tenant_restricted_key
This key is scoped to one tenant and the request touched another. Use an account-wide key or the right tenant's key.
rate_limited
Over 20 req/s (burst 60) on this key. The SDKs retry this automatically honoring Retry-After; if you see it, you're sustainedly over — spread the work out.
Request shape
invalid_json
The body isn't valid JSON.
invalid_body
The body doesn't match the endpoint's schema — the message names the field.
unreadable_body
The body couldn't be read (truncated or oversized request).
missing_fields
A required field is absent — the message lists which.
not_found
No such resource in your account. IDs are account-scoped: another account's ID 404s rather than 403s.
idempotency_key_reuse
This Idempotency-Key was already used with a different body. Reuse a key only to retry the identical request.
invalid_start
Unparseable start timestamp — use RFC 3339.
invalid_end
Unparseable end timestamp — use RFC 3339.
invalid_range
start must be before end.
internal_error
Something failed on our side. Retry; contact support if it persists.
database_unavailable
Brief outage on our side. Retry with backoff.
Messaging
empty_message
Provide body, media_urls, or both.
body_too_long
The body exceeds the maximum length. Split the message.
invalid_to
The destination isn't a valid E.164 phone number (+14155550123).
recipient_opted_out
They texted STOP. Do not retry — delivery is blocked until they text START. Design for it: check /v1/opt_outs or handle this code.
from_number_not_found
The from number isn't one of this tenant's numbers.
number_not_sms_capable
This number can't send SMS (voice-only).
campaign_not_approved
The number's 10DLC campaign is still in review. Test mode approves instantly; live approval takes days.
missing_consent
The opt-in form submission lacked required consent.
message_not_found
No such message in your account.
conversation_not_found
No such conversation in your account.
Numbers & porting
invalid_phone
Not a valid E.164 phone number.
number_unavailable
This number was just taken. Search again and pick another.
phone_number_not_found
No such number in your account.
invalid_phone_numbers
The list is empty or contains non-E.164 entries.
numbers_not_portable
At least one number can't be ported — the message lists which. Run POST /v1/port_ins/check first.
port_in_not_found
No such port-in in your account.
port_in_not_submittable
Only draft port-ins can be submitted.
port_in_not_cancellable
This port-in has progressed past the point of cancellation.
invalid_service_address
The service address is incomplete — street, city, state, and ZIP are required.
e911_address_invalid
The address failed E911 validation. Verify it with the postal service format.
Voice
invalid_to_number
The customer number to dial isn't valid E.164.
invalid_connect_to
The agent number to bridge isn't valid E.164.
call_not_active
DTMF and gather commands need an in-progress call.
destination_not_supported
The exchange is a known high-cost destination (access stimulation) and can't be dialed — from any leg or ring target.
call_not_found
No such call in your account.
voicemail_not_found
No such voicemail in your account.
recording_not_found
No such recording in your account.
routing_config_not_found
No such routing config in your account.
routing_config_in_use
Numbers still reference this routing config — detach them first.
10DLC compliance
brand_not_found
No such brand in your account.
brand_not_vetted
The brand hasn't completed vetting; campaigns need a vetted brand.
campaign_not_found
No such campaign in your account.
legal_name_required
The brand's registered legal name is required.
invalid_ein
The EIN must be nine digits (12-3456789 accepted).
invalid_entity_type
Entity type must be one of the documented values (e.g. private_profit).
invalid_billing_phone_number
The brand contact phone isn't valid E.164.
invalid_contact_email
The brand contact email isn't a valid address.
invalid_use_case
Use case must be one of the documented campaign use cases.
description_required
Campaigns need a description of the traffic.
invalid_sample_messages
Provide 1–5 realistic sample messages.
Tenants & webhooks
tenant_not_found
No such tenant in your account.
name_required
Tenants need a display name.
invalid_timezone
Not an IANA timezone (America/Phoenix).
external_ref_taken
Another tenant already carries this external_ref. They're unique per account.
webhook_endpoint_not_found
No such webhook endpoint in your account.
url_required
Webhook endpoints need a URL.
invalid_url
The webhook URL must be a valid https:// address.
unknown_event_type
The event type filter names an event we don't emit — see Webhooks for the list.