Google is retiring first-party data upload through the Google Ads API on June 15, 2026. The UploadClickConversions, UploadConversionAdjustments, and CustomerMatchUserList paths that have been standard for years will stop working. Not with deprecation warnings. With hard errors.
The replacement is the Data Manager API. It is not a drop-in replacement. The endpoint, payload shape, OAuth scope, identifier format, and even the concept of a destination ID are all different. Google's sunset announcement covers the what and the when. It does not cover the how, and the official documentation will not save you from the edge cases.
I migrated a live production pipeline last month — two brands, two countries, daily offline conversions, weekly Customer Match syncs — and hit every wall. This post is the practical guide I wish had existed. Not a checklist written by someone who read the docs but never sent a row.
What is actually happening
If you are running offline conversion uploads, Customer Match syncs, or Store Sales feeds through the Ads API today, you have until June 15, 2026 to move. After that, your existing code stops working.
The new path is a completely separate service. Same data. Same business logic. Different everything else.
What stayed the same
The data requirements are unchanged. You still need a GCLID or click timestamp. You still need conversion action IDs. You still need SHA-256 hashed emails and phones for Customer Match. You still batch rows, handle partial failures, and retry on 500s. The business logic is identical. Only the delivery mechanism changed.
What changed
The endpoint. Old: googleads.googleapis.com/v16/customers/{id}:uploadClickConversions. New: datamanager.googleapis.com/v1/events:ingest. Different hostname, version, resource naming, verb structure — all of it.
The OAuth scope. You need https://www.googleapis.com/auth/datamanager in addition to existing Google Ads scopes. If your token only has adwords scope, Data Manager returns a 403 that looks like authentication failure but is actually a scope error. I lost two hours to this. The fix is five minutes. The docs do not make the scope requirement obvious.
The payload shape. The old path used a protobuf-style JSON body with conversionActions, gclid, conversionDateTime, and conversionValue. The new path uses a flat array of events under events[], each with event_name, event_time, user_identifiers[], and attribution_token or attribution_token_timestamp. Click-based conversions send the GCLID as an attribution token. Impression-based sends the gclid_date_time_pair. The field names are different enough that find-and-replace will not work.
The identifier format. For Customer Match, the old path accepted { hashedEmail: "..." }. The new path uses { emailAddress: "..." } — still SHA-256 hex, but the key name changed. Same for phone: { phoneNumber: "..." } instead of { hashedPhoneNumber: "..." }. Send the old keys and the request validates, but the identifiers are silently ignored. Your audience stops growing. You get HTTP 200 and zero members added. I watched a 5,000-member upload do exactly this. The response metadata tells you why, but only if you know to look.
The destination ID. Old path: resource name customers/{id}/userLists/{id}. New path: numeric productDestinationId. Not a resource name. A plain integer. Pass a resource name and the API accepts it and does nothing. No error. Just silence.
Transaction ID sanitisation. Data Manager is stricter than the old path. It rejects @, spaces, and most special characters. Safe set: [A-Za-z0-9._:+|-]. If your transaction IDs contain email addresses or UUIDs, sanitise before sending. The old path was lenient. The new path is not.
The migration in practice
Step one: enable the API. In Google Cloud Console, enable Data Manager API on the project that owns your OAuth credentials. This is not automatic. It is not enabled by default when you enable Google Ads API.
Step two: add the scope. Regenerate your OAuth token with the datamanager scope. Service accounts need the scope in the token minting code. Refresh token flows need a re-run of the consent flow with the new scope included. Users will see a new permission screen.
Step three: rewrite the payload builder. This is the bulk of the work. Old shape:
{
"conversions": [{
"gclid": "...",
"conversionDateTime": "...",
"conversionAction": "customers/.../conversionActions/...",
"conversionValue": 100,
"currencyCode": "INR"
}]
}
New shape:
{
"events": [{
"event_name": "purchase",
"event_time": "2026-05-15T10:00:00Z",
"user_identifiers": [{"identifier": {"gclid": "..."}}],
"attribution_token": "...",
"attribution_token_timestamp": "...",
"conversion_metadata": {
"conversion_action_id": "...",
"conversion_value": 100,
"currency_code": "INR"
}
}]
}
Same information. Completely different envelope.
Step four: handle the response format. The old path returned GoogleAdsRow objects with inline partial failures. The new path returns a top-level results array and a separate errors array. Retry logic needs rewriting.
Step five: test with a single row. Send one conversion. One Customer Match member. Wait twenty minutes. Check the UI. Confirm the conversion shows up and the audience grows by one. Then scale: ten, a hundred, then production.
The gotcha that cost two hours
Data Manager requires a customer_id in request metadata. Not the MCC ID. The actual customer ID of the account that owns the conversion action or audience. The old path used MCC ID as login-customer-id header with child account ID in the resource name. Data Manager wants the child account ID in a dedicated customer_id metadata field. Send the MCC ID and you get a 404 saying the conversion action does not exist. It exists. You are looking in the wrong account.
The other gotcha that cost an hour
Phone number hashing changed. The old path accepted SHA-256 of digits only. Data Manager wants E.164 format first, then SHA-256. +919994276557 → SHA-256, not 9994276557 → SHA-256. If your pipeline strips country codes before hashing, phone matches drop to zero. Add a country code lookup step.
What this means for Customer Match
The Data Manager path is audienceMembers:ingest. Batch limit is 5,000 members per request, same as before. Membership duration defaults to 540 days. Reference audiences by numeric productDestinationId, not resource name. Identifier keys are emailAddress and phoneNumber.
One operational difference: the old path made members targetable immediately. Data Manager has a processing delay. In production testing, new members are targetable within four to six hours. Plan for the lag if you have same-day activation use cases.
Should you migrate now
Yes. June 15, 2026 is not far. This is not a find-and-replace. It is a rewrite of your upload pipeline. If you run daily conversion uploads or weekly Customer Match syncs, you need to be on Data Manager well before June to catch the edge cases.
If you are not running first-party uploads today, enable the API and scope now anyway. It takes five minutes and you will be ready when your attribution strategy needs it.
What I would do differently
Test with a single Customer Match member and a single conversion on day one. Do not assume the payload shape is similar enough to batch-test immediately. Enable the Data Manager API in Cloud Console before writing any code — the 403 scope error will send you down a rabbit hole of checking service account permissions that are fine. And read the identifier key names carefully. hashedEmail versus emailAddress is a one-word difference that silently breaks everything.
The Data Manager API is the future of Google Ads first-party data. It is better designed than the old path. It is also different enough that you cannot wing the migration. Start now. Test small. Read the field names twice.