Stripe Payment Links attribution: find the channel behind each sale
Stripe stores no campaign or referrer for a Payment Link sale. See what it does keep, 3 ways to tag each sale with its channel, and how renewals inherit it.
Muzahid Maruf, FounderUpdated
On this page
Explore with AI
Opens this article inside the chosen assistant with a ready-made prompt.
To attribute a Stripe Payment Links sale, put the channel in the client_reference_id (200 characters) or in link metadata (50 keys, 500-character values), the only fields that reach the sale.
UTM codes on a link reach only your redirect URL, because no Stripe API object has a field for them.
The Checkout Session has no referrer field either, and you publish no pages on buy.stripe.com, where the Google tag for Google Analytics 4 would have to run.
Key takeaways
- UTM codes reach only your redirect URL and never the Checkout Session, so the source must ride in client_reference_id or link metadata.
- Stripe's limits: 150 characters per UTM value, 50 metadata keys per object, and a 200-character client_reference_id that a 36-character UUID fits.
- A renewal creates no Checkout Session, so copy the channel onto the subscription after payment 1.
- TrackRev is affiliate software with attribution built in, and its pixel tags buy.stripe.com links on its own, and revenue figures start at $29 a month.
What Stripe keeps from a Payment Link sale
Stripe's page on tracking a payment link lists 5 UTM codes and the client_reference_id parameter. UTM codes work only with redirect confirmation, and they are appended to that redirect URL.
The Checkout Session object has no UTM field, and Stripe's OpenAPI spec for API version 2026-09-30 has no utm_ field on any object.
| Field | Where you set it | Where Stripe puts it | Limit or catch |
|---|---|---|---|
| UTM codes | Query string of the link URL (5 codes) | Appended to your redirect URL after payment | Redirect confirmation required; 150 characters per value; no Session field keeps them |
client_reference_id | Link URL, Buy Button or pricing table attribute | Checkout Session and the checkout.session.completed event | 200 characters; letters, digits, dashes, the _ character; invalid values dropped silently |
| Link metadata | Payment Link API object | Copied onto each Checkout Session the link creates | 50 keys, 40-character key names, 500-character values; same for every buyer |
payment_intent_data.metadata | Payment Link API object | PaymentIntents the link creates in payment mode | One-time prices only |
subscription_data.metadata | Payment Link API object | Subscriptions the link creates | Needs at least 1 recurring price |
| Link analytics | Automatic | Payments and analytics tab in the Stripe Dashboard | Views, sales and revenue only; up to 18 hours late; unavailable for recurring prices |
Sources: Stripe's payment link tracking page, metadata guide and API reference for creating a payment link, October 2026.
Link metadata is the same for every buyer, so it names a channel only when each channel has its own link.
Setting it is an API job, since the Stripe Dashboard steps on Stripe's tracking page cover only the client reference ID.
Why Google Analytics 4 and Safari lose the source
Google's cross-domain setup needs the same G- tag ID on every domain in the measurement, and you publish no pages on Stripe's, so Google Analytics 4 can log the click on your page but never the purchase.
The GA4 revenue-by-channel guide shows the symptoms.
In Safari, WebKit's Intelligent Tracking Prevention deletes script-written cookies and localStorage after 7 days without interaction, so a newsletter reader who buys 8 days after the click arrives with no stored ID.
A Set-Cookie header from your own server falls outside that rule, so I'd set the ID there. The Safari ITP post covers the rest.
Three methods for Payment Links attribution
Pick by volume: 2 or 3 channels fit the first method, affiliates, many placements and renewals need the second, and the third adds UTM capture to a thank-you page you already run.
One Payment Link per channel, no code
Create a Payment Link per placement and read Stripe's per-link numbers in the Payments and analytics tab, which skips links with recurring prices, so this method suits one-time products.
To keep an exportable reference, click the arrow beside Copy, choose URL parameters, then Client reference ID, and enter a value such as nl-2026-10 for the October newsletter.
An example with made-up numbers: a $49 template pack sells through 3 URLs, with 14 sales on the newsletter link, 6 on a Google Ads link and 2 on the affiliate link.
Revenue is $686, $294 and $98, which sums to $1,078. Refund 2 of the 14 newsletter sales and that link's net revenue falls to 12 × $49 = $588.
Against a made-up $250 of Google Ads spend, the 6 sales return $294, or $1.18 per $1. Every new placement costs a new URL.
A visitor ID joined on the Stripe webhook
On the first visit, create a UUID, store it in a first-party cookie and save the landing page, referrer and UTM codes against it. Add the UUID as client_reference_id to each Payment Link; its 36 characters fit the 200-character limit.
// On your own pages, before the visitor can click a Payment Linkfunction visitorId() { var m = document.cookie.match(/(?:^|; )cid=([^;]+)/); if (m) return m[1]; var id = crypto.randomUUID(); // 36 characters: hex digits and dashes document.cookie = "cid=" + id + "; Max-Age=31536000; Path=/; SameSite=Lax; Secure"; return id;} var cid = visitorId(); // Send the landing context; your endpoint keeps the first row per IDnavigator.sendBeacon("/api/visits", JSON.stringify({ cid: cid, landing: location.href, referrer: document.referrer})); document.querySelectorAll('a[href^="https://buy.stripe.com/"]').forEach(function (a) { var u = new URL(a.href); u.searchParams.set("client_reference_id", cid); a.href = u.toString();});On the paid event, the handler joins the reference to the stored visit and writes the channel to 2 Stripe objects: the subscription or PaymentIntent, plus the customer.
// Inside your Stripe webhook handler, after stripe.webhooks.constructEvent()// Handle 2 events: ACH debits and other delayed methods fire the async one days laterconst paidEvents = ["checkout.session.completed", "checkout.session.async_payment_succeeded"]; if (paidEvents.includes(event.type)) { const session = event.data.object; if (session.payment_status === "unpaid") return; // delayed method: wait for the async event const visit = await db.visits.findFirst(session.client_reference_id); // may be null const metadata = { channel: visit ? visit.source : "unknown", // your own label, e.g. "newsletter" campaign: visit ? visit.campaign : "none", cid: session.client_reference_id || "none", }; // Renewals inherit subscription metadata; one-time sales use a guest customer, // so the PaymentIntent is the object to write to there if (session.mode === "subscription") { await stripe.subscriptions.update(session.subscription, { metadata }); } else if (session.payment_intent) { await stripe.paymentIntents.update(session.payment_intent, { metadata }); } if (session.customer) { await stripe.customers.update(session.customer, { metadata }); }}Test locally with stripe listen from the Stripe CLI. The Buy Button docs and pricing table docs take the same reference as a client-reference-id attribute, with the same 200-character limit.
A redirect page that records the Checkout Session
Set the confirmation behavior to redirect and put {CHECKOUT_SESSION_ID} in the URL. Per Stripe's post-payment page, Stripe fills in the session ID and appends the link's UTM codes (up to 5), so your thank-you page gets both.
The page records a sale only if the buyer's browser loads it, and 2 buyers on one tagged URL look identical. The free UTM builder makes the URLs.
Subscriptions, delayed payments and refunds
client_reference_id sits on the Checkout Session, and a renewal creates none: month 2 is an invoice.
Per the Invoice object reference, each invoice carries a snapshot of the subscription metadata taken at finalization, for invoices created on or after June 29, 2023 (in API version 2026-09-30 it sits at parent.subscription_details.metadata). Write the channel to subscription metadata.
Suppose 10 buyers from one newsletter take a $29 monthly plan for 12 months. That is 10 × $29 × 12 = $3,480, and the first payments are $290 of it, about 8%.
Crediting only the first charge makes the newsletter look 12 times smaller. The subscription LTV attribution guide reports lifetime revenue by channel.
Stripe says bank debits and vouchers can take 2 to 14 days to confirm, and a refund typically takes 5 to 10 business days to reach a statement.
Count revenue on a paid status and drop it when you issue the refund.
What TrackRev reads from Payment Link sales
TrackRev is SaaS affiliate software, and its pixel is the built-in tracking layer (Indie at $29 a month for 1 site, Growth at $299 a year for up to 10).
On pages where the pixel runs, it adds its 36-character visitor ID as client_reference_id to every buy.stripe.com link and to Buy Button and pricing table embeds.
A read-only restricted Stripe key lets TrackRev find the Checkout Session behind each paid charge and credit the click behind that reference. With no ID, matching falls back to the email passed to identify().
| Setting | Value |
|---|---|
| Pixel link sweeps | At load, at 0.8, 1.5 and 2.5 seconds, and on click |
| Stripe sync and window | Hourly poll, or seconds with the optional webhook; 90-day attribution window |
| Default model | Last touch; first touch and linear are 1 dropdown away |
| Renewals and refunds | Renewals inherit the original click, and a refund removes the credit on the next sync |
| Visitor cookie | Written from JavaScript with a 1-year lifetime; Safari's 7-day rule can delete it sooner |
| Paid plans, all with Revenue Attribution | Indie $29 a month (1 workspace, 1 tracked domain), Growth $299 a year (up to 10 workspaces, 10 tracked domains) |
| Free plan | 50 links and 1,000 tracked events a month, no revenue figures |
| Where it adds no ID | Links pasted into email, DMs or a bio, and links that already carry a client_reference_id |
Source: TrackRev's pixel script and plan limits, October 2026.
When your own webhook is enough
One product on 2 or 3 links needs no tool, and an existing webhook endpoint makes the visitor-ID method a few dozen lines plus a visits table.
Renewals, refunds and delayed payments are the harder part, and affiliates turn each one into a commission change, which is where I'd pay for a tool.
Checkout Sessions you create yourself take metadata at creation, and the Checkout attribution guide and metadata setup guide cover that route.
Found this useful? Share it.
Frequently asked questions
- Only in the redirect. With redirect confirmation, Stripe appends the 5 UTM codes to your redirect URL, and the Stripe API has no field to read them back.
- No. The 200-character limit allows letters, digits, dashes and the _ character only, so the @ and period in an email make Stripe drop the value silently.
- Give each send a URL with its own client_reference_id, such as nl-2026-10 or dm-batch-3, since no script runs in a mail client.
- A renewal is an invoice, and only the first payment has a Checkout Session, so copy the channel into subscription metadata after payment 1.
- No. TrackRev polls paid charges hourly with a restricted key, and the optional webhook cuts that to seconds. Without Checkout Session read access, matching falls back to email.
- Last touch credits the channel that sent the buyer to the link, and first touch credits the one that introduced them. TrackRev defaults to last touch with a 90-day window.

Written by
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