Stripe revenue by traffic source, without cookies: a practical setup
"Which channel brings paying customers?" sounds like a basic question, but most analytics setups answer it badly. The visit happens in the browser, while the payment happens on Stripe's checkout page and is confirmed on Stripe's servers. Something has to connect the two, and that link is where revenue gets lost.
This guide explains the three common approaches and their trade-offs, then walks through a setup that attributes Stripe payments, including subscription renewals, without cookies.
Why revenue goes missing in GA4
- Consent. In the EU and UK, GA4 usually runs behind a consent banner. Visitors who decline are not measured, so their purchases have no source. Consent mode can model some of it, but modelled numbers are estimates.
- The thank-you page isn't reliable. A purchase event fired on the success page is lost when the buyer closes the tab, the redirect fails, or an ad blocker drops the request.
- Renewals never touch your site. A subscription renewal happens entirely inside Stripe. Unless you send it to analytics from your server, it doesn't exist there, and lifetime revenue per channel is badly understated.
- Refunds, currencies and duplicates. Client-side events don't know about refunds, and a reloaded thank-you page can count the same order twice.
Three ways to connect a payment to a visit
| Approach | How it works | Weak spots |
|---|---|---|
| Thank-you page event | JavaScript on the success page sends a purchase with the amount | Lost on closed tabs and blockers; no renewals; easy to double-count |
| Server event with a client ID | Your backend sends the payment with the analytics client ID (e.g. GA4 Measurement Protocol) | Needs the client ID, which lives in a cookie; you must store it at checkout |
| User ID join | The site tells analytics who the signed-in user is; the payment carries the same user ID | Only works for signed-in buyers; guest checkouts stay unattributed |
For SaaS products, where people sign up before paying, the user ID join is the most robust: it needs no cookie, survives closed tabs, and works for every renewal.
The setup, step by step
1. Identify signed-in users
After sign-in, tell your analytics tool the user's ID. Use your internal ID, not an email address. In PageLens:
pagelens.identify(user.id)
2. Pass the same ID to Stripe
When you create a Checkout Session, set client_reference_id. For subscriptions, also copy it into the subscription's metadata, because renewal invoices don't carry client_reference_id:
const session = await stripe.checkout.sessions.create({
mode: 'subscription',
line_items: [{ price: 'price_123', quantity: 1 }],
client_reference_id: user.id,
subscription_data: { metadata: { pagelens_user_id: user.id } },
success_url: 'https://example.com/welcome',
cancel_url: 'https://example.com/pricing',
});
3. Listen to the right webhook events
checkout.session.completedfor one-time payments. Checkpayment_status === 'paid', because delayed payment methods complete the session before the money arrives.invoice.paidfor subscriptions, both the first payment and every renewal.
Don't count a subscription checkout from both events, or the first payment is counted twice. A simple rule: ignore checkout.session.completed when mode is subscription and let invoice.paid handle it.
4. Handle the details
- Verify signatures. Check the
Stripe-Signatureheader against your endpoint's signing secret, using the raw request body, and reject old timestamps. - De-duplicate. Stripe retries webhooks until you return 2xx. Store each payment's ID and ignore repeats.
- Convert amounts. Stripe sends amounts in the smallest unit: 4900 means $49.00. Zero-decimal currencies such as JPY or KRW are the exception, where 1000 means ¥1000.
- Pick an attribution rule. A common choice is the buyer's most recent visit within 30 days, which credits the channel that brought them back to pay. Keep payments you can't match as "unattributed" rather than dropping them, so totals still add up to what Stripe reports.
What you can answer once it works
- Revenue and revenue per visitor by channel, referrer and UTM campaign, so you can compare paid ads with SEO on money, not clicks.
- Which landing pages and countries produce buyers.
- Whether visitors from ChatGPT and Perplexity actually pay, a question most dashboards can't answer yet.
Doing it without writing the webhook handler
PageLens revenue attribution does steps 3 and 4 for you: add the webhook URL in Stripe (or Paddle), paste the signing secret, and payments appear by channel, campaign, country and landing page. It handles signatures, retries, subscriptions and zero-decimal currencies, and needs no cookies. For checkouts other than Stripe and Paddle, one pagelens.track('purchase', { revenue: 49, currency: 'USD' }) call on the success page works too.