Conversions
Import offline and enhanced conversions into Google Ads through the Data Manager API with POST /v1/ads/conversions, and choose which conversions Google bids on with account, campaign and custom conversion goals.
By the end of this page, an offline conversion (deal closed, lead qualified, subscription renewed) is imported into Google Ads.
Before you start
You need a connected googleads account and a conversion action id. Find a conversion action lists the ones you have.
Send a conversion event
Call POST /v1/ads/conversions with accountId, the conversion action's resource name as destinationId and the events.
It goes through the Data Manager API ingestEvents method. As of December 2025 Google recommends it for all new integrations, replacing the legacy uploadClickConversions.
The same endpoint handles both attribution paths:
- Click attribution, when you include a
gclid,gbraidorwbraidfrom the originating ad click. - Enhanced Conversions for Leads, when you include a hashed email or phone because no click id is available.
PII is SHA-256 hashed server-side.
For @gmail.com and @googlemail.com addresses, Zernio strips dots and +suffix from the local part before hashing, per Google's spec. So john.doe+promo@gmail.com matches johndoe@gmail.com.
Zernio uses the Google Ads account you already connected. There is no additional OAuth and no developer token to apply for.
eventTime is unix seconds in UTC, not milliseconds: 1798848000 is 2027-01-02T00:00:00Z. A millisecond value lands the conversion thousands of years in the future, where Google drops it.
adjustmentTime on the adjustments call below uses the same unit.
import Zernio from '@zernio/node';
const zernio = new Zernio();
const { data: sent } = await zernio.conversions.sendConversions({
body: {
accountId: '66b2e19d8c3f5a7e9d0b1c2d',
destinationId: 'customers/1234567890/conversionActions/987654321',
consent: { adUserData: 'GRANTED', adPersonalization: 'GRANTED' },
events: [{
eventName: 'Purchase',
eventTime: 1798848000,
eventId: 'order_abc_123',
value: 129.99,
currency: 'USD',
user: {
email: 'customer@example.com',
phone: '+14155551234',
clickIds: { gclid: 'EAIaIQobChMI...' }
}
}]
}
});
console.log(sent.eventsReceived, sent.eventsFailed);Response (200):
{
"platform": "googleads",
"eventsReceived": 1,
"eventsFailed": 0,
"failures": [],
"traceId": "a1b2c3d4-..."
}traceId is Google's request id. Quote it if conversions do not appear as expected.
Conversions take up to 3 hours to appear in Google Ads reports.
Each request takes up to 2,000 events, and Zernio chunks larger batches automatically.
EEA and UK consent
Google enforces consent signaling for European users under the February 2026 Data Manager restrictions.
Pass consent at the request root with adUserData and adPersonalization set to GRANTED or DENIED.
Omit the field for non-EEA requests where you have no consent data. Google applies region defaults.
Deduplication
Pass a stable eventId on every event. Zernio maps it to Google's transactionId, so repeated uploads of the same conversion are deduped.
Find a conversion action
Call GET /v1/accounts/{accountId}/conversion-destinations. It returns every enabled conversion action across the accessible customers.
const { data: destinations } = await zernio.conversions.listConversionDestinations({
path: { accountId: '66b2e19d8c3f5a7e9d0b1c2d' }
});
console.log(destinations.destinations);Response (200):
{
"platform": "googleads",
"destinations": [
{ "id": "customers/1234567890/conversionActions/987654321", "name": "Offline purchase", "type": "PURCHASE", "status": "active" }
]
}id is the conversion action resource name, and type its category (PURCHASE, LEAD, SIGN_UP, ...).
Google locks the event type to the conversion action, not to the per-event eventName. When no listed action fits, create one for uploads.
Create a conversion action for uploads
When no existing action fits, call POST /v1/accounts/{accountId}/conversion-destinations with adAccountId, name and type. type takes either form:
- a unified name such as
Purchase,Subscribe,CompleteRegistration,LeadorSchedule - a Google
ConversionActionCategoryvalue such asPURCHASE,SUBSCRIBE_PAID,SIGNUP,IMPORTED_LEADorBOOK_APPOINTMENT
Google creates it with type=UPLOAD_CLICKS, which is immutable after creation.
Calling again with the same name reuses the existing action. The same name with a different category returns 409 with code idempotency_conflict.
Google-only fields:
countingType:MANY_PER_CLICK(the default) orONE_PER_CLICK.primaryForGoal: defaultfalse, so the action is record-only until you opt it into Smart Bidding.
curl -X POST "https://zernio.com/api/v1/accounts/66b2e19d8c3f5a7e9d0b1c2d/conversion-destinations" \
-H "Authorization: Bearer $ZERNIO_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "adAccountId": "1234567890", "name": "Offline purchase", "type": "Purchase" }'Response (201):
{
"platform": "googleads",
"destination": { "id": "customers/1234567890/conversionActions/987654321", "name": "Offline purchase", "type": "PURCHASE", "status": "active" }
}The id it returns is the destinationId for the upload above.
Website tag actions
Website conversion actions with their tag snippets (the gtag.js code a customer pastes onto their site) are a separate surface from the upload actions above.
GET /v1/ads/conversions/actions lists them, and POST /v1/ads/conversions/actions creates a WEBPAGE action (List conversion actions).
The list returns every action type by default (filter with type). Each action carries origin and primaryForGoal, which place it in the conversion goals below.
The same WEBPAGE actions are the events of the account's Google tag, which also installs them on Shopify and WordPress.
Conversion goals
Google bids on goals, not on individual actions.
A goal is a category and origin pair (for example PURCHASE from WEBSITE). Every conversion action with that category and origin belongs to it.
Three switches decide what a campaign optimizes for:
| Switch | Where | Endpoint |
|---|---|---|
biddable on an account goal | Account default, followed by every campaign at goalConfigLevel: CUSTOMER | GET and PATCH /v1/ads/conversions/goals |
primaryForGoal on an action | Primary actions count toward bidding and the Conversions column; secondary ones are observation-only (All conversions) | PATCH /v1/ads/conversions/actions/{actionId} |
| Campaign goals or a custom goal | One campaign, at goalConfigLevel: CAMPAIGN | GET and PATCH /v1/ads/campaigns/{campaignId}/conversion-goals |
All of these take accountId. Add adAccountId (the customer id) when the connection has several Google customers.
Reads are cached for 10 minutes. They fall back to the last successful copy with stale: true when Google's quota runs out.
Account goals
GET /v1/ads/conversions/goals lists the account-default goals, one per category and origin. Each has biddable and its conversion actions flagged primaryForGoal:
curl "https://zernio.com/api/v1/ads/conversions/goals?accountId=66b2e19d8c3f5a7e9d0b1c2d&adAccountId=1234567890" \
-H "Authorization: Bearer $ZERNIO_API_KEY"Response (200):
{
"customerId": "1234567890",
"goals": [
{
"category": "PURCHASE",
"origin": "WEBSITE",
"biddable": true,
"resourceName": "customers/1234567890/customerConversionGoals/PURCHASE~WEBSITE",
"conversionActions": [
{ "id": "987654321", "name": "Offline purchase", "status": "ENABLED", "primaryForGoal": true }
]
}
],
"cachedAt": "2027-01-31T08:00:00.000Z",
"stale": false
}PATCH on the same path sets biddable on up to 100 goals in one mutate, addressed by category and origin. It returns the re-read list.
Campaigns at goalConfigLevel: CUSTOMER follow the change.
curl -X PATCH "https://zernio.com/api/v1/ads/conversions/goals" \
-H "Authorization: Bearer $ZERNIO_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"accountId": "66b2e19d8c3f5a7e9d0b1c2d",
"adAccountId": "1234567890",
"goals": [{ "category": "SIGNUP", "origin": "WEBSITE", "biddable": false }]
}'Primary and secondary actions
Flip one action between primary and secondary with PATCH /v1/ads/conversions/actions/{actionId}. actionId is Google's numeric conversion action id:
curl -X PATCH "https://zernio.com/api/v1/ads/conversions/actions/987654321" \
-H "Authorization: Bearer $ZERNIO_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "accountId": "66b2e19d8c3f5a7e9d0b1c2d", "primaryForGoal": false }'Response (200): { "customerId": "1234567890", "action": { "id": "987654321", "primaryForGoal": false } }.
A primary action only drives bidding when its goal is biddable.
Actions you create for uploads start secondary unless you pass primaryForGoal: true.
Custom goals
A custom goal is a named set of conversion actions a campaign can bid on instead of a category.
POST /v1/ads/conversions/custom-goalscreates one fromnameandconversionActionIds(1 to 100 numeric ids), and returns itsid.GETlists them.PATCH .../custom-goals/{goalId}renames it or replaces its whole action set.DELETEremoves it.
Google refuses the delete (400) while any campaign still uses the goal, so switch those campaigns first.
Campaign goals
GET /v1/ads/campaigns/{campaignId}/conversion-goals reads one campaign's goals (biddable per category and origin), its goalConfigLevel and its customConversionGoalId.
CUSTOMERmeans the campaign follows the account goals.CAMPAIGNmeans it uses its own goals or the custom goal.
PATCH on the same path changes any of the three in one mutate and returns the re-read state:
curl -X PATCH "https://zernio.com/api/v1/ads/campaigns/21874563210/conversion-goals" \
-H "Authorization: Bearer $ZERNIO_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "goalConfigLevel": "CAMPAIGN", "customConversionGoalId": "7418529630" }'customConversionGoalId: null clears the custom goal.
Google refuses that (400) while the campaign stays at CAMPAIGN level with no biddable goals. Send goalConfigLevel: "CUSTOMER" in the same request to fall back to the account goals.
Adjust an uploaded conversion
Call POST /v1/ads/conversions/adjustments to change a conversion after the fact:
- retract it (refund, chargeback, churn)
- restate its value
- enhance it with first-party identifiers
Identify the original by orderId (the eventId you sent, required for ENHANCEMENT) or by gclid plus conversionTime.
RESTATEMENT takes the corrected total in restatementValue, not a delta.
curl -X POST "https://zernio.com/api/v1/ads/conversions/adjustments" \
-H "Authorization: Bearer $ZERNIO_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"accountId": "66b2e19d8c3f5a7e9d0b1c2d",
"destinationId": "customers/1234567890/conversionActions/987654321",
"adjustments": [{ "adjustmentType": "RETRACTION", "adjustmentTime": 1799107200, "orderId": "order_abc_123" }]
}'Response (200):
{ "platform": "googleads", "adjustmentsReceived": 1, "adjustmentsFailed": 0, "failures": [] }Adjustments go through the classic Google Ads API, so this endpoint is Google-only. Meta and LinkedIn return 405.
If it fails
| Error | Cause | Fix |
|---|---|---|
200 with eventsFailed above 0 | Google rejected some events, such as CONSENT_REQUIRED for EEA users | Fix the events failures[] names and resend only those |
409 idempotency_conflict on a conversion action create | The name exists with a different category | Pick another name, or reuse the existing action |
405 on adjustments | The account is Meta or LinkedIn, and adjustments are Google-only | Send adjustments for Google accounts only |
failures[] names each rejected event by eventIndex and eventId:
{
"platform": "googleads",
"eventsReceived": 0,
"eventsFailed": 1,
"failures": [
{ "eventIndex": 0, "eventId": "order_abc_123", "message": "Consent is required for EEA users", "code": "CONSENT_REQUIRED" }
],
"traceId": "a1b2c3d4-..."
}The status is 200 even on partial failure. Branch on eventsFailed, fix the named events and resend only those.
Related
- Google Ads tracking tags: click-URL parameters on the campaign.
- LinkedIn Conversions API and Meta Ads Conversions API: the same endpoint on the other networks.
- Send conversions and Adjust uploaded conversions: every field.
- List account conversion goals, Set a conversion action primary or secondary and Update campaign conversion goals: the goal endpoints.
Insights & GAQL
Read rolled-up Google Ads metrics from the campaign tree, and run any read-only GAQL query with GET /v1/ads/insights.
Google Tag
Read or switch on a Google Ads account's Google tag, manage its website conversion actions and auto-tagging, check conversion counts, and install it on Shopify or WordPress.