Stripe Affiliate Tracking: How Attribution Works
A technical walkthrough of the Stripe webhook events and metadata that keep affiliate commissions accurate through the subscription lifecycle.
"Stripe-native" gets thrown around a lot. Here's what it actually takes.
I hear "Stripe-native" used to describe affiliate tools that do nothing more than pull a nightly CSV of Stripe customers and match names against a spreadsheet of referral codes. That's Stripe-adjacent, not Stripe-native. Real attribution means listening to the event stream Stripe actually sends and reacting to it as a subscription changes, rather than only when someone first signs up.
This is the level of detail I wish someone had handed me before we built the Stripe integration for Referralful. I ended up piecing it together from six different doc pages and a couple of old GitHub issues, so I'm writing the version I wish had existed. If you're evaluating whether a tool, ours or anyone else's, does this properly, here's exactly what "properly" involves.
Where attribution actually happens: three separate moments
Affiliate tracking on Stripe isn't one event. It's three separate problems solved in sequence:
- Capturing who gets credit, before the customer pays anything.
- Confirming the credit only counts once money actually moves.
- Keeping that credit accurate as the subscription changes shape over its lifetime.
Most affiliate tracking implementations solve step 1 and stop there. That's why so many programs quietly overpay on canceled subscriptions and underpay on upgrades: nobody wired up steps 2 and 3.
Step 1: attach the affiliate ID before checkout, in two places
When someone clicks an affiliate's referral link, a cookie gets set (60 days by default at Referralful) storing which affiliate sent them. When that visitor starts checkout, you attach the affiliate ID to the Stripe Checkout Session, usually through client_reference_id or a custom key in metadata.
Here's the part almost every tutorial skips. Metadata on a Checkout Session lives only on that Session object. It doesn't automatically show up on the Subscription or Customer created from it. You need the affiliate ID to survive on the subscription, because that's the object telling you about renewals, upgrades, and cancellations for the next year, so you have to pass it explicitly through subscription_data.metadata when you create the session, not just the top-level metadata field. Skip that and your affiliate ID disappears the moment the real subscription object takes over.
Step 2: don't attribute on session creation, attribute on completion
A Checkout Session can be created and abandoned. Cards get declined, tabs get closed, people bail on the last field. If your tracking logic fires the moment someone starts checkout, you'll credit affiliates for sales that never closed.
The event to listen for is checkout.session.completed. It fires once the session actually finishes: Stripe has the customer's payment method and has created the subscription behind it. Pull the affiliate ID back out of the session's metadata here and write the referral to your database. This is also the first point where you know the real subscription ID, customer ID, and plan, so it's the right place to calculate the first commission, pending final confirmation from customer.subscription.created, which can still land as incomplete if extra authentication is required.
Step 3: subscribe to what happens after the sale
This is what separates a real Stripe integration from a one-time webhook handler.
| Event | What it means for commissions |
|---|---|
customer.subscription.created | Confirms the subscription exists; status can be incomplete if payment authentication is pending, so hold the payout if it is |
customer.subscription.updated | Fires on renewals, plan changes, and coupon or discount changes; this is how you catch an upgrade or downgrade and adjust a percentage-based commission going forward |
customer.subscription.deleted | The subscription is canceled; stop accruing new commission for that customer from here on |
charge.refunded | The customer got money back; claw back or void the tied commission rather than let it get paid out |
A tool that only listens to checkout.session.completed gets the first commission right and goes blind after that. A customer upgrading from a $49 plan to a $149 plan in month three should show up in a percentage-based payout. A customer who cancels in month two should stop accruing commission in month two, not run forever. A refund needs to reverse the commission tied to it. None of that happens without the full lifecycle, and it's the real reason recurring-commission programs, ours defaults to 30% for the first 12 months, need more plumbing than a one-time affiliate payout ever did.
What about trials that haven't converted yet
If your checkout flow includes a free trial, through trial_period_days or an explicit trial_end on the subscription, the status Stripe sends is trialing, not active. Most SaaS affiliate programs I've seen, ours included, wait for the trial to convert before paying anything out, because a trial signup isn't revenue yet. Stripe fires customer.subscription.trial_will_end three days before the trial ends as a heads up, and then the subscription moves to active, or to past_due or canceled if the card fails, once the first real invoice is due. That transition, caught through customer.subscription.updated, is the actual moment a trial becomes a paying customer. It's the moment a commission-on-conversion program should fire, not the moment someone starts the trial.
Testing this without waiting for a real customer
Stripe's CLI lets you forward test webhooks to a local server with stripe listen --forward-to localhost:3000/webhooks/stripe, and stripe trigger checkout.session.completed fires a fake event through that same pipe. Run through the whole sequence in test mode: checkout completed, subscription created, then manually trigger an update and a cancellation, before you point any of this at a live Stripe account. Catching a bug in test mode costs nothing. Catching the same bug because an affiliate got shorted their commission costs you a conversation you don't want to have.
A note on webhook reliability
None of this works if you're not verifying the Stripe-Signature header on every webhook request against your endpoint's signing secret. Skip that and you're trusting unauthenticated POST requests to move real commission money. Stripe also retries webhooks that don't return a 2xx response, so the handler needs to be idempotent: the same checkout.session.completed event landing twice should never create two commissions for one sale. We key commission records off the Stripe event ID for exactly this reason.
Why this matters more for SaaS than for one-time sales
An ecommerce affiliate program is basically done after step 2: someone buys a t-shirt, the affiliate gets paid once, done. A SaaS subscription keeps generating events for years. A customer who upgrades from a $99/mo plan to a $299/mo plan eighteen months in is worth roughly three times as much to the affiliate who sent them, and only a lifecycle-aware integration catches that automatically. That's the real argument for a Stripe-native tool over a generic tracking pixel: commission has to follow the subscription's actual state well past the moment it started. We wrote a broader walkthrough of the rest of the setup, cookies, commission rules, payout timing, in our guide to setting up a SaaS affiliate program, and if you'd rather see the raw API surface than take my word for how the pieces fit, the endpoints are documented at developers.referralful.com.
If you're building this yourself instead of buying it, the affiliate program software comparison covers what a dedicated tool handles for you versus what you'd be maintaining on your own webhook infrastructure.
FAQ
What Stripe webhook event should trigger the first affiliate commission?
checkout.session.completed, not the creation of the Checkout Session. That's the point where you know the customer actually completed payment setup, not just started it.
Does Checkout Session metadata automatically appear on the subscription it creates?
No. Metadata set directly on a Checkout Session stays on that Session object only. To have it show up on the resulting Subscription, you need to pass it separately through subscription_data.metadata when you create the session.
How does a Stripe-native tool handle a customer who upgrades their plan?
By listening for customer.subscription.updated, comparing the new plan against the old one, and recalculating the commission going forward if the program pays a percentage of revenue rather than a flat amount.
What happens to commission if a customer gets refunded?
A properly built integration listens for charge.refunded and reverses or withholds the associated commission rather than letting it get paid out as if the sale still stood.
If you want to see this whole flow running instead of just reading about it, spin up a free Referralful account and watch the webhook log fill in real time.
Run your SaaS affiliate program on Stripe
Referralful tracks every referral, calculates commissions, and pays affiliates through Stripe. Free until your first affiliate joins.