How to Do Bulk IP Lookups — Step-by-Step Guide
You have a list of IPs — from a log file, a security feed, or a user cohort — and you need geolocation, ISP, and threat data for all of them. This guide walks through building a bulk lookup pipeline that handles large lists, null fields, and rate limits properly. The full script at the end is tested against the live API and ready to use.
Why Use the Bulk Endpoint
One Request, Multiple IPs
Send up to 100 IPs in a single API call — no loops, no rate-limit anxiety, no client-side batching.
Same Rich Response Per IP
Every IP in the batch gets the full response: geoLocation, timezone, currency, flag, connection/ASN, security, and user-agent data.
JSON, XML, or CSV
Choose the output format that fits your pipeline. Default is JSON; add &format=xml or &format=csv for exports and spreadsheets.
Field Filtering Built In
Only need country and ISP? Add &get=geoLocation,connection to strip the response down to the fields you use — less bandwidth, faster parse.
Step 1 — Get Your API Key
Create a free account — no credit card needed. You get 2,000 lookups per day on the free plan. Paid plans start at $2.50/month and include bulk lookups with higher quotas. Once you have your key, test it works:
curl -sS "https://api.ipwho.org/ip/8.8.8.8?apiKey=YOUR_API_KEY" Step 2 — Your First Bulk Request
The bulk endpoint is GET /bulk/{ip1,ip2,...}?apiKey=YOUR_API_KEY.
Pass a comma-separated list of IPs — up to ~100 per call. Start with curl
to see the raw response shape:
curl -sS "https://api.ipwho.org/bulk/8.8.8.8,1.1.1.1,9.9.9.9?apiKey=YOUR_API_KEY"
Each IP in data.responseArray gets the full response:
geoLocation (continent, country, city, lat/long),
connection (ASN, ISP, domain), timezone,
currency, security, and more.
Fields nest inside their parent objects — country is at
geoLocation.country, ISP is at connection.isp.
Step 3 — Parse the Response in Python
Using only the standard library (urllib, no pip install needed):
import urllib.request, json
ips = ["8.8.8.8", "1.1.1.1", "9.9.9.9"]
url = f"https://api.ipwho.org/bulk/{','.join(ips)}?apiKey=YOUR_API_KEY"
req = urllib.request.Request(url, headers={"User-Agent": "MyApp/1.0"})
with urllib.request.urlopen(req, timeout=10) as resp:
data = json.loads(resp.read())
for entry in data["data"]["responseArray"]:
ip = entry["ip"]
geo = entry.get("geoLocation") or {}
country = geo.get("country") or "Unknown"
city = geo.get("city") or "N/A"
isp = entry.get("connection", {}).get("isp", "Unknown")
print(f"{ip:15s} -> {city:20s} {country:20s} ISP: {isp}") Step 4 — Handle Null Fields
Infrastructure IPs (8.8.8.8, 1.1.1.1) often return null for
city, region, and postal code. Direct access like entry["geoLocation"]["city"]
will blow up. Use a safe accessor:
# Safe field access — every geo field can be null
def safe_get(d, *keys, default="N/A"):
for k in keys:
d = d.get(k) if isinstance(d, dict) else None
if d is None:
return default
return d if d is not None else default
country = safe_get(entry, "geoLocation", "country")
city = safe_get(entry, "geoLocation", "city")
isp = safe_get(entry, "connection", "isp") Step 5 — Batch Large Lists
The bulk endpoint handles ~100 IPs per call. If you have 500 IPs, split them into chunks of 100 and collect results:
# Batch large IP lists in chunks of 100
def lookup_batch(ips, api_key):
chunk_size = 100
all_results = []
for i in range(0, len(ips), chunk_size):
chunk = ips[i:i+chunk_size]
url = f"https://api.ipwho.org/bulk/{','.join(chunk)}?apiKey={api_key}"
req = urllib.request.Request(url, headers={"User-Agent": "MyApp/1.0"})
with urllib.request.urlopen(req, timeout=15) as resp:
data = json.loads(resp.read())
if data.get("success"):
all_results.extend(data["data"]["responseArray"])
return all_results
Add a small time.sleep(0.1) between chunks if you're pushing into
the rate-limit window. The free plan is generous (~2,000/day), but paid plans
remove that ceiling.
Step 6 — The Full Script
Everything together — reads IPs from stdin, one per line, and outputs
a pipe-delimited table. Copy, paste, replace YOUR_API_KEY, run.
Tested against the live API (2026-10-23).
#!/usr/bin/env python3
"""Bulk IP lookup — enrich a list of IPs using the IPWho bulk endpoint."""
import urllib.request, json, sys
API_KEY = "YOUR_API_KEY" # Get yours at ipwho.org/free-plan
BATCH_SIZE = 100
def safe_get(d, *keys, default="N/A"):
for k in keys:
d = d.get(k) if isinstance(d, dict) else None
if d is None:
return default
return d if d is not None else default
def lookup_batch(ips):
all_results = []
for i in range(0, len(ips), BATCH_SIZE):
chunk = ips[i:i+BATCH_SIZE]
url = f"https://api.ipwho.org/bulk/{','.join(chunk)}?apiKey={API_KEY}"
req = urllib.request.Request(url, headers={"User-Agent": "IPWho-Bulk/1.0"})
with urllib.request.urlopen(req, timeout=15) as resp:
data = json.loads(resp.read())
if data.get("success"):
all_results.extend(data["data"]["responseArray"])
else:
print(f"Error on chunk {i}: {data}", file=sys.stderr)
return all_results
if __name__ == "__main__":
# Read IPs from stdin, one per line
ips = [line.strip() for line in sys.stdin if line.strip()]
if not ips:
print("Usage: cat ips.txt | python3 bulk_lookup.py", file=sys.stderr)
sys.exit(1)
results = lookup_batch(ips)
for entry in results:
ip = entry["ip"]
country = safe_get(entry, "geoLocation", "country")
city = safe_get(entry, "geoLocation", "city")
isp = safe_get(entry, "connection", "isp")
threat = safe_get(entry, "security", "isThreat")
print(f"{ip:15s} | {country:20s} | {city:20s} | {isp:25s} | threat={threat}") Example run:
$ echo "8.8.8.8" | python3 bulk_lookup.py
8.8.8.8 | United States | N/A | Google | threat=low
1.1.1.1 | Unknown | N/A | Cloudflare | threat=low
9.9.9.9 | Unknown | N/A | Quad9 | threat=low Ready to Build?
The bulk endpoint is included on every paid plan — starting at $2.50/month. No separate "bulk add-on" to buy.