Troubleshooting: message undelivered or failed
Your message reached a terminal failure state — the Delivery Log showsundelivered, failed, rejected, bounced, or expired — and you
need to know why, and what to change before re-sending. This page walks
you through reading the failure fields, mapping the common causes to
their fix, and knowing when a retry is useless.
If the row never left the queue (queued, scheduled, pending),
work the queue-side troubleshooting page
instead. If the row is stuck at sent or submitted_no_receipt with
no terminal outcome at all, see
Troubleshooting: message sent but no delivery receipt.
The symptom map: four terminal failures are not the same thing
All four failure statuses are terminal (is_terminal: true,
state_class: "terminal"), but they point at different layers of the
delivery chain. Read the status first — it tells you whose side failed.
Three places read the same state:
- The Delivery Log status filter —
/messages/delivery-log?status=undelivered(orfailed,rejected,bounced,expired). Overviews are in the delivery log guide. GET /api/v1/messages/:id— the authoritative row state, plusmetadata.classified_error_codeon terminal failures.- The
message.failedwebhook event — all five failure statuses (includingexpired) fan out on this event; branch on the payload’sstatusfield, not the event type.
The failure fields to decode
Two layers of failure data ride on a terminal row:metadata.classified_error_code— the normalized, machine-readable failure category. One value, same across channels, present on theGET /api/v1/messages/:idresponse for terminal failures. Branch your integration on this field, not on the display label.error_code/error_message— the carrier’s own wording of the failure. They ride on themessage.failedwebhook payload (data.error_code,data.error_message) and are optional — the platform omits both keys when the carrier returned no failure reason, so treat them as may-be-absent. Successful transitions (message.sent,message.delivered,message.read) clear any prior failure reason.
classified_error_code for programmatic handling; read
error_code / error_message when you open a ticket or trace a
carrier.
Cause → fix
Match the cause class from the table above to its fix:What not to do
- Do not spam
POST /api/v1/messages/:id/retryagainst an unreachable handset or a rejected sender. Retry re-runs the same gate — an unreachable handset, an unregistered sender, a content violation fail deterministically the second time too. Fix the gate first; retry the row or send a new message after. The same rule the queue-side troubleshooting page lays out applies here. - Do not count
expiredas a failure. It is a closed row with an unknown outcome; it can mask a successful delivery. Exclude it from your hard-failure rate and track it separately.
When to escalate
Open a support ticket when one of these holds:- A single message shows an unexpected
classified_error_codeand the cause-to-fix table above does not explain it. - A fleet of failures on one destination, route, or carrier that
persists after you adjust routing — the delivery log
status=undeliveredfilter plus the deliverability analytics top-N failure-reason breakdown is the aggregate view to quote. - You need the provider-side trace: supply the failure fields from this page.
- Your tenant ID (shown in the dashboard under Settings →
Organization, and returned by the
GET /api/v1/meresponse asorganizationId). - One message ID (
msg_…) of a representative failed row. - The
classified_error_codeof that row.
See also
- Troubleshooting: message stuck in queued — the pre-queue stage, and the same retry-discipline rule
- Troubleshooting: message sent but no delivery receipt
— the intermediate-state sibling, for rows holding at
sent/submitted_no_receipt - Delivery lifecycle — the concept-level
state machine and the
metadata.classified_error_codebranch point - Message status lifecycle — per-status semantics and the full transition table
- Webhook events reference — the
message.failedpayload (error_code,error_message,is_terminal) and the reconciliation behaviour for late-arriving DLRs - Number Lookup — validate and enrich a destination before re-sending
- Delivery log — the dashboard surface behind
the
status=undeliveredfilter