Skip to main content

Note de langue

Lorsqu’une traduction n’est pas disponible, le contenu anglais est affiché comme solution de repli. Conservez les codes d’erreur, les chemins d’API et les blocs de code.

Troubleshooting: message undelivered or failed

Your message reached a terminal failure state — the Delivery Log shows undelivered, 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 (or failed, rejected, bounced, expired). Overviews are in the delivery log guide.
  • GET /api/v1/messages/:id — the authoritative row state, plus metadata.classified_error_code on terminal failures.
  • The message.failed webhook event — all five failure statuses (including expired) fan out on this event; branch on the payload’s status field, 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 the GET /api/v1/messages/:id response 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 the message.failed webhook 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.
Read classified_error_code for programmatic handling; read error_code / error_message when you open a ticket or trace a carrier.
The per-status decoder:

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/retry against 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 expired as 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_code and 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=undelivered filter 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.
Include all three so support can pull the DLR pipeline trace without a back-and-forth:
  • Your tenant ID (shown in the dashboard under Settings → Organization, and returned by the GET /api/v1/me response as organizationId).
  • One message ID (msg_…) of a representative failed row.
  • The classified_error_code of that row.
Before the ticket, pull the row’s self-serve route-trace timeline — GET /api/v1/messages/:id/trace reads the failure end to end (validation gates, routing hop, provider submission, carrier receipt) and the failure path pins the stage (validation / submission / carrier) the send died on. The analytics endpoint’s top-N failure-reason breakdown is the fleet-level view support quotes back at you; run it for a narrow window when a chronic pattern is what you are reporting.

See also