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 — Founder of TrackRev.io

Muzahid MarufUpdated

Attribution · 10 min read
On this page
  1. 01Connect Google Analytics to Stripe in 6 steps
  2. 02Limits that matter for this build
  3. 03How much revenue Google Analytics 4 can credit to a channel
  4. 04Where the connection breaks
  5. 05Four ways to join the two systems
  6. 06Reading the channel from Stripe instead

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.

Browser: capture the IDs, then start checkout
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 livesReaches renewal invoicesSize limit
Checkout Session metadataNo, it stays on the Session500 characters per value
subscription_data.metadataYes, as a snapshot on each invoice500 characters per value
Payment Link client_reference_idOnly if you copy it onto the subscription200 characters
Server: create the Session with both IDs
// 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.

CurrencyStripe amountValue to sendDivision
USD390039.00By 100
USD109910.99By 100
EUR450045.00By 100
JPY500500None

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.

Server: forward a paid invoice to Google Analytics 4
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

LimitValue
Measurement Protocol events per request25
Parameters per event25
Custom parameters per item10
Event name length40 characters
Parameter value length, standard property100 characters
Request body sizeUnder 130 kB
Non-conversion requests per property100 million an hour
Backdating an eventUp to 72 hours
Joining a server event to a browser eventWithin 48 hours
Unwanted referrals per data stream50
Stripe client_reference_id200 characters
Stripe metadata50 keys, 40-character keys, 500-character values
Stripe webhook retries, live modeUp to 3 days
BigQuery export, standard property1 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.

PaymentHours after the stored visitInside the 48-hour windowAmount
First charge, no trialUnder 1Yes$39
First charge after a 7-day trial168No$39
First charge after a 14-day trial336No$39
Month 2 renewal, no trial720No$39
Month 6 renewal, no trial3,600No$39
Month 12 renewal, no trial7,920No$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.

Where the connection breaks

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.

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.

CaseCookie lifetime
Default for the _ga cookie2 years
Chrome, visitor does not return400 days
Safari, visitor does not return7 days
Safari, link decoration detected24 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.

DataRetention options
Event data, standard property2 or 14 months
Event data, Google Analytics 3602, 14, 26, 38 or 50 months
Google signed-in data26 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.

ApproachJoin keyRenewalsMain limit
Webhook to the Measurement ProtocolClient ID in Stripe metadataSent, outside the 48-hour windowA blocked tag leaves no ID
Google Analytics 4 BigQuery export plus a Stripe warehouseClient ID, joined in SQLYes, with your own credit rule1 million events a day
Offline conversion upload to Google AdsGCLID from the ad clickReports in Google AdsNeeds the click's GCLID
Billing-side attributionEmail bound to a visitor IDCredits the click under the model you pickNeeds 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.

PostLinkedIn

Frequently asked questions

Muzahid Maruf — Founder of TrackRev.io

Written by

Muzahid Maruf

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