Skip to content

serverTracker.trackPurchase()

The server-side counterpart to tracker.trackPurchase(). Fires GA4 via the Measurement Protocol, plus an Ads conversion when conversionLabels.purchase is configured. Returns a structured ServerHelperResult with per-destination outcome.

clientId is required — the server has no _ga cookie of its own. Forward it from the browser via tracker.getClientId() or tracker.exportContext().

serverTracker.trackPurchase(input: ServerPurchaseInput): Promise<ServerHelperResult>;
type ServerPurchaseInput = {
transactionId: string;
value: number;
currency: string;
items: TrackbridgeItem[];
clientId: string;
userId?: string;
gclid?: string;
gbraid?: string;
wbraid?: string;
affiliation?: string;
coupon?: string;
shipping?: number;
tax?: number;
userData?: UserData;
consent?: ServerConsent;
};
FieldRequiredNotes
transactionIdyesMust match the browser-side trackPurchase call. No auto-generation.
valueyesOrder total in currency.
currencyyesISO 4217.
itemsyesTrackbridgeItem[]. Empty arrays are sent as [].
clientIdyesGA4 client ID forwarded from the browser.
userIdnoOptional GA4 user_id.
gclid / gbraid / wbraidnoClick identifiers from the browser session. Required by Ads if you want this conversion attributed to a click.
affiliation, coupon, shipping, taxnoGA4 params.
userDatanoHashed per enhanced conversions. Dropped if consent.ad_user_data === 'denied'.
consentnoPer-call consent signals. Omit to send the request in full.
type ServerHelperResult = {
ads: HelperSendResult;
ga4: HelperSendResult;
};
type HelperSendResult =
| { ok: true }
| { ok: false; error: Error }
| { skipped: true; reason: 'no_label_configured' | 'refund_ads_unsupported' };

ads carries the Ads-side outcome:

  • { ok: true } if the Ads API accepted the upload.
  • { ok: false, error } if the Ads API rejected, OAuth refresh failed, or the network failed.
  • { skipped: true, reason: 'no_label_configured' } if conversionLabels.purchase is unset.

ga4 carries the GA4 MP outcome with the same { ok: true } | { ok: false, error } shape. GA4 does not have a “skipped” path for trackPurchase — it always fires when ga4MeasurementId is configured.

Throws synchronously if transactionId is missing or empty. Throws if ads is configured but conversionLabels.purchase references an unknown conversionActions key.

app/api/webhooks/stripe/route.ts
import { serverTracker } from '@/lib/tracker.server';
export async function POST(req: Request) {
const event = await verifyStripeSignature(req);
if (event.type !== 'checkout.session.completed') return new Response();
const order = await db.orders.findByStripeId(event.data.object.id);
const result = await serverTracker.trackPurchase({
transactionId: order.id,
value: order.total,
currency: order.currency,
items: order.items,
clientId: order.gaClientId, // captured from tracker.getClientId() at checkout
gclid: order.gclid,
userData: { email: order.customer.email },
});
if (!result.ads.ok && !('skipped' in result.ads)) reportError(result.ads.error);
if (!result.ga4.ok && !('skipped' in result.ga4)) reportError(result.ga4.error);
return new Response();
}