Docs › IP API
The IP API is a read-only IP geolocation API. Every endpoint returns JSON, authenticates via a single bearer token, and accepts both IPv4 and IPv6 addresses. No SDK is required.
Overview
The IP API answers three questions about an IP address: where is it, who operates it, and
should you trust it? It's a deliberately small surface: a single
GET lookup endpoint that returns the
address's location (continent down to postal code, plus a display-ready
label), its network operator (ASN and organization), security
signals (Tor, proxy, VPN, datacenter flags), and its timezone. Give it an address in the
path or query string, or give it nothing at all and it looks up the address the request
came from.
- Three sources, one answer. Every lookup cross-references three independent IP data sources, with built-in fallback and redundancy between them, so an outage or blind spot in any one source never takes your lookups down with it.
-
Read-only. All endpoints are
GET. There is no POST/PUT/DELETE surface. -
JSON-only responses.
Content-Type: application/jsonon every response. Successful payloads sit under a top-leveldatakey with the lookup record indata.objects; failures come back undererrorsas an array of objects carrying amessage. -
One shape, every time. Every response carries the identical
structure: every block and key, on every lookup. A value the data can't name for a
given address comes back
null— there are no optional keys to feature-detect. -
Bring an IP, or don't. With no address supplied, the caller's own
address is looked up, so
/ip/v1alone is a "what's my IP" and a geolocation call in one. - IPv4 & IPv6. Both address families are accepted everywhere an IP is: the path slug, the query string, or the caller's own connection.
Try it now
Two ways to try the IP API right now. Neither requires an account.
1. Fire a live request from this page. Click the button below. The response slides open in the Explorer panel.
// Demo key: no account required.
const response = await fetch(
'https://api.restcountries.com/ip/v1/216.209.248.188',
{ headers: { 'Authorization': 'Bearer rc_live_demo' } }
);
const data = await response.json();
2. Run it from your terminal.
Click the copy button below, then paste anywhere
curl lives.
curl "https://api.restcountries.com/ip/v1/216.209.248.188?pretty=1" \
-H "Authorization: Bearer rc_live_demo"
The demo key returns a real, correctly-shaped sample record plus a
data._demo notice block; it never touches your account or
quota.
Quick start
Sign up to get an API key, then paste this into your terminal (swap out the API key placeholder):
curl "https://api.restcountries.com/ip/v1/216.209.248.188?pretty=1" \
-H "Authorization: Bearer YOUR_API_KEY"
You'll get a single-object data.objects array back with the
full geolocation record. Drop the 216.209.248.188 segment to look up
your own address instead. From there, browse the Lookup section for
the full surface, or the Response objects reference to see
every field.
Base URL & versioning
IP API endpoints live under:
https://api.restcountries.com/ip/v1
The v1 path segment is the API version. It's the current, stable
version; backwards-compatible additions (new fields, new enrichments) ship inside it without
a bump. See Versioning.
Append ?pretty to any request to pretty-print the JSON response,
which is handy while exploring from a browser or terminal.
Business-plan customers also get a dedicated EU-region endpoint at a separate
hostname (eu-west-1) for in-region routing.
Lookup
Returns the geolocation record for one IP address. The address to look up is resolved
in a fixed precedence order: the path slug
(/ip/v1/216.209.248.188) wins, then the
ip query parameter
(/ip/v1?ip=216.209.248.188), and when neither is supplied, the
address the request itself came from. A value that isn't a syntactically valid IPv4 or
IPv6 address is rejected with a 400 before any lookup
runs. An address that is valid but can't be located, because it's private or reserved
(e.g. 192.168.1.1, 127.0.0.1),
also resolves to a 400, with the reason in the message.
Path examples
Query parameters
ip
string
response_fields
string
response_fields=ip,location.label returns an object with
exactly those two keys). Comma-separated dot-paths; ordering doesn't matter. Omit the
parameter to return every field.
response_fields_omit
string
response_fields_omit=timezone returns the full object minus
that branch). Comma-separated dot-paths; ordering doesn't matter. Composes with
response_fields: when both are present, the response is
first projected to response_fields, then
response_fields_omit prunes from that subset (so the omit
list wins on conflict).
Useful for trimming heavy nested branches like
location.country.flag without listing every other field
you want.
pretty
boolean
?pretty) and truthy values
(true, 1)
enable; anything else (including
false and 0)
leaves the response compact.
Example
curl "https://api.restcountries.com/ip/v1/135.23.185.84?pretty=1" \
-H "Authorization: Bearer YOUR_API_KEY"
const response = await fetch(
'https://api.restcountries.com/ip/v1/135.23.185.84',
{ headers: { 'Authorization': 'Bearer YOUR_API_KEY' } }
);
const data = await response.json();
fetch(
'https://api.restcountries.com/ip/v1/135.23.185.84',
{ headers: { 'Authorization': 'Bearer YOUR_API_KEY' } }
)
.then(function (response) { return response.json(); })
.then(function (data) { console.log(data); });
import requests
response = requests.get(
'https://api.restcountries.com/ip/v1/135.23.185.84',
headers={'Authorization': 'Bearer YOUR_API_KEY'}
)
data = response.json()
$ch = curl_init('https://api.restcountries.com/ip/v1/135.23.185.84');
curl_setopt($ch, CURLOPT_HTTPHEADER, ['Authorization: Bearer YOUR_API_KEY']);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$data = json_decode(curl_exec($ch), true);
require 'net/http'
require 'json'
uri = URI('https://api.restcountries.com/ip/v1/135.23.185.84')
request = Net::HTTP::Get.new(uri)
request['Authorization'] = 'Bearer YOUR_API_KEY'
response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) do |http|
http.request(request)
end
data = JSON.parse(response.body)
package main
import (
"encoding/json"
"io"
"net/http"
)
req, _ := http.NewRequest("GET", "https://api.restcountries.com/ip/v1/135.23.185.84", nil)
req.Header.Set("Authorization", "Bearer YOUR_API_KEY")
resp, _ := http.DefaultClient.Do(req)
defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)
var data map[string]interface{}
json.Unmarshal(body, &data)
use reqwest::blocking::Client;
use serde_json::Value;
let client = Client::new();
let response = client
.get("https://api.restcountries.com/ip/v1/135.23.185.84")
.header("Authorization", "Bearer YOUR_API_KEY")
.send()?;
let data: Value = response.json()?;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
HttpClient client = HttpClient.newHttpClient();
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("https://api.restcountries.com/ip/v1/135.23.185.84"))
.header("Authorization", "Bearer YOUR_API_KEY")
.GET()
.build();
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
String body = response.body();
using System.Net.Http;
using System.Net.Http.Json;
using System.Text.Json;
var client = new HttpClient();
client.DefaultRequestHeaders.Add("Authorization", "Bearer YOUR_API_KEY");
var data = await client.GetFromJsonAsync<JsonElement>(
"https://api.restcountries.com/ip/v1/135.23.185.84");
import Foundation
var request = URLRequest(url: URL(string: "https://api.restcountries.com/ip/v1/135.23.185.84")!)
request.addValue("Bearer YOUR_API_KEY", forHTTPHeaderField: "Authorization")
let (data, _) = try await URLSession.shared.data(for: request)
let json = try JSONSerialization.jsonObject(with: data)
{
"data": {
"objects": [
{
"ip": "135.23.185.84",
"type": "IPv4",
"location": {
"label": "Toronto, Ontario",
"continent": {
"name": "North America",
"codes": { "common": "NA" }
},
"country": {
"name": "Canada",
"codes": { "alpha_2": "CA" },
"flag": {
"emoji": "🇨🇦",
"html_entity": "🇨🇦",
"url_png": "https://flags.restcountries.com/v5/w640/ca.png",
"url_svg": "https://flags.restcountries.com/v5/svg/ca.svg"
},
"attributes": { "is_eu": false }
},
"subdivision": {
"name": "Ontario",
"codes": { "common": "ON", "iso_3166_2": "CA-ON" }
},
"city": { "name": "Toronto" },
"coordinates": { "lat": 43.64, "lng": -79.433 },
"postal": { "code": "M6K" }
},
"connection": {
"asn": { "number": 5645 },
"organization": { "name": "TekSavvy Solutions Inc." },
"domain": "teksavvy.com"
},
"security": {
"tor": false,
"proxy": false,
"vpn": null,
"anonymous": false,
"data_center": false
},
"timezone": {
"id": "America/Toronto",
"current": {
"abbreviation": "EDT",
"offset": { "seconds": -14400, "iso_8601": "-04:00" },
"attributes": { "is_dst": true }
}
}
}
]
}
}
data.objects always holds exactly one record — the resolved
address — in the full shape above: every block and key ships on every lookup, and a value
the data can't name for a given address comes back null. See
Response objects for every field.
Response objects
Every lookup answers with a single record in data.objects,
and every record carries the same complete shape: every block and key
below is present on every response, with null marking a value
the data can't name for that address. There are no optional keys to feature-detect. Codes
are always uppercase on output, and each codes key names its
provenance: alpha_2 and
iso_3166_2 are ISO standards,
common is the industry-conventional form.
Top level
ip
string
ip parameter, or the caller's own address, whichever
the request resolved to.
type
string
IPv4 or
IPv6.
location
label
string
Toronto, Ontario,
Paris, France). Degrades to broader names when
finer data is unknown (e.g. United States), and is
null only when nothing at all is nameable — so it's
always safe to render.
continent
object
{ name, codes.common }
(e.g. North America,
NA). common because
two-letter continent codes are industry convention, not an ISO standard.
country
object
{ name, codes.alpha_2, flag, attributes.is_eu }.
flag carries emoji,
html_entity, url_png and
url_svg (CDN-hosted assets, identical to the
Countries API's flag block).
attributes.is_eu is true for EU member states, false
otherwise, null when unknown.
subdivision
object
{ name, codes.common, codes.iso_3166_2 }.
common is the local short form (e.g.
ON, ENG);
iso_3166_2 is the full ISO 3166-2 code (e.g.
CA-ON, GB-ENG).
city
object
{ name } (e.g.
Toronto).
coordinates
object
{ lat, lng }: approximate floats, rounded
to at most five decimal places (roughly one-meter precision).
postal
object
{ code }: the postal or ZIP code for the location
(e.g. M6K).
connection
asn
object
{ number }: the autonomous system number announcing
the address, as an integer (e.g. 5645).
organization
object
{ name }: the network operator's name (e.g.
TekSavvy Solutions Inc.).
domain
string
teksavvy.com).
security
Reputation flags for the address. Each is true,
false, or null — null meaning
the data can't say, not that the answer is no, so choose your own default when
gating on these.
tor
boolean
proxy
boolean
vpn
boolean
anonymous
boolean
data_center
boolean
timezone
id
string
America/Toronto) — the stable identifier, accepted
directly by standard timezone libraries.
current
object
id because they change with daylight saving:
abbreviation (e.g.
EDT),
offset.seconds (e.g.
-14400),
offset.iso_8601 (e.g.
-04:00), and
attributes.is_dst.
Errors
Error responses use standard HTTP status codes plus a JSON body that names what went wrong.
Each entry in the errors array includes a
message describing the failure.
{
"errors": [
{
"message": "The value \"not-an-ip\" is not a valid IPv4 or IPv6 address."
}
]
}
Most IP-specific failures are 400s raised while validating
the request, each with a plain-language message:
- The resolved value isn't a syntactically valid IPv4 or IPv6 address.
- The address is valid but can't be located: it's private, reserved, or otherwise
unroutable (e.g.
192.168.1.1,127.0.0.1). The message carries the reason.
| Status | Meaning |
|---|---|
200 | OK. |
400 | Malformed request: an invalid or unlocatable IP. |
401 | Missing, unrecognized, revoked, or
expired API key, across both Authorization header and ?api-key=
query-param forms. |
403 | Forbidden. Returned when the account is frozen (e.g. monthly request limit exceeded). Frozen accounts clear on the monthly billing anniversary or when you upgrade. |
404 | Path doesn't match a known endpoint. |
405 | HTTP method not allowed. IP
endpoints are GET-only. |
410 | API version is no longer active. |
429 | Too Many Requests. Returned by the edge rate limiter when sustained traffic exceeds 20 requests per 10 seconds, and also when IP geolocation itself is temporarily rate limited; either way, retry shortly. |
500 | Unhandled server error. |
502 | The IP geolocation service is temporarily unavailable and the lookup couldn't be completed. Retry shortly. |
Data sources
Lookups are resolved live on every request, so a repeat query always reflects the latest available data for that address; there's no refresh cadence to think about. Coverage spans both IPv4 and IPv6 and includes city-level location, network operator details (ASN, organization), security reputation flags, and timezone. Country enrichments and flag assets come from the same data behind the Countries API, so codes, records, and flag URLs line up across the two APIs.
Gotchas
CORS & browser origins
Calling this API from frontend JavaScript is blocked until the page's
hostname is listed on the API key's allowed origins, so a key that works
from your server will fail in the browser until you add it. Requests that
carry no Origin header (server-to-server calls,
curl) are never affected. Add hostnames on the
API Keys page, and see
Browser usage & CORS for the
matching rules.