Streamlining Offline Conversion Tracking for Google Ads: A Developer’s Journey
Offline conversion tracking is vital for understanding the full customer journey — from clicking on a Google ad to converting on your app…

Server Side Google Ads Conversation Tracking
Streamlining Offline Conversion Tracking for Google Ads: A Developer’s Journey
Offline conversion tracking is vital for understanding the full customer journey — from clicking on a Google ad to converting on your app. In this guide, I’ll walk you through a lightweight solution that captures a Google Click Identifier (GCLID) from your homepage and reports sign-ups as offline conversions to Google Ads — all without bulky SDKs or tag managers.
This guide is for written for developers who want to track google ads conversions server side — the entire code and account setup process takes ~20 minutes. ty
What We’re Building 🏗️
Our goal is to track which sign-ups come from Google Ads with minimal overhead. The overall flow is:
- Capture the GCLID: When a user lands on your homepage (e.g.,
example.com), a middleware extracts thegclidfrom the URL and saves it as a cookie. - Report Conversions: When the user signs up (e.g., on
app.example.com), the app reads the cookie. If a validgclidexists, it triggers a single REST API call to Google Ads, reporting the conversion via offline conversions.
The Technical Setup 🔧
Before diving into the code, you need to set up a few Google Ads accounts and configurations. Here’s how:
1. Create a Service Account 🔐
- Why? This account is used to authenticate API calls.
- How? Follow the Google Ads API Service Account Documentation to create one.
2. Set Up a Google Ads Manager Account 🤗
- Purpose: Your service account needs a Google Ads manager account.
- How? Create one following the guidelines in this Google Ads Manager Account setup guide.
3. Link Your Manager Account to Your Google Ads Account 🔗
- How? Link your new manager account to the existing Google Ads account (the one you want to track conversions for) by following the instructions here.
- Tip: Your Google Ads account will receive an invitation email — be sure to click Accept Request.
4. Developer Token and Test Accounts 🔑
- Developer Token: Initially, the developer token is approved for use with test accounts only. For production use, you will need to request production access from Google.
- Testing: Use a test account as outlined in the Test Accounts Best Practices. Make sure the country is set to US to avoid network errors.
- Connecting Your Service Account: In your test account manager, navigate to Admin → Access and security → Users and invite your service account email with access level “standard.”
5. Create a Conversion Action 🥳
- How? In your Google Ads account, create the conversion action for sign-ups. Then, copy the conversion action ID (often called
ctId) from the URL. Refer to this guide for details.
Environment Variables for Configuration 📂
Rather than hard-coding values, keep your configuration clean by using environment variables. Below is a snippet using Zod for validation — feel free to use your preferred method:
import { z } from "zod";
export const envs = z.object({
/**
* Your Google Ads customer ID
* @example 111-222-3333
*/
GOOGLE_CUSTOMER_ID: z
.string()
.transform((rawCustomerId) => rawCustomerId.replaceAll("-", "")),
/**
* Your Google Ads manager ID
* @example 222-333-4444
*/
GOOGLE_MANAGER_ID: z
.string()
.transform((rawManagerId) => rawManagerId.replaceAll("-", "")),
/**
* Your conversion action ID
* @example 9999999999
*/
GOOGLE_ADS_CONVERSION_ID: z.string(),
/**
* Developer token from Google Ads
* @example ABCDEF_XXXXXXXXXXXXXX
*/
GOOGLE_ADS_TOKEN: z.string(),
/**
* Service account email for Google Ads API
*/
GOOGLE_SERVICE_ACCOUNT_EMAIL: z.string(),
/**
* Private key for the service account
*/
GOOGLE_SERVICE_ACCOUNT_PRIVATE_KEY: z.string()
});
Using environment variables helps catch configuration errors early and keeps your code flexible.
Capturing the GCLID on the Homepage 🤓
On your homepage, use a middleware to extract the gclid from the URL and set it as a cookie. This cookie will later be used to identify users who came from Google Ads:
export const GOOGLE_ADS_COOKIE_NAME = "google_source_cookie";
if (searchParams.has("gclid")) {
const googleId = searchParams.get("gclid");
if (googleId !== null) {
req.nextUrl.searchParams.delete("gclid");
const response = NextResponse.redirect(req.nextUrl);
response.cookies.set({
name: GOOGLE_ADS_COOKIE_NAME,
value: googleId,
httpOnly: false,
expires: addDays(new Date(), 30),
});
return response;
}
}
Reporting Conversions on Sign-Up 🤖
When a user signs up, inspect the cookies for the gclid and then send the conversion event:
const googleAdsCookie = allCookies?.find(
({ name }) => name === GOOGLE_ADS_COOKIE_NAME,
);
await googleAdsService.sendGoogleAdsConversion({
conversionLabel: "Sign-Up",
// Assign a conversion value based on the user's plan
conversionValue: tier === "pro" ? 30 : tier === "basic" ? 20 : 10,
currencyCode: "USD",
gclid: googleAdsCookie?.value ?? null,
userId,
});
The Heart of the Solution: Our google-ads service 🙇
The heart of this solution is the GoogleAdsService class. It handles authentication and sends conversion events to Google Ads. Below is the complete class, ready to be copied into your project:
import { format } from "date-fns";
import { JWT } from "google-auth-library";
import { env } from "~/env";
import { getLogger } from "~/utils/logger.utils";
import { INTERCEPT_THIRD_PARTIES } from "~/utils/third-party.config";
class GoogleAdsService {
private log = getLogger({ module: "google-ads-service" });
private async getAccessTokenFromServiceAccount() {
const jwtClient = new JWT({
email: env.GOOGLE_SERVICE_ACCOUNT_EMAIL,
key: env.GOOGLE_SERVICE_ACCOUNT_PRIVATE_KEY,
scopes: ["<https://www.googleapis.com/auth/adwords>"],
});
const result = await jwtClient.authorize();
return result.access_token ?? null;
}
async sendGoogleAdsConversion({
userId,
...conversion
}: {
conversionLabel: string;
conversionValue: number;
currencyCode?: string;
gclid: string | null;
userId: string;
}): Promise<void> {
if (conversion.gclid === null) {
return;
}
// Optionally intercept third-party requests for testing purposes.
if (INTERCEPT_THIRD_PARTIES) {
this.log.info("Intercepted google conversion event", conversion);
return;
}
this.log.info("Google Ad Sign-Up", {
userId,
gclid: conversion.gclid,
});
const accessToken = await this.getAccessTokenFromServiceAccount();
const apiEndpoint = `https://googleads.googleapis.com/v19/customers/${env.GOOGLE_CUSTOMER_ID}:uploadClickConversions`;
const headers = {
"login-customer-id": env.GOOGLE_MANAGER_ID,
"developer-token": env.GOOGLE_ADS_TOKEN,
"content-type": "application/json",
Authorization: `Bearer ${accessToken}`,
};
const payload = JSON.stringify({
conversions: [
{
conversionAction: `customers/${env.GOOGLE_CUSTOMER_ID}/conversionActions/${env.GOOGLE_ADS_CONVERSION_ID}`,
conversionDateTime: format(new Date(), "yyyy-MM-dd HH:mm:ssxxx"),
conversionValue: conversion.conversionValue,
currencyCode: conversion.currencyCode ?? "USD",
gclid: conversion.gclid,
},
],
partialFailure: true,
validateOnly: false,
debugEnabled: true,
});
const start = Date.now();
const response = await fetch(apiEndpoint, {
method: "POST",
headers,
body: payload,
});
this.log.info(
`POST ${apiEndpoint} ${response.status} in ${Date.now() - start}ms`,
);
if (!response.ok) {
const errorText = await response.text();
this.log.error(`Failed to send conversion: ${errorText}`, {
headers: JSON.stringify(headers),
payload: JSON.stringify(payload),
});
} else {
const result = await response.json();
this.log.info("Success", { data: JSON.stringify(result) });
}
}
}
export const googleAdsService = new GoogleAdsService();
Key Points to Remember 😉
- GCLID Validity: Google only accepts valid GCLIDs. Testing the conversion flow fully requires a live campaign — see Google Ads API documentation for more details.
- Test Accounts: Use test accounts during development to avoid affecting production data. For testing, refer to the Test Accounts Best Practices. When moving to production, you will need to request production access for your developer token.
- Error Handling: The current error handling is a simple proof of concept. You might want to add additional fallbacks or improved logging to better handle any issues in a production environment.
Final Thoughts 💡
By using environment variables for configuration and following these detailed setup steps, you can build a lean, efficient Google Ads conversion handler without unnecessary bloat. This approach allows you to focus on delivering accurate conversion data while reducing integration complexity.
I hope this guide helps you overcome common hurdles and simplifies offline conversion tracking in your own projects. Happy coding! 👩💻👨💻
메타데이터
- post_id
- c8158d471090
- slug
- streamlining-offline-conversion-tracking-for-google-ads-a-developers-journey-c8158d471090
- url
- https://medium.com/@dorbn/streamlining-offline-conversion-tracking-for-google-ads-a-developers-journey-c8158d471090
- canonical_url
- https://medium.com/@dorbn/streamlining-offline-conversion-tracking-for-google-ads-a-developers-journey-c8158d471090
- author_url
- https://medium.com/@dorbn
- status
- ok
- fetched_at
- 2026-07-07 06:42:44