Track Conversions in GA4 and Meta
Fire purchase events from the embed with zero extra fetches, let the hosted page inject your pixels for you, or forward conversions server-side — with attribution captured end to end.
How it works
Where the purchase event fires depends on which checkout surface you use. In the embed, your page hosts the pixels; on the hosted page, Throttle injects them.
| Surface | Who hosts the pixels | How the purchase fires |
|---|---|---|
| Embedded checkout (iframe) | Your storefront page — gtag.js / the Meta pixel are already loaded for the rest of your site. | throttle.completed (and onSucceeded in the React SDK) carries total, currency, and lineItems so you fire the purchase event yourself — no extra fetch. Throttle never injects pixels inside the iframe; that would double-count. |
| Hosted checkout page | Throttle — gtag.js and/or the Meta pixel are injected when tracking settings are configured and consent passes. | The page fires the purchase events automatically right before the post-completion redirect, using beacon transport so the events survive the navigation. |
| Server-side (either surface) | Nobody — Throttle forwards the purchase from its backend. | When enabled, Throttle POSTs the purchase to the GA4 Measurement Protocol and Meta Conversions API at the payment moment, using the attribution snapshot on order.clientContext. |
Embed recipe
Protocol v1.4 additively extends throttle.completed
with total (minor units), currency, and lineItems. Convert minor units to major (
total / 100) before handing values to analytics.
import { CheckoutEmbed } from '@usethrottle/checkout-react';
<CheckoutEmbed
sessionId={sessionId}
parentOrigin="https://shop.example.com"
onSucceeded={({ orderId, total, currency, lineItems }) => {
const value = (total ?? 0) / 100; // minor units -> major
const items = lineItems ?? [];
// GA4 purchase
window.gtag?.('event', 'purchase', {
transaction_id: orderId,
value,
currency: currency ?? 'USD',
items: items.map((item) => ({
item_id: item.sku ?? item.id ?? item.name,
item_name: item.name,
quantity: item.quantity,
price: item.unitPrice / 100,
})),
});
// Meta pixel purchase — eventID enables CAPI dedup
window.fbq?.(
'track',
'Purchase',
{
value,
currency: currency ?? 'USD',
content_type: 'product',
content_ids: items
.map((item) => item.sku ?? item.id)
.filter((id): id is string => Boolean(id)),
},
{ eventID: orderId },
);
router.push(`/thank-you?order=${orderId}`);
}}
/>; sku is the merchant catalog id
(the cart line item's referenceId). Use it as item_id / content_ids
so purchase events join against your
product feed
for dynamic remarketing. The analytics fields are optional — payment-only sessions with no
cart snapshot omit them, so guard with fallbacks as above.
Hosted setup
For the hosted checkout page, configure tracking once per application and environment via GET/PUT /api/v1/tracking-settings. Only the public
ids reach the buyer's browser; secrets stay server-side and come back masked from GET.
curl -X PUT https://api.usethrottle.dev/api/v1/tracking-settings \
-H "X-API-Key: sk_live_xxx" \
-H "Content-Type: application/json" \
-d '{
"ga4MeasurementId": "G-XXXXXXXXXX",
"metaPixelId": "1234567890123456",
"requireConsent": true
}' Configure tracking settings
PUT /api/v1/tracking-settings with your ga4MeasurementId and/or metaPixelId. Settings are per application and per environment, so configure your production environment separately from sandboxes.
Pass the consent signal
Append consent=granted (or consent=denied) to the checkout URL when you hand the buyer off. With requireConsent true (the default), no consent signal means no pixels are injected. If you run your own consent platform upstream and only send consenting buyers, you can set requireConsent to false instead.
Add the checkout domain to GA4 cross-domain linking
In GA4 Admin, add checkout.usethrottle.dev to your cross-domain linking domain list. Throttle configures gtag.js with linker.accept_incoming, so the _gl parameter your storefront appends stitches the buyer into the same GA4 session instead of starting a new one on the checkout domain.
requireConsent defaults to true: without an explicit consent=granted parameter on the checkout URL, no
pixels are injected and no browser events fire. An explicit consent=denied always wins, even when requireConsent is off. Server-side forwarding honors
the same signal via clientContext.consent — analytics gates GA4 and marketing gates Meta.
Server-side conversions
Browser pixels miss buyers with ad blockers and tabs closed mid-redirect. Set serverSideEnabled plus the per-channel secrets and
Throttle forwards the purchase from its backend at the payment moment: card payments at
capture, Net-N invoices at authorization (issuance), and subscription renewal charges
skipped — renewals are not new acquisitions.
curl -X PUT https://api.usethrottle.dev/api/v1/tracking-settings \
-H "X-API-Key: sk_live_xxx" \
-H "Content-Type: application/json" \
-d '{
"serverSideEnabled": true,
"ga4ApiSecret": "your-ga4-mp-api-secret",
"metaCapiAccessToken": "your-meta-capi-access-token"
}' - GA4 Measurement Protocol — sends a
purchaseevent withtransaction_idset to the order id, using the capturedgaClientId/gaSessionIdwhen available. Requiresga4MeasurementId+ga4ApiSecret(create the API secret under the GA4 data stream's Measurement Protocol settings). - Meta Conversions API — sends a
Purchaseevent withevent_idset to the order id anduser_databuilt from the SHA-256-hashed buyer email, server-stamped IP address and user agent, and thefbp/fbccookies fromclientContext. RequiresmetaPixelId+metaCapiAccessToken.
| Channel | Dedup rule |
|---|---|
| GA4 | Pick browser or server — Measurement Protocol and gtag purchases do not reliably dedup. When server-side GA4 is on, the hosted page automatically suppresses its browser purchase; embed integrators should likewise skip the browser GA4 purchase. transaction_id = orderId guards against page-level double-fires. |
| Meta | Both fire — the browser pixel and CAPI event are deduplicated natively by Meta via eventID = orderId. Keep the browser fbq Purchase in place; it improves match quality. |
Attribution capture (clientContext)
POST /api/v1/checkout/sessions accepts an optional clientContext attribution blob. The hosted page also
auto-captures what it can at completion (UTMs, click ids, GA and Meta cookies, landing
page, referrer) and Throttle merges the two per key — completion-time values win — then
server-stamps ipAddress and userAgent. The merged snapshot lands on order.clientContext (merchant order GET) and on the order.created webhook payload.
// Your server, at session create time
const session = await checkout.createSession({
applicationId: process.env.THROTTLE_APPLICATION_ID!,
cartId: cart.id,
returnUrl: 'https://shop.example.com/checkout/success',
cancelUrl: 'https://shop.example.com/cart',
clientContext: {
landingPage: 'https://shop.example.com/spring-sale?utm_source=google',
referrer: 'https://www.google.com/',
utmSource: 'google',
utmMedium: 'cpc',
utmCampaign: 'spring-sale',
gclid: 'Cj0KCQjw...',
gaClientId: '123456789.1700000000',
gaSessionId: '1700000000',
fbp: 'fb.1.1700000000000.1234567890',
fbc: 'fb.1.1700000000000.IwAR2...',
consent: { analytics: true, marketing: true }
}
});
Accepted keys: landingPage, referrer, utmSource, utmMedium, utmCampaign, utmTerm, utmContent, gclid, fbclid, msclkid, ttclid, gaClientId, gaSessionId, fbp, fbc, and consent (
{ analytics?, marketing? }). Unknown keys are
rejected; ipAddress / userAgent are server-stamped and rejected as client
input.
_ga
/ _fbp / _fbc
cookies on your storefront, persist them with the cart, and send them as clientContext when your server mints the session. The
hosted page's completion-time capture is a best-effort fallback.