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:
purchaseat 1 point per 1 [your currency]sportsbook_betat 1,casino_betat 0.1 – anddepositsimply 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_changedto congratulate climbers (the event carriesfrom,toandpoints). - 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):
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:
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):
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_keyreturns the original redemption (its grant id, markedduplicate, 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
402with 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_grantedevent 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.