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, Delivery Logs, API analytics, and webhook endpoint operations. The voice equivalent of this guide is Where to look first when an outbound call fails.The four failure surfaces in one diagram
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.
The triage decision tree
(a) Send returned 4xx: Request Logs
Open Developer → Request Logs, set the API key filter to the integration’s prefix (for exampledv_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 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.
(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 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.Per-channel gotchas that change the pane you open first
- WhatsApp template rejection. Two distinct shapes. A pre-send refusal (422
TEMPLATE_NOT_APPROVEDwhile the template awaits provider approval, 400WHATSAPP_TEMPLATE_VAR_COUNT_MISMATCHwhen 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 atrejectedwith 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
failedor 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 and check your registration posture in the 10DLC registration guide; 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
rcsrow terminal-failed and ansmsrow 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. - 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 can create them for Cloudflare-hosted zones.
Worked examples
One SMS stuck in queued
The send:
- The 202 rules Request Logs out; open Developer → Delivery Logs instead and paste
msg_2c4d6e8f0a1b3c5dinto the search box. The Message id match strip pins the one row: channelsms, directionoutbound, statusqueued. - Click the row. The drawer shows the Accepted step at
created_atand stops: no Routed step, no Submitted step, no error on any step. - Compare
created_atto 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. - Confirm from a script with the same vocabulary the page uses:
GET /api/v1/messages?message_ids=msg_2c4d6e8f0a1b3c5dreturns the row withstatus: "queued", acreated_at, and nosent_at. - Capture
msg_2c4d6e8f0a1b3c5d,req_7b3f9a21, andcreated_at, and escalate per the next section.
One WhatsApp template rejection
The send:- Open Developer → Request Logs, filter to your key prefix, status
4xx, path/messages/whatsapp. The row is there in amber with a400. - Open the row and read the response body:
error.codenames the refusal,error.detailssays 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. - 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, statusrejected) would carry Meta’s reason on the terminal step. Same operator question, different pane. - Fix the variable count (or wait out template approval for
TEMPLATE_NOT_APPROVED), resend, and confirm the new attempt reaches a terminaldeliveredin Delivery Logs.
When to escalate
Most failures resolve in-console: a payload fix fromerror.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.
- The request id (
req_…) from the Request Logs drawer or the API response’smeta.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_codeanderror_messagefrom the terminal step when present. - The API key prefix that authenticated the send, and the recipient as shown on the row.
See also
- Delivery Logs console — the per-message lifecycle, filters, and shareable URLs
- Request Logs console — per-call payloads, filters, and the live tail
- API analytics dashboards — the aggregate view and the alert-to-request drill
- Operate a webhook endpoint — health panel, retries, and replay
- DLR webhook consumer guide — per-channel delivery-receipt semantics
- Where to look first when an outbound call fails — the sister guide for voice