# Changelog

URL: https://igregulator.io/docs/changelog/
Markdown: https://igregulator.io/docs/changelog.md

> Every change to the iGregulator API, MCP server and docs, newest first: new endpoints and fields, fixes, corrections, deprecations, breaking-change policy.

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.

## Versioning policy

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-change policy

- 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](https://www.rfc-editor.org/rfc/rfc9745)) on affected responses the day the field is marked — a structured-field date, e.g. `Deprecation: @1790812800`.
  - `Sunset` header ([RFC 8594](https://www.rfc-editor.org/rfc/rfc8594)) 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.

## 1.21.0 — 2026-10-01

- **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](https://mcp.igregulator.io/.well-known/oauth-protected-resource/mcp/account));
  `app.igregulator.io` is its authorization server
  ([RFC 8414 metadata](https://app.igregulator.io/.well-known/oauth-authorization-server/api/auth)):
  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](https://igregulator.io/docs/mcp/#connect-from-claudeai-chatgpt-or-claude-code-sign-in--no-key).
- **Connected apps** under [API keys](https://app.igregulator.io/api-keys#connected-apps):
  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.

## 1.20.0 — 2026-10-01

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

### `/v1/check`

- **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](https://igregulator.io/docs/confidence/#which-host-answers--exact-www-then-the-rest-of-the-domain).
- **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`.

### Licences, operators, jurisdictions

- **`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.

### MCP server

- **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`.

### Data — registers and corrections

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](https://app.igregulator.io/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`.

### Docs

- Every client example branches on `verdict` and quotes `verdict_detail`.
- New pages or sections:
  - [which host answers](https://igregulator.io/docs/confidence/#which-host-answers--exact-www-then-the-rest-of-the-domain)
  - [hostnames we accept](https://igregulator.io/docs/confidence/#hostnames-we-accept)
  - what a [batch row](https://igregulator.io/docs/batch/) carries and leaves out
  - [`as_of` date formats](https://igregulator.io/docs/point-in-time/#date-semantics)
- 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`.

## 1.19.0 — 2026-09-30

- **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](https://igregulator.io/docs/badge/).

## 1.18.0 — 2026-09-30

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](https://igregulator.io/docs/confidence/#verdict--the-answer-in-one-field).
  `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.

## 1.17.0 — 2026-09-30

- **`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](https://igregulator.io/docs/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).

## 1.16.0 — 2026-09-30

- **`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](https://app.igregulator.io/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](https://igregulator.io/) prints it under the hero.

## 1.15.0 — 2026-09-30

- **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](https://igregulator.io/docs/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](https://igregulator.io/terms/) (§1) now name the Isle of Man GSC among the
  registers we read. A coverage update only: nothing about your rights or
  obligations changed.

## 1.14.1 — 2026-09-29

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

## Docs corrections — 2026-09-29

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](https://igregulator.io/docs/rate-limits/#response-headers).
- **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](https://igregulator.io/docs/errors/) 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](https://igregulator.io/docs/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](https://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.

## 1.14.0 — 2026-09-28

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

## 1.13.3 — 2026-09-28

- **`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`.

## 1.13.2 — 2026-09-27

- **`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`.

## 1.13.1 — 2026-09-27

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

## 1.13.0 — 2026-09-27

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.

## 1.12.0 — 2026-09-20

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](https://igregulator.io/docs/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.

## 1.11.0 — 2026-09-18

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.

## 1.10.0 — 2026-09-17

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

## 1.9.0 — 2026-07-16

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](https://igregulator.io/docs/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.

## 1.8.0 — 2026-05-29

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](https://igregulator.io/docs/batch/).
- **`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](https://igregulator.io/docs/confidence/).
- **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](https://igregulator.io/docs/point-in-time/).
- **`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](https://igregulator.io/docs/mcp/).

## 1.7.0 — 2026-04-29

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

## 1.6.1 — 2026-04-28

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

## 1.6.0 — 2026-04-28

- **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/](https://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](https://igregulator.io/docs/coverage-methodology/). Phase 4 WHOIS / Tranco enrichment will close this for both regulators in one pass.

## 1.5.0 — 2026-04-28

- **Trust-signals + positioning sweep.** New pages: [/about](https://igregulator.io/about/), [/terms](https://igregulator.io/terms/), [/privacy](https://igregulator.io/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](https://igregulator.io/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.

## 1.4.0 — 2026-04-28

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

## 1.3.1 — 2026-04-28

- **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`.

## 1.3.0 — 2026-04-28

- **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](https://igregulator.io/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.

## 1.2.0 — 2026-04-28

- **Anjouan jurisdiction live.** 5th regulator: Anjouan Gaming Authority (AGA), ~1,275 licences ingested daily from [anjouangaming.com/license-register/](https://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/](https://igregulator.io/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).

## 1.1.0 — 2026-04-28

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

## 1.0.0 — 2026-04-19

**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](https://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/](https://igregulator.io/docs/api/) — unified docs at [/docs](https://igregulator.io/docs/) supersede it.

## 0.3.0 — 2026-04-17

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

## 0.2.0 — 2026-04-08

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

## 0.1.0 — 2026-03-28

- First operational release. UKGC-only coverage.
- Core schema: `operators`, `licenses`, `domains`, `jurisdictions`.
- Dashboard lives at `app.igregulator.io`.
