Loyalty
Referral and rewards infrastructure: campaigns drive referral codes, event-triggered point grants, and payout rules; a per-user points ledger records every credit and debit; and a reward catalog turns points into redemptions - with fraud and velocity controls on the referral path.
When to reach for it: refer-a-friend programs, purchase-driven points earning, redeemable reward catalogs, participant dashboards, campaign conversion analytics.
When not to: discount codes that gate a checkout belong to Promo Code; ranked standings of top earners belong to Leaderboard; a bare counter with no ledger semantics is a KV key.
Concepts
- Campaigns are the top-level construct - there is no "program"
entity. A campaign has a type (
referral,points, ortiered), rules (attribution window, fraud switches), event triggers, and payout rewards. - Referral codes are issued under a campaign and owned by the JWT
subject that creates them. Ids (
campaign_id,reward_id) are caller-supplied; codes can be auto-generated. - Referrals attribute a referee to a code, then advance
pending -> convertedwhen a qualifying event arrives. - The event engine matches ingested events against campaign triggers;
actions are
complete_referral,award_points, orcreate_reward. - The points ledger is per-user and campaign-independent - referral payouts, grants, event awards, and redemptions share one balance and history.
- The reward catalog is separate from campaign payout rewards: catalog items are what users spend points on; campaign rewards configure what referrals and events pay out.
Campaigns
snug loyalty campaigns create --campaign-id docs4-referral \
--campaign-type referral --description "Docs referral campaign" \
--attribution-window-days 30 --block-same-ip
snug loyalty campaigns update --campaign-id docs4-referral --active false
snug loyalty campaigns delete --campaign-id docs4-referral--block-same-ip and --allow-self-referral are bare switches - present
means on, absent means off (both default off).
The CLI creates a campaign with rules only. Event triggers and payout
rewards are configured over HTTP - send them on create, or replace them
later with PATCH /api/v1/loyalty/campaigns/{campaign_id} (verified):
curl -X POST -H "Authorization: Bearer $SNUG_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"campaign_id": "docs4-referral", "campaign_type": "referral",
"description": "Docs referral campaign",
"rules": { "attribution_window_days": 30, "block_same_ip": true },
"triggers": [ { "event_name": "signup.completed", "action": "complete_referral" } ],
"rewards": {
"referrer_reward": { "type": "points", "value": 500, "description": "Referrer bonus" },
"referee_reward": { "type": "points", "value": 200, "description": "Welcome bonus" }
}
}' http://localhost:4000/api/v1/loyalty/campaigns{
"status": 201,
"msg": "Created",
"data": {
"campaign": {
"campaign_id": "docs4-referral",
"campaign_type": "referral",
"active": true,
"rules": { "attribution_window_days": 30, "block_same_ip": true, ... },
"triggers": [ ... ],
"rewards": { ... },
"created_at": "2026-08-28T12:47:52.242639Z"
}
}
}GET /api/v1/loyalty/campaigns speaks the
shared search grammar plus an
active_only flag; note that q matches the description, not the
campaign id (q=Docs finds the campaign above, q=docs4 does not).
Referral codes
snug loyalty codes create --campaign-id docs4-referral --custom-code DOCSFRIEND
snug loyalty codes create --campaign-id docs4-referral # auto-generated, e.g. CZMEFAXL
snug loyalty codes get --user-id docs-waveThe code is owned by the calling JWT subject (the referrer) and the
response includes a shareable referral_link.
The referral arc
snug loyalty referrals validate --referral-code DOCSFRIEND --referee-id user456 --ip-address 203.0.113.5
snug loyalty referrals create --referral-code DOCSFRIEND --referee-id user456 --ip-address 203.0.113.5
snug loyalty event --event-name signup.completed --user-id user456validate is a dry run, create persists a pending referral, and a
qualifying event converts it and pays both sides. The full lifecycle -
triggers, payouts, award_points events, and every fraud and velocity
refusal - is on Referrals and the event engine.
Points ledger
snug --output json loyalty points balance --user-id docs4-other
snug loyalty points grant --user-id docs4-other --points 100 --reason "Docs bonus"
snug loyalty points history --user-id docs4-otherThe balance payload carries balance, lifetime_earned, and
lifetime_redeemed (captured: 200 / 200 / 0 after one referee reward).
Grants only credit: --points must be positive, otherwise
400 invalid_points_amount (write --points=-50 to even reach the server;
a bare -50 is rejected by the CLI parser). Debits happen only through
redemption. History entries carry transaction_type, amount,
balance_after, a reason (system-generated ones look like
referral_reward:docs4-referral or event:purchase.completed), and
campaign_id when a campaign caused the entry - newest first, up to the
configured history limit.
Reward catalog and redemption
snug loyalty rewards create --reward-id docs4-mug --name "Docs Mug" \
--description "Ceramic mug" --points-cost 250 --stock 5 --active
snug loyalty rewards list --active-only
snug loyalty rewards update --reward-id docs4-mug --active false
snug --output json loyalty points redeem --user-id docs4-other --reward-id docs4-mug
snug loyalty points redemptions --user-id docs4-other{
"redemption_id": "tbaraMeVCJgYRsCDcrMe",
"user_id": "docs4-other",
"reward_id": "docs4-mug",
"points_spent": 250, "new_balance": 50
}Behaviors, each reproduced live:
- Redemption spends the reward's
points_cost- you pick a reward, not an amount. The raw HTTP body requires apointsfield, but its value is ignored: sending"points": 1still charged 250. - A balance below the cost fails with
400 insufficient_points(Insufficient points: required 100000, available 50). - A stocked reward decrements on each redemption; at zero it fails with
400 reward_out_of_stock. An inactive one fails with400 reward_inactive. There is no delete endpoint - deactivate instead. - Redeeming, listing redemptions, and the dashboard are self-or-admin: with
another user's id they fail
403 FORBIDDENunless the token is a platform admin.
Dashboard and analytics
snug loyalty dashboard --user-id docs-wave
snug loyalty analytics --campaign-id docs4-referralThe dashboard is referrer-centric: current points, total earnings,
per-campaign stats (signups, conversions, paid rewards), and recent
referrals for codes the user owns. Analytics aggregates one campaign into
total_participants, total_referrals, total_conversions,
conversion_rate, and total_rewards_distributed - after the referral
arc above it reported 1 / 1 / 1 / 1.0 / 700.0.
Limits and configuration
Campaign count (100), auto-generated code length (8), the referral velocity
window and cap, and the points history page limit are tunable via the
LOYALTY_* variables in the
Loyalty CONFIG reference.
Reference
- Loyalty API - every endpoint, callable
- Related: Promo Code for checkout discount codes, Leaderboard for ranked standings, KV for raw counters