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.
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):
{
"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:
campaignIdadSetId(the Google ad group)statusmatchTypenegative(truereturns 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:
idis Zernio's keyword id.platformCriterionIdis Google'sad_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.
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:
{
"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
idfrom the list above - Google's native
{adSetId}~{platformCriterionId}, the tail ofresourceName(for example165489732105~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:
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:
{
"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:
curl -X DELETE "https://zernio.com/api/v1/ads/keywords/165489732105~987654321" \
-H "Authorization: Bearer $ZERNIO_API_KEY"Response (200):
{
"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:
POST /v1/ads/accounts/negative-keyword-listscreates a list withaccountId,nameand optionalkeywords(up to 5,000). IncludecustomerIdwhen the connection has multiple customers. Creation does not attach the list to a campaign and is not idempotent.GETon that collection lists shared lists.GET /v1/ads/accounts/negative-keyword-lists/{listId}reads one with its keywords and criterion IDs;PUTon that path renames it.PUT /v1/ads/accounts/negative-keyword-lists/{listId}/keywordsreplaces the full desired set. Changes apply atomically and affect every attached campaign. Unchanged criteria retain their IDs.GET /v1/ads/campaigns/{campaignId}/negative-keyword-listsreads attachments.PUTon that path replaces the fulllistIdsset, up to 20 lists from the same Google customer. An empty array detaches all lists without deleting their keywords.- 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.
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:
{
"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.
{
"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.
countriesomitted means worldwide.languageConstantIdpicks the language (1000is English).customerIdis only needed when the connection has several Google Ads accounts.- Paging:
pageSizepluspageTokenfrompaging.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
429withdetails.budgetScope: "account"or"user"clears within the minute. - A
429withdetails.quotaExhausted: trueis Google's quota. Wait untildetails.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.
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.