Keyword Metrics by Country
Localized metrics for up to 500 keywords in one country.
curl -X POST https://seonio.com/api/v1/keywords/country \ -H "Content-Type: application/json" \ -H "X-API-Key: $SEONIO_API_KEY" \ -d '{"country": "de", "keywords": ["seo tools", "keyword research"]}'
{
"country": "de",
"keywords": [
{
"text": "seo tools",
"volume": 8100,
"cpc": 640,
"low_cpc": 280,
"high_cpc": 1120,
"competition": 58,
"difficulty": 61,
"intent": "commercial",
"monthly_trend": [
{
"month": "2026-08",
"value": 8100
},
{
"month": "2026-07",
"value": 6600
},
{
"month": "2026-06",
"value": 6600
}
]
},
{
"text": "keyword research",
"volume": 5400,
"cpc": 590,
"low_cpc": 240,
"high_cpc": 990,
"competition": 52,
"difficulty": 57,
"intent": "informational",
"monthly_trend": [
{
"month": "2026-08",
"value": 5400
},
{
"month": "2026-07",
"value": 4400
},
{
"month": "2026-06",
"value": 4400
}
]
}
]
}What to send and what comes back
Send a list of keywords and a country and get back each keyword's search volume, CPC range, competition, difficulty, intent and 12-month trend for that specific market. The language is derived from the country automatically.
Request body
| Field | Type | Required | Description |
|---|---|---|---|
keywords |
string[] | required | 1–500 keywords to look up in the given country. Billed per request plus each started block of 10 keywords. |
country |
string | optional | Two-letter ISO country code (default "us"). The language is derived from it.
50 supported countriesdzAlgeriaarArgentinaauAustraliaatAustriabdBangladeshbeBelgiumbrBrazilbgBulgariacaCanadaczCzechiadkDenmarkegEgyptfiFinlandfrFrancedeGermanyghGhanagrGreecehuHungaryinIndiaidIndonesiaieIrelanditItalyjpJapankeKenyamyMalaysiamxMexicomaMorocconlNetherlandsnzNew ZealandngNigerianoNorwaypkPakistanphPhilippinesplPolandptPortugalroRomaniasaSaudi ArabiarsSerbiasgSingaporezaSouth AfricaesSpainlkSri LankaseSwedenchSwitzerlandthThailandtrTurkiyeaeUnited Arab EmiratesgbUnited Kingdom (also uk, returned as gb)usUnited StatesvnVietnam |
Response
| Field | Type | Description |
|---|---|---|
country |
string | The country the metrics were resolved for. |
keywords[].text |
string | The keyword. Keywords with no data are omitted. |
keywords[].volume |
integer | Average monthly search volume in the country. |
keywords[].cpc |
integer | null | Average cost per click in US cents; null when the keyword's metrics came from Google Ads keyword ideas, which have no average (low_cpc and high_cpc are still set). |
keywords[].low_cpc |
integer | Google Ads' low top-of-page bid in US cents. |
keywords[].high_cpc |
integer | Google Ads' high top-of-page bid in US cents. |
keywords[].competition |
integer | Competition score (0–100). |
keywords[].difficulty |
integer | null | Keyword difficulty (0–100); null when the keyword's metrics came from Google Ads keyword ideas, which have no difficulty. |
keywords[].intent |
string | Search intent (informational, navigational, commercial, transactional, unclear), or null when not classified yet: each call classifies up to 100 more, highest volume first. |
keywords[].monthly_trend |
object[] | The last 12 months of search volume, newest to oldest, each as { month: YYYY-MM, value }, labelled with the Google Ads month each value belongs to (the newest is usually two months back). Empty when Google has no monthly data. Until October 2026 these months were mislabeled; the values were right. |
Copy, paste, add your key
The same request in cURL, Python and Node.js.
curl -X POST https://seonio.com/api/v1/keywords/country \
-H "Content-Type: application/json" \
-H "X-API-Key: YOUR_API_KEY" \
-d '{"country": "de", "keywords": ["seo tools", "keyword research"]}'
import requests
resp = requests.post(
"https://seonio.com/api/v1/keywords/country",
headers={"X-API-Key": "YOUR_API_KEY"},
json={"country": "de", "keywords": ["seo tools", "keyword research"]},
)
print(resp.json())
const resp = await fetch("https://seonio.com/api/v1/keywords/country", {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-API-Key": "YOUR_API_KEY",
},
body: JSON.stringify({"country": "de", "keywords": ["seo tools", "keyword research"]}),
});
console.log(await resp.json());
Run a real request
Send this request from your browser with your API key, or leave the key empty to see the sample response.
A successful run costs $0.01 / request + $0.001 / started 10 keywords. With no key, you see the sample response and nothing is charged.
{
"country": "de",
"keywords": [
{
"competition": 58,
"cpc": 640,
"difficulty": 61,
"high_cpc": 1120,
"intent": "commercial",
"low_cpc": 280,
"monthly_trend": [
{
"month": "2026-08",
"value": 8100
},
{
"month": "2026-07",
"value": 6600
},
{
"month": "2026-06",
"value": 6600
}
],
"text": "seo tools",
"volume": 8100
},
{
"competition": 52,
"cpc": 590,
"difficulty": 57,
"high_cpc": 990,
"intent": "informational",
"low_cpc": 240,
"monthly_trend": [
{
"month": "2026-08",
"value": 5400
},
{
"month": "2026-07",
"value": 4400
},
{
"month": "2026-06",
"value": 4400
}
],
"text": "keyword research",
"volume": 5400
}
]
}
What developers build with it
- Localizing keyword research to a specific market.
- Comparing demand for the same keywords across countries.
- Bulk-checking volume and difficulty before targeting a region.
The same contract on every endpoint
Error responses are JSON with a "detail" field. It is a message string unless the row above says otherwise.
| Code | When |
|---|---|
200 | Success. The lookup completed and was charged, even if the response is empty. /keywords/global charges only the keywords it looked up. |
401 | Missing or unknown API key. |
402 | Not enough balance. "detail" is an object with "error", "message", "balance_required" and "balance_available". Requests still running hold their price until they finish, so a retry can succeed once they do. |
403 | The account is not verified yet, or it is locked. |
422 | Invalid request (wrong shape or types, a keyword over 200 characters, a URL the page checker cannot audit, a domain that does not exist, or a bare public suffix such as co.uk that groups many websites) — never charged. "detail" is usually a list of validation errors; an answer that retrying cannot change (such as a domain without a DNS record) is a message string with an X-Outcome header naming it: missing, unsupported, too_large or refused_input. |
429 | Rate limit exceeded, never charged. Requests per second per account by plan: 5 on a free account, 10 on Starter, 15 on Pro and 20 on Business; retry after the Retry-After header. Also the page checker's provider limit. |
503 | A data provider failed or timed out (X-Outcome: temporary). Nothing was charged; retry after the seconds in the Retry-After header. |
Questions, answered
Simple answers for developers using the API.
Every endpoint has a published USD price or formula. Requests draw from the same testing, included and Extra Usage balance as website and MCP actions: testing first, then included usage, then Extra Usage expiring soonest.
API actions require a verified account with a free $1 balance or an active subscription balance. Create and manage API keys from your account, then send the key in the X-API-Key header.
Yes. Your account shows API usage and remaining balance, so you can track costs while building and running automated workflows.
REST API and MCP requests per second depend on your plan: 5 on a free account, 10 on Starter, 15 on Pro and 20 on Business. Website data requests from the same account count toward the same second. Requests above the limit return HTTP 429 with a Retry-After header and are not charged; wait a second and retry.
Turn search insights into measurable growth
Start your SEO research in seconds.