[ PAID MEDIA ]
/keyword-research
Google Keyword Planner via the Google Ads API.
ACME Agency, <id> and you@example.com mark values that are per-agency — your install fills them with YOUR clients and accounts. If a section references a helper script you don't have yet, it ships with that workflow's install.Keyword Research
What this does
Wraps Google's <id> (<id> and generateKeywordIdeas) so we can pull real Keyword Planner data without using the Ads UI. Same data you'd see in the Ads Manager UI, but scriptable.
Prerequisites — Standard Access
Status: APPROVED and working. The Keyword Planner endpoints require Google Ads API Standard Access, and our developer token now has it — generateKeywordIdeas / <id> return exact volumes, competition, and CPC ranges (confirmed 2026-06-16, ACME Agency run). No <id> errors.
Calling-account caveat still applies: search-volume accuracy depends on the calling account (--customer-id / the client's google_ads_id) having recent ad spend. Active spenders return exact numbers; truly dormant accounts can return bucketed ranges ("1K–10K"). Kaiser's account (Meta-only, low Google spend) still returned exact numbers, so most of our accounts qualify — but if you get bucketed ranges, pass --customer-id of a high-spend account in the MCC.
(Historical note: the token previously had Basic Access only; Standard Access was applied for via https://support.google.com/adspolicy/contact/new_token_application and has since been granted.)
Modes
1. Historical — validate existing keywords
Given a campaign's keyword list, pull monthly search volume + competition for each. Highlights:
- Zero-volume keywords (drop these — they kill quality score)
- Keywords intended as EXACT that should be PHRASE/BROAD (or vice versa)
- High-cost keywords that may need bid adjustment
node ACME Agency/scripts/keyword_research.mjs \
--client "ACME Agency" --geo AT \
--keywords-file "ACME Agency/clients/ACME Agency/at_keywords.json"
Accepted keyword-file shapes:
- Ad-group object (
{ ad_groups: [ { name, exact, phrase, broad } ] }) — preserves match-type intent - Flat array (
["kw1", "kw2"]) - Wrapped (
{ keywords: [...] })
2. Ideas — discover new keywords
Seed with keywords, a URL, or both:
# From seed keywords
node ACME Agency/scripts/keyword_research.mjs \
--client "ACME Agency" --geo DE \
--seeds "bioklimatische pergola,lamellendach,terrassenüberdachung"
# From a competitor URL
node ACME Agency/scripts/keyword_research.mjs \
--client "ACME Agency" --geo AT \
--url "https://www.competitor.at/pergolen"
# Both
node ACME Agency/scripts/keyword_research.mjs \
--client "ACME Agency" --geo AT \
--seeds "pergola" --url "https://anmeldung.<id>.de/"
Languages
Default behavior: local language for the geo + English. Rationale: many users in DE/AT/HR have their browser/account set to English even when searching in their local language, so excluding English under-counts demand.
Override:
--lang de,en— explicit code list--no-english— local only--lang-ids 1001,1000— pass raw Google language constant IDs
Built-in geo + language mapping (see ACME Agency/scripts/lib/keyword_planner.mjs):
- Geos: AT, DE, CH, HR, BA, RS, SI, ME, MK, IT, FR, ES, UK, US, CA, IE, NL, BE, PL, RO
- Languages: en, de, fr, es, it, pt, nl, ro, pl, hr, sr, sl, bg, cs, sk, hu, tr, el
For anything else, pass the numeric ID directly (e.g. --geo 2792 for Turkey).
Output
For each run:
- CSV at
ACME Agency/clients/<ClientFolder>/keyword_research/<mode>_<geo>_<date>.csv - JSON snapshot alongside (for downstream analysis)
- Stdout summary: top-N keywords by volume, ad-group breakdown (historical mode), zero-volume drop list
- If
--slack: a summary message to the client's Slack channel
CSV columns (historical): ad_group, keyword, intended_match_type, avg_monthly_searches, competition, competition_index, low_bid_eur, high_bid_eur, <id>, volume_drop_vs_intent, note
CSV columns (ideas): keyword, avg_monthly_searches, competition, competition_index, low_bid_eur, high_bid_eur, <id>
Match-type recommendation heuristic
The script applies a simple rule to suggest a match type:
| Avg monthly searches | Recommended match type |
|---|---|
| 0 | DROP — no demand, kills Quality Score |
| 1–99 | PHRASE if competition=HIGH, otherwise BROAD |
| 100–999 | PHRASE |
| ≥ 1000 | EXACT |
These are starting points — humans override based on intent.
Customer ID
The script uses the client's google_ads_id from clients.json as the calling customer. Search-volume accuracy depends on the calling account having recent ad spend — dormant accounts return bucketed ranges (e.g. "1K–10K") instead of exact numbers. All our active client accounts qualify.
If a client doesn't have a Google Ads account yet (or you want to probe a niche from scratch), pass --customer-id <id> of any spending account in the MCC.