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", "CW", "IOM", "KH", "MGA", "TGC", "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 7 jurisdictions we cover (AN, CW, IOM, KH, MGA, TGC, 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.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, or more than 100 entries.
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"].get("input", row["query"]["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); }}