Batch domain check
POST /v1/check/batch resolves up to 100 domains in one request, so a
KYB sweep or affiliate-list audit is one round trip instead of N. 200
merchants = 2 calls, not 200.
Authenticated (the single GET /v1/check stays keyless); domains only.
Request
Section titled “Request”curl -s -X POST https://api.igregulator.io/v1/check/batch \ -H "Authorization: Bearer $IGREGULATOR_KEY" \ -H "Content-Type: application/json" \ -d '{"domains":["bet365.com","www.virginbet.com","casino.org"]}'Response
Section titled “Response”{ "count": 3, "checked_jurisdictions": ["AN", "BE", "CO", "CW", "DE", "ES", "IOM", "KH", "MGA", "ON", "SE", "TGC", "UA", "UKGC"], "results": [ { "query": { "domain": "bet365.com", "input": "bet365.com" }, "verdict": "licensed", "verdict_detail": "bet365.com is listed, as of our read on 2026-09-30, on an active UK Gambling Commission licence held by Hillside (UK Gaming) ENC (licence 055149-R-331499-004); see GET /v1/check for this domain's 2 other operator links.", "match": { "operator": "Hillside (UK Gaming) ENC", "status": "active", "domain_status": "active", "...": "…" }, "confidence": "high", "_meta": { "register": { "jurisdiction": "UKGC", "last_read_at": "2026-09-30T03:00:26.364Z", "fresh": true, "sla_hours": 24 } } }, { "query": { "domain": "www.virginbet.com", "input": "www.virginbet.com" }, "verdict": "licensed", "verdict_detail": "www.virginbet.com is listed, as of our read on 2026-09-30, on an active UK Gambling Commission licence held by Virgin Bet Limited (licence 054310-R-330640-007, white label).", "match": { "operator": "Virgin Bet Limited", "domain_association": "white_label", "...": "…" }, "confidence": "high", "_meta": { "register": { "...": "…" } } }, { "query": { "domain": "casino.org", "input": "casino.org" }, "verdict": "generic_term", "verdict_detail": "casino.org is on no licence we hold in the 14 jurisdictions we cover (AN, BE, CO, CW, DE, ES, IOM, KH, MGA, ON, SE, TGC, UA, UKGC), and its name contains the generic gambling term \"casino\", so we will not guess an operator from it — not a finding that it is unlicensed.", "match": null, "confidence": "low", "match_absence_reason": "generic_term" } ], "_meta": { "checked_at": "2026-09-30T18:48:03.561Z", "stale_jurisdictions": [{ "code": "TGC", "last_read_at": "2026-09-21T04:15:08.476Z" }] }}Said once, at the top, because it is the same for every row:
checked_jurisdictions (the registers every row was checked against) and
_meta — checked_at, and stale_jurisdictions, the covered registers past
their freshness window, which weaken every “not found” in the batch.
What a row carries — and what it leaves out
Section titled “What a row carries — and what it leaves out”A row is resolved exactly like GET /v1/check (same matching, same verdict
rules), but it is not the whole single-check response. Every row that is not an
error carries:
query—domain, the hostname we looked up, andinput, the string you sent (hostnames).verdictandverdict_detail— branch on the first, quote the second (confidence scoring).match— the same object as onGET /v1/check, ornull.confidence— always present;noneon a miss (lowwhen the miss is ageneric_term).match_absence_reason— only whenmatchisnull.related_hosts[]— only when your host (and itswww.counterpart) isn’t stored but other hosts on its registrable domain are (related hosts)._meta.register— when there is a matched jurisdiction: when we last read its register and whether that read is inside its freshness window.
A row does not carry:
jurisdictions[]— a dual-licensed domain’s other operator links.verdict_detailsays how many there are;GET /v1/checkon that domain lists them.as_of— the batch takes noas_of. AskGET /v1/check?as_of=per domain.alternatives[], a per-rowchecked_jurisdictions(it is at the top), or the rest of the single check’s_meta(source_url,scraped_at, …).
Partial success
Section titled “Partial success”One bad entry doesn’t fail the batch. An entry we can’t look up — not a string, longer than 253 characters, or not a hostname even after normalising — comes back in its place as an error row, and every other domain still resolves:
{ "query": { "input": "https://bet365.com/" }, "match": null, "confidence": "none", "error": "invalid_hostname", "error_detail": "Not a hostname: pass a bare hostname — no scheme, path, port or underscores." }error is invalid_hostname for a string that isn’t a hostname (or is longer than
253 characters) and invalid_input for an entry that isn’t a string at all;
error_detail says which. query carries only input — what you sent — since
there is no hostname to report.
An error row has no verdict and no verdict_detail: we did not check it,
so there is nothing to say about it — report it as unchecked, never as not found.
The whole request is refused (400 invalid_query) only when the body itself is
wrong: not JSON, no domains array, an empty array, more than 100 entries, or
a body larger than 64 KB.
Hostnames
Section titled “Hostnames”Each entry is normalised before it is looked up, and query.domain is the
result:
- lowercased, surrounding whitespace trimmed;
- a trailing dot dropped (
bet365.com.→bet365.com); - an internationalised (Unicode) name converted to punycode, the form registers
list (
bücher.example→xn--bcher-kva.example).
query.input is what you sent, as you sent it, so you can join a row back to your
own list. A URL (https://bet365.com/) is not a hostname — strip the scheme and
path yourself. A host and its www. counterpart are the same site; any other
subdomain is a different host
(which host answers).
Limits & semantics
Section titled “Limits & semantics”POSTonly.GET /v1/check/batch(or any other method) answers405 method_not_allowedwithAllow: POST, key or no key; for one domain without a key, useGET /v1/check?domain=.- Max 100 domains per request; paginate beyond.
- Domains are resolved with bounded concurrency server-side — order of
resultsfollows the order you sent. - Counts as one request against your plan quota today.
- Each result uses the same matching as
GET /v1/check: the host and itswww.counterpart (one site), then other hosts on the registrable domain (never taken as your host), then a name match.
Clients
Section titled “Clients”Branch on verdict — only licensed and licensed_provisional mean licensed
now — and keep verdict_detail with the result:
import requests
r = requests.post( "https://api.igregulator.io/v1/check/batch", headers={"Authorization": f"Bearer {KEY}"}, json={"domains": domains[:100]},)r.raise_for_status()for row in r.json()["results"]: sent = row["query"]["input"] # every row has it; an error row has no "domain" if row.get("error"): # Not checked at all — no verdict. Fix the entry and send it again. print(sent, "→ not checked:", row["error"]) elif row["verdict"] == "licensed": print(sent, "→ licensed:", row["verdict_detail"]) elif row["verdict"] == "licensed_provisional": # In force, provisionally: accept per your policy, and recheck later. print(sent, "→ licensed, provisionally (recheck):", row["verdict_detail"]) else: # Not confirmed as licensed by the registers we cover. Not "unlicensed": # review it, with the sentence. print(sent, f"→ review ({row['verdict']}):", row["verdict_detail"])const res = await fetch('https://api.igregulator.io/v1/check/batch', { method: 'POST', headers: { Authorization: `Bearer ${KEY}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ domains: domains.slice(0, 100) }),});if (!res.ok) throw new Error(`HTTP ${res.status}`);const { results } = await res.json();for (const row of results) { const sent = row.query.input ?? row.query.domain; if (row.error) { // Not checked at all — no verdict. Fix the entry and send it again. console.log(sent, '→ not checked:', row.error); } else if (row.verdict === 'licensed' || row.verdict === 'licensed_provisional') { // licensed_provisional: in force, provisionally — recheck later. console.log(sent, `→ ${row.verdict}:`, row.verdict_detail); } else { // Not confirmed as licensed by the registers we cover — review, never "unlicensed". console.log(sent, `→ review (${row.verdict}):`, row.verdict_detail); }}