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.
  • GET /v1/stats gains three additive parts, each with its definition in definitions:
    • jurisdictions.items[].by_status — each register’s licence records by current status, every status present.
    • status_changes_7d — the same changes status_changes_30d counts, over the last 7 days: total, by_status (how many moved to each status) and recent, up to six of them, newest first and at most one per regulator, each with the regulator page it was read from. recent names only operators whose registered name carries a company legal form, so no sole trader is named; the counts include everyone.
    • geo_checks — how many distinct domains we have checked from inside at least one country, from which countries, and since when. An observation, never a licence status.
  • Error suggestion texts no longer say paid plans are closed: a quota, limit, plan_inactive, plan_ended or export_requires_pro answer now points to the Billing page (Pro and Business are invoiced for three months or more). Codes and reasons are unchanged.
  • New 402 reason plan_ended: an account’s plan can now have an end date (for example a plan granted for three months). After it, every keyed request answers 402 payment_required with reason: plan_ended and the date in suggestion, and no webhook is delivered to the account’s endpoints. Keys, watchlist and webhook endpoints are kept; renewing the plan restores them at once. The same request without the Authorization header is answered within the per-IP limit.
  • Webhooks are no longer delivered to accounts whose plan is canceled.
  • Geo-checks: refused now counts as a definite result. Per country the latest definite result is shown, so a newer refused replaces an older reachable (a site that closes a country with a bare 403 no longer keeps showing “Reachable”). When every probe gets 403, a browser in the country is asked too where we have one: if it shows the site the result is reachable (the 403 was a bot wall), if it is refused too the reason says so.
  • GET /v1/check (a domain query): new domain_registration — what the domain’s registry publishes about the registration, read from the registry’s own RDAP server: registered_at, expires_at, last_changed_at, registrar, status (EPP codes such as client hold), source_url (the RDAP record) and read_at. It describes the registered name (stake.com for www.stake.com). Present only when we have read a record; absent for TLDs that publish no RDAP (.de, .es, .se, .be, …). A fact about the domain, never part of the verdict. Re-read every 30 days; new domains first.
  • Export (/v1/export/domains): domain_registered_at, domain_expires_at, domain_registrar from the same record.
  • MCP check_domain passes domain_registration through (dates, registrar, status).
  • New webhook event domain.delisted — the mirror of domain.listed: a register stopped listing sites on an operator’s licence. One event per operator and register per read, each domain with delisted_at, first_seen_at and the source that read it. It is the register’s own statement, never an absence elsewhere, and says nothing about the licence — which licenses shows with its status today, often still active. Only de-listings recorded from today on are sent; earlier ones were never dated. Watchlist and jurisdiction filters apply. See /docs/webhooks.
  • Geo-checks: two results that say what was read instead of unknown. served_unverified (“Reachable, not verified”) — home connections in the country were served the page, but no browser in the country rendered it. refused (“Refused (HTTP 403)”) — every probe in the country got HTTP 403; not a statement that the site geo-blocks the country. Checks already stored with those readings are re-labelled. GET /v1/geo-checks prefers, per country, a definite result over these two, and these over unknown.
  • Geo-checks: does a casino site open in a country its licence does not reach? New for Business (10 domains per UTC month) and Enterprise (no cap):

    • POST /v1/geo-checks requests a domain in one or more of ten markets (GB, SE, NL, DE, ES, IT, FR, BR, CA = Ontario, AU; all ten when left out). One request is one against the allowance, whatever the number of countries; a request recent results answer entirely (from_cache) is free.
    • GET /v1/geo-checks?domain= returns the latest shown check per country.
    • GET /v1/geo-check-requests lists your own requests and their status.

    A check reads the page from home internet connections in each country, reads the page’s own country lists, and renders it in a browser in the country (in_country_render). reachable needs that browser; without it a served page is unknown (“HTML only, no in-country render”). Every reachable is confirmed by a person on its screenshot before anyone sees it. Ontario results carry register: whether iGaming Ontario lists the domain. We also scan domains on offshore licences in the background, so a result does not mean anyone requested it. Results: reachable, geo_blocked, waf_blocked, parked, gateway, not_casino and unknown, each with a reason. This is an observation about a site on a date, never a licence status. Who requested a check is shown to nobody else. See geo-checks.

  • In the dashboard, a domain’s page shows Availability by country to signed-in accounts: results for Pro, results plus a request button for Business and Enterprise. Never on the public page.

  • match no longer follows the freshest read. Among a domain’s listed links with an active licence, match (and jurisdictions[0]) is now the link we have known longest. It used to be the licence read most recently, so a domain licensed in two registers could switch match with the order the registers were read in (bet365.com answered with its Swedish licence, read at 04:35, over its UKGC one, read at 03:00). Freshness is still each entry’s register.last_read_at.

  • New 402 reason geo_checks_require_business — every geo-check endpoint, reads included, is Business and Enterprise (details.available_on) — and 429 reason geo_check_quota_exceeded.

  • verification_page_kind: what the regulator’s page for a domain is. New field next to verification_url on /v1/check (match and every jurisdictions[] entry), on /v1/operators/{slug} domains[], and as a column of GET /v1/export/domains. Values:

    • domain_certificate: a page the regulator issues for this domain alone, such as a CGA Certificate of Operation (cert.cga.cw/certificate) or a Tobique seal.
    • licence_certificate: the CGA certificate for the licence (cert.cga.cw/token), which lists the domain among its “Approved domains”.
    • licensee_page: the MGA verification page for the licensee, which lists the domain among its websites.
    • other: a page we have not classified.
    • null: there is no page.

    For Curaçao, this tells a domain’s own Certificate of Operation apart from the licence’s list of approved domains.

  • The domain export’s verification_url is now given on the CGA’s current host (cert.gcb.cw → cert.cga.cw), as /v1/check already did.

  • /v1/check’s verification_url description now names MGA verification pages, which it has served for MGA links all along.

  • Ukraine: a licence PlayCity terminated by its decision is revoked. The register prints “licence terminated by the agency’s decision of … No. …” with no grounds; such a licence was unknown and is now revoked (six licences). Its upstream_status keeps the register’s wording (“Licence terminated by PlayCity’s decision of … No. …; the register gives no grounds”). A licence whose last entry is a court’s ruling stays unknown. No field changed name or type.

A fourteenth jurisdiction: Ukraine. Additive — no field changed name or type.

  • Ukraine (UA, country UA, currency UAH). The State Agency of Ukraine PlayCity is read nightly at 05:35 UTC: its register of every licence — a PDF linked from playcity.gov.ua — two requests a night. We load its three online licence types: Online casino, Online poker and Bookmaking (all license_category remote) — 53 licences over 36 operators and 38 hosts at the first read. One licence record per issuing decision: a licensee whose 2021 licence ran out and who took a new one has two records.
  • English throughout. The register is in Ukrainian; licence types, operator names (in Latin letters by the official transliteration, legal form in English: SPEISYKS LLC) and upstream_status (an English rendering of the register’s line, built from its dates and decision numbers, never a free translation) are English. The register’s own text stays with each record.
  • license_number for UA is ours: PlayCity prints no licence numbers, so the value is UA/CASINO/<company code>/<decision date>/<decision number> (UA/POKER/…, UA/BETTING/…). license_reference_is_ours is true, license_number_kind is igregulator_reference, and verdict_detail never quotes it as the regulator’s number. It round-trips through /v1/check?license_number=.
  • Statuses come from the licence’s own row. PlayCity prints what happened to each licence: revoked by the agency’s decision → revoked; its term ran out → expired. A licence the agency ended by a decision whose grounds the register does not print is unknown — not revoked: the same wording covers a sanction and the licensee’s own request. A licence whose last entry is a court’s ruling on the agency’s decision is unknown while the case stands. A licence that leaves the register would be not_in_register.
  • Not in our data: land casinos, slot halls, per-machine and per-table licences, bookmaker shops and the state lotteries. /v1/check answers not_found for such a site, which is a limit of our coverage and not a finding that the site is unlicensed. PlayCity’s illegal-sites tracker is a block list, not a licence register, and is not read.
  • /v1/health/coverage: a 24 h freshness SLA, and every loaded licence counts toward domain_coverage. checked_jurisdictions on a /v1/check miss includes UA from the register’s first load; /v1/jurisdictions lists it. verification_url is null — PlayCity publishes no page per domain.
  • MCP: server instructions and tool descriptions name fourteen jurisdictions, and get_jurisdiction accepts UA.
  • Jurisdiction hub: app.igregulator.io/jurisdictions/ukraine.
  • The Terms of Service (§1) now name PlayCity among the registers we read. A coverage update only: nothing about your rights or obligations changed.

A thirteenth jurisdiction: Colombia. Additive — no field changed name or type.

  • Colombia (CO, country CO, currency COP). Coljuegos, Colombia’s gaming regulator, is read nightly at 05:30 UTC: its page of online gaming operators it authorises (“Operadores de Juegos Online Autorizados por Coljuegos”), one request a night — 14 licences over 14 operators and ~15 .co / .com.co hosts at the first read. One licence record per concession contract; license_type_raw is Juegos operados por internet (online gaming concession) and license_category is remote. A domain is the website the page names for the operator.
  • license_number for CO is the regulator’s: Coljuegos’ own contract number, as the page prints it (2182, C2261). So, unlike KH, TGC, IOM, ON, SE, ES, BE and DE, license_reference_is_ours is false, license_number_kind is regulator in exports, and verdict_detail quotes the number. It round-trips through /v1/check?license_number=.
  • Not in our data: land-based gaming (juegos localizados) and the lotteries. /v1/check answers not_found for such a site, which is a limit of our coverage and not a finding that the site is unlicensed.
  • Statuses: active and not_in_register only. active = the concession is on Coljuegos’ page of authorised online operators (the page is the licence’s status_source_url). not_in_register = it is no longer listed. Coljuegos publishes no structured list of ended, suspended or terminated concessions, so revoked, suspended and surrendered are never set for CO, and absence can only ever mean not_in_register — never “unlicensed”.
  • /v1/health/coverage: a 24 h freshness SLA, and every loaded licence counts toward domain_coverage. checked_jurisdictions on a /v1/check miss includes CO from the register’s first load; /v1/jurisdictions lists it. verification_url is null — Coljuegos publishes no page per domain; its page of authorised operators is cited as the licence’s status_source_url.
  • MCP: server instructions and tool descriptions name thirteen jurisdictions, and get_jurisdiction accepts CO.
  • Jurisdiction hub: app.igregulator.io/jurisdictions/colombia.
  • The Terms of Service (§1) now name Coljuegos among the registers we read. A coverage update only: nothing about your rights or obligations changed.

An eleventh and a twelfth jurisdiction: Belgium and Germany. Additive — no field changed name or type.

  • Belgium (BE, country BE, currency EUR). The Belgian Gaming Commission (Kansspelcommissie) is read nightly at 05:20 UTC: its three published lists of active licences, one per online licence class — 81 licences over 76 operators and 79 .be hosts at the first read. We load the three online licence classes of the Gaming Act of 7 May 1999, each the online extension of a land licence: A+ (online casino games), B+ (online arcade games) and F1+ (online betting), as license_type_raw and license_types. One licence record per row the Commission lists; license_category is remote for all. A domain is a site the row names; a site the Commission lists for two operators is linked to both.
  • Germany (DE, country DE, currency EUR). The Gemeinsame Glücksspielbehörde der Länder (GGL) is read nightly at 05:25 UTC: its official whitelist of permitted operators — 72 licences over 50 operators and 123 hosts at the first read. We load the four internet game types the GGL itself permits under the GlüStV 2021: Virtuelle Automatenspiele (virtual slots), Online-Poker, Sportwetten (sports betting) and Pferdewetten im Internet (online horse betting), one licence record per holder and game type, as the whitelist writes them; a retail-only betting permit is not loaded. license_category is remote for all. A domain is a website the whitelist lists for the permit (a listing with a path, such as pokerstars.de/sports, links its host).
  • Not in our data: for Belgium, the land-based licence classes (A, B, C, F2) and the Loterie Nationale; for Germany, online casino table games licensed by a Land (Schleswig-Holstein, Bayern), lotteries (the state DLTB lotteries, lottery brokers, charity lotteries) and land casinos, though the whitelist names them. /v1/check answers not_found for such a site, which is a limit of our coverage and not a finding that the site is unlicensed.
  • Statuses: active and not_in_register only, for both. active = the licence is on the Commission’s list of active licences (its class list is the licence’s status_source_url) or on the GGL’s whitelist (the whitelist page is). A Belgian licence the Commission still lists past its expiration date stays active, with that date as its expiry_date. not_in_register = it is no longer listed. Neither regulator publishes a list of warnings, suspensions, withdrawals or ended permits we can read as a status (the GGL’s public warnings are not licence statuses), so revoked, suspended and surrendered are never set for BE or DE, and absence can only ever mean not_in_register — never “unlicensed”.
  • license_number for BE and DE is our reference, as for KH, TGC, IOM, ON, SE and ES: BE/APLUS/<dossier id>, BE/BPLUS/<dossier id>, BE/F1PLUS/<dossier id> (the Commission publishes a dossier id, not a licence number) and DE/SLOTS/<key>, DE/POKER/<key>, DE/SPORTS/<key>, DE/HORSES/<key> (the whitelist prints no permit number; <key> is made from the holder’s name). So license_reference_is_ours is true on /v1/check and the licence endpoints, license_number_kind is igregulator_reference in exports, and verdict_detail never quotes the reference as a licence number. The references round-trip through /v1/check?license_number=.
  • /v1/health/coverage: a 24 h freshness SLA for both, and every loaded licence counts toward domain_coverage. checked_jurisdictions on a /v1/check miss includes BE and DE from each register’s first load; /v1/jurisdictions lists them. verification_url is null for both — neither regulator publishes a page per domain; the class list or the whitelist is cited as the licence’s status_source_url.
  • MCP: server instructions and tool descriptions name twelve jurisdictions, and get_jurisdiction accepts BE and DE.
  • Jurisdiction hubs: app.igregulator.io/jurisdictions/belgium and app.igregulator.io/jurisdictions/germany.
  • The Terms of Service (§1) now name the Belgian Gaming Commission and the GGL among the registers we read. A coverage update only: nothing about your rights or obligations changed.

A tenth jurisdiction: Spain. Additive — no field changed name or type.

  • Spain (ES, country ES, currency EUR). The Dirección General de Ordenación del Juego (DGOJ) is read nightly at 05:05 UTC: its list of operators with a licence, and each operator’s own page there — 78 operators, 135 licences and 126 .es domains. We load Spain’s state-level online licences under Ley 13/2011: one licence record per general licence (license_type_raw Apuestas, Concursos or Otros juegos), with the singular licences under it (Ruleta, Black Jack, Póquer, Apuestas deportivas de contrapartida, …) as its license_types, as the DGOJ writes them. license_category is remote for all. A domain is one the operator’s DGOJ page lists.
  • Not in our data: the state lotteries — SELAE (Loterías y Apuestas del Estado) and ONCE — which are reserved by law and hold no DGOJ licence, and land-based and regional gambling, which Spain’s autonomous communities license. /v1/check answers not_found for such a site, which is a limit of our coverage and not a finding that the site is unlicensed.
  • Statuses: active and not_in_register only. active = the licence is listed under “Licencias generales” on the operator’s DGOJ page, which is the licence’s status_source_url; not_in_register = it no longer is. The DGOJ states the end of a licence in prose resolutions and its sanctions register names fines, not licence statuses, so neither is read as a status: revoked, suspended and surrendered are never set for Spain, and absence can only ever mean not_in_register — never “unlicensed”.
  • license_number for ES is our reference, as for KH, TGC, IOM, ON and SE: ES/APUESTAS/<key>, ES/CONCURSOS/<key>, ES/OTROS-JUEGOS/<key>, where <key> is the DGOJ’s own page slug for the operator. The DGOJ publishes no licence number, so license_reference_is_ours is true on /v1/check and the licence endpoints, license_number_kind is igregulator_reference in exports, and verdict_detail never quotes the reference as a licence number. The references round-trip through /v1/check?license_number=.
  • /v1/health/coverage: a 24 h freshness SLA, and every loaded licence counts toward domain_coverage. checked_jurisdictions on a /v1/check miss includes ES from the register’s first load; /v1/jurisdictions lists it. verification_url is null — the DGOJ’s operator page lists an operator’s domains, but it is not a page per domain; it is cited as the licence’s status_source_url.
  • MCP: server instructions and tool descriptions name ten jurisdictions, and get_jurisdiction accepts ES.
  • Jurisdiction hub: app.igregulator.io/jurisdictions/spain.
  • The Terms of Service (§1) now name the DGOJ among the registers we read. A coverage update only: nothing about your rights or obligations changed.

Webhooks learn about domains. Nothing existing changes shape; everything here is an addition, with one widening noted below.

  • New event domain.listed. Sent when we record domains under an operator for the first time — one event per operator per read, with all of them in data.domains. Each carries first_seen_at, the regulator’s verification page where there is one, and variant_of for a numbered copy of another of the operator’s domains. operator_first_domains says whether the operator had any domain on record before. See webhooks.
  • Endpoints filter by jurisdiction. POST and PATCH /v1/webhooks accept jurisdictions: ["CW", "AN"]; the endpoint then receives only events about those registers. Empty or omitted = all, which is what existing endpoints keep.
  • register_first_load on license.issued and domain.listed. true during the first week we read a register, when every record in it is new to us whatever its age.
  • Expiry events are no longer lost to a missed day. license.expiring_30d / _60d / _90d were sent only on the exact day; if our emitter was down that day the event never came. They now follow within 7 days. Widening: days_until_expiry was always the window’s number (30, 60, 90) and can now be lower on a late event — it is the days actually left.

The leftovers of the 1.23.0 read-through. A successful API answer keeps its shape; one webhook payload field changes its meaning, and one error code changes.

  • license.issued: data.issued_at is the register’s issue date, or null. It carried the time we first recorded the licence — so a licence a register has listed since 2019, loaded by us last night, was announced as issued last night. That time is now in a new field, data.first_seen_at (ISO-8601 datetime). issued_at is YYYY-MM-DD where the register publishes an issue date and null where it does not. If you parsed issued_at as a datetime, read first_seen_at instead. This changes a field in place, which our policy otherwise forbids: the old value stated something we had not read. Events stored before this release keep their old shape in GET /v1/watchlist/events.
  • What license.issued means is now written down: we recorded the licence for the first time. It is sent when a regulator grants a licence and also, once per licence, when we start reading a register: every licence in it, however old. The event and when it fires are unchanged; issued_at against first_seen_at tells the two cases apart where the register publishes a date.
  • A plan cap holds under simultaneous requests. POST /v1/webhooks and POST /v1/watchlist/operators counted and then inserted in two steps, so two requests sent at the same moment could both pass a cap with room for one; the same was true of two PATCH /v1/webhooks/{id} active: true at once, or one beside a create. Each account’s cap-counted writes now run one at a time: the request over the cap gets the 403 quota_exceeded it always should have.
  • A 409 carries code: conflict. POST /v1/watchlist/operators for an operator already on the list (watchlist_duplicate) and POST /v1/webhooks/{id}/test for an endpoint with no valid signing secret (no_active_secret) answered code: invalid_query — the code of a malformed request, for a request that was well-formed. The status and details.reason are unchanged, and the reference has always said to branch on those. If you matched code === "invalid_query" on a 409, match the status or the reason.
  • HEAD /v1/export/{dataset} no longer runs the export. A HEAD was answered by doing everything a GET does and dropping the body: the query ran and one of the day’s exports was used up. It now goes through the same checks (same 400, 402, 404, 429), reports X-Export-Daily-* as they stand, and answers 200 with the file’s Content-Type and Content-Disposition — without X-Export-Row-Count and X-Dataset-Generated-At, which only a read can know.
  • check_domain_batch: one bad entry no longer fails the call. The tool’s input schema required every entry to be 3–253 characters, so a list holding one string that is not a hostname was refused whole, before the API saw it — while the tool’s description (and POST /v1/check/batch) say such an entry is an error on its own row. The schema now asks for what the API asks for: 1–100 strings.
  • An X-Source request header is dropped at the edge. Our MCP server marks the calls it forwards with X-Source: mcp, and the API believed that header from anyone. It only ever labelled our own traffic statistics — no limit, quota or answer depended on it — and nothing you receive changes.

Fixes from a read-through of the API for bugs. Nothing here changes the shape of a successful answer.

  • A finished delivery is closed. Saving a delivered or abandoned outcome failed in the database, so the delivery stayed pending and was sent again on every worker tick. No endpoint was active while this was live; the first working one would have received the same event_id repeatedly.
  • Deliveries go to the address that was checked. The private-address check and the request used to resolve the hostname separately. The request now connects only to the addresses the check accepted; ::, NAT64, 6to4, Teredo and multicast ranges are refused, and an IPv6 literal in brackets is judged as an address. Redirects are not followed, as before.
  • Resuming an endpoint counts against your plan’s cap. PATCH /v1/webhooks/{id} with active: true answers 403 webhook_quota_exceeded when other active endpoints already fill the cap.
  • GET /v1/webhooks/{id}/deliveries answers 404 for an id that is not one of your endpoints (it answered 200 with an empty list).
  • as_of on a name-only match no longer reports the look-alike operator’s licence history. as_of is resolved for a licence-number query or a host a register lists; otherwise as_of.license_id is null.
  • ?as_of= with no value is a 400, also on /v1/operators/{slug} and /v1/licenses/{license_id} (it was ignored and today’s status was returned).
  • A license_number over 64 characters or an as_of over 40 is reported as such, with details.field, instead of “domain or license_number required”.
  • POST /v1/check/batch: the body may be up to 64 KB. A row’s query.input echoes a string cut to 300 characters, a number or boolean, or null.
  • GET /v1/operators/search matches q literally — % and _ are ordinary characters. A signed-in MCP connection gets the full limit, like a key.
  • /v1/operators/search/<anything> needs a key, like every other operator route.
  • Bad input is a 400. A control character in a URL (%00) and an offset above 1,000,000 used to answer 500.
  • The keyless limit counts IPv6 per /64, not per address.
  • trial accounts are served as Starter: the same endpoints and limits. The 402 endpoint_requires_paid_plan and the 429 prelaunch_daily_cap answers are gone.
  • CORS allows PATCH and DELETE from the allowed origins.
  • The check_domain summary says “iGregulator reference” where the number is ours (KH, TGC, IOM, ON, SE), not “licence”.

Documentation only — nothing changed on the wire, and the API version stays 1.22.0. The API reference was re-read against the code, operation by operation; where the two disagreed, the reference now says what the API does.

  • GET /v1/health returns { status, service, checks: { postgres: { ok, latency_ms, error? }, redis: { … } }, timestamp }. The reference described checks as a map of booleans and left out service and timestamp.
  • GET /v1/jurisdictions. The UKGC example showed "Ancillary"; the value is "Ancillary Remote". license_types here is a hand-set reference list — for MGA and Kahnawake it does not use the words licence records carry (Type 1…, Interactive Gaming Permit / CSPA), so do not validate a licence against it.
  • GET /v1/check never sends confidence: "none": on a miss the field is left out (only /v1/check/batch rows carry "none"). The verdict rules are now listed in the order they are applied — name_match_only is decided before the licence status is looked at. verification_page_status / _read_at are on domain matches only; _meta.stale_jurisdictions also comes with a related-host answer.
  • Errors, per operation. Each 400, 403, 404 and 409 now shows the body that operation sends (the 404 of /v1/jurisdictions/{code} showed “Operator not found”, its 400 a hostname error). A keyed endpoint’s 429 shows the key’s limits (monthly_request_limit_reached, per_second_limit_exceeded), not the per-IP one. code and details.reason are listed in full — method_not_allowed, forbidden and hostname_too_long were missing — as are the extra details fields of limit errors (limit, current_usage, reset_at, plan_tier).
  • Responses that were not listed. /v1/licenses/{license_id}/history can answer 400 (an id that is not a UUID). The four endpoints that work without a key answer 402 when a key is sent and its account has no active plan. /v1/stats ignores a key and never answers 401, 402 or 429.
  • Response headers are listed on every operation: X-RateLimit-*, X-RateLimit-Policy, RateLimit-Policy and X-Upgrade-URL without a key; X-RateLimit-* (your monthly quota) and X-Monthly-Quota-* with one.
  • as_of on /v1/operators/{slug} and /v1/licenses/{license_id} takes the same two formats as on /v1/check — YYYY-MM-DD, or an ISO-8601 datetime with a UTC offset. The reference said only “ISO datetime supported”.
  • Webhooks. A webhook URL may be http:// or https:// (the reference said https only); a refused URL carries one of six SSRF reasons, not always private_ip_blocked. Delivered and abandoned deliveries are kept 30 days, not 7. GET /v1/webhooks/{id}/deliveries answers an unknown id with an empty list, and reads an out-of-range limit as a default instead of refusing it. Delivery status failed is in the vocabulary but nothing sets it. There is no replay endpoint. webhook.endpoint_degraded is not sent, so the rotate-secret page no longer tells you to subscribe to it.
  • Watchlist. GET /v1/watchlist/events with neither since nor cursor returns the last hour. On GET /v1/watchlist, limit is your plan’s cap on watchlist size, not a page size (the endpoint returns at most 50 rows), and license_status is the status of one of the operator’s licences, not a summary.
  • Operators. country is null for almost every operator. In /v1/operators/search, _meta.total_sources is always 1 (a placeholder), and % / _ in q act as wildcards. include_history takes exactly true or false.
  • Export. The NDJSON response now shows the columns of each dataset; an unknown parameter, status or format is a 400, and the order of the checks is stated.
  • Schemas say which fields are always present (required), and nullable fields are typed as such (/v1/health/coverage, /v1/stats).
  • Every operation page printed curl …/v1/check?domain=bet365.com under its path, whatever the operation. Gone.
  • Guides: the regulatory-actions envelope has _meta; a keyless request refused with a 400 still spends one of the ten per hour; the export daily limit sends Retry-After; X-Export-Dataset is a response header; the batch Python sample no longer fails on an error row; a £5 million fine is 500000000 minor units (the webhook guide had one zero too many).

Two new jurisdictions: nine registers, not seven. Additive — no field changed name or type.

  • Sweden (SE, country SE). The Swedish Gambling Authority’s licence register (Spelinspektionen) is read nightly at 04:35 UTC from its Excel export. Two licence types only: commercial online gaming (Kommersiellt online) and betting (Kommersiellt vadhållning) — 139 licences over 80 holders, listing 197 hosts, on 2026-10-07. Not loaded: B2B software permits, land-based gaming, state gaming machines, ships, and public-benefit and state lotteries. Some of those lotteries run websites; /v1/check answers not_found for them, which is a limit of our coverage and not a finding that the site is unlicensed. Statuses: active (the register says Aktiv), not_in_register (the licence left the register) and unknown (any other status word). The register lists valid licences only and the Authority’s supervision decisions do not name the holder in a field we can read, so revoked and suspended are never set for Sweden. issued_date and expiry_date are the register’s. A domain is the host of each listed web address; the address as listed (betsson.com/sv) is in the licence’s raw_data.listed_urls.
  • Ontario (ON, country CA). iGaming Ontario’s operator directory is read nightly at 04:50 UTC — 49 operators and 84 gaming sites on 2026-10-07. The Alcohol and Gaming Commission of Ontario (AGCO) is the regulator that registers operators; iGaming Ontario contracts with them and publishes the only public list, which the AGCO’s own list page points at. What we report is what that directory lists. Statuses: active (listed) and not_in_register (no longer listed) — Ontario publishes no revocation, suspension or exit in a form we read, and the directory gives no dates, no licence type and no number. A domain is the host of each listed site; the directory’s link as published is in raw_data.sites[].url. The directory states an “accurate as of” date of its own and is served through a long cache; last_read_at is when we read it.
  • license_number for ON and SE is our reference, as for KH, TGC and IOM: ON/IGO/<id> (the directory’s own id for the operator), SE/ONLINE/<key> and SE/BETTING/<key>. Neither register publishes a licence or registration number, so license_reference_is_ours is true on /v1/check and the licence endpoints, license_number_kind is igregulator_reference in exports, and verdict_detail never quotes the reference as a licence number. The references round-trip through /v1/check?license_number=.
  • For both, absence from the register can only ever mean not_in_register — never revoked, never “unlicensed”.
  • /v1/health/coverage: both have a 24 h freshness SLA (one request each a night), and every loaded licence counts toward domain_coverage.
  • checked_jurisdictions on a /v1/check miss includes ON and SE from each register’s first load; /v1/jurisdictions lists both. verification_url is null for both — neither publishes a per-domain verification page.
  • MCP: server instructions and tool descriptions name nine jurisdictions, and get_jurisdiction accepts ON and SE.
  • Jurisdiction hubs: app.igregulator.io/jurisdictions/ontario and /jurisdictions/sweden.
  • The Terms of Service (§1) now name iGaming Ontario’s directory and Spelinspektionen among the registers we read. A coverage update only: nothing about your rights or obligations changed.
  • 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.