Skip to content

Stripe UTM tracking: pass UTMs into Checkout metadata

How to pass UTM parameters and visit IDs into Stripe Checkout metadata, what breaks with Payment Links and renewals, and a simpler path with traffic.cx.

Karthick Published Last updated 9 min read

On this page

Stripe Checkout has no built-in field for UTM parameters. The tags live on the landing URL; Checkout is created later on your server, and nothing joins the two unless you copy the tags (or a visit ID) into the session. The usual DIY path: capture utm_source, utm_medium and utm_campaign when the visitor arrives, put them in Checkout Session metadata when checkout starts, and for subscriptions also set subscription_data.metadata so renewals keep the same keys. It works, but you own the capture, the copy rules, the renewals and the gaps. If you mainly want "which campaign brought paying customers", a tool that already ties Stripe payments to the visit is less work.

Last updated 4 November 2026. The Stripe steps below are based on Stripe's docs as of that date; we haven't run them on a live Checkout Session or Payment Link. The docs can change, so the links are inline. For a tested walkthrough that tags a campaign link and follows it through to a payment in traffic.cx, see Track a UTM campaign through to revenue. For connecting Stripe payments into GA4 instead, see How to connect Stripe to Google Analytics 4.

What Stripe does and doesn't do with UTMs

A Checkout Session accepts metadata (key-value pairs you set) and an optional client_reference_id. It has no UTM parameter of its own (Stripe Create a Checkout Session reference, checked 4 November 2026).

Payment Links are different: you can append utm_source, utm_medium, utm_campaign, utm_content and utm_term to the buy URL. With confirmation behaviour set to redirect, Stripe adds those same UTM values to your redirect URL after payment. That carries the campaign tags to the thank-you page; it does not, by itself, store them as searchable campaign rows next to every renewal and refund.

So for server-created Checkout you write the tags into metadata yourself. For Payment Links, Stripe can echo UTMs on the redirect URL, and you can also set client_reference_id on the link for reconciliation.

Capture UTMs (and optionally a visit ID) into Checkout

1. Save the tags when the visit starts

Read the query string on the first page of the visit and keep the values until checkout. A common pattern is sessionStorage or a first-party cookie you control. Use lowercase names with underscores (newsletter, not Newsletter) so later reports don't split the same campaign into two rows.

js

// On the landing page, once. Example only: adapt to your stack.
const params = new URLSearchParams(window.location.search);
const utm = {
  utm_source: params.get('utm_source') || '',
  utm_medium: params.get('utm_medium') || '',
  utm_campaign: params.get('utm_campaign') || '',
  utm_content: params.get('utm_content') || '',
  utm_term: params.get('utm_term') || '',
};
sessionStorage.setItem('utm', JSON.stringify(utm));

Don't put an email address, name or customer ID in a UTM tag. Campaign links get shared, logged and forwarded.

2. Put them in Checkout Session metadata

When the visitor starts checkout, send the stored tags to your server. Your server creates the Checkout Session and writes them as metadata. Stripe allows up to 50 key-value pairs, with keys up to 40 characters and values up to 500 (Stripe metadata docs, checked 4 November 2026). Skip empty values: posting an empty string unsets a key.

For subscriptions, also set subscription_data.metadata. Stripe does not copy Checkout Session metadata onto the Subscription automatically; you set it with subscription_data.metadata so subscription and invoice events carry the same keys (Stripe: metadata use cases; Stripe: metadata copy rules).

js

// Server-side helper. Example only: keep auth, cart checks and price IDs on the server.
import Stripe from 'stripe';

const stripe = new Stripe(process.env.STRIPE_SECRET_KEY);

export async function createCheckout({ utm, visitIds }) {
  const metadata = {
    ...Object.fromEntries(
      Object.entries(utm).filter(([, value]) => value),
    ),
    ...visitIds, // optional: e.g. tcx_pvid / tcx_vid for traffic.cx
  };

  const session = await stripe.checkout.sessions.create({
    mode: 'subscription',
    line_items: [{ price: process.env.STRIPE_PRICE_ID, quantity: 1 }],
    success_url: 'https://example.com/thanks',
    cancel_url: 'https://example.com/pricing',
    metadata,
    subscription_data: { metadata }, // renewals / invoices can read the same keys
  });

  return session.url;
}

For one-off payments (mode: 'payment'), session metadata is enough. If you create your own PaymentIntents, put the same keys on the PaymentIntent's metadata. Stripe includes that metadata on the object inside webhook events such as checkout.session.completed (Stripe: metadata).

With a Payment Link and no server session:

  1. Append UTM codes to the buy URL when you share it, or when the visitor clicks, if you want Stripe to echo them on the redirect URL after payment (Stripe: track a payment link). UTM values may use alphanumeric characters, dashes or underscores, up to 150 characters; invalid values are discarded.
  2. Set confirmation behaviour to redirect if you need those UTMs on the thank-you URL.
  3. For a per-click visit ID, set client_reference_id on the URL (up to 200 characters, alphanumeric / dashes / underscores). It is returned on checkout.session.completed for reconciliation. Don't put secrets in it, and don't overwrite the field if your app already uses it for another reference.

html

<a href="https://buy.stripe.com/test_123" data-stripe-link>Buy now</a>
<script>
  // Example: attach a pageview ID as client_reference_id when using traffic.cx.
  document.querySelectorAll('[data-stripe-link]').forEach((link) => {
    link.addEventListener('click', () => {
      const id = window.trafficcx?.pageviewId?.();
      if (id) {
        const url = new URL(link.href);
        url.searchParams.set('client_reference_id', `tcx_${id}`);
        link.href = url.toString();
      }
    });
  });
</script>

Payment Link metadata you set on the link itself is copied onto each Checkout Session the link creates in a one-time snapshot (Stripe: metadata). That helps when each campaign has its own link; it doesn't replace reading UTMs from a shared landing page into a server-created session.

4. Subscriptions and renewals

Put the campaign tags (or visit IDs) on subscription_data.metadata at Checkout creation. Stripe then sets them on the Subscription, and invoice events can expose them under the subscription details snapshot (Stripe: metadata use cases). Your own reporting still has to decide whether a renewal credits the original campaign or a later visit. Stripe stores the keys; it doesn't run an attribution model for you.

Where the DIY method gets hard

  • You still need somewhere to join payment and visit. Metadata on Stripe is storage. Dashboards that only list charges won't turn utm_campaign=october_newsletter into revenue per visitor or conversion rate by channel unless you build that report, or send the payment into an analytics tool.
  • Copy rules are easy to miss. Session metadata alone doesn't land on the Subscription. Forget subscription_data.metadata and renewal invoices won't carry the tags (Stripe: metadata).
  • Payment Link UTMs land on the redirect URL. If the buyer pays and never hits your thank-you page, or you only listen to webhooks, those query-string UTMs never reach your server unless you also wrote them into link metadata or client_reference_id somehow.
  • Success-page counting misses people. Buyers who close the tab after paying never fire a thank-you pixel. Count from checkout.session.completed / invoice.paid, not from the redirect alone.
  • You can't see what didn't match. If you only credit payments that have UTM keys, payments without them disappear from campaign totals. Nothing in Stripe's metadata model labels that missing share as "Unattributed" for you.

The simpler path with traffic.cx

Instead of storing raw UTM strings on every Stripe object and building the join yourself, traffic.cx records the visit (including its source and campaign tags) and links the payment with one visit ID.

From the Stripe setup guide and Stripe revenue by traffic source (checked 4 November 2026):

  • Connect Stripe with a restricted key (Checkout Sessions, Invoices, PaymentIntents and Refunds read; Webhook Endpoints write). traffic.cx creates the webhook and imports the last 90 days of payments.
  • Pass tcx_pvid (and, in cookie mode, tcx_vid) into Checkout Session metadata; for subscriptions, put the same keys on subscription_data.metadata so renewals carry them. Payment Links take the pageview ID as client_reference_id with a tcx_ prefix.
  • Read the IDs in the browser with trafficcx.pageviewId() / trafficcx.visitorId() when checkout starts, validate them on your server, then add them to the session (how revenue attribution works).
  • Subscription renewals and refunds arrive from Stripe on their own. Renewals for a known customer inherit the attribution of that customer's first payment. Payments that can't be matched stay in your totals as Unattributed instead of dropping out of the report (UTM to revenue walkthrough).
  • You choose last non-direct (default) or first touch, with a lookback of 90 days by default (settable 7–365 days).

The tracker sets no visitor cookie by default. In that mode, a payment links to the visit where checkout started; optional cookie mode also links earlier visits from the same browser (how revenue attribution works).

Revenue attribution is included on every plan, Free too: one site, 3,000 events a month, the last 30 days of reports. Sign up with Google or email; no card is needed and there's no trial that runs out (pricing, checked 4 November 2026). A payment received through a provider integration does not count as an event.

Keep DIY metadata if you need the raw UTM strings inside Stripe for your own CRM or fulfilment. The two can run together: pass both your UTM keys and tcx_pvid if you want. For a fuller picture of Stripe revenue by source once the connector is live, see Stripe revenue by traffic source.

FAQ

Does Stripe track UTM parameters automatically?

Not on server-created Checkout Sessions. There is no UTM field on the Session; you write the tags into metadata (or client_reference_id) yourself. On Payment Links, you can append UTM codes to the buy URL; with redirect confirmation, Stripe adds them to your redirect URL after payment (Stripe docs, checked 4 November 2026).

Where do I put UTMs on a Stripe Checkout Session?

In the Session's metadata when you create it. For subscriptions, also set subscription_data.metadata so the Subscription and later invoices carry the same keys (Stripe: metadata use cases).

Yes, as URL parameters on the Payment Link. Stripe documents utm_source, utm_medium, utm_campaign, utm_content and utm_term, returned on the redirect URL when confirmation is set to redirect (Stripe: track a payment link). Use client_reference_id if you need a per-click visit or order ID on checkout.session.completed.

Will subscription renewals keep the original UTM tags?

Only if you stored them on the Subscription via subscription_data.metadata (or update the Subscription later). Checkout Session metadata alone is not copied to the Subscription (Stripe: metadata). Even then, Stripe stores the keys; your reporting decides how renewals are credited.

How do I see Stripe revenue by UTM campaign without building the join myself?

Use an analytics tool that connects to Stripe and attributes payments to visits. In traffic.cx, you connect Stripe with a restricted key and pass the visit's pageview ID into checkout metadata; the revenue report then shows revenue by channel, source, campaign and landing page (Stripe setup guide).

Is traffic.cx free?

Yes, on one site: every feature, 3,000 events a month and the last 30 days of reports. Sign up with Google or email; no card is needed and there's no trial that runs out (pricing).