Loyalty program model
Loyalty in Orbit is a points-and-tiers program computed on top of the events you already send to the CDP. There is no separate loyalty ledger to provision, backfill, or sync — the CDP event stream is the ledger, and every balance and tier you read is a projection over it. This page explains that model: where points come from, how program configuration is versioned, how tiers climb, and how redemptions stay safe under concurrency. For the setup workflow, see the Loyalty program guide; for the per-endpoint contract, see the Loyalty API reference.The mental model: the CDP event stream is the ledger
Most loyalty vendors ship a dedicated points ledger — a table you maintain beside your customer data, kept in sync by a batch job. Orbit takes the event-sourced approach instead: a contact’s points accrue from their own behavioral events (anorder-completed track earns points per the program’s earn rules), burn via reserved loyalty.points_redeemed events, and every read recomputes the balance from the full event history at the moment you ask.
Consequences of that design:
- No drift. Backfills, imports, and webhook-driven events feed the program the moment they land. There is nothing to re-sync.
- A full audit trail. Every earn, burn, and operator adjustment is an immutable event with a timestamp and a payload, inspectable in the contact’s event timeline.
- Zero new tables. A workspace’s loyalty data occupies no storage beyond the event stream it already owns.
pointsExpiryDays after it was earned (when the program sets an expiry window), and redemptions consume the oldest live lots first — the standard FIFO expiry model.
Accrual: earn rules map event names onto points
An earn rule names a CDPtrack event and one of two shapes:
A
perUnit rule ignores events whose named property is missing or non-numeric — a malformed order grants nothing rather than granting garbage. A program carries up to 100 rules, and the same event name can appear under several rules (both a flat attendance bonus and a per-dollar multiplier on order-completed, say).
Operator adjustments are events too: a manual credit appends a loyalty.points_adjusted event, which behaves exactly like an earn — it opens its own FIFO lot, ages under the same expiry window, and counts toward tier.
Program configuration: event-sourced revisions
Program configuration follows the same event-sourced discipline. You never mutate a stored row; every create, update, or reset appends one revision event (loyalty.program_configured) under a workspace-level sentinel, and the active program is the latest non-tombstone revision. Resetting the program appends a tombstone, which returns the workspace to the built-in default without touching a single point of member history — the ledger and the config are separate event families sharing one store.
If you never author a program, Orbit still computes balances against a sensible default: a sign-up bonus, points per dollar on orders, a review bonus, and a four-tier ladder with a 365-day expiry.
Because a program body fully replaces the previous revision, an update is a new version, not a patch — always PUT the complete program, not just the rule you changed.
Tiers: a one-way ladder on lifetime points
Tiers are an ordered ladder keyed on lifetime earned points — the sum of everything a contact has ever earned, never reduced by spending. A program must contain a tier at threshold0 so every member holds a current tier from their first event.
The spendable-balance / lifetime-points split is deliberate: redemption drains the balance but never the lifetime total, so climbing a tier is one-way — a member who redeems their entire balance keeps their tier and their progress toward the next one. Tier evaluation returns the current tier, the next threshold, and progress toward it, which is what the dashboard ladder and wallet-pass label render.
Redemption: overspend-safe burns under an advisory lock
A redemption (or an operator debit) runs inside a transaction guarded by a per-contact advisory lock. The transaction takes the lock for that one contact, recomputes the balance from the event stream, validates the spend, and appends a singleloyalty.points_redeemed event — or rolls the whole thing back. Two concurrent redemption requests for the same contact therefore serialize; the classic “both requests read the same balance and both spend it” race cannot overspend a shared balance. A spend that exceeds the available balance is rejected with a conflict, and the API tells you the available and requested amounts.
POST /members/:contactId/redeem accepts an optional reward label (a SKU or reward name) recorded on the redemption event, so downstream fulfillment can see what the customer burned points on.
Where redemption meets incentives
The ledger knows points moved; paying out the reward is a separate concern. Spending points against a loyalty card on a Wallet Pass routes through the incentives fulfillment engine — the same primitive behind referral and survey rewards — which mints a discount code, calls a connected gift-card network, or records a manual payout for your team to fulfill. Wallet passes carry the program; incentives pay out what the program owes.The trait surface for segments and journeys
Every computed projection ships as contact traits, so points and tiers are first-class inputs to targeting:
Segment conditions like
loyalty_points_balance >= 500 or loyalty_tier == 'vip' read these traits, and journeys can branch on them — nudge members nearing a threshold, suppress discount offers to top-tier holders, or trigger a “points expiring soon” nudge within the program’s expiry window.
Worked example: earn, preview, redeem
- A customer’s
order-completedevent lands withproperties.total = 129.00. TheperUnitrule from the example above grants 129 points (plus any flat bonuses other rules attach to the same event). GET /members/:contactIdrecomputes the projection: balance up by 129, lifetime up by 129, tier re-evaluated.- Before adopting a tuned program, dry-run it with
POST /preview: the endpoint simulates your candidate config against sample events and returns what balances and tiers would look like — pure computation, nothing persisted. - The customer spends 500 points:
POST /members/:contactId/redeemtakes the per-contact lock, verifies 500 ≤ available, and appends the burn. Balance drops by 500; lifetime and tier do not move.
Related reading
- Loyalty program setup guide — the step-by-step operator workflow.
- Incentives: catalog, ledger, and fulfillment — the engine redemptions route through.
- The CDP event model — the stream this ledger projects over.
- Wallet Passes — the carrier for loyalty cards on Apple and Google Wallet.
- Loyalty API reference — the per-endpoint contract.