Skip to content

Changelog

API-level changes only. Internal refactors, scraper updates, and infra changes don’t appear here unless they surface in a response shape or error behaviour.

All /v1/* endpoints are maintained indefinitely. We do not silently retire versioned routes. Future major versions will be introduced under a new path (/v2/*), with v1 and v2 running in parallel for a minimum of 12 months after v2 launch. Migration guidance lands in this changelog at v2 launch.

  • Breaking changes always land behind a new URL path (/v2/...) — we never break a stable endpoint’s response shape in place.
  • Additive changes (new fields, new endpoints, loosened validation) ship at any time without a version bump.
  • Deprecation flow for individual fields inside a stable version:
    • Deprecation header (RFC 9745) on affected responses the day the field is marked — a structured-field date, e.g. Deprecation: @1790812800.
    • Sunset header (RFC 8594) with the removal date as an HTTP-date, minimum 90 days out.
    • Changelog entry below with the migration guidance.
    • For agents: Link: …; rel="deprecation" (RFC 9745) surfaced alongside the header.
    • No field is deprecated today, so none of these headers is sent.
  • Connect from claude.ai, ChatGPT and Claude Code by signing in — no key to paste. Add https://mcp.igregulator.io/mcp/account as a custom connector and sign in with your iGregulator account; the app gets read-only licence lookups — all ten MCP tools — on your plan, counted against your monthly quota exactly as an API key’s calls are. The endpoint is an OAuth 2.1 protected resource (RFC 9728 metadata); app.igregulator.io is its authorization server (RFC 8414 metadata): dynamic client registration, PKCE (S256 only), one scope (mcp:read), one-hour opaque access tokens, rotating refresh tokens, iss on every authorization response. Without a token it answers 401 with WWW-Authenticate: Bearer resource_metadata="…", scope="mcp:read". It also accepts an API key. See MCP server.
  • Connected apps under API keys: every app you approved, where it returns to, when you approved it and when it last got a token — and Revoke, which stops its tokens on the next request.
  • /mcp is unchanged: anonymous for check_domain, search_operators, list_jurisdictions and check_coverage (10/hour per IP), or an API key for all ten. auth.md, the MCP server card and /.well-known/mcp.json now describe both ways in.

Fixes from a blind test by four context-free testers (a KYB analyst on the REST API, a developer integrating it, an assistant on the MCP server, and an edge-case tester). Every field that existed keeps its name and type. Answers that change for the same request are marked Behaviour change.

  • Behaviour change — the licence comes before the listing. A licence that is not active is licence_not_active even when the regulator has also de-listed the domain. It used to be domain_not_listed, which hid a revocation behind “no longer listed”. verdict_detail now says both. 344 stored hosts changed verdict this way (211 surrendered, 93 not_in_register, 18 revoked, 18 expired, 3 pending, 1 suspended).
  • Behaviour change — www.X and X are one site. Both spellings are looked up together, and both get the same answer, from the one the register lists (match.matched_domain). 712 licensed domains queried with www. and 24 www.-listed ones queried bare (betfair.com) used to answer not_found or generic_term.
  • Behaviour change — new verdict related_host_listed, in place of licensed for other subdomains. A host that isn’t listed, on a domain where other hosts are, is never answered as licensed: a listing covers the host the regulator names (zz-fake.unibet.com used to answer licensed).
    • One operator holds those hosts: match describes that operator’s host, with the new match_type: related_host. The verdict is related_host_listed, or licence_not_active / domain_not_listed.
    • Several operators: match is null, with the new match_absence_reason: shared_registrable. The verdict is related_host_listed if one of the hosts is listed now, else not_found. No name match is tried.
    • Both cases: the new related_hosts[] lists those hosts (host, operator, operator_slug, jurisdiction, domain_status; at most 10). The registrable domain is taken from the Public Suffix List with its private section, so customers of a shared hosting platform stay separate. See which host answers.
  • Behaviour change — strict as_of. Only a real YYYY-MM-DD date or an ISO-8601 datetime with a UTC offset is accepted. 2026/09/22, 2026-02-30 and 2026-09-22T10:00 (no offset) now return 400 invalid_query (field: as_of); they used to answer 200 for a different instant. Today’s date now answers as of now; it used to be refused as “future”. The same applies on /v1/licenses/{id} and /v1/operators/{slug}.
  • as_of says which licence it answers for. On /v1/check, as_of gains scope: "licence", license_id, license_number, operator and, on a domain query, link_note: we do not record when a domain was linked to a licence, so the answer is that licence’s status on the date. verdict still describes today. verdict_detail now opens with the answer for your date (“On 2026-03-01 we were not yet tracking … licence …, so its status then is unknown. Today: …”).
  • Licence numbers. Case, spaces and separators are ignored.
    • Behaviour change: a licence-number match is match_type: license_number (it was domain_exact).
    • An earlier UKGC version of a number we hold (055148-R-331498-001) finds the licence as the register lists it now. The new superseded_number ({ requested, current, note }) says so, and verdict_detail opens with the note.
  • Hostnames.
    • The input is trimmed and lowercased, one trailing dot is dropped, and a Unicode name is converted to punycode. query.input echoes what you sent when it differs.
    • A domain longer than 253 characters is 400 hostname_too_long.
  • Behaviour change — batch: one bad entry (not a string, too long, not a hostname) is a per-row error (invalid_input / invalid_hostname) with error_detail, never a whole-request 400. Every row carries query.input, and related_hosts[] where it applies. GET /v1/check/batch is now 405 with Allow: POST (it was 404).
  • jurisdictions[] rows gain license_reference_is_ours, matched_domain and register ({ jurisdiction, last_read_at, fresh, sla_hours } — that link’s own register freshness).
  • Freshness wording. verdict_detail dates the read that listed the domain. A stale register read gets its own clause, naming the register and the date of its last read.
  • Scoping. checked_jurisdictions and _meta.stale_jurisdictions now also come with a name match and a related-host answer.
  • What the regulator’s own page says. verification_page_status and verification_page_read_at are added on match and jurisdictions[]: the licence status the page at verification_url printed at our latest read, verbatim (Active, Revoked, VALID), and when. A word that isn’t “in force” means the page is not proof. It moves no status. A cert.gcb.cw link is served on cert.cga.cw.
  • status_qualifier and license_reference_is_ours on /v1/licenses/{id}, licenses[] of /v1/operators/{slug}, and /v1/operators/{slug}/licenses, as on a /v1/check match. A provisional Curaçao licence no longer reads as plain active there.
  • Behaviour change — dates: issued_date and expiry_date are YYYY-MM-DD, the day the register prints. They were midnight-UTC timestamps (2026-06-09T00:00:00.000Z).
  • domains[] on /v1/operators/{slug} carry verification_url and verification_page_status / verification_page_read_at.
  • Regulatory actions: /v1/operators/{slug}/regulatory-actions adds _meta.
    • _meta.note says what the list does and doesn’t mean. On an empty list, it says that is not a clean record.
    • _meta.sources_read lists the UKGC, MGA and CGA publications we read.
  • Behaviour change: /v1/jurisdictions/{code}/operators with an unknown code is 404 jurisdiction_not_found. It was 200 with an empty list.
  • Jurisdiction codes and operator slugs resolve in any case (ukgc, Flutter-UK-Limited).
  • Keyless 429: X-RateLimit-Reset is the top of the next UTC hour (it was hh:59:59). The body adds details.limit and details.reset_at.
  • Export: the domains export’s standing gains licensed_provisional (link and licence active, with a status qualifier).
  • OpenAPI:
    • info.version is the API version (1.20.0).
    • 401/402/429 are documented on every keyed operation.
    • 400s are documented on /v1/licenses/{id} and /v1/operators/{slug}.
  • Licence history notes no longer point at an internal file.
  • Tool output uses tool names. API sentences that named a REST route (“see GET /v1/check”) now name the tool instead.
  • get_operator:
    • It returns up to 50 domains by default, listed links first, with domains_total, domains_returned and domains_more. domains_limit (up to 1,000) asks for more.
    • Its licences carry status_qualifier and license_reference_is_ours.
  • Tool input:
    • get_jurisdiction takes any case.
    • get_license given a licence number explains that it needs the UUID, and where to get it.
  • New fields pass through check_domain and check_domain_batch: related_hosts, superseded_number, query.input, error_detail, the new as_of and jurisdictions[] fields, and verification_page_status.
  • Empty regulatory actions lead with the note.

Corrections applied on 2026-10-01 at 01:19 UTC. Nothing was deleted: wrong events are marked corrected_at and stay on the record (see corrections).

  • 755 → revoked events withdrawn. They sat on licences the regulator lists as given up: 711 UKGC licences the Commission lists as Surrendered, and 44 Curaçao licences “revoked at the request of the operator”. as_of inside those windows now answers corrected. When a mapping fix re-reads the same wording as a different status, the UKGC and CW loaders now withdraw the old reading themselves.
  • Curaçao “Assessment in progress”. 148 active → pending events, written under the old rule, are withdrawn; the CGA keeps such a licence in force. 13 licences still held pending are back to active (provisional_under_assessment), citing the latest OGL register read, each with a history row explaining why.
  • 95 register domains stored as URLs (https://49s.co.uk/, www.winkbingo.com/) are re-keyed to their hostname, so they now match. Loaders normalise listings: scheme, case, trailing slash and dot, punycode. A listing of a page on a shared host stays a page listing and never licenses the bare host.
  • Kahnawake: the regulator website is https://gamingcommission.ca; the old value did not resolve.

Loader changes take effect at each register’s next read:

  • Kahnawake: upstream_status is null for KH licences. The KGC’s lists publish no status, and “Active” was our word.
  • Anjouan and Tobique: dates now follow each read — AN expiry and issue dates and licence types, TGC expiry dates. Renewals were stuck a year behind (319 AN licences, 34 TGC). Tobique status provenance also moves on a read of an unchanged page.
  • Malta: a verification page whose status line and licence row name different statuses is unknown, with both words kept, rather than either one.
  • Curaçao certificate pages: the verifier records the status each page prints, which feeds verification_page_status.
  • UKGC regulatory actions: a “payment in lieu of a financial penalty” now fills fine_amount_cents.
  • Every client example branches on verdict and quotes verdict_detail.
  • New pages or sections:
  • Keyless counters are per route, and MCP tools count against the route they call.
  • Only corrected_at means an event was withdrawn.
  • Example licence numbers are current, and the headline counts come from /v1/stats.
  • Embeddable licence badge — https://app.igregulator.io/badge/<domain>.svg. An SVG of the verdict /v1/check?domain= returns for the domain (same link, same rank), with the domain and the date of our last read of the record on it. Green only for an active licence on a domain link the regulator lists; amber for a provisional Curaçao licence, or when our last read is older than the register’s freshness target (it then says “last verified”); grey for every other licence status (named as what it is), a de-listed domain (“no longer listed by Anjouan”; the regulator is named) and a miss (“not found in the 7 registers we cover”). No licence number for Kahnawake, Tobique or the Isle of Man, whose registers publish none. ?style=large draws a two-line card with the licence number where there is one. Cached for an hour (Cache-Control: public, max-age=3600, s-maxage=3600); a non-hostname is a 400 badge. Free, no key. Every brand page has the embed code. See Licence badge.

Additive: every existing field keeps its name, type and meaning.

  • verdict and verdict_detail on /v1/check and every batch row. verdict is the answer in one field — licensed, licensed_provisional, domain_not_listed, licence_not_active, name_match_only, not_found or generic_term — derived from status, domain_status, status_qualifier and confidence by the rules on confidence scoring. verdict_detail is one sentence to quote as it stands: it names the regulator, operator and licence, says when we last read the listing, scopes a miss to the registers we cover and names any that are stale, and never calls anything “unlicensed”. In blind tests, agents given status: active, confidence: high and domain_status: delisted answered “licensed”; that response now says domain_not_listed.
  • New match fields: regulator_name (e.g. Curaçao Gaming Authority), license_id (the UUID for /v1/licenses/{id}), license_reference_is_ours (true for KH, TGC and IOM, whose license_number is our reference), status_source_url and status_observed_at (the status’s own provenance, as on /v1/licenses/{id}), matched_domain (the host the register lists — it can differ from the query, e.g. www.) and domain_last_listed_at (when a regulator source last listed that host on the licence; null once it is de-listed — we keep no “de-listed since” time we could stand behind).
  • _meta on /v1/check: checked_at (when the answer was computed, on every response); register — { jurisdiction, last_read_at, fresh, sla_hours } for the matched jurisdiction, the same freshness rule as /v1/health/coverage; and, on a miss or a name-only match, stale_jurisdictions — [{ code, last_read_at }] for the covered registers past their SLA, each of which weakens a “not found”. scraped_at is unchanged; on a name match or a miss it has always been the request time, and is now documented as such.
  • Batch: each row carries verdict, verdict_detail and, when a jurisdiction matched, _meta.register; _meta.checked_at and _meta.stale_jurisdictions are sent once at the top.
  • GET /v1/export/{dataset} — CSV/JSON export (Pro and above). A whole dataset as one streamed file: licences (one row per licence), operators (one per legal entity) or domains (one per domain ↔ operator link), as CSV (?format=csv, the default) or NDJSON (?format=json, a _meta line first), optionally sliced by ?jurisdiction= and ?status=. Every row carries its status provenance (status_source_url, status_observed_at, last_listed_at, not_listed_since) and a license_number_kind that marks the KH, TGC and IOM numbers as iGregulator references. Headers: X-Dataset-Generated-At, X-Export-Row-Count, X-Export-Daily-*. Current state only: as_of is a 400 (as_of_not_supported), as is any parameter exports don’t take. See export.
  • Plans. Pro gets 10 exports per UTC day, Business 100, Enterprise no cap — per account, shared with the dashboard’s new Export button. An export counts as one request against the monthly quota. Starter and legacy trial keys get 402 payment_required, details.reason: "export_requires_pro" (with X-Upgrade-URL); over the daily cap → 429 rate_limited, details.reason: "export_daily_limit_reached", with Retry-After. Pro isn’t open yet — email founder@igregulator.io.
  • MCP: no export tool (a file download isn’t a tool call).
  • GET /v1/stats — headline counts, each with its definition. Licence records (licences.total, licences.by_status — every status present, only active means licensed now); per jurisdiction its licences, active licences, when we last read its register and whether that read is inside its 24 h / 48 h window (the same rule as /v1/health/coverage); operators with at least one licence record; domains with an active link (domains.linked) and how many of those carry the regulator’s own certificate or seal page (domains.regulator_verified); brand pages (the /brands directory’s total); licence status changes a scraper read in the last 30 days (status_changes_30d — first sightings, data-only updates, withdrawn events, our correction migrations, and a read that only puts a status back after a withdrawn event are excluded); withdrawn events (corrections); the latest register read; and generated_at. The body’s definitions says what each number counts. No key and no per-IP cap: every caller gets one payload, recounted at most every 10 minutes (Cache-Control: public, max-age=300). The homepage prints it under the hero.
  • Isle of Man coverage. The Isle of Man Gambling Supervision Commission (IOM, country IM) is the seventh jurisdiction. Its Online Gambling Licensee Register (isleofmangsc.com) is read nightly at 04:25 UTC: 53 licences, all active on the first read, across the three licence types the Commission names (Full, Network Services, Software Supply), and 50 domain links on 27 licensees. The register page is the source; the .xlsx it links is read as a cross-check, and a licence the two disagree on is unknown rather than a guess. A status other than active comes only from the Commission’s Former Licence Holders page: Surrendered → surrendered, Cancelled (a cancellation under OGRA 2001 s.13) → revoked; any other wording sets nothing, and a licence that merely leaves the register becomes not_in_register. The Commission publishes no per-domain verification page, so verification_url is null for IOM.
  • license_number for KH, TGC and IOM is our reference. The Kahnawake, Tobique and Isle of Man registers publish no licence number (the Isle of Man publishes no company number either). The value we return — KH/IG/<slug>, KH/CSPA/<slug>, TGC/B2C/<slug>, TGC/B2B/<slug>, IOM/OGRA/<name key> — identifies the record in our API and round-trips through /v1/check?license_number=, but no regulator issued it: don’t quote it as one. The field is unchanged; the OpenAPI spec now says so.
  • /v1/health/coverage: IOM’s freshness SLA is 24 h, like the other single-fetch registers (AN, TGC) — it had fallen back to the 48 h default — and its domain_coverage counts Full licensees as the consumer-facing subset (14 of 16 have a domain). See coverage methodology.
  • checked_jurisdictions on a /v1/check miss has included IOM since the first load, and /v1/jurisdictions lists it with its licence types.
  • MCP: tool descriptions and server instructions name seven jurisdictions, and get_jurisdiction accepts IOM.
  • The Terms of Service (§1) now name the Isle of Man GSC among the registers we read. A coverage update only: nothing about your rights or obligations changed.
  • Fixed: GET /v1/watchlist/events was empty without a webhook endpoint. It returned only events queued for your endpoints. It now returns every operator event (license.*, regulatory_action.added) for the operators on your watchlist, endpoint or not, plus anything queued for your endpoints, as before. Same envelope and event_id as the webhook. An event we have since withdrawn as wrong is not served.
  • Fixed: each page of /v1/watchlist/events repeated the previous page’s last event, and with limit=1 has_more never turned false. Saved cursors keep working.
  • Fixed: a legacy trial key got 402 on endpoints that work without a key (/v1/jurisdictions, /v1/operators/search, /v1/health/coverage). They now answer it, within the key’s 1,000/day cap.
  • Limit and plan errors no longer send you to the pricing page (paid tiers are not open yet); they name founder@igregulator.io.

Documentation only — nothing changed on the wire. Pages that described the API differently from how it behaves now match the code.

  • Rate-limit headers. On keyed calls X-RateLimit-Limit / -Remaining / -Reset describe your monthly quota, not the per-second limit; X-RateLimit-Policy is only ever tier=unlimited or tier=authenticated, and RateLimit-Policy is sent on keyless calls only. See rate limits.
  • Enforcement. Monthly quotas and per-second limits are enforced now. There is no 10,000/day cap on every plan: the daily cap is 1,000 per key and applies only to legacy trial keys, which are also limited to GET /v1/check. Business is 100 req/s, not uncapped.
  • Errors. Error handling is now the full code + details.reason matrix. The keyless 429 body is { reason: "quota_exceeded", suggestion } (no limit / window_seconds / upgrade_url); a monthly quota 429 is quota_exceeded, not rate_limited; a rejected webhook URL is 400 invalid_query with an SSRF details.reason (there is no invalid_webhook_url code).
  • Webhooks. webhook.endpoint_degraded is reserved and not sent — poll GET /v1/webhooks/:id/deliveries?status=abandoned and watch X-iGregulator-Missed-Deliveries instead. license.status_changed always has a previous_status, and is not sent for a move to expired (license.expired) or for a new licence (license.issued). api_version is a single value; per-subscriber pinning is not built. Seven attempts means the first plus six retries. GET /v1/watchlist/events returned only events queued for your active webhook endpoints (fixed in 1.14.1). The Python polling example now runs.
  • Pagination and endpoints. /v1/operators/:slug/licenses and /v1/licenses/:id/history are not paginated; search and regulatory actions default to 20 rows (max 100); /v1/jurisdictions/:code/operators is ordered by internal id, not by name. Endpoints now maps batch, regulatory actions, watchlist, webhooks and /v1/health/coverage. Operator search matches display name, slug and registered name — not trading names.
  • MCP. Rate-limit headers do not pass through the MCP server; the API’s error body does, as { http_status, … }. Quotas are per account, not per key.
  • Deprecation is RFC 9745 and Sunset is RFC 8594 — we had cited RFC 9745 for Sunset. Neither header is sent today.
  • as_of and _meta. The AI-agents guide now lists every knowledge value (corrected, no_such_license, no_license_resolved were missing). _meta.source_modified_at is always null today.
  • Numbers refreshed to 2026-09-28: 6,597 licences, 5,638 operators, 12,009 domains, per-jurisdiction domain coverage.
  • Verification-page cadence. “Re-read nightly” overstated it: the verifier re-reads up to 500 certificates per jurisdiction per night, oldest first — every Tobique seal nightly, each Curaçao certificate every 2–3 days.
  • Regulatory actions. An empty GET /v1/operators/:slug/regulatory-actions means no action is linked to that operator, not a clean record — many published actions are not matched to an operator yet.
  • The 1.13.0 entry below said to dedupe on type + data; the envelope field is event.
  • Freshness wording. The docs intro no longer says “updated within 24 hours of regulator changes”: every register is read daily, each status shows when we last read it, and live per-register freshness is at igregulator.io/status.
  • Removed links to our source repository, which is private — they answered 404. The MCP page no longer calls the server open source; tool feedback goes to founder@igregulator.io.
  • The MCP server works without a key. initialize and tools/list no longer need an Authorization header, and four tools answer anonymously — check_domain, search_operators (top 3), list_jurisdictions, check_coverage — under the REST API’s public limit of 10 requests per hour per IP (your IP, not the gateway’s). The other six say in their descriptions that they need a free key, and answer an anonymous call with a structured error plus a how_to_fix pointing at signup. A malformed Authorization header is still a 401. The discovery manifests carry auth.required: false and an anonymous block.
  • status_observed_at is the latest read that still says the status. On GET /v1/licenses/{id} and the licences in GET /v1/operators/{slug}, it (and the evidence snapshot behind it) now moves every time we re-read status_source_url and find the same status. It used to stay at the read that first set the status for licences whose status comes from the main register — most UKGC licences cited April. Statuses from enforcement registers and MGA’s verification pages already behaved this way. When a status changed is in license_history.
  • upstream_status no longer contradicts status. When status is revoked, suspended, surrendered or expired and the register’s word says the licence is in force (valid, Active, Licensed, Assessment in progress…), upstream_status is now null. The status came from a different publication — an enforcement register, an advisory notice — and the register row had not caught up; served side by side, the word read as the regulator disagreeing. 10 licences were affected (Anjouan, Kahnawake, Curaçao). A negative word next to active is always shown. The raw register row is unchanged in raw_data on /v1/licenses.
  • Refused and failed requests no longer count against the monthly quota. Any 4xx (bad parameter, unknown slug) or 5xx response is not charged, and its quota headers report usage without it. The per-second burst limit still counts them. Until now every response was charged, although the quota was always meant to bill answers only.

Additive on the wire. Two data corrections change what some Curaçao licences answer.

  • New field status_qualifier on /v1/check → match and on each jurisdictions[] entry, and on the MCP check_domain verdict. Set when status is right but not the whole truth; null otherwise. Today it has one value: provisional_under_assessment — a Curaçao licence the CGA register lists as “date Assessment in progress”. Under the CGA’s transitional regime a provisional licence past its stated term stays in force until the CGA communicates a final decision, so status stays active (and expires_at may be in the past). Treat it as licensed, flag that a decision is outstanding, and re-check. 263 licences carry it today. We considered answering pending and did not: that would tell you a licensed operator is not licensed.
  • Curaçao: “Revoked as of …, at the request of the operator” is now surrendered (43 licences), not revoked. The CGA’s verb, the operator’s act — the same distinction UKGC Surrendered got in 1.10.0. upstream_status keeps the CGA’s exact words. A licence the CGA also lists in its Enforcement Register stays revoked.
  • Webhooks: fixed a bug that re-sent the most recent event every minute. The emitter’s cursor lost microseconds and re-matched its own last row. If you received repeated events whose data was identical, they were duplicates of one event — dedupe on event + data.
  • Provenance: those 263 provisional licences now cite the latest register read as their status source, and 54 Curaçao status changes that were never written to licence history now have a history row saying so.

Additive on the wire. One of these changes what as_of answers for a small set of licences — read the first bullet if you use point-in-time.

  • as_of no longer serves back events we withdrew. New knowledge value: corrected, with status_as_of: null and a correction object (changed_at, withdrawn_status, corrected_at, note). The 93 revocations we had inferred from a licence dropping off a register were corrected on 2026-09-17 (see 1.10.0), but history is never deleted here, and as_of read the latest event without checking whether it had been withdrawn — so a date inside one of those windows still answered revoked / observed. It now answers corrected. We do not fall back to the previous status either: that would report active for a period in which the register did not list the licence. If you branch on knowledge === 'observed', nothing changes for you — corrected lands in your “unknown” path, which is where it belongs. See point-in-time.
  • /v1/check — _meta.source_url now cites the page that published the matched licence’s status, as /v1/licenses/{id} has since 1.11.0. For most licences that is still the register. Where it is not — a Curaçao revocation from the CGA Enforcement Register, an MGA status from the company’s verification page, a certificate-backed Curaçao licence — you now get that page instead of a register that never said so.
  • Fixed: ?as_of= on /v1/check was silently ignored. The parameter has been documented since point-in-time shipped, but on /v1/check — and therefore on the MCP check_domain tool — it never reached the handler: the response carried no as_of object and match.status was simply today’s. /v1/operators/{slug} and /v1/licenses/{id} were not affected. It works now, and a malformed or future as_of answers 400 invalid_query instead of 200. If you relied on /v1/check?as_of= for a historical verdict, you were reading the current status — check for the as_of object in the response and re-run those lookups.
  • MCP: get_operator returns license_id on each licence. It is the UUID get_license and get_license_history take, and no tool returned it, so those two were unreachable from an agent. Their descriptions now cover the provenance fields (status_source_url, status_observed_at, not_listed_since, last_listed_at) and corrected history events.

Additive. Every status now says where it came from, and one billing bug is fixed.

  • Per-status provenance on licences — status_source_url, status_observed_at and last_listed_at on GET /v1/licenses/{id} and on the licences inside GET /v1/operators/{slug}. A licence’s register (source_url) is often not where its status was published: a Curaçao revocation comes from the CGA Enforcement Register, an Anjouan suspension from the Authority’s suspended-licences page, a Kahnawake termination from an advisory notice. status_source_url is that page; last_listed_at is when we last saw the licence in its register.
  • _meta.source_url for a licence now cites the status’s source (falling back to the register when they are the same page). If you show “source” next to a status, this is the URL that actually published it.
  • Evidence on history events — GET /v1/licenses/{id}/history gains snapshot_sha256 and snapshot_fetched_at: the hash of the bytes we read and when we first read them. It is the hash, not a file — checkable against your own copy of the regulator’s page. null where we hold no stored copy — we started keeping the bytes on 2026-09-17.
  • Enforcement statuses are read from the regulator’s own publication on every run, in every jurisdiction that has one: AGA’s revoked / suspended lists, the CGA Enforcement Register, KGC’s advisory notices, the MGA enforcement register. Tobique publishes none, so a Tobique licence can only ever be active or not_in_register. A “Notice of Cancellation” from the MGA is a notice, not a cancellation, and changes no status.
  • MGA statuses are now read, not assumed. Every MGA licence used to be active because the register “only lists live licensees”. We now read the per-licence word the MGA publishes: Licensed/Approved → active, Surrendered → surrendered, Expired → expired, Suspended/Voluntary Suspended → suspended (verbatim wording in upstream_status). A handful of MGA licences changed status as a result. A word we have never seen maps to unknown, never to a default.
  • GET /v1/operators/search — _meta.scraped_at is now when we last read a register about that operator (its freshest licence check). It used to be the operator row’s own timestamp, which only moves on a rename — so it claimed April for records verified three days ago. _meta.freshness_range follows.
  • Fixed: authenticated calls to /v1/operators/{slug}, /v1/licenses, /v1/watchlist and /v1/webhooks were metered twice (fixed 2026-09-16). The per-second limit behaved as half your plan’s, and monthly quota was consumed 2× on those routes. If you saw unexplained 429s on operator lookups, that was us. September usage counters for affected keys may still read high until the 1 October reset.

Additive on the wire, but two status values changed on existing records. If you store our status verbatim, re-read the licences you hold.

  • Two new license_status values: not_in_register and surrendered. Neither is a revocation, and neither is active.
    • not_in_register — the regulator’s public register no longer lists the licence, since not_listed_since. Registers drop rows for many reasons — expiry, surrender, renumbering, a bad day on their side — and none of them is published, so we do not publish one either.
    • surrendered — the operator gave the licence up. UKGC publishes it verbatim (“Surrendered”); Kahnawake calls it a voluntary termination.
  • What changed on existing records. Licences that had dropped off a register were reported as revoked. That asserted an enforcement decision no regulator had made, and it was wrong: 80 licences across six jurisdictions carried it. They are now not_in_register, except the seven where a regulator does publish an explicit decision (those are revoked or suspended, sourced to that publication). Separately, 627 UKGC licences published as Surrendered were mapped to revoked and are now surrendered. Every corrected record’s history says what happened and when, and the superseded event is kept and marked corrected rather than deleted.
  • If you branch on status, if (status === 'revoked') block() now misses these values — by design, because they are not revocations. Approve only on active and route the rest to a human. GET /v1/licenses/{id}/history gains note, corrected_at and correction_note; licences gain not_listed_since.
  • upstream_status is null when a licence is not_in_register: there is no current regulator wording for a licence the register does not list, and repeating the last one we read produced pages that said “revoked · upstream: valid”.

All additive — no breaking changes.

  • verification_url on /v1/check. Where the regulator publishes a per-domain verification page — a Curaçao certificate (cert.cga.cw) or a Tobique validation seal (validate.thetgc.ca) — match.verification_url links straight to it, so you can confirm any verdict against the primary source in one click. null where the regulator has no such page (UKGC, MGA, KH, AN). We re-read every stored verification page nightly and move domain_status only on a clean read of the regulator’s own words — in both directions, never on a guess.
  • Multi-jurisdiction domains: jurisdictions[] on /v1/check. The data model is now many-to-many — a brand can be licensed by two legal entities in two jurisdictions at once (spinsup.com: Anjouan + Tobique; me88.com: Anjouan active + Curaçao). When a matched domain has more than one (operator, jurisdiction) pair, the response carries a best-first jurisdictions[] array with per-link status, domain_status, domain_association, and verification_url. Per-link status means “delisted in Curaçao” can no longer clobber “active in Anjouan” — both are true at once, and now both are visible. Absent for ordinary single-licence domains.
  • Curaçao domain coverage 0% → 91%. Curaçao publishes no bulk domain list, so we built the harvest around the regulator’s own verification portal: the CGA /token registry (which lists each licence’s complete approved-domain cluster — and is more complete than the published OGL PDF) plus archived certificate URLs. ~3,500 CW domains loaded, every one carrying its regulator verification_url, kept fresh by the nightly re-verification pass and a weekly /token discovery sweep.
  • Tobique domain coverage 0% → live. Same authoritative-seal approach against validate.thetgc.ca: one seal page lists the licence’s whole registered-website cluster. Weekly headless-render discovery finds new seals (they’re JS-injected in casino footers, invisible to plain fetches); the nightly shared verifier keeps every found seal current. Coverage is growing run by run — see coverage methodology.
  • MCP check_domain output now includes verification_url and (when present) the compact jurisdictions[] array, so agents cite the regulator’s own page and never phrase a single-register verdict about a dual-licensed brand.
  • OpenAPI: CheckMatch.verification_url, the DomainJurisdiction schema and CheckResult.jurisdictions are documented in the spec.

All additive — no breaking changes.

  • Batch domain check. New POST /v1/check/batch (authenticated) resolves up to 100 domains in one request; each result mirrors the single-check shape, checked_jurisdictions is returned once at the top, and a malformed hostname comes back as a per-row error without failing the batch. See Batch domain check.
  • match_absence_reason + checked_jurisdictions on /v1/check. When match is null, the response now says why — generic_term (ambiguous label) vs no_record_found (checked, not present) — and lists the exact registers checked, so a “not licensed” verdict is scoped to coverage, never absolute. Present only on a miss. See Confidence scoring.
  • Point-in-time lookups (?as_of=). /v1/check, /v1/licenses/{id}, and /v1/operators/{slug} accept ?as_of= to reconstruct a licence’s status as of a past date from transition history — strictly within our observation window (knowledge: observed | before_tracking | no_such_license; never extrapolated before tracking_since; future dates 400). See Point-in-time lookups.
  • domains[].association on operator detail. GET /v1/operators/{slug} now returns direct / white_label per domain (the column was always populated; the field was missing from the response).
  • /v1/check domain matching now collapses www / apex / subdomain variants via the Public Suffix List (registrable-domain fallback after exact-host), so virginbet.com and www.virginbet.com resolve to the same operator. Single-owner guard prevents over-matching shared white-label platform domains.
  • Rate-limit headers on authenticated responses. X-RateLimit-Limit / -Remaining / -Reset + X-Upgrade-URL now accompany authenticated /v1/* responses, not just the public path.
  • OpenAPI completeness. GET /v1/operators/{slug}/regulatory-actions and GET /v1/health/coverage are now in the spec; ApiError.code documents payment_required + quota_exceeded.
  • MCP server caught up to the API. New tools check_domain_batch, get_operator_regulatory_actions, check_coverage; check_domain + get_operator accept as_of. Tool output is now lean (a compact verdict, not the full REST payload) — and match_absence_reason + checked_jurisdictions are preserved on a miss, so agents never collapse “not found” into “unlicensed”. See MCP server.
  • Tobique Gaming Commission (TGC) re-enabled — 6th jurisdiction live. The Cloudflare Worker proxy at igregulator-scraper-proxy.scvgr-agent.workers.dev now bridges btc → thetgc.ca, bypassing the IP-reputation block that paused us in 1.6.1. Same scraper code, same daily 04:15 UTC slot — only the transport changed (HMAC-signed POST to the Worker, the Worker fetches the upstream CF→CF). Scraper opts in via USE_PROXY=true; other scrapers unaffected.
  • New @igregulator/scraper-utils package carrying the reusable proxyFetch helper. Future jurisdictions whose upstream blocks our IP can opt in by setting two env vars (USE_PROXY=true, PROXY_HMAC_SECRET=…) and adding their hostname to the Worker’s ALLOWED_DOMAINS list — no code change in the scraper itself.
  • Migration 0018 re-inserts the TGC jurisdiction row removed in 0017.
  • Tobique Gaming Commission (TGC) ingestion deferred. PR #119 shipped the scraper, but the upstream thetgc.ca blocks the production origin IP at the Cloudflare edge (HTTP 403 across all UAs and header shapes). Other hosts return 200; this is an IP-reputation block on the Hetzner range. We’ve surgically rolled the public surfaces back to 5 jurisdictions while the scraper code stays merged. Re-enables cleanly once we land a Cloudflare Worker proxy on scvgr-agent.workers.dev. Investigation + path-forward documented in an internal write-up.
  • Tobique Gaming Commission (TGC) added — 6th jurisdiction. (Reverted on the public surface — see 1.6.1. Scraper code stays merged for re-enable when the proxy lands.) ~160 licences ingested daily from thetgc.ca/license-holders/. License-type vocabulary B2C / B2B. License numbers are synthesised as TGC/<TYPE>/<slug> (TGC doesn’t publish IDs, same convention as KH). Cron 04:15 UTC; regulatory tier shifted +15 min. The fuzzy + domain-exact match flow on /v1/check includes TGC automatically.
  • Domain coverage 0% out of the box for TGC. Same upstream-doesn’t-publish-websites bucket as Curaçao. Documented at /docs/coverage-methodology. Phase 4 WHOIS / Tranco enrichment will close this for both regulators in one pass.
  • Trust-signals + positioning sweep. New pages: /about, /terms, /privacy. Footer reorganised — Company column now lists About / Changelog / Terms / Privacy. Stale “MCP server (soon)” replaced with a live link.
  • Legal disclaimer surfaced. Now rendered as a callout at the top of /docs and embedded in the OpenAPI spec’s top-level info.description so agents reading the spec see it. Same wording: results are informational, customers responsible for their own compliance decisions.
  • Hero copy iteration — pain-driven. “iGaming licensing intelligence API” → “Verify gambling operator licenses before they cost you.” Sub-copy mentions the buyer profiles (compliance teams, payment providers, affiliate networks) explicitly. Meta description, og:description, llms.txt opening line aligned. No data or endpoint changes.
  • MGA domain enrichment. The MGA scraper now fans out from each B2C license to the legacy authorisation.mga.org.mt/verification.aspx page and pulls the Website URL(s) field. Runs daily in the same 03:15 UTC slot as the primary register pass, ~30 s wall time at p-limit 5, no separate cron entry. Recovers ~110 B2C operator domains from a previous baseline of zero. B2B / CRP licenses are skipped — the upstream verification page omits the Website URL section for non-consumer-facing license classes.
  • Coverage methodology update. /v1/health/coverage now exposes domain_coverage.{operators_with_domain, normative_operators, coverage_pct} per jurisdiction. The denominator is operators where domain disclosure is normative for their license type — excludes B2B-only types (CSPA, B2B, Non-Remote, Ancillary, supplier permits, etc.) that don’t have consumer-facing domains by design. Methodology change only; no data changes. Affected metrics under the new denominator:
    • AN — 98% of B2C operators have ≥1 domain
    • CW — 0% (upstream ceiling, no scraper-side work possible)
    • KH — 69% of Interactive Gaming Permit holders (gap is upstream non-disclosure)
    • MGA — 0% pre-1.4.0 enrichment, ~90% post
    • UKGC — 40% of Remote-license operators (upstream meaningful ceiling, the rest don’t operate consumer sites)
  • UKGC parser audit closed without code changes. Time-boxed re-audit confirmed the ~17.3% raw figure is 96% of the 482-operator meaningful ceiling (Active + White Label distinct accounts in domain-names.csv). Inactive-domain rows aren’t ingested deliberately to avoid stale /v1/check matches. Findings documented in an internal write-up.
  • MCP server verified live in production. End-to-end smoke against https://mcp.igregulator.io/mcp: tools/list returns all 7 tools, check_domain resolves UKGC + AN matches with correct confidence hints, api_request_log rows tagged source='mcp'. DNS via Cloudflare proxy, TLS via the existing Origin CA cert (extended to cover the new subdomain). Pricing page lists MCP support on Starter onwards. Claude Code path documented at /docs/mcp via claude mcp add --transport http.
  • MCP server live at mcp.igregulator.io. Streamable-HTTP transport (current MCP spec, SSE for streaming). Seven tools exposed: check_domain, search_operators, get_operator, list_jurisdictions, get_jurisdiction, get_license, get_license_history. Bearer-token auth — same API keys as the direct HTTP API; tool calls forward to api.igregulator.io and count against your existing per-key quota. Setup walkthrough at /docs/mcp. Discovery manifests at /.well-known/mcp.json on all three iGregulator surfaces.
  • api_request_log.source column added. New requests are tagged http or mcp so admin analytics can split MCP usage from direct-HTTP usage. No client-visible change.
  • Anjouan jurisdiction live. 5th regulator: Anjouan Gaming Authority (AGA), ~1,275 licences ingested daily from anjouangaming.com/license-register/. License-type vocabulary B2C / B2B / White Labeling. The fuzzy + domain-exact match flow on /v1/check includes AN automatically — no client-side change needed. Coverage table at /docs/ refreshed.
  • Marketing copy correction. Hero / meta / llms.txt now say “Daily-refreshed … updated within 24 hours” instead of “Real-time.” The data was never real-time; the new wording matches what scrapers actually deliver (cron 03:00–04:00 UTC, depending on jurisdiction).
  • Flat field removal on /v1/check. Top-level legacy keys (licensed, jurisdiction, license_number, operator, status, expires_at) removed along with the Deprecation / Sunset / Link headers. Read match.* instead. Removed ahead of the announced 2026-05-19 sunset because no customers are integrated against the flat shape yet.
  • Pre-launch surface gating. trial keys are now restricted to /v1/check only (1,000/day per-key cap). Other authenticated endpoints return 402 payment_required with details.reason=endpoint_requires_paid_plan. Behaviour lifts automatically once PRELAUNCH_DAILY_CAP=0 (post-Stripe).
  • Webhook retention 7 → 30 days. webhook_deliveries history now matches the webhook_events replay window. Existing rows live longer immediately; nothing to migrate.
  • Generic-label blocklist on /v1/check widened. Now substring-match instead of exact-match — bestcasino.com, casino-bonus.com return confidence: low like casino.com already did. Licensed brands containing a generic keyword (bet365.com, pokerstars.com) still resolve via the domain hit before the gate runs.
  • OpenAPI spec documents the per-jurisdiction license_types vocabulary inline so client-side enums can be coded against the audited values (UKGC Remote/Non-Remote/Ancillary Remote, MGA Type 1-4/B2B/B2C, CW B2C/B2B, KH Interactive Gaming Permit/CSPA).

Initial public docs release.

  • /v1/check response shape: { query, match, alternatives, confidence }. match.confidence ∈ high | medium | low, match_type ∈ domain_exact | trading_name_fuzzy | name_similarity, domain_association ∈ direct | white_label | null.
  • Public endpoints: /v1/check, /v1/jurisdictions, /v1/operators/search. Rate-limited 10 req / IP / hour.
  • White Label ingestion live for UKGC — domains with Status = 'White Label' in the UKGC register now load with association = 'white_label' on the domain row.
  • OpenAPI 3.1 spec available at api.igregulator.io/openapi.json (canonical source; Scalar + starlight-openapi both consume it). The old Swagger UI on api.igregulator.io/docs now 301-redirects to /docs/api/ — unified docs at /docs supersede it.
  • Regulatory actions surfaced on the operator detail page. Cross-jurisdiction feed: UKGC Public Register, MGA Decisions, CGA Warnings.
  • New endpoint: /v1/operators/:slug/regulatory-actions (authenticated).
  • Kahnawake Gaming Commission jurisdiction added.
  • Curaçao scraper migrated to the post-LOK OGL PDF source.
  • Licence category harmonisation: remote | non-remote | ancillary | permit | other.
  • First operational release. UKGC-only coverage.
  • Core schema: operators, licenses, domains, jurisdictions.
  • Dashboard lives at app.igregulator.io.