Skip to main content

Run a loyalty and rewards program end to end

Orbit ships the three pieces of a rewards program as separate surfaces: loyalty computes points and tiers from your event stream, incentives issues and audits the rewards those points buy, and wallet passes put the card and the coupons on the customer’s phone. This guide wires all three into one program with a single running example, Aurora Coffee Rewards: a member earns a point per dollar, climbs a tier ladder, redeems points for a free-drink coupon, and carries a wallet card whose balance refreshes after every purchase. The route through the program:

Prerequisites

  • CDP events flowing. Points accrue from events you already send (account.created, order-completed, …). If ingestion is not wired yet, start with the CDP event model and the CDP API.
  • An API key with contacts:read, contacts:write, and message-send permission. Reads need contacts:read; enrollment, redemption, and the final delivery message need the rest.
  • One test contact with an E.164 phone number, created or imported per the import guide. The examples use ctn_9f8e7d6c5b4a.

1. Design the program: earn rules and tiers

A program is a name, a list of earn rules, a tier ladder, and an optional point-expiry window. For Aurora Coffee:
  • Points per spend — 1 point per whole dollar on order-completed, computed from the order total.
  • Signup bonus — 50 points on account.created.
  • Expiry — unused points expire after 365 days.
  • Tiers — Member at 0, Regular at 250, Gold at 1,000 lifetime points.
Three rules keep the ladder honest:
  • Anchor a tier at threshold 0. The schema rejects a program without a base tier; every member must always have a current tier.
  • Thresholds are lifetime-earned, not current balance. A member who earned 1,200 points and redeemed 1,000 stays Gold. Redemptions never demote.
  • Every rule needs a stable id so you can raise a rate or retire a rule later without touching the rest of the program.

Preview before you adopt

POST /api/v1/loyalty/preview runs the accrual engine against sample events with no database write. Use it to sanity-check the math before the program goes live:
The response attributes points per event (50 for the signup, 12 for the 12.40 order with floor rounding) and reports the resulting tier. A config missing its base tier fails here with a normal 422, not at production time.

2. Fund the rewards: the incentive catalog and the ledger

Redemption is where loyalty hands off to the incentives engine. Two concepts matter before you price any reward:
  • A ledger entry is the issued-reward record: who got it, why (source), what type and amount, and where it stands in its lifecycle (issued → redeemed or voided). It is the audit trail, nothing more.
  • Fulfillment is how the value actually reaches the customer. Reward types are discount_code (Orbit mints a redeemable code), credit (your tenant account balance), gift_card (a gift-card network; manual until provider credentials are provisioned), and custom (you fulfil it yourself and mark the row redeemed when done).
GET /incentives/providers lists the fulfillment providers available per reward type, so you can price the catalog against what is actually wired:
For Aurora Coffee the catalog is deliberately boring: Budget earn rates against this catalog before launch: at 1 point per dollar, a 100-point coupon costs you a drink per 100 dollars of spend. The ledger’s amount and currency fields let you sum value issued per period later, which is the spend side of the program’s economics (see Ops and limits). A redemption routes through the engine and tags the resulting ledger row source: "loyalty", so program payouts, referral payouts, and one-off source: "api" promos all live in the same ledger. Issue standalone rewards only for triggers that are yours (a service-recovery gesture, a win-back offer); program rewards should always come from a redemption so the points debit and the ledger entry stay paired.

3. Deliver the reward: wallet pass and coupon

Issue the member card at enrollment

Issue a loyalty_card pass the moment a member joins, keyed idempotently to the enrollment so a retried call replays the original pass instead of minting a second card on the customer’s phone:
Save two values from the response: data.id (the wps_… pass id you update and void against) and data.platforms.google.save_url (the delivery link). Check GET /api/v1/wallet-passes/platforms before a bulk enrollment; you can issue passes before Apple and Google credentials are connected, but the delivered save link only works once they are. The pass is useless until it is on the phone. Put the save link in the channel the member opted into, exactly as the wallet-pass guide does:
The same body with "channel": "whatsapp" delivers over WhatsApp for members whose consent covers it. SMS consent does not grant WhatsApp consent; enroll each member on the channel their record shows.

Refresh the balance on every purchase webhook

When the POS posts the next order-completed event, the member’s balance moves. Push the new balance to the pass so the phone is the member’s source of truth:
Each accepted update increments the pass’s generation counter, and the wallet app on the phone refreshes when the counter moves. The member sees 62 points without re-saving anything. Read the balance for the update from GET /api/v1/loyalty/members/ctn_9f8e7d6c5b4a, which returns available, lifetimeEarned, and the current tier.

Deliver the redeemed coupon

When the member redeems 100 points for the free-drink coupon, the incentives engine mints the code and the ledger row carries it. Read it back filtered to the member:
Two delivery styles both work:
  • Message — splice the row’s code into an SMS or WhatsApp message ("Your free drink is ready: code AUR-4F2K. Show it at the counter.").
  • Coupon pass — issue type: "coupon" with the code as barcode_message and expires_at set to the offer deadline, so the POS scanner reads it straight off the phone. Void the pass once the redemption is confirmed at the till.
Mark a custom reward redeemed with POST /api/v1/incentives/{id}/redeem when the barista hands the perk over, so the lifecycle stays honest.

4. Trigger accrual from order events

Points only move when events land, so the program is only as good as the pipe between your POS and the CDP. Three patterns cover it: Webhook consumer. Your POS backend posts each paid order as an order-completed event to your CDP ingest endpoint, with the order id in properties and the member’s contact identity. Deduplicate on the order id in your sender so a POS retry never double-accrues; accrual itself recomputes from the event stream, so a duplicate event would otherwise double the points. Segment-triggered journey. If you would rather keep the trigger declarative, a segment-triggered journey can react to the same events and run the enrollment side (issue the pass, send the save link) without bespoke consumer code. Keyword opt-in. For walk-in signups, a keyword rule (“text BEANS to your number”) adds the contact and emits the account.created event that fires the signup bonus, so the keyword path and the API path feed the same earn rules. For a backfill of historical orders, CDP file ingest replays them in chunks; because balances recompute from the full history, backfilled orders accrue exactly as live ones would.

5. Campaign tie-in: win back lapsed members

A lapsed member who still holds points is the cheapest win-back audience you have. Build the segment from loyalty traits plus recency: loyalty_tier or loyalty_points_balance above zero, and no order-completed in the last 60 days. The traits segments can match on are described alongside Segments in the loyalty guide. Run that segment through a win-back journey with fallback channels: SMS first, WhatsApp or email as fallback, and let the message carry the member’s own number: “You have 320 Aurora points, 30 away from a free drink. They expire 31 Dec.” Points-expiry copy converts because the expiry is real, configured in the program itself. Before the send, apply the same tenant controls you would on any campaign: channel consent per recipient, your quiet-hours schedule, suppression and frequency caps, and your approved opt-out footer on every step. These are your tenant’s controls; review quiet hours, message suppression, and opt-out rules before enabling the journey.

6. Ops and limits: ledger audit, reversals, and spend caps

Audit. GET /api/v1/incentives/issued?source=loyalty is the program’s payout report; add contact_id to see one member’s full reward history, or reward_type to isolate coupons from gift cards. Page with limit and offset (max 200) and follow has_more. The ledger page in the dashboard shows the same rows with the same filters, mirrored at Marketing → Incentives and /outbound/incentives. Void and reversal. Two separate reversals exist, and a refund that involves a reward usually needs both:
  • POST /api/v1/incentives/{id}/void cancels an incentive that was issued in error or never fulfilled. Void only applies from issued; a row that is already redeemed or voided answers with a 409 describing its current state.
  • POST /api/v1/loyalty/members/{contactId}/adjust reverses or grants points directly, using the same overspend-safe path as redemption. When a refunded order had earned points, debit them here so the balance and the tier math stay truthful.
Treat 409 from the incentive transitions and from wallet-pass update/void as expected termination, not a retryable error. Spend caps. Rewards pay out real value, so give the program a budget like any other channel. The controls you own: the earn rates and tier benefits in the program config, the reward catalog you price in Section 2, idempotency keys that make a retry replay rather than double-mint, and void for anything issued in error. The ledger’s amount/currency fields are the measurement: summing issued value per month is your reward spend. Your account’s spend limits sit outside the program as the outer bound on platform spend; the program config is the dial inside it. The dashboard. The Loyalty page (under Marketing → Loyalty, mirrored at /outbound/loyalty) shows the active program card (earn rules, tiers, expiry window) and the members panel with each member’s computed balance and tier. It mirrors the API: anything you can PUT to the program you can edit in the program dialog, and Redeem points / Adjust points act on the selected member.

Program live checklist

  • Preview run against sample events returns the expected points and tier, and the program has its base tier at threshold 0.
  • Enrollment issues the pass with an idempotency_key derived from the enrollment, not the contact id, so a re-issue after a void mints a fresh pass.
  • GET /wallet-passes/platforms checked before bulk delivery; save-link message sent over a consented channel with the opt-out instruction in place.
  • The purchase webhook updates the pass after every earn and spend, so the phone never shows a stale balance.
  • A redemption mints the coupon, the ledger row carries the code, and a custom reward has a documented redeem-when-done step.
  • Refund runbook covers both halves: void the incentive, adjust the points.
  • Win-back segment reviewed for consent and recency before the first send.
  • Ledger filters (source=loyalty, contact_id) verified as the monthly payout report.