AnswerLine Start free

, Keyword Research · Local SEO · Google

International keyword research: markets and languages

International keyword research is the same research repeated per market, where a market is a country and a language together, and the differences between markets are the finding. The SERP for “accounting software” in the US, “Buchhaltungssoftware” in Germany and “logiciel comptable” in France, Canada and Switzerland each has its own questions, features, AI Overview behavior and competitors. The steps below define markets, collect SERPs per market with this API’s country parameter and native-language queries, and compare them. This API returns no search volume, so pair it with a volume source (why).

Definitions

The parameters

From the OpenAPI reference, POST /v1/monitor/google takes:

Parameter Format Role
country ISO 3166-1 alpha-2, such as DE Result geography. Required in the standard request shape.
gl Same codes, either case Google’s own name for the same setting. Send country or gl; different values in both is a 400.
url Full Google search URL Alternative shape to query and country.

The language of a Google Search market comes from the country (or hl, when set) and from the language the query is written in. location or uule targets a city.

GET /v1/countries returns the supported country codes; pass ?model=google (or aioverview, aimode, chatgpt and so on) to filter by engine. Check it once when you define markets rather than discovering an unsupported code as a validation error mid-run.

AI engines take market parameters too. AI Mode accepts country, gl, hl, location and uule, and Copilot, Perplexity and Grok take any country. ChatGPT and Gemini take country, and both answer from 213 countries; Gemini also takes a US state. Multilingual AI answers covers monitoring those per language.

Canonical location names

AI Mode’s location expects names from Google’s geo targets list. The file published on 2026-08-12 (Google Ads API geotargets, checked 2026-09-17) has columns Criteria ID, Name, Canonical Name, Parent ID, Country Code, Target Type and Status. Canonical names do not always follow English spelling or an obvious region: in that file, Munich is Munich,Bavaria,Germany and Lyon is Lyon,Auvergne-Rhone-Alpes,France. Look names up in the file rather than typing them.

Step 1: define the market matrix

List markets explicitly, with the reason each exists:

MARKETS = [
    {"id": "us-en", "country": "US", "lang": "en"},
    {"id": "us-es", "country": "US", "lang": "es"},      # Spanish-language searchers in the US
    {"id": "gb-en", "country": "GB", "lang": "en"},
    {"id": "de-de", "country": "DE", "lang": "de"},
    {"id": "ch-de", "country": "CH", "lang": "de"},      # same language as DE, different market
    {"id": "ch-fr", "country": "CH", "lang": "fr"},
    {"id": "ca-en", "country": "CA", "lang": "en"},
    {"id": "ca-fr", "country": "CA", "lang": "fr"},
    {"id": "br-pt", "country": "BR", "lang": "pt"},
]

Three rules:

  1. Separate languages within a country. Canada in English and Canada in French are different research projects, each with keywords written in its own language.
  2. Separate countries within a language. German in Germany, Austria and Switzerland shares words but not competitors, currencies or regulations.
  3. Add a market only if you would act on it. Every market multiplies cost.

Step 2: build native seeds

Translation gets you a first guess. The local SERP tells you whether locals search that way.

  1. Write or translate 10 to 30 seed keywords per market, ideally reviewed by a native speaker.
  2. Run each seed in its market.
  3. Read relatedSearches[].query and peopleAlsoAsk[].question from the response. These are phrased by the market.
  4. Replace seeds that return thin or off-topic related searches with the phrasing the SERP suggests.

A seed whose related searches are about something else entirely is a mistranslation; drop it early before it spawns a hundred wrong expansions.

Step 3: collect SERPs as batches

One task per keyword per market. Submit them with POST /v1/async/task/batch (1 to 500 tasks per request), keyed by meaning so re-runs are safe:

import os
from datetime import date

import requests

API = "https://api.answerline.dev"
HEADERS = {"Authorization": f"Bearer {os.environ['ANSWERLINE_API_KEY']}"}
WEBHOOK = "https://research.example.com/hooks/serp"

def tasks_for(keywords: dict[str, list[str]], run: str) -> list[dict]:
    """keywords maps market id to that market's keyword list."""
    by_id = {m["id"]: m for m in MARKETS}
    return [
        {
            "taskType": "GOOGLE",
            "payload": {
                "query": kw,
                "country": by_id[market]["country"],
                "include": {"aioverview": {}},
            },
            "idempotencyKey": f"intl:{run}:{market}:{kw}",
            "webhook": {"url": WEBHOOK},
        }
        for market, kws in keywords.items()
        for kw in kws
    ]

def submit(tasks: list[dict]) -> list[dict]:
    created = []
    for i in range(0, len(tasks), 500):
        chunk = tasks[i:i + 500]
        body = requests.post(f"{API}/v1/async/task/batch", headers=HEADERS, json=chunk, timeout=60).json()
        for item in body["results"]:
            if item["success"]:
                created.append({"key": chunk[item["index"]]["idempotencyKey"], "task_id": item["task"]["id"]})
            elif item["error"]["code"] != "RESOURCE_ALREADY_EXISTS":
                print(chunk[item["index"]]["idempotencyKey"], item["error"]["code"])
    return created

run = date.today().isoformat()
created = submit(tasks_for({"de-de": ["buchhaltungssoftware", "rechnungsprogramm"], "ca-fr": ["logiciel comptable"]}, run))

Idempotency keys are unique across your account, and long keyword text makes long keys; if that becomes unwieldy, use a keyword id from your own table instead of the text. Store task_id next to the key so a reconciliation pass can fetch anything whose webhook never arrived with GET /v1/async/task/{taskId}. Receiving and verifying webhooks is covered in webhook receiver design.

Step 4: reduce each SERP to a comparable record

Store one row per keyword, market and run:

from urllib.parse import urlparse

def host(url: str) -> str:
    h = urlparse(url).netloc.lower()
    return h[4:] if h.startswith("www.") else h

def record(market: str, keyword: str, result: dict) -> dict:
    organic = [r for r in result.get("organicResults", []) if r.get("position", 99) <= 10]
    aio = result.get("aioverview")
    return {
        "market": market,
        "keyword": keyword,
        "top10": [host(r["link"]) for r in organic],
        "top10_urls": [r["link"] for r in organic],
        "has_aio": aio is not None,
        "aio_sources": [host(s["url"]) for s in (aio or {}).get("sources", [])],
        "paa": [q["question"] for q in result.get("peopleAlsoAsk", [])],
        "related": [r["query"] for r in result.get("relatedSearches", [])],
        "ads": len(result.get("ads", [])),
        "local": bool(result.get("localResults")),
    }

Step 5: compare markets

Map equivalent keywords across markets with a concept id (for example accounting-software maps to buchhaltungssoftware in de-de, logiciel comptable in ca-fr). Then compare per concept.

Competitor overlap

def overlap(a: list[str], b: list[str]) -> float:
    sa, sb = set(a), set(b)
    return len(sa & sb) / len(sa | sb) if sa | sb else 0.0

Low overlap between two markets for the same concept means different competitors: global category leaders in one, local specialists in the other. That changes both content strategy and who you benchmark against.

Feature differences

For each concept, tabulate per market: AI Overview present, ads count, local results present, PAA count. Typical findings worth acting on:

Question sets

Compare paa lists per concept. Questions that appear only in one market (regulation, tax, a local payment method, a local competitor) are the content that localization by translation misses.

Step 6: check which of your pages rank where

International sites often rank the wrong locale: the US English page in Germany, the French-France page in Quebec. From the stored top10_urls:

EXPECTED = {"us-en": "/en-us/", "de-de": "/de-de/", "ca-fr": "/fr-ca/", "ch-fr": "/fr-ch/"}

def locale_mismatches(rows: list[dict], my_host: str) -> list[tuple]:
    out = []
    for row in rows:
        expected = EXPECTED.get(row["market"])
        for url in row["top10_urls"]:
            if host(url) == my_host and expected and expected not in url:
                out.append((row["market"], row["keyword"], url))
    return out

Adapt the path check to your URL scheme (subdirectories, subdomains or country domains). Each mismatch is a candidate hreflang, canonical or content issue to investigate.

City-level research inside a market

Some markets are not uniform. Google Search and AI Mode take location with a canonical name, so for local-intent concepts you can see how Google’s AI answer changes by city with POST /v1/monitor/aimode:

{ "prompt": "steuerberater", "country": "DE", "hl": "de", "location": "Munich,Bavaria,Germany" }

location cannot be combined with uule. Every city is another request at the same price.

What it costs

Costs at the Starter tier: a Google Search request costs $0.00103 for one page, include.aioverview adds $0.00071, and each extra results page adds $0.00071. An AI Mode request costs $0.00206.

Run Tasks Credits
200 keywords × 4 markets, one page 800 $0.824
500 keywords × 6 markets, with AI Overview 3,000 $5.22
Seed validation: 20 seeds × 9 markets, with AI Overview 180 $0.3132
50 local concepts × 10 cities in one market, AI Mode 500 $1.03

Markets multiply everything. Validate seeds on a small set first; the 100 free requests a new account gets work on any engine, enough to spot-check a few seeds before the full validation run. Tier prices are on /pricing.

Pitfalls

See the keyword research use case and the Google Search engine page to start.

Questions

Which parameters control country and language in this API?

On Google Search, country (or gl) sets the result geography as an ISO 3166-1 alpha-2 code, and the language you write the query in carries the market's language; hl sets the interface language, and location or uule a city or region. AI Mode takes the same fields. GET /v1/countries lists the supported country codes, filterable by engine.

Can the interface language differ from the country?

Yes. On Google Search, write the query in the market's language, so Spanish in the US or French in Canada are separate markets. On AI Mode, hl also overrides the interface language derived from the country.

Should I translate my keyword list for each market?

Use translation only to create seeds, then expand from the local results page: related searches and People Also Ask in the market's language show how people there phrase the need. Translated keywords often are not what locals search.

How do I check that the right language version of my site ranks in each market?

Collect organic results per market, find the URLs on your domain, and compare each URL's locale path or subdomain with the market's expected locale. A mismatch, such as the US page ranking in Germany, points to hreflang or localization problems.

What does a multi-market keyword run cost?

Each keyword in each market is one request. A Google Search request is $0.00103 on every tier, plus $0.00071 with the AI Overview. 500 keywords across 6 markets with AI Overview detection is 3,000 tasks and $5.22.

Try it on your own prompts

100 free requests on any engine, no card. One POST returns the answer, sources and citations as JSON.

Keep reading