Enhanced Conversions vs Conversion Import
Google uses similar language for two related but different jobs. Enhanced Conversions for web improves an existing online conversion by attaching hashed first-party data to the event. A browser tag, Google tag, or GTM setup records the conversion with a click identifier and order ID; the enhanced signal helps Google match that conversion to signed-in users when browser identifiers are incomplete. This mode fits standard ecommerce purchases, lead forms, bookings, and checkout flows where the final conversion happens on the website and the page can still run Google instrumentation.
Conversion Import is the server-side path. Your backend, CRM, subscription platform, or tracking pipeline uploads conversions through the Google Ads API. It applies when the true conversion happens after the page view: qualified leads, offline sales, delayed payment capture, call center deals, renewals, or orders created by a backend job. In practice, mature teams often use both modes. The web page tag captures the immediate checkout conversion, while the server-side upload handles events that happen after checkout or need stronger backend governance.
The decision is not ideological. If the page reliably knows the conversion, value, order ID, and consent state, the web tag mode is simple and fast. If the server is the source of truth, use uploads. If both can see the same conversion, define ownership carefully so Google does not receive duplicate unrelated events.
Prerequisites
Most failed Google Ads Enhanced Conversions projects do not fail because the payload is complex. They fail because account access, conversion action ownership, OAuth scopes, consent, or click ID capture were assumed instead of verified. Confirm the following before writing production upload code.
Step-by-step setup
Build the integration in layers. First prove authorization. Then prove the conversion action resource. Then send a minimal click-ID conversion. Only after that should you add enhanced identifiers, fallback logic, batching, retries, and monitoring.
Create the OAuth client and capture a refresh token
Google Ads API uploads use OAuth, not a static account key. Create an OAuth client in Google Cloud, request the Ads scope, complete the consent screen with the Google user that owns or manages the Ads account, and store the refresh token in your secrets manager. Treat the token as production infrastructure because failed refreshes stop conversion uploads silently until monitoring catches them.
GET https://accounts.google.com/o/oauth2/v2/auth
?client_id=GOOGLE_CLIENT_ID
&redirect_uri=https://app.example.com/oauth/google/callback
&response_type=code
&scope=https://www.googleapis.com/auth/adwords
&access_type=offline
&prompt=consent
POST https://oauth2.googleapis.com/token
content-type: application/x-www-form-urlencoded
code=AUTH_CODE&
client_id=GOOGLE_CLIENT_ID&
client_secret=GOOGLE_CLIENT_SECRET&
redirect_uri=https://app.example.com/oauth/google/callback&
grant_type=authorization_codeResolve the customer ID and conversion action resource
Use the customer account that owns the conversion action, not just the manager account you log into every day. Store customer IDs without hyphens and store conversion actions as resource names. That gives every upload job a stable target and avoids the common mistake of sending a valid conversion to the wrong child account.
customer_id = "1234567890"
conversion_action_id = "987654321"
conversion_action =
"customers/1234567890/conversionActions/987654321"Call the conversion upload endpoint
Batch uploads should be small enough to retry safely and large enough to avoid noisy per-order API calls. Use partial failure mode so one bad identifier does not block the whole batch. For documentation and internal runbooks, many teams describe this as posting to /customers/{id}/conversionUploads:upload; in the current REST API this maps to the customer uploadClickConversions method.
POST https://googleads.googleapis.com/v22/customers/{customer_id}:uploadClickConversions
developer-token: GOOGLE_ADS_DEVELOPER_TOKEN
login-customer-id: MANAGER_CUSTOMER_ID
authorization: Bearer ACCESS_TOKEN
content-type: application/json
{
"partialFailure": true,
"validateOnly": false,
"debugEnabled": false,
"conversions": []
}Send the base conversion payload
The first payload should be boring: one conversion action, one timestamp, one value, one currency, one order ID, one click identifier, and explicit consent. Get this accepted before adding enhanced identifiers. A minimal accepted payload proves account access, conversion action ownership, timestamp formatting, and click ID capture.
{
"partialFailure": true,
"conversions": [
{
"conversionAction": "customers/1234567890/conversionActions/987654321",
"conversionDateTime": "2026-04-23 14:31:09+02:00",
"conversionValue": 129.95,
"currencyCode": "EUR",
"orderId": "ORDER-10492",
"gclid": "EAIaIQobChMI...",
"conversionEnvironment": "WEB",
"consent": {
"adUserData": "GRANTED",
"adPersonalization": "GRANTED"
}
}
]
}Add enhanced conversion identifiers
Enhanced Conversions depends on normalized and SHA-256 hashed user data. Normalize before hashing: lowercase emails, trim whitespace, remove separators from phone numbers, and format phone numbers consistently. Do not hash nulls, placeholders, or values collected without the right consent basis. Send multiple identifiers when you have them because match quality usually improves when email and phone arrive together.
import crypto from "node:crypto";
function sha256(value: string) {
return crypto
.createHash("sha256")
.update(value.trim().toLowerCase())
.digest("hex");
}
const userIdentifiers = [
{ hashedEmail: sha256("customer@example.com") },
{ hashedPhoneNumber: sha256("+491701234567") },
];Choose click ID first, enhanced identifiers as fallback
For web purchases, click identifiers remain the cleanest attribution key. Use GCLID when present, GBRAID for iOS app-originated click paths, and WBRAID for iOS web-originated click paths. When no click ID survives into the order, upload enhanced conversion identifiers so Google can attempt a user match. Keep the logic explicit, and never invent a click ID from session data.
const conversion = {
conversionAction,
conversionDateTime: order.paidAtGoogleAdsFormat,
conversionValue: order.total,
currencyCode: order.currency,
orderId: order.id,
conversionEnvironment: "WEB",
consent: {
adUserData: order.consent.ad_user_data,
adPersonalization: order.consent.ad_personalization,
},
};
if (order.gclid) conversion.gclid = order.gclid;
else if (order.gbraid) conversion.gbraid = order.gbraid;
else if (order.wbraid) conversion.wbraid = order.wbraid;
else conversion.userIdentifiers = userIdentifiers;Payload structure
Google Ads field names differ slightly between REST JSON, client libraries, and internal event schemas. The table below uses snake_case labels because those are common in ecommerce data models, but your final API request should map them to the current Google Ads API casing for the method you call.
| Field | Required | Purpose | Example |
|---|---|---|---|
| conversion_action | Yes | Names the Google Ads conversion action that should receive the event. | customers/1234567890/conversionActions/987654321 |
| conversion_date_time | Yes | Tells Google when the conversion happened in the advertiser timezone. | 2026-04-23 14:31:09+02:00 |
| conversion_value | Recommended | Feeds value-based bidding, ROAS reporting, and revenue diagnostics. | 129.95 |
| currency_code | Required with value | Defines the ISO currency for the uploaded conversion value. | EUR |
| order_id | Recommended | Deduplicates repeat uploads and joins diagnostics back to commerce records. | ORDER-10492 |
| user_identifiers.hashed_email | Enhanced fallback | Matches the conversion to signed-in Google users when click IDs are absent or incomplete. | sha256(lowercase_trimmed_email) |
| user_identifiers.hashed_phone_number | Enhanced fallback | Adds a second durable first-party identifier for match quality. | sha256(+491701234567) |
| address_info | Optional | Provides name and postal address signals when email or phone are unavailable. | first_name, last_name, country_code, postal_code |
| gclid | One click ID or identifier | Attributes standard web conversions to the Google click that produced the visit. | EAIaIQobChMI... |
| gbraid | Conditional | Handles iOS app-related click paths where GCLID is not available. | 0AAAAA... |
| wbraid | Conditional | Handles iOS web click paths where privacy controls prevent a standard GCLID. | CjwKCA... |
| consent settings | Yes for governed regions | Carries ad user data and personalization choices into the upload. | adUserData: GRANTED, adPersonalization: DENIED |
Consent signals
Enhanced Conversions should not be treated as a way around consent. The upload must reflect the user choice your site collected. In Google’s consent model, the important ad fields are ad_user_data and ad_personalization. ad_user_data controls whether user data may be sent to Google for advertising purposes. ad_personalization controls whether that data may be used for personalized advertising. Your internal event can store these as a google-ads-consent object, then map them into the upload payload.
{
"google-ads-consent": {
"ad_user_data": "GRANTED",
"ad_personalization": "DENIED"
}
}Consent Mode v2 should be integrated before the conversion upload layer, not bolted on afterward. The page-level consent state should be available before tags fire, persisted with the session, attached to the order or lead, and used by the server upload job. If consent is denied for ad_user_data, remove hashed email, hashed phone, and address_info from the payload. If ad_personalization is denied, pass that denial forward rather than assuming the platform will infer it from missing identifiers.
This matters operationally as well as legally. Diagnostics are easier to read when every uploaded conversion has an explicit consent state. A sudden drop in match rate can then be traced to actual consent mix, a CMP release, or missing identifiers instead of a vague suspicion that Google rejected the batch.
Testing + validation
Start validation with validateOnly set to true, then send a controlled real upload with partial failure enabled. Once the API accepts the payload, move into Google Ads. Open the conversion action, inspect the diagnostics tab, and compare upload behavior against your backend event log. The goal is to prove not only that Google accepted the request, but that the conversion action is healthy enough to influence reporting and bidding.
Match rate
Watch the percentage of uploaded enhanced conversions Google can match. Use a practical guardrail — investigate when match rate moves ≥ 10 points down or stays ≤ your historical baseline for a full day. A weak match rate usually means identifiers are missing, not normalized before hashing, collected too late, or blocked by consent state.
Attribution lift
Compare reported conversions and value before and after the upload becomes stable. Lift should be evaluated over days, not minutes, because diagnostics and bidding feedback are delayed.
Diagnostic alerts
Review warnings for formatting, consent, duplicate order IDs, stale timestamps, missing click IDs, and unverified conversion actions. Fix these before judging campaign performance.
Conversion action status
Confirm the action is recording, included in account goals if intended, and not marked inactive, unverified, or recently edited in a way that changes optimization behavior.
Troubleshooting
Treat upload errors as data quality feedback. Log the Google request ID, customer ID, conversion action, order ID, selected click ID, consent state, and partial failure details. Without those fields, the same error can point to five different systems.
The identifier is empty, malformed, unhashed, double-hashed, or not normalized before hashing. Rebuild hashing around clean lowercase email and E.164-style phone values.
The conversion timestamp is earlier than the click. Check timezone conversion, delayed payment capture, imported legacy orders, and server clocks.
The access token is expired, the refresh token was revoked, or the OAuth scope is wrong. Refresh the token and confirm the adwords scope is present.
The OAuth user or manager account cannot access the target customer or conversion action. Verify login-customer-id, customer hierarchy, and user permissions.
Google could not find a matching ad click for the supplied ID or identifiers. This is expected for non-Google traffic when debug checks are strict; filter by source and tune debugEnabled.
The same order ID was uploaded for the same conversion action. Keep order IDs stable for idempotency, but avoid resending corrected conversions as new purchases.
FAQ
Do Enhanced Conversions replace the Google tag?
Not always. Enhanced Conversions for web commonly starts with the Google tag or GTM sending a conversion and order ID, then the API enhances that conversion with hashed user data. Server-side uploads are useful when the final conversion source is backend checkout, CRM, subscription billing, or a lead qualification system.
Should I upload every order or only Google Ads orders?
For enhanced conversion fallback workflows, many teams upload all eligible conversion events and let Google match what belongs to Google Ads traffic. Keep consent filtering in place and decide whether debug mode should surface expected CLICK_NOT_FOUND cases.
Can I send both GCLID and hashed email?
Yes. For web conversions, a click ID plus enhanced user identifiers is typically stronger than either signal alone. The important part is that the data describes the same conversion and was collected with appropriate consent.
How fast do diagnostics update?
Uploads return API acceptance quickly, but Google Ads diagnostics and reporting can lag. Evaluate status over at least a full day of normal traffic, and longer when conversion volume is low.
What timestamp should I use for delayed payments?
Use the time the conversion actually occurred according to your business definition. For ecommerce, that is usually paid order time, not first checkout start. Keep the timezone explicit so Google does not compare the conversion against the click incorrectly.
What consent values should I send?
Map your CMP or Consent Mode v2 state into ad_user_data and ad_personalization. When consent is denied, do not send personal identifiers and send the denied state so upload behavior matches the user choice.
Next reads
How TrackLayer captures click IDs, hashes identifiers, and routes Google Ads conversions server-side.
Audit whether consent state is visible before marketing tags and conversion uploads fire.
Move server-side GTM workflows into a managed tracking pipeline without losing diagnostics.