Systems Library / Marketing Automation / How to Set Up Google Ads Offline Conversion Tracking
Marketing Automation paid advertising

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

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

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

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

Related Systems