Analytics

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.

SurfaceWho hosts the pixelsHow 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 pageThrottle — 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.

onSucceeded purchase events
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 content_ids join key
Each line item's 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.

Configure tracking ids
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
  }'
1

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.

2

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.

3

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.

Consent defaults to required
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.consentanalytics 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.

Enable server-side forwarding
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 purchase event with transaction_id set to the order id, using the captured gaClientId / gaSessionId when available. Requires ga4MeasurementId + ga4ApiSecret (create the API secret under the GA4 data stream's Measurement Protocol settings).
  • Meta Conversions API — sends a Purchase event with event_id set to the order id and user_data built from the SHA-256-hashed buyer email, server-stamped IP address and user agent, and the fbp / fbc cookies from clientContext. Requires metaPixelId + metaCapiAccessToken.
ChannelDedup rule
GA4Pick 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.
MetaBoth 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.
Delivery is fire-and-forget
Forwarding failures are logged and not retried in v1. Treat server-side conversions as an analytics signal, not an accounting source of truth — use webhooks for durable side effects.

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.

Pass clientContext at session create
// 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.

Pass it from your server for best fidelity
The buyer's first-party attribution cookies live on your domain, not the checkout domain. Capture UTMs, click ids, and the _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.