Batch IP Lookup API: Look Up 1,000 IP Addresses in One Request

Updated

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:

JSON
{"ips": ["8.8.8.8", "80.110.0.1", "2001:4860:4860::8888"]}

The key rules:

ItemValue
EndpointPOST https://api.iplookupapi.com/v1/batch
Body{"ips": [...]} with 1 to 1,000 IPv4 or IPv6 addresses (100 with a trial key)
GET variantGET /v1/batch?ips=... with up to 100 comma-separated addresses
Authenticationapikey HTTP header (recommended) or apikey query parameter
fieldsComma-separated dot paths, for example ip,location.country.alpha2,timezone.id
languagede, en, es, fr, ja, pt, ru, zh (default en)
hostname1 or true adds reverse DNS for each address; this adds response time
ResponseA 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:

JSON
{
  "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-Cost response header shows the total for the call.
  • Invalid items are free. Anything that is not an IP address is listed in errors and 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 429 before it looks up anything. The quota.required field 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.1 comes back with its range_type (for example PRIVATE_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:

CodeMeaning
401Missing or invalid API key
403Your plan does not include the batch endpoint (error.code plan_upgrade_required); nothing is looked up or billed
422Validation error, for example no valid IP in the batch or more than 1,000 items
429Rate 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

Shell
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:

Shell
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.

JavaScript
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)

Python
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
<?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.

Python
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:

PlanPrice (USD/month)Requests per monthFits roughly
Free$0150Testing; one batch of 150 addresses uses the whole month
Basic$9.9965,000Up to 65 full batches of 1,000
Professional$34.99650,000Up to 650 full batches of 1,000
Enterprise$74.992,500,000Up 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.alpha2 returns 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 hostname off 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_ips with ipaddress.ip_address(raw).is_private if 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"