A program is the ruleset all referrals run against — commission shape, attribution window, hold period, fraud guards, payout config. You can run more than one in parallel; each affiliate is enrolled into one specific program (or several, with separate stats).
Endpoints
/affiliate/programsJWT/affiliate/programsJWT/affiliate/programs/:nameJWT/affiliate/programs/:nameJWT/client/affiliate/programsJWTProgram shape
type AffiliateProgram = {
name: string; // unique within the org; used as the join key
title: string; // display name
description?: string;
status: 'draft' | 'active' | 'paused' | 'archived';
type: 'referral' | 'influencer' | 'partner' | string;
trackingCookieAge: number; // days; default 30
commission: CommissionConfig;
referredReward: ReferredRewardConfig;
attribution: {
windowDays: number; // how long after click to credit; default 30
model: 'first_click' | 'last_click';
allowSelfReferral: boolean;
};
payout: {
holdDays: number; // commission stays 'held' for this long; default 7
minPayout: number;
autoCredit: boolean;
};
fraud: {
maxReferralsPerDay: number;
requireUniqueEmail: boolean;
requirePaidOrder: boolean;
minOrderAmount: number;
blockSameIP: boolean;
};
stats: {
totalAffiliates: number;
totalReferrals: number;
totalConversions: number;
totalCommissionPaid: number;
conversionRate: number;
};
};
Commission structure
Three types are supported, plus per-affiliate and per-product overrides on top.
Flat
Every conversion pays the same fixed amount.
{
"commission": {
"type": "flat",
"flatAmount": 25
}
}
Percentage
A percentage of the order amount, with an optional cap.
{
"commission": {
"type": "percentage",
"percentage": 10,
"maxCommission": 500
}
}
Tiered
Percentage that scales with the affiliate's lifetime conversion count. The first tier whose [minConversions, maxConversions] range contains the affiliate's current totalConversions wins.
{
"commission": {
"type": "tiered",
"tiers": [
{ "minConversions": 0, "maxConversions": 10, "percentage": 10 },
{ "minConversions": 11, "maxConversions": 50, "percentage": 15 },
{ "minConversions": 51, "percentage": 20 }
],
"maxCommission": 1000
}
}
Override precedence
When calculating commission for an order, the service applies these in order (first match wins):
- Per-affiliate override — set with
PUT /affiliate/affiliates/:id/commission-override. Useful for VIPs or special deals. - Per-product override —
affiliateCommissionon the product record. Per-item commission for high-margin or low-margin SKUs. - Program-level commission — the program config above.
Items with affiliateEligible: false are skipped in per-item calculations.
Creating a program
await fetch('/affiliate/programs', {
method: 'POST',
headers: {
orgid: 'my-org',
Authorization: `Bearer ${jwt}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
name: 'creator',
title: 'Creator Partner Program',
description: 'For content creators promoting our courses',
status: 'active',
type: 'influencer',
trackingCookieAge: 60,
commission: {
type: 'tiered',
tiers: [
{ minConversions: 0, maxConversions: 25, percentage: 15 },
{ minConversions: 26, percentage: 20 },
],
maxCommission: 500,
},
referredReward: { type: 'discount', value: 10, valueType: 'percentage' },
attribution: { windowDays: 60, model: 'last_click', allowSelfReferral: false },
payout: { holdDays: 14, minPayout: 50, autoCredit: true },
fraud: {
maxReferralsPerDay: 50,
requireUniqueEmail: true,
requirePaidOrder: true,
minOrderAmount: 0,
blockSameIP: true,
},
}),
});
The name is the immutable join key — change title for display, but keep name stable since referral records reference it.
Updating
await fetch('/affiliate/programs/creator', {
method: 'PUT',
headers: { orgid: 'my-org', Authorization: `Bearer ${jwt}`, 'Content-Type': 'application/json' },
body: JSON.stringify({ status: 'paused' }),
});
The PUT does a partial merge into data. Existing affiliates and referrals stay attached; pausing a program just stops new attributions.
Referred-reward config
The referredReward block defines what the new customer (the one being referred) gets. Common shapes:
// 10% discount
{ "type": "discount", "value": 10, "valueType": "percentage" }
// $20 credit
{ "type": "credit", "value": 20 }
// No reward — the partner gets paid, the customer pays full price
{ "type": "none" }
Discount-style rewards generate a one-time coupon on the order; credit rewards push to the customer's wallet via Finance.
Listing programs
// Staff
const programs = await fetch('/affiliate/programs?status=active', {
headers: { orgid: 'my-org', Authorization: `Bearer ${jwt}` },
}).then(r => r.json());
// Customer (only active programs they can join)
const joinable = await fetch('/client/affiliate/programs', {
headers: { orgid: 'my-org', Authorization: `Bearer ${jwt}` },
}).then(r => r.json());
The stats block on a program record is denormalised — it's updated by the tracking service on every attribution and payout, not computed on read. Use GET /affiliate/stats/program/:programName for ad-hoc queries that don't have to be cheap.