How to connect Google Analytics to Stripe revenue, and where it breaks
Connect Google Analytics to Stripe revenue in 6 steps, with code. See why a 14-day trial or a month-2 renewal falls outside what GA4 can credit to a channel.
Muzahid Maruf, FounderUpdated
On this page
Explore with AI
Opens this article inside the chosen assistant with a ready-made prompt.
To connect Google Analytics to Stripe revenue, your server sends a purchase event to Google Analytics 4 each time Stripe confirms a payment, tagged with the visitor's client ID so the sale joins the visit that earned it.
The 6 steps below work for a first payment made on the day of the click.
They become unreliable for the first charge of a trial longer than 2 days and for every renewal, because Google says Measurement Protocol events arriving more than 48 hours after the browser event may not be processed as expected for conversion attribution.
Google Analytics 4 records only what its tag or the Measurement Protocol sends it, and Stripe files payments under IDs of its own, so one identifier has to travel from the browser into Stripe and back.
Stripe's guide to analyzing the Checkout funnel with Google Analytics 4 uses this route, but its server-side sample sends an empty params object, which records no value.
Key takeaways
- Google Analytics 4 and Stripe share no identifier, so the usual build stores the client ID and session ID in Stripe metadata and posts a purchase event to the Measurement Protocol from a webhook.
- Google says Measurement Protocol events should arrive within 48 hours of the browser event they join, so a 14-day trial (336 hours) or a month-2 renewal (720 hours) cannot reliably credit the original campaign.
- A Payment Link silently drops a client ID that contains a period, and the Stripe invoice ID is the transaction_id that stops 3 days of webhook retries from double counting a purchase.
- A standard Google Analytics 4 property keeps event data for 2 or 14 months, and the 26-month option belongs to Google Analytics 360.
- Reading revenue from Stripe and joining it to a click by email, as TrackRev does from $29 a month with revenue attribution built into its affiliate software, skips the browser ID but needs the buyer's email at signup or checkout.
Connect Google Analytics to Stripe in 6 steps
You need the Editor role on the property, a webhook endpoint, and a server route that creates Stripe Checkout Sessions.
Step 1: Exclude checkout.stripe.com from referrals
Customers returning from Stripe Checkout arrive with checkout.stripe.com as the referrer, so a new session can be filed under it instead of the campaign. Stripe's funnel guide says to exclude it.
In Google Analytics 4, open Admin, Data streams, your web stream, Configure tag settings, then List unwanted referrals, and add the domain. A stream holds up to 50, and past sessions keep their source.
Step 2: Create a Measurement Protocol API secret
Secrets belong to a Google Analytics 4 data stream, so open Measurement Protocol API secrets in the web stream from step 1, create one, and copy it with the measurement ID that starts with G-.
Posts go to https://www.google-analytics.com/mp/collect, or to region1.google-analytics.com for EU collection. Keep the secret server-side, because anyone holding it can send arbitrary events to your property.
Step 3: Read the client ID and session ID in the browser
The gtag.js get command returns client_id and session_id for a Google Analytics 4 measurement ID. Call it when the visitor clicks checkout and post both values to the route that creates the Session.
A client ID is 2 numbers joined by a period, which matters in step 4. A blocked tag can leave the callbacks silent, so the sample starts checkout after 1.5 seconds regardless.
function startCheckout() { let started = false; const ids = { clientId: "", sessionId: "" }; const go = () => { if (started) return; started = true; fetch("/api/checkout", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(ids), }) .then((res) => res.json()) .then(({ url }) => { window.location.href = url; }); }; setTimeout(go, 1500); // a blocked tag never calls back if (typeof gtag !== "function") return go(); gtag("get", "G-XXXXXXXXXX", "client_id", (clientId) => { ids.clientId = String(clientId); gtag("get", "G-XXXXXXXXXX", "session_id", (sessionId) => { ids.sessionId = String(sessionId); go(); }); });}Step 4: Store both IDs on the Stripe objects
Put the 2 IDs in metadata on the Checkout Session. Subscriptions also need subscription_data.metadata, which Stripe writes to the Subscription and snapshots onto every invoice at parent.subscription_details.metadata, so a renewal webhook can find the client ID months later.
Payment Links fix their metadata when you create the link, so append client_reference_id instead. Per Stripe's URL parameters page, it takes up to 200 characters of letters, digits, - and _, and silently drops anything else.
A client ID has a period, so swap it before sending and restore it in the webhook. The guides to Payment Links attribution and Stripe metadata attribution cover those routes without Google Analytics 4.
| Where the ID lives | Reaches renewal invoices | Size limit |
|---|---|---|
| Checkout Session metadata | No, it stays on the Session | 500 characters per value |
| subscription_data.metadata | Yes, as a snapshot on each invoice | 500 characters per value |
| Payment Link client_reference_id | Only if you copy it onto the subscription | 200 characters |
// POST /api/checkoutexport async function POST(req: Request) { const { clientId, sessionId } = await req.json(); const ga = { ga_client_id: clientId, ga_session_id: sessionId }; const session = await stripe.checkout.sessions.create({ mode: "subscription", line_items: [{ price: "price_XXXXXXXX", quantity: 1 }], success_url: "https://example.com/welcome", cancel_url: "https://example.com/pricing", metadata: ga, // stays on the Session subscription_data: { metadata: ga } // copied onto every invoice }); return Response.json({ url: session.url });} // Payment Links: a client ID like 1234567890.1700000000 has a period,// which client_reference_id would drop, so swap it before appendingfunction paymentLink(clientId: string, sessionId: string) { const ref = clientId.replace(".", "_") + "-" + sessionId; return "https://buy.stripe.com/XXXX?client_reference_id=" + ref;}// In the webhook, reverse it:// const [cid, sid] = session.client_reference_id.split("-");// const clientId = cid.replace("_", ".");Step 5: Send purchase and refund events from a webhook
Subscribe an endpoint to 2 events, invoice.paid for subscriptions and checkout.session.completed for one-time payments, and verify each signature before acting. The Google Analytics 4 purchase event takes a value without tax and an items array, so the sample reads total_excluding_tax.
Stripe amounts are in the smallest currency unit.
| Currency | Stripe amount | Value to send | Division |
|---|---|---|---|
| USD | 3900 | 39.00 | By 100 |
| USD | 1099 | 10.99 | By 100 |
| EUR | 4500 | 45.00 | By 100 |
| JPY | 500 | 500 | None |
Stripe lists JPY as a zero-decimal currency. The sample divides every amount by 100, so add a branch for those.
Stripe retries failed deliveries for up to 3 days in live mode and may deliver the same event more than once, while Google Analytics 4 deduplicates purchases that share a transaction ID, so the invoice ID goes in that field.
An empty ID makes every purchase look like a duplicate. Refunds use the refund event with the original invoice ID, sent from a charge.refunded handler.
const MP_URL = "https://www.google-analytics.com/mp/collect" + "?measurement_id=" + process.env.GA_MEASUREMENT_ID + "&api_secret=" + process.env.GA_API_SECRET; export async function POST(req: Request) { const event = stripe.webhooks.constructEvent( await req.text(), req.headers.get("stripe-signature")!, process.env.STRIPE_WEBHOOK_SECRET!, ); if (event.type === "invoice.paid") { const invoice = event.data.object as Stripe.Invoice; const meta = invoice.parent?.subscription_details?.metadata ?? {}; if (meta.ga_client_id && invoice.amount_paid > 0) { await fetch(MP_URL, { method: "POST", body: JSON.stringify({ client_id: meta.ga_client_id, events: [{ name: "purchase", params: { transaction_id: invoice.id, currency: invoice.currency.toUpperCase(), // two-decimal currencies only; GA4 value excludes tax value: (invoice.total_excluding_tax ?? invoice.amount_paid) / 100, session_id: meta.ga_session_id, engagement_time_msec: 100, // lets the event show in Realtime items: [{ item_id: "starter-monthly", item_name: "Starter plan" }], }, }], }), }); } } return new Response(null, { status: 200 });}Step 6: Validate before trusting the numbers
Post the payload to https://www.google-analytics.com/debug/mp/collect first. It returns an empty validationMessages array for a valid Google Analytics 4 event and does not check the API secret.
The live endpoint returns no HTTP error code even for a malformed event, so a handler that watches status codes never sees a failure. Test in a separate property, then compare purchases sent with paid Stripe invoices after 30 days.
Limits that matter for this build
| Limit | Value |
|---|---|
| Measurement Protocol events per request | 25 |
| Parameters per event | 25 |
| Custom parameters per item | 10 |
| Event name length | 40 characters |
| Parameter value length, standard property | 100 characters |
| Request body size | Under 130 kB |
| Non-conversion requests per property | 100 million an hour |
| Backdating an event | Up to 72 hours |
| Joining a server event to a browser event | Within 48 hours |
| Unwanted referrals per data stream | 50 |
| Stripe client_reference_id | 200 characters |
| Stripe metadata | 50 keys, 40-character keys, 500-character values |
| Stripe webhook retries, live mode | Up to 3 days |
| BigQuery export, standard property | 1 million events a day |
Sources: Google Analytics Measurement Protocol reference, Google Analytics Help and Stripe documentation, October 2026.
How much revenue Google Analytics 4 can credit to a channel
Take a $39 monthly plan sold to a visitor who clicks a Google ad on day 0 and pays the same day. Year one is 12 payments, $468 in all.
The first payment arrives within an hour of the browser event, and each later one arrives a month further out.
| Payment | Hours after the stored visit | Inside the 48-hour window | Amount |
|---|---|---|---|
| First charge, no trial | Under 1 | Yes | $39 |
| First charge after a 7-day trial | 168 | No | $39 |
| First charge after a 14-day trial | 336 | No | $39 |
| Month 2 renewal, no trial | 720 | No | $39 |
| Month 6 renewal, no trial | 3,600 | No | $39 |
| Month 12 renewal, no trial | 7,920 | No | $39 |
Hours are counted from the visit whose IDs the checkout stored, using 30-day months.
Without a trial, 1 payment in 12 sits inside the window: $39 of $468, or 8.3% of year-one revenue.
Google says only that later events may not be processed as expected for attribution, and does not say what Google Analytics 4 does with them.
Send one late test event and check which source it lands under before you build a report on it.
Trials and renewals
Stripe's Checkout Session object reports a subscription with a free trial as paid once its $0 trial invoice is processed, so the session completes with amount_total at 0.
Send that moment as a custom trial_start event marked as a key event, while the visit is inside the 48-hour window. Renewals carry no session Google can credit.
I would let Google Analytics 4 judge acquisition up to the signup and leave the money after it to Stripe or a billing-side tool, as subscription LTV attribution describes.
Cookies, blockers and consent
Google's cookie documentation sets the _ga cookie to expire after 2 years by default and says browsers cap first-party cookies for visitors who do not return.
WebKit's policy adds a 24-hour cap for Safari Intelligent Tracking Prevention when it detects link decoration on the landing page.
| Case | Cookie lifetime |
|---|---|
| Default for the _ga cookie | 2 years |
| Chrome, visitor does not return | 400 days |
| Safari, visitor does not return | 7 days |
| Safari, link decoration detected | 24 hours |
A visitor who clicks an ad on day 1 and returns on day 9 with no visit between arrives as a new client, so the campaign is gone before Stripe is involved.
A blocked tag or a consent banner leaves no client ID at all, and the Session carries empty metadata.
Skip those events and log how many you skip, since an invented ID would credit the sale to a visit that never happened. The Safari ITP guide covers the browser side.
Retention
The setting affects Explorations and funnel reports and leaves standard aggregated reports alone, so a 24-month lifetime value comparison by channel cannot come from Explorations in a standard Google Analytics 4 property. The BigQuery export is the way around it.
| Data | Retention options |
|---|---|
| Event data, standard property | 2 or 14 months |
| Event data, Google Analytics 360 | 2, 14, 26, 38 or 50 months |
| Google signed-in data | 26 months at most |
From Google's data retention page.
Four ways to join the two systems
Pick the first row for one-time or same-day sales, the second for teams with a warehouse, and the fourth when most revenue is renewals. The third feeds Google Ads bidding.
| Approach | Join key | Renewals | Main limit |
|---|---|---|---|
| Webhook to the Measurement Protocol | Client ID in Stripe metadata | Sent, outside the 48-hour window | A blocked tag leaves no ID |
| Google Analytics 4 BigQuery export plus a Stripe warehouse | Client ID, joined in SQL | Yes, with your own credit rule | 1 million events a day |
| Offline conversion upload to Google Ads | GCLID from the ad click | Reports in Google Ads | Needs the click's GCLID |
| Billing-side attribution | Email bound to a visitor ID | Credits the click under the model you pick | Needs the email at signup |
Join keys and limits as described in Google's and Stripe's documentation, October 2026.
Reading the channel from Stripe instead
Billing-side attribution reverses the order: the click is recorded first, on a tracked link, and the payment is read from Stripe afterward, so no browser ID has to survive until checkout.
TrackRev, SaaS affiliate software with revenue attribution built in, works this way. Setup is a read-only restricted Stripe key and a one-line pixel.
TrackRev joins each paid charge to a click through the customer's email, which the pixel binds to the visitor at signup or checkout.
It syncs hourly, backfills 90 days on the first sync, credits renewals to the original click, and nets refunds and chargebacks out of channel revenue.
The same join covers 6 processors: Stripe, Paddle Billing, Polar, Lemon Squeezy, Creem and Dodo Payments.
Last-touch is the default, with first-touch and linear one dropdown away. Revenue by channel needs a paid plan from $29 a month on Indie, with no limit on tracked revenue; the free plan covers link tracking only.
Email is the join key, so test the buyer who pays under a different address than the one they signed up with.
TrackRev does not replace Google Analytics 4 for page engagement or content performance, and a team that wants only a standalone attribution product may prefer another tool.
For the reporting side, see GA4 not showing revenue by channel and attributing Stripe revenue to marketing channels.
Found this useful? Share it.
Frequently asked questions
- No. Google Analytics 4 records only what its tag or the Measurement Protocol sends, so a Stripe payment appears only after your server posts it. The usual build saves the client ID in Stripe metadata when checkout starts and sends a purchase event from the payment webhook.
- A customer returning from Stripe Checkout can arrive with checkout.stripe.com as the referrer, so Google Analytics 4 files the visit as a referral. Listing the domain under unwanted referrals fixes new visits, not past ones. Direct traffic also appears when the visitor's cookie was lost between click and payment.
- Only if you send them. Stripe produces an invoice.paid event for each renewal with no browser involved, so your server posts the purchase to Google Analytics 4 using the client ID saved in subscription metadata. It arrives about 720 hours after the visit, which makes channel credit unreliable under Google's 48-hour guidance.
- Typical causes are tax left inside the value, amounts sent in minor units such as 1099 for $10.99, refunds that never reached Google, payments with no stored client ID, and renewals sent too late to attribute. Stripe's balance is also net of fees, so compare against paid invoices.
- Standard properties offer 2 or 14 months, which affects Explorations and funnel reports only. The 26, 38 and 50 month options exist for Google Analytics 360.
- Yes. Google Ads accepts offline conversions tied to the GCLID of the original ad click, which is separate from the client ID. Google advises uploading an extra day of data with each import, because conversions within 1 day of the click may not be recorded yet.

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