Google Ads Enhanced Conversions Integration
Overview
Enhanced Conversions is a Google Ads feature that improves the accuracy of conversion measurement by sending hashed first-party customer data (email, phone, name, address) along with conversion events. This integration automatically enriches Google Ads conversion data with CDP customer information to enable better attribution and reporting.
Key Benefits
- Improved Attribution Accuracy: Match more conversions to ad interactions (typically 5-15% lift)
- Better Campaign Optimization: More accurate data enables better automated bidding
- Privacy-Compliant: All PII data is hashed using SHA-256 before transmission
- Seamless Integration: Automatic enrichment from CDP customer data
- Multi-Tenant Support: Configure separately for each tenant/brand
How It Works
graph LR
A[Order Placed] --> B[VTEX captures gclid]
B --> C[Conversion queued in DB]
C --> D[Worker enriches with CDP data]
D --> E[Hash PII SHA-256]
E --> F[Upload to Google Ads API]
F --> G[Google matches conversion]
G --> H[Improved attribution]
Privacy and Compliance
Data Protection Standards
The Enhanced Conversions system is designed with privacy-first principles:
- SHA-256 Hashing: All PII is hashed before transmission
- GDPR/LGPD Compliance: No raw personal data leaves your infrastructure
- Secure Transmission: HTTPS/TLS for all API communication
- Data Minimization: Only necessary fields are transmitted
- Right to Erasure: Conversion data can be removed on request
What Data Is Sent
| Data Type | Required | Hashed | Format |
|---|---|---|---|
| Yes | Yes | SHA-256 (64 chars) | |
| Phone | No | Yes | SHA-256 with E.164 normalization |
| First Name | No | Yes | SHA-256 lowercase |
| Last Name | No | Yes | SHA-256 lowercase |
| Street Address | No | Yes | SHA-256 lowercase |
| City | No | Yes | SHA-256 lowercase |
| State | No | Yes | SHA-256 lowercase |
| Postal Code | No | Yes | SHA-256 (no spaces/dashes) |
| Country Code | No | No | ISO 2-letter (e.g., AR, US) |
Example of Hashed Data
# Original data (never transmitted)
email = "miguel@example.com"
phone = "+5491112345678"
# Hashed data (what Google receives)
hashed_email = "a665a45920422f9d417e4867efdc4fb8a04a1f3fff1fa07e998e86f7f7a27ae3"
hashed_phone = "b7f3c5e4d8a9b2f1e3d6c8a7b9e2f4d1c8b5a7e9f3d2b4c6a8e1f7d9b3c5a8e4"
Data Normalization Process
Email Normalization
Google Ads requires specific email normalization to maximize match rates:
def normalize_email(email: str) -> str:
"""
1. Trim whitespace
2. Convert to lowercase
3. Remove dots before @ in Gmail addresses
"""
email = email.strip().lower()
# Gmail dot-removal
if '@gmail.com' in email or '@googlemail.com' in email:
local, domain = email.split('@')
local = local.replace('.', '')
email = f"{local}@{domain}"
return email
# Examples:
# "Miguel.Angel@Gmail.com" → "miguelangel@gmail.com"
# " user@example.com " → "user@example.com"
Phone Normalization
Phone numbers are normalized to E.164 international format:
def normalize_phone(phone: str, country_code: str = '54') -> str:
"""
1. Remove all non-numeric characters
2. Add country code if missing
3. Format as E.164 (+country + number)
"""
phone = re.sub(r'\D', '', phone) # Keep only digits
if not phone.startswith(country_code):
phone = f"{country_code}{phone}"
return f"+{phone}"
# Examples:
# "11-1234-5678" (Argentina) → "+5491112345678"
# "(555) 123-4567" (USA) → "+15551234567"
Name and Address Normalization
def normalize_name(name: str) -> str:
"""
1. Trim whitespace
2. Convert to lowercase
3. Remove special characters
"""
name = name.strip().lower()
name = re.sub(r'[^a-z\s]', '', name)
return name
# Examples:
# "José María" → "jose maria"
# "O'Brien" → "obrien"
Hashing Methodology (SHA-256)
Why SHA-256?
- One-way function: Cannot be reversed to recover original data
- Deterministic: Same input always produces same hash
- Collision-resistant: Virtually impossible for two inputs to produce same hash
- Industry standard: Used by Google, Facebook, and other ad platforms
Implementation Example
import hashlib
def hash_data(data: str) -> str:
"""
Hash data using SHA-256
Returns:
64-character hexadecimal string
"""
return hashlib.sha256(data.encode('utf-8')).hexdigest()
# Example output
hash_data("miguel@example.com")
# → "a665a45920422f9d417e4867efdc4fb8a04a1f3fff1fa07e998e86f7f7a27ae3"
Complete Normalization + Hashing Pipeline
from google_ads_enhanced_conversions import EnhancedConversionsAPI
api = EnhancedConversionsAPI(credentials)
# Example customer data
customer = {
'email': ' Miguel@Example.com ',
'phone': '11-1234-5678',
'first_name': 'José María',
'last_name': "O'Brien",
'street_address': '123 Main St',
'city': 'Buenos Aires',
'state': 'CABA',
'postal_code': 'C1234ABC',
'country_code': 'AR'
}
# All normalization + hashing happens automatically
hashed_email = api.normalize_and_hash_email(customer['email'])
hashed_phone = api.normalize_and_hash_phone(customer['phone'], country_code='54')
hashed_name = api.normalize_and_hash_name(customer['first_name'])
print(f"Email: {hashed_email}")
print(f"Phone: {hashed_phone}")
print(f"Name: {hashed_name}")
Setup Instructions
Prerequisites
- Google Ads Account with conversion tracking enabled
- Google Ads API Access with OAuth 2.0 credentials
- Conversion Action created in Google Ads
- GCLID Auto-tagging enabled in Google Ads
- CDP Database Access with customer data
Step 1: Create Google Ads Conversion Action
# In Google Ads UI:
1. Go to Tools & Settings > Conversions
2. Click "+ New conversion action"
3. Choose "Website" or "Import"
4. Set conversion name: "Purchase" (or custom)
5. Category: "Purchase"
6. Value: "Use different values for each conversion"
7. Count: "Every" (for e-commerce)
8. Attribution model: "Data-driven" (recommended)
9. Enable "Enhanced conversions" in settings
10. Note the Conversion Action ID (format: 123456789)
Step 2: Get Google Ads API Credentials
Option A: Using Google Ads API Center (Recommended)
# 1. Apply for Google Ads API access
https://ads.google.com/home/tools/api-center/
# 2. Create OAuth 2.0 credentials
https://console.cloud.google.com/apis/credentials
# 3. Create OAuth Client ID:
- Application type: Web application
- Authorized redirect URIs: http://localhost:8080
# 4. Download credentials JSON
Option B: Using Google Cloud Console
# 1. Create or select a Google Cloud project
https://console.cloud.google.com/
# 2. Enable Google Ads API
https://console.cloud.google.com/apis/library/googleads.googleapis.com
# 3. Create OAuth 2.0 Client ID (see Option A step 3)
# 4. Get Developer Token
- Go to Google Ads UI
- Tools & Settings > API Center
- Apply for developer token access
Step 3: Generate Refresh Token
# Use Google Ads OAuth flow
# This script generates a long-lived refresh token
from google_auth_oauthlib.flow import Flow
# Configure OAuth flow
flow = Flow.from_client_secrets_file(
'client_secrets.json',
scopes=['https://www.googleapis.com/auth/adwords'],
redirect_uri='http://localhost:8080'
)
# Generate authorization URL
auth_url, _ = flow.authorization_url(prompt='consent')
print(f'Visit this URL to authorize: {auth_url}')
# After authorization, exchange code for refresh token
code = input('Enter the authorization code: ')
flow.fetch_token(code=code)
credentials = flow.credentials
print(f'Refresh token: {credentials.refresh_token}')
Step 4: Configure Conversion Action in Database
-- Insert conversion action configuration
INSERT INTO enhanced_conversions.conversion_actions (
tenant_id,
conversion_action_id,
conversion_action_name,
conversion_category,
is_active,
developer_token,
client_id,
client_secret,
refresh_token,
login_customer_id,
ads_customer_id
) VALUES (
56, -- Your tenant ID
'123456789', -- Conversion action ID from Google Ads
'Purchase',
'PURCHASE',
true,
'YOUR_DEVELOPER_TOKEN',
'YOUR_CLIENT_ID.apps.googleusercontent.com',
'YOUR_CLIENT_SECRET',
'YOUR_REFRESH_TOKEN',
NULL, -- Optional: Manager account ID (if using MCC)
'123-456-7890' -- Google Ads customer ID
);
Step 5: Enable GCLID Capture in VTEX
Ensure your VTEX store captures GCLID parameter:
// VTEX Checkout customization
// Add to orderPlaced event handler
window.addEventListener('orderPlaced', function(event) {
const order = event.data;
const gclid = new URLSearchParams(window.location.search).get('gclid');
if (gclid) {
// Send gclid with order data
fetch('/api/track-conversion', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
order_id: order.orderId,
gclid: gclid,
value: order.totalValue,
currency: order.currencyCode
})
});
}
});
Step 6: Deploy Enhanced Conversions Worker
See Operations Guide for worker deployment.
API Usage Examples
Upload Single Conversion
from google_ads_enhanced_conversions import EnhancedConversionsAPI
from datetime import datetime
from decimal import Decimal
# Initialize API
credentials = {
'developer_token': 'YOUR_DEVELOPER_TOKEN',
'client_id': 'YOUR_CLIENT_ID',
'client_secret': 'YOUR_CLIENT_SECRET',
'refresh_token': 'YOUR_REFRESH_TOKEN',
'customer_id': '123-456-7890'
}
api = EnhancedConversionsAPI(credentials)
# Customer data from CDP
customer_data = {
'email': 'customer@example.com',
'phone': '+5491112345678',
'first_name': 'María',
'last_name': 'González',
'street_address': 'Av. Corrientes 1234',
'city': 'Buenos Aires',
'state': 'CABA',
'postal_code': 'C1043',
'country_code': 'AR'
}
# Upload conversion
result = api.upload_conversion_adjustment(
conversion_action_id='123456789',
gclid='Cj0KCQjw...', # From URL parameter
conversion_date_time=datetime.now(),
conversion_value=Decimal('15000.00'),
currency_code='ARS',
customer_data=customer_data,
order_id='v123456-01'
)
print(f"Success: {result['success']}")
print(f"Identifiers sent: {result['customer_identifiers_count']}")
Batch Upload Conversions
# Upload multiple conversions at once
conversions = [
{
'conversion_action_id': '123456789',
'gclid': 'Cj0KCQjw...',
'conversion_date_time': datetime(2024, 10, 8, 14, 30),
'conversion_value': Decimal('15000.00'),
'currency_code': 'ARS',
'customer_data': {
'email': 'customer1@example.com',
'phone': '+5491112345678'
},
'order_id': 'v123456-01'
},
{
'conversion_action_id': '123456789',
'gclid': 'Cj0KCQjy...',
'conversion_date_time': datetime(2024, 10, 8, 15, 45),
'conversion_value': Decimal('8500.00'),
'currency_code': 'ARS',
'customer_data': {
'email': 'customer2@example.com',
'phone': '+5491198765432'
},
'order_id': 'v123457-01'
}
]
results = api.batch_upload_conversions(conversions)
print(f"Total: {results['total_conversions']}")
print(f"Successful: {results['successful']}")
print(f"Failed: {results['failed']}")
if results['errors']:
for error in results['errors']:
print(f"Error for {error['gclid']}: {error['error']}")
Query Conversion Actions
# List all conversion actions for account
conversion_actions = api.get_conversion_actions()
for action in conversion_actions:
print(f"ID: {action['id']}")
print(f"Name: {action['name']}")
print(f"Category: {action['category']}")
print(f"Status: {action['status']}")
print(f"Type: {action['type']}")
print("---")
Queue Conversion for Batch Processing
-- Queue a conversion to be processed by the worker
SELECT enhanced_conversions.queue_conversion(
p_tenant_id := 56,
p_gclid := 'Cj0KCQjw...',
p_conversion_action_id := '123456789',
p_conversion_date_time := NOW(),
p_conversion_value := 15000.00,
p_currency_code := 'ARS',
p_order_id := 'v123456-01',
p_customer_data := jsonb_build_object(
'hashed_email', encode(sha256('customer@example.com'::bytea), 'hex'),
'hashed_phone', encode(sha256('+5491112345678'::bytea), 'hex')
)
);
Match Rate Optimization
Factors That Improve Match Rates
- Multiple Identifiers: Send email + phone + name + address (not just email)
- Data Quality: Accurate, up-to-date customer information
- Normalization: Proper formatting before hashing
- Timing: Upload conversions within 90 days of click
- Volume: Higher conversion volume = better matching
Expected Match Rates
| Data Combination | Expected Match Rate |
|---|---|
| Email only | 60-70% |
| Email + Phone | 70-80% |
| Email + Phone + Name | 75-85% |
| Email + Phone + Name + Address | 80-90% |
Improving Match Rates
-- Check data completeness for your tenants
SELECT
tenant_id,
COUNT(*) as total_customers,
COUNT(email) as has_email,
COUNT(phone) as has_phone,
COUNT(first_name) as has_first_name,
COUNT(last_name) as has_last_name,
COUNT(street) as has_address,
ROUND(100.0 * COUNT(phone) / COUNT(*), 2) as phone_completion_rate,
ROUND(100.0 * COUNT(first_name) / COUNT(*), 2) as name_completion_rate
FROM cdp.customers
WHERE tenant_id IN (SELECT DISTINCT tenant_id FROM enhanced_conversions.conversion_actions)
GROUP BY tenant_id
ORDER BY total_customers DESC;
Troubleshooting
Common Issues
1. Authentication Failed
Error:
{
"error": "AUTHENTICATION_ERROR",
"message": "Invalid refresh token"
}
Solution:
# Regenerate refresh token using OAuth flow
# Refresh tokens can expire if not used for 6 months
python generate_refresh_token.py
2. Conversion Action Not Found
Error:
{
"error": "CONVERSION_ACTION_NOT_FOUND",
"message": "Conversion action 123456789 does not exist"
}
Solution:
-- Verify conversion action ID in Google Ads
-- Update configuration if needed
UPDATE enhanced_conversions.conversion_actions
SET conversion_action_id = 'CORRECT_ID'
WHERE tenant_id = 56;
3. Invalid GCLID
Error:
{
"error": "INVALID_GCLID",
"message": "GCLID has expired or is invalid"
}
Solution:
- Verify GCLID capture is working on website
- Conversions must be uploaded within 90 days of click
- Check that auto-tagging is enabled in Google Ads
4. Duplicate Conversion
Error:
{
"error": "DUPLICATE_CONVERSION",
"message": "Conversion with this order_id already exists"
}
Solution:
-- Check for duplicate orders
SELECT *
FROM enhanced_conversions.uploads
WHERE order_id = 'v123456-01'
AND tenant_id = 56;
-- If duplicate, this is expected behavior (prevents double-counting)
5. Customer Data Required
Error:
{
"error": "CUSTOMER_DATA_REQUIRED",
"message": "At least email is required for enhanced conversions"
}
Solution:
-- Verify customer data availability in CDP
SELECT
o.order_id,
c.email,
c.phone,
c.first_name,
c.last_name
FROM vtex_orders o
LEFT JOIN cdp.customers c ON o.email = c.email
WHERE o.order_id = 'v123456-01'
AND o.tenant_id = 56;
-- If customer not found, enrichment from VTEX MasterData is needed
Debug Mode
# Enable verbose logging
import logging
logging.basicConfig(level=logging.DEBUG)
logger = logging.getLogger('google_ads_enhanced_conversions')
logger.setLevel(logging.DEBUG)
# Run upload with detailed logs
result = api.upload_conversion_adjustment(...)
Testing with Dry-Run Mode
# Initialize worker in dry-run mode (no actual uploads)
from enhanced_conversions_worker import EnhancedConversionsWorker
worker = EnhancedConversionsWorker(dry_run=True)
results = worker.process_all_pending()
print(f"Would upload {results['total']} conversions")
Monitoring and Validation
Verify Uploads in Google Ads
# 1. Go to Google Ads UI
# 2. Tools & Settings > Conversions
# 3. Click on your conversion action
# 4. Check "Enhanced conversions" status
# 5. View "Diagnostics" tab for match rate
# Expected:
# - Status: "Recording conversions"
# - Enhanced conversions: "On"
# - Match rate: 70-85% (after 7 days)
Check Upload Status in Database
-- Recent uploads
SELECT
id,
tenant_id,
gclid,
conversion_value,
upload_status,
uploaded_at,
error_message
FROM enhanced_conversions.uploads
ORDER BY created_at DESC
LIMIT 20;
-- Success rate by tenant
SELECT
tenant_id,
COUNT(*) as total_uploads,
SUM(CASE WHEN upload_status = 'success' THEN 1 ELSE 0 END) as successful,
SUM(CASE WHEN upload_status = 'failed' THEN 1 ELSE 0 END) as failed,
ROUND(100.0 * SUM(CASE WHEN upload_status = 'success' THEN 1 ELSE 0 END) / COUNT(*), 2) as success_rate
FROM enhanced_conversions.uploads
GROUP BY tenant_id;
Monitor Daily Statistics
-- Daily performance metrics
SELECT *
FROM enhanced_conversions.v_daily_performance
WHERE tenant_id = 56
ORDER BY upload_date DESC
LIMIT 30;
-- Pending conversions queue
SELECT *
FROM enhanced_conversions.v_pending_uploads;
Integration with Other Systems
Customer Match Integration
Enhanced Conversions works alongside Google Ads Customer Match:
- Customer Match: Uploads audience lists for targeting
- Enhanced Conversions: Enriches individual conversion events
Both use the same hashing methodology and can share credentials.
-- Shared credentials configuration
SELECT
tenant_id,
'Customer Match' as system,
customer_id as ads_account
FROM google_ads.configurations
UNION ALL
SELECT
tenant_id,
'Enhanced Conversions' as system,
ads_customer_id as ads_account
FROM enhanced_conversions.conversion_actions
WHERE tenant_id = 56;
Bid Automation Integration
Enhanced Conversions provides more accurate data for bid automation:
-- View conversion performance for bid optimization
SELECT
ca.tenant_id,
ca.conversion_action_name,
COUNT(u.id) as total_conversions,
SUM(u.conversion_value) as total_value,
AVG(u.conversion_value) as avg_order_value,
SUM(CASE WHEN u.upload_status = 'success' THEN 1 ELSE 0 END) as enhanced_conversions
FROM enhanced_conversions.uploads u
JOIN enhanced_conversions.conversion_actions ca
ON u.tenant_id = ca.tenant_id
AND u.conversion_action_id = ca.conversion_action_id
WHERE u.conversion_date_time >= CURRENT_DATE - INTERVAL '30 days'
GROUP BY ca.tenant_id, ca.conversion_action_name;
Best Practices
Data Quality
- Clean Email Addresses: Remove invalid/test emails from CDP
- Phone Number Validation: Ensure phone numbers include country code
- Name Consistency: Standardize name formatting across sources
- Address Enrichment: Fill missing address fields when possible
Upload Timing
- Real-time: Upload conversions as they happen (recommended)
- Batch: Process in batches every 30 minutes (current implementation)
- Historical: Can upload conversions up to 90 days old
Security
- Encrypt Credentials: Store Google Ads credentials encrypted in database
- Rotate Tokens: Refresh OAuth tokens before expiration
- Audit Logs: Monitor all uploads for anomalies
- Access Control: Limit database access to conversion data
Performance
- Batch Size: 100 conversions per batch (optimal)
- Concurrent Uploads: Process multiple tenants in parallel
- Rate Limiting: Respect Google Ads API quotas
- Retry Logic: Maximum 3 attempts for failed uploads
References
Official Documentation
- Google Ads Enhanced Conversions Guide
- Google Ads API Enhanced Conversions
- SHA-256 Hashing Requirements
- Google Ads API Python Client
Internal Documentation
Related Workers
- Enhanced Conversions Worker: Processes conversion uploads (this system)
- Google Ads Customer Match Worker: Uploads audience lists
- Bid Automation Worker: Optimizes Google Ads bidding
Last updated: October 8, 2025 Version: 1.0.0 Author: Miguel Angel Hernandez Status: Production-ready