Skip to main content

Outbound loyalty: points, tiers, and member balances

The Outbound → Loyalty surface in the dashboard is the operator console for your loyalty program: it shows the active program’s earn rules and tier ladder, lists every member with their computed balance, and drives adjustments and redemptions. This page names the model that console runs on — where points come from, how tiers promote, how the wallet pass consumes a balance, and where membership begins and ends. The loyalty program model page covers the event-sourced internals (how balances project over the CDP event stream); this page anchors the shipped Outbound surface to what you’ll actually see and do in it. For the setup workflow, see the Loyalty program guide; for the per-endpoint contract, see the Loyalty API reference.

1. What a loyalty program is in Orbit

A loyalty program rewards repeat behaviour with three things:
  • Points — a balance each member accumulates. Points are earned from behavioural events you already send to the CDP, spent through redemptions, and adjusted by operators.
  • Tiers — an ordered ladder (Member → VIP) keyed on a member’s lifetime earned points. Tiers carry benefits you name (“2x points”, “Free shipping”) and are the primary segmentation signal.
  • Member balances — the spendable remainder, computed separately from the lifetime total. The split is deliberate: redemption drains the balance but never the lifetime figure, so throwing points away never demotes a member.
The dashboard page renders exactly that: a program card with its earn rules and tier ladder, and a members panel listing every member’s tier, spendable balance, lifetime total, and expiring-soon points. Because the program is computed over the CDP event stream, an unconfigured workspace still runs a sensible built-in default (sign-up bonus, points per dollar on orders, a review bonus, a four-tier ladder with 365-day expiry) — “Customize program” on the page turns the default into your own revision. The other surfaces read the same model, not a parallel copy of it: segments and journeys branch on the loyalty_points_balance, loyalty_lifetime_points, and loyalty_tier contact traits, and wallet passes display the same balance the console shows.

2. Accrual rules: which events earn, how tiers promote

Accrual starts at inbound and behavioural channel activity — the events you publish to the CDP. An earn rule maps an event name onto points, in one of two shapes: A channel event earns only when it matches a rule. Send the same behavioural signal from SMS, WhatsApp, your own webhook, or an import job, and it accrues identically — the rule matches on event name, not on channel. A perUnit rule skips events whose named property is missing or non-numeric rather than granting garbage, and the same event name can feed several rules (a flat attendance bonus plus a per-dollar multiplier, say). Tier promotion runs on the lifetime total, not the spendable balance. Every earn (including operator-credit adjustments) adds to lifetimeEarned, and a member’s tier is the highest tier whose threshold that total has reached. Two consequences:
  • Promotion is one-way. Spending points never demotes — a member who redeems their entire balance keeps their tier and the progress toward the next one.
  • Everyone has a tier from the first event. The ladder must contain a tier at threshold 0; the schema enforces this, which is what the base-tier badge in the members panel relies on.
Tier transitions surface in three places: the member’s loyalty_tier trait (which segments and journeys read), the tier badge on the members panel, and the tier label on the member’s wallet pass. Before adopting a tuned program, dry-run its accrual and promotion with POST /preview — pure computation, nothing persisted.

3. Balance and redemption flow

The balance is the spendable surplus: unexpired earns, minus burns, minus operator debits. Each earn opens a lot that expires pointsExpiryDays later (when the program sets an expiry window), and redemptions consume the oldest live lots first — FIFO. The members panel’s “Expiring soon” column is that FIFO schedule surfaced, and it’s the number a “points expiring soon” journey nudge reads. Redemption is the one action that burns the balance. Whether it starts at a pass present, a click in your app, or the console’s Redeem button, the flow is the same:
  1. The balance is recomputed at request time — never a cached counter.
  2. Concurrent redemptions against the same member serialize, so two requests can never read the same balance and both spend it. An overspend is rejected with 409 INSUFFICIENT_POINTS, and the response reports both the available and the requested amounts.
  3. A successful spend reduces the spendable balance and leaves lifetime points — and therefore tier — untouched.
  4. The reward itself (a discount code, a gift card, a custom perk) pays out through the incentives fulfillment engine — the same primitive behind referral and survey rewards. The ledger knows points moved; incentives delivers what the program owes.
The wallet pass surface. A member carrying the program on their phone holds a Wallet Pass of type loyalty_card. The pass is a display, not a ledger: it renders the balance passed to it, so the consume-and-update loop is — accept a redemption, then push an update that carries the new balance (and tier label when a promotion crossed). Each accepted update advances the pass’s generation, and the holder’s wallet app refreshes from that counter — a member who redeemed at the till sees the new balance on the pass before they’ve left the store, without saving anything again. Because redemption and the pass update are idempotent against the same member record, a retried client or an out-of-order push can’t double-spend; the worst case is a corrected generation on the next update.

4. Membership lifecycle

Membership is implicit: any contact with loyalty activity is a member. There is no separate enrolment row to create.
  • Enrol. A member appears the first time one of their events matches an earn rule, or when an operator credits them points (the console’s Adjust path). Conventionally, the account.created-class event earns the sign-up bonus, so enrolment usually coincides with the first behavioural signal. The same moment is the natural trigger to issue the loyalty card and deliver its save link over SMS or email.
  • Active. Events arrive, balances move, tiers promote, redemptions burn. The members panel sorts standing by tier and balance; the FIFO expiry windows keep the ledger bounded.
  • Freeze. A member can be suspended without losing anything: unmatched events earn nothing, and a suspended account’s redemptions can be gated at your side with the same overspend-safe rejection the API returns (409). Points and tier sit exactly where they were — a freeze freezes the flow, not the standing. To hard-stop accrual entirely, remove the member’s event source or suppress their send path; nothing in the program config is per-member.
  • Close out. To retire a member, revoke the link between them and the program: stop their earn path, void their wallet pass, and let remaining lots expire on the FIFO schedule — or burn the residual balance with a final adjustment. Because the ledger derives from events, there is no orphan membership row to clean up.
Program-level close-out is a different blade: resetting the program to the default, from the console or DELETE /program, rewrites only the configuration revision. Member history is untouched — balances and lifetime totals survive a config reset.

5. API parity: balance reads, tier transitions, event ingestion

Everything the Outbound → Loyalty console shows is reachable through the Loyalty API with the same semantics: Balance and tier reads are on-demand projections over the member’s event history — the API always tells you the member’s up-to-the-second standing, including expiringSoon and nextExpiryAt for the FIFO-window fields. Event ingestion flows through the CDP ingestion endpoints; the earn rules pick up matched events the moment they land — a backfill or an import accrues identically to a live behavioural signal. Tier transitions are never written back: promote via the lifetime figure, read the promoted tier from the member’s loyalty_tier trait, and propagate it wherever your member-facing surface displays standing.

6. Example: SMS-driven earn, wallet-pass redemption

A complete earn-to-burn loop on the shipped surfaces:
  1. Enrol. A new customer’s account.created event arrives via your CDP ingestion call. The flat sign-up bonus rule grants 100 points, which immediately puts them at the base tier. Issue a loyalty_card wallet pass and send its save link over SMS.
  2. Earn. The customer places an order; your order-completed event lands with properties.total = 129.00. The per-dollar rule accrues 129 more (plus any flat bonuses the same event matches), the tier ladder re-evaluates, and the next earn or spend updates the pass fields — the holder’s wallet stays the source of truth.
  3. Engage. A journey branches on loyalty_tier == 'member' and loyalty_points_balance >= 150 and sends an SMS nudge: “You have 229 points — redeem for free shipping.” Deep-link the redemption surface of your app or the member’s own account page.
  4. Redeem. The member spends 150 at checkout. The redemption request recomputes the balance, validates 150 ≤ available, and decrements the spendable figure (lifetime and tier untouched). The incentives engine pays out the free-shipping code. Your server then updates the wallet pass with the 79-remainder balance; the phone refreshes off the new generation the moment it checks.
None of this path touches outbound SMS mechanics — earn comes from the behavioural events you ingest, and the program config governs which of those events accrue. Configure send-side controls (quiet hours, frequency caps, suppression) exactly as you would for any campaign path that nudges points or redemptions.