Skip to main content

Troubleshooting: message sent but no delivery receipt

Your message left the queue and the provider accepted it — the Delivery Log shows sent or submitted_no_receipt — but neither delivered nor undelivered ever lands. This page walks you through what that state means, how long each channel takes to return a delivery receipt (DLR), and how to tell a delayed receipt from a recipient who is genuinely unreachable. If the message has not left the queue at all (queued, scheduled, pending), work the queue-side troubleshooting page first — this page covers only the post-queue stage.

The status ladder: sent is not an outcome

Once a message reaches sent, the provider accepted it on the wire. From there the only forward moves are:
and the failure branches (undelivered, failed, rejected, expired). Two properties of this stage drive most confusion:
  • sent is intermediate, not an outcome. A delivery receipt must still come back from the carrier or provider. Until it does, the message has no final answer — every lifecycle webhook for this stage carries state_class: "intermediate" and is_terminal: false.
  • submitted_no_receipt is also intermediate. When no receipt arrives inside the per-channel grace window, Orbit promotes the row from sent to submitted_no_receipt. That is a sentinel meaning “submission accepted; no receipt yet — outcome unknown,” not a successful delivery and not a failure. A late receipt can still land and overwrite it: the row flips to delivered (or a failure status) and a fresh webhook fires. This is the submitted_no_receipt → later delivered reconciliation flow documented in the webhook events reference.
The full per-status semantics live in the message status lifecycle reference; the concept-level walkthrough is the delivery lifecycle page.
submitted_no_receipt arrives on the message.failed event type because there is no dedicated event for it — branch on the payload’s status field, not the event type, and keep it out of your hard-failure metrics. See expired and submitted_no_receipt (delivered via message.failed).

Where receipts get delayed per channel

A delayed receipt almost always means the receipt path is slow or dropped somewhere outside the platform. Where the delay sits differs by channel:
  • SMS (and MMS, voice, fax, RCS) — the carrier’s SMSC runs a store-and-forward protocol. Submission is acknowledged immediately, but the DLR travels back later: carriers batch receipts, and some routes only emit one when the handset moves or polls. The grace window is 30 minutes; a receipt can arrive long after that and still reconcile, up to the late-arrival window. Some destinations and routes never emit receipts at all.
  • WhatsApp — Meta pushes status callbacks (sentdeliveredread) to Orbit in near real time, but Meta-side delays, a recipient who blocked you, or a recipient with no active WhatsApp account can hold the delivered status indefinitely while the platform-side row stays at sent.
  • Email — the provider reports delivery and bounces asynchronously via webhooks. A recipient mail server can hold the message in queue and bounce hours later, so no bounce in the first minutes is not evidence of delivery.
  • Meta DM channels (Instagram, Messenger) — Meta’s Send API never emits delivery receipts, so on these channels the row always resolves to submitted_no_receipt after 5 minutes. For an opted-in recipient, Meta guarantees delivery on accept — treat this variant as functionally delivered. The message metadata carries no_dlr_channel: true so you can distinguish this case from the ambiguous SMPP one.
Receipts that arrive after the grace window still reconcile into the row and fire a webhook, up to the platform’s late-arrival window (DEVOTEL_DLR_LATE_ARRIVAL_MAX_AGE_DAYS). A receipt that lands beyond that window cannot be reconciled; the row resolves to expired. See the late-arrival notes in the webhook events reference for the reconciliation behaviour.

Distinguish “receipt missing” from “recipient unreachable”

The intermediate statuses tell you no receipt arrived. They do not, on their own, tell you whether the recipient got the message. Check these in order:
  1. Read the row by API. GET /api/v1/messages/:id returns the authoritative status. If it is still sent, the grace window has not closed yet; submitted_no_receipt means it has.
  2. Look for a later event. A subsequent message.delivered (or a message.failed with a terminal status like undelivered) supersedes the earlier submitted_no_receipt. If you mirror statuses into your own datastore, apply updates by message ID — a late receipt arriving after you logged the sentinel is normal.
  3. Check the Provider Reference column. In the Delivery Log, open the row and read the Provider Reference column. Populated means the provider handed back a wire-level ID for the submission. When you contact a carrier or escalate, that reference is what the provider traces against.
  4. Compare with the destination’s history. A high submitted_no_receipt share on SMS concentrated in one destination country or route points at a non-cooperating route; the same share spread across every destination points at a receipt-path problem.

Channel checklists

Work the list for the channel that produced the stuck row. SMS / MMS / RCS
  • Wait past the 30-minute grace window before diagnosing — the row moving to submitted_no_receipt inside that window is the normal sentinel, not a stalled send.
  • If the row flips to delivered minutes or hours later, that is a late-carrier receipt reconciling; treat the destination as slow-DLR, not broken.
  • If the same destination shows a persistent submitted_no_receipt rate, the carrier route is not returning receipts. Track it as a separate KPI from delivered and route through a different sender pool if delivery confirmation matters for the use case.
  • If many destinations all flip at once, suspect the receipt path (a provider edge or webhook interruption) rather than the carriers — escalate.
WhatsApp
  • Confirm the template you sent is still approved — Meta pauses templates on quality and policy status changes.
  • Check the recipient side: a blocked business number or a recipient without a WhatsApp account leaves the row without a delivered callback while Meta still accepts the send.
  • Watch message.failed for Meta’s error payload (error_code, error_message) — a bounced template or policy refusal surfaces there, not as a missing receipt.
Email
  • Allow hours, not minutes: recipient mail servers queue and can bounce well after acceptance. The bounced status arrives on the same asynchronous webhook path as delivery.
  • A sent email that later reports bounced is a recipient-side reject — handle it as a separate outcome from provider-side failures.
  • If no delivery and no bounce ever appears, verify your webhook endpoint subscription covers the email channel’s events before assuming the provider is at fault.

What not to do

Do not retry or resend while the row is at sent or submitted_no_receipt. Retry acts on terminal failures; a still-intermediate row can turn delivered after a late receipt, and a second copy you sent in the meantime reaches the recipient twice. Likewise, do not count submitted_no_receipt rows in your failure metrics — the outcome is unknown, and overwriting the tracker when the late receipt lands is expected behaviour.

When to escalate

Open a support ticket when one of these holds:
  • A row sits at submitted_no_receipt for more than 24 hours with no terminal event.
  • A wide spread of destinations (not one recipient or one route) stopped returning receipts at the same time.
  • You need the provider-side trace: include the Provider Reference from the Delivery Log row alongside the IDs below.
Include both of these so support can trace the row 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).
  • The message ID of one stuck row (msg_…).
That pair lets support pull the exact DLR pipeline trace for the row and the provider-side receipt state without re-describing the symptom.

See also