How to track attribution through Stripe Checkout

A Stripe Checkout Session holds 200 characters of reference ID and 50 metadata pairs but no UTM code. Add a visitor ID before the redirect and read it back.

Muzahid Maruf — Founder of TrackRev.io

Muzahid MarufUpdated

Stripe attribution · 8 min read
On this page
  1. 01What a Checkout Session keeps and what it drops
  2. 02Why analytics tools lose the buyer at Checkout
  3. 03Stripe Checkout attribution tracking, step by step
  4. 04A worked example with numbers
  5. 05What this setup leaves uncovered
  6. 06What TrackRev does with a Checkout Session

Explore with AI

Opens this article inside the chosen assistant with a ready-made prompt.

Stripe Checkout attribution tracking comes down to one handoff: your server puts a visitor ID on the Checkout Session before the redirect, and your webhook reads it back when the payment completes.

A Checkout Session has no UTM, referrer or landing page field, and Stripe's OpenAPI spec for API version 2026-09-30 has no UTM field on any object.

Stripe's Checkout lifecycle has 4 steps: your server creates the Session, the buyer is redirected to its URL, the buyer pays, and a webhook reports the payment. Attribution gets written in step 1 and read in step 4.

Key takeaways

  • A Stripe Checkout Session returns only what your server wrote at creation: one client_reference_id of up to 200 characters and up to 50 metadata pairs. UTM codes have no field.
  • Create the Session when the buyer clicks Buy, record the link on checkout.session.completed and revenue on invoice.paid, keyed by subscription ID so payment 12 of a $49 plan credits the channel of payment 1.
  • Success-page counts miss buyers who pay and close the tab, and Stripe retries a failed webhook for up to 3 days, so count sales from the webhook.
  • Safari deletes JavaScript-written cookies after 7 days without interaction, so save the visitor ID on the user record at signup.

What a Checkout Session keeps and what it drops

A Checkout Session returns the attribution data your server wrote at creation and nothing else about the visit.

FieldSet withComes back onLimit
Reference IDclient_reference_idThe Session and checkout.session.completed200 characters
Session metadatametadataThe Session and its events50 pairs, 40-character keys, 500-character values
Payment-mode copypayment_intent_data.metadataThe PaymentIntentSame 50, 40 and 500 limits
Subscription-mode copysubscription_data.metadataThe SubscriptionSame 50, 40 and 500 limits
Return URLsuccess_url with {CHECKOUT_SESSION_ID}The URL the buyer lands onStripe fills in the Session ID
Buyer emailcustomer_email, to prefillcustomer_details.email800 characters
UTM codes, referrer, landing pageNo parameter existsNowhere0 fields

Limits from Stripe's Create a Checkout Session reference and metadata guide, October 2026.

Stripe's metadata guide says metadata doesn't copy to related objects on its own, so a source written only on the Checkout Session never reaches the Subscription or its invoices. The Stripe metadata setup guide lists the copy rules.

Why analytics tools lose the buyer at Checkout

Where it breaksWhat happensNumber to knowFix
Payment pageYour tags don't run on Stripe's page, including one served from a custom domain such as payments.example.com1 custom domain per accountWrite the ID into the Checkout Session
Return tripGoogle Analytics 4 files the visit as a referral50 unwanted referrals per streamExclude checkout.stripe.com
Success pageCounts miss paid buyers and repeat on refresh3 days of webhook retriesCount from the webhook
SafariCookies created in JavaScript are deleted7 days without interactionSet the cookie from your server

Stripe's Google Analytics 4 funnel guide tags only your product, success and canceled pages, so a client-side tag sees the Buy click and the return trip.

It says to add checkout.stripe.com to the referral exclusion list, which Google's unwanted referrals page documents under Admin, Data streams, Configure tag settings. The GA4 revenue-by-channel guide covers the revenue mismatch.

WebKit's tracking prevention page says Intelligent Tracking Prevention deletes cookies created in JavaScript, plus localStorage and other script-writable storage, after 7 days without the visitor interacting with the site.

A reader who clicks a newsletter link on Monday and next opens the site 9 days later arrives with no stored ID.

I'd set the cookie from your server and save the visitor ID on the user record at signup, since SaaS buyers usually register before they pay. The Safari ITP post covers what else survives.

Stripe Checkout attribution tracking, step by step

Build against a Stripe sandbox, where test card 4242 4242 4242 4242 with any future expiry and any 3-digit CVC completes a payment, and run stripe listen --forward-to localhost:4242/webhook to receive events locally.

Step 1: Give every visitor an ID and save the first visit

On the first page view, create a 36-character UUID, set it as a cookie from your server, and store the landing URL, referrer and 5 UTM parameters against it. Write first touch once and let last touch update.

Use a random ID in place of an email address, since an anonymous visitor has no email yet. The stored referrer covers links whose UTM codes a shortener stripped (why UTM parameters get stripped).

Step 2: Create the Session when the buyer clicks

Create the Checkout Session inside the click handler, because one built when the pricing page loads carries whatever the cookie held at that moment and can sit open for 24 hours.

Per Stripe's redirect guide, hosted pages take a success_url and embedded Checkout takes a return_url. Embedded Checkout renders on your page, so your cookie and tags stay in place while the buyer pays.

Express route behind the Buy button (Node)
// vid is the visitor ID from your first-party cookie; visit is the row saved in step 1app.post("/create-checkout-session", async (req, res) => {  const vid = req.cookies.vid;  const visit = await getVisit(vid);   const session = await stripe.checkout.sessions.create({    mode: "subscription",    line_items: [{ price: "price_123", quantity: 1 }],    client_reference_id: vid,                 // 36 characters, the limit is 200    metadata: { vid },                        // visible on the Session itself    subscription_data: {                      // copied onto the Subscription      metadata: { vid, first_touch: visit.firstSource, last_touch: visit.lastSource },    },    // payment mode takes payment_intent_data.metadata in place of subscription_data    // embedded Checkout takes return_url in place of success_url    success_url: "https://example.com/welcome?session_id={CHECKOUT_SESSION_ID}",    cancel_url: "https://example.com/pricing",  });   res.redirect(303, session.url);});

The funnel guide notes that success-page counts miss purchases when the redirect fails and over-count refreshes, and Stripe's fulfillment guide requires webhooks for that reason.

According to the webhook guide, live-mode deliveries retry for up to 3 days, the same event can arrive twice, and order isn't guaranteed, so verify the Stripe-Signature header, log event IDs and key rows by object ID.

Bank debits and vouchers can take 2 to 14 days to confirm, as Stripe's Payment Links tracking page says, so also listen for checkout.session.async_payment_succeeded.

A subscription's first invoice repeats the $49 the Checkout Session reports, so pick one revenue source.

I'd count revenue from invoices and use the Checkout Session only to learn who the buyer was, which sends payment 1 and payment 12 down the same path.

A one-time payment has no invoice by default, so count the Checkout Session's amount_total once payment_status is paid.

Webhook handler (Node)
app.post("/webhook", express.raw({ type: "application/json" }), async (req, res) => {  let event;  try {    event = stripe.webhooks.constructEvent(      req.body, req.headers["stripe-signature"], process.env.STRIPE_WEBHOOK_SECRET);  } catch (err) {    return res.sendStatus(400);  }  if (await seenEvent(event.id)) return res.sendStatus(200);   // deliveries can repeat   const obj = event.data.object;  if (event.type === "checkout.session.completed" && obj.client_reference_id) {    await saveLink({      sessionId: obj.id,      subscriptionId: obj.subscription,          // null in payment mode      vid: obj.client_reference_id,      email: obj.customer_details?.email,        // fallback join key    });  }  if (event.type === "invoice.paid") {    await saveRevenue({      invoiceId: obj.id,      subscriptionId: obj.parent?.subscription_details?.subscription ?? obj.subscription,  // older API versions      cents: obj.amount_paid,      reason: obj.billing_reason,                // subscription_create, then subscription_cycle    });  }  res.sendStatus(200);});

When a webhook endpoint listens for checkout.session.completed and a success URL is set, Checkout waits up to 10 seconds for your response before redirecting the buyer, so write the link row first and defer slow work.

Step 4: Carry the channel to renewals and refunds

A renewal creates no Checkout Session, so it earns its channel through the subscription ID saved in step 3.

A charge.refunded event carries a PaymentIntent, and Stripe's Invoice Payments API can look up the invoice that PaymentIntent paid, which leads back to the subscription and the channel to debit. The subscription LTV guide covers lifetime revenue by channel.

Step 5: Test the chain before you trust it

Run a sandbox subscription checkout, retrieve the Checkout Session to confirm the visitor ID, and check that both events reached your tables with 1 shared subscription ID.

Then delete the cookie and run a second checkout to exercise the email fallback.

A worked example with numbers

Take 40 new subscribers on a $49 plan, which adds $1,960 of MRR. With an ID on every Checkout Session the revenue splits by channel, and unmatched buyers stay visible as their own row.

ChannelNew subscribersNew MRRShare of new MRR
Newsletter sponsorship14$68635.0%
Google Ads9$44122.5%
Affiliate links6$29415.0%
Organic search5$24512.5%
No ID found6$29415.0%
Total40$1,960100%

Illustrative figures, not customer data.

One newsletter subscriber who stays 12 months pays $588, yet a report built only from Checkout Sessions credits the newsletter with $49.

MonthInvoices paidCredited with the subscription joinCredited from Sessions only
11$49$49
33$147$49
66$294$49
1212$588$49

Illustrative figures for one $49 subscriber.

The 6 unmatched buyers in the first table stand for $3,528 a year (6 × $588), and the newsletter's 14 subscribers are worth $8,232 (14 × $588) if none cancels, the figure to weigh against what the sponsorship cost.

What this setup leaves uncovered

Buyers who click on a phone and pay on a laptop arrive with a second cookie, so only the email they registered with can join them.

Payment Links skip your server-side Session creation and need the ID added another way, which the Payment Links attribution guide walks through.

What TrackRev does with a Checkout Session

TrackRev is SaaS affiliate software with link tracking and revenue attribution built in, and its pixel writes a first-party cookie named vid that lasts 365 days.

Pass it as client_reference_id when your server creates a Checkout Session; the Stripe integration page covers the pixel and the restricted key.

Because the pixel writes the cookie from JavaScript, WebKit's 7-day rule applies, so call trk.identify(email) at signup to enable an email match as well.

TrackRev reads Stripe with a restricted key and checks 4 places for the visitor ID, in this order.

OrderWhere the sync looks
1metadata.vid on the charge
2client_reference_id on the Checkout Session
3metadata.vid on the Checkout Session
4The buyer's email, once identify() has run

Lookup order in TrackRev's Stripe sync.

A renewal inherits the first order's visitor through the subscription ID, and a refunded charge flips its order to refunded on the next sync.

The sync runs hourly, or on 3 webhook events (charge.succeeded, charge.refunded and checkout.session.completed) when the key allows webhook writes. Revenue figures need a paid plan, from Indie at $29 a month (pricing), with last-touch, first-touch or linear credit.

The free plan covers link tracking with 50 links and 1,000 tracked events a month.

Found this useful? Share it.

PostLinkedIn

Frequently asked questions

Muzahid Maruf — Founder of TrackRev.io

Written by

Muzahid Maruf

Founder, TrackRev.io & Contant.io

Muzahid Maruf founded TrackRev.io, SaaS affiliate software with no limit on tracked revenue, and Contant.io. He writes about affiliate programs.

Writes about Marketing attribution · Link tracking · Revenue analytics · SaaS growth

Stop guessing where your revenue comes from.

Set up TrackRev in about five minutes. The free plan covers 1,000 events a month, no card needed.

Start free