Batch IP Lookup API: Look Up 1,000 IP Addresses in One Request
Short answer
The batch endpoint looks up 1 to 1,000 IPv4 or IPv6 addresses in one POST request to https://api.iplookupapi.com/v1/batch and returns location, connection, time zone and currency data for each address, keyed by IP. Each valid address counts as one request; invalid items are not billed and duplicates are billed once. Batch lookups are included in every plan, including Free.
On this page
The batch endpoint takes a list of up to 1,000 IP addresses and returns the full lookup for each one in a single response. You send one POST request instead of 1,000 single lookups, and you get the same data as the single-IP info endpoint: country, region, city, coordinates, ISP, ASN, time zone and currency. This page covers the request format, billing, code in four languages, a complete script for large files, and how to pick a plan by volume.
When to use a batch IP lookup
A batch request fits any job where you already have a list of addresses and want the data in one pass:
- Log enrichment. Add country, ASN and ISP to web server or firewall logs before you load them into your analytics tool.
- Sign-up and order lists. Check where new accounts or orders come from, for example to spot orders whose IP country does not match the billing country.
- Analytics exports. Turn a column of raw IPs into countries, regions and time zones for reporting.
- Fraud review queues. Pull connection type and, on higher plans, VPN, proxy and Tor flags for every flagged session at once.
If you need data for one visitor in real time, use the single lookup at /v1/info instead. Batch is for lists.
How the batch endpoint works
Send a POST request to https://api.iplookupapi.com/v1/batch with a JSON body:
{"ips": ["8.8.8.8", "80.110.0.1", "2001:4860:4860::8888"]}The key rules:
| Item | Value |
|---|---|
| Endpoint | POST https://api.iplookupapi.com/v1/batch |
| Body | {"ips": [...]} with 1 to 1,000 IPv4 or IPv6 addresses (100 with a trial key) |
| GET variant | GET /v1/batch?ips=... with up to 100 comma-separated addresses |
| Authentication | apikey HTTP header (recommended) or apikey query parameter |
fields | Comma-separated dot paths, for example ip,location.country.alpha2,timezone.id |
language | de, en, es, fr, ja, pt, ru, zh (default en) |
hostname | 1 or true adds reverse DNS for each address; this adds response time |
| Response | A data object keyed by IP address, one full result per address |
Use the header for the key. Query strings end up in server logs and browser history; headers usually do not.
Response format
The response has a top-level data object. Each key is an IP address, and each value is the same object the info endpoint returns, or the subset you asked for with fields. Items that are not IP addresses appear in an errors object with the reason. A shortened example for {"ips": ["8.8.8.8", "not-an-ip"]} with fields=ip,location.country.alpha2:
{
"data": {
"8.8.8.8": {
"ip": "8.8.8.8",
"location": {
"country": {
"alpha2": "US"
}
}
}
},
"errors": {
"not-an-ip": "Not a valid IP address."
}
}Without fields, each entry holds the full object: ip, hostname, type (v4 or v6), range_type, connection (ASN, organization, ISP, range), location (continent, country with currencies and calling codes, region, city, zip, latitude, longitude), timezone and security. The batch documentation lists every field.
Billing, limits and errors
Billing is per address, not per HTTP call:
- Each valid IP address counts as one request. A batch of 1,000 valid addresses uses 1,000 requests of your monthly quota. The
X-Costresponse header shows the total for the call. - Invalid items are free. Anything that is not an IP address is listed in
errorsand not billed. - Duplicates are billed once. This includes different notations of the same IPv6 address. The result is keyed by the spelling you sent first.
- Quota is checked first. If your remaining quota cannot cover the whole batch, the API answers
429before it looks up anything. Thequota.requiredfield in the response tells you how many requests the batch needed. - Private and reserved addresses do not fail the batch. An address such as
10.0.0.1comes back with itsrange_type(for examplePRIVATE_NETWORK) and no location data. Private ranges such as 10.0.0.0/8 are defined in RFC 1918 and are not routed on the public internet, so no location exists for them.
The status codes you will see most often:
| Code | Meaning |
|---|---|
| 401 | Missing or invalid API key |
| 403 | Your plan does not include the batch endpoint (error.code plan_upgrade_required); nothing is looked up or billed |
| 422 | Validation error, for example no valid IP in the batch or more than 1,000 items |
| 429 | Rate limit reached, or the monthly quota cannot cover the batch |
GET /v1/status returns your quota and usage and does not count against it. Call it before a large job.
Code examples
All examples send the API key in the apikey header. Replace YOUR_API_KEY, or set the IPLOOKUPAPI_KEY environment variable. You can register for a free key.
curl
curl -X POST "https://api.iplookupapi.com/v1/batch?fields=ip,location.country.alpha2,timezone.id" \
-H "apikey: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"ips": ["8.8.8.8", "80.110.0.1", "2001:4860:4860::8888"]}'The GET variant, for up to 100 addresses:
curl "https://api.iplookupapi.com/v1/batch?ips=8.8.8.8,80.110.0.1&fields=ip,location.country.alpha2" \
-H "apikey: YOUR_API_KEY"JavaScript (Node.js 18+)
Node.js 18 and later ship fetch, so you need no extra package.
const API_KEY = process.env.IPLOOKUPAPI_KEY;
const ENDPOINT = "https://api.iplookupapi.com/v1/batch?fields=ip,location.country.alpha2,timezone.id";
async function batchLookup(ips) {
const res = await fetch(ENDPOINT, {
method: "POST",
headers: { apikey: API_KEY, "Content-Type": "application/json" },
body: JSON.stringify({ ips }),
});
if (!res.ok) {
throw new Error(`HTTP ${res.status}: ${await res.text()}`);
}
return res.json();
}
batchLookup(["8.8.8.8", "80.110.0.1", "2001:4860:4860::8888"])
.then((body) => {
for (const [ip, info] of Object.entries(body.data)) {
console.log(ip, info.location?.country?.alpha2 ?? "-", info.timezone?.id ?? "-");
}
if (body.errors) console.log("Not looked up:", body.errors);
})
.catch((err) => console.error(err.message));Python (requests)
import os
import requests
resp = requests.post(
"https://api.iplookupapi.com/v1/batch",
params={"fields": "ip,location.country.alpha2,connection.asn"},
headers={"apikey": os.environ["IPLOOKUPAPI_KEY"]},
json={"ips": ["8.8.8.8", "80.110.0.1", "2001:4860:4860::8888"]},
timeout=60,
)
resp.raise_for_status()
body = resp.json()
for ip, info in body["data"].items():
country = ((info.get("location") or {}).get("country") or {}).get("alpha2")
asn = (info.get("connection") or {}).get("asn")
print(ip, country, asn)
print("Billed requests:", resp.headers.get("X-Cost"))PHP (cURL)
<?php
$apiKey = getenv('IPLOOKUPAPI_KEY');
$payload = json_encode(['ips' => ['8.8.8.8', '80.110.0.1', '2001:4860:4860::8888']]);
$ch = curl_init('https://api.iplookupapi.com/v1/batch?fields=ip,location.country.alpha2');
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => $payload,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 60,
CURLOPT_HTTPHEADER => [
'apikey: ' . $apiKey,
'Content-Type: application/json',
],
]);
$response = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if ($response === false || $status !== 200) {
fwrite(STDERR, "Request failed (HTTP $status): " . curl_error($ch) . PHP_EOL);
exit(1);
}
$body = json_decode($response, true);
foreach ($body['data'] as $ip => $info) {
echo $ip . ' ' . ($info['location']['country']['alpha2'] ?? '-') . PHP_EOL;
}Look up a large file of IPs: complete Python script
This script reads a text or CSV file with one IP per line (the first column is used), validates and dedupes the addresses locally, sends them in chunks of 1,000, retries on rate limits with exponential backoff, and writes one CSV row per address. It stops with a clear message if your monthly quota cannot cover a chunk, because retrying will not help in that case.
Run it with python batch_lookup.py ips.txt results.csv. It needs Python 3.8+ and requests.
import csv
import ipaddress
import os
import random
import sys
import time
import requests
API_URL = "https://api.iplookupapi.com/v1/batch"
API_KEY = os.environ["IPLOOKUPAPI_KEY"]
CHUNK_SIZE = 1000
MAX_RETRIES = 6
FIELDS = ",".join([
"ip", "type", "range_type.type",
"location.country.alpha2", "location.region.name", "location.city.name",
"location.latitude", "location.longitude",
"timezone.id", "connection.asn", "connection.isp",
])
COLUMNS = ["ip", "status", "type", "range_type", "country", "region", "city",
"latitude", "longitude", "timezone", "asn", "isp"]
def read_ips(path):
"""Return (valid, invalid): valid IPs deduped in input order, invalid lines."""
valid, invalid = {}, []
with open(path, newline="", encoding="utf-8") as f:
for row in csv.reader(f):
if not row or not row[0].strip():
continue
raw = row[0].strip()
if raw.lower() == "ip": # skip a header row
continue
try:
valid[str(ipaddress.ip_address(raw))] = None
except ValueError:
invalid.append(raw)
return list(valid), invalid
def lookup_chunk(ips):
"""POST one chunk; retry on rate limits and server errors."""
for attempt in range(MAX_RETRIES):
resp = requests.post(
API_URL,
params={"fields": FIELDS},
headers={"apikey": API_KEY},
json={"ips": ips},
timeout=120,
)
if resp.status_code == 200:
return resp.json()
if resp.status_code == 429:
try:
body = resp.json()
except ValueError:
body = {}
if "quota" in body: # monthly quota too small: retrying will not help
needed = body["quota"].get("required", len(ips))
sys.exit(f"Quota exceeded: this chunk needs {needed} requests. "
"Upgrade your plan or wait for the quota reset.")
elif resp.status_code < 500:
sys.exit(f"HTTP {resp.status_code}: {resp.text}")
wait = min(60, 2 ** attempt) + random.uniform(0, 1)
print(f"HTTP {resp.status_code}, retrying in {wait:.1f}s", file=sys.stderr)
time.sleep(wait)
sys.exit("Giving up after repeated errors.")
def get(obj, path):
for key in path.split("."):
if not isinstance(obj, dict):
return None
obj = obj.get(key)
return obj
def to_row(ip, info):
return {
"ip": ip, "status": "ok", "type": info.get("type"),
"range_type": get(info, "range_type.type"),
"country": get(info, "location.country.alpha2"),
"region": get(info, "location.region.name"),
"city": get(info, "location.city.name"),
"latitude": get(info, "location.latitude"),
"longitude": get(info, "location.longitude"),
"timezone": get(info, "timezone.id"),
"asn": get(info, "connection.asn"),
"isp": get(info, "connection.isp"),
}
def main(src, dst):
ips, invalid = read_ips(src)
print(f"{len(ips)} unique valid IPs, {len(invalid)} invalid lines", file=sys.stderr)
with open(dst, "w", newline="", encoding="utf-8") as f:
writer = csv.DictWriter(f, fieldnames=COLUMNS)
writer.writeheader()
for raw in invalid:
writer.writerow({"ip": raw, "status": "invalid"})
for start in range(0, len(ips), CHUNK_SIZE):
chunk = ips[start:start + CHUNK_SIZE]
body = lookup_chunk(chunk)
data = body.get("data") or {}
errors = body.get("errors") or {}
for ip in chunk:
if ip in data:
writer.writerow(to_row(ip, data[ip]))
elif ip in errors:
writer.writerow({"ip": ip, "status": f"error: {errors[ip]}"})
else:
writer.writerow({"ip": ip, "status": "no data"})
print(f"Done {start + len(chunk)} of {len(ips)}", file=sys.stderr)
if __name__ == "__main__":
if len(sys.argv) != 3:
sys.exit("Usage: python batch_lookup.py <input.txt|csv> <output.csv>")
main(sys.argv[1], sys.argv[2])Tip
Run the script on a small sample first, for example the first 50 lines of your file. Check the CSV, then run the full file. Each valid address counts against your quota on every run, so do not re-run a full file just to change the output format.
Choosing a plan by volume
Since every valid address counts as one request, size your plan by the number of unique addresses you look up per month:
| Plan | Price (USD/month) | Requests per month | Fits roughly |
|---|---|---|---|
| Free | $0 | 150 | Testing; one batch of 150 addresses uses the whole month |
| Basic | $9.99 | 65,000 | Up to 65 full batches of 1,000 |
| Professional | $34.99 | 650,000 | Up to 650 full batches of 1,000 |
| Enterprise | $74.99 | 2,500,000 | Up to 2,500 full batches of 1,000 |
Some quick math:
- 50,000 IPs a month fits Basic: 50 batches of 1,000, with 15,000 requests left over.
- 300,000 IPs a month needs Professional. Basic tops out at 65,000.
- 1,000,000 IPs a month needs Enterprise.
The data also depends on the plan. Location, connection, time zone, currency and batch are included from Free. Network data (hosting, CDN, mobile carrier) and company and abuse contacts start at Basic. Security data, meaning VPN, proxy and Tor flags and the threat score, starts at Professional. Yearly billing gives you two months free. See pricing for current details.
Tips for faster, cheaper batch lookups
- Dedupe before you send. Logs repeat the same addresses many times. The API bills duplicates within one batch once, but it cannot dedupe across batches or across days. Dedupe the whole file first, and cache results you already have.
- Ask only for the fields you need.
fields=ip,location.country.alpha2returns a fraction of the full object, which cuts transfer and parsing time. If all you need is the country, the IP to country API page shows the relevant fields. For scheduling or local-time features, see the IP time zone API. - Keep batches at 1,000. Fewer, full batches mean fewer round trips. Use GET only for short lists of up to 100.
- Leave
hostnameoff unless you need it. Reverse DNS adds response time to every batch. - Filter private addresses before you send. Internal addresses from your own network return no location, so they add little to a geolocation report. The script above keeps them; skip them in
read_ipswithipaddress.ip_address(raw).is_privateif you do not need them.
Note
IP location is an estimate based on network data, not a street address. If a result shows a location you know is wrong, you can report the wrong location.
To use IP lookups from an AI agent instead of code, connect the hosted MCP server.
Frequently asked questions
Can I do batch IP lookups for free?
Yes. The batch endpoint of iplookupapi is included in every plan, including the Free plan. The Free plan includes 150 requests a month, and each valid address in a batch counts as one request, so a free account can look up 150 addresses a month in total. For larger lists, see the pricing page.
How many IP addresses can I look up in one batch request?
A POST request to https://api.iplookupapi.com/v1/batch accepts 1 to 1,000 IPv4 or IPv6 addresses in the JSON body as {“ips”: […]}. A GET request with comma-separated addresses in the ips parameter accepts up to 100. For longer lists, split them into chunks of 1,000 and send the chunks one after another.
Does a batch of 1,000 IPs count as 1 request or 1,000 requests?
It counts as 1,000 requests. Every valid IP address in a batch is billed as one request against your monthly quota. Items that are not IP addresses are not billed, and duplicate addresses are billed only once.
Does the batch IP lookup API support IPv6?
Yes. You can mix IPv4 and IPv6 addresses in the same batch. Each result has a type field (v4 or v6), and different notations of the same IPv6 address count as one address.
What happens to invalid IP addresses in a batch?
They do not fail the batch. Items that are not IP addresses are listed in an errors object with the reason, and they are not billed. Private or reserved addresses such as 10.0.0.1 are valid IPs: they come back with their range_type and without location data.
How can I check IP reputation in bulk?
On the Professional plan and up, batch results include a filled security object with flags such as is_vpn, is_proxy, is_tor, is_datacenter and is_abuser, plus a threat_score. Add fields=ip,security to a batch request to get only those flags for up to 1,000 addresses at a time.
Look up IP addresses in your code
iplookupapi returns the location, provider, time zone and security flags of any IP address as JSON. The free plan includes 150 requests a month.
curl "https://api.iplookupapi.com/v1/info?ip=8.8.8.8" \
-H "apikey: YOUR_API_KEY"