Skip to main content

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

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.

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 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 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_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 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 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.
  • 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 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:
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.

See also