Skip to main content

Wire delivery-report webhooks per channel, end to end

You send on SMS, WhatsApp, RCS, email, or voice and you want delivery outcomes pushed to your server instead of polling the Delivery Log. This tutorial closes the loop in one pass: pick the right events per channel, subscribe, verify signatures, rehearse in the sandbox before registering, and debug a misbehaving delivery by request id. Pages that overlap this topic each answer a narrower question — the event catalog is for browsing payload schemas and pinning them in CI, the durable consumer guide is endpoint hardening generally, and DLR outcomes monitoring is the WARN-log classification readout. This page is the wiring tutorial that connects them.

1. Event vocabulary per channel

Four message events carry the outbound lifecycle on every messaging channel, plus the call family for voice: Subscribe to all four message events even if you only care about failures: message.sent is how you distinguish “Orbit never accepted the send” from “the channel lost it downstream,” and message.delivered lets you reconcile that message.failed genuinely means terminal.

The changed semantics you must code against

The event names are stable; what each means per channel is not. These are the rows that change how you wire:
  • Meta DM (Messenger, Instagram): no DLR exists. Meta’s Send API never emits delivery receipts, so a DM send can only produce message.sent followed by either nothing or a safety-net message.failed reading submitted_no_receipt after a 5-minute window. Treat message.sent as fully accepted on these channels and do not alert on the missing message.delivered — it will never come. See DLR outcomes monitoring for the designed-benign classification.
  • Viber and WhatsApp: a 24-hour DLR window. A send that receives no receipt inside 24 hours ages to undelivered and fires message.failed — a late genuine receipt still corrects the row and re-fires message.delivered (or message.read). Your consumer must accept a correcting event after a terminal one.
  • SMS on SMPP-backed routes: a 30-minute window. A healthy carrier returns receipts well inside 30 minutes. A send with no receipt inside that window ages to undelivered and fires message.failed; a late receipt corrects it. A sustained no-receipt rate on this lane is a carrier problem, not a platform problem.
  • Email semantics are mailbox-acceptance. message.delivered means the receiving server accepted the message — it is not an open or a click. Pair it with the engagement events (email.opened, email.clicked) if you need recipient interaction.
  • Voice has no delivery concept. call.answered is the voice equivalent of delivered; call.failed covers busy, no-answer, and carrier failure. Subscribe to the full call lifecycle instead of a single event.
Events are at-least-once and can arrive out of order across messages. Dedupe on the envelope id, and key each event to the message by data.message_id — never by arrival order.

2. Subscribe

In the dashboard

Open Developer → Webhooks, click Add endpoint, paste your HTTPS URL, and tick the events from the table above. The dashboard shows the signing secret once — copy it now; it is the value whsec_... your verifier needs in Step 3.

Over the API

POST /api/v1/webhooks/endpoints with the event list per channel you just mapped:
The response carries the endpoint id and the signing secret (shown only here). Subscribing to a name outside the catalog returns 422 VALIDATION_ERROR — resolve event names against the events catalog first. One endpoint can serve several channels; the data.channel field on every message event tells you which channel the receipt came from, so a single subscription covers SMS, WhatsApp, RCS, email, and voice if your handler branches on data.channel.

3. Verify the signature

Every registered delivery carries X-Orbit-Signature: t=<unix_ts>,v1=<hex> — an HMAC-SHA256 of <t>.<raw_body> under your endpoint secret. Verify before parsing, answer 401 on failure, and never ack an unverified payload with 200. The full protocol lives at Webhook security.

Node.js (standard library)

Python (standard library)

Prefer the SDK one-call verifier per language — complete framework handlers for Node, Python, Go, PHP, Ruby, Java, and C# are on Verify webhook signatures, and the standard- library equivalents are on Verify without the SDK. During a secret rotation both X-Orbit-Signature (current secret) and X-Orbit-Signature-Next (previous secret) arrive until the 7-day grace window ends.

4. Sandbox rehearsal — tester first, then a real subscription

Do not register a production endpoint against an untested receiver. Two rehearsal steps, both without a carrier: Step A — fire the Webhook Tester. Open Developer → Webhook Tester, paste your URL, pick message.delivered, and send. The tester delivers through the real dispatcher but unsigned — a receiver that verifies correctly answers 401/403 to it. Send tester events to confirm your route exists, your TLS is valid, and your handler parses the envelope; expect signature verification to reject them, per the Webhook Tester checklist. Step B — register with a test key and replay a delivery. Mint a sandbox key (dv_test_sk_), register the endpoint over the API with that key, and drive a magic-number send. The trailing digit picks the outcome; trailing digit 3 returns message.failed (the stored status is undelivered, a handset reject):
Your endpoint receives message.sent immediately, then message.failed about a second later — the same sequence a carrier produces on a handset reject. Verify the signature against the test-endpoint secret, confirm your failure branch ran, then walk the other digits (2 delivered, 4 pre-submit failure with no sent ack, 9 delayed delivery) before you swap in a live key. The full digit table is the sandbox magic numbers playbook.
Sandbox receipts exist only on the webhook stream — the stored message keeps its test status. If your consumer reads the Delivery Log instead of trusting the stream, rehearsal and production behave differently.

5. Debug a delivery

When an expected event never lands, or lands and your handler rejects it, correlate on two handles:
  1. meta.request_id on the API response. Every send response carries it. Open Developer → Request Logs, find the request by id, and confirm Orbit accepted the send — a 422 or 502 at the request layer means no DLR events will ever fire.
  2. data.message_id (and data.to, the recipient id) on the event envelope. Open the Delivery Log filtered by the message id. The row’s terminal status is the ground truth; the webhook event you expected is downstream of the same transition the row shows. Check Developer → Webhooks → your endpoint → Deliveries for the dispatch record — a failing endpoint shows its failed attempts and the retry timeline there.
The split is quick: Request Logs answer “did the send get in,” the Delivery Log answers “what outcome did the channel report,” and the endpoint Deliveries panel answers “did the webhook leave and what did your server return.” Work them in that order and every missing event resolves to one of the three.

Compliance

Everything on this page is a tenant-owned control: which events you subscribe to, which URL receives them, and how your consumer reacts to each outcome. The channel windows (Meta DM no-DLR, the Viber/WhatsApp 24-hour window, the SMPP 30-minute window) are platform safety nets you read, not timers you set. Outbound delivery stays within the Devotel softswitch contract described in DLR and MO gateway pipeline — subscribing to DLR events sends no traffic anywhere new.