DEV Community

Mihir kanzariya
Mihir kanzariya

Posted on

Tying a Stripe subscription back to the affiliate who referred it

Every affiliate program has the same first hard problem: when a payment succeeds, which affiliate gets the credit? The tracking link and the Stripe payment are two different worlds, and nothing connects them unless you deliberately carry the attribution through. Here is how to thread it end to end on Stripe.

The chain you have to preserve

A referred signup passes through several Stripe objects, and the affiliate id has to survive every hop:

click (cookie) -> Checkout Session -> Subscription -> Invoice -> Charge

Drop the id anywhere in that chain and the webhook that finally tells you money moved has no idea who to pay.

Start at Checkout

When you create the Checkout Session, stamp the affiliate id on it. Two fields matter:

const session = await stripe.checkout.sessions.create({
  mode: 'subscription',
  line_items: [{ price: PRICE_ID, quantity: 1 }],
  // handy, but lives only on the session
  client_reference_id: affiliateId,
  // propagates onto the Subscription you are about to create
  subscription_data: {
    metadata: { affiliate_id: affiliateId },
  },
  success_url: '...',
  cancel_url: '...',
});
Enter fullscreen mode Exit fullscreen mode

client_reference_id is convenient but it stays on the Session. The important one is subscription_data.metadata, because that copies the affiliate id onto the Subscription itself, which is the object every future renewal traces back to.

Read it off the invoice, not the session

For a subscription you do NOT want to grant commission on checkout.session.completed. That fires once, at signup, so you would miss every renewal. Listen to invoice.paid instead, which fires for the first payment AND every recurring one:

// webhook: invoice.paid
const invoice = event.data.object;

// pull the subscription to read the metadata you stamped at checkout
const subscription = await stripe.subscriptions.retrieve(invoice.subscription);
const affiliateId = subscription.metadata.affiliate_id;
if (!affiliateId) return; // not a referred customer

await recordCommission({
  affiliate_id: affiliateId,
  invoice_id: invoice.id,
  amount_cents: Math.round(invoice.amount_paid * COMMISSION_RATE),
  // first invoice or a renewal? billing_reason tells you
  kind: invoice.billing_reason, // 'subscription_create' vs 'subscription_cycle'
});
Enter fullscreen mode Exit fullscreen mode

billing_reason is what lets you enforce rules like commission for the first 12 months only, or first-payment bounties, without guessing.

The cookie-to-checkout gap

The one part Stripe cannot help with is getting the affiliate id onto the Checkout Session in the first place. That is on you: read the affiliate cookie on your server when the user clicks subscribe, and pass it into the session create call. Rely on a client-side value and an adblocker or a cleared cookie silently drops the attribution, so the affiliate never gets paid for a sale they earned.

Why metadata beats your own lookup table

You could keep a table mapping customer to affiliate. But subscriptions get their billing anchor changed, customers get merged, test data leaks into prod. Stamping the id directly on the Stripe object means the source of truth travels with the object. When you are debugging a missing commission six months later, subscription.metadata.affiliate_id is a five-second answer instead of a join across three tables.

(I work on Referralful, which does this attribution automatically on top of Stripe. The mechanics above are the same whether you build it or buy it.)

Top comments (0)