FloorSync / Guide for Stripe founders
Stripe to GA4: purchases and renewals with the Measurement Protocol
GA4 only sees a Stripe sale if the buyer comes back to a page with your tag on it, and never sees a renewal. The Measurement Protocol lets your server tell GA4 directly. The hard part is tying the sale to the visitor who bought.
Short answer: keep the visitor’s client_id (from the _ga cookie) when you create the Checkout session. In your webhook, POST a purchase with that client_id and the session or invoice id as transaction_id to /mp/collect. Check it on /debug/mp/collect first: the real endpoint answers 204 even when the event is wrong.
1. What you need from GA4
- The Measurement ID (
G-…): Admin → Data streams → your web stream. - A Measurement Protocol API secret: same stream → Measurement Protocol API secrets → Create. Server only.
2. Keep the client_id at checkout
The _ga cookie looks like GA1.1.1234567890.1700000000; the client_id is the last two numbers, 1234567890.1700000000. Read it when your server creates the Checkout session and keep it in the session’s metadata and against the Stripe customer (renewals need it months later).
const ga = req.cookies._ga || '' // GA1.1.1234567890.1700000000
const clientId = ga.split('.').slice(-2).join('.') // 1234567890.1700000000
// create the session with metadata: { ga_client_id: clientId }, and keep it with the customer
No client_id? GA4 still needs one, so you make one up, and GA4 counts the buyer as a brand-new visitor from nowhere. The sale shows; the ad that brought it doesn’t get credit. Propulse also suggests a stable user_id (your own, or the Stripe customer id) so renewals stay with the same person.
3. Send the purchase from your webhook
const s = event.data.object // a paid checkout.session.completed
const body = JSON.stringify({
client_id: s.metadata.ga_client_id,
user_id: s.customer, // optional, no email or name: GA4 takes no personal data
timestamp_micros: event.created * 1e6,
events: [{
name: 'purchase',
params: {
transaction_id: s.id, // cs_… (an invoice in_… for a renewal)
value: s.amount_total / 100,
currency: s.currency.toUpperCase(),
engagement_time_msec: 1,
},
}],
})
const url = `https://www.google-analytics.com/mp/collect?measurement_id=${GA4_ID}&api_secret=${GA4_SECRET}`
await fetch(url, { method: 'POST', body }) // answers 204, right or wrong
For a renewal (invoice.paid with billing_reason: subscription_cycle) send the same with the stored client_id and the invoice id as transaction_id. Name it purchase to see it in revenue reports, or a custom name if you want renewals kept apart; Propulse lays out both choices.
4. Mind the limits
- 72 hours.
timestamp_microscan go back at most 72 hours. A webhook you replay later than that is too old for GA4. - It’s a supplement. Google says the Measurement Protocol is meant to add to your GA tag, not replace it. Keep gtag on your site.
- No personal data. No email, phone or name in GA4 events.
5. Check it before you trust it
/mp/collect answers 204 No Content whether or not the event made sense. Send the same body to https://www.google-analytics.com/debug/mp/collect first: it answers with validationMessages (empty means valid) and records nothing. Then send for real and look in GA4’s Realtime or DebugView.
Or: forward the event and let FloorSync do 2–5
FloorSync keeps the client_id from checkout (POST /api/v1/attribution, the _ga cookie works as it is), sends sales and renewals to GA4 with the right transaction_id, checks your first event on GA4’s validation server, skips what’s too old, and logs each send. Node, from the docs:
import { createHash } from 'node:crypto'
// event: what stripe.webhooks.constructEvent(...) returned
const text = JSON.stringify(event)
await fetch('https://floorsync.towers.app/api/v1/events/stripe', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Floor-Key': FLOORSYNC_KEY,
'x-amz-content-sha256': createHash('sha256').update(text).digest('hex'),
},
body: text,
signal: AbortSignal.timeout(10000),
}).catch(() => {}) // never let FloorSync slow down your own webhook
See what GA4 would get, right here
Untick “GA client_id” on the renewal and watch the note: that’s the buyer GA4 can’t place. Worked out in your browser; nothing is sent.
Check your own GA4, free
With a free key the dry run uses your own GA4 property, and GA4’s validation server answers. Just your email, no card. $29 a month per business after 7 free days, only when you start the plan.
Sources: Google’s Measurement Protocol docs and sending events; Propulse on renewals in GA4. Checked November 2026.