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.
- Customer completes payment on Stripe and gets redirected to your thank-you page with
?session_id={CHECKOUT_SESSION_ID}in the URL - 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
- The sGTM client template calls the Stripe Checkout Sessions API server-side with your secret key
- Stripe returns the full session object - line items, customer details, tax, discounts, payment status
- The template builds a complete GA4
purchaseevent and runs the container - All your sGTM tags fire with the enriched data
- 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_idfrom the Stripe payment intent IDvalue,currency,tax,shipping- all converted from Stripe's cents formatcouponanddiscountfrom promotion codespayment_type- card, klarna, link, etc.items[]withitem_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_idextracted from the_gacookie (with a generated fallback)ga_session_idandga_session_numberfrom the_ga_XXXXXXcookie- Real
ip_overrideanduser_agentfrom 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 Statex-ga-gcd- Google Consent Defaultx-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_idfor 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.