GraphQL API
curl --request POST \
--url https://api.countrystatecity.in/v1/graphql \
--header 'X-CSCAPI-KEY: <api-key>'import requests
url = "https://api.countrystatecity.in/v1/graphql"
headers = {"X-CSCAPI-KEY": "<api-key>"}
response = requests.post(url, headers=headers)
print(response.text)const options = {method: 'POST', headers: {'X-CSCAPI-KEY': '<api-key>'}};
fetch('https://api.countrystatecity.in/v1/graphql', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://api.countrystatecity.in/v1/graphql",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_HTTPHEADER => [
"X-CSCAPI-KEY: <api-key>"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"net/http"
"io"
)
func main() {
url := "https://api.countrystatecity.in/v1/graphql"
req, _ := http.NewRequest("POST", url, nil)
req.Header.Add("X-CSCAPI-KEY", "<api-key>")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.post("https://api.countrystatecity.in/v1/graphql")
.header("X-CSCAPI-KEY", "<api-key>")
.asString();require 'uri'
require 'net/http'
url = URI("https://api.countrystatecity.in/v1/graphql")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Post.new(url)
request["X-CSCAPI-KEY"] = '<api-key>'
response = http.request(request)
puts response.read_bodyGraphQL API
GraphQL API
Query countries, states, cities, regions, and subregions with GraphQL instead of REST
POST
/
v1
/
graphql
GraphQL API
curl --request POST \
--url https://api.countrystatecity.in/v1/graphql \
--header 'X-CSCAPI-KEY: <api-key>'import requests
url = "https://api.countrystatecity.in/v1/graphql"
headers = {"X-CSCAPI-KEY": "<api-key>"}
response = requests.post(url, headers=headers)
print(response.text)const options = {method: 'POST', headers: {'X-CSCAPI-KEY': '<api-key>'}};
fetch('https://api.countrystatecity.in/v1/graphql', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://api.countrystatecity.in/v1/graphql",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_HTTPHEADER => [
"X-CSCAPI-KEY: <api-key>"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"net/http"
"io"
)
func main() {
url := "https://api.countrystatecity.in/v1/graphql"
req, _ := http.NewRequest("POST", url, nil)
req.Header.Add("X-CSCAPI-KEY", "<api-key>")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.post("https://api.countrystatecity.in/v1/graphql")
.header("X-CSCAPI-KEY", "<api-key>")
.asString();require 'uri'
require 'net/http'
url = URI("https://api.countrystatecity.in/v1/graphql")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Post.new(url)
request["X-CSCAPI-KEY"] = '<api-key>'
response = http.request(request)
puts response.read_bodyThe GraphQL endpoint covers the same geographic data as the REST API — countries, states, cities, regions, and subregions — through a single endpoint. Ask for exactly the fields you need, and resolve nested relationships (a country’s states, a state’s cities) in one request instead of chaining several REST calls yourself.
This returns the first 2 matching countries (sorted by name), up to 10 states for each, and up to 10 cities for each of those states: a page at every level, not every match. It is estimated at 23 lookups (1 for
A custom plan can also be set to an intermediate level that returns the Full fields except
That response is for
Field errors carry
Root list queries (
Available on the Professional and Business plans. Compare plans to add GraphQL to your API key.
When to use it
Reach for GraphQL when a screen needs a nested shape — for example a country with its states and each state’s cities — in one round trip. For a single flat list, the REST endpoints work just as well: they return the complete list in one response and are simpler to cache by URL.Authentication
string
required
Your API key, the same header every REST endpoint uses.
Sending a query
Send aPOST request with a JSON body containing a query string (and optional variables):
cURL
curl -X POST 'https://api.countrystatecity.in/v1/graphql' \
-H 'X-CSCAPI-KEY: YOUR_API_KEY' \
-H 'Content-Type: application/json' \
-d '{"query": "{ country(ciso: \"US\") { name capital currency } }"}'
JavaScript
const response = await fetch('https://api.countrystatecity.in/v1/graphql', {
method: 'POST',
headers: {
'X-CSCAPI-KEY': 'YOUR_API_KEY',
'Content-Type': 'application/json',
},
body: JSON.stringify({
query: '{ country(ciso: "US") { name capital currency } }',
}),
});
const { data } = await response.json();
Python
import requests
response = requests.post(
'https://api.countrystatecity.in/v1/graphql',
json={'query': '{ country(ciso: "US") { name capital currency } }'},
headers={'X-CSCAPI-KEY': 'YOUR_API_KEY'},
)
response.raise_for_status()
data = response.json()['data']
200 - Response
{
"data": {
"country": {
"name": "United States",
"capital": "Washington",
"currency": "USD"
}
}
}
Exploring the schema
AGET request to the same endpoint with an Accept header that includes text/html (what a browser sends) returns GraphiQL, an interactive schema explorer with autocomplete and inline docs for every type and field.
The page is protected like any query: the request that loads it must carry your X-CSCAPI-KEY header, and your plan must include GraphQL. Typing the URL into the address bar sends no key, so it returns 401 instead of the explorer. To open it:
- Install a browser extension that adds custom request headers, and set
X-CSCAPI-KEY: YOUR_API_KEYforapi.countrystatecity.in. - Open
https://api.countrystatecity.in/v1/graphql. - In GraphiQL’s Headers panel, add
{"X-CSCAPI-KEY": "YOUR_API_KEY"}. GraphiQL sends its queries with the headers from this panel.
POST.
Loading the page and each request GraphiQL sends count toward your daily and monthly usage like any other request. If your key restricts allowed domains or IPs, GraphiQL’s requests are checked against those restrictions too.
Nested queries
Fields that return related entities —Country.states, State.cities, Region.subregions, Subregion.countries — resolve in the same HTTP request, so one round trip replaces a chain of REST calls. Each nested list returns a page (20 rows per parent unless you pass limit), and every parent row adds a lookup to the query budget, so keep the outer lists small:
{
countries(searchQuery: "United", limit: 2) {
name
states(limit: 10) {
name
cities(limit: 10) {
name
}
}
}
}
countries, 1 per country for states, 1 per state for cities) and 222 rows, well inside the budget.
Nested lookups are not merged into a single database query. The server still fetches each parent’s children separately; it runs those lookups a limited number at a time in parallel and reuses a result when the same parent and page appear twice in one request. So the cities of 20 different states still cost 20 lookups.
Fields by plan
Two plan settings apply, separately:- The GraphQL feature decides whether you can call
/v1/graphqlat all. Professional and Business include it, and a custom plan can too. Supporter, for example, has full field access over REST but no GraphQL. - Your data access level decides which fields come back, using the same per-field filtering as REST. Professional and Business have full access, so every field in the schema is returned. A custom plan can have a lower level.
null rather than an error, so one query can safely ask for fields you might not have:
| Type | Basic | Full adds |
|---|---|---|
Country | id, name, iso2, iso3, phonecode, capital, currency, native, emoji, latitude, longitude, region, region_id, subregion, subregion_id, timezones | numeric_code, currency_name, currency_symbol, tld, nationality, population, gdp, area_sq_km, postal_code_format, postal_code_regex, emojiU, translations, wikiDataId |
State | id, name, iso2, country_id, country_code, latitude, longitude, timezone | fips_code, iso3166_2, type, level, parent_id, native, population, translations, wikiDataId |
City | id, name, kind | state_id, state_code, country_id, country_code, latitude, longitude, timezone, population, type, level, parent_id, native, translations, wikiDataId |
Region | id, name | wikiDataId, translations |
Subregion | id, name, region_id | wikiDataId, translations |
translations (and, for countries, states and cities, wikiDataId).
Only fields defined in the GraphQL schema can be requested. REST’s localized_name and matched_locale are not in it, and there are no locale or include_translations arguments: asking for localized_name is a validation error, not null. To show a name in another language, select translations (a JSON-encoded string keyed by language code) and pick the language in your app, or use the REST locale parameter.
Available queries
| Query | Returns |
|---|---|
countries(searchQuery, limit, offset) | All countries |
country(ciso) | One country by numeric ID or ISO2 code |
states(country, searchQuery, limit, offset) | States in one country |
allStates(searchQuery, limit, offset) | All states globally |
state(country, iso) | One state by country and state code |
cities(country, state, searchQuery, limit, offset) | Cities in a country, optionally narrowed to one state |
city(id) | One city by numeric ID |
regions(searchQuery, limit, offset) | All regions |
region(id) | One region by numeric ID |
subregions(region, searchQuery, limit, offset) | Subregions in one region (numeric region ID) |
subregion(id) | One subregion by numeric ID |
country(ciso) takes a numeric ID passed as a string, or an ISO2 code. An ISO3 code such as "USA" passes validation but matches nothing, so the query returns null; convert it first with Lookup Country by ISO Code. The country argument of states, state, and cities takes an ISO2 code, and state(iso) and cities(state) take the state’s code (for example CA).
searchQuery (2 to 100 characters) keeps rows whose name (or, for countries, states, and cities, native name) contains it, ignoring case. A single-item query that matches nothing returns null with no error.
city(id) has no REST equivalent — it’s the one query only available through GraphQL, since REST only ever exposed cities scoped to a country or state.Limits
Each HTTP request to/v1/graphql counts as one request against your plan’s daily and monthly limits, however many lookups it runs, including a request rejected for exceeding the query budget.
Pagination
Every list field, at the root or nested, takeslimit and offset. They are arguments on the field inside your query, not URL query parameters.
| List field | Default limit | Maximum limit |
|---|---|---|
Root queries: countries, states, allStates, cities, regions, subregions | 1,000 | 1,000 |
Nested fields: Country.states, State.cities, Region.subregions, Subregion.countries | 20 per parent | 1,000 |
offset defaults to 0. Lists are sorted by name, then ID, so stepping offset by limit pages through the full list. A limit above 1,000, or a negative limit or offset, is rejected rather than capped: it fails that field with a BAD_REQUEST error (see Errors). limit: 0 returns an empty list.
Unlike the REST list endpoints, a GraphQL list never returns more than one page. Watch the nested default in particular: { country(ciso: "US") { states { name } } } returns only the first 20 states. Ask for more with states(limit: 100), or page with offset.
Query budget
Before running a query, the API estimates its cost from thelimit of every list field (the value you pass, or the default above), not from how many rows actually match:
- Lookups: every field that returns an object or a list of objects costs one lookup per parent row it runs under. Scalar fields such as
nameare free. - Rows: every list field adds its
limitmultiplied by the number of parent rows.
400 before any data is fetched:
400 - Query too expensive
{
"errors": [
{
"message": "Query is too expensive: its estimated cost exceeds the budget (1001 data lookups, 21000 rows; limits: 50 lookups, 25000 rows). Add or lower `limit:` arguments on list fields, remove a level of nesting, or split the request into several queries.",
"extensions": {
"code": "QUERY_TOO_EXPENSIVE",
"statusCode": 400,
"estimatedFetches": 1001,
"estimatedRows": 21000,
"maxFetches": 50,
"maxRows": 25000
}
}
]
}
{ countries { states { name } } }: countries defaults to 1,000 rows, so states would run 1,000 lookups. To stay within the budget, add or lower limit on the outer lists, drop a level of nesting, or split the work into several requests. For example, fetch a page of countries first, then request each country’s states and cities in a separate query, using offset for further pages.
Queries are also limited to 5 levels of nesting. That is exactly the deepest chain the schema allows (regions → subregions → countries → states → cities), so in practice the budget is the limit you will reach.
Errors
The HTTP status and body depend on where a request fails:| Where it fails | HTTP status | Body |
|---|---|---|
| Before GraphQL runs: for example a missing or invalid key, a plan without GraphQL, or a usage limit reached | 401, 403, 429 | The REST error envelope (status, message, details); see Errors & Rate Limits |
Query budget (QUERY_TOO_EXPENSIVE), or a document too large to analyze (QUERY_TOO_COMPLEX) | 400 | errors only, no data |
| Syntax error, or a field or argument that isn’t in the schema | 200, or 400 if you send Accept: application/graphql-response+json | errors only, no data |
| Inside a field: invalid argument, a plan feature the field needs, temporary overload | 200 | errors plus whatever data could be resolved |
extensions.code and extensions.statusCode: BAD_REQUEST (400) for an invalid argument such as a malformed country code or a limit over 1,000; FORBIDDEN (403) when your plan lacks a feature that field needs, with extensions.details naming the feature (standard Professional and Business plans include every feature these queries use); and API_ERROR for anything else. A 503 with GraphQL is busy. Please retry shortly. is temporary, so retry after a short wait.
The failed field becomes null. When that field is nullable (the single-item queries such as country, and the nested lists such as Country.states), the rest of data still comes back. This response is for { country(ciso: "US") { name states(limit: 5000) { name } } }:
200 - Partial data
{
"errors": [
{
"message": "limit must not exceed 1000.",
"extensions": {
"code": "BAD_REQUEST",
"statusCode": 400
}
}
],
"data": {
"country": {
"name": "United States",
"states": null
}
}
}
countries, states, cities, and the others) can’t be null, so an error in one of them makes all of data null.
Start querying
Compare API plans
Choose Professional or Business to add GraphQL to your API key.
Errors & Rate Limits
Handle the
401, 403, and 429 responses returned before a query runs.