serverTracker.trackConversion()
Uploads one click conversion to the Google Ads API. Resolves the label against your conversionActions map, attaches click identifiers and hashed userData if provided, and uses your transactionId as the dedup key for the matching browser-side call.
Signature
Section titled “Signature”serverTracker.trackConversion(input: ServerConversionInput): Promise<void>;ServerConversionInput
Section titled “ServerConversionInput”type ServerConversionInput = { label: string; value?: number; currency?: string; transactionId?: string; gclid?: string; gbraid?: string; wbraid?: string; userData?: UserData;};UserData is the same shape as on the browser side.
| Field | Required | Notes |
|---|---|---|
label | yes | A key from ads.conversionActions. Throws if missing from the map. |
value | no | Conversion value, in currency. |
currency | no | ISO 4217 currency code. Required by Google when value is set. |
transactionId | no — but always pass one | Same dedup key as the browser-side call. If omitted, auto-generated and logged. |
gclid | no | Forwarded from the browser via getClickIdentifiers(). |
gbraid | no | Same. |
wbraid | no | Same. |
userData | no | Identity fields. The SDK normalizes and hashes identically to the browser. |
Returns
Section titled “Returns”Promise<void>. Resolves after the Ads API call completes — success or failure. Non-2xx responses do not throw; they’re logged via console.warn if debug: true.
Behavior
Section titled “Behavior”-
Looks up
input.labelinads.conversionActionsto get theconversionActionresource name. -
Fetches an Ads API access token via the OAuth refresh-token provider (cached until ~60 seconds before expiry).
-
POSTs to
https://googleads.googleapis.com/<apiVersion>/customers/<customerId>:uploadClickConversionswith:{"conversions": [{"conversionAction": "customers/.../conversionActions/...","conversionDateTime": "<RFC 3339 with timezone>","orderId": "<transactionId>","conversionValue": <value>,"currencyCode": "<currency>","gclid": "<gclid>","userIdentifiers": [{ "hashedEmail": "..." },{ "hashedPhoneNumber": "..." },{ "addressInfo": { "hashedFirstName": "...", "hashedLastName": "...", "hashedStreetAddress": "...", "city": "...", "state": "...", "postalCode": "...", "countryCode": "..." } }]}],"partialFailure": true} -
Headers include
Authorization: Bearer <accessToken>,developer-token: <developerToken>,Content-Type: application/json, and optionallylogin-customer-id.
partialFailure: true means a 200 OK can still report per-conversion errors in the response body. With debug: true, the body is logged so you can see which conversions Google rejected.
Errors
Section titled “Errors”Throws at call time if ads was not configured on createServerTracker:
Error: [trackbridge] trackConversion requires `ads` to be configured on createServerTrackerThrows if label is not in ads.conversionActions:
Error: [trackbridge] no conversionAction configured for label "foo" — add it to ads.conversionActions on createServerTrackerAlways warns (regardless of debug) when transactionId is missing or empty:
[trackbridge] ⚠️ trackConversion called without transactionId → Auto-generated: tb_<uuid> → Dual-send disabled for this call. Pass a transactionId you control to enable cross-side dedup. → See: trackbridge.dev/docs/dedupWarns (debug-gated) on Ads API non-2xx:
[trackbridge] Ads API returned <status><response body>Warns (debug-gated) on network errors:
[trackbridge] Ads API request failed: <error>Example
Section titled “Example”import 'server-only';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);
await serverTracker.trackConversion({ label: 'purchase', value: order.total, currency: order.currency, transactionId: order.id, gclid: order.gclid, gbraid: order.gbraid, wbraid: order.wbraid, userData: { email: order.customer.email, phone: order.customer.phone, firstName: order.customer.firstName, lastName: order.customer.lastName, address: { street: order.billingAddress.street, city: order.billingAddress.city, region: order.billingAddress.region, postalCode: order.billingAddress.postalCode, country: order.billingAddress.country, }, }, });
return new Response();}