EC for Leads vs EC for Web
Enhanced Conversions for Web and Enhanced Conversions for Leads sound similar because both rely on first-party data, but they solve different attribution problems. EC for Web improves an online conversion that already happened on the website, usually through a Google tag or GTM event that sends hashed customer details alongside a purchase or lead submit. The browser is still the place where the conversion is defined. This is the right fit when the final event actually happens on the page and the page can reliably hold the order ID, consent state, and identifiers at the moment the conversion fires.
EC for Leads is the workflow for offline or CRM-resolved lead events. The user might submit a form on Tuesday, become an MQL on Wednesday, get accepted by sales on Friday, and turn into an opportunity weeks later. Those milestones do not live in the browser anymore. They live in Salesforce, HubSpot, or a backend workflow. That is why the B2B setup depends on server-side upload jobs, explicit conversion action design, and lifecycle timestamps from the CRM rather than a page-only tag.
Why B2B needs EC for Leads
B2B buyers do not convert in one pageview. The first browser form submit is useful, but it is rarely the conversion the business wants to optimize budget against. Sales teams care about whether the lead met qualification rules, whether the account entered pipeline, and whether revenue eventually closed. Those events are offline in the advertising sense: they happen after the visit, inside CRM and sales systems, often after routing, enrichment, human review, and multi-touch follow-up.
That is exactly why EC for Leads exists. You need a server-side upload path that turns those CRM milestones into Google Ads conversions. Without that upload layer, the platform sees form volume but not business quality. With it, campaigns can be evaluated against MQLs, SQLs, opportunities, and eventually won revenue instead of only top-of-funnel submits.
Prerequisites
Google Ads linked to the source CRM system such as Salesforce or HubSpot, with the target conversion actions already created and owned by the correct customer account.
Conversion action setup agreed with revenue operations before implementation starts, including which lifecycle stages should count as primary optimization events versus reporting-only milestones.
OAuth client, developer token, and a refresh token stored in secrets for a Google user who can upload conversions into the destination customer ID.
Reliable gclid capture at the exact form submission moment, with the click ID persisted onto the lead or contact record before routing or enrichment modifies the record.
Hashed user_identifiers ready from first-party lead data, normalized before SHA-256 hashing so email and phone can be used when click identifiers are missing or incomplete.
Setup
Build the integration in layers and resist the urge to send every downstream stage on day one. The fastest way to a stable rollout is to prove customer ownership, gclid persistence, one accepted lead upload, and only then expand into SQL, opportunity, and adjustment logic.
Create separate conversion actions for each B2B milestone
Do not collapse the entire pipeline into one generic lead action. In B2B, media teams, rev ops, and sales need to know whether Google generated an MQL, a sales-qualified lead, or real pipeline. Create explicit conversion actions for the lifecycle points you plan to upload. That gives bidding and reporting distinct targets and makes adjustment logic much easier to reason about later.
// Example internal mapping
const conversionActions = {
mql: "customers/1234567890/conversionActions/1111111111",
sql: "customers/1234567890/conversionActions/2222222222",
opportunity: "customers/1234567890/conversionActions/3333333333",
closedWon: "customers/1234567890/conversionActions/4444444444",
};Capture gclid at the form submit boundary and write it into CRM
The landing URL is where the click identifier first appears, but the CRM record is where B2B attribution is usually resolved later. Capture gclid, gbraid, or wbraid when the form is submitted, not only in a browser cookie, and write it onto the lead or contact before routing workflows or dedup jobs run. If the CRM object loses the click ID, later offline uploads will have to rely on weaker identifier matching.
type LeadCapturePayload = {
email: string;
phone?: string;
company?: string;
gclid?: string;
gbraid?: string;
wbraid?: string;
};
await crm.contacts.create({
email: form.email,
phone: form.phone,
company: form.company,
gclid: attribution.gclid,
gbraid: attribution.gbraid,
wbraid: attribution.wbraid,
});Authenticate the upload worker with an OAuth refresh token
Google Ads offline lead uploads are infrastructure, not a dashboard task. Use an OAuth refresh token for a Google user with access to the Ads customer, exchange it for an access token in the worker, and log which customer ID and manager hierarchy the upload is targeting. Most early failures come from valid tokens pointing at the wrong customer account or a user that can view campaigns but cannot upload conversions.
POST https://oauth2.googleapis.com/token
content-type: application/x-www-form-urlencoded
client_id=GOOGLE_CLIENT_ID&
client_secret=GOOGLE_CLIENT_SECRET&
refresh_token=GOOGLE_REFRESH_TOKEN&
grant_type=refresh_tokenUpload the qualified-lead conversion through the lead upload endpoint
Once the CRM marks a lead as an MQL or SQL, build the upload from the CRM record, not from the page session. The payload should include the conversion action resource, timestamp, currency and value if used, the stored click ID when present, and hashed user_identifiers as backup matching data. Many internal runbooks describe this family of requests as posting to /customers/{id}/conversionUploads. Keep that abstraction if it matches your platform, but make sure the actual Google Ads API method and field casing stay current in code.
POST https://googleads.googleapis.com/v22/customers/1234567890:uploadClickConversions
developer-token: GOOGLE_ADS_DEVELOPER_TOKEN
login-customer-id: 9999999999
authorization: Bearer ACCESS_TOKEN
content-type: application/json
{
"partialFailure": true,
"validateOnly": false,
"conversions": [
{
"conversionAction": "customers/1234567890/conversionActions/2222222222",
"conversionDateTime": "2026-04-24 10:42:00+02:00",
"conversionValue": 250.0,
"currencyCode": "USD",
"gclid": "EAIaIQobChMI-example",
"orderId": "lead_98431_sql",
"userIdentifiers": [
{ "hashedEmail": "973dfe463ec85785f5f95af5ba3906ee..." },
{ "hashedPhoneNumber": "b49f9168e8a886ffd61a090b51a26e11..." }
]
}
]
}
// Internal abstraction example
await post("/customers/1234567890/conversionUploads", leadUploadPayload);Prepare user_identifiers and conversion_adjustment payloads for later stages
B2B programs rarely stop at one upload. After the initial qualified-lead event, many teams either upload later lifecycle actions as separate conversions or adjust the original conversion when value or status changes. Keep normalized identifiers and the original upload keys available so downstream stage changes can send either a fresh conversion or a conversion_adjustment without rebuilding attribution data from scratch.
import crypto from "node:crypto";
function sha256(value: string) {
return crypto
.createHash("sha256")
.update(value.trim().toLowerCase())
.digest("hex");
}
const user_identifiers = [
{ hashedEmail: sha256("buyer@example.com") },
{ hashedPhoneNumber: sha256("+14155550123") },
];
const conversion_adjustment = {
adjustmentType: "RESTATE",
conversionAction: "customers/1234567890/conversionActions/3333333333",
adjustmentDateTime: "2026-05-06 15:10:00+02:00",
gclidDateTimePair: {
gclid: "EAIaIQobChMI-example",
conversionDateTime: "2026-04-24 10:42:00+02:00",
},
restatementValue: {
adjustedValue: 48000,
currencyCode: "USD",
},
userIdentifiers: user_identifiers,
};Make uploads idempotent and traceable back to CRM history
Every upload should be explainable from one CRM record, one lifecycle transition, and one conversion action. Use stable IDs for lead-stage events, store Google request metadata, and protect the worker against repeated uploads when enrichment jobs, warehouse syncs, or CRM merge flows replay the same record. Idempotency matters more in B2B because volumes are lower and a few duplicates can materially distort campaign quality signals.
const dedupeKey = [
lead.crmId,
lead.stage,
lead.stageChangedAt.toISOString(),
destinationConversionAction,
].join(":");
if (await uploadsLog.exists(dedupeKey)) return;
const response = await googleAds.uploadLeadConversion(payload);
await uploadsLog.insert({
dedupeKey,
crmId: lead.crmId,
stage: lead.stage,
conversionAction: destinationConversionAction,
googleRequestId: response.requestId,
});Lead stage mapping
The mapping between CRM stage and Google Ads conversion action should be agreed before launch. If marketing, rev ops, and sales use different definitions for qualification, the upload worker will faithfully send ambiguous data. Keep the action names simple and make sure one CRM transition maps to one clearly owned Ads action.
| CRM stage | Google Ads conversion action | Upload mode | Operational note |
|---|---|---|---|
| MQL | Qualified lead | Initial offline conversion | Use when marketing has enough evidence to say the lead is worth sales review. |
| SQL | Sales qualified lead | Separate conversion or adjustment | Often the first stage worth optimizing campaigns toward in B2B. |
| Opp | Opportunity created | High-value offline conversion | Best when the CRM amount is reliable and pipeline creation is the media KPI. |
| Closed-won | Closed won | Final value upload or restatement | Use to feed revenue truth back into Google Ads once finance status is confirmed. |
| Closed-lost | Disqualified or lost opportunity | Retraction or negative adjustment policy | Depends on the action design. Some teams adjust value down, others keep it for reporting only. |
Timing nuances
Google Ads diagnostics and reporting do not update instantly. In practice, teams should expect visible delay and give the system up to 72 hours before judging whether a new EC for Leads workflow is healthy. That lag is uncomfortable for launch-day debugging, but it is normal. Use API acceptance, request logs, and partial failure responses as the immediate truth. Use the Ads UI later as the reporting truth.
The operational pattern is simple: send the CRM lifecycle time honestly, keep retries idempotent, and do not re-upload the same stage every hour just because the UI has not caught up yet. If a lead becomes SQL today and opportunity next week, upload those as distinct lifecycle moments. If a stage was uploaded with the wrong value, use your adjustment policy rather than inventing a second conversion that muddies the timeline.
Sensitive gclid handling
The gclid originates on the landing URL, which means it is transient unless you capture it intentionally. For B2B, that capture has to survive long enough to reach the lead or contact in the CRM. The safest pattern is to collect it when the form is submitted and store it on the CRM record as a first-class attribution field. That field should persist through merges, routing, enrichment, and lifecycle updates so later uploads can reference the original ad click.
This data is sensitive because it ties an ad click to a known lead record. Treat it as restricted attribution metadata, not as a casual front-end parameter. TrackLayer bridge handles this automatically by capturing the click ID from the landing URL, carrying it through the form boundary, and writing it into the CRM payload before the lead record is finalized.
Troubleshooting
Most EC for Leads failures are not Google Ads mysteries. They are systems problems with one of four roots: bad account wiring, missing click IDs, weak identifier normalization, or duplicate CRM replay behavior. Troubleshooting is faster when every upload is logged with CRM ID, conversion action, stage name, lifecycle timestamp, and Google request metadata.
PERMISSION_DENIED
The OAuth user or manager hierarchy cannot upload into the target customer or conversion action. Check customer ownership, login-customer-id, and user access first.
CLICK_NOT_FOUND
The stored gclid is missing, expired, malformed, or was never written to the CRM object. Audit capture at submit time before changing matching logic.
INVALID_USER_IDENTIFIER
Identifiers are empty, unhashed, double-hashed, or not normalized before hashing. Lowercase and trim emails, normalize phones, and never hash placeholders.
DUPLICATE_CONVERSION
The same stage change has been uploaded twice. Add a dedupe key using CRM record ID, stage, stage-change time, and conversion action.
TIMESTAMP_OUT_OF_RANGE
Your stage-change timestamp, timezone, or adjustment reference date is wrong. Compare CRM lifecycle time against the original click and the advertiser timezone.
FAQ
Is EC for Leads the same as Enhanced Conversions for Web?
No. EC for Web improves a website conversion that already fired on the page. EC for Leads is the server-side upload path for lead and CRM events that happen later, often offline from the original session.
Should B2B teams optimize to form submit or SQL?
Usually both are useful, but they serve different jobs. Form submit gives faster volume and operational monitoring. SQL or opportunity is often the better optimization signal once volume is high enough and sales qualification is consistent.
Do I need both gclid and hashed identifiers?
Yes when you can collect them lawfully. gclid is still the cleanest click-level key. Hashed identifiers improve resilience when the click ID is missing, delayed, or partially lost across systems.
How should I handle lead merges in the CRM?
Preserve the original click IDs and upload history on the surviving record. Without merge-safe attribution fields, later stage uploads can reference the wrong person or lose the original Google click.
Can closed-lost be sent to Google Ads?
It can, but the design should be intentional. Some teams send a negative adjustment or restatement to remove overvalued pipeline. Others keep closed-lost out of Ads and analyze it only in BI. The right choice depends on bidding strategy and governance.