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.
conversionDateTimeisyyyy-mm-dd hh:mm:sswith a mandatory UTC offset, a space rather than aT. 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.comandgooglemail.comonly, dots removed from the local part and everything from a+dropped.Jane.Doe+Shopping@Gmail.comhashes asjanedoe@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
hashedEmailandhashedPhoneNumber, at most five identifiers per conversion.addressInfois rejected here (it is a Customer Match field). partialFailureis alwaystrue, as Google documents. Row-level problems come back inpartialFailureErrorrather 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
- 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. - 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.
- Enhanced conversions diagnostics live under Tools → Conversions → Diagnostics.
Limitations
conversionDateTimemust 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.
userIpAddressand 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_idto be "two positive numbers joined by a period". SaaS Pro Max usesproperties.$ga_client_idwhen your app supplies one (copy it from the_gacookie 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_idandengagement_time_msecare 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.
$pageviewbecomespageview.
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.