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.

Get a free API key → View pricing Bulk lookup overview