If you use Stripe Checkout or Payment Links, you have a tracking problem. The checkout happens on Stripe's domain, your tracking pixels never see the purchase, and by the time the customer lands on your thank-you page, you have a session ID but no order data.

Most solutions involve either exposing your Stripe secret key client-side (don't do that), building a Node.js proxy, or stitching together Zapier/Make workflows that give you partial data at best.

I built a server-side GTM client template that solves this with zero extra infrastructure. One sGTM template + one JS snippet on your thank-you page. That's it.

How it works

The flow is straightforward:

Stripe Checkout → Thank-you page → JS snippet → sGTM Client → Stripe API → GA4 / Meta CAPI / Google Ads / etc.

  1. Customer completes payment on Stripe and gets redirected to your thank-you page with ?session_id={CHECKOUT_SESSION_ID} in the URL
  2. A small JS snippet on the page reads the session ID, collects all browser cookies (GA, Meta, TikTok, LinkedIn, etc.), and POSTs everything to your sGTM endpoint
  3. The sGTM client template calls the Stripe Checkout Sessions API server-side with your secret key
  4. Stripe returns the full session object - line items, customer details, tax, discounts, payment status
  5. The template builds a complete GA4 purchase event and runs the container
  6. All your sGTM tags fire with the enriched data
  7. The snippet removes the session ID from the URL so refreshing the page doesn't fire the event again

Your Stripe secret key never leaves the server. The browser only sends the session ID and cookies.

What data you get

The template extracts everything useful from the Stripe session and structures it for GA4 and Meta CAPI compatibility.

Ecommerce

  • transaction_id from the Stripe payment intent ID
  • value, currency, tax, shipping - all converted from Stripe's cents format
  • coupon and discount from promotion codes
  • payment_type - card, klarna, link, etc.
  • items[] with item_name, item_id, price, quantity, item_variant

User data

Stripe collects customer information during checkout - email, phone, billing address, name. The template maps all of it to GA4 and Meta CAPI compatible user_data:

  • Email and phone (formatted to E.164 with 200+ country codes)
  • First name and last name (split from the cardholder name)
  • Full billing address - street, city, postal code, region, country
  • Business name (when collected separately from personal name)
  • _tag_mode: "MANUAL" so Meta CAPI uses the exact data you provide

A note on name resolution: Stripe stores personal and business names in separate fields. customer_details.name is often the business name, not the person's name. The template uses customer_details.individual_name for the personal name and falls back to billing_details.name (the cardholder name from the payment) if needed.

Session stitching

This is where it gets interesting. Because the POST comes directly from the browser on your thank-you page, you have real browser context - not a server-to-server webhook with no client data.

  • client_id extracted from the _ga cookie (with a generated fallback)
  • ga_session_id and ga_session_number from the _ga_XXXXXX cookie
  • Real ip_override and user_agent from request headers
  • page_location, page_referrer, page_title, screen_resolution, language

GA4 sees this purchase as part of the customer's existing session. No orphaned conversions.

Consent and DMA

The JS snippet reads your CMP cookie (Cookiebot by default, with examples for OneTrust and others) and sends the consent state to sGTM. The template maps it to:

  • x-ga-gcs - Google Consent State
  • x-ga-gcd - Google Consent Default
  • x-ga-dma - Digital Markets Act flag (dynamically set based on the user's country via Cloudflare headers)
  • x-ga-npa - No personalized ads signal

Ad platform cookies

The snippet collects 30+ cookies from every major ad platform:

  • Google Analytics and Ads (_ga, _gcl_aw, _gcl_au, FPID, etc.)
  • Meta (_fbp, _fbc)
  • TikTok (_ttp, ttclid)
  • Snapchat (_scclid, _scid)
  • LinkedIn (li_fat_id)
  • Microsoft/Bing (uet_vid, _uetmsclkid)
  • Pinterest (_epik)
  • Klaviyo, affiliate networks (Awin, Rakuten, Outbrain, Taboola)

All cookies are included both as individual event data fields and as a combined cookie string.

Setup

1. Stripe Payment Link

In your Payment Link settings, set "After payment" to "Don't show confirmation page" and redirect to:

https://yourdomain.com/thank-you?session_id={CHECKOUT_SESSION_ID}

Stripe replaces the placeholder with the actual session ID.

For custom Stripe integrations (not Payment Links), add the same parameter to your success_url when creating a Checkout Session:

const session = await stripe.checkout.sessions.create({
  success_url: 'https://yourdomain.com/thank-you?session_id={CHECKOUT_SESSION_ID}',
});

2. Thank-you page

Add snippet.js in a <script> tag before </body>. Two things to update:

The sGTM endpoint URL:

const sgtmEndpoint = 'https://yourdomain.com/your-sgtm-path/stripe-purchase';

The consent detection (if you don't use Cookiebot):

// OneTrust
const consent = getCookie('OptanonConsent');
const analyticsConsent = consent && consent.indexOf('C0002:1') > -1;
const marketingConsent = consent && consent.indexOf('C0004:1') > -1;

// No CMP
const analyticsConsent = true;
const marketingConsent = true;

3. sGTM

Import template.tpl as a new Client Template, create a Client from it, and configure:

  • Request Path: /stripe-purchase (must match the snippet URL)
  • Stripe Secret Key: your sk_live_... key
  • Allowed Origin: your thank-you page domain (e.g. https://yourdomain.com)

Publish and test.

Security

  • The Stripe secret key lives only in your sGTM container, server-side. It is never sent to the browser.
  • The template verifies payment_status === 'paid' before firing any tags. Unpaid sessions are rejected.
  • CORS is configurable - you can lock the endpoint to your domain.
  • The browser only sends the session ID and cookies. All Stripe API communication is server-to-server.

Why not just use Stripe webhooks?

Webhooks give you order data, but they come from Stripe's servers - no browser context, no cookies, no session stitching. You get a conversion event with revenue but no way to connect it to the user's browsing session in GA4.

This template gives you both: full Stripe order data AND real browser context, because the request originates from the customer's browser on your thank-you page.

Approach Stripe key exposed? Extra infrastructure? Session stitching? Full ecom data?
This template No No Yes Yes
Client-side Stripe.js Yes (publishable) No Yes Limited
Custom backend proxy No Yes No Yes
Stripe webhooks No Yes No Yes
Zapier/Make No Yes No Partial

Limitations

  • Designed for Stripe Checkout and Payment Links with redirect. Custom payment flows need to append ?session_id={CHECKOUT_SESSION_ID} to the success URL.
  • The snippet removes the session ID from the URL after firing, preventing duplicates on refresh. But if a user navigates back via browser history with the original URL, the event could fire again. Use transaction_id for deduplication in your tags.
  • Cookies must be on the same domain as the thank-you page.

Get it

The template is open source under Apache 2.0.

If you run into issues or have questions, open a GitHub issue or reach out.