Dan Antony

Node Script to Look Up Google Ads Keyword Planner Ideas

PPC & Paid Search·Other·Google Ads

What it does

Takes a list of seed keywords and queries the Google Ads Keyword Planner GenerateKeywordIdeas endpoint. Returns monthly search volume, competition level, competition index, and top-of-page bid ranges for each keyword. Outputs a sorted ASCII table to stdout and optionally saves a JSON file.

The code

#!/usr/bin/env node
/**
 * Google Ads Keyword Planner — GenerateKeywordIdeas lookup
 * REST API shape per https://developers.google.com/google-ads/api/docs/keyword-planning/generate-keyword-ideas
 *
 * Usage:
 *   tsx keyword-planner-lookup.ts
 *     No args — runs the hardcoded KEYWORDS list against US geo
 *     and overwrites dan-marketing-keyword-volumes.json.
 *
 *   tsx keyword-planner-lookup.ts --geo=in --keywords="google ads script exclude search terms,negative keyword script"
 *     Ad-hoc lookup. Prints results to stdout only — pass --out=<path>
 *     to also save a JSON file.
 *
 * Flags:
 *   --geo=us|in         Geo target. Default: us. (India = in)
 *   --keywords="a,b,c"  Comma-separated seed keywords. Default: the
 *                        hardcoded KEYWORDS list below.
 *   --lang=<constant>   Language constant. Default: languageConstants/1000 (English)
 *   --out=<path>        Save results JSON here. Default: only saved when
 *                        run with no flags at all (the original behaviour);
 *                        otherwise printed to stdout and not saved unless
 *                        --out is given explicitly.
 */
const API_VERSION = 'v23';
const BASE_URL = `https://googleads.googleapis.com/${API_VERSION}`;
const TOKEN_URL = 'https://oauth2.googleapis.com/token';
function getConfig() {
    const get = (k) => {
        const v = process.env[k];
        if (!v)
            throw new Error(`Missing env var: ${k}`);
        return v;
    };
    return {
        clientId: get('GOOGLE_ADS_CLIENT_ID'),
        clientSecret: get('GOOGLE_ADS_CLIENT_SECRET'),
        refreshToken: get('GOOGLE_ADS_REFRESH_TOKEN'),
        developerToken: get('GOOGLE_ADS_DEVELOPER_TOKEN'),
        loginCustomerId: get('GOOGLE_ADS_LOGIN_CUSTOMER_ID'),
        customerId: get('GOOGLE_ADS_CUSTOMER_ID'),
    };
}
async function getAccessToken(config) {
    const res = await fetch(TOKEN_URL, {
        method: 'POST',
        headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
        body: new URLSearchParams({
            client_id: config.clientId,
            client_secret: config.clientSecret,
            refresh_token: config.refreshToken,
            grant_type: 'refresh_token',
        }),
    });
    if (!res.ok) {
        throw new Error(`OAuth error ${res.status}: ${await res.text()}`);
    }
    const data = await res.json();
    return data.access_token;
}
async function generateKeywordIdeas(config, keywords, geoTargets = ['geoTargetConstants/2840'], // US
language = 'languageConstants/1000') {
    const accessToken = await getAccessToken(config);
    const url = `${BASE_URL}/customers/${config.customerId}:generateKeywordIdeas`;
    const body = {
        customerId: config.customerId,
        language,
        geoTargetConstants: geoTargets,
        includeAdultKeywords: false,
        keywordPlanNetwork: 'GOOGLE_SEARCH',
        keywordSeed: { keywords },
    };
    const res = await fetch(url, {
        method: 'POST',
        headers: {
            'Authorization': `Bearer ${accessToken}`,
            'developer-token': config.developerToken,
            'login-customer-id': config.loginCustomerId,
            'Content-Type': 'application/json',
        },
        body: JSON.stringify(body),
    });
    if (!res.ok) {
        const errText = await res.text();
        throw new Error(`Keyword Planner API error ${res.status}: ${errText}`);
    }
    const data = await res.json();
    return (data.results ?? []).map(r => ({
        keyword: r.text ?? 'unknown',
        avgMonthlySearches: parseInt(r.keywordIdeaMetrics?.avgMonthlySearches ?? '0'),
        competition: r.keywordIdeaMetrics?.competition ?? 'UNSPECIFIED',
        competitionIndex: parseInt(r.keywordIdeaMetrics?.competitionIndex ?? '0'),
        lowTopOfPageBidMicros: r.keywordIdeaMetrics?.lowTopOfPageBidMicros,
        highTopOfPageBidMicros: r.keywordIdeaMetrics?.highTopOfPageBidMicros,
    }));
}
// ---------------------------------------------------------------------------
// Geo targets — Google Ads geo target constant IDs
// ---------------------------------------------------------------------------
const GEO_TARGETS = {
    us: { id: 'geoTargetConstants/2840', label: 'US' },
    in: { id: 'geoTargetConstants/2356', label: 'India' },
    india: { id: 'geoTargetConstants/2356', label: 'India' },
};
function parseArgs(argv) {
    const opts = {};
    for (const arg of argv) {
        if (arg.startsWith('--geo='))
            opts.geo = arg.slice('--geo='.length).trim().toLowerCase();
        else if (arg.startsWith('--keywords=')) {
            opts.keywords = arg
                .slice('--keywords='.length)
                .split(',')
                .map(k => k.trim())
                .filter(Boolean);
        }
        else if (arg.startsWith('--lang='))
            opts.lang = arg.slice('--lang='.length).trim();
        else if (arg.startsWith('--out='))
            opts.out = arg.slice('--out='.length).trim();
        else
            throw new Error(`Unknown argument: ${arg}. See the usage comment at the top of this file.`);
    }
    return opts;
}
// ---------------------------------------------------------------------------
// Main
// ---------------------------------------------------------------------------
const KEYWORDS = [
    // P0
    'google ads data manager api',
    'meta capi setup',
    'meta conversions api guide',
    'manage google ads from terminal',
    'command line ad operations',
    'gtm enhanced conversions setup',
    'enhanced conversions google ads',
    'google ads api migration',
    // P1
    'linkedin ads google sheets python',
    'google ads scripts reporting',
    'automate internal linking',
    'dynamic keyword insertion wordpress',
    'cookie based conversion tracking gtm',
    // P2
    'schema markup guide',
    'free website with free domain',
    'linkedin social selling',
    // Semantic
    'marketing automation cli',
    'server side tracking setup',
    'first party data conversion tracking',
    'seo automation tools',
];
async function main() {
    const config = getConfig();
    const opts = parseArgs(process.argv.slice(2));
    const usingDefaults = !opts.geo && !opts.keywords && !opts.lang;
    const geoKey = opts.geo ?? 'us';
    const geo = GEO_TARGETS[geoKey];
    if (!geo) {
        throw new Error(`Unknown --geo=${opts.geo}. Known values: ${Object.keys(GEO_TARGETS).join(', ')}`);
    }
    const keywords = opts.keywords ?? KEYWORDS;
    const lang = opts.lang ?? 'languageConstants/1000';
    console.log(`Looking up ${keywords.length} keywords...`);
    console.log(`Account: ${config.customerId} | Geo: ${geo.label} | Lang: ${lang}\n`);
    const results = await generateKeywordIdeas(config, keywords, [geo.id], lang);
    results.sort((a, b) => b.avgMonthlySearches - a.avgMonthlySearches);
    console.log('Keyword'.padEnd(45), 'Avg Monthly', 'Competition', 'Comp Index');
    console.log('-'.repeat(85));
    for (const r of results) {
        const compLabel = r.competition.replace('COMPETITION_', '').toLowerCase();
        console.log(r.keyword.slice(0, 44).padEnd(45), String(r.avgMonthlySearches).padStart(11), compLabel.padStart(11), String(r.competitionIndex).padStart(10));
    }
    const fs = await Promise.resolve().then(() => require('node:fs'));
    const defaultOutPath = './dan-marketing-keyword-volumes.json';
    const outPath = opts.out ?? (usingDefaults ? defaultOutPath : null);
    if (outPath) {
        fs.writeFileSync(outPath, JSON.stringify(results, null, 2));
        console.log(`\nSaved to: ${outPath}`);
    }
    else {
        console.log('\n(Ad-hoc lookup — pass --out=<path> to save this as JSON.)');
    }
}
main().catch(err => {
    console.error('Fatal error:', err.message);
    process.exit(1);
});

When to use this

Auditing keyword volumes for existing campaigns or account structure decisions; generating keyword research for content planning or new campaign builds; running ad-hoc checks during SOP workflows without touching the Google Ads UI; tracking volume changes over time by saving JSON snapshots and comparing diffs; or replacing manual Keyword Planner exports with an auditable, scriptable workflow.

Prerequisites

  • A Google Ads manager or linked MCC with API access enabled.
  • OAuth2 credentials set up in Google Cloud Console (client ID + client secret).
  • A refresh token obtained via the OAuth2 flow for your Google Ads account.
  • Your Google Ads developer token (must be in 'enabled' status for production MCCs).
  • Node.js 18+ (uses built-in fetch — no extra dependencies).
  • Six env vars set before running: GOOGLE_ADS_CLIENT_ID, GOOGLE_ADS_CLIENT_SECRET, GOOGLE_ADS_REFRESH_TOKEN, GOOGLE_ADS_DEVELOPER_TOKEN, GOOGLE_ADS_LOGIN_CUSTOMER_ID, GOOGLE_ADS_CUSTOMER_ID.

How to implement

  1. 1

    Set environment variables

    Export all six required env vars in your shell, or load them from a .env file: GOOGLE_ADS_CLIENT_ID, GOOGLE_ADS_CLIENT_SECRET, GOOGLE_ADS_REFRESH_TOKEN, GOOGLE_ADS_DEVELOPER_TOKEN, GOOGLE_ADS_LOGIN_CUSTOMER_ID, GOOGLE_ADS_CUSTOMER_ID.

  2. 2

    Run with the default keyword list

    Run with no flags to use the built-in keyword list and overwrite the tracked JSON file: `node keyword-planner-lookup.js`.

  3. 3

    Run an ad-hoc lookup

    Pass custom keywords as a comma-separated list. Results print to stdout only — no file is written unless you specify --out: `node keyword-planner-lookup.js --geo=in --keywords="google ads api,meta capi,server side tracking"`.

  4. 4

    Save ad-hoc results to a specific file

    Use --out to write JSON output to a path of your choosing instead of the default: `node keyword-planner-lookup.js --geo=us --keywords="conversion tracking,first party data" --out=./volumes-q4.json`.

  5. 5

    Change geo target and language

    Use --geo to target a different country (us or in). Use --lang to override the language constant: `node keyword-planner-lookup.js --geo=in --lang=languageConstants/2057 --keywords="email marketing tools"`.

Caveats

Only supports Google Search network (keywordPlanNetwork is hardcoded to GOOGLE_SEARCH — no Display or YouTube). Only two geo constants are mapped (US and India); other countries require adding entries to GEO_TARGETS in the source. API calls count against your Google Ads API quota — be careful with large keyword lists in CI loops. The default keyword list is Dan's account-specific list and will not be relevant to your account — always pass --keywords. The default output path is a relative ./dan-marketing-keyword-volumes.json.