Skip to content

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.

serverTracker.trackConversion(input: ServerConversionInput): Promise<void>;
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.

FieldRequiredNotes
labelyesA key from ads.conversionActions. Throws if missing from the map.
valuenoConversion value, in currency.
currencynoISO 4217 currency code. Required by Google when value is set.
transactionIdno — but always pass oneSame dedup key as the browser-side call. If omitted, auto-generated and logged.
gclidnoForwarded from the browser via getClickIdentifiers().
gbraidnoSame.
wbraidnoSame.
userDatanoIdentity fields. The SDK normalizes and hashes identically to the browser.

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.

  1. Looks up input.label in ads.conversionActions to get the conversionAction resource name.

  2. Fetches an Ads API access token via the OAuth refresh-token provider (cached until ~60 seconds before expiry).

  3. POSTs to https://googleads.googleapis.com/<apiVersion>/customers/<customerId>:uploadClickConversions with:

    {
    "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
    }
  4. Headers include Authorization: Bearer <accessToken>, developer-token: <developerToken>, Content-Type: application/json, and optionally login-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.

Throws at call time if ads was not configured on createServerTracker:

Error: [trackbridge] trackConversion requires `ads` to be configured on createServerTracker

Throws if label is not in ads.conversionActions:

Error: [trackbridge] no conversionAction configured for label "foo" — add it to ads.conversionActions on createServerTracker

Always 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/dedup

Warns (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>
app/api/webhooks/stripe/route.ts
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();
}