> ## Documentation Index
> Fetch the complete documentation index at: https://docs.orbit.devotel.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Troubleshooting: message undelivered or failed

> Read a terminal failure end to end — undelivered vs. failed vs. rejected vs. bounced, the classified error code and the carrier's raw error fields on the message.failed webhook, per-status cause decoders, and cause-to-fix mapping before you retry.

# 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](/reference/troubleshooting)
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](/troubleshooting/submitted-no-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.

| Status        | Who failed                         | What it means                                                                                                                                                                                                                                                                                                    |
| ------------- | ---------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `undelivered` | Destination side                   | The carrier tried to reach the recipient and could not — handset unreachable, invalid or deactivated number, carrier spam filter. The provider accepted and submitted the message; the failure came back from the carrier's delivery attempt.                                                                    |
| `failed`      | Send path                          | The send erred on the way to the provider — a provider-side error, a transient upstream failure, or a send-path exception. The message never reached, or never surmounted, the carrier's delivery attempt.                                                                                                       |
| `rejected`    | Content/sender side                | The provider or carrier refused the send outright — content policy, unregistered or unapproved sender-ID, 10DLC registration missing, or trust-score gate. Nothing was transmitted.                                                                                                                              |
| `bounced`     | Recipient mail server (email only) | The receiving mail server rejected the message — bad mailbox, full mailbox, domain policy. Counts like `failed` but is a distinct status so you can separate recipient-side rejects from provider-side ones. See the [email caveat in the delivery lifecycle](/concepts/delivery-lifecycle#per-channel-caveats). |
| `expired`     | Receipt path                       | A delivery receipt arrived after the late-arrival window closed, so the outcome (delivered or failed) is unknowable. It fans out on the `message.failed` webhook event; treat it as a closed row with an unknown outcome, not as a carrier failure.                                                              |

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](/guides/delivery-log).
* `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.

```json theme={null}
{
  "type": "message.failed",
  "data": {
    "message_id": "msg_01ABCDEF",
    "status": "undelivered",
    "state_class": "terminal",
    "is_terminal": true,
    "channel": "sms",
    "timestamp": "2026-08-20T12:00:05Z",
    "error_code": "30003",
    "error_message": "Unreachable destination handset"
  }
}
```

The per-status decoder:

| Status        | Common cause                                                                                                                                                            | Decoder hint                                                                                                                                             |
| ------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `undelivered` | Handset unreachable (off, roaming with no data, powered down long-term), invalid or deactivated number, destination carrier spam filter, routing gap on the destination | Check `classified_error_code` for the normalized cause; check the raw `error_code`/`error_message` for the carrier's own wording when you open a ticket. |
| `failed`      | Provider-side error, transient upstream outage, send-path timeout                                                                                                       | Usually transient; the raw carrier code tells you whether to route differently. Check the analytics failure-reason breakdown before re-sending at scale. |
| `rejected`    | Content violation, unregistered sender-ID, missing 10DLC campaign, trust-score gate, unapproved WhatsApp template                                                       | Deterministic — the same send fails the same way until the gate clears. Check sender registration status first.                                          |
| `expired`     | DLR arrived past the late-arrival window                                                                                                                                | Not a carrier failure — the outcome is unknowable. The underlying message may have delivered; resend only if you actually need the content delivered.    |

## Cause → fix

Match the cause class from the table above to its fix:

| Cause class                                                                                | Fix                                                                                                                                                                                                                                                                                                                           |
| ------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Handset unreachable / invalid number (`undelivered`)                                       | Run a [Number Lookup](/numbers/lookup) / HLR query against the destination — it reports carrier, line type, portability, SIM-swap, roaming, and live-reachability, so you can tell a dead number from a live one. Drop dead numbers from the pool with an opt-out or suppression flag; do not re-send to them.                |
| Send-path error (`failed`)                                                                 | Check the [deliverability analytics](/api-reference/endpoints/analytics) top-N failure-reason breakdown for a systematic pattern (one carrier, one country) before re-sending. Transient failures resolve on their own.                                                                                                       |
| Rejected send (`rejected`)                                                                 | Confirm sender registration (10DLC campaign approved in the US, sender-ID approved in countries that gate it) before you re-send. Fix the content policy violation if the carrier returned one — see the [compliance troubleshooting page](/compliance/troubleshooting-pending-gated-surfaces) for sender-registration gates. |
| Destination blocked by opt-out / suppression (any terminal failure)                        | When `classified_error_code` implies blocking or the row carries a suppression flag, check the recipient's opt-out state before re-sending. See [Opt-out suppression](/compliance/opt-out-suppression) for the receipt/ingestion model.                                                                                       |
| Spam filtering on a licensed route (`undelivered`) concentrated on one destination/carrier | Route around the carrier-filtering path — move the affected destinations to a different sender pool or licensed route. Concentrated filtering on one route is the signal to route, not to retry.                                                                                                                              |
| `expired` after a slow DLR                                                                 | Clear the hold (validity window, quiet hours, balance) and re-send from the queue. The `expired` row itself is closed.                                                                                                                                                                                                        |

## 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](/reference/troubleshooting) 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](/api-reference/endpoints/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.

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

* [Troubleshooting: message stuck in queued](/reference/troubleshooting)
  — the pre-queue stage, and the same retry-discipline rule
* [Troubleshooting: message sent but no delivery receipt](/troubleshooting/submitted-no-receipt)
  — the intermediate-state sibling, for rows holding at `sent` / `submitted_no_receipt`
* [Delivery lifecycle](/concepts/delivery-lifecycle) — the concept-level
  state machine and the `metadata.classified_error_code` branch point
* [Message status lifecycle](/api-reference/messages-status-lifecycle) —
  per-status semantics and the full transition table
* [Webhook events reference](/reference/webhook-events) — the
  `message.failed` payload (`error_code`, `error_message`, `is_terminal`)
  and the reconciliation behaviour for late-arriving DLRs
* [Number Lookup](/numbers/lookup) — validate and enrich a destination
  before re-sending
* [Delivery log](/guides/delivery-log) — the dashboard surface behind
  the `status=undelivered` filter
