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

Insights & GAQL

Read rolled-up Google Ads metrics from the campaign tree, and run any read-only GAQL query with GET /v1/ads/insights.


By the end of this page, you can read spend, CPC and CPM per campaign, ad group and ad from Zernio's synced metrics.

Before you start

You need a googleads account.

Read ad analytics

Pick the read by what you need. You can also run any read-only GAQL SELECT against Google with GET /v1/ads/insights.

You wantUseNotes
A dashboard: spend, CPC, CPM per campaign, ad group or adGET /v1/ads/tree, GET /v1/ads, GET /v1/ads/{adId}Served from Zernio's synced metrics, no Google call on the request path.
One dimension split outGET /v1/ads/{adId}/analytics, GET /v1/ads/campaigns/{campaignId}/analyticsThe cross-platform breakdowns endpoint.
The search terms that triggered your adsGET /v1/ads/search-termsGoogle's search_term_view, ordered by cost.
Anything Google's reporting can answerGET /v1/ads/insightsRaw GAQL passthrough.

Rolled-up metrics from the campaign tree

Call GET /v1/ads/tree with accountId for spend, CPC and CPM at campaign, ad group and ad level in one read.

fromDate and toDate set the metrics window (the last 90 days by default, 730 days at most).

platform=google scopes a profile that also has Meta or LinkedIn ads.

curl "https://zernio.com/api/v1/ads/tree?accountId=66b2e19d8c3f5a7e9d0b1c2d&platform=google&fromDate=2027-01-01&toDate=2027-01-31" \
  -H "Authorization: Bearer $ZERNIO_API_KEY"

Response (200), trimmed to one campaign:

{
  "campaigns": [
    {
      "platformCampaignId": "21874563210",
      "campaignName": "US DevOps Search",
      "platform": "google",
      "status": "active",
      "advertisingChannelType": "SEARCH",
      "currency": "USD",
      "adSetCount": 1,
      "adCount": 1,
      "metrics": { "spend": 412.5, "impressions": 18400, "clicks": 512, "ctr": 2.78, "cpc": 0.81, "cpm": 22.42, "conversions": 12.4 },
      "adSets": [
        {
          "platformAdSetId": "165489732105",
          "adSetName": "US DevOps Search",
          "status": "active",
          "adCount": 1,
          "metrics": { "spend": 412.5, "impressions": 18400, "clicks": 512, "ctr": 2.78, "cpc": 0.81, "cpm": 22.42, "conversions": 12.4 },
          "ads": [
            { "_id": "66f0a1b2c3d4e5f6a7b8c9d0", "name": "US DevOps Search", "status": "active", "metrics": { "spend": 412.5, "impressions": 18400, "clicks": 512 } }
          ]
        }
      ]
    }
  ],
  "pagination": { "page": 1, "limit": 20, "total": 1, "pages": 1 }
}
  • platformAdSetId is the Google ad group.
  • advertisingChannelType is Google's own serving surface (SEARCH, DISPLAY, PERFORMANCE_MAX, SHOPPING, VIDEO, ...).
  • currency is the ad account's own, never normalised to USD.
  • conversions is fractional because Google reports modeled and attributed conversions.

Backfill. A 202 carrying backfillPending: true means part of the range is still being backfilled. Retry until the call answers 200.

Discovered campaigns of every type sync into the tree with metrics, including Performance Max, Shopping and Video.

Deletes. Deleting an ad or a campaign is a soft delete. The objects stay in the tree as cancelled, and their historical spend still counts toward any date range they fall in.

Campaign budgets and impression share

GET /v1/ads/campaigns includes Google budget metadata in budget and campaignBudget after the next successful sync. It does not fetch Google live.

GET /v1/ads/campaigns/{campaignId}/analytics returns the latest synced budget as campaign.budget, which is null before sync.

Budget fieldMeaning
amountMicrosExact decimal micros as a string. Daily budgets use Google's amount_micros; custom-period budgets use total_amount_micros.
explicitlySharedWhether the budget is shared, or null when unknown. Shared writes require allowSharedBudgetUpdate: true; unknown sharing status cannot be overridden.
resourceNameGoogle's campaign_budget.resource_name, or null when unavailable.
deliveryMethodGoogle's delivery method, typically STANDARD, or null when unavailable.

For Google campaigns, the campaign analytics endpoint also adds these fields under analytics.summary for the requested inclusive date range:

  • searchImpressionShare
  • searchBudgetLostImpressionShare
  • searchRankLostImpressionShare
  • searchTopImpressionShare
  • searchAbsoluteTopImpressionShare

These are ratios, not percentages: 0.42 represents 42%.

Unavailable values are null, and Google's threshold sentinel values are preserved.

The fields come from one query over the whole range, without daily segmentation, and are cached for 10 minutes. They are not daily values to sum from analytics.daily.

Read freshness per response

analytics.impressionShareCache.cachedAt and analytics.impressionShareCache.stale describe the impression-share query, independently of the synced spend, clicks and other metrics.

Keep that freshness separate from the rest of the campaign report.

Google Search ad details on GET /v1/ads/{adId} use top-level cachedAt and stale for the current RSA text, pins and URLs. Google mutations invalidate that read.

Enrichment requires a stored advertisingChannelType: "SEARCH". Other or unknown channels return stored details.

Enrichment failure. The endpoint returns the stored ad with 200 and no cache metadata. A successful status alone does not confirm a fresh Google read.

Raw GAQL queries

Call GET /v1/ads/insights with a googleads account and query. It runs any read-only GAQL SELECT and returns Google's rows verbatim.

import Zernio from '@zernio/node';

const zernio = new Zernio();

const { data: report } = await zernio.adinsights.queryAdInsights({
  query: {
    accountId: '66b2e19d8c3f5a7e9d0b1c2d',
    query: `SELECT campaign.name, metrics.clicks, metrics.cost_micros, segments.date
            FROM campaign
            WHERE segments.date DURING LAST_30_DAYS
            ORDER BY metrics.cost_micros DESC`
  }
});

console.log(report.data);

Response (200), camelCase rows verbatim:

{
  "customerId": "1234567890",
  "fieldMask": "campaign.name,metrics.clicks,metrics.costMicros,segments.date",
  "data": [
    {
      "campaign": { "name": "US DevOps Search" },
      "metrics": { "clicks": "512", "costMicros": "412500000" },
      "segments": { "date": "2027-01-31" }
    }
  ],
  "paging": { "nextPageToken": null }
}

paging.nextPageToken is null above because one page held every row.

Google caps a page at 10,000 rows, so a report that can exceed it has to follow the cursor. Pass the token back as pageToken and repeat until it comes back null.

const gaql = `SELECT campaign.name, metrics.clicks, metrics.cost_micros, segments.date
              FROM campaign
              WHERE segments.date DURING LAST_30_DAYS
              ORDER BY metrics.cost_micros DESC`;

const rows = [];
let pageToken;

do {
  const { data: page } = await zernio.adinsights.queryAdInsights({
    query: { accountId: '66b2e19d8c3f5a7e9d0b1c2d', query: gaql, pageToken }
  });
  rows.push(...page.data);
  pageToken = page.paging.nextPageToken ?? undefined;
} while (pageToken);

This is the same endpoint that serves Meta's flexible insights. The account's platform picks the contract.

For Meta you pass objectId plus fields. For Google you pass query.

You can query campaign, keyword, search-term, geo, demographic, asset and shopping resources, change_event, and any segments.*.

That covers the reports the synced metrics do not model: search terms, quality score, auction-insight adjacents, per-segment splits.

RuleDetail
Read-onlySELECT statements only.
PagingFixed at 10,000 rows per page; follow paging.nextPageToken with pageToken.
customerIdOnly needed when the connection has several Google Ads accounts.
ValidationGoogle's, verbatim: an invalid query returns a 400 carrying Google's message.
NumbersCounters are int64s encoded as strings; monetary fields are micros of the account currency.

Selecting segments.date requires a finite date filter (DURING LAST_30_DAYS, an explicit BETWEEN, ...). Google rejects an unbounded date-segmented query, and its message says so verbatim.

GAQL queries run live against Google and pass the per-user burst limit. See quotas.

Legacy date fields

Queries run against Google Ads API v25, so write GAQL against the v25 field reference.

Google removed campaign.start_date and campaign.end_date in v23. Zernio still accepts them for backward compatibility, by rewriting them to campaign.start_date_time and campaign.end_date_time.

The rows still carry campaign.startDate and campaign.endDate as YYYY-MM-DD.

In WHERE, comparisons with =, <, <=, >, >=, BETWEEN and IS [NOT] NULL against a 'YYYY-MM-DD' literal are translated. Any other form returns Google's 400.

New code should select the _date_time fields directly.

Search terms

Call GET /v1/ads/search-terms with accountId for the search queries that triggered your ads, with matched-keyword status and spend.

They are the raw material for wasted-spend analysis and negative-keyword lists.

It defaults to the last 30 days and orders rows by cost, descending. campaignId and adGroupId narrow it.

curl "https://zernio.com/api/v1/ads/search-terms?accountId=66b2e19d8c3f5a7e9d0b1c2d&fromDate=2027-01-01&toDate=2027-01-31" \
  -H "Authorization: Bearer $ZERNIO_API_KEY"

Response (200):

{
  "customerId": "1234567890",
  "data": [
    {
      "searchTerm": "internal developer platform pricing",
      "status": "NONE",
      "matchType": "NEAR_PHRASE",
      "campaignId": "21874563210",
      "campaignName": "US DevOps Search",
      "adGroupId": "165489732105",
      "adGroupName": "US DevOps Search",
      "impressions": 340,
      "clicks": 21,
      "costMicros": 18400000,
      "conversions": 1,
      "conversionsValue": 129.99
    }
  ],
  "paging": { "nextPageToken": null },
  "cachedAt": "2027-01-31T08:00:00.000Z",
  "stale": false
}

status says whether the term is already a keyword or a negative (ADDED, EXCLUDED, ADDED_EXCLUDED, NONE).

stale: true means Google throttled the request and this is the last successful fetch, not a live read.

If it fails

ErrorCauseFix
400 invalid_field_value on queryGoogle's query validator rejected the GAQL, for example a segments.date with no finite date rangeFix the field the message names, such as adding a finite date filter, and repeat the query
stale: true on search termsGoogle throttled the request, so this is the last successful fetchRead again later for a live result

The 400 carries Google's validator message verbatim:

{
  "error": "Error in query: A date segment (segments.date) must be filtered with a finite date range.",
  "type": "invalid_request_error",
  "code": "invalid_field_value",
  "param": "query",
  "platform": "google"
}

Add a finite date filter and repeat the query.

Related

  • Keywords: synced keyword criteria and the Keyword Planner.
  • Limits and errors: the per-user burst limit and Google's quota.
  • Query ad insights, Search terms and Get campaign tree: every parameter.
Was this page helpful?

Customer Match

Create a Google Customer Match audience from a CRM email list with POST /v1/ads/audiences and a hashed member upload.

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.

On this page

Before you startRead ad analyticsRolled-up metrics from the campaign treeCampaign budgets and impression shareRead freshness per responseRaw GAQL queriesLegacy date fieldsSearch termsIf it failsRelated