Documentation

Program setup

Create an affiliate program and choose its commission structure.

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

POST/affiliate/programsJWT
GET/affiliate/programsJWT
GET/affiliate/programs/:nameJWT
PUT/affiliate/programs/:nameJWT
GET/client/affiliate/programsJWT

Program 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):

  1. Per-affiliate override — set with PUT /affiliate/affiliates/:id/commission-override. Useful for VIPs or special deals.
  2. Per-product overrideaffiliateCommission on the product record. Per-item commission for high-margin or low-margin SKUs.
  3. 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.