Skip to content
SEO API

Keyword Research

Volume, CPC, competition and a 12-month trend for up to 100 keywords in one call.

POST /api/v1/keywords/global$0.001 / keywordAuth: X-API-Key header
seonio / rest apiFrom the API docs
RequestPOST /api/v1/keywords/global
curl -X POST https://seonio.com/api/v1/keywords/global \
  -H "Content-Type: application/json" \
  -H "X-API-Key: $SEONIO_API_KEY" \
  -d '{"keywords": ["keyword research", "seo tools", "domain authority checker", "xqzv 7731", "how to fix a leaking kitchen faucet without calling a plumber"]}'
Response200 OK
{
  "keywords": [
    {
      "query": "keyword research",
      "text": "keyword research",
      "match": "exact",
      "volume": 550000,
      "cpc": 37,
      "low_cpc": 37,
      "high_cpc": 212,
      "competition": 18,
      "monthly_trend": [
        {
          "month": "2026-08",
          "value": 450000
        },
        {
          "month": "2026-07",
          "value": 201000
        },
        {
          "month": "2026-06",
          "value": 27100
        }
      ],
      "data_through": "2026-08",
      "source": "google_ads"
    },
    {
      "query": "seo tools",
      "text": "seo tools",
      "match": "exact",
      "volume": 110000,
      "cpc": 86,
      "low_cpc": 86,
      "high_cpc": 455,
      "competition": 53,
      "monthly_trend": [
        {
          "month": "2026-08",
          "value": 301000
        },
        {
          "month": "2026-07",
          "value": 90500
        },
        {
          "month": "2026-06",
          "value": 90500
        }
      ],
      "data_through": "2026-08",
      "source": "google_ads"
    },
    {
      "query": "domain authority checker",
      "text": "domain authority checker",
      "match": "exact",
      "volume": 135000,
      "cpc": 41,
      "low_cpc": 41,
      "high_cpc": null,
      "competition": 76,
      "monthly_trend": [
        {
          "month": "2026-08",
          "value": 110000
        },
        {
          "month": "2026-07",
          "value": 110000
        },
        {
          "month": "2026-06",
          "value": 110000
        }
      ],
      "data_through": "2026-08",
      "source": "google_ads_estimated_months"
    }
  ],
  "no_data": [
    "xqzv 7731"
  ],
  "unsupported": [
    {
      "query": "how to fix a leaking kitchen faucet without calling a plumber",
      "reason": "too_long"
    }
  ],
  "unavailable": []
}
Contract

What to send and what comes back

Send a list of keywords and get back the core metrics most SEO and PPC tools are built on: worldwide volume, CPC, competition and a dated monthly trend. Every non-blank keyword you send comes back once, in your order, in one of four lists: keywords (with data), no_data, unsupported or unavailable. You pay for each distinct keyword we look up, with data or without; unsupported and unavailable keywords are free. Keywords in scripts we can't measure worldwide come back in unsupported; the country endpoint answers them per country.

Request body

Request body
FieldTypeRequiredDescription
keywords string[] required 1–100 keywords to look up. Billed once per distinct keyword looked up: "SEO Tools" and "seo tools" are one keyword, "straße" and "strasse" are two. Blank entries are ignored; an entry longer than 200 characters rejects the whole request with a 422 (not billed).

Response

Response fields
FieldTypeDescription
keywords[].query string The keyword as you sent it (trimmed). Entries keep your order, duplicates included.
keywords[].text string The keyword the numbers are for: your keyword, normalized (lowercase, punctuation removed), or its closest spelling when match is closest.
keywords[].match string "exact": text is your keyword with only case, spacing and character width changed ("SEO" as "seo"). "closest": we measured a nearby form, with punctuation and symbols dropped ("C++" as "c", "node.js" as "node js") or the closest spelling our worldwide lookup can read ("straße" as "strasse", "iphone ١٥" as "iphone 15"). The numbers are that form's, since Google counts spellings separately.
keywords[].volume integer Average monthly search volume worldwide (Google Ads).
keywords[].cpc integer Cost per click in US cents: the same value as low_cpc, kept for existing clients.
keywords[].low_cpc integer Google Ads' low top-of-page bid in US cents. When source is google_ads_estimated_months it is an estimate from a second source, which can differ.
keywords[].high_cpc integer | null Google Ads' high top-of-page bid in US cents; null when source is google_ads_estimated_months (that source has no high bid).
keywords[].competition integer Google Ads competition index (0–100).
keywords[].monthly_trend object[] Up to 12 months of search volume, newest to oldest, each as { month: YYYY-MM, value }, labelled with the Google Ads month each value belongs to. With source google_ads the months are Google's own; with google_ads_estimated_months they are estimated (the newest is two months before the request) and can be one month off. Empty when no months are known.
keywords[].data_through string | null The newest month in monthly_trend (YYYY-MM); null when monthly_trend is empty.
keywords[].source string "google_ads": our stored Google Ads history, with Google's own months. We use it when its newest month is at least as new as the second source's would be today. "google_ads_estimated_months": the same Google Ads metrics from a second source, months estimated.
no_data string[] The keywords you sent (as sent, in your order) that we looked up and that have no search data. Billed like keywords with data.
unsupported object[] The keywords you sent (in your order) that we did not look up, each as { query, reason }. Not billed. Usually empty. On 2026-10-16 it changed from a list of strings to these objects, and stopped being billed.
unsupported[].query string The keyword as you sent it (trimmed).
unsupported[].reason string "empty": nothing left after removing punctuation and symbols (e.g. "!!!" or an emoji). "too_long": more than 8 words or 70 characters once punctuation is removed. "script": a script our worldwide lookup cannot read, non-Latin or mixed (e.g. "天気"), or a closest spelling that would go past those limits; /api/v1/keywords/country answers these per country from Google.
unavailable string[] The keywords you sent (as sent, in your order) whose lookup failed at the provider. Not billed; send them again. When some keywords are unavailable and none was answered, the call is a 503 instead and you are not charged.
Code

Copy, paste, add your key

The same request in cURL, Python and Node.js.

cURL
curl -X POST https://seonio.com/api/v1/keywords/global \
  -H "Content-Type: application/json" \
  -H "X-API-Key: YOUR_API_KEY" \
  -d '{"keywords": ["keyword research", "seo tools", "domain authority checker", "xqzv 7731", "how to fix a leaking kitchen faucet without calling a plumber"]}'
Python
import requests

resp = requests.post(
    "https://seonio.com/api/v1/keywords/global",
    headers={"X-API-Key": "YOUR_API_KEY"},
    json={"keywords": ["keyword research", "seo tools", "domain authority checker", "xqzv 7731", "how to fix a leaking kitchen faucet without calling a plumber"]},
)
print(resp.json())
Node.js
const resp = await fetch("https://seonio.com/api/v1/keywords/global", {
  method: "POST",
  headers: {
    "Content-Type": "application/json",
    "X-API-Key": "YOUR_API_KEY",
  },
  body: JSON.stringify({"keywords": ["keyword research", "seo tools", "domain authority checker", "xqzv 7731", "how to fix a leaking kitchen faucet without calling a plumber"]}),
});
console.log(await resp.json());
Playground

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.001 / keyword. With no key, you see the sample response and nothing is charged.

POST /api/v1/keywords/globalSample response
{
  "keywords": [
    {
      "competition": 18,
      "cpc": 37,
      "data_through": "2026-08",
      "high_cpc": 212,
      "low_cpc": 37,
      "match": "exact",
      "monthly_trend": [
        {
          "month": "2026-08",
          "value": 450000
        },
        {
          "month": "2026-07",
          "value": 201000
        },
        {
          "month": "2026-06",
          "value": 27100
        }
      ],
      "query": "keyword research",
      "source": "google_ads",
      "text": "keyword research",
      "volume": 550000
    },
    {
      "competition": 53,
      "cpc": 86,
      "data_through": "2026-08",
      "high_cpc": 455,
      "low_cpc": 86,
      "match": "exact",
      "monthly_trend": [
        {
          "month": "2026-08",
          "value": 301000
        },
        {
          "month": "2026-07",
          "value": 90500
        },
        {
          "month": "2026-06",
          "value": 90500
        }
      ],
      "query": "seo tools",
      "source": "google_ads",
      "text": "seo tools",
      "volume": 110000
    },
    {
      "competition": 76,
      "cpc": 41,
      "data_through": "2026-08",
      "high_cpc": null,
      "low_cpc": 41,
      "match": "exact",
      "monthly_trend": [
        {
          "month": "2026-08",
          "value": 110000
        },
        {
          "month": "2026-07",
          "value": 110000
        },
        {
          "month": "2026-06",
          "value": 110000
        }
      ],
      "query": "domain authority checker",
      "source": "google_ads_estimated_months",
      "text": "domain authority checker",
      "volume": 135000
    }
  ],
  "no_data": [
    "xqzv 7731"
  ],
  "unavailable": [],
  "unsupported": [
    {
      "query": "how to fix a leaking kitchen faucet without calling a plumber",
      "reason": "too_long"
    }
  ]
}
Use cases

What developers build with it

  • Bulk keyword explorers and research dashboards.
  • Content briefs that need volume and trend at a glance.
  • Prioritising a backlog by demand and competition.
Status codes

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.

Status codes
CodeWhen
200Success. The lookup completed and was charged, even if the response is empty. /keywords/global charges only the keywords it looked up.
401Missing or unknown API key.
402Not 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.
403The account is not verified yet, or it is locked.
422Invalid 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.
429Rate 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.
503A data provider failed or timed out (X-Outcome: temporary). Nothing was charged; retry after the seconds in the Retry-After header.
FAQ

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.

Free $1 to test · No card required

Turn search insights into measurable growth

Start your SEO research in seconds.

SEO researchREST APIMCP