Referrals and the Event Engine
A referral's life: a referrer shares a code, the referee is attributed with
referrals create, and the referral sits pending until a qualifying
event converts it and pays out. The flows below use a campaign whose trigger was
signup.completed -> complete_referral with a 500-point referrer reward
and a 200-point referee reward.
Attributing a referee
snug loyalty referrals validate --referral-code DOCSFRIEND --referee-id docs4-other --ip-address 203.0.113.5
snug loyalty referrals create --referral-code DOCSFRIEND --referee-id docs4-other \
--ip-address 203.0.113.5 --device-fingerprint fp-docs4validate is a dry run - it runs the same fraud checks but persists
nothing, answering { "valid": true } or naming what would fail:
{ "valid": false, "reasons": ["block_same_ip"] }create persists the attribution and returns the referral in status
pending, with the ip and fingerprint stored in context:
{
"referral": {
"referral_id": "docs4-referral:docs4-other",
"referrer_id": "docs-wave",
"referee_id": "docs4-other",
"referral_code": "DOCSFRIEND",
"status": "pending",
"context": { "device_fingerprint": "fp-docs4", "ip_address": "203.0.113.5" },
"created_at": "2026-08-28T12:48:01.329394Z"
}
}Conversion by event
POST /api/v1/loyalty/events (CLI: snug loyalty event) matches the event
name against every active campaign's triggers. A complete_referral
trigger converts the referee's pending referral and fulfills the campaign's
configured rewards; points-type rewards are credited to the ledger
immediately:
snug --output json loyalty event --event-name signup.completed --user-id docs4-other{
"event_id": "MAXhbPWmnLtQNrCdGACc",
"processed": true,
"points_granted": 200,
"matched_campaigns": ["docs4-referral"],
"referral": { "referral_id": "docs4-referral:docs4-other", "status": "converted", ... },
"payouts": [
{ "user_id": "docs-wave", "beneficiary": "referrer", "reward_type": "points",
"value": 500.0, "status": "paid", "transaction_id": "TqXQWTWUqTCsXQzEBXDH" },
{ "user_id": "docs4-other", "beneficiary": "referee", "reward_type": "points",
"value": 200.0, "status": "paid", "transaction_id": "UrEAwrZsqLdNqhnKjfYM" }
]
}points_granted counts points credited to the event's user - the
referrer's 500 appear in the payout list and in their own balance, not in
that number.
Awarding points from events
An award_points trigger reads the point amount from the event's
context.points. Verified with a points-type campaign triggering on
purchase.completed:
snug loyalty event --event-name purchase.completed --user-id docs4-other \
--context '{"points":30,"amount":49.99}'The response reported points_granted: 30 and the balance rose by 30. If
the campaign's referee_reward is a positive points value, it acts as a
multiplier on context.points rather than a fixed amount - leave it
unset for a plain 1:1 credit. Events with no context.points grant
nothing.
Fraud and velocity refusals
All four, against a campaign with block_same_ip: true and
self-referral off:
- Self-referral - the code owner refers themselves:
403 self_referral_blocked(Self-referral is not permitted for this campaign). Allowed only when the campaign setsallow_self_referral. - Duplicate attribution - the same referee referred twice into one
campaign:
409 referral_already_exists. - Same-IP fraud - a second, different referee from an IP the campaign
has already seen:
403 fraud_detected(Fraud detected: block_same_ip). Only enforced when the campaign setsblock_same_ip, and only when--ip-addressis supplied. - Velocity cap - too many referrals by one referrer in the window. With
the default 10 per hour, the 11th create returned
429 velocity_limit_exceeded(Velocity limit exceeded: 10 referrals per 3600 seconds).
An inactive campaign refuses referrals too - validate reports
{ "valid": false, "reasons": ["campaign_inactive"] } (verified after
campaigns update --active false).
--device-fingerprint is stored on the referral but does not currently
participate in any fraud check. The window and cap are the
LOYALTY_VELOCITY_CHECK_WINDOW_SECONDS and
LOYALTY_MAX_REFERRALS_PER_VELOCITY_WINDOW variables in the
Loyalty CONFIG reference.