Blog

Batch lookups: check 100 IPs in one call

Single-IP /v1/check is the right call when you're scoring one address in a request path — a signup, a checkout, a login. But two jobs don't fit that shape: enriching logs and backfilling a table of addresses you already collected. For those, firing one HTTP request per IP is mostly overhead.

POST /v1/check/batch takes up to 100 IPs in one call and returns the same per-IP payload as the single endpoint — one round trip instead of a hundred.

The request

Send a JSON body with an ips array. Mixed IPv4 and IPv6 is fine:

$ curl "https://api.geoq.io/v1/check/batch" \
    -H "x-api-key: $GEOQ_API_KEY" \
    -H "Content-Type: application/json" \
    -d '{"ips":["8.8.8.8","2001:4860:4860::8888","45.83.91.2"]}'

Enriching logs in Node.js

Collect the addresses, chunk to 100, send one batch. Each result is either a full payload or an inline { "ip": "...", "error": "invalid_ip" } — one bad entry never fails the whole request, and results come back in input order so you can match by position:

// Enrich a batch of log lines — one round trip for up to 100 IPs.
const ips = logLines.map((l) => l.remote_addr).slice(0, 100);

const res = await fetch('https://api.geoq.io/v1/check/batch', {
  method: 'POST',
  headers: {
    'x-api-key': process.env.GEOQ_API_KEY,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({ ips }),
});

const { count, results } = await res.json();
for (const r of results) {
  if (r.error) { console.warn(r.ip, r.error); continue; } // invalid entries come back inline
  console.log(r.ip, r.risk.level, r.risk.reasons);
}

Backfilling in Python

Same idea for a one-off backfill — chunk a long list and reuse a session:

import os, requests

# Backfill: score historical IPs in chunks of 100.
def chunked(xs, n=100):
    for i in range(0, len(xs), n):
        yield xs[i:i + n]

session = requests.Session()
session.headers['x-api-key'] = os.environ['GEOQ_API_KEY']

for chunk in chunked(all_ips):
    res = session.post('https://api.geoq.io/v1/check/batch', json={'ips': chunk})
    for r in res.json()['results']:
        if 'error' in r:
            print(r['ip'], r['error'])
        else:
            store(r['ip'], r['risk']['score'], r['risk']['reasons'])

How batch counts against your quota

Worth being blunt about, because it's the question everyone asks: a batch is metered per IP, not per call. A batch of 50 IPs counts as 50 lookups against your plan, exactly as if you'd made 50 single calls. Batching saves you round trips and latency — it does not make lookups cheaper.

Two more honesty notes:

  • Metering is approximate during beta; where there's ambiguity we round in your favour.
  • Send more than 100 IPs in one call and the request is rejected with 400 — chunk client-side.

Next steps

See the full batch endpoint reference for status codes and the response shape, the response schema for every per-IP field, or rate limits for quota behaviour. No key yet? Grab a free one — no card.

Signals are probabilistic, not facts. Don't make a sole-basis automated decision about a person — see the acceptable use policy.

Keep reading

Get a free key — 5,000 lookups/day, no card.

Every signal and the same risk score as every paid plan. Upgrade only when you outgrow it.