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<ServerConversionResult>;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”type ServerConversionResult = { ads: SendResult;};
type SendResult = | { ok: true } | { ok: false; error: Error };Resolves after the Ads API call completes. Runtime failures — network errors, non-2xx responses, and OAuth refresh failures — are captured on result.ads.error rather than thrown. Configuration errors (missing ads, unknown label) still throw at call time. With debug: true, the same failures are also logged via console.warn.
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/dedupCaptures Ads API non-2xx into result.ads.error (also console.warn if debug: true):
[trackbridge] Ads API returned <status>: <response body>Captures network errors and OAuth refresh failures into result.ads.error (also console.warn if debug: true):
[trackbridge] Ads API request failed: <error>The OAuth refresh-token flow runs as part of the upload call; if Google rejects the refresh (e.g., a revoked refresh token), the failure surfaces as result.ads.error with a message like [trackbridge] OAuth refresh failed with 401 Unauthorized. It does not throw.
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);
const result = 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, }, }, });
if (!result.ads.ok) reportError(result.ads.error);
return new Response();}