Appearance
Rewards flow
Two halves of one loop. A shopper works through a campaign — scanning products, placing an order, filling in a profile — and each completed step moves their progress. When the campaign completes they claim the reward, and what lands is a discount they apply in the cart.
Play the flow
Pick Earn a reward for the loyalty half, Spend it at checkout for what happens to the reward afterwards.
4:4713
500 ◉ your points balance
campaigns
Scan 3 products2 of 3 steps›
Place your first ordernot started›
Add your birthdaycomplete›
Screen not designed yet — drawn from what the documented flow requires.
Space plays and pauses, ← and → step. These screens have no design yet, so they are drawn from what the documented calls require.
Progress is not something the app posts
The single most important thing about this flow: the client never reports progress. There is no "complete this step" endpoint. Progress is the server's reaction to an event that happened somewhere else — a scan, an order, a profile update — and the app's job is to read it back.
campaigns/almost-completed is how the app asks "what did that event just move?". It takes an event_item matching exactly one of five shapes, and its body is one of the three in the mobile set validated directly rather than under payload:
event_type | Companion key | Typically fired after |
|---|---|---|
scan-auth | product | a product authentication |
order | order | an order is placed |
collectible_id | collectible | a collectible is claimed |
user_update | user | the profile is edited |
user_login | user | a login |
The full algorithm behind the progress numbers is in campaign progress tracking.
Guest pair, customer pair
Nearly every campaign endpoint exists twice: a Brand mount that lists campaigns, and a Customer mount that adds this user's progress to the same list. So the signed-out rewards screen is not a different screen — it is the same call without the progress.
| What the screen needs | Guest | Signed in |
|---|---|---|
| The campaign list | GET /brand/mobile/loyalty/campaigns | GET /brand/mobile/user/loyalty/campaigns |
| Grouped with banners (prefer this pair) | GET /mobile/loyalty/campaigns/group | GET /mobile/user/loyalty/campaigns/group |
| One campaign | GET /brand/mobile/loyalty/campaigns/:id | GET /brand/mobile/user/loyalty/campaigns/:id |
Campaigns resolve against loyaltyBrandId from the token, falling back to brandId — so which campaigns a shopper sees is decided by the mobile key their app was issued, not by anything in the request. Full field lists are on Loyalty and campaigns.
Claiming: one endpoint, two naming traps
PUT /api/v1/mobile/loyalty/milestone/redeem/:milestoneId/:claimed is the claim. Two things about it catch people:
- The path says
milestone; what it redeems is a campaign step. The vocabulary in the product is campaign → steps; the route kept an older word. A campaign's steps are stored as itsrewards[]. - Its body skips the
payloadwrapper. Send{ "collectibles": [1, 2, 3] }at the top level. Wrapping it, as almost every other endpoint requires, fails.
A claimed reward is a discount
Nothing about spending a reward is a loyalty call. What the claim leaves behind is a discount row against the customer, which is why the second path is entirely cart endpoints:
GET /mobile/discount/reward— what the customer holds.includeExpireddefaults tofalse.POST /mobile/discount/applicable— which of them this bag qualifies for. A different question, andhubIdis required unless the token carries it.PUT /mobile/explicitDiscount— apply codes, as an uppercased string array. They can also ride along onsyncBag, which saves the round trip.PUT /mobile/syncBag— the discounted total comes back from the same call that prices everything else.
Points work the same way: they are spent on the bag (points, plus points_source per line), not on the payment screen. See the e-commerce flow for where that sits in checkout.
Points, balances and history
The rewards screen's balance and history are their own endpoints, separate from campaigns:
| Screen element | Endpoint |
|---|---|
| Points balance | GET /mobile/loyalty/customer — or loyalty/points if the brand is on the points ledger |
| Points history | GET /mobile/loyalty/point_transaction — with limit/offset the response becomes { count, limit, offset, transactions }; without them it is a bare array |
| Scan history | GET /mobile/loyalty/scan_transaction · scanned_products — these always paginate |
| Referrals | GET /mobile/loyalty/referred · referral-transactions |