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

# Where to look first when a message send fails

> A triage order for failed message sends across the four developer observability surfaces: Request Logs for the HTTP shape, Delivery Logs for the message lifecycle, API analytics for aggregates, and webhook deliveries for the event bus, with a symptom-to-pane decision tree, per-channel gotchas, and what to capture before you escalate.

# Where to look first when a message send fails

A failed send leaves traces in up to four places, and each surface answers a different question. This guide gives the debugging order: which pane to open first for each failure shape, the filter that isolates it, what a healthy row looks like, and how to carry an id from one surface to the next. The mechanics of each pane stay on their own pages: [Request Logs](/guides/api-request-logs-console), [Delivery Logs](/guides/developer-delivery-logs), [API analytics](/guides/api-analytics-reader), and [webhook endpoint operations](/guides/webhook-endpoint-operations).

The voice equivalent of this guide is [Where to look first when an outbound call fails](/guides/voice-call-failure-triage).

## The four failure surfaces in one diagram

```text theme={null}
your send
  │
  ├─ the HTTP call itself errored (4xx / 5xx)
  │     → Request Logs          one row per API call, full request + response bodies
  │
  ├─ 202 Accepted, but the message never reached a terminal state
  │     → Delivery Logs         one row per message, lifecycle steps, carrier reasons
  │
  ├─ a window of traffic looks wrong, not one message
  │     → API analytics         per-endpoint / per-key aggregates, latency-spike alerts
  │
  └─ the message changed state, but your system never reacted
        → Settings → Webhooks   delivery attempts, retry history, replay
```

The split between the first two is the 202. A `4xx` in Request Logs means the send never became a message, so Delivery Logs has nothing to show for it; the fix lives in the request shape or the tenant setting the response body names. A `202 Accepted` with a stuck or failed row means the API took the send and the lifecycle stopped downstream, which is Delivery Logs territory. API analytics never shows one message; it tells you which endpoint, key, or hour to zoom into before you drop back to the per-message surfaces. Webhook deliveries are the fourth surface because the message can be perfectly delivered and your consumer can still have missed the event.

| Surface | Console route | Answers | Granularity |
| - | - | - | - |
| Request Logs | Developer → Request Logs | Did my HTTP request arrive in a valid shape? | One API call, full payloads |
| Delivery Logs | Developer → Delivery Logs | What happened to this message downstream? | One message, lifecycle steps |
| API analytics | Developer → API analytics | Which endpoint, key, or hour is misbehaving? | Hourly or daily aggregates |
| Webhook deliveries | Settings → Webhooks (endpoint detail) | Did my consumer receive each event? | One delivery attempt |

## The triage decision tree

| Symptom | First pane | Filter to apply | What "good" looks like |
| - | - | - | - |
| Send returned 4xx | Request Logs | API key prefix + status `4xx` + path `/messages` | The same filter on a healthy send shows a green `202` whose response body carries a `msg_…` id. |
| Send returned 202 but the message never left `queued` | Delivery Logs | Paste the `msg_…` id in the search box | The drawer's steps move past Accepted to Routed and Submitted within seconds of `created_at`, then reach a terminal stamp. |
| Message shows `sent` but the recipient never saw it | Delivery Logs | Channel + status `failed` or `rejected`, or the recipient in free text | A terminal `delivered` / `read` with its timestamp; when a carrier refuses, the terminal step carries the reason instead of silence. |
| Burst of failures at a time of day | API analytics | Timeframe = the burst window, **By endpoint** sorted by errors | Per-endpoint error counts sit flat against the cohort and no latency-spike alert card fires for the window. |
| Webhook consumer silently missed events | Settings → Webhooks (endpoint detail) | Deliveries table → **Errors only** / **Retrying now** | The health panel reports the last 24 hours of deliveries as succeeding and the Deliveries table shows `2xx` attempts. |

### (a) Send returned 4xx: Request Logs

Open Developer → Request Logs, set the API key filter to the integration's prefix (for example `dv_live_sk_…`), status to `4xx`, and path to `/messages`. The table narrows to amber rows; click one. The drawer renders the response body, and the `error` envelope is the whole diagnosis: `error.code` is the machine-readable string the [Error Code Reference](/reference/error-codes) indexes on, `error.details` names the field or tenant setting that failed, and `meta.docs_url` points at the per-code page. Do not open Delivery Logs for this send; a 4xx never created a message row, and the empty result there is the expected outcome, not a second clue.

### (b) 202 but stuck in `queued`: Delivery Logs by id

Copy the `msg_…` id from the 202 response body and paste it into the Delivery Logs search box. The *Message id match* strip pins exactly one row regardless of the date filter. Open it: the drawer lists the lifecycle steps in order with a timestamp each. A row whose steps stop after Accepted never reached routing; read `created_at` against the clock. If the send is minutes old, refresh; the lifecycle can still be running. If it is hours old with no Routed step, the send stalled at the platform layer; capture it per [When to escalate](#when-to-escalate).

### (c) `sent` but never seen by the recipient: terminal state + carrier reason

In Delivery Logs, filter by channel and status `failed` or `rejected`, or put the recipient in the free-text box. Click the row: the terminal step carries `error_code` and `error_message` whenever the carrier returned a reason, for example `CARRIER_UNREACHABLE` / "Subscriber unreachable". A `submitted` row that never terminates is the other shape; its failed step names the carrier-side reason when one came back. What a terminal state means differs per channel (SMS delivery receipts, WhatsApp reads, email bounces), so once you have the terminal step, read the per-channel DLR semantics in the [DLR webhook consumer guide](/guides/dlr-webhook-consumer) before you decide whether the reject is retryable.

### (d) Burst of failures at a time of day: API analytics, then pivot

Open Developer → API analytics and set the timeframe to the burst window. The **By endpoint** tab sorted by errors shows which route drove the spike, and a latency-spike alert card flags any endpoint whose p95 left the cohort. Click **View request logs** on the offending row: the console opens Developer → Request Logs already scoped to that method and path prefix, so you are reading the individual failing requests instead of the aggregate. Copy the request ids of the failing rows; they are the pivot from "the window looked bad" to "these exact sends failed, here is why."

### (e) Webhook consumer silently missed events: health panel + replay

Open Settings → Webhooks, open the endpoint, and read the health panel at the top: it aggregates the last 24 hours of real deliveries. Below it, filter the Deliveries table to **Errors only** or **Retrying now**, click a failed row, and read your endpoint's actual response per attempt; the retry history shows how each attempt died. Replay one row with **Replay**, or a whole outage window with **Replay deliveries by range**; the cross-endpoint view lives on Developer → Webhooks → Events. The full operational mechanics, including the replay API, are in [Operate a webhook endpoint](/guides/webhook-endpoint-operations).

## Per-channel gotchas that change the pane you open first

* **WhatsApp template rejection.** Two distinct shapes. A pre-send refusal (422 `TEMPLATE_NOT_APPROVED` while the template awaits provider approval, 400 `WHATSAPP_TEMPLATE_VAR_COUNT_MISMATCH` when variable counts disagree) is a 4xx at send time: Request Logs is the pane, and no Delivery Logs row exists for the attempt. An async Meta rejection after a 202 is the opposite: Request Logs shows a healthy 202 and the Delivery Logs row terminates at `rejected` with Meta's reason on the terminal step. Fix the template or the variable shape, then resend from your normal send path.
* **SMS carrier filtering.** Filtering happens downstream of the 202, so it never shows in Request Logs. The Delivery Logs terminal step names the reason when the carrier returns one; silent filtering shows as `failed` or as submitted-no-receipt with no reason at all. When the reason is absent, score the body before your next send with the [Deliverability Lab](/guides/campaign-deliverability-lab) and check your registration posture in the [10DLC registration guide](/guides/10dlc-registration); unregistered high-volume traffic is filtered by design.
* **RCS fallback to SMS.** When a smart-send arms a fallback chain, an undelivered RCS primary spawns a second send on the fallback channel. Delivery Logs then shows two rows to the same recipient: the `rcs` row terminal-failed and an `sms` row behind it. Read the pair before you call the send a failure; the cascade working as designed is a red row followed by a delivered one. The chain mechanics are in [smart-send fallback chains](/guides/smart-send-fallback-chains).
* **Email DNS absent.** Sends from a domain without SPF, DKIM, and DMARC either fail at the domain gate or bounce after submission; retrying does not help because the fix is DNS records, not the payload. The Delivery Logs reject names the bounce class; the domain row in the console lists the exact missing records, and [email DNS auto-configuration](/guides/email-cloudflare-dns-auto-configure) can create them for Cloudflare-hosted zones.

## Worked examples

### One SMS stuck in `queued`

The send:

```bash theme={null}
curl -X POST "https://api.orbit.devotel.io/api/v1/messages/sms" \
  -H "X-API-Key: dv_live_sk_your_key_here" \
  -H "Content-Type: application/json" \
  -d '{
    "to": "+14155552671",
    "from": "+14155550100",
    "body": "Your Devotel Orbit verification code is 493021."
  }'
```

```json theme={null}
{
  "data": { "id": "msg_2c4d6e8f0a1b3c5d", "status": "queued" },
  "meta": { "request_id": "req_7b3f9a21" }
}
```

The console walkthrough:

1. The 202 rules Request Logs out; open Developer → Delivery Logs instead and paste `msg_2c4d6e8f0a1b3c5d` into the search box. The *Message id match* strip pins the one row: channel `sms`, direction `outbound`, status `queued`.
2. Click the row. The drawer shows the Accepted step at `created_at` and stops: no Routed step, no Submitted step, no error on any step.
3. Compare `created_at` to now. Minutes old means the lifecycle can still be running; refresh before concluding anything. Hours old with no routing step means the send stalled between acceptance and routing.
4. Confirm from a script with the same vocabulary the page uses: `GET /api/v1/messages?message_ids=msg_2c4d6e8f0a1b3c5d` returns the row with `status: "queued"`, a `created_at`, and no `sent_at`.
5. Capture `msg_2c4d6e8f0a1b3c5d`, `req_7b3f9a21`, and `created_at`, and escalate per the next section.

### One WhatsApp template rejection

The send:

```bash theme={null}
curl -X POST "https://api.orbit.devotel.io/api/v1/messages/whatsapp" \
  -H "X-API-Key: dv_live_sk_your_key_here" \
  -H "Content-Type: application/json" \
  -d '{
    "to": "+4915123456789",
    "template_name": "order_shipped_v2",
    "template_params": ["{{1}}", "{{2}}", "{{3}}"]
  }'
```

```json theme={null}
{
  "error": {
    "code": "WHATSAPP_TEMPLATE_VAR_COUNT_MISMATCH",
    "details": "template declares 2 positional placeholders; request supplied 3 template_params",
    "docs_url": "/errors/WHATSAPP_TEMPLATE_VAR_COUNT_MISMATCH"
  }
}
```

The console walkthrough:

1. Open Developer → Request Logs, filter to your key prefix, status `4xx`, path `/messages/whatsapp`. The row is there in amber with a `400`.
2. Open the row and read the response body: `error.code` names the refusal, `error.details` says the template declares two placeholders and the request sent three. That is the whole fix; Delivery Logs has no row for this attempt because the 4xx never created a message.
3. Contrast with the async shape: had the same request returned 202 and Meta later refused the template, Request Logs would show a green 202 and Delivery Logs (channel `whatsapp`, status `rejected`) would carry Meta's reason on the terminal step. Same operator question, different pane.
4. Fix the variable count (or wait out template approval for `TEMPLATE_NOT_APPROVED`), resend, and confirm the new attempt reaches a terminal `delivered` in Delivery Logs.

## When to escalate

Most failures resolve in-console: a payload fix from `error.details`, a template fix, a DNS record, a replay. Escalate to Devotel Orbit support when:

* A row sits non-terminal past the delivery window with no routing or submission step.
* A carrier-rejection cluster persists after you rotate sender, template, or body, and the terminal-step reasons point at carrier-side blocks.
* Request Logs shows 5xx rows from Orbit itself; capture several request ids so support can pull the traces.
* Replays succeed but deliveries fail again identically, which puts the fault at your consumer rather than the bus; capture one event id and the attempt history anyway, because that contrast is the diagnosis.

Open the ticket from **Support → Timeline** with:

* The request id (`req_…`) from the Request Logs drawer or the API response's `meta.request_id`.
* The message id (`msg_…`) from the send response or the Delivery Logs row.
* Exact timestamps: `created_at`, `sent_at`, and the terminal stamp, as displayed.
* The channel and direction, plus `error_code` and `error_message` from the terminal step when present.
* The API key prefix that authenticated the send, and the recipient as shown on the row.

For day-to-day workflow guidance on Support → Timeline, see the [support timeline operator guide](/guides/support-timeline-operator-guide).

## See also

* [Delivery Logs console](/guides/developer-delivery-logs) — the per-message lifecycle, filters, and shareable URLs
* [Request Logs console](/guides/api-request-logs-console) — per-call payloads, filters, and the live tail
* [API analytics dashboards](/guides/api-analytics-reader) — the aggregate view and the alert-to-request drill
* [Operate a webhook endpoint](/guides/webhook-endpoint-operations) — health panel, retries, and replay
* [DLR webhook consumer guide](/guides/dlr-webhook-consumer) — per-channel delivery-receipt semantics
* [Where to look first when an outbound call fails](/guides/voice-call-failure-triage) — the sister guide for voice


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.