API reference
One endpoint, five output formats, no SDK. Everything on this page describes what the API does today.
Quickstart
Look up an address with a GET request. Sections we know nothing about are left out of the body rather than returned empty, so check for a key before reading into it.
curl 'https://ip-api.dev/api?q=8.8.8.8&pretty=1&key=YOUR_KEY'Endpoint and parameters
GET https://ip-api.dev/api
| Field | Type | Description |
|---|---|---|
| q | string, required | The IPv4 address, IPv6 address, or AS number to look up. AS numbers are written as AS13335 or as13335. A CIDR mask is rejected — the endpoint looks up single addresses. |
| key | string, optional | Your API key. Without it the request is counted against the keyless daily quota and answered with the reduced response body. |
| pretty | 1, optional | Set to 1 to indent JSON with two spaces. Ignored by the csv, text, html and toon formats. |
| format | json | csv | text | html | toon, optional | Explicit response format for clients that cannot conveniently set Accept. Overrides the Accept header. Omit both to receive JSON. |
Output formats
Request the canonical /api endpoint and select its representation with the HTTP Accept header. If setting headers is inconvenient, use format; it takes precedence over Accept. With neither, the response is JSON. The format-specific paths remain convenient fixed aliases.
| Alias | Content-Type | Notes |
|---|---|---|
| /api | application/json | The default. Nested objects are preserved. |
| /api/csv | text/csv | One header row plus one row per lookup. Nested keys are flattened with dots. |
| /api/text | text/plain | One key=value line per field. Bulk results are separated by a blank line. |
| /api/html | text/html | A plain two-column table per lookup. |
| /api/toon | text/toon | Lossless, typed TOON v4 for token-efficient LLM and agent input. |
CSV, text and HTML flatten nested fields into dotted keys such as location.country, and arrays are joined with spaces. TOON preserves the JSON structure and primitive types; uniform bulk arrays use its compact tabular form.
curl 'https://ip-api.dev/api?q=8.8.8.8' -H 'Accept: text/toon'curl 'https://ip-api.dev/api?q=8.8.8.8&format=toon'ip,is_bogon,is_datacenter,is_anycast,is_tor,is_proxy,is_vpn,is_abuser,is_mobile,is_satellite,is_crawler,organization.name,asn.number,asn.name,location.country_code,location.latitude,location.longitude,elapsed_ms
8.8.8.8,false,true,false,false,false,false,false,false,false,false,Google LLC,15169,Google LLC,US,,,60.643ip: 8.8.8.8
network:
cidr: 8.8.8.0/24
rir: arin
asn:
number: 15169
name: Google LLC
services: []
location:
country_code: US
precision: region
is_vpn: false
elapsed_ms: 1.234Keys and rate limits
Pass your key as the key query parameter. There is no header-based authentication.
- No key: 100 requests per UTC day, answered with the reduced response body described below. IPv4 callers are counted per address. IPv6 callers are counted per prefix, because a single subscriber holds many addresses: 100 per /64, 200 per /56, and 300 per /48.
- Free key: 1,000 requests per day and the full response body.
- Paid plans raise the monthly allowance — see pricing.
- The counter resets at 00:00 UTC. Going over returns
429.
Bulk lookups
POST to the same path with a JSON body to look up between 1 and 100 addresses at once. Bulk requires a key, always returns the full response body, and answers with an array in the order you sent.
curl -X POST 'https://ip-api.dev/api?key=YOUR_KEY&pretty=1' \
-H 'Content-Type: application/json' \
-d '{"ips": ["1.1.1.1", "8.8.8.8", "9.9.9.9"]}'[
{ "ip": "1.1.1.1", "network": { "rir": "apnic", ... }, "asn": { ... }, "elapsed_ms": 61.2 },
{ "ip": "8.8.8.8", "network": { "rir": "arin", ... }, "asn": { ... }, "elapsed_ms": 58.9 },
{ "ip": "not-an-ip", "error": "Invalid IP Address or AS Number", "elapsed_ms": 0.01 }
]Entries that are not valid addresses come back as an object with an error field instead of failing the whole request. AS numbers are not accepted in bulk bodies.
Response fields
These are the fields in the full response body, returned when you send a key.
Top level
| Field | Type | Description |
|---|---|---|
| ip | string | The address you asked about, echoed back as you sent it. |
| hostname | string | The reverse DNS hostname for the address. Omitted when we have no PTR record for it. |
| services | array | VPN, proxy and platform-egress matches. Always an array; empty when none are known. |
| is_bogon | boolean | True for addresses that should never appear on the public internet: private, loopback, link-local, carrier-grade NAT, documentation, multicast and reserved space. Computed from the address itself. |
| is_datacenter | boolean | True when the datacenter section is present, meaning a published cloud range or a hosting-classified AS covers the address. |
| is_anycast | boolean | True when measurement data shows the prefix is announced from multiple physical sites. |
| is_tor | boolean | True when the address is a known Tor exit node. |
| is_proxy | boolean | True when the address belongs to a platform egress network such as iCloud Private Relay or Cloudflare WARP, or is otherwise flagged as a proxy. |
| is_vpn | boolean | True when the address is an exit node for a commercial VPN service. |
| is_abuser | boolean | True when a network covering the address appears on an abuse blocklist we ingest. |
| is_mobile | boolean | True when the AS originating the route for this address is classified as a mobile carrier. |
| is_satellite | boolean | True when the originating AS is classified as a satellite provider. |
| is_crawler | boolean | True when a verified crawler range covers the address. The crawler object carries its name. |
| crawler | object | Verified crawler details. Present only on a match; crawler.name is the bot name. |
| elapsed_ms | number | How long the lookup took on our side, in milliseconds, to three decimal places. Present on every response, including errors. |
asn — routing
Omitted when no BGP route covers the address.
| Field | Type | Description |
|---|---|---|
| asn.number | number | The AS number. |
| asn.name | string | null | The organization that holds the AS number. |
| asn.domain | string | null | The AS organization's domain when a source publishes it. |
| asn.country_code | string | null | Two-letter country code registered for the organization. |
| asn.route | string | The most specific BGP prefix covering the address that this AS originates. |
| asn.rir | string | null | The registry the AS number was assigned by. |
| asn.type | string | null | Business category such as hosting, isp, education or government. |
| asn.active | boolean | True when the AS currently originates at least one prefix. |
| asn.updated_at | string | null | Date the source ASN organization record last changed. |
| asn.abuse_score | object | Fraction of announced addresses on an abuse list as numeric score (0–1) and low/medium/high level. |
network — registry assignment
The most-specific whois network, or the broad RIR allocation as a fallback. Omitted only when neither is known.
| Field | Type | Description |
|---|---|---|
| network.cidr | string | Most-specific registered network, in CIDR notation. |
| network.name | string | null | Registry network name (netname), kept distinct from the organization. |
| network.type | string | null | Registry allocation or assignment type. |
| network.rir | string | null | Lowercase regional registry code. |
| network.country_code | string | null | Country code on the registry network record. |
| network.rdap_url | string | null | Public RDAP URL for the registered network. |
| network.abuse_score | object | null | Numeric score and level for this CIDR. Null only when network is a broad allocation fallback with no scored whois record. |
organization — registered operator
Omitted when the registry network names no organization or domain.
| Field | Type | Description |
|---|---|---|
| organization.name | string | null | Organization named on the registered network. |
| organization.domain | string | null | The organization's domain, when the registry publishes one. |
abuse_contact
Omitted when the registry publishes no abuse email.
| Field | Type | Description |
|---|---|---|
| abuse_contact.name | string | null | Name of the abuse contact or team. |
| abuse_contact.address | string | null | Postal address on the abuse record. |
| abuse_contact.country_code | string | null | The abuse contact's country, falling back to the network's country. |
| abuse_contact.email | string | The abuse email address. The section is omitted entirely when the registry publishes no email. |
| abuse_contact.phone | string | null | Phone number on the abuse record. |
datacenter — hosting
Omitted when the address is not in a published cloud range and its AS is not classified as hosting.
| Field | Type | Description |
|---|---|---|
| datacenter.provider | string | null | The provider name, such as Google or AWS. |
| datacenter.domain | string | null | The provider's domain. Null when the match came from AS classification rather than a published range. |
| datacenter.network | string | null | The covering network in CIDR notation. |
| datacenter.country_code | string | null | Country for the range. Null for matches from published cloud ranges, which do not carry one. |
| datacenter.region | string | null | Provider region, such as us-east4, when the range names one. |
| datacenter.city | null | Reserved. Always null today. |
services — VPN, proxy and platform egress
One stable array covers all relay services. The kind enum distinguishes commercial VPNs, scanned proxies and platform egress such as iCloud Private Relay or Cloudflare WARP.
| Field | Type | Description |
|---|---|---|
| services[].kind | vpn | proxy | platform_egress | What kind of relay service the match represents. |
| services[].provider | string | null | Service name, such as NordVPN or iCloud Private Relay. |
| services[].url | string | null | The service's website. |
| services[].role | string | null | Address role: exit_node, server or relay. |
| services[].confidence | string | How the match was established: enumerated, interpolated or assumed. |
| services[].last_seen | string | ISO 8601 timestamp of the last time we saw this range. |
| services[].exit.region | string | null | The region the service advertises for this exit. |
| services[].exit.country_code | string | null | Two-letter country code for the exit. |
| services[].exit.city | string | null | City the service advertises for this exit. |
| services[].exit.latitude | number | null | Latitude the service advertises for this exit. |
| services[].exit.longitude | number | null | Longitude the service advertises for this exit. |
location — geolocation
Omitted when we cannot determine a country for the address.
| Field | Type | Description |
|---|---|---|
| location.continent | string | null | Continent name, such as North America. |
| location.continent_code | string | null | Two-letter continent code, such as NA or EU. |
| location.country | string | null | Country name in English. |
| location.country_code | string | Two-letter ISO country code. The section is omitted entirely when we cannot determine a country. |
| location.state | string | null | The subdivision name, resolved from the code the geofeed published where possible. |
| location.city | string | null | City name as the geofeed published it. |
| location.latitude | number | null | Latitude from the geofeed, or the centre of the matched city or region. |
| location.longitude | number | null | Longitude, from the same source as latitude. |
| location.zip | string | null | Postal code, when the geofeed published one. |
| location.timezone | string | null | IANA time zone name, such as America/New_York. |
| location.local_time | string | null | Current local time in that zone, as an ISO 8601 string. |
| location.local_time_unix | number | null | The same instant as a Unix timestamp in seconds. |
| location.is_dst | boolean | null | Whether daylight saving time is in effect in that zone now. |
| location.utcoffset | string | null | Current offset from UTC, such as -04:00. |
| location.is_eu_member | boolean | Whether the country is a member of the European Union. |
| location.calling_code | string | null | International dialling code for the country. |
| location.currency_code | string | null | ISO 4217 currency code for the country. |
| location.precision | street | city | region | country | Semantic resolution of the result. This does not claim a radius we cannot substantiate. |
| location.basis | operator_geofeed | registry_allocation | Whether the country/place came from an operator-published geofeed or registry allocation. |
Keyless response
Requests without a key get a strict subset of the same canonical structure. Field names and nesting never change between plans.
| Field | Type | Description |
|---|---|---|
| ip | string | The address you asked about. |
| is_bogon | boolean | Same meaning as in the full response. |
| is_datacenter | boolean | Same as above. |
| is_anycast | boolean | Same as above. |
| is_tor | boolean | Same as above. |
| is_proxy | boolean | Same as above. |
| is_vpn | boolean | Same as above. |
| is_abuser | boolean | Same as above. |
| is_mobile | boolean | Same as above. |
| is_satellite | boolean | Same as above. |
| is_crawler | boolean | Same as above. |
| crawler.name | string | Present only for a verified crawler match. |
| organization.name | string | null | Present when a registered organization is known. |
| asn.number / asn.name | number / string | null | The canonical ASN identity subset. |
| location.country_code / latitude / longitude | string / number | null | The canonical geolocation subset. |
| elapsed_ms | number | Lookup time in milliseconds. |
AS number lookups
Pass an AS number as q to get the organization and every prefix the AS announces. The response shape is different from an address lookup.
{
"asn": {
"number": 13335,
"name": "Cloudflare, Inc.",
"domain": "cloudflare.com",
"country_code": "US",
"route": "1.0.0.0/24",
"rir": "arin",
"active": true
},
"prefixes": {
"ipv4": ["1.0.0.0/24", "1.1.1.0/24", "..."],
"ipv6": ["2400:cb00::/32", "..."]
},
"elapsed_ms": 412.8
}prefixes.ipv4 and prefixes.ipv6 hold the announcements. An AS we know nothing about returns a 400.
Errors
Errors use the selected response format and always carry error and elapsed_ms. A negotiation error has no selected representation, so its explanatory body is JSON.
| Status | Body | When |
|---|---|---|
| 400 | {"error":"Invalid IP Address or AS Number","elapsed_ms":0.015} | q is missing, is not a valid address or AS number, carries a CIDR mask, or names an AS we have no data for. |
| 400 | {"error":"Body must be {\"ips\": [...]} with 1 to 100 addresses","elapsed_ms":0.02} | A bulk POST body that is not valid JSON, or has no ips array, or has more than 100 entries. |
| 400 | {"error":"Invalid output format. Use json, csv, text, html, or toon","elapsed_ms":0.01} | format names something the API does not provide. |
| 401 | {"error":"An API key is required for bulk lookups","elapsed_ms":0.007} | A bulk POST without a key. |
| 429 | {"error":"Rate limit exceeded","limit":500,"elapsed_ms":8.1} | The keyless daily quota for your IP is used up. |
| 406 | {"error":"No acceptable output format is available","elapsed_ms":0.01} | Accept excludes every representation the API provides. |
CORS and caching
- Every response carries
Access-Control-Allow-Origin: *, so you can call the API straight from a browser. OPTIONSpreflight is answered with200and allowsGET, POST, OPTIONS.- Responses are sent with
Cache-Control: no-store. Cache on your side if you need to. - Negotiated
/apiresponses includeVary: Accept, so compliant caches keep representations separate.
Examples
const response = await fetch(
'https://ip-api.dev/api?q=8.8.8.8&key=YOUR_KEY',
)
const data = await response.json()
console.log(data.location?.country, data.asn?.name)
if (data.is_vpn || data.is_tor || data.is_abuser) {
console.log('flagged:', data.ip)
}import requests
response = requests.get(
"https://ip-api.dev/api",
params={"q": "8.8.8.8", "key": "YOUR_KEY"},
timeout=10,
)
response.raise_for_status()
data = response.json()
print(data["asn"]["name"], data["location"]["country"])Ready to go? Get a free key or browse the IP range explorer.