In 2026 you upload offline click conversions to Google Ads with the Data Manager API: a single POST to datamanager.googleapis.com/v1/events:ingest, authenticated by an OAuth token, no developer token needed at runtime. The old path most guides still describe, ConversionUploadService.UploadClickConversions, is now allowlist-gated and will reject a fresh integration outright. If you are wiring up offline conversions for the first time this year, the classic tutorial you found is a dead end, and this is the route that actually works.
I moved a live account onto this exact path recently: a multi-location US medical practice running Google Ads for phone calls and online bookings, where the real conversion happens days after the click, inside the practice management system, not on the website. That gap is what offline conversion upload closes. Below is the working setup, the two traps that waste the most time, and how to keep the whole thing HIPAA-conscious.
Quick answer
For a new integration in 2026, use the Data Manager API. The classic service is only available if your developer token already has upload history.
| ConversionUploadService (classic Ads API) | Data Manager API (events:ingest) | |
|---|---|---|
| Endpoint | UploadClickConversions RPC / REST | POST datamanager.googleapis.com/v1/events:ingest |
| Status for new tokens | Allowlist-gated since 15 Jun 2026 | Open, sanctioned path |
| Rejects new tokens with | CUSTOMER_NOT_ALLOWLISTED_FOR_THIS_FEATURE | n/a |
| Developer token at runtime | Required | Not required |
| Auth | OAuth + developer token | OAuth with datamanager scope |
| Target of the upload | Conversion action (UPLOAD_CLICKS) | productDestinationId = that action's ID |
| "Success" response means | Accepted for processing | Accepted for processing |
The single most important thing on that table: "success" does not mean "attributed" on either path. More on that below, because it is the mistake I see most.
Why is UploadClickConversions blocked in 2026?
Google gated offline click uploads behind an allowlist. Since 15 June 2026, a call to ConversionUploadService.UploadClickConversions fails with CUSTOMER_NOT_ALLOWLISTED_FOR_THIS_FEATURE whenever the developer token has no prior offline-conversion history. Tokens that were already uploading before that date keep working, so if you inherited an old integration it may look fine. Stand up a brand-new one on the same RPC and it dies on the first request.
This is the part that trips people up, because the classic method is still fully documented and every older blog post points at it. The documentation being live is not the same as the feature being available to your token. There is no self-serve button to request the allowlist for a new build either. The intended answer from Google is: do not use the RPC, use the Data Manager API. So that is what a 2026 integration does.
What is the Google Ads Data Manager API?
The Data Manager API is Google's consolidated ingestion endpoint for first-party and offline event data. Instead of a Google Ads-specific RPC, you POST a batch of events to one URL and name where they should land. For offline click conversions, "where" is a conversion action inside a Google Ads account.
Two things make it easier to operate than the old service:
- No developer token at runtime. The ingest call needs only an OAuth access token carrying the
datamanagerscope. You are not threading a developer token, a login-customer-id header, and a manager-account approval through every request. That removes a whole class of setup friction, especially for a small team without a standing MCC relationship. - The destination is explicit in the body. Every event batch says exactly which account and which conversion action it feeds, so one credential can route events to several destinations without per-account client objects.
You still need the classic Ads API for one job: reading the numbers back to verify them. That read needs a developer token, but reading conversion metrics does not require the offline-upload allowlist, so a basic-access token is enough. Write with Data Manager, verify with the Ads API.
How do I upload one offline conversion?
Here is the whole flow for a single event. In production you batch many events per request, but the shape is identical.
- Create (or find) the conversion action. In Google Ads, create a conversion action of type UPLOAD_CLICKS, which the UI labels "Website - Import from clicks". Note its numeric ID. That ID is your
productDestinationId. - Get an OAuth access token with the
datamanagerscope. Standard Google OAuth: a service account with domain-wide delegation, or a refresh token from an authorized user. The token is the only credential the ingest call needs. - Capture the gclid at click time. The
gclidlands on your landing page as a URL parameter. Store it against the lead the moment the form or booking is created, because you will need it later when the conversion actually happens offline. - Fire the value when the real conversion occurs. When the appointment is booked, kept, or paid, look up the stored gclid, attach the value, and send the event.
POSTtoevents:ingest. One HTTP call, shown below.
POST https://datamanager.googleapis.com/v1/events:ingest
Authorization: Bearer ya29.<oauth-access-token>
Content-Type: application/json{
"destinations": [
{
"operatingAccount": {
"accountType": "GOOGLE_ADS",
"accountId": "1234567890"
},
"productDestinationId": "987654321"
}
],
"events": [
{
"transactionId": "bk_8f3c1a94_2026-08-01",
"eventTimestamp": "2026-08-01T14:32:07Z",
"eventSource": "WEB",
"currency": "USD",
"conversionValue": 180.00,
"adIdentifiers": {
"gclid": "Cj0KCQjw...redacted"
}
}
]
}A few fields carry more weight than they look:
| Field | Why it matters |
|---|---|
productDestinationId | The UPLOAD_CLICKS conversion action ID. Point it at the wrong action and the event lands somewhere you are not looking. |
adIdentifiers.gclid | The only link back to the ad click. No gclid, no attribution. Store it early. |
eventTimestamp | RFC3339 in UTC (the trailing Z). Must fall inside the action's click-through conversion window or it is ignored. |
transactionId | Your idempotency key. Send the same event twice with the same transactionId and Google dedupes it instead of double-counting. |
conversionValue + currency | Feeds value-based bidding. If you cannot know exact revenue, a stable per-type estimate still beats a flat "1 conversion". |
That transactionId is cheap insurance. Offline pipelines retry, cron jobs overlap, someone reruns yesterday's batch. A stable transaction id per real-world event means a retry is safe by construction.
Why does a 200 not mean the conversion was attributed?
This is the biggest trap, and it is entirely invisible if you only watch HTTP status codes. An HTTP 200 from events:ingest means "accepted for processing." It does not mean "attributed." There is no per-event flag in the response telling you a given gclid matched a click and registered against the action. The API takes your batch, says 200, and does the matching asynchronously on Google's side.
So the failure mode is a pipeline that returns 200 for weeks while zero conversions actually register, because the gclids are stale, the timestamps fall outside the window, or the events point at the wrong action. Every log line is green. Nothing is landing.
The only real confirmation is to read the conversion action's metrics back through the Ads API after 24 to 72 hours, and check that all_conversions is climbing. A GAQL query does it:
SELECT
conversion_action.name,
conversion_action.type,
conversion_action.status,
metrics.all_conversions,
metrics.all_conversions_value
FROM conversion_action
WHERE conversion_action.type = 'UPLOAD_CLICKS'
AND segments.date DURING LAST_7_DAYSIf all_conversions moves after your uploads, the loop is closed. If it stays flat while your ingest calls keep returning 200, you have a matching problem, not a transport problem, and the status code was never going to tell you. Build this read-back check into the same job that uploads. Getting that verification loop right is most of what solid Google Ads conversion tracking actually is: not the upload, but proving the upload landed.
The "Connect a data source" banner is a trap, not a to-do
Second trap, and it looks exactly like an unfinished setup begging to be completed. Open the UPLOAD_CLICKS conversion action you are feeding, and Google Ads shows a red "Connect a data source" banner and a "pending review" status. Every instinct says click the button and finish the job.
Do not click it. For an action fed through the Data Manager API, that banner is cosmetic and is expected to persist. The API itself is the data source. The UI just does not draw a tidy "connected via Data Manager API" state, so it keeps nagging. I have watched conversions register through the API, with all_conversions climbing on the read-back query, while that red banner sat there the whole time insisting nothing was connected.
Clicking "Connect" attaches a second, parallel data source to the same action. Now you have two feeds pointing at one conversion action, and depending on what you wire up, you can double-count every conversion. You went looking to fix a warning and created a real data-integrity bug. Leave the banner alone and trust the all_conversions number instead. The metric is ground truth; the banner is decoration.
How do I keep offline conversions HIPAA-conscious?
For a healthcare advertiser this is not optional, and click-based upload happens to make it clean. The only fields that leave your system for a click conversion are an anonymous gclid, a numeric value, and a timestamp. No name, no email, no diagnosis, no record. Google gets "click ABC was worth 180 dollars on this date" and nothing about who the person is.
On the engagement I mentioned, that principle drove the whole design. Only the anonymous click id, a value, and a timestamp ever leave the system. Where a phone number had to travel for call matching, it was hashed first, never sent raw. No patient data is persisted into the upload pipeline at any point: the gclid is stored against a lead, the value is computed from a generic service-type price (not from a bill), and the payload is assembled from those non-identifying pieces. The value itself was an estimate, because the point of value-based bidding is a stable relative signal, not accounting-grade revenue. A booked visit is worth some multiple of a no-show; often only roughly half of booked appointments convert to the higher-value outcome anyway, so a conservative per-type value keeps the signal honest without ever touching a patient record.
If you are building this for a clinic, the design rule is simple: decide what a conversion is worth from a lookup table, not from anything patient-specific, and let only the click id, value, and timestamp cross the boundary. That is the difference between a conversion feed you can defend and one that quietly ships protected data to an ad platform. A HIPAA-aware offline conversion tracking setup treats that boundary as the actual deliverable.
Least privilege, and whose account the token lives in
Two operational details decide whether this pipeline is safe, and whether it survives you leaving.
The ad API has one scope that covers both reading and writing. The same kind of token your dashboard uses to read spend can, technically, change campaigns. You cannot narrow that at the token level, only at the account-user level: create a separate user on the ad account with a read-only role and mint the read token under them, so the writing token lives only where writing is actually needed. A service account does not solve this, because access to an ad account is granted by an email invitation that has to be accepted in an inbox, and a service account has no inbox. The users are real people, and the token inherits the role of the one it was minted under.
And reissue the uploading token under the client's own account, not yours, in the middle of the project rather than at the end. If it is minted under your login, the day you are removed from the account the nightly upload stops with no error, conversions simply stop appearing, and nobody connects the two events. The rule that saves the handoff: everything that must keep running is owned by the client, and you are added as a guest, never the reverse.
One more habit that pays for itself: before you send real rows, run the batch through a validate-only pass if your client library exposes one. A dry run catches most request-shape mistakes before a single event reaches a live account.
What breaks, and how to catch it
Most of the pain is not the API. It is the plumbing around it. Here is the shortlist from real builds.
| Symptom | Likely cause | Fix |
|---|---|---|
CUSTOMER_NOT_ALLOWLISTED_FOR_THIS_FEATURE | Still calling the classic RPC on a new token | Switch to Data Manager events:ingest |
200s, but all_conversions stays flat | Stale gclid, timestamp outside the window, or wrong action | Check the read-back query, fix the match, not the transport |
| Conversions double-counted | "Connect a data source" was clicked | Detach the second source; the API is the source |
| Duplicate events on retries | No transactionId | Add a stable idempotency key per real-world event |
| Auth failures at runtime | Missing datamanager scope on the token | Re-mint the OAuth token with the right scope |
Build the verification read before the happy path, the same way you would add error handling before shipping any automation. If the loop that proves conversions landed does not exist, you do not have offline conversion tracking, you have a script that returns 200.
I am Alex Cheberko. I build this end to end, remote and worldwide including the US: gclid capture on the site, offline value computed at the real conversion moment, the ingest pipeline, and the read-back verification that proves it works, all with only anonymous data crossing the line. If your conversion value lives in a CRM or an EHR days after the click and your bidding is flying blind because of it, that is precisely the gap I close. Send me how your leads turn into revenue and I will tell you whether it is a two-week build or something smaller before you commit a dollar. The first week of any project is refundable, so if the shape is wrong, you find out fast.
Tags
Frequently asked questions
Can I still use ConversionUploadService to upload offline conversions in 2026?
Only if your developer token already has offline-conversion history. Since 15 June 2026, a token with no prior uploads gets CUSTOMER_NOT_ALLOWLISTED_FOR_THIS_FEATURE, so any new integration has to use the Google Ads Data Manager API instead.
Do I need a Google Ads developer token to call the Data Manager API?
No. The events:ingest endpoint authenticates with an OAuth token that carries the datamanager scope. You only need a developer token for the classic Ads API, which you still use later to read back and verify the conversions.
Does an HTTP 200 from events:ingest mean my conversion was attributed?
No. A 200 means Google accepted the event for processing, nothing more. There is no per-event attribution flag in the response, so you confirm attribution by reading the conversion action's all_conversions metric 24 to 72 hours later.
Why does my conversion action still say 'pending review' with a 'Connect a data source' banner?
For an action you feed through the Data Manager API, that banner is cosmetic and it stays. The API itself is the data source. Do not click Connect, because it attaches a second parallel source and can double-count.
What conversion action type do I upload offline clicks to?
An action of type UPLOAD_CLICKS, shown in the UI as 'Website - Import from clicks'. Its ID becomes the productDestinationId in the ingest request body.
Can I upload offline conversions without sending patient or customer data to Google?
Yes. For click conversions, only an anonymous gclid, a value, and a timestamp have to leave your system. No name, email, or record needs to be sent, which is how a HIPAA-conscious build stays clean.
Need something like this built?
Free 15-min discovery call. I'll listen, ask honest questions, and tell you if I can help.