> ## 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.

# Appointment reminders end to end: schedule, confirm, fall back, and verify

> The complete appointment-reminder workflow in one walkthrough — pick a transactional template, schedule the send with scheduled_at, route YES/NO replies into a flow, opt out of the Inbox, consume signed DLR webhooks, add an SMS→WhatsApp fallback hop, and read the outcome in the ledger.

# Appointment reminders end to end

An appointment reminder is the archetype of transactional messaging: a one-to-one send whose timing, template, and reply handling all matter, and whose consent came from the appointment itself — not from a campaign. The pieces each have their own page. This guide stitches them into one workflow: schedule → template → reply trigger → DLR webhook → fallback → ledger verification.

Everything below is tenant-owned configuration. The reminder's timing gates, its fallback chain, and its consent posture are controls you set; the platform applies them and never mandates a regulatory gate on your behalf.

## 0. The shape of the flow

1. **Class and template** — mark the send `category: "transactional"` so campaign-side controls (frequency caps that list only `marketing`, campaign quiet-hours fallbacks) leave it alone by design.
2. **Schedule** — `POST /api/v1/messages/sms` with `scheduled_at` and the appointment-relative fire time. The message parks until the platform scheduler dispatches it.
3. **Reply handling** — a keyword rule on `YES` / `NO` (or a flow trigger) turns the recipient's confirmation into a booking update, no agent required.
4. **No Inbox noise** — the transactional no-inbox gate keeps the reminder from minting a conversation per destination.
5. **DLR webhooks** — `message.delivered` / `message.failed` events, signature-verified, drive your "reminder landed" state machine.
6. **Fallback** — an SMS → WhatsApp hop (per-message cascade or campaign ladder) raises the delivery floor.
7. **Timing gates** — quiet hours and frequency caps interact with the scheduled fire time; know which ones touch transactional traffic and which do not.
8. **Verification** — the order-notifications ledger reads the reminder class's delivery rate, time-to-first-attempt, and failure breakdown.

## 1. Pick the transactional class and template

Mark the message `category: "transactional"` and build its body from a template whose variables are appointment fields — recipient name, date, time, location, reply instruction.

```json theme={null}
{
  "to": "+14155552671",
  "body": "Hi Sarah, reminder: dental appointment tomorrow at 10:15 with Dr. Patel. Reply YES to confirm or NO to reschedule.",
  "category": "transactional"
}
```

Why the category matters:

* **Frequency caps** — a cap whose `applies_to_categories` lists only `["marketing"]` never gates this send. Without the category stamp, a blanket (keyless) cap can reject a reminder that collided with the same contact's marketing receipts. See [Frequency caps](/guides/frequency-caps).
* **Campaign-side windows** — campaign quiet-hours fallbacks and send-window optimization exist for bulk traffic; the transactional stamp keeps the reminder out of that classification path entirely.
* **Ledger attribution** — the [order-notifications ledger](/guides/order-notifications-ledger) groups machine-triggered transactional traffic separately from campaign reports; marking the class correctly is what makes section 8's readback coherent.

For WhatsApp, use an approved template (a MARKETING-category template will pass the window check as well; a UTILITY or AUTHENTICATION template is the cleaner match for the class). For SMS, the `category` field on the send body is the stamp.

## 2. Schedule the reminder with `scheduled_at`

Pass the appointment-relative fire time on the send endpoint. The API answers `202` and the message parks in `scheduled` until the platform scheduler promotes it — see [Schedule one-off sends with `scheduled_at`](/guides/message-scheduling) for the full surface (edit, cancel, inspect).

```bash theme={null}
REMINDER_AT=$(date -u -d '2026-10-06 10:15:00 America/New_York - 24 hours' +%Y-%m-%dT%H:%M:%SZ)

curl -X POST https://api.orbit.devotel.io/api/v1/messages/sms \
  -H "X-API-Key: dv_live_sk_your_key_here" \
  -H "Content-Type: application/json" \
  -d '{
    "to": "+14155552671",
    "body": "Hi Sarah, reminder: dental appointment tomorrow at 10:15 with Dr. Patel. Reply YES to confirm or NO to reschedule.",
    "category": "transactional",
    "scheduled_at": "2026-10-05T14:15:00Z"
  }'
```

Compute the fire time in your booking system at booking time (appointment minus 24 h, minus 1 h, or whatever your cadence is) and send an explicit `Z` or offset — never an offset-less `datetime-local` string, which reinterpreted as UTC shifts the fire time. Until the row promotes to `queued` you can `PATCH /api/v1/messages/{id}` (appointment moved → new `scheduled_at`) or `POST /api/v1/messages/{id}/cancel` (appointment cancelled) — both free, both gated on the row still being scheduled.

### Do you want `send_at_type`?

`send_at_type: "optimize_with_send_window"` treats `scheduled_at` as the earliest acceptable time and shifts out-of-window sends to the next window open. For reminders, **use `fixed` (the default)**: a reminder whose value depends on landing 24 h before the appointment must not slide to the next morning because a send-window knob moved it. The optimization mode is the right choice for compliance-sensitive marketing traffic, not for this workflow. See [Send-window optimization with `send_at_type`](/guides/message-scheduling-send-window-optimization).

## 3. Route confirmation replies into a flow

The reminder invites `YES` / `NO`. Handle those replies without opening an agent thread: [keyword auto-reply rules](/guides/keyword-auto-reply-rules) watch the inbound and execute the moment one matches — send the acknowledgement and trigger a flow that updates the booking.

* **`YES` (exact match)** — reply `Confirmed — see you tomorrow at 10:15.` and trigger a confirmation flow that writes the status back to your booking system.
* **`NO` (exact match)** — reply a reschedule link or trigger a reschedule flow that offers new slots.

Two details from the keyword-rules surface that matter here: an **exact** match confines the rule to a bare `YES`/`NO` (any letter case), and the catch-all — a regex `.*` rule created **last** — only fires when nothing earlier claimed the reply, so a recipient who answers with a sentence instead of the keyword still lands somewhere sensible (forward to an agent, or into a triage flow). If the reply starts a flow, the flow's trigger is the keyword event and its nodes do the booking update — see [Build your first automation flow](/guides/build-first-flow) for trigger payload and node shape.

## 4. Opt into the transactional no-inbox gate

By default every outbound send upserts an Inbox conversation and rings the new-conversation bell on a destination's first appearance. For a reminder fired at thousands of fresh destinations that is one bell per recipient — none of which have anything to answer. The [transactional no-inbox gate](/guides/transactional-inbox-no-conversation) suppresses the conversation upsert while preserving the full Messages row and DLR trail.

Opt in explicitly on the termination rule with `inbox: "none"` on the deliver hop when you terminate reminders through one; for WhatsApp AUTHENTICATION-category templates the gate infers transactional status without a rule. If the recipient replies, the inbound path opens the thread then — when there is something to answer — and the keyword rules in step 3 have already claimed the confirmations that matter.

## 5. Consume DLR webhooks, signatures verified

Subscribe to `message.sent`, `message.delivered`, `message.failed` (and `message.read` where the channel returns it) and advance your booking-side "reminder status" state machine on each event. The event payload's `id` is the message-row id your send returned — correlate on it.

Verify every incoming webhook before acting on it: read the raw body, check `X-Orbit-Signature` (with `X-Devotel-Signature` as fallback for old queued deliveries), and answer `401` on failure. The one-call-per-language verifiers are in [Verify webhook signatures with the SDK](/guides/verify-webhook-signatures); the no-SDK path is in [Webhook signature verify polyglot](/guides/webhook-signature-verify-polyglot). Event semantics are at-least-once — a late genuine DLR corrects a row the no-receipt safety net aged, and your consumer must treat corrections as normal traffic. See [DLR outcomes monitoring](/guides/dlr-outcomes-monitoring) for which WARN classes that path emits and what they mean.

## 6. Add an SMS → WhatsApp fallback hop

If SMS delivery matters more than SMS cost, chain a fallback: when the SMS hop fails (or produces no delivery report in your window), advance to WhatsApp. For one-shot transactional traffic use the **per-message cascade policy** on `/messages/smart-send`; the campaign `channels[]` ladder is for bulk sends and the Verify profile chain is for OTP. Ordering rules, failure semantics (`failed` vs `no_delivery` vs `no_engagement`), and per-hop billing are in [Fallback chains](/guides/fallback-chains).

Two bounds to set on any chain: `max_price` (skip a hop whose projected price exceeds the cap) and `max_total_price` (trim the waterfall at a cumulative budget). Every hop is billed independently, so a chain without a declared ceiling is a cost surprise waiting for the first carrier outage.

## 7. How quiet hours and frequency caps interact with the scheduled fire time

Scheduling changes *when* a send fires; the gates it must pass at fire time are unchanged. Layered, they resolve like this:

* **Org-wide quiet-hours gate** — when you've enabled a channel gate (Settings → Compliance → Quiet hours, or `PATCH /api/v1/settings/compliance/quiet-hours`), a scheduled send whose fire time lands inside the window re-parks to the next allowed instant at fire time on channels where re-park applies. That is usually the right behavior for a reminder — it slides to 9 AM rather than firing at 3 AM. If you need the reminder to fire verbatim regardless, leave the gate off for that channel (your call — the gate is opt-in per channel). See [Quiet hours: org-wide channel gates vs. the campaign fallback window](/guides/quiet-hours-configuration).
* **Campaign fallback window** — does not touch this workflow. It applies to drip and journey sends dispatched through campaign rails, and a bare `POST /api/v1/messages` reminder is not campaign traffic.
* **Frequency caps** — only gate the send when the cap's `applies_to_categories` is absent (blanket) or includes the category on the send. With `category: "transactional"` set (step 1), a marketing-scoped cap cannot touch the reminder; a blanket cap can. Scope your caps deliberately, or size the blanket cap so a day's reminder volume per contact can never reach the ceiling. See [Frequency caps](/guides/frequency-caps).
* **DST and timezone** — pass `recipient_timezone` when you know it and let the platform resolve the recipient-local window; DST gaps and folds shift the dispatch rather than skipping it. See [Recipient timezone resolution](/concepts/recipient-timezone-resolution).

## 8. Verify outcomes in the order-notifications ledger

The [order-notifications ledger](/guides/order-notifications-ledger) is the reading surface for machine-triggered transactional traffic — reminder traffic included, because the class stamp from step 1 is what routes these sends into the transactional reading rather than campaign reports. Over your reminder window the ledger header computes:

* **Delivery rate** — delivered (plus read where returned) over terminal sends, in-flight excluded.
* **Time-to-first-attempt** — send-to-first-delivery-signal lag per channel; a rising number on the SMS hop is the case for the fallback chain in step 6.
* **Channel mix** — which hop actually carried the send against the intended first choice.
* **Failure breakdown** — rejected vs undelivered vs failed, grouped by rejection code, so recipient-side hygiene (`undelivered`) separates from dispatch/configuration faults (`rejected`).

Filter and export from the ledger page (24 h / 7 d / 30 d / custom range, channel filter, CSV), or pull the same rows from `/api/v1/analytics/messages` with the channel and status filters for a scheduled BI read. A reminder-stage that silently stopped firing shows as a missing series, not a drop in volume — that distinction is the first thing to check when confirmations stop arriving.

## Related

* [Schedule one-off sends with `scheduled_at`](/guides/message-scheduling) — the parking surface, edit/cancel semantics, and accepted formats.
* [Send-window optimization with `send_at_type`](/guides/message-scheduling-send-window-optimization) — the optimization mode you deliberately skip for reminders.
* [Keyword auto-reply rules](/guides/keyword-auto-reply-rules) — `YES`/`NO` matching, flow triggers, and the catch-all.
* [Keep one-time codes out of the Inbox](/guides/transactional-inbox-no-conversation) — the no-inbox gate mechanics.
* [Verify webhook signatures with the SDK](/guides/verify-webhook-signatures) — per-language verification handlers.
* [Fallback chains](/guides/fallback-chains) — ordering rules, failure semantics, per-hop cost bounds.
* [Quiet hours: org-wide channel gates vs. the campaign fallback window](/guides/quiet-hours-configuration) — which windows touch which traffic classes.
* [Frequency caps](/guides/frequency-caps) — category scoping so the reminder never hits a marketing-shape ceiling.
* [Order notifications ledger](/guides/order-notifications-ledger) — the verification surface.
* [DLR outcomes monitoring](/guides/dlr-outcomes-monitoring) — reading the WARN classes the no-receipt path emits.
