Overview
Base URL: https://ipwhois.net/api/v1. Every endpoint returns JSON with Content-Type: application/json. Add ?pretty to any request for indented output. All responses carry Access-Control-Allow-Origin: *, so the API can be called from browser code (restrict such keys to your websites, see Authentication).
| Endpoint | What it does | Plan | Counted as |
|---|---|---|---|
| GET /ip/{ip} | IP data: location, timezone, ASN, ISP and more | All | 1 IP lookup |
| GET /ip | The same for the IP the request comes from | All | 1 IP lookup |
| POST /ip/batch | Many IPs in one request | Paid | 1 per IP |
| GET /blacklist/{ip} | Blacklist status, threat type and confidence | All | 1 blacklist check |
| POST /blacklist/batch | Many IPs in one request | Paid | 1 per IP |
| GET /mac/{mac} | MAC vendor and address flags | All | 1 MAC lookup |
| GET /mac/search?q= | OUI prefixes registered to a company | Paid | 1 MAC lookup |
| GET /usage | Plan and quota of the key | All | Not counted |
A request to the base URL without a path returns this list as JSON.
Authentication
Every endpoint needs an API key. Create a free account and open the dashboard: the first key is created for you. Keys look like ipw_ followed by 40 hex characters. Send the key in one of three ways, checked in this order:
# Header (recommended)
curl https://ipwhois.net/api/v1/ip/8.8.8.8 -H "X-API-Key: YOUR_API_KEY"
# Bearer token
curl https://ipwhois.net/api/v1/ip/8.8.8.8 -H "Authorization: Bearer YOUR_API_KEY"
# Query string (for quick tests; keys end up in logs this way)
curl "https://ipwhois.net/api/v1/ip/8.8.8.8?key=YOUR_API_KEY"
A request without a key gets HTTP 401 missing_key; a malformed or revoked key gets 401 invalid_key.
Keys used in browser code
A key that ships in JavaScript is visible to everyone. On the dashboard, edit the key and list the websites it may be used from (example.com, *.example.com). The API then accepts the key only when the request's Origin or Referer is on one of those hosts; everything else gets HTTP 403 origin_not_allowed. Keys without a website list work from anywhere, so keep them on the server.
You can roll a key from the dashboard at any time: a new secret replaces the old one instantly, name and website list stay.
Plans and quotas
The free plan gives each API its own daily allowance and returns the basic field set. A paid plan opens every field of all three APIs and pools one monthly quota across them: every lookup counts as one, whichever API it goes to, and a batch of 50 addresses counts as 50. Prices are shown without VAT; full details on the pricing page.
| Plan | Price | Lookups | Rate limit | Batch | Keys | Fields |
|---|---|---|---|---|---|---|
| Free non-commercial |
€0 | 1,000 IP, 1,000 blacklist and 1,000 MAC a day | 2/s | no | 1 | basic |
| Starter | €9/mo | 250,000 a month, all APIs | 5/s | 100 | 3 | full |
| Pro | €29/mo | 1,500,000 a month, all APIs | 15/s | 100 | 5 | full |
| Business | €79/mo | 6,000,000 a month, all APIs | 30/s | 1,000 | 10 | full |
Free quotas reset at 00:00 UTC. Monthly quotas reset on the first day of the month, 00:00 UTC. An expired paid plan falls back to the free plan; the keys keep working.
Rate limits and quota headers
Each plan has a per-second limit (see the table above). Requests over it are answered with HTTP 429 rate_limited and a Retry-After: 1 header. When the daily or monthly quota is used up, the API answers HTTP 429 quota_exceeded until the reset. Rejected requests are not counted against the quota, but they are shown as "rejected" on the dashboard and in /usage.
Every successful response carries these headers (also exposed to browser code through CORS):
X-Plan: free
X-RateLimit-Limit: 2
X-Quota-Limit: 1000
X-Quota-Remaining: 999
X-Quota-Reset: 1790380800
| Header | Meaning |
|---|---|
X-Plan | Plan code of the key: free, starter, pro or business |
X-RateLimit-Limit | Requests per second allowed on the plan |
X-Quota-Limit | Quota of the current period: the daily allowance of this API on the free plan, the monthly pool on paid plans |
X-Quota-Remaining | Lookups left in the period after this request |
X-Quota-Reset | Unix time (UTC) when the period resets |
The same numbers are repeated in the meta object of every metered response, so you can read them without touching headers.
Response format
Successful responses have "success": true, the echoed input (ip or mac), the data grouped in objects, and a meta object with your plan, field level and quota:
"meta": {
"plan": "free",
"fields": "basic",
"quota": {
"limit": 1000,
"used": 2,
"remaining": 998,
"period": "day",
"resets_at": "2026-09-26T00:00:00+00:00"
}
}
Fields with no data are null, never missing, so your code can rely on the shape. Booleans are real booleans, numbers are numbers.
Trimming the response with fields
Add ?fields= with a comma-separated list of paths to keep only what you need. Dots select nested keys. success, ip, mac, version and meta are always included; unknown paths are ignored. The filter also applies to each result of a batch.
{
"success": true,
"ip": "1.1.1.1",
"version": 4,
"location": { "country": "Australia" },
"network": { "isp": "Cloudflare" },
"meta": { "plan": "free", "fields": "basic", "quota": { "limit": 1000, "used": 3, "remaining": 997, "period": "day", "resets_at": "2026-09-26T00:00:00+00:00" } }
}
IP lookup
Location, timezone and network data for any public IPv4 or IPv6 address. Without an address the API looks up the IP the request comes from, which is handy for "what is my IP" features. Coordinates come with an accuracy radius; the geolocation reflects where the network block is registered and used, not the physical position of one device.
{
"success": true,
"ip": "8.8.8.8",
"version": 4,
"location": {
"continent": "North America",
"continent_code": "NA",
"country": "United States",
"country_code": "US",
"region": "California",
"city": "Mountain View",
"postal_code": "94035",
"latitude": 37.38605,
"longitude": -122.08385,
"accuracy_radius_km": 1000,
"is_eu": false
},
"timezone": { "id": "America/Los_Angeles" },
"network": {
"asn": 15169,
"isp": "Google Cloud",
"organization": "Google LLC",
"domain": "cloud.google.com",
"connection_type": "hosting"
},
"meta": {
"plan": "free",
"fields": "basic",
"quota": { "limit": 1000, "used": 2, "remaining": 998, "period": "day", "resets_at": "2026-09-26T00:00:00+00:00" }
}
}
Paid plans return the same objects with more keys plus country_info, security and abuse:
{
"success": true,
"ip": "8.8.8.8",
"version": 4,
"location": {
"continent": "North America",
"continent_code": "NA",
"country": "United States",
"country_code": "US",
"region": "California",
"city": "Mountain View",
"postal_code": "94035",
"latitude": 37.38605,
"longitude": -122.08385,
"accuracy_radius_km": 1000,
"is_eu": false
},
"timezone": {
"id": "America/Los_Angeles",
"utc_offset": "-07:00",
"is_dst": true,
"current_time": "2026-09-25T04:14:29-07:00"
},
"network": {
"asn": 15169,
"isp": "Google Cloud",
"organization": "Google LLC",
"domain": "cloud.google.com",
"connection_type": "hosting",
"asn_name": "GOOGLE",
"org_domain": "cloud.google.com",
"holding": "Alphabet Inc.",
"holding_domain": "abc.xyz",
"usage": "Google Public DNS",
"hostname": "dns.google",
"range": { "start": "8.8.8.0", "end": "8.8.8.255", "size": 256 },
"rpki": "valid",
"speed_mbps": { "download": 100000, "upload": 100000 }
},
"country_info": {
"calling_code": "+1",
"tld": ".us",
"currency": { "code": "USD", "symbol": "$" },
"languages": ["en"]
},
"security": {
"is_vpn": false,
"vpn_provider": null,
"is_proxy": false,
"is_tor": false,
"is_hosting": true,
"is_threat": false,
"threat_type": null,
"threat_confidence": 0,
"threat_reports": 0,
"threat_last_seen": null
},
"abuse": { "email": "[email protected]" }
}
Fields
| Field | Type | Plan | Description |
|---|---|---|---|
ip | string | All | The address, IPv6 in compressed form |
version | 4 | 6 | All | IP version |
location.continent, continent_code | string | All | Continent name and two-letter code |
location.country, country_code | string | All | Country name and ISO 3166-1 alpha-2 code |
location.region, city, postal_code | string | All | Region, city and postal code where known |
location.latitude, longitude | number | All | Coordinates of the estimated location |
location.accuracy_radius_km | number | All | Radius around the coordinates that the location is expected to fall in |
location.is_eu | boolean | All | Country is an EU member |
location.address, address_type | string | Paid | Street address of the datacenter or office, with datacenter or office; only present when known |
timezone.id | string | All | IANA timezone, e.g. Europe/Berlin |
timezone.utc_offset, is_dst, current_time | string, boolean, string | Paid | Current UTC offset, daylight saving flag and local time (ISO 8601) |
network.asn | number | All | Autonomous system number announcing the address |
network.isp, organization, domain | string | All | Service provider, registered organisation and its website domain |
network.connection_type | string | All | residential, cable, mobile, hosting, business, backbone, education, research, government, military, financial, healthcare, media, satellite, fixed_wireless or ixp. VPN ranges report hosting. |
network.asn_name | string | Paid | Registered name of the AS |
network.org_domain | string | Paid | Domain of the organisation |
network.holding, holding_domain | string | Paid | Parent company that owns the ISP, when it is part of a group |
network.usage | string | Paid | What the range is used for, e.g. Google Public DNS, FTTH customers, mobile |
network.hostname | string | Paid | Reverse DNS name (PTR) of the address |
network.range | object | Paid | The block the address belongs to: start, end and size for IPv4, start, end and prefix for IPv6 |
network.rpki | string | Paid | valid, invalid, unrouted or not_found (IPv4 only, null for IPv6) |
network.speed_mbps | object | Paid | Average measured download and upload speed of the ISP, null when unknown |
country_info | object | Paid | calling_code, tld, currency.code, currency.symbol and languages of the country |
security.is_vpn, vpn_provider | boolean, string | Paid | Address belongs to a known VPN service, and which one |
security.is_proxy, is_tor, is_hosting | boolean | Paid | Open proxy, Tor exit node, datacenter or VPN range |
security.is_threat, threat_type, threat_confidence, threat_reports, threat_last_seen | mixed | Paid | Listed in the IPWhois.net blacklist, with threat type, confidence 0 to 100, number of reports and time of the last one |
abuse.email | string | Paid | Abuse contact of the network (IPv4) |
Private, reserved and unknown addresses
Private (RFC 1918), loopback, link-local, carrier-grade NAT, multicast, documentation and other reserved space is answered without a database lookup. The response is still counted and has a different shape:
{
"success": true,
"ip": "192.168.1.1",
"version": 4,
"reserved": true,
"reserved_type": "private",
"description": "Private network (RFC 1918)",
"security": { "is_private": true, "is_cgnat": false }
}
reserved_type is one of private, loopback, link_local, cgnat, multicast, documentation, benchmark or reserved. A public address that is not in any database returns "found": false with a message. IPv6 addresses get country and network data from our own IPv6 table and city-level location from GeoLite2; city fields are null when no city is known.
Batch IP lookup
Send up to 100 addresses per request on Starter and Pro, 1,000 on Business (the exact number for your plan is in the table above). The body is JSON: {"ips": ["8.8.8.8", "1.1.1.1"]}. A plain JSON array or a form field ips=8.8.8.8,1.1.1.1 is accepted as well. Duplicates are removed. Each address is counted as one lookup and every result has the full field set. The fields filter applies to every result.
curl -X POST https://ipwhois.net/api/v1/ip/batch \
-H "X-API-Key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"ips": ["8.8.8.8", "1.1.1.1", "not-an-ip"]}'
{
"success": true,
"count": 3,
"results": [
{ "success": true, "ip": "8.8.8.8", "version": 4, "location": { ... }, "timezone": { ... }, "network": { ... }, "security": { ... } },
{ "success": true, "ip": "1.1.1.1", "version": 4, "location": { ... }, "timezone": { ... }, "network": { ... }, "security": { ... } },
{ "success": false, "ip": "not-an-ip", "error": { "code": "invalid_ip", "message": "Not a valid IPv4 or IPv6 address." } }
],
"meta": { "plan": "pro", "fields": "full", "quota": { "limit": 1500000, "used": 3, "remaining": 1499997, "period": "month", "resets_at": "2026-10-01T00:00:00+00:00" } }
}
Invalid addresses fail inside results and do not fail the request. A batch larger than the plan allows is rejected as a whole with HTTP 400 batch_too_large; on the free plan the endpoint answers HTTP 402 plan_required. A GET without addresses answers 405 method_not_allowed.
Blacklist check
Tells you whether an address is in the IPWhois.net blacklist, a database of addresses reported for brute-force attacks, spam, scanning and other abuse by Fail2Ban, CSF and manual reports. This is our own list; it does not query external DNSBLs.
{
"success": true,
"ip": "193.46.255.86",
"listed": true,
"threat_type": "brute-force",
"confidence": 85,
"last_seen": "2026-09-25 10:47:05",
"meta": { ... }
}
{ "success": true, "ip": "8.8.8.8", "listed": false, "meta": { ... } }
Paid plans add the report history:
{
"success": true,
"ip": "193.46.255.86",
"listed": true,
"threat_type": "brute-force",
"confidence": 85,
"last_seen": "2026-09-25 10:47:05",
"total_reports": 5,
"distinct_reporters": 3,
"first_seen": "2026-03-28 21:27:52",
"source": "multiple",
"country_code": "GB",
"isp": "Unmanaged Ltd",
"reports": [
{ "threat_type": "brute-force", "message": "Fail2Ban sshd ban", "source": "fail2ban", "created_at": "2026-08-25 03:00:15" },
{ "threat_type": "brute-force", "message": "Fail2Ban sshd ban", "source": "fail2ban", "created_at": "2026-04-02 18:34:15" }
]
}
| Field | Type | Plan | Description |
|---|---|---|---|
listed | boolean | All | Address is in the blacklist. When false, no other fields follow. |
threat_type | string | All | Main category, e.g. brute-force, spam, scanner, web-attack |
confidence | number | All | 0 to 100, grows with the number and spread of reports |
last_seen | string | All | Time of the most recent report (UTC) |
total_reports, distinct_reporters | number | Paid | Number of reports and of independent reporters |
first_seen, source | string | Paid | First report and the source of the listing |
country_code, isp | string | Paid | Country and provider of the address at the time of listing |
reports | array | Paid | Up to 20 most recent reports with threat_type, message, source and created_at |
The batch endpoint takes the same body as the IP batch ({"ips": [...]}), has the same size limits and returns count, results and meta.
MAC vendor lookup
Resolves a MAC address or OUI prefix to the registered manufacturer, using the IEEE MA-L, MA-M and MA-S registries (the most specific block wins). Any common format works: 00:1A:2B:3C:4D:5E, 00-1A-2B-3C-4D-5E, 001A.2B3C.4D5E or 001A2B. At least the first six hex digits are required.
{
"success": true,
"mac": "00:1A:2B:3C:4D:5E",
"found": true,
"oui": "00:1A:2B",
"normalized": "00:1A:2B:3C:4D:5E",
"is_multicast": false,
"is_local": false,
"is_randomized": false,
"vendor": { "company": "Ayecom Technology Co., Ltd.", "country_code": "TW" },
"meta": { ... }
}
"vendor": {
"company": "Ayecom Technology Co., Ltd.",
"country_code": "TW",
"short_name": "AyecomTechno",
"website": "ayecom.com.tw",
"country": "Taiwan",
"address": "No. 25, R&D Road 2, Science-Based Industrial Park Hsinchu TW 300",
"industry": null,
"founded": null,
"headquarters": null,
"total_ouis": 1
},
"block": { "type": "MA-L", "start": "001A2B000000", "end": "001A2BFFFFFF", "size": 16777216 },
"device": { "type": null, "interface": null, "is_virtual_machine": false, "vm_platform": null },
"registry_updated": "2015/11/17"
| Field | Type | Plan | Description |
|---|---|---|---|
found | boolean | All | A vendor is registered for the prefix. When false, vendor is null. |
oui | string | All | First three bytes, colon separated |
normalized | string | All | Full address as AA:BB:CC:DD:EE:FF, null when only a prefix was given |
is_multicast | boolean | All | Group address (I/G bit set) |
is_local | boolean | All | Locally administered (U/L bit set), not assigned by a manufacturer |
is_randomized | boolean | All | Locally administered unicast, the pattern phones use for MAC randomisation |
vendor.company, country_code | string | All | Registered company and its country |
vendor.short_name, website, country, address | string | Paid | Short name, website, country name and registered address |
vendor.industry, founded, headquarters, total_ouis | mixed | Paid | Company details where known and the number of prefixes registered to it |
block | object | Paid | Registry block: type (MA-L, MA-M, MA-S), start, end, size |
device.type, interface | string | Paid | Device and interface type where known |
device.is_virtual_machine, vm_platform | boolean, string | Paid | Prefix belongs to VMware, VirtualBox, Hyper-V, QEMU/KVM, Parallels, Xen or Docker |
registry_updated | string | Paid | Date of the registry record |
Search by company
/mac/search?q= (alias vendor=) lists the prefixes registered to companies whose name contains the query, at least two characters, up to 100 results ordered by company name. It counts as one MAC lookup.
{
"success": true,
"query": "Ayecom",
"count": 1,
"results": [
{ "oui": "00:1A:2B", "company": "Ayecom Technology Co., Ltd.", "country_code": "TW", "block_type": "MA-L" }
],
"meta": { ... }
}
Usage and plan of a key
Returns the plan, the per-second limit and today's counters of the account the key belongs to. Free accounts see the daily quota per API, paid accounts the monthly pool. This call is free and not rate limited by quota, so you can poll it from a dashboard.
{
"success": true,
"plan": { "code": "free", "name": "Free", "expires_at": null },
"rate_limit_per_second": 2,
"today": { "ip": 7, "blacklist": 1, "mac": 3, "rejected": 5 },
"daily_quota": { "ip": 1000, "blacklist": 1000, "mac": 1000 }
}
// paid plans replace daily_quota with
"monthly_quota": { "limit": 1500000, "used": 4821, "remaining": 1495179 }
Errors
Errors use HTTP status codes and a JSON body with "success": false and an error object. The code is stable and meant for your program; the message is for people and may change.
{
"success": false,
"error": {
"code": "rate_limited",
"message": "Too many requests per second (limit 2/s on the Free plan)."
}
}
| HTTP | Code | When |
|---|---|---|
| 400 | invalid_ip | Not a valid IPv4 or IPv6 address |
| 400 | invalid_mac | Fewer than 6 or more than 12 hex digits |
| 400 | invalid_query | MAC search with fewer than 2 characters |
| 400 | empty_batch | Batch request without addresses |
| 400 | batch_too_large | More addresses than the plan allows per batch |
| 400 | unavailable | The blacklist or MAC database is temporarily unavailable; retry later |
| 401 | missing_key | No API key in the request |
| 401 | invalid_key | Malformed, unknown or revoked key |
| 402 | plan_required | Batch or MAC search on the free plan |
| 403 | origin_not_allowed | Key is restricted to websites and the request came from elsewhere |
| 403 | account_suspended | The account is suspended; contact support |
| 404 | not_found | Unknown endpoint |
| 405 | method_not_allowed | Batch called with GET and no addresses |
| 429 | rate_limited | Per-second limit exceeded; Retry-After: 1 is set |
| 429 | quota_exceeded | Daily (free) or monthly (paid) quota used up; X-Quota-Reset tells when it resets |
Inside a batch, a bad address produces the same error object on that single result while the request itself succeeds.
Code samples
Replace YOUR_API_KEY with a key from the dashboard. Each sample looks up 8.8.8.8 and prints the city and ISP.
curl https://ipwhois.net/api/v1/ip/8.8.8.8 \
-H "X-API-Key: YOUR_API_KEY"
# only two fields, indented
curl "https://ipwhois.net/api/v1/ip/8.8.8.8?fields=location.city,network.isp&pretty" \
-H "X-API-Key: YOUR_API_KEY"
import requests
r = requests.get("https://ipwhois.net/api/v1/ip/8.8.8.8",
headers={"X-API-Key": "YOUR_API_KEY"}, timeout=10)
data = r.json()
if data["success"]:
print(data["location"]["city"], data["network"]["isp"])
else:
print(r.status_code, data["error"]["code"], data["error"]["message"])
$ch = curl_init("https://ipwhois.net/api/v1/ip/8.8.8.8");
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 10,
CURLOPT_HTTPHEADER => ["X-API-Key: YOUR_API_KEY"],
]);
$data = json_decode(curl_exec($ch), true);
curl_close($ch);
if (!empty($data["success"])) {
echo $data["location"]["city"], " ", $data["network"]["isp"];
} else {
echo $data["error"]["code"], ": ", $data["error"]["message"];
}
const r = await fetch("https://ipwhois.net/api/v1/ip/8.8.8.8", {
headers: { "X-API-Key": "YOUR_API_KEY" }
});
const data = await r.json();
if (data.success) {
console.log(data.location.city, data.network.isp);
} else {
console.error(r.status, data.error.code, data.error.message);
}
// batch (paid plans)
const b = await fetch("https://ipwhois.net/api/v1/ip/batch", {
method: "POST",
headers: { "X-API-Key": "YOUR_API_KEY", "Content-Type": "application/json" },
body: JSON.stringify({ ips: ["8.8.8.8", "1.1.1.1"] })
});
const { results } = await b.json();
Legacy endpoints
The endpoints that existed before the v1 API keep working unchanged, so nothing you built on them breaks:
https://ipwhois.net/api/mac/v1/lookup.php?mac=00:1A:2B:3C:4D:5E(and?vendor=for reverse search): 100 requests per day per IP without a key.https://bl.ipwhois.net/api/check.php?ip=1.2.3.4: the original blacklist check, described in the Full Blacklist API documentation.
They have their own response formats and limits. New integrations should use the v1 API described on this page: one key, one response shape, quota headers and batch support.
Attribution and licence
On the free plan, attribution is required: show a visible credit such as "IP data by IPWhois.net" linking to https://ipwhois.net wherever you display data from the API. The free plan is for testing and personal projects; commercial use needs a paid plan, which has no attribution requirement.
The IPWhois.net database is built from regional registry data, BGP routing tables, provider geofeeds and our own measurements. It also includes GeoLite2 data created by MaxMind, available from maxmind.com, and the IP2Location LITE database from lite.ip2location.com. When you redistribute or display free-plan data, keep those credits together with ours.
Need higher volumes, a database licence or a custom limit? Contact us with your expected volume.