How to Set Up Google Ads Offline Conversion Tracking
Send CRM data back to Google Ads to optimize for real revenue, not clicks.
Jay Banlasan
The AI Systems Guy
Google ads offline conversion tracking setup lets you send real sales data back to Google so it optimizes for revenue instead of clicks. If your business closes deals over the phone, through a CRM, or in person, this is the single most impactful thing you can do for campaign performance.
I set this up for a service business and their cost per qualified lead dropped 40% in 3 weeks. Google stopped wasting money on people who click but never buy.
What You Need Before Starting
- Google Ads developer token and OAuth credentials
- A CRM that captures the Google Click ID (GCLID)
- Python 3.8+ with
google-adsSDK installed - Conversion action created in Google Ads
Step 1: Capture the GCLID
When someone clicks your Google ad, the URL contains a gclid parameter. Capture and store it with the lead:
// Add to your landing page
document.addEventListener('DOMContentLoaded', function() {
const params = new URLSearchParams(window.location.search);
const gclid = params.get('gclid');
if (gclid) {
// Store in hidden form field
const hiddenField = document.querySelector('input[name="gclid"]');
if (hiddenField) hiddenField.value = gclid;
// Also store in cookie as backup
document.cookie = `gclid=${gclid}; max-age=7776000; path=/`;
}
});
Step 2: Create the Conversion Action
from google.ads.googleads.client import GoogleAdsClient
def create_offline_conversion_action(customer_id, name="CRM Sale"):
client = GoogleAdsClient.load_from_storage("google-ads.yaml")
conversion_action_service = client.get_service("ConversionActionService")
conversion_action = client.get_type("ConversionAction")
conversion_action.name = name
conversion_action.type_ = client.enums.ConversionActionTypeEnum.UPLOAD_CLICKS
conversion_action.category = client.enums.ConversionActionCategoryEnum.PURCHASE
conversion_action.status = client.enums.ConversionActionStatusEnum.ENABLED
conversion_action.value_settings.default_value = 0
conversion_action.value_settings.always_use_default_value = False
operation = client.get_type("ConversionActionOperation")
operation.create = conversion_action
response = conversion_action_service.mutate_conversion_actions(
customer_id=customer_id, operations=[operation]
)
resource_name = response.results[0].resource_name
print(f"Created conversion action: {resource_name}")
return resource_name
Step 3: Upload Conversions from Your CRM
from datetime import datetime
def upload_offline_conversions(customer_id, conversions):
client = GoogleAdsClient.load_from_storage("google-ads.yaml")
conversion_upload_service = client.get_service("ConversionUploadService")
click_conversions = []
for conv in conversions:
click_conversion = client.get_type("ClickConversion")
click_conversion.gclid = conv["gclid"]
click_conversion.conversion_action = client.get_service("GoogleAdsService").conversion_action_path(
customer_id, conv["conversion_action_id"]
)
click_conversion.conversion_date_time = conv["conversion_time"]
click_conversion.conversion_value = conv["value"]
click_conversion.currency_code = conv.get("currency", "USD")
click_conversions.append(click_conversion)
request = client.get_type("UploadClickConversionsRequest")
request.customer_id = customer_id
request.conversions = click_conversions
request.partial_failure = True
response = conversion_upload_service.upload_click_conversions(request=request)
success = 0
for result in response.results:
if result.gclid:
success += 1
print(f"Uploaded {success}/{len(conversions)} conversions")
return response
Step 4: Pull Conversions from Your CRM
import sqlite3
def get_pending_conversions(crm_db_path):
conn = sqlite3.connect(crm_db_path)
rows = conn.execute("""
SELECT gclid, closed_date, deal_value, conversion_action_id
FROM deals
WHERE gclid IS NOT NULL
AND gclid != ''
AND status = 'won'
AND uploaded_to_google = 0
""").fetchall()
conn.close()
conversions = []
for row in rows:
conversions.append({
"gclid": row[0],
"conversion_time": datetime.strptime(row[1], "%Y-%m-%d").strftime("%Y-%m-%d %H:%M:%S+00:00"),
"value": float(row[2]),
"conversion_action_id": row[3],
})
return conversions
def mark_uploaded(crm_db_path, gclids):
conn = sqlite3.connect(crm_db_path)
for gclid in gclids:
conn.execute("UPDATE deals SET uploaded_to_google = 1 WHERE gclid = ?", (gclid,))
conn.commit()
conn.close()
Step 5: Schedule Daily Uploads
if __name__ == "__main__":
customer_id = os.getenv("GOOGLE_ADS_CUSTOMER_ID")
conversions = get_pending_conversions("crm.db")
if conversions:
upload_offline_conversions(customer_id, conversions)
mark_uploaded("crm.db", [c["gclid"] for c in conversions])
print(f"Uploaded {len(conversions)} offline conversions")
else:
print("No pending conversions to upload")
# Run daily at 9 AM
0 9 * * * cd /path/to/project && python3 upload_conversions.py
Google needs at least 30 conversions per month to optimize effectively. Upload daily so the algorithm gets fresh signals. The improvement in lead quality is typically visible within 2-3 weeks.
What to Build Next
Add conversion value adjustments that update previously uploaded conversions when deal values change. Then build a GCLID capture audit that checks what percentage of your leads have valid GCLIDs.
Related Reading
- AI in Paid Advertising: The Complete Overview - the complete ad operations landscape
- The Feedback Loop That Powers Everything - sending conversion data back to ad platforms
- AI in CRM: Beyond Contact Storage - connecting CRM data to ad optimization
Want this system built for your business?
Get a free assessment. We will map every system your business needs and show you the ROI.
Get Your Free Assessment