The hard part of affiliate tracking on Stripe isn't the first sale. It's making month 6's renewal invoice still know which affiliate to pay.
Here's the situation. An affiliate link sets ?ref=abc. Someone clicks it, pokes around, closes the tab, and subscribes three days later through Stripe Checkout. By the time invoice.payment_succeeded fires, and again every month after that, the browser is long gone. So where does the affiliate ID actually live?
Step 1: capture the ref on click and persist it
When a visitor lands with ?ref=abc, store it in a first-party cookie (match your cookie window, say 60 days) with localStorage as a backup:
const params = new URLSearchParams(window.location.search);
const ref = params.get("ref");
if (ref) {
document.cookie = `affiliate_ref=${ref}; max-age=${60 * 24 * 60 * 60}; path=/; SameSite=Lax`;
localStorage.setItem("affiliate_ref", ref);
}
Step 2: attach the ref when you create the Checkout Session
This is where most implementations lose the recurring case. client_reference_id is handy, but it lives on the session, not on future invoices. For commissions that recur, put the ref on the subscription's metadata too:
const session = await stripe.checkout.sessions.create({
mode: "subscription",
line_items: [{ price: priceId, quantity: 1 }],
client_reference_id: ref, // attributes THIS checkout
subscription_data: {
metadata: { affiliate_ref: ref }, // rides along on every renewal invoice
},
success_url: "https://yourapp.com/welcome",
cancel_url: "https://yourapp.com/pricing",
});
Step 3: read it in the webhook, on the invoice, not just the session
Credit the commission from the recurring event so renewals attribute, not only the first payment:
// invoice.payment_succeeded
const invoice = event.data.object;
const subscription = await stripe.subscriptions.retrieve(invoice.subscription);
const ref = subscription.metadata.affiliate_ref;
if (ref) {
await creditCommission({
affiliateRef: ref,
invoiceId: invoice.id, // idempotency key, see below
amount: invoice.total_excluding_tax, // already post-discount, post-proration
});
}
The gotchas that bite later
-
client_reference_idlives on the Checkout Session only. If that's your sole source of truth, month 2 renews with no affiliate attached. Store the ref on the subscription (or customer) metadata so it survives. - Stripe metadata is a flat string map with capped keys and values. Store the affiliate's id, not a JSON blob of their whole profile.
- A plan change can spin up a new subscription. Metadata doesn't copy itself forward, so carry it over on upgrade, or attribute at the customer level instead.
- You will receive the same invoice webhook more than once. Key the commission insert on
invoice.id(a unique index) so a retry can't double-pay.
The one-line version: attribute on the subscription, settle on the invoice. Do that and a referral you earned in January still pays out correctly in July.
I build Referralful, Stripe-native affiliate software for SaaS, so this is the exact plumbing it handles for you. The pattern above works the same whether you use us or roll your own.
Top comments (0)