The Loyalty Engine is built around a symmetric earn-and-burn model. Every interaction is either an earn — a purchase that awards points, issues a tangible reward (coupon, gift card, free item, discount voucher), or both — or a burn, where a member redeems points for a discount at checkout. Points and rewards are equal first-class outcomes of an earn; you choose either or both per campaign.
When a customer makes a purchase, your POS or checkout calls POST /transactions/evaluate. The engine evaluates all active campaigns against the basket and, for each matching campaign, awards points and/or issues a tangible reward (coupon, gift card, free item, or discount voucher).
SPEND_PERCENTAGE — award a percentage of the basket total as points. E.g. 1% of R150 = 150 points.FIXED_POINTS — award a flat number of points per transaction that meets minimum basket criteria.STAMP_CARD, SURPRISE_DELIGHT, SIGNUP_BONUS, ON_DEMAND, and more — see the Campaigns guide for all nine rule types.Each matched campaign in the response includes a rewardIds array — UUIDs of any tangible rewards issued by that campaign. An empty array means the campaign awarded points only. Resolve reward details via GET /members/:memberId/rewards.
When a customer wants to spend their points, call POST /transactions/redeem with the number of points to redeem. The engine validates the balance, applies burn rules (points-per-rand rate), enforces discount caps, and returns the discount in cents to apply at checkout.
Transaction flow
Both /evaluate and /redeem are idempotent on transactionRef. If your POS retries after a network timeout, the engine returns the original result — no double-awarded points, no duplicate reward issued, no double-redeemed balance.
Every member has a pointsBalance maintained in a double-entry ledger. Earn transactions credit the ledger; burn transactions debit it. The balance is always consistent — there is no way to go below zero.
Tangible rewards aren't points, so they live outside the points ledger. Each reward a campaign issues becomes a row with its own state machine (PENDING_FULFILMENT → ISSUED → EXPIRED/FAILED) and a generated code. List a member's rewards with GET /members/:memberId/rewards, and subscribe to the reward.issued webhook to be notified the moment a code is ready. See the Rewards reference for states, types, and endpoints.