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

# Customer alerts and wallet engagement for digital banks: an end-to-end walkthrough

> Compose a digital-bank alerting and loyalty posture end to end in Devotel Orbit: map transaction, fraud-suspect, and marketing sends to push, WhatsApp, and SMS with cascade failover, enroll customers with consent capture and opt-out handling, keep quiet hours and frequency caps from ever touching a fraud alert, issue and update a wallet pass with a balance Live Activity, reconcile delivery through the delivery log and DLR webhooks, and escalate an undelivered fraud alert to a voice call.

# Customer alerts and wallet engagement for digital banks: an end-to-end walkthrough

This walkthrough takes a digital-bank operations team from the first alert-matrix decision to a live, observable alerts-and-loyalty setup. It composes pieces that already ship: the [notify cascade](https://docs.orbit.devotel.io/guides/notify-cascade-failover.md) for cross-channel failover, [per-campaign quiet hours](https://docs.orbit.devotel.io/guides/per-campaign-quiet-hours.md) and [frequency caps](https://docs.orbit.devotel.io/guides/frequency-caps.md) for alert fatigue, [wallet passes](https://docs.orbit.devotel.io/guides/wallet-passes.md) and [iOS Live Activities](https://docs.orbit.devotel.io/guides/push-live-activities.md) for loyalty and balance surfaces, the [DLR webhook consumer](https://docs.orbit.devotel.io/guides/dlr-webhook-consumer.md) for delivery reconciliation, and the [first outbound voice call](https://docs.orbit.devotel.io/guides/first-outbound-voice-call.md) flow for escalation. The examples use one fictional digital bank, Meridian Bank, 400,000 customers, one loyalty program.

Every control on this page is tenant-owned. Your organization decides which events alert, which channels carry each class, which windows and caps apply to marketing, and what happens when a fraud alert cannot be delivered. Orbit provides the channels and the controls; it does not attest to any banking regulation or card-network rule on your behalf. If your program answers to one, your team maps these controls to it.

## 1. Map the alert matrix

Before any configuration, write down the class of every send you will make. Three classes cover a digital bank:

| Class | Examples | `category` on the send | Channels | Fatigue posture |
| - | - | - | - | - |
| Transaction alerts | Debit or credit posted, low balance, payment due | `transactional` | Push fan-out at T+0, then WhatsApp → SMS cascade | Exempt from caps and quiet hours by category |
| Fraud-suspect | Card control, new-device sign-in, suspected fraud probe | `transactional` | Push fan-out at T+0, then WhatsApp → SMS cascade, voice escalation per section 5 | Exempt; escalation path defined |
| Marketing | Cashback promos, product offers, referral nudges | `marketing` | Fan-out over opted-in channels | Marketing-scoped caps and campaign quiet hours apply |

The category column is the load-bearing one. Sends tagged `category: "transactional"` ignore any cap scoped to `applies_to_categories: ["marketing"]`, which is how a fraud alert stays deliverable to a customer who already hit today's marketing ceiling (section 2).

### Push is a parallel leg, not a cascade hop

Fire the push fan-out at T+0 with `POST /api/v1/push/send`, and let the waterfall carry the phone-number side. Push addresses device tokens and has no per-recipient delivery receipt to escalate from, so it never sits inside the waterfall: the [cascade](https://docs.orbit.devotel.io/guides/notify-cascade-failover.md) advances to the next hop only on a terminal receipt for the active hop, and a push send produces none. Customers with the app installed get the alert on the lock screen while hop 1 is still pending; everyone else is covered by the WhatsApp → SMS chain.

Order the waterfall richest → cheapest and bound the window. A fraud probe wants a short window; a payment-due reminder can use a longer one. The window accepts 30 to 86400 seconds.

### Worked cascade: a fraud-suspect alert

```bash theme={null}
curl -X POST "https://api.orbit.devotel.io/api/v1/notify" \
  -H "X-API-Key: $ORBIT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "mode": "waterfall",
    "fallback_window_seconds": 120,
    "from": "Meridian",
    "metadata": { "alert_class": "fraud-suspect", "alert_id": "fraud_8h2k" },
    "bindings": [
      { "channel": "whatsapp", "address": "+14155552671" },
      { "channel": "sms",      "address": "+14155552671" }
    ]
  }'
```

Only the WhatsApp hop fires on this call; the SMS hop arms and fires only if WhatsApp reports back undelivered inside 120 seconds. The response's `fallback_chain` names the armed tail, and `GET /api/v1/notify/:notifyId` later shows which leg landed the customer ([track a cascade](https://docs.orbit.devotel.io/guides/track-a-cascade.md) reads every column). A queued hop is not a delivered message: treat the confirmed delivery receipt as the terminal signal, and never re-POST the whole notify to retry one failed hop.

## 2. Enroll a customer

### Consent capture at onboarding

Record channel-level consent on the contact row when the account opens, not when the first campaign needs it:

* `phone` in E.164 for the WhatsApp and SMS hops, plus a device token for push once the app registers ([push device tokens](https://docs.orbit.devotel.io/guides/push-device-tokens-and-scheduled-sends.md)).
* A consent field per channel (`consent_sms`, `consent_whatsapp`, `consent_push`), created as [custom fields](https://docs.orbit.devotel.io/guides/custom-fields.md) if your onboarding import does not carry them. WhatsApp requires the customer to opt in before your first message over that channel; store that opt-in and the date it was given.
* The alert class each consent covers. A customer who consented to account alerts has not consented to promos; keep the two as separate fields so section 2's caps and section 1's matrix can read them independently.

### Opt-out handling

An opt-out writes to the suppression list of the account that sent, per channel. The [opt-out rules guide](https://docs.orbit.devotel.io/guides/opt-out-rules.md) covers the keyword and per-channel semantics; the [opt-outs console](https://docs.orbit.devotel.io/guides/audience-opt-outs-console.md) is where operations reads and manages the resulting lists.

The decision you own: what an opt-out removes the customer from. Most banks keep marketing opt-out and account-alert suppression as separate lists, because a customer who stops promos still expects the fraud alert. Orbit enforces exactly the suppression entries your tenant holds, so the separation is a list-hygiene choice you make and maintain: marketing sends check the marketing list, alert sends check the alert list, and a STOP to a promo never writes to the alert list.

### Quiet hours and caps that never block a fraud alert

Two controls keep alert fatigue real without ever gating a fraud alert:

* **Frequency caps.** Create your caps with `applies_to_categories: ["marketing"]` ([frequency caps](https://docs.orbit.devotel.io/guides/frequency-caps.md), console in [Settings → Frequency caps](https://docs.orbit.devotel.io/guides/settings-frequency-caps-console.md)). A cap scoped that way never counts a `transactional` send, so "3 marketing SMS per contact per rolling day" coexists with unlimited alerts. The trap is the keyless cap: a cap with no category filter applies to every category, and a blanket cap sized for marketing can reject a fourth OTP in the window. Scope every cap deliberately.
* **Quiet hours.** Put marketing campaigns inside a window with [per-campaign quiet hours](https://docs.orbit.devotel.io/guides/per-campaign-quiet-hours.md); a campaign override can only narrow the window it inherits, so marketing timing tightens without ever widening your tenant default. Leave the transactional send lane unwindowed, which is your configuration to make ([quiet-hours configuration](https://docs.orbit.devotel.io/guides/quiet-hours-configuration.md)). One asymmetry to plan around: any voice hop, in any class, only dials inside the federal 8 AM to 9 PM recipient-local window, and no workspace setting relaxes it (section 5).

## 3. Issue and update a wallet pass

Issue a loyalty card at enrollment, with the balance and tier as display fields and an idempotency key tied to the enrollment event:

```bash theme={null}
curl -X POST https://api.orbit.devotel.io/api/v1/wallet-passes/issue \
  -H "X-API-Key: $ORBIT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "type": "loyalty_card",
    "contact_id": "ctn_9f8e7d6c5b4a",
    "title": "Meridian Rewards",
    "subtitle": "Blue tier",
    "organization_name": "Meridian Bank",
    "barcode_message": "MBR-10482",
    "barcode_format": "QR_CODE",
    "fields": [
      { "key": "points", "label": "Points", "value": "0" },
      { "key": "tier", "label": "Tier", "value": "Blue" }
    ],
    "idempotency_key": "enroll-ctn_9f8e7d6c5b4a-2026-10"
  }'
```

Save `data.id` (the `wps_…` pass id) and `data.platforms.google.save_url` from the response, then deliver the save link over the channel the customer opted into with `POST /api/v1/messages`, exactly as the [wallet passes guide](https://docs.orbit.devotel.io/guides/wallet-passes.md) walks. If the retry path for the issue call exists anywhere (a queue worker, a backfill), keep the `idempotency_key`; without it a retry mints a second pass on the customer's phone.

### Worked update: points post and tier change

Update after every earn or spend event; only the fields you send change, and each accepted update pushes `generation` forward so a saved pass refreshes in place:

```bash theme={null}
curl -X POST https://api.orbit.devotel.io/api/v1/wallet-passes/wps_a1b2c3/update \
  -H "X-API-Key: $ORBIT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "fields": [
      { "key": "points", "label": "Points", "value": "1,240" },
      { "key": "tier", "label": "Tier", "value": "Gold" }
    ]
  }'
```

Drive these updates from the [incentives ledger](https://docs.orbit.devotel.io/guides/incentives-ledger.md): the ledger entry is the source of truth for what the customer earned, and the pass (and the Live Activity below) are renders of it. One writer, two surfaces, no drift.

### The balance Live Activity

For the glanceable surface, run a Live Activity per customer session with `activity_type: "account-balance"`. Start it when the app foregrounds the account view, and update it on posted transactions; [Live Activities](https://docs.orbit.devotel.io/guides/push-live-activities.md) covers the token handoff and end semantics. An update replaces `content_state` in full, so send the complete object:

```bash theme={null}
curl -X PUT https://api.orbit.devotel.io/api/v1/push/live-activities/liveActivity_8k2mqa \
  -H "X-API-Key: $ORBIT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "content_state": { "balance": "4,182.09", "lastTxn": "Coffee -6.40", "posted": true },
    "title": "Meridian checking",
    "body": "Coffee -6.40 posted. Balance 4,182.09",
    "apns_priority": 5
  }'
```

End the activity when the session closes; an orphaned balance tile on a lock screen reads as stale data. Use `apns_priority: 10` for the fraud-suspect variant and 5 for routine balance movement.

When a pass-adjacent purchase has a slow third-party leg, an instant top-up through a partner or a physical card order, protect the wallet debit with the two-phase hold pattern: `placeHold` at commit, `captureHold` on confirmation, `releaseHold` on failure ([wallet holds](https://docs.orbit.devotel.io/guides/wallet-hold-capture.md)). The pass shows the customer the outcome; the hold keeps the ledger honest while the partner thinks.

## 4. Delivery observability

Close every alert with a delivery record, from two surfaces that must agree.

**Delivery log.** The [delivery log](https://docs.orbit.devotel.io/guides/delivery-log.md) is the read surface for final status per channel and per hop: which leg of the waterfall landed the customer, which error code terminated the losers, and what each hop cost. After a fraud-alert batch, the rows you act on are the terminal failures; export them as your `missed-fraud-<date>` segment for section 5.

**DLR webhook reconciliation.** Subscribe your backend to delivery receipts ([DLR webhook consumer](https://docs.orbit.devotel.io/guides/dlr-webhook-consumer.md)) and reconcile: register the endpoint with `POST /api/v1/webhooks/endpoints`, dedupe on the delivery id, and treat the webhook as at-least-once (one initial attempt plus retries on a doubling backoff, then a dead-letter queue you can requeue from). Your reconciliation loop matches webhook transitions against the delivery log and against the `alert_id` you stamped in the notify `metadata`, so one ledger answers "did the customer get the fraud alert" for support, risk, and ops.

Two reading rules keep the reconciliation honest:

* A queued cascade row is armed, not settled. Only the confirmed receipt is terminal.
* One alert, one notify id. Retry logic rebuilds and resends a failed hop; it never re-POSTs the batch, so the ledger never double-counts a send.

## 5. Escalation path: undelivered fraud alert to voice

When every messaging hop terminally fails on a fraud-suspect alert, the next leg is a voice call to the customer. Set the call path up once with the [first outbound voice call](https://docs.orbit.devotel.io/guides/first-outbound-voice-call.md) flow (number, routing, and a test call end to end), then wire the escalation one of two ways:

* **As the last hop of the waterfall.** Add a `voice` binding after `sms` with a `max_price`; the call fires only when SMS reports undelivered inside the window, and the per-hop receipt records the outcome in the same ledger as section 4.
* **As an escalation worker.** Your reconciliation loop from section 4 watches for the all-hops-failed outcome on `alert_class: "fraud-suspect"` and places the call, which lets you add your own rules (attempt once, then page the on-call analyst).

Both compositions tag the send `transactional`, so marketing caps never touch the escalation.

One schedule fact binds the voice leg: it only dials inside the federal 8 AM to 9 PM recipient-local window, and no tenant setting relaxes it. A 3 AM fraud event whose messages all failed will not get a 3 AM call; the refusal surfaces as `TCPA_FEDERAL_DIALING_WINDOW_BLOCKED`. Plan the overnight posture deliberately: the escalation worker pages your on-call team instead, or arms the call for window open. That is a posture decision for your risk team, and the delivery record shows which path each alert took.

## 6. Compliance posture as tenant choices

The controls on this page are levers your organization sets; none of them is a claim about what your regulator requires.

* **Consent and suppression records** are your data, held per tenant: the consent fields from section 2, the opt-out lists from the [opt-outs console](https://docs.orbit.devotel.io/guides/audience-opt-outs-console.md), and the marketing-versus-alerts separation as written policy your team can point to.
* **Quiet hours and frequency caps** are your windows and ceilings, default-open until you set them, per the [frequency caps console](https://docs.orbit.devotel.io/guides/settings-frequency-caps-console.md) and [quiet-hours configuration](https://docs.orbit.devotel.io/guides/quiet-hours-configuration.md). The single exception is the federal voice dialing window in section 5, which is platform-level and not a tenant toggle.
* **Agreements.** Where your program needs them, the [data processing agreement](https://docs.orbit.devotel.io/compliance/data-processing-agreement.md) and [BAA](https://docs.orbit.devotel.io/compliance/baa.md) (with the [recording-consent companion](https://docs.orbit.devotel.io/guides/compliance-dpa-baa-recording-consent.md) if you add voice recording) are the contract surfaces. Whether they apply to you is your counsel's question; the records above exist either way.
* **Examination export.** [Audit log export](https://docs.orbit.devotel.io/guides/audit-log-export.md) produces the account of who changed which control and when, and the delivery log export produces the account of what was sent and how it resolved. Keep both standing so an examiner's request is a pull, not a project.

## See also

* [Cascade (waterfall) fallback chains](https://docs.orbit.devotel.io/guides/notify-cascade-failover.md) — the fan-out versus waterfall decision and the per-recipient chain preview.
* [Frequency caps](https://docs.orbit.devotel.io/guides/frequency-caps.md) and the [console guide](https://docs.orbit.devotel.io/guides/settings-frequency-caps-console.md) — category scoping that keeps caps off transactional sends.
* [Per-campaign quiet hours](https://docs.orbit.devotel.io/guides/per-campaign-quiet-hours.md) — narrowing marketing windows without touching the alert lane.
* [Opt-out rules](https://docs.orbit.devotel.io/guides/opt-out-rules.md) and the [opt-outs console](https://docs.orbit.devotel.io/guides/audience-opt-outs-console.md) — keyword semantics and per-channel suppression management.
* [Wallet passes](https://docs.orbit.devotel.io/guides/wallet-passes.md) and [wallet holds](https://docs.orbit.devotel.io/guides/wallet-hold-capture.md) — issue, update, void, and the two-phase debit for slow third-party legs.
* [iOS Live Activities](https://docs.orbit.devotel.io/guides/push-live-activities.md) — start, update, and end semantics for the balance surface.
* [DLR webhook consumer](https://docs.orbit.devotel.io/guides/dlr-webhook-consumer.md) and the [delivery log](https://docs.orbit.devotel.io/guides/delivery-log.md) — the two surfaces a reconciliation loop keeps in agreement.
* [First outbound voice call](https://docs.orbit.devotel.io/guides/first-outbound-voice-call.md) — standing up the escalation leg end to end.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.