Worked request and response samples
The chain every loyalty integration walks: read the tenant’s active program, enroll a member by accrual, read the balance back, preview a redemption, burn points, and page the ledger. Every block is a full envelope as the API returns it — copy the request, substitute your own contact id, and compare the{ data, meta } shape.
1. Read the active loyalty program
GET /api/v1/loyalty/programprogram_source is "default" until a tenant authors its own program with POST /api/v1/loyalty/program (then "custom"); burn behaviour is fixed (FIFO, oldest lot first) and the redemption amounts are per request — see step 4.
Request
2. Enroll a member — send their first behavioural event
POST /api/v1/track (CDP endpoint)track endpoint with the contact id, the event name from a rule in step 1, and any rule field (total here) as properties; points accrue per the program.
Request
3. Accrue points
Points accrue from the events you already track — a purchase event referencing the order id, a campaign action — per the earn rules. Every read below projects the balance over that stream, so there is no write step to sync earned points.
4. Preview a redemption (simulate)
POST /api/v1/loyalty/previewdata.state projects the exact balance and tier that set would produce. Pure: nothing is written, so safe to poll from a checkout page.
Request
5. Burn — redeem from a member’s balance
POST /api/v1/loyalty/members/{contactId}/redeemreward and reference are recorded on the ledger event for reconciliation. The response spreads the post-burn state — the same balance/tier/traits shape as step 3 — plus the redemption event id.
Request
6. Page the member ledger
GET /api/v1/loyalty/members?limit=25&offset=0 (then the per-member GET /api/v1/loyalty/members/{contactId} for the full detail)limit 1–100, default 25; offset from 0). Each row carries the computed state. A redemption or a debit reversal (POST …/adjust with "direction": "debit") shows as totalRedeemed growth — and since reversal events are the adjustment/redeem rows in the same stream, event_count and last_activity_at move in step with the burn, never lowering lifetimeEarned.
Request
Errors worth branching on
These five failures cover the loyalty surface above — read your program (GET /program), balance reads (GET /members, GET /members/:contactId), and burns (POST /members/:contactId/redeem). Each block below is a full{ error, meta } envelope as the API returns it, and the matrix at the bottom answers retry vs surface for the same five classes. For the platform-wide decision table these branches plug into, see the error handling guide.
401 — Unauthorized
401 on this page is the bearer key failing before the route ran — it never means the resource is wrong. Rotate the key or re-mint the scoped token; retrying the same request changes nothing.
403 — Forbidden
403 means the key authenticated but the operation is gated by scope (contacts:read for balance/program/preview reads, contacts:write to burn or adjust) — check the key’s scopes on the developer page; a 403 is never a data-not-found shape.
422 — Schema, or a burn invariant
The schema rejection:422 case failed schema validation — branch on error.details.field and resend with a non-negative integer for points instead of blind-retrying the same body. The 409 failed the earned-vs-burn invariant — re-read the balance from the ledger (step 3 or 6) and resend with points no greater than available; the API validates the whole request under a per-contact lock, so a burn beyond the balance never writes a reversal that goes negative.
429 — Rate
Retry-After header and error.details.retry_after are both set on every 429 — resend the SAME request after the lower of the two.