API documentation
Base URL: https://api.ipatlas.dev. All responses are JSON. CORS is enabled for all origins, so you can call the API from the browser.
Endpoints
GET /v1/me | Lookup of the caller's IP address. |
GET /v1/{ip} | Lookup of any IPv4 or IPv6 address. |
POST /v1/signup | Body {"email":"[email protected]"}. Returns a free API key, shown once. |
GET /health | Service status and data build date. |
Example
curl https://api.ipatlas.dev/v1/8.8.8.8
{
"ip": "8.8.8.8",
"continent": "NA",
"country_code": "US",
"country": "United States",
"is_eu": false,
"region": "California",
"city": "Mountain View",
"latitude": 37.422,
"longitude": -122.085,
"timezone": "America/Los_Angeles",
"asn": { "number": 15169, "organization": "Google LLC" },
"attribution": "IP geolocation data by DB-IP.com (CC BY 4.0, https://db-ip.com). Served by ipatlas.dev"
}
Fields can be missing when the database has no value. Private and reserved addresses (for example 192.168.0.1) return {"ip": "...", "reserved": true}. Addresses without data return 404. The timezone is derived from the coordinates and is approximate.
Authentication
A key is optional. Send it in any of these ways:
curl -H "X-API-Key: ipa_..." https://api.ipatlas.dev/v1/me
curl -H "Authorization: Bearer ipa_..." https://api.ipatlas.dev/v1/me
curl "https://api.ipatlas.dev/v1/me?key=ipa_..."
Prefer a header: query strings tend to end up in logs. Do not put your key in public front-end code you cannot rotate; use the keyless tier from browsers.
Limits
| Keyless | Free key | |
|---|---|---|
| Quota | 1,000 requests / day / IP address | 50,000 requests / month (UTC calendar month) |
| Rate | 10 req/s | 50 req/s |
Going over returns 429 with a JSON error; the per-second limit also sends Retry-After: 1. Responses carry X-RateLimit-Limit and X-RateLimit-Remaining (per day without a key, per month with a key). Errors: 400 invalid IP, 401 unknown key, 404 no data.
Code examples
Each example has a keyless call and a keyed call, retries once when the per-second limit is hit (429 with a Retry-After header), and raises an error that includes the API's message for everything else. A 429 without Retry-After means the daily (keyless) or monthly (keyed) quota is used up: retrying will not help. Pass only addresses you have validated, and keep keys on the server. /v1/me returns the location of the machine that calls it, so from a server use /v1/{ip} with the visitor's address.
curl
# keyless
curl https://api.ipatlas.dev/v1/8.8.8.8
# with a key; -i shows the headers, --fail-with-body exits non-zero on 4xx/5xx (curl 7.76+)
curl -i --fail-with-body -H "X-API-Key: $IPATLAS_KEY" https://api.ipatlas.dev/v1/8.8.8.8
# 429 handling: --retry 1 retries once and honours Retry-After (per-second limit)
curl --retry 1 https://api.ipatlas.dev/v1/8.8.8.8
JavaScript (browser and Node 18+)
async function lookup(ip = "me", key) {
for (let attempt = 0; attempt < 2; attempt++) {
const r = await fetch(`https://api.ipatlas.dev/v1/${encodeURIComponent(ip)}`, {
headers: key ? { "X-API-Key": key } : {},
});
const wait = Number(r.headers.get("Retry-After"));
if (r.status === 429 && attempt === 0 && wait > 0) { // per-second limit: wait, retry once
await new Promise(done => setTimeout(done, wait * 1000));
continue;
}
const body = await r.json().catch(() => ({}));
if (!r.ok) throw new Error(`ipatlas ${r.status}: ${body.error ?? "error"}`); // daily/monthly quota 429 lands here
return body;
}
}
// keyless (fine in a browser)
console.log((await lookup("8.8.8.8")).country_code);
// keyed (Node only: never ship a key in front-end code)
console.log((await lookup("8.8.8.8", process.env.IPATLAS_KEY)).city);
Python (requests)
import os, time
from urllib.parse import quote
import requests
def lookup(ip="me", key=None):
headers = {"X-API-Key": key} if key else {}
for attempt in range(2):
r = requests.get("https://api.ipatlas.dev/v1/" + quote(ip, safe=""), headers=headers, timeout=5)
if r.status_code == 429 and attempt == 0 and r.headers.get("Retry-After"):
time.sleep(float(r.headers["Retry-After"])) # per-second limit: wait, retry once
continue
if r.ok:
return r.json()
try:
msg = r.json().get("error")
except ValueError:
msg = r.reason
raise RuntimeError(f"ipatlas {r.status_code}: {msg}") # daily/monthly quota 429 lands here
print(lookup("8.8.8.8")["country_code"]) # keyless
print(lookup("8.8.8.8", os.environ.get("IPATLAS_KEY"))["city"]) # keyed
Go
package main
import (
"encoding/json"
"fmt"
"net/http"
"net/url"
"os"
"strconv"
"time"
)
type Geo struct {
IP string `json:"ip"`
CountryCode string `json:"country_code"`
City string `json:"city"`
Timezone string `json:"timezone"`
ASN struct {
Number int `json:"number"`
Organization string `json:"organization"`
} `json:"asn"`
}
var client = &http.Client{Timeout: 5 * time.Second}
func lookup(ip, key string) (*Geo, error) {
for attempt := 0; ; attempt++ {
req, err := http.NewRequest("GET", "https://api.ipatlas.dev/v1/"+url.PathEscape(ip), nil)
if err != nil {
return nil, err
}
if key != "" {
req.Header.Set("X-API-Key", key)
}
resp, err := client.Do(req)
if err != nil {
return nil, err
}
secs, _ := strconv.Atoi(resp.Header.Get("Retry-After"))
if resp.StatusCode == 429 && attempt == 0 && secs > 0 { // per-second limit: wait, retry once
resp.Body.Close()
time.Sleep(time.Duration(secs) * time.Second)
continue
}
defer resp.Body.Close()
if resp.StatusCode != 200 { // daily/monthly quota 429 lands here
var e struct{ Error string }
json.NewDecoder(resp.Body).Decode(&e)
return nil, fmt.Errorf("ipatlas %d: %s", resp.StatusCode, e.Error)
}
var g Geo
return &g, json.NewDecoder(resp.Body).Decode(&g)
}
}
func main() {
g, err := lookup("8.8.8.8", "") // keyless
if err != nil {
fmt.Println(err)
return
}
fmt.Println(g.CountryCode, g.City)
if k := os.Getenv("IPATLAS_KEY"); k != "" { // keyed
g, err = lookup("8.8.8.8", k)
fmt.Println(g, err)
}
}
PHP (cURL extension)
<?php
function lookup(string $ip = "me", ?string $key = null): array {
for ($attempt = 0; $attempt < 2; $attempt++) {
$retry = 0;
$ch = curl_init("https://api.ipatlas.dev/v1/" . rawurlencode($ip));
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 5,
CURLOPT_HTTPHEADER => $key ? ["X-API-Key: $key"] : [],
CURLOPT_HEADERFUNCTION => function ($c, $line) use (&$retry) {
if (stripos($line, "retry-after:") === 0) $retry = (int) trim(substr($line, 12));
return strlen($line);
},
]);
$body = curl_exec($ch);
$code = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);
if ($body === false) throw new RuntimeException("ipatlas unreachable");
if ($code === 429 && $attempt === 0 && $retry > 0) { sleep($retry); continue; } // per-second limit
$data = json_decode($body, true) ?? [];
if ($code !== 200) throw new RuntimeException("ipatlas $code: " . ($data["error"] ?? "error"));
return $data; // a daily/monthly quota 429 throws above
}
}
echo lookup("8.8.8.8")["country_code"]; // keyless
echo lookup("8.8.8.8", getenv("IPATLAS_KEY") ?: null)["city"]; // keyed
Ruby (net/http)
require "net/http"
require "json"
require "erb"
def lookup(ip = "me", key = nil)
uri = URI("https://api.ipatlas.dev/v1/#{ERB::Util.url_encode(ip)}")
2.times do |attempt|
req = Net::HTTP::Get.new(uri)
req["X-API-Key"] = key if key
res = Net::HTTP.start(uri.host, uri.port, use_ssl: true, open_timeout: 5, read_timeout: 5) { |http| http.request(req) }
wait = res["Retry-After"].to_i
if res.code == "429" && attempt == 0 && wait > 0 # per-second limit: wait, retry once
sleep wait
next
end
data = (JSON.parse(res.body) rescue {})
raise "ipatlas #{res.code}: #{data["error"]}" unless res.code == "200" # daily/monthly quota 429 lands here
return data
end
end
puts lookup("8.8.8.8")["country_code"] # keyless
puts lookup("8.8.8.8", ENV["IPATLAS_KEY"])["city"] # keyed
Guides
- Show the visitor's country and city in JavaScript: browser only, no key
- Redirect or set the locale by country in Next.js middleware
- Server-side country lookup with caching (Express or nginx)
- IP lookup in a Cloudflare Worker
- Get a visitor's country from an IP address in Python
- Get a visitor's country from an IP address in PHP
- IP geolocation middleware for Node.js and Express
Attribution (required on the free plan)
If you show or use the data in a public product, link to DB-IP, for example in a footer or about page:
<a href="https://db-ip.com">IP Geolocation by DB-IP</a>
The data is the DB-IP City Lite and ASN Lite databases, licensed CC BY 4.0, updated monthly. We do not modify the underlying records, except that the timezone is added from coordinates.
Accuracy
IP geolocation is approximate. Country is usually right; city is often off by tens of kilometres, and mobile, VPN and corporate networks can be wrong. Do not use it for anything that needs certainty.