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

Keywords

Read the Search keywords your Google campaigns run, add keywords to an ad group, and research new ones with the Keyword Planner.


By the end of this page, you can:

  • read the keywords your campaigns already run (synced, free to query)
  • add keywords to an ad group
  • research keywords you do not run yet (Keyword Planner, live against Google)

You need a googleads account.

Synced keyword criteria

Call GET /v1/ads/keywords. It returns the Google Search keyword criteria (positive and negative) synced from connected accounts, one row per ad-group keyword.

TypeScript
import Zernio from '@zernio/node';

const zernio = new Zernio();

const { data: synced } = await zernio.adcampaigns.listAdKeywords({
  query: {
    accountId: '66b2e19d8c3f5a7e9d0b1c2d',
    adAccountId: '1234567890',
    matchType: 'exact',
    negative: false,
    search: 'platform',
    limit: 100
  }
});

console.log(synced.keywords);

Response (200):

JSON
{
  "keywords": [
    {
      "id": "66f2b3c4d5e6f7a8b9c0d1e2",
      "platformCriterionId": "987654321",
      "resourceName": "customers/1234567890/adGroupCriteria/165489732105~987654321",
      "platform": "google",
      "adAccountId": "1234567890",
      "campaignId": "21874563210",
      "campaignName": "US DevOps Search",
      "adSetId": "165489732105",
      "adSetName": "US DevOps Search",
      "keyword": "internal developer platform",
      "matchType": "exact",
      "status": "active",
      "negative": false,
      "qualityScore": 7,
      "metrics": { "windowDays": 30, "clicks": 84, "impressions": 2210, "cost": 61.4, "conversions": 3, "firstPageCpc": 0.42, "firstPositionCpc": 1.9 }
    }
  ],
  "pagination": { "page": 1, "limit": 100, "total": 1, "pages": 1 }
}

Filters:

  • campaignId
  • adSetId (the Google ad group)
  • status
  • matchType
  • negative (true returns negative keywords only)
  • search (a case-insensitive substring on the keyword text)

metrics is a trailing 30-day window in the account currency.

qualityScore is Google's 1 to 10 score, null when unrated. quality carries the same score plus its three component ratings.

Each row carries two ids:

  • id is Zernio's keyword id.
  • platformCriterionId is Google's ad_group_criterion.criterion_id, which is unique only within its ad group.

resourceName is Google's full resource name ending in {adSetId}~{platformCriterionId}. That tail is the keyword id Google itself uses.

Match that tail against GAQL rows. It is also what labels take in keywordIds.

Freshness. The list refreshes about once a day per Google Ads customer. The keyword sweep rides the ads discovery pass on a slower slot, so keywords added on Google can take up to a day to appear here.

A newly connected account is populated on its first discovery pass, and a manual sync refreshes it immediately.

Campaign-level negative keywords are not included, only ad-group-level criteria. Read those with GET /v1/ads/campaigns/{campaignId}/negative-keywords.

For live, current-state keyword data, query GAQL directly. See Insights and GAQL.

Add keywords to an ad group

Call POST /v1/ads/keywords with accountId, the ad group as adSetId and keywords. It adds to the ad group without touching the keywords already there.

negative: true adds ad-group-level negatives instead.

Terminal
curl -X POST "https://zernio.com/api/v1/ads/keywords" \
  -H "Authorization: Bearer $ZERNIO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "accountId": "66b2e19d8c3f5a7e9d0b1c2d",
    "adSetId": "165489732105",
    "keywords": ["developer portal", { "text": "platform engineering", "matchType": "phrase" }]
  }'

Response (201), the same row shape as the list:

JSON
{
  "keywords": [
    { "id": "66f2b3c4d5e6f7a8b9c0d1e3", "adSetId": "165489732105", "keyword": "developer portal", "matchType": "broad", "status": "active", "negative": false },
    { "id": "66f2b3c4d5e6f7a8b9c0d1e4", "adSetId": "165489732105", "keyword": "platform engineering", "matchType": "phrase", "status": "active", "negative": false }
  ]
}

To replace the whole set, send targeting.keywords or targeting.negativeKeywords on PUT /v1/ads/{adId}.

Each supplied list is the full desired set for the ad group, affecting sibling ads too.

Matching uses case-insensitive text plus match type. Omitting matchType means broad match.

  • Unchanged criteria retain their IDs, status, bids, labels and history.
  • Missing criteria are removed.
  • Changing text or match type removes the old criterion and creates a new one.

Change or remove one keyword

Pause or re-enable a single keyword with PATCH /v1/ads/keywords/{keywordId} (Update keyword). status is active or paused.

keywordId takes either form:

  • the id from the list above
  • Google's native {adSetId}~{platformCriterionId}, the tail of resourceName (for example 165489732105~987654321)

A bare criterion id is rejected with a 400, because it is only unique within its ad group.

Nothing else in the ad group is touched:

Terminal
curl -X PATCH "https://zernio.com/api/v1/ads/keywords/66f2b3c4d5e6f7a8b9c0d1e2" \
  -H "Authorization: Bearer $ZERNIO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "status": "paused" }'

Response (200), the updated row:

JSON
{
  "keyword": {
    "id": "66f2b3c4d5e6f7a8b9c0d1e2",
    "adSetId": "165489732105",
    "keyword": "internal developer platform",
    "matchType": "exact",
    "status": "paused",
    "negative": false
  }
}

DELETE on the same path (Remove keyword) takes either id form. It removes the criterion from its ad group, positive or negative:

Terminal
curl -X DELETE "https://zernio.com/api/v1/ads/keywords/165489732105~987654321" \
  -H "Authorization: Bearer $ZERNIO_API_KEY"

Response (200):

JSON
{
  "removed": true,
  "keywordId": "66f2b3c4d5e6f7a8b9c0d1e2",
  "platformCriterionId": "987654321",
  "resourceName": "customers/1234567890/adGroupCriteria/165489732105~987654321",
  "adSetId": "165489732105",
  "campaignId": "21874563210"
}

Google gives negative keywords no status, so PATCH on one returns 422. Remove it instead.

A 404 means the keywordId is not a keyword on one of your connected accounts.

Campaign negatives and shared lists

Campaign negatives are separate from the synced ad-group criteria.

GET /v1/ads/campaigns/{campaignId}/negative-keywords reads them. PUT on the same path replaces the full desired keywords set, up to 1,000 entries.

Strings mean broad match. Objects accept text and matchType.

Send keywords: [] to clear the campaign's negatives.

For negatives reused across campaigns, manage a shared list:

  1. POST /v1/ads/accounts/negative-keyword-lists creates a list with accountId, name and optional keywords (up to 5,000). Include customerId when the connection has multiple customers. Creation does not attach the list to a campaign and is not idempotent.
  2. GET on that collection lists shared lists. GET /v1/ads/accounts/negative-keyword-lists/{listId} reads one with its keywords and criterion IDs; PUT on that path renames it.
  3. PUT /v1/ads/accounts/negative-keyword-lists/{listId}/keywords replaces the full desired set. Changes apply atomically and affect every attached campaign. Unchanged criteria retain their IDs.
  4. GET /v1/ads/campaigns/{campaignId}/negative-keyword-lists reads attachments. PUT on that path replaces the full listIds set, up to 20 lists from the same Google customer. An empty array detaches all lists without deleting their keywords.
  5. Detach a list from every campaign before DELETE /v1/ads/accounts/negative-keyword-lists/{listId}.

Shared-list reads are cached for 10 minutes. When Google throttles the request, they can fall back to the last successful copy for up to seven days.

Check cachedAt and stale before treating the result as current.

Keyword Planner: ideas

Call POST /v1/ads/keywords/ideas with seed keywords, a seed URL, or both. It runs Keyword Planner's idea generation.

TypeScript
const { data: ideas } = await zernio.adinsights.generateKeywordIdeas({
  body: {
    accountId: '66b2e19d8c3f5a7e9d0b1c2d',
    seedKeywords: ['platform engineering tools'],
    seedUrl: 'https://example.com/platform',
    countries: ['US', 'GB'],
    pageSize: 50
  }
});

console.log(ideas.data);

Response (200), rows in Google's raw shape:

JSON
{
  "customerId": "1234567890",
  "data": [
    {
      "text": "internal developer platform",
      "keywordIdeaMetrics": {
        "avgMonthlySearches": "2400",
        "competition": "MEDIUM",
        "competitionIndex": "48",
        "lowTopOfPageBidMicros": "1250000",
        "highTopOfPageBidMicros": "6800000",
        "monthlySearchVolumes": [ { "year": "2026", "month": "DECEMBER", "monthlySearches": "2900" } ]
      }
    }
  ],
  "paging": { "nextPageToken": "CAoQ..." }
}

Keyword Planner: historical metrics

Call POST /v1/ads/keywords/historical-metrics with exact keywords you already have (up to 1,000 per call).

It returns historical search volume, competition and top-of-page bid ranges, plus averageCpcMicros when includeAverageCpc is set.

JSON
{
  "accountId": "66b2e19d8c3f5a7e9d0b1c2d",
  "keywords": ["internal developer platform", "platform engineering tools"],
  "countries": ["US"],
  "includeAverageCpc": true
}

The response has the same customerId and data shape as ideas, with no paging. Historical metrics is a single batch.

Keyword Planner rules

Both Planner calls above share these.

They do not apply to GET /v1/ads/keywords, which returns Zernio's synced rows with numeric counters.

  • Rows come back verbatim from Google.
  • Counters are int64s encoded as strings. Parse them before doing math.
  • Every bid or CPC value is in micros of the account currency. Divide by 1,000,000.
  • countries omitted means worldwide. languageConstantId picks the language (1000 is English).
  • customerId is only needed when the connection has several Google Ads accounts.
  • Paging: pageSize plus pageToken from paging.nextPageToken (ideas only).

If it fails

The Keyword Planner calls and the keyword writes on this page run live against Google. They pass the per-user burst limit of 15 requests a minute (quotas).

  • A 429 with details.budgetScope: "account" or "user" clears within the minute.
  • A 429 with details.quotaExhausted: true is Google's quota. Wait until details.resetsAt.

Back off before retrying. See rate limits for the general rule.

Related

  • Create ads: keywords on a new Search campaign.
  • Insights and GAQL: live keyword and search-term data.
  • List Search keywords, Add Search keywords, Keyword ideas and Historical metrics: every field.
Was this page helpful?

Targeting

Target Google campaigns by location, language, age, gender, income, interests and Customer Match lists, look up Google's ids, and change a live campaign's locations, languages and devices.

Customer Match

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

On this page

Synced keyword criteriaAdd keywords to an ad groupChange or remove one keywordCampaign negatives and shared listsKeyword Planner: ideasKeyword Planner: historical metricsKeyword Planner rulesIf it failsRelated