Documentation menu

Google Ads and GA4

One adapter, two modes:

  • ads — offline click conversions uploaded to the Google Ads API, with enhanced conversions for leads.
  • ga4 — events sent to Google Analytics 4 through the Measurement Protocol.

Verified against the Google documentation on 2026-09-03: Upload click conversions, Sunset dates, Enhanced conversions for leads, OAuth 2.0 for web server apps, GA4 Measurement Protocol reference.


Mode ads

Prerequisites

  • A Google Ads account, and a conversion action of type "Import from clicks" in it.
  • A Google Ads manager account — the developer token lives there.
  • A Google Cloud project with the Google Ads API enabled and an OAuth client.

What to collect

Value Where to find it Stored as
Customer ID Top right of the Google Ads UI, as 123-456-7890. Enter it digits only: 1234567890. Config
Login customer ID The manager account's customer ID, digits only. Required only when you authenticate through a manager account; leave empty when the OAuth user is on the client account directly. Config
Conversion action ID Tools → Conversions → your action. The ctId parameter in the URL. Used as the default when a mapping rule does not name one. Config
Developer token Manager account → Tools → API Center. Needs at least Basic access to send live traffic. Vault
OAuth client ID and client secret Google Cloud console → APIs & Services → Credentials → OAuth 2.0 Client IDs (type "Desktop app" or "Web application"). Vault
Refresh token Generated once by authorising the OAuth client for the scope https://www.googleapis.com/auth/adwords with access_type=offline. The google-ads-python generate_user_credentials.py helper or the OAuth Playground both work. Vault

Watch the API version field. It defaults to v25. Major versions live roughly twelve months, and Google keeps about five of them alive at a time — the field exists so you can move forward without a deploy.

What SaaS Pro Max sends

POST https://googleads.googleapis.com/v25/customers/1234567890:uploadClickConversions
Authorization: Bearer <access token>
developer-token: <developer token>
login-customer-id: <manager customer id, when set>
Content-Type: application/json

{
  "customerId": "1234567890",
  "conversions": [{
    "gclid": "…",                               // or wbraid, or gbraid
    "conversionAction": "customers/1234567890/conversionActions/555",
    "conversionDateTime": "2026-08-15 12:34:56+00:00",
    "conversionEnvironment": "WEB",
    "conversionValue": 49,
    "currencyCode": "USD",
    "orderId": "…",
    "userIdentifiers": [
      { "hashedEmail": "<sha256>", "userIdentifierSource": "FIRST_PARTY" },
      { "hashedPhoneNumber": "<sha256>", "userIdentifierSource": "FIRST_PARTY" }
    ]
  }],
  "partialFailure": true,
  "validateOnly": false
}

Notes that matter:

  • The access token is fetched with the refresh token and cached in memory until a minute before it expires, so a burst of conversions costs one token exchange, not one per event.
  • conversionDateTime is yyyy-mm-dd hh:mm:ss with a mandatory UTC offset, a space rather than a T. SaaS Pro Max emits UTC (+00:00), which is unambiguous regardless of your account's time zone.
  • The email is normalised Google's way before hashing: trimmed, lowercased, and for gmail.com and googlemail.com only, dots removed from the local part and everything from a + dropped. Jane.Doe+Shopping@Gmail.com hashes as janedoe@gmail.com. Meta, TikTok and LinkedIn do not do this, so their hashes of the same address legitimately differ.
  • The phone is E.164 with the leading +.
  • The upload service accepts only hashedEmail and hashedPhoneNumber, at most five identifiers per conversion. addressInfo is rejected here (it is a Customer Match field).
  • partialFailure is always true, as Google documents. Row-level problems come back in partialFailureError rather than rejecting the request.
  • When there is no click identifier but there is a hashed identifier, the conversion is still uploaded — that is enhanced conversions for leads. With neither, nothing is sent.

Mapping

The "Conversion action" column of a mapping rule takes either the numeric conversion action ID or a full customers/…/conversionActions/… resource name. A rule with neither, and no default configured, is an error rather than a guess.

Verifying

  1. Press Send test event. SaaS Pro Max sends the upload with validateOnly: true, so Google checks the payload without recording a conversion. A green result means the credentials, customer ID, conversion action and payload are all valid.
  2. For a real conversion: Tools → Conversions → your action → Recent conversions. Uploads can take up to 3 hours to appear, and up to 24 hours to be attributed.
  3. Enhanced conversions diagnostics live under Tools → Conversions → Diagnostics.

Limitations

  • conversionDateTime must be after the click. A conversion uploaded for a click that Google has no record of is accepted and then silently unattributed.
  • One conversion per request.
  • userIpAddress and session attributes are allowlist-only and are not sent.

Mode ga4

What to collect

Value Where to find it Stored as
Measurement ID GA4 → Admin → Data streams → your web stream. Format G-XXXXXXX. Config
API secret The same data stream page → Measurement Protocol API secrets → Create. Vault

What SaaS Pro Max sends

POST https://www.google-analytics.com/mp/collect?measurement_id=G-…&api_secret=…
Content-Type: application/json

{
  "client_id": "1234567890.987654321",
  "user_id": "<distinct id, when identified>",
  "timestamp_micros": 1786…,
  "events": [{
    "name": "purchase",
    "params": {
      "session_id": "…", "engagement_time_msec": 1,
      "value": 49, "currency": "USD", "transaction_id": "…",
      "page_location": "…", "page_referrer": "…", "page_title": "…",
      "items": [{ "item_id": "SKU-1", "item_name": "SKU-1", "quantity": 2, "price": 20 }]
    }
  }]
}
  • GA4 requires client_id to be "two positive numbers joined by a period". SaaS Pro Max uses properties.$ga_client_id when your app supplies one (copy it from the _ga cookie for the best joining), and otherwise derives a stable pair of numbers from the anonymous id. A derived id will not join to the same user as your on-page GA4 tag.
  • session_id and engagement_time_msec are always sent; without them GA4 under-reports sessions and engagement.
  • Event names are sanitised into GA4's grammar: an alphabetic first character, then word characters, at most 40. $pageview becomes pageview.

Verifying

Press Send test event. The adapter posts to https://www.google-analytics.com/debug/mp/collect, which validates without recording and returns validationMessages. An empty list is a pass, and SaaS Pro Max reports any message it gets back.

This matters more than it sounds: the live endpoint returns 2xx with an empty body even for a malformed payload, so the debug endpoint is the only feedback loop GA4 offers.

Limitations

  • Backdating is capped at 72 hours; older events are clamped or rejected.
  • 25 events per request, 25 parameters per event.
  • No response body means a live send can only be confirmed in GA4's Realtime report, not by the adapter.