Affiliates
Affiliate management and per-order attribution for Medusa v2. Link Medusa promotion codes to affiliate accounts, track earned revenue per order across the placed/captured/completed/voided lifecycle, and enforce stacking rules at cart time.
Features
Section titled “Features”- Affiliate accounts with contact info, addresses, and status (
active,restricted,inactive) - Link one or more Medusa promotion codes to an affiliate via a module link
- Per-order attribution ledger with
placed_at,captured_at,completed_at, andvoided_attimestamps - Nightly reconciliation job that back-fills lifecycle timestamps and picks up orders missed by real-time events
- On-demand recalculation endpoint per affiliate
- Cart-level guard that prevents an affiliate code from stacking with other promotions when disabled
- Stats endpoint per affiliate with day/week/month/year/all windows, bucketed by currency
- Admin pages under
/admin/affiliatesfor managing affiliates, addresses, and linked promotion codes
Installation
Section titled “Installation”Inside your medusa backend root folder:
yarn add medusa-plugin-affiliatesReplace “yarn add” with the correct command for your package manager if you are using (for example) npm, pnpm, or bun.
After installing the plugin, you must run Medusa’s migration tool to create the plugin’s database tables.
yarn medusa db:migrateConfiguration
Section titled “Configuration”Enable in your medusa-config.ts file. Example:
module.exports = defineConfig({ //... other config plugins: [ { resolve: 'medusa-plugin-affiliates', options: { payoutBasis: 'completed', defaultCommissionBasis: 'gross', defaultCommissionRate: 0.1, allowStackingWithNonAffiliatePromotions: false } } // ... other plugins ]})Plugin options
Section titled “Plugin options”| Option | Type | Default | Description |
|---|---|---|---|
payoutBasis |
'placed' | 'captured' | 'completed' |
'completed' |
Which order lifecycle event triggers commission earning. placed counts revenue as soon as the order is placed; captured waits until payment is captured; completed waits until the order is fully completed (fulfilled). Voided orders are always excluded regardless of setting. |
defaultCommissionBasis |
'gross' | 'net' |
'net' |
Whether commission is calculated against the order subtotal before discounts (gross) or after discounts (net). Used as the default when an individual affiliate does not override it. |
defaultCommissionRate |
number |
unset | Default commission rate applied when an individual affiliate does not have one set. Expressed as a decimal (0.1 = 10%). If neither the default nor a per-affiliate rate is set, attributions record commission_rate: null until backfilled. |
allowStackingWithNonAffiliatePromotions |
boolean |
true |
If false, the cart-level workflow hook rejects any attempt to combine an affiliate promotion code with a non-affiliate promotion code on the same cart. Rejection surfaces to the storefront as a NOT_ALLOWED error. |
At most one affiliate promotion code is allowed on a cart at a time regardless of allowStackingWithNonAffiliatePromotions — this is a hard constraint enforced by the same guard.
Attribution model
Section titled “Attribution model”Each row in the affiliate_attribution table represents one order attributed to one affiliate through one linked promotion:
affiliate_id,promotion_id,order_id— the triad the row is keyed by (order_idis unique)currency_code,gross_subtotal,net_subtotal— captured at the time of the initial insertplaced_at,captured_at,completed_at,voided_at— filled in as the order’s state advancescommission_rate,commission_amount,payout_id,paid_at— reserved for downstream payout tooling
Attributions are inserted the first time the reconciliation job (or the on-demand endpoint) sees the order. Later runs update lifecycle timestamps as the order moves through its states; existing timestamps are never overwritten.
Nightly attribution reconciliation job
Section titled “Nightly attribution reconciliation job”A scheduled job at 0 1 * * * (01:00 daily, server time) walks recent orders and updates the attribution ledger:
- Uses a watermark based on the latest ledger timestamp minus 24 hours of overlap so nothing gets missed at the boundary
- Only touches orders whose
updated_atis after the watermark - Inserts a new attribution row the first time an affiliate-linked promotion appears on an order
- Updates
captured_at,completed_at, andvoided_aton existing rows as those states become true; never overwrites a previously-set timestamp - Skips orders that don’t carry any affiliate-linked promotion codes
Like all Medusa scheduled jobs, it only runs on worker processes (WORKER_MODE=worker or shared). Success and failure are logged with the affiliate module’s logger.
On-demand recalculation
Section titled “On-demand recalculation”Trigger the same reconciliation for a single affiliate at any time:
POST /admin/affiliates/:id/attributions/recalculateUseful after fixing a mis-linked promotion, importing historical orders, or investigating a discrepancy.
Promotion-stacking guard
Section titled “Promotion-stacking guard”The plugin registers a validate hook on updateCartPromotionsWorkflow (and the equivalent draft-order workflow) that inspects incoming promo codes against the current cart’s codes:
- Rejects if more than one affiliate code is applied at once
- Rejects if an affiliate code is added to a cart with non-affiliate codes when
allowStackingWithNonAffiliatePromotions: false - Rejects if a non-affiliate code is added to a cart with an affiliate code under the same option
- When a new affiliate code replaces an existing one, the old affiliate code is automatically removed
All rejections throw MedusaError.Types.NOT_ALLOWED with a message the storefront can surface.
Per-affiliate stats are available at:
GET /admin/affiliates/:id/stats?window=<day|week|month|year|all>&basis=<placed|captured|completed>Response buckets orders by currency, with order_count, gross_total, net_total, average_order_value_gross, and average_order_value_net. Voided attributions are excluded regardless of the window.
The affiliate’s own currency_code (if set) is returned as the first bucket; remaining currencies follow alphabetically.
- Create and manage affiliates from Settings > Affiliates in the Medusa admin.
- Attach one or more addresses per affiliate and mark one as primary for tax/payout routing.
- Link Medusa promotion codes to an affiliate from the affiliate detail page. Use standard Medusa promotion configuration (percentage or flat, rules, etc.) — this plugin only owns the affiliate ↔ promotion link and the attribution ledger.
- Reactivate and retire promotion links without deleting them via the promotion detail actions.
- Review earnings per affiliate via the Stats section on the detail page or the
/statsendpoint.