Documentation overview

Loyalty Program

Your brand can run a full points-and-tiers loyalty program: customers earn points on their spending, climb named tiers, and redeem points for rewards from your promo catalog. Everything runs automatically once configured – points accrue nightly, tier changes can trigger journeys, and balances are available in every email.

Where: Brand Settings → Loyalty (/brand?tab=loyalty).

What counts as "spend" – set this deliberately

This is the most important decision in the program, and it differs by industry:

  • Commerce / retail: award on completed purchases. Points from an order that is later returned are not taken back yet (see Honest limits), so if returns are common, award on an event that fires once the return window has closed.
  • SaaS / subscriptions: award on cleared subscription payments.
  • Gaming & betting: award on wagers, never on deposits. Deposits are refundable – if deposits earned points, a player could deposit, collect points, and withdraw. Rates also differ by product: sportsbook stakes typically earn the full rate, while slots/casino turnover earns a fraction (the same balance cycles through slots many times over).

Earning is an explicit allowlist: nothing earns points unless you list it. That's deliberate – a catch-all rate would quietly start minting points on refundable money (deposits) the moment your data begins sending it. You list the exact event types that earn and the rate for each:

  • purchase at 1 point per 1 [your currency]
  • sportsbook_bet at 1, casino_bet at 0.1 – and deposit simply isn't listed, so it can never earn.

Event types are picked from what your data actually sends (with event counts), plus a custom option for events you haven't started sending yet. If your brand's industry is recognized, the editor suggests a starting shape with the reasoning attached – apply it, then edit the event names to match your own data.

Simulate before you commit

"Simulate on my last 12 months" replays your proposed rules over your real revenue history – the same math the nightly engine runs – and shows:

  • total points that would have been issued, and to how many customers,
  • how points split across your spend types (this is where you catch a deposit accidentally earning),
  • where customers would land across your tiers,
  • worst-case reward liability: if you tell it what one point is worth in rewards, it prices every issued point as redeemed. Reality is cheaper (not all points get redeemed), but the worst case is the number to budget against.

Nothing is saved or awarded by simulating.

Tiers, and what happens when you change them

Tier standing comes from lifetime earned points – redeeming a reward never demotes anyone.

When you edit tiers on a running program, you choose the change policy:

  • Grandfather existing members (default): raising a threshold never demotes someone who already earned the status. Recommended.
  • Re-evaluate everyone: the new thresholds apply strictly on the next nightly run, in both directions.

Using the program in your marketing

  • Tokens in any email, SMS or message: {{p.points_balance}}, {{p.loyalty_tier_name}}, {{loyalty.terms_url}}. Liquid works too: {% if loyalty.points_balance > 1000 %}…{% endif %}.
  • Journeys: trigger on the custom event loyalty_tier_changed to congratulate climbers (the event carries from, to and points).
  • Customer 360: each person's points and tier appear in their value profile.

Integrating redemption into your product

Your customers spend points inside YOUR product – an account page, a rewards tab, a "redeem" button at checkout. Your backend talks to three endpoints, authenticated with an API key with Loyalty program ticked (Settings → API keys). Server-to-server only: never ship the key in browser code.

1. Show the balance (account page header):

bash
curl -H "X-Api-Key: theaicmo_..." \
  "https://theaicmo.com/api/loyalty/balance?identity=anna@example.com"

Returns points, lifetime points, tier, and the program shape (all tiers + thresholds + terms URL) so you can render "2,071 pts · Silver – 7,929 to Gold".

2. List the rewards shop:

bash
curl -H "X-Api-Key: theaicmo_..." \
  "https://theaicmo.com/api/loyalty/rewards"

Returns up to 100 active rewards that have a points price (points_cost) – the brand's own and the account-wide ones. Rewards without a points price stay out of the shop – they're for journeys and manual grants.

3. Redeem (when the customer clicks the button):

bash
curl -X POST -H "X-Api-Key: theaicmo_..." \
  -H "Content-Type: application/json" \
  -d '{"identity": "anna@example.com", "promo_id": "<reward id>",
       "idempotency_key": "order-2041"}' \
  https://theaicmo.com/api/loyalty/redeem

The catalog's points price is charged automatically (you can override with points). The response is the receipt: what was spent, the grant id, and the new balance. Safety built in:

  • Idempotent: retrying with the same idempotency_key returns the original redemption (its grant id, marked duplicate, with the current balance) – never a second debit, even when both requests arrive at the same moment. A key is scoped to the brand.
  • No overdrafts: a redemption larger than the balance answers 402 with the current balance and the required amount. Two redemptions at the same moment are taken one after the other, so together they can never spend more than the balance.
  • The redemption records the reward as a grant for that customer, and a promo_granted event lands in your data – so a journey can follow up ("your free spins are live"). Handing the reward over in your product – a code, the spins themselves – is your backend's step, keyed on the grant id in the receipt.

Terms & conditions

Loyalty programs normally publish terms (how points are earned, expiry, how changes are handled). Link yours in the editor and include it in messages as {{loyalty.terms_url}}. Whether specific disclosures are required for your market stays your call – encode what your license mandates as your own guardrail rules.

Point expiry

If you set an expiry window, points age out nightly on FIFO terms: redemptions are assumed to spend your oldest points first, and whatever earned points fall outside the window (and weren't spent) are debited with an expire entry in the ledger. Expiry reduces the spendable balance only – lifetime points and tier standing are never touched. For each customer, the nightly expiry and a redemption are taken one after the other, so the same points are never both spent and expired, and the balance never goes below zero.

Multiple brands

Points are a brand relationship. Each brand configures its own program, and a customer shared between two of your brands holds a separate balance with each – Brand B can never spend points earned at Brand A. With one program on the account the API resolves the brand automatically; with several, pass client_id to say which brand you mean (the API answers brand_required rather than guessing). In multi-brand accounts, only events stamped with a brand accrue to that brand's pot.

Honest limits (current version)

  • Refunds and chargebacks don't take points back yet, and there is no manual points adjustment yet either.