Zernio
APIFor you
Zernio
APIFor you
Platforms

Google Ads

Overview

Build

Create adsPerformance MaxDemand Gen

Target

TargetingKeywordsCustomer Match

Measure

Insights & GAQLConversionsGoogle TagURL tracking tags

Operate

Manage adsRecommendationsAccounts & labels

Reference

Limits and errors
Dashboard
OverviewPlatformsAPI ReferenceResources
Google Ads

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, gbraid or wbraid from 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.

TypeScript
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):

JSON
{
  "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.

TypeScript
const { data: destinations } = await zernio.conversions.listConversionDestinations({
  path: { accountId: '66b2e19d8c3f5a7e9d0b1c2d' }
});

console.log(destinations.destinations);

Response (200):

JSON
{
  "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, Lead or Schedule
  • a Google ConversionActionCategory value such as PURCHASE, SUBSCRIBE_PAID, SIGNUP, IMPORTED_LEAD or BOOK_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) or ONE_PER_CLICK.
  • primaryForGoal: default false, so the action is record-only until you opt it into Smart Bidding.
Terminal
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):

JSON
{
  "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:

SwitchWhereEndpoint
biddable on an account goalAccount default, followed by every campaign at goalConfigLevel: CUSTOMERGET and PATCH /v1/ads/conversions/goals
primaryForGoal on an actionPrimary 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 goalOne campaign, at goalConfigLevel: CAMPAIGNGET 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:

Terminal
curl "https://zernio.com/api/v1/ads/conversions/goals?accountId=66b2e19d8c3f5a7e9d0b1c2d&adAccountId=1234567890" \
  -H "Authorization: Bearer $ZERNIO_API_KEY"

Response (200):

JSON
{
  "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.

Terminal
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:

Terminal
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-goals creates one from name and conversionActionIds (1 to 100 numeric ids), and returns its id.
  • GET lists them.
  • PATCH .../custom-goals/{goalId} renames it or replaces its whole action set.
  • DELETE removes 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.

  • CUSTOMER means the campaign follows the account goals.
  • CAMPAIGN means 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:

Terminal
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.

Terminal
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):

JSON
{ "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

ErrorCauseFix
200 with eventsFailed above 0Google rejected some events, such as CONSENT_REQUIRED for EEA usersFix the events failures[] names and resend only those
409 idempotency_conflict on a conversion action createThe name exists with a different categoryPick another name, or reuse the existing action
405 on adjustmentsThe account is Meta or LinkedIn, and adjustments are Google-onlySend adjustments for Google accounts only

failures[] names each rejected event by eventIndex and eventId:

JSON
{
  "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.
Was this page helpful?

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.

On this page

Before you startSend a conversion eventEEA and UK consentDeduplicationFind a conversion actionCreate a conversion action for uploadsWebsite tag actionsConversion goalsAccount goalsPrimary and secondary actionsCustom goalsCampaign goalsAdjust an uploaded conversionIf it failsRelated