Skip to main content

Order dispatch notifications workflow

A dispatch pipeline ships one transactional message per fulfillment stage — order confirmation, shipped, out for delivery, delivered, plus the exception branch. This guide builds that pipeline as one loop: emit the stage event, send a templated message with the stage tagged on it, and you can then read the whole lineage per order in the order-notifications ledger instead of joining delivery logs yourself. The ledger page is the operator’s reading surface; this page is the developer’s build recipe. The pairing below is the tested shape — your commerce system, a per-stage templated send, a delivery trail, and the alert rules that watch the series.

1. Model the fulfillment stages

Fix the stage vocabulary first, because every later join (ledger groups, alert rule filters) reads the same strings: Put every event exact names in one place (a constants module your emitters import) — the ledger groups the series by whatever string you stamp, and a order.shiping typo becomes a silent second series you cannot merge back. Two design rules on the stage list:
  • order.exception is one bucket. A delayed delivery and a failed attempt are both expected traffic. Stamp them all on order.exception and carry the sub-reason (delayed, failed_attempt, refunded) as your own metadata key — one stage to alert on, sub-reasons still recoverable per order.
  • Stages are event names, not message text. The body copy for each stage is a template you maintain separately; the stage tag is the join key.

2. Emit each stage event

Two emitters feed the same ledger — pick whichever owns the stage transition:
  • The commerce spine. If your storefront runs on the commerce checkout spine, its state machine already emits the cart/checkout transitions (paid, fulfilled), and your flow listens for the transition and fires the message below. The stage tag rides on the send either way.
  • Direct fulfillment hooks. Your own order-management system POSTs POST /api/v1/messages/sms (or the matching per-channel route) on each stage change. The rest of this guide assumes this shape; the tag/ledger/DLR steps are identical for spine emitters.
A spine event that reaches “fulfilment stage X” and a direct POST for the same stage tag produce the same ledger row. Do not wire both for one stage — a stage you emit twice reads as double volume on the series.

3. Send a templated message with the stage tagged

Branch the stage event into a templated send and stamp the stage plus your own correlation key on metadata. The ledger groups by the stage; your DLR consumer (step 5) joins on the order id. Both travel on the message body’s metadata map and echo back on the delivery webhooks:
Template the body per stage rather than concatenating in code: a shipped notification rendered from a template keeps the copy reviewable and keeps marketing language out (step 8). A 202 Accepted queues the send; the terminal delivered / failed state arrives on the webhook.

4. Choose the channel with SMS↔WhatsApp fallback

Order notifications are reachability traffic, not engagement traffic — a delivered confirmation matters more than the richer channel. The per-message cascade_policy on smart-send arms a fallback chain the terminal-DLR hook consumes, so an undelivered WhatsApp hop falls to SMS instead of dying:
Pick sms first where that is the recipient’s known channel, whatsapp first where you hold a WhatsApp opt-in — the resolver stamps the executable chain onto the message metadata either way, and the stage tag rides through every hop. The full rule set (channel eligibility, ordering, the auto_fallback: false opt-out) is on the fallback chains page.

5. Subscribe to delivery webhooks, join onto the stage badge

Subscribe to the message-event webhooks (message.sent, message.delivered, message.read where the channel returns it, message.failed) and verify signatures — the per-channel vocabulary and HMAC checks are on the delivery webhooks guide. On each event, your consumer:
  1. Reads the echoed metadata.stage + metadata.order_id.
  2. Marks that stage’s send as sent / delivered / read / failed on your order record.
  3. Accepts a correcting event after a terminal one — a WhatsApp undelivered that later delivers re-fires message.delivered.
The ledger upstream of you is already doing the same join — this step is for the badge on your order page, not for the ledger.

6. Read anomalies: missing stage series and failure spikes

Open Insights → Order notifications ledger with the per-stage grouping. Two anomaly shapes matter:
  • Missing stage series. A stage that silently stopped firing (confirmation volume drops to zero) renders as a missing row on its series, not a dip — a volume dip still shows the stage. Route your emitter health this way: a broken webhook subscription kills order.shipped while order.confirmation keeps flowing.
  • Failure spikes. Filter on status=failed (dispatch/configuration faults) versus status=undelivered (recipient-side hygiene) — the rejection-code breakdown tells you which one spiked. A sudden failed bump on one stage usually marks a template edit the channel rejected.
Both readings are tenant-owned filters on your own data.

7. Attach usage-anomaly alert rules

Guard the series itself, not just the aggregate. Wire org-scoped rules on the usage & delivery anomaly alert rules surface:
  • A delivery-rate rule (sms_delivery_rate, guards a drop) catches the carrier-side failure spike — with the stage tag on the ledger you can confirm the drop localizes to one stage before you dig.
  • A volume rule (outbound_message_volume, guards a surge) catches a duplicate-emitter regression — step 2’s “do not wire both” bug lands here.
For the missing-stage shape specifically, the ledger’s per-stage zero is the judgment an aggregate rule cannot express on its own: keep the stage-split check at the emitter (step 2), the alert rule as the backstop. Rules fire into the dashboard Notification Center; they never gate a send.

8. Keep transactional classification clean

A notification is transactional because its content is strictly about the order. The moment a stage send carries promotional copy (“while you’re here — 20% off accessories”), the carrier and content-moderation layers reclassify the flow as marketing, and it stops reading as transactional traffic in the ledger. Three rules keep the class clean:
  1. Template bodies reference only the order and its fulfillment state — no upsell, no cross-sell, no review requests.
  2. Keep each stage’s template scoped to that stage’s event — the exception branch alerts on the problem, it does not pitch a replacement product.
  3. Marketing goes on the campaign rails with their own quiet hours and unsubscribe handling — never appended to a stage send.

See also