Zernio
Zernio
PlatformsGoogle Ads

Build

Create Ads

Target

KeywordsCustomer Match

Measure

Insights & GAQLConversionsURL Tracking Tags

Operate

Limits and errors
Dashboard
llms.txtOpenAPI
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.


When you finish this page you can read spend, CPC and CPM per campaign, ad group and ad from Zernio's synced metrics, and run any read-only GAQL SELECT against Google with GET /v1/ads/insights. You need a googleads account.

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), and 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, ...), and currency is the ad account's own, never normalised to USD. conversions is fractional because Google reports modeled and attributed conversions. 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. 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. If enrichment fails, 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 count toward the per-user ops budget; see quotas.

Search terms

Call GET /v1/ads/search-terms with accountId for the search queries that triggered your ads, with matched-keyword status and spend, 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's daily quota was exhausted and this is the last successful fetch, not a live read.

If it fails

A 400 carries Google's query validator message verbatim, naming the offending field:

{
  "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 ops budget.
  • 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.

On this page

Rolled-up metrics from the campaign treeCampaign budgets and impression shareRead freshness per responseRaw GAQL queriesSearch termsIf it failsRelated