Methodology

Google Ads API Offline-Conversion Upload — the OfflineConversionUploadGclidService end-to-end

Copy-pasteable recipe for firing closed-revenue conversions to Google Ads via the OfflineConversionUploadGclidService. Schema, request payload, the GAQL queries that confirm the upload landed, and the six failure modes to verify against before declaring the wire-up complete.

Google's bidder learns from your offline conversions more aggressively than any other platform in the fanout — which makes this the one wire-up where getting the gclid, the hash, and the timezone right pays back the fastest. Here is the request, the GAQL that proves it landed, and the six ways it silently fails.

This is the Google Ads half of the server-side conversion attribution stack — the OfflineConversionUploadGclidService request, the prerequisites that must be true first, the verification queries, and the failure-mode checklist. It assumes the pillar's prerequisites are in place: a Worker receiving the canonical event, a milestone definition in writing, and consent propagating through the pipeline.

Prerequisites — what must already be true

Five things, each checkable in the Ads UI before you write any wire-up code:

  • A developer token at standard access (basic access is rate-limited too hard for production) — apply 1–3 business days ahead.
  • An offline conversion action (Tools → Conversions → New → Import → CRM). Set count to "One" for service businesses, attribution to data-driven if eligible, and capture the resource name customers/{id}/conversionActions/{id} — the request needs it.
  • OAuth credentials with the adwords scope; the refresh token lives in the Worker as a secret.
  • The gclid flowing to the CRM. Landing pages capture ?gclid= from the URL, persist it as a hidden form field, and store it on the record. No gclid, no attribution — the upload becomes noise.
  • Enhanced-conversion data hashed at the Worker — email and phone SHA-256 hashed (lowercased, trimmed), plaintext never appearing downstream of the CRM.

The request payload

One UploadClickConversionsRequest carrying an array of ClickConversion records:

{
  "customer_id": "1234567890",
  "conversions": [
    {
      "gclid": "Cj0KCQiA...",
      "conversion_action": "customers/1234567890/conversionActions/987654321",
      "conversion_date_time": "2026-05-26 14:32:18-05:00",
      "conversion_value": 7500.00,
      "currency_code": "USD",
      "order_id": "crm-record-abc-123:lead-milestone",
      "user_identifiers": [
        { "hashed_email": "5e884898da28047151d0e56f8dc6292773603d0d6aabbdd62a11ef721d1542d8" },
        { "hashed_phone_number": "5994471abb01112afcc18159f6cc74b4f511b99806da59b3caf5a9c173cacfc5" }
      ]
    }
  ],
  "partial_failure": true,
  "validate_only": false
}

Six fields warrant attention. gclid is the captured click ID — expired ones (older than the ~90-day window) are dropped silently and surface only in partial_failure_error. conversion_action must match the account exactly, or you get CONVERSION_ACTION_NOT_FOUND. conversion_date_time must be in the account's reporting timezone (YYYY-MM-DD HH:MM:SS±HH:MM) — a UTC Worker firing into a UTC-5 account dates conversions five hours early, sometimes outside the window. conversion_value / currency_code are the closed-revenue amount and ISO 4217 code in the account's reporting currency. order_id is the dedupe key — use a stable, globally-unique value the CRM owns. user_identifiers carries enhanced conversions, each a hashed_email, hashed_phone_number, or address block, hashed as hex SHA-256, lowercase.

Keep partial_failure: true so a single bad record doesn't reject the batch — Google returns per-conversion failures inline instead.

Enhanced-conversion hashing — where Google differs from Meta

Three normalization steps before the hash, and one place this diverges from Meta CAPI: email — lowercase, trim, SHA-256 (Google does not do Gmail dot/plus normalization as of 2026 — lowercase + trim only); phone — prefix with + and country code, strip non-digits, then hash, so (415) 555-0100 becomes +14155550100 (Meta omits the prefix — this is the divergence that breaks teams reusing one hash function); name — lowercase and trim. Output must be hex-encoded lowercase — Google rejects uppercase hex silently.

The GAQL query that confirms it landed

Uploads appear in the Conversions UI within 24 hours (faster for high-traffic accounts). This query confirms volume independently of the UI:

SELECT
  segments.conversion_action,
  segments.date,
  metrics.all_conversions,
  metrics.all_conversions_value
FROM customer
WHERE segments.date DURING LAST_7_DAYS
  AND segments.conversion_action = 'customers/1234567890/conversionActions/987654321'

The all_conversions count should track your CRM milestone count within about ±5% (allowing for click-ID expiry, dedupe drops, and validation failures). A materially lower count is the symptom; the failure modes below are the diagnosis.

Want this wire-up shipped, audited, and verified? Talk to the team that runs it for living operators. →

The six failure modes

  • gclid expiry. The window is ~90 days. Capture the click date alongside the gclid; reject older events at the Worker before upload.
  • Conversion-action mismatch. Pin the resource name in the Worker's env; a wrong customer-ID prefix is the cleanest error signature.
  • Hash format. Uppercase hex, missing canonicalization, or base64 all fail matching silently — the conversion uploads but enhanced attribution doesn't happen. Unit-test against Google's test vectors.
  • Timezone drift. Format conversion_date_time in the account's reporting timezone, retrieved once at Worker start and cached.
  • Currency mismatch. currency_code must match the account; multi-currency operators convert at the Worker, not Google's edge (see multi-currency attribution).
  • Dedupe-key collision. Reusing an order_id across distinct conversions silently merges them. Use {record_id}:{milestone_name}, not just {record_id}.

Post-launch verification

Four checks before you call the Google side done: upload success rate in Logpush above ~98% (sub-95% means a failure mode is firing systematically); conversion-count parity via the GAQL query daily for 14 days, tracking the CRM count within ±5%; enhanced-conversion match rate in the Conversions UI's Diagnostics tab, which should climb past ~60% within 30 days (a low rate points at hash-format failures); and partial-failure log review — treat the inline per-conversion failures as warnings, not noise, and the systematic patterns surface in the first week.

Closing

The Google Ads offline-conversion upload is the single most consequential wire-up in the stack, because Google's smart-bidding learns from this signal harder than any other platform's. Getting it right inside the first month is the highest-leverage move you can make on the cost-per-acquired-customer curve. The failure modes above are not theoretical — every one shows up in wire-ups that shipped without oversight — and the verification checklist is non-negotiable. The boring path that prints.

Ready to wire Google Ads offline conversions correctly the first time? Book a 30-minute call →

Share X LinkedIn
Build it yourself?

Get the kit, not just the theory.

We'll send the build checklist behind this post — and the next pillar when it ships. One email, no drip sequence. Unsubscribe in one click.

Want this built for you?

Book a discovery call. We'll walk your numbers.

20 minutes. Tell us what's broken, hear what we'd ship in the next 90 days. No pitch deck.