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. Events that reach us in the hour before the nightly run earn the following night.
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.
Who earns no points
A customer earns no new points while they are on one of your blacklists set to Blocks everything (a self-exclusion register, for example), their data was erased, or a support conversation of theirs was marked sensitive and not cleared (and, on a brand with personalisation set up, while they asked you to stop personalising for them or your exclusion field marks them). What decides it is when our nightly read reaches an event, not when the event arrived: an event the read reaches while one of these holds never earns, not even later. The read stays an hour behind (see the top of this page), and a new program's first nights read your whole history, so a customer one of these holds for on those nights earns nothing on any of their past events. Points they earned before stay theirs: the balance and the tier stand, points expire on your usual terms, and redeeming follows the blacklist rule under Redeem below. Customers with no marketing channel open, known only by your customer ID included, earn as before, and so do test customers on the event types you list beyond your revenue (they never earn on revenue).
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". The balance always answers, also for a customer who cannot redeem (see below): the points are still theirs, and you may need them to answer the customer or your support team. Whether your account page shows them while redeeming is blocked is your call.
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.
Add the customer to get the same answer a redemption would give before you show them the shop. By your own customer id:
curl -H "X-Api-Key: theaicmo_..." \
"https://theaicmo.com/api/loyalty/rewards?identity=P-10234"
By e-mail:
curl -H "X-Api-Key: theaicmo_..." \
"https://theaicmo.com/api/loyalty/rewards?identity=anna@example.com"
Or both:
curl -H "X-Api-Key: theaicmo_..." \
"https://theaicmo.com/api/loyalty/rewards?identity=P-10234&email=anna@example.com"
Use the identifier your loyalty data is keyed on as identity and pass
the other as email or external_id, so both are checked. A phone is
checked too. For a customer who cannot redeem, this answers
403 not_eligible with no rewards in it.
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). If you know the customer by more than one identifier, add
email, external_id or phone to the body: they are checked against
your blacklists, while the points are always spent from identity. 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. - Blacklists apply: a customer on one of your blacklists set to
Blocks everything cannot redeem. Nothing is spent and nothing is
granted. The answer is
403 not_eligible, and it never says which list or why, so your till or account page can show it as it is. The check covers every identifier you send and the ones on the customer's record with you, so an entry on their customer id also stops a redemption made with their e-mail or phone. Lists set to Suppresses marketing or Flag only do not stop a redemption the customer asked for. - Nothing is redeemed unchecked: if the blacklist check cannot run,
the answer is
503 suppression_unavailablewith aRetry-Afterheader, and nothing is redeemed. Retry with the sameidempotency_key. - A blocked customer's retry is refused too: once a customer is on
such a list, a retry with the key of an earlier redemption also answers
403, not the earlier receipt. That redemption still stands, so hand the reward over on the grant id of the first receipt you received.
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.