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.