REST Countries

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/json on every response. Successful payloads sit under a top-level data key with the lookup record in data.objects; failures come back under errors as an array of objects carrying a message.
  • 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/v1 alone 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();

Run this request →

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

GET /ip/v1/{ip}

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

Look up a specific IP
GET /ip/v1/216.73.216.151 Run →
IPv6 example
GET /ip/v1/2001:4860:4860::8888 Run →
IP as query parameter
GET /ip/v1?ip=1.1.1.1 Run →
Your own IP (no address supplied)
GET /ip/v1 Run →

Query parameters

ip string
IP address to look up when it isn't given as the path slug; the path slug takes precedence when both are present. Omit it (and the path slug) to look up the caller's own address.
response_fields string
Allowlist of properties to include in the returned object. When set, the response carries only those properties and nothing else (e.g. 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
Blocklist of properties to drop from the returned object (e.g. 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-print the JSON response. The bare flag (?pretty) and truthy values (true, 1) enable; anything else (including false and 0) leaves the response compact.

Example

Request
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)
Response
{
  "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
The address that was looked up: the path slug, the ip parameter, or the caller's own address, whichever the request resolved to.
type string
The address family: IPv4 or IPv6.

location

label string
Display-ready place summary (e.g. 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
The first-level subdivision (state, province, or — for the UK — constituent country): { 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
The operator's primary domain (e.g. 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
The address is a known Tor exit node.
proxy boolean
The address is a known open or commercial proxy.
vpn boolean
The address is a known VPN egress point.
anonymous boolean
The address belongs to an anonymizing service of any kind (Tor, proxy, VPN, or similar).
data_center boolean
The address sits in a hosting or datacenter range rather than consumer space.

timezone

id string
The IANA timezone name (e.g. America/Toronto) — the stable identifier, accepted directly by standard timezone libraries.
current object
The moment-of-request observations, kept apart from 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.
StatusMeaning
200OK.
400Malformed request: an invalid or unlocatable IP.
401Missing, unrecognized, revoked, or expired API key, across both Authorization header and ?api-key= query-param forms.
403Forbidden. Returned when the account is frozen (e.g. monthly request limit exceeded). Frozen accounts clear on the monthly billing anniversary or when you upgrade.
404Path doesn't match a known endpoint.
405HTTP method not allowed. IP endpoints are GET-only.
410API version is no longer active.
429Too 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.
500Unhandled server error.
502The 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.

Clean to use. The data you receive is yours to use directly in your own product; there's no upstream attribution chain to maintain. (Use of the REST Countries API itself is governed by the Terms.)

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.