Skip to content

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.

1 / 7
4:4713
rewards
500 ◉ your points balance
history
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_typeCompanion keyTypically fired after
scan-authproducta product authentication
orderorderan order is placed
collectible_idcollectiblea collectible is claimed
user_updateuserthe profile is edited
user_loginusera 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 needsGuestSigned in
The campaign listGET /brand/mobile/loyalty/campaignsGET /brand/mobile/user/loyalty/campaigns
Grouped with banners (prefer this pair)GET /mobile/loyalty/campaigns/groupGET /mobile/user/loyalty/campaigns/group
One campaignGET /brand/mobile/loyalty/campaigns/:idGET /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 its rewards[].
  • Its body skips the payload wrapper. 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:

  1. GET /mobile/discount/reward — what the customer holds. includeExpired defaults to false.
  2. POST /mobile/discount/applicable — which of them this bag qualifies for. A different question, and hubId is required unless the token carries it.
  3. PUT /mobile/explicitDiscount — apply codes, as an uppercased string array. They can also ride along on syncBag, which saves the round trip.
  4. 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 elementEndpoint
Points balanceGET /mobile/loyalty/customer — or loyalty/points if the brand is on the points ledger
Points historyGET /mobile/loyalty/point_transaction — with limit/offset the response becomes { count, limit, offset, transactions }; without them it is a bare array
Scan historyGET /mobile/loyalty/scan_transaction · scanned_products — these always paginate
ReferralsGET /mobile/loyalty/referred · referral-transactions

Batch platform, Shopify app and API documentation