Tracking is the bridge between a partner's link and a paid order. The flow has two halves: a public click endpoint that records a referrer hit and stores the affiliate code in a cookie or query param, and a server-side conversion step where the Storefront calls AffiliateTrackingService.recordConversion after an order is paid.
Endpoints
Public
/affiliate/public/resolve/:codeNo auth/affiliate/public/trackNo auth/affiliate/public/apply-codeNo authStaff
/affiliate/links/generateJWT/affiliate/referralsJWT/affiliate/referrals/:idJWT/affiliate/referrals/:id/transitionJWT/affiliate/referrals/bulk-transitionJWTPartner self-service
/client/affiliate/me/linkJWT/client/affiliate/me/linkJWT/client/affiliate/me/referralsJWTGenerating a link
A partner can generate their own link, or staff can do it on their behalf.
// Partner — generates a link to a specific product
const { url, code } = await fetch('/client/affiliate/me/link?program=creator', {
method: 'POST',
headers: {
orgid: 'my-org',
Authorization: `Bearer ${jwt}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({ productSlug: 'pro-plan', page: '/pricing' }),
}).then(r => r.json());
// url example: https://shop.example.com/pricing?ref=ALEX-2K9
The ref query param carries the affiliate's code — a short, human-readable handle. Your site can also accept the code through a path segment or a cookie; whatever it sees, it sends to POST /affiliate/public/track.
Resolving a code
When a visitor lands on a page with ?ref=ALEX-2K9, the front-end resolves the code to confirm it's live and to get partner display info:
const res = await fetch(`/affiliate/public/resolve/${code}`, {
headers: { orgid: 'my-org' },
}).then(r => r.json());
// { valid: true, affiliateName: 'Alex Reyes', programName: 'creator', reward: {...} }
// or { valid: false }
If valid, store the code in a cookie keyed by program.trackingCookieAge (typically 30 days).
Tracking a click
await fetch('/affiliate/public/track', {
method: 'POST',
headers: { orgid: 'my-org', 'Content-Type': 'application/json' },
body: JSON.stringify({
code: 'ALEX-2K9',
email: '[email protected]', // optional, if known
source: 'link', // 'link' | 'code' | 'qr'
}),
});
This writes a pending referral record (or updates an existing one if the same email already clicked). Pending referrals expire after attribution.windowDays if no order lands.
Applying a code at checkout
For checkout flows where the user types a referral code (rather than clicking a link), call /apply-code:
const r = await fetch('/affiliate/public/apply-code', {
method: 'POST',
headers: { orgid: 'my-org', 'Content-Type': 'application/json' },
body: JSON.stringify({ code: 'ALEX-2K9', email: '[email protected]' }),
}).then(r => r.json());
if (r.valid) {
// r.reward — the customer-side reward to apply (discount, credit)
// r.affiliateName, r.programName — for the "Referred by ..." UI
}
This combines a resolve and a track in one call.
Conversion attribution
The Storefront automatically calls AffiliateTrackingService.attributeOrder(orgId, orderInfo) after every paid order — you do not call it directly from front-end code. The flow is:
// Inside storefront.service.ts (server-side, after payment success)
await this.affiliateTrackingService.attributeOrder(orgId, {
affiliateCode: order.data.affiliateCode, // from cookie / applied code
orderId: order.sk,
orderNumber: order.data.orderNumber,
orderAmount: order.data.totalAmount,
customerEmail: order.data.customerEmail,
customerId: order.data.customerId,
items: order.data.items,
});
The service is idempotent — calling it twice for the same orderId is a no-op. This matters because the Storefront calls it once on checkout creation and again on the payment webhook; only the first call wins.
Initial referral status
Based on program.payout.holdDays:
holdDays === 0and program checks pass →qualified(commission credited immediately)holdDays > 0→commission_held(commission booked, waiting out the hold window)- Fraud check failure →
rejected - Customer requested refund →
reversed
See Commissions for the full state machine.
Listing referrals
Staff side:
const referrals = await fetch(
'/affiliate/referrals?status=commission_held&program=creator&page=1&pageSize=50',
{ headers: { orgid: 'my-org', Authorization: `Bearer ${jwt}` } }
).then(r => r.json());
Partner self-service:
const mine = await fetch(
'/client/affiliate/me/referrals?program=creator&status=converted',
{ headers: { orgid: 'my-org', Authorization: `Bearer ${jwt}` } }
).then(r => r.json());
Manual transitions
Staff can move any referral through the state machine via the unified transition endpoint:
await fetch(`/affiliate/referrals/${id}/transition`, {
method: 'POST',
headers: { orgid: 'my-org', Authorization: `Bearer ${jwt}`, 'Content-Type': 'application/json' },
body: JSON.stringify({
action: 'reverse', // approve | reject | hold | reverse | restore | expire | recalculate | override-amount
reason: 'Customer refunded',
actor: '[email protected]',
}),
});
Bulk transition by ids or filter is at /affiliate/referrals/bulk-transition.
The legacy /referrals/:id/approve and /referrals/:id/reject endpoints still work — they're thin aliases that route through transition. Use transition directly in new code.