> ## Documentation Index
> Fetch the complete documentation index at: https://docs.orbit.devotel.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Order dispatch notifications workflow: stage events → templated sends → ledger lineage

> Build a fulfillment notification pipeline end to end: model the fulfillment stages, emit each stage event as a templated transactional send tagged for the ledger, arm an SMS↔WhatsApp fallback, correlate delivery webhooks onto the stage badge, and attach anomaly rules so a missing stage or a failure spike pages you.

# 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](/guides/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:

| Stage value              | When it fires                                 |
| ------------------------ | --------------------------------------------- |
| `order.confirmation`     | Order placed and accepted                     |
| `order.shipped`          | Carrier handoff confirmed                     |
| `order.out-for-delivery` | Final-mile leg started                        |
| `order.delivered`        | Terminal success                              |
| `order.exception`        | Delayed, failed attempt, refund, cancellation |

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](/concepts/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:

```bash theme={null}
curl -X POST https://api.orbit.devotel.io/api/v1/messages/sms \
  -H "X-API-Key: dv_live_sk_..." \
  -H "Content-Type: application/json" \
  -d '{
    "to": "+14155552671",
    "body": "Order #8421 shipped — track your package.",
    "metadata": {
      "stage": "order.shipped",
      "order_id": "8421"
    }
  }'
```

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](/guides/smart-send-fallback-chains) arms a fallback chain the terminal-DLR hook consumes, so an undelivered WhatsApp hop falls to SMS instead of dying:

```bash theme={null}
curl -X POST https://api.orbit.devotel.io/api/v1/messages/smart-send \
  -H "X-API-Key: dv_live_sk_..." \
  -H "Content-Type: application/json" \
  -d '{
    "to": "+14155552671",
    "body": "Order #8421 shipped — track your package.",
    "message_type": "transactional",
    "cascade_policy": {
      "fallback_channels": ["whatsapp", "sms", "email"]
    },
    "metadata": {
      "stage": "order.shipped",
      "order_id": "8421"
    }
  }'
```

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](/guides/fallback-chains).

## 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](/guides/wire-dlr-webhooks-per-channel). 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](/guides/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](/guides/usage-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

* [Order notifications ledger](/guides/order-notifications-ledger) — the reading surface this pipeline feeds
* [Fallback chains](/guides/fallback-chains) — all three fallback surfaces and when to use each
* [Smart-send cascade policy](/guides/smart-send-fallback-chains) — the per-message chain the recipe above arms
* [Wire delivery webhooks per channel](/guides/wire-dlr-webhooks-per-channel) — the event vocabulary, signatures, and sandbox tester
* [Usage & delivery anomaly alert rules](/guides/usage-anomaly-alert-rules) — the org-scoped rules you attach in step 7
* [Commerce checkout spine](/concepts/commerce-checkout-spine) — the alternative emitter for the same ledger
