API Documentation

IP-Atlas gives you IP geolocation, ASN data, and datacenter detection through one REST endpoint. Every field is included on every plan — you pay for throughput, not fields.

Base URL: https://api.ip-atlas.io

Response format: JSON (Content-Type: application/json)

Source IP handling: CF-Connecting-IP, then X-Real-IP, then the first entry of X-Forwarded-For, then the TCP source address.

Authentication

Pass your API key in the X-API-Key header:

# Every paid endpoint; all /json/* also accept this.
curl https://api.ip-atlas.io/json/8.8.8.8 \
  -H "X-API-Key: ipa_live_YOUR_API_KEY_HERE"

The /json/* endpoints also accept unauthenticated requests (free anonymous tier, 60 req/min per IP). Endpoints under /v1/* always require a key.

Keys must be sent via the X-API-Key header. The legacy ?key=… query-string fallback is no longer accepted — query strings leak keys via access logs, browser history, and Referer headers.

Key permissions

Every API key carries three independent scopes. Lookups and MMDB are read-only (none or read); only Account has a write level (full). You choose them when you create the key on your dashboard keys page — pick a level per section, or use the “Agent key (read-only)” preset (sets every section to read), which is the recommended credential to hand an automated agent.

Sectionnonereadfull
Lookupsblockedall IP lookups — single (/json, /v1/json) and batch (/v1/batch)
MMDBblockeddownload (/v1/mmdb/*)— (no write exists)
Accountblockedview status/usage (GET /v1/account)read + change billing settings (POST /v1/account/settings) + rotate the key

A request that exceeds the key’s scope returns 403:

{ "error": "insufficient_scope", "required": "lookups:full" }

Keys created before scopes existed default to full access on every section, so nothing breaks. A read-only agent key can pull the MMDB and verify the account is active while being unable to spend money or change the account.

No key — at any scope, including account:full — can manage cards, cancel the subscription, or delete the account. Those actions live only behind a logged-in browser session, never the API.

Rate limits

PlanMonthlyDailyBurst
Free2,000(none)60 req/min anonymous
Plus ($19/mo)150,000(none)
Pro ($79/mo)1,000,000(none)
Business ($249/mo)10,000,000(none)
Pay-as-you-gounlimited (billed per use)(none)

Every successful response includes advisory headers:

HeaderMeaning
X-RateLimit-Limit-MonthMonthly quota (always present for authenticated requests)
X-RateLimit-Remaining-MonthRemaining requests this calendar month (always present for authenticated requests)
X-RateLimit-Limit-DayOnly present on legacy plans that have a daily cap. The current model uses monthly quotas only.
X-RateLimit-Remaining-DayOnly present on legacy plans that have a daily cap.
X-Overage-Charged-CentsUSD cents charged on this request (0 if within quota)
X-Spend-Cap-Reachedtrue when a 429 is caused by your spend cap

On 429 Too Many Requests the response also includes Retry-After with the seconds until the window resets.

Overage billing

Every plan (including Free) has Automatic Overage Billing enabled by default. Once you pass your monthly quota, requests keep succeeding and are billed transparently.

Every plan: $0.40 per 1,000 requests beyond the included quota. Auto-billed at month end via Stripe.

Accrued overage is billed on the 1st of the following month via a Stripe Invoice Item. You'll receive warning emails at 80% and 95% of your monthly quota. To limit exposure:

Errors

All errors return JSON of the form {"error": "<message>"}.

StatusCondition
400Malformed request (bad JSON, invalid IP, missing fields)
401Missing or revoked API key on endpoints that require one
404Unknown Stripe customer on /portal
405Method not allowed (e.g. GET on /v1/batch)
410Endpoint removed (e.g. legacy /signup — use /checkout)
429Quota exceeded, per-IP rate limit hit, or spend cap reached — see Retry-After
500Server error — transient, retry with backoff
502Upstream (Stripe) failure

GET/json/{ip}

Look up geolocation, ASN, organisation, datacenter and VPN detection for a specific IPv4 or IPv6 address.

curl https://api.ip-atlas.io/json/8.8.8.8
{
  "ip": "8.8.8.8",
  "country": "US",
  "country_name": "United States",
  "registry": "arin",
  "asn": 15169,
  "org": "GOOGLE",
  "is_datacenter": true,
  "is_vpn": false
}

GET/json

Look up the caller's own IP. Same response shape as /json/{ip}.

curl https://api.ip-atlas.io/json

POST/v1/batch

Look up up to 100 IPs in a single request. Requires an API key; each IP counts as one request against your quota.

curl -X POST https://api.ip-atlas.io/v1/batch \
  -H "X-API-Key: ipa_live_..." \
  -H "Content-Type: application/json" \
  -d '{"ips":["8.8.8.8","1.1.1.1","not-an-ip"]}'
{
  "results": [
    {"ip":"8.8.8.8", "country":"US", ...},
    {"ip":"1.1.1.1", "country":"AU", ...},
    {"ip":"not-an-ip", "error":"invalid IP address"}
  ]
}

Results are returned in the same order as the input list. Each element is either a full lookup result or a {"ip": "...", "error": "..."} record for per-IP failures. Request-level failures (invalid key, over-quota, spend cap) return a normal 4xx/5xx, not inside the results array.

The entire batch is preflighted against your quota before any lookup runs.

GET/v1/account

Returns plan metadata, live usage, and billing settings for the key presented. Does not count against your quota.

curl https://api.ip-atlas.io/v1/account -H "X-API-Key: ipa_live_..."
{
  "plan": "developer",
  "plan_display_name": "Plus",
  "key_prefix": "ipa_live_abc1",
  "email": "<your-email>",
  "status": "active",
  "created_at": "2026-04-22T18:14:03Z",
  "daily_used": 0,
  "monthly_used": 14823, "monthly_limit": 150000,
  "stripe_customer_id": "cus_...", "has_subscription": true,
  "overage_enabled": true,
  "overage_rate_cents_per_million": 40000,
  "overage_accrued_cents": 0,
  "spend_cap_cents": null
}

The plan field uses internal codes for API-stable identification: free, developer, startup, business, payg. The plan_display_name field gives the marketing name ("Free", "Plus", "Pro", "Business", "Pay-as-you-go", "Enterprise"). Build clients against plan — it's stable. Use plan_display_name for display copy.

POST/v1/account/rotate

⚠ Destructive operation. This immediately revokes your current API key. Any service still using the old key will start returning 401 within ~1 second. Save the new plaintext key from the response before you redeploy. If you want to test rotation without breaking production, do it in a staging key first.

Revokes the current key and issues a replacement with the same plan, email, and Stripe linkage. The new plaintext key is returned once.

curl -X POST https://api.ip-atlas.io/v1/account/rotate -H "X-API-Key: ipa_live_..."
{"api_key": "ipa_live_9f8e...", "key_prefix": "ipa_live_9f8e"}

POST/v1/account/settings

Update billing settings for your key. Fields not present are unchanged.

curl -X POST https://api.ip-atlas.io/v1/account/settings \
  -H "X-API-Key: ipa_live_..." \
  -H "Content-Type: application/json" \
  -d '{"overage_enabled": false, "spend_cap_cents": 2500}'

Pass "clear_spend_cap": true to remove an existing cap.

GET/health

Unauthenticated liveness check. No rate limit.

curl https://api.ip-atlas.io/health
{"ok": true, "ips_loaded": 255000, "asns_loaded": 519000} // Counts shown are approximate; the live response reflects the data set at refresh time.

POST/checkout

Creates a Stripe Checkout session. Use plan: "free" for a Setup-mode checkout (card authorised, no charge). Paid plans run in Subscription mode. Most users hit this through the pricing page.

curl -X POST https://api.ip-atlas.io/checkout \
  -H "Content-Type: application/json" \
  -d '{"plan":"developer","email":"<your-email>"}'

Billing portal

Manage your card, download invoices, or cancel your plan from your dashboard billing page. The portal is session-gated and is not exposed as a public API endpoint — opening it requires a logged-in browser session, not an API key.

Node.js SDK

The official Node.js SDK is available on npm as @trellisdigitalservices/ip-atlas.

npm install @trellisdigitalservices/ip-atlas
import { IPAtlas } from '@trellisdigitalservices/ip-atlas';

const client = new IPAtlas({ apiKey: 'ipa_live_...' });
const result = await client.lookup('8.8.8.8');
console.log(result.country, result.org);

Python SDK

The official Python SDK is on PyPI as ipatlas.

pip install ipatlas
from ipatlas import IPAtlas

client = IPAtlas(api_key="ipa_live_...")
result = client.lookup("8.8.8.8")
print(result["country"], result["org"])

# Batch (up to 100 IPs)
batch = client.lookup_batch(["8.8.8.8", "1.1.1.1"])

Without an API key the client uses the auth-less endpoints (rate-limited to 60 req/min per source IP). With a key, your monthly quota applies.

curl

All you need is your API key in the X-API-Key header.

# Single lookup (anonymous)
curl https://api.ip-atlas.io/json/8.8.8.8

# Single lookup with auth
curl https://api.ip-atlas.io/json/8.8.8.8 \
  -H "X-API-Key: ipa_live_..."

# Batch with auth
curl -X POST https://api.ip-atlas.io/v1/batch \
  -H "X-API-Key: ipa_live_..." \
  -H "Content-Type: application/json" \
  -d '{"ips":["8.8.8.8","1.1.1.1"]}'

JavaScript / fetch

// Single lookup
const res  = await fetch('https://api.ip-atlas.io/json/8.8.8.8', {
  headers: { 'X-API-Key': 'ipa_live_...' }
});
const data = await res.json();

// Batch lookup
const batch = await fetch('https://api.ip-atlas.io/v1/batch', {
  method: 'POST',
  headers: { 'X-API-Key': 'ipa_live_...', 'Content-Type': 'application/json' },
  body: JSON.stringify({ ips: ['8.8.8.8', '1.1.1.1'] })
}).then(r => r.json());

Python / requests

import requests

KEY = "ipa_live_..."
HEADERS = {"X-API-Key": KEY}

# Single lookup
data = requests.get("https://api.ip-atlas.io/json/8.8.8.8", headers=HEADERS).json()

# Batch lookup
batch = requests.post(
    "https://api.ip-atlas.io/v1/batch",
    headers=HEADERS,
    json={"ips": ["8.8.8.8", "1.1.1.1"]}
).json()

Go

req, _ := http.NewRequest("GET", "https://api.ip-atlas.io/json/8.8.8.8", nil)
req.Header.Set("X-API-Key", "ipa_live_...")
resp, err := http.DefaultClient.Do(req)
defer resp.Body.Close()
var result map[string]any
json.NewDecoder(resp.Body).Decode(&result)

PHP

$ch = curl_init('https://api.ip-atlas.io/json/8.8.8.8');
curl_setopt($ch, CURLOPT_HTTPHEADER, ['X-API-Key: ipa_live_...']);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$data = json_decode(curl_exec($ch), true);

Response fields

FieldTypeDescription
ipstringThe queried IP
countrystringISO 3166-1 alpha-2 country code
country_namestringFull English country name
continent_codestringTwo-letter continent code (NA, EU, AS, AF, OC, SA)
regionstringState or region name
citystringCity name
latitudefloatLatitude (city-level centroid)
longitudefloatLongitude (city-level centroid)
asnintegerAutonomous System Number
orgstringOrganization registered with the ASN
is_datacenterbooleanIP belongs to a known cloud / datacenter provider
is_vpnbooleanIP is associated with a known VPN or Tor exit service
is_proxybooleanIP is likely a proxy (VPN or non-trusted datacenter)
abuse_contactstringNetwork abuse contact email from RIR WHOIS
postalstringPostal / ZIP code (populated where source data has coverage)
timezonestringIANA timezone identifier (populated where source data has coverage)
country_codestringSame as country. Alias for migration compatibility with services that use this field name.
registrystringRegional Internet Registry name (arin, ripe, apnic, lacnic, afrinic, or empty for unrouted).

All 18 fields are included on every tier. No per-field upsells.

CORS & browser use

Do not call the IP-Atlas API directly from browser JavaScript with your API key. Anything in your client-side code can be read by anyone visiting the page — pasting your key into a fetch() call exposes it to harvesting and burns through your quota.

The right place to put the key is on a server you control: your own backend, a serverless function (Cloudflare Worker, Vercel Function, AWS Lambda), or an edge route. Your browser code calls your endpoint; your endpoint calls IP-Atlas with the key. That keeps the key off the wire that the browser sees.

Server-to-server is also faster: you can cache common lookups, batch requests, and avoid the CORS round-trip entirely.

If you have a one-off browser-only need (a static demo, a CodePen, a hackathon prototype), use the auth-less GET /json and GET /json/{ip} endpoints — they're rate-limited (60 req/min per IP) and don't require any key. They return the same shape, just without per-account quota accounting.

Access-Control-Allow-Origin: *
Access-Control-Allow-Methods: GET, POST, OPTIONS
Access-Control-Allow-Headers: Content-Type, X-API-Key

Preflight (OPTIONS) returns 204 No Content. The permissive CORS policy supports the auth-less demo use case above; it is not an endorsement of shipping API keys in browser code.

Data sources

Data is hot-swapped daily without a service restart (atomic directory swap + SIGHUP).