Appearance
Loyalty and campaigns
6. Points, transactions and referrals
Source: routes/v1/loyalty_route.js → loyalty_controller.js, plus referral_route.ts.
| # | Endpoint | Auth | Handler | Params |
|---|---|---|---|---|
| 6.1 | GET /api/v1/mobile/loyalty/customer | Customer | getLoyaltyUser | none |
| 6.2 | GET /api/v1/mobile/loyalty/point_transaction | Customer | getUserPointTransaction | query limit (int ≥1), offset (int ≥0) |
| 6.3 | GET /api/v1/mobile/loyalty/scan_transaction | Customer | getUserScanTransaction | query limit, offset |
| 6.4 | GET /api/v1/mobile/loyalty/scanned_products | Customer | getUserScannedProducts | query limit, offset, group_by (only strain) |
| 6.5 | GET /api/v1/mobile/loyalty/brand | Customer | getLoyaltyBrand | none |
| 6.6 | GET /api/v1/mobile/loyalty | Brand | getLoyaltyBrand | none — same handler as 6.5, guest variant |
| 6.7 | GET /api/v1/mobile/loyalty/referred | Customer | getAllReferrals | none |
| 6.8 | GET /api/v1/mobile/loyalty/referral-transactions | Customer | getAllLoyaltyTransactions | none |
When limit/offset are supplied on 6.2 the response becomes { count, limit, offset, transactions }; without them it is the bare array. 6.3/6.4 always paginate.
6.9 POST /api/v1/mobile/loyalty/refer
Auth: Customer · Handler: postLoginReferral · Body payload: referralCode ✅ (string).
6.10 PUT /api/v1/mobile/loyalty/milestone/redeem/:milestoneId/:claimed
Auth: Customer · Handler: redeemMilestoneReward Path: milestoneId (number) ✅, claimed (number) ✅. Body: not wrapped in payload — send { "collectibles": [1, 2, 3] } (array of numbers, optional).
6.11 POST /api/v1/mobile/loyalty/create-user-point-transaction
Auth: Customer · Handler: createUserPointTransaction Body payload: reason ✅ (string), and exactly one (xor) of points_used, points_earned, amount_spend, amount_earned (numbers). Optional: type, note, reason_id (strings).
6.12 Points ledger service
Some brands' points, transactions and levels are held by a standalone points ledger rather than by the customer backend. These four routes are the backend proxying that service, resolving it by loyaltyBrandId from the token and falling back to brandId. Auth: Customer on all.
| Endpoint | Handler | Params |
|---|---|---|
GET /api/v2/brand/mobile/loyalty/points | getUserPoints | none |
GET /api/v2/brand/mobile/loyalty/transactions | getUserTransactions | query limit, offset |
GET /api/v2/brand/mobile/loyalty/brand-levels | getBrandLevels | none |
POST /api/v2/brand/mobile/loyalty/init | initUser | none — call once after signup to create the loyalty profile |
Read them instead of 6.1–6.4 when the brand is on the ledger; the endpoints above answer for brands that are not. Nothing in the mobile set tells a client which it is — that comes from the brand's own configuration.
⚠️ These pass the ledger's body straight back (
res.send(result)), so there is no{ success, result }envelope around them, and nohandleCatchturning a failure into a 200. Parse them on their own terms.
7. Loyalty campaigns
Source: routes/v1/loyalty_campaign_route.ts → loyalty_campaign_controller.ts
Most campaign endpoints come as a guest pair + customer pair: the customer one adds per-user progress. Campaigns resolve against loyaltyBrandId from the token, falling back to brandId.
7.1 List campaigns
GET /api/v1/brand/mobile/loyalty/campaigns— Brand —getAllLoyaltyCampaignsGET /api/v1/brand/mobile/user/loyalty/campaigns— Customer —getAllLoyaltyCampaignsForUser
Query (all optional): level_id (int, guest variant only — the user variant takes the level from the token), tag (comma-separated, [A-Za-z0-9_-], ≤200 chars), type (comma-separated, same pattern), active (boolean).
7.2 Campaigns grouped by tag
GET /api/v1/brand/mobile/loyalty/campaigns/tag— Brand —getLoyaltyCampaignsByTagsGET /api/v1/brand/mobile/user/loyalty/campaigns/tag— Customer —getLoyaltyCampaignsByTagsForUser
No parameters.
7.3 Campaigns grouped by tag, with banner (newer, prefer over 7.2)
GET /api/v1/mobile/loyalty/campaigns/group— Brand —getLoyaltyCampaignsByTagsWithBannerGET /api/v1/mobile/user/loyalty/campaigns/group— Customer —getLoyaltyCampaignsByTagsWithBannerForUser(adds overall progress)
No parameters. Note these two sit under /api/v1/mobile/..., not /brand/.
7.4 GET /api/v1/brand/mobile/loyalty/campaigns/category/:categoryName
Auth: Brand · Handler: getLoyaltyCampaignsByCategory · Path: categoryName (string) ✅.
7.5 Campaign by id
GET /api/v1/brand/mobile/loyalty/campaigns/:id— Brand —getLoyaltyCampaignByIdGET /api/v1/brand/mobile/user/loyalty/campaigns/:id— Customer —getLoyaltyCampaignByIdForUser
Path: id (integer) ✅. ⚠️ The customer route is declared twice in the route file (identical); the second declaration is dead — see §23.
7.6 POST /api/v1/brand/mobile/user/loyalty/campaigns/almost-completed
Auth: Customer · Handler: getAlmostCompletedCampaigns Body is validated directly, not under payload:
json
{ "event_item": { "event_type": "collectible_id", "collectible": { "collectible_id": 12 } } }event_item ✅ must match exactly one of these shapes:
event_type | Companion key | Shape |
|---|---|---|
order | order | any ✅ |
scan-auth | product | any ✅ |
collectible_id | collectible | { collectible_id: number ✅ } |
user_update | user | { firstName?, dateOfBirth?, phoneNumber?, email? } ✅ |
user_login | user | { mode?, source?, medium? } ✅ |
7.7 GET /api/v1/brand/mobile/loyalty/collectibles/trending
Auth: Brand · Handler: getTrendingCollectibles · Query: tag (comma-separated, optional).
7.8 GET /api/v1/brand/mobile/loyalty/collectible/:collectible_id/campaign
Auth: Brand · Handler: getCampaignsForCollectible · Path: collectible_id (integer) ✅.
7.9 Collectible campaign progress
GET /api/v1/brand/mobile/user/loyalty/collectible/:collectible_id/pre_campaign_progressGET /api/v1/brand/mobile/user/loyalty/collectible/:collectible_id/campaign_progress
Auth: Customer · Both hit the same handler getCampaignsForCollectibleSecondLastStep with identical behaviour. Path: collectible_id (integer) ✅.
Mini-games
A mini-game is played in the app, scored server-side and converted into points. It is held by the same points ledger as 6.12, so these routes are the backend proxying it and the responses come back unenveloped in the same way. Source: routes/v2/miniGames_routes.ts → app/v2/controller/miniGames_controller.ts.
| Endpoint | Auth | Handler | Params |
|---|---|---|---|
GET /api/v2/brand/mobile/mini-games | Brand | listMiniGames | query limit, offset |
GET /api/v2/brand/mobile/mini-game/user | Customer | getMiniGameUser | none |
GET /api/v2/brand/mobile/mini-game/user/all | Customer | getAllMiniGameUser | query limit, offset |
GET /api/v2/brand/mobile/mini-game/leaderboard | Customer | getMiniGameLeaderboard | none |
POST /api/v2/brand/mobile/mini-game/session | Customer | postMiniGameSession | body payload: mini_game_id ✅ (number) → returns session_id |
POST /api/v2/brand/mobile/mini-game/score | Customer | postMiniGameScore | body payload: session_id ✅ (string), score ✅ (number ≥0) |
POST /api/v2/brand/mobile/mini-game/score-conversion | Customer | postMiniGameScoreConversion | body payload: session_id ✅ (string) → converts the score to points |
POST /api/v2/brand/mobile/mini-game/consume-points | Customer | postMiniGameConsumePoints | body payload: item ✅ (only "lives"), mini_game_id (positive int) |
One round is three calls in order: open a session, submit a score against it, then run the score-conversion that credits the points. Buying extra lives is consume-points, and spends points rather than earning them. Both score and score-conversion validate session_id as a string — send it as one whatever type it arrives in.