Third-party cookies are mostly gone. Safari blocks them, Firefox blocks them, and Chrome is finishing the job. If your affiliate attribution leans on a third-party cookie set on the advertiser's domain, a large share of your referrals vanish before checkout.
I build Referralful, a Stripe-native affiliate tool, so I spend most of my week on this exact problem. Here is the approach that survives cookie blocking, with the Stripe-specific parts that trip people up.
The attribution chain
A referral has to survive four hops:
- Someone clicks an affiliate link.
- They land on your site, browse, maybe leave and come back days later.
- They start a Stripe Checkout session.
- They pay, sometimes on a different device.
Each hop is a place to lose the referral. Cookies handle hop 2 badly across browsers and not at all across devices.
Step 1: capture the referral first-party
When the affiliate link hits your domain (?ref=jane), read the code and store it in two places, not one: a first-party cookie on your domain, plus localStorage as a backup. First-party cookies on your own domain are not the ones browsers are killing. The 60-day window most programs advertise is just a cookie max-age.
// on landing, if ?ref= is present
const ref = new URL(location.href).searchParams.get('ref')
if (ref) {
document.cookie = `rf_ref=${ref}; Max-Age=${60*24*60*60}; Path=/; SameSite=Lax`
localStorage.setItem('rf_ref', ref)
}
Step 2: hand the referral to Stripe at checkout
This is the step people miss. Do not rely on the cookie still being readable when the webhook fires. Pass the referral into the Checkout Session as client_reference_id (or metadata) when you create it:
const session = await stripe.checkout.sessions.create({
mode: 'subscription',
line_items: [{ price: priceId, quantity: 1 }],
client_reference_id: readRef(), // 'jane'
metadata: { affiliate_ref: readRef() },
success_url: '...', cancel_url: '...',
})
Now the referral lives inside the Stripe object. It no longer depends on a cookie, a device, or how many days pass before payment.
Step 3: attribute on the webhook, not the browser
Listen for checkout.session.completed and read the reference back. That is your attribution. Server-side, deterministic, and device-independent.
// webhook
if (event.type === 'checkout.session.completed') {
const s = event.data.object
const ref = s.client_reference_id || s.metadata?.affiliate_ref
if (ref) await creditAffiliate(ref, s.customer, s.amount_total)
}
Step 4: recurring commissions follow renewals
The hard part with SaaS is that the first payment is not the only one. Store the affiliate against the Stripe customer at first attribution, then listen to invoice.paid for that customer and credit the commission again on each renewal, for as long as your terms allow (we use the first 12 months of payments).
if (event.type === 'invoice.paid') {
const inv = event.data.object
const ref = await affiliateForCustomer(inv.customer)
if (ref && withinCommissionWindow(inv.customer)) {
await creditAffiliate(ref, inv.customer, inv.amount_paid)
}
}
Three things we learned the hard way
-
Refunds claw back commissions. Listen to
charge.refundedand reverse the credit, or your payouts slowly drift above your revenue. - Self-referrals are most of your fraud. Block a conversion when the affiliate's email or card fingerprint matches the buyer.
-
Coupon codes are a second attribution path. If an affiliate shares a code instead of a link, attribute on the
discountin the session.
None of this needs a third-party cookie. The trick is to stop treating the browser as the source of truth and let Stripe carry the referral through to the webhook.
Disclosure: I build Referralful, which does the above for Stripe SaaS. The pattern is the same whether you build it yourself or buy it.
Top comments (0)