Skip to main content

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/program
Fetch the active program — its earn rules, tiers, and points-expiry window — before enrolling anyone so you can render the ladder. program_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)
There is no enroll call — a contact enrolls when you track an event that matches an earn rule. Send the tenant’s real CDP 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.
Track the accrual event, then the member detail read reflects it immediately. Request
Balance after the read

4. Preview a redemption (simulate)

POST /api/v1/loyalty/preview
Simulate the program against sample events before you burn — the body carries optional per-event properties (here the same purchase value and order reference the member’s real events carry), and the response data.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}/redeem
Burns points under a per-contact advisory lock, so two concurrent redemptions cannot both spend the same balance. reward 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)
The members list pages by most-recent activity (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

A 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

A 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:
A burn that would take the member’s earned balance negative:
The 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

The Retry-After header and error.details.retry_after are both set on every 429 — resend the SAME request after the lower of the two.

402 — Feature gate

Surface plan upsell — no retry path opens until the limit is raised.

60-second retry matrix