Skip to content

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.

Terminal window
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"]}'
{
"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, and input, the string you sent (hostnames).
  • verdict and verdict_detail — branch on the first, quote the second (confidence scoring).
  • match — the same object as on GET /v1/check, or null.
  • confidence — always present; none on a miss.
  • match_absence_reason — only when match is null.
  • related_hosts[] — only when your host (and its www. 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_detail says how many there are; GET /v1/check on that domain lists them.
  • as_of — the batch takes no as_of. Ask GET /v1/check?as_of= per domain.
  • alternatives[], a per-row checked_jurisdictions (it is at the top), or the rest of the single check’s _meta (source_url, scraped_at, …).

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.

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

  • POST only. GET /v1/check/batch (or any other method) answers 405 method_not_allowed with Allow: POST, key or no key; for one domain without a key, use GET /v1/check?domain=.
  • Max 100 domains per request; paginate beyond.
  • Domains are resolved with bounded concurrency server-side — order of results follows 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 its www. counterpart (one site), then other hosts on the registrable domain (never taken as your host), then a name match.

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);
}
}