# Endpoints

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

> Every iGregulator REST endpoint on one page: which work without a key, what each returns, and where to read more — check, batch, operators, licences, webhooks.

Every endpoint has full schemas in the [API reference](https://igregulator.io/docs/api/) and an
interactive executor in the [playground](https://igregulator.io/docs/playground/). This page
is a map, not a reference — use it to pick which surface you need.

## Public (no key)

10 requests per IP per hour on each of these without a key; with a valid
key, your plan's quota applies instead ([rate limits](https://igregulator.io/docs/rate-limits/)).

| Endpoint | Use |
| --- | --- |
| `GET /v1/check` | Verify a domain or licence number in one round trip. Answers with a `verdict` (`licensed`, `licensed_provisional`, `licence_not_active`, `domain_not_listed`, `related_host_listed`, `name_match_only`, `not_found`, `generic_term`) and a quotable `verdict_detail`; a host and its `www.` counterpart are the same site, other subdomains are not (`related_hosts[]`); `match` names the regulator (`regulator_name`), the licence (`license_id` for `/v1/licenses/{id}`) and where its status was published (`status_source_url`, `status_observed_at`); `_meta.register` / `_meta.stale_jurisdictions` say how fresh the registers behind it are. Dual-licensed domains carry a `jurisdictions[]` array, and `match.verification_url` links to the regulator's own verification page where one exists (CW cert / TGC seal). See the [confidence guide](https://igregulator.io/docs/confidence/#verdict--the-answer-in-one-field). |
| `GET /v1/jurisdictions` | List the seven jurisdictions we cover, with name / country / currency / licence types. |
| `GET /v1/operators/search?q=…` | Search operators by display name, slug or registered (legal) name — a case-insensitive substring match. Trading names and domains are not searched; use `/v1/check` for a domain. Unauthenticated: capped at 3 rows. |
| `GET /v1/health/coverage` | Per-jurisdiction scraper freshness — last successful scrape, age in hours, fresh vs stale flag per our SLA (UKGC / AN / TGC / IOM 24 h, MGA / CW / KH 48 h) — plus `domain_coverage` ([methodology](https://igregulator.io/docs/coverage-methodology/)). |

## Authenticated (Bearer token)

| Endpoint | Use |
| --- | --- |
| `POST /v1/check/batch` | Up to 100 domains in one request, resolved like `/v1/check` — `verdict`, `verdict_detail`, `match`, `confidence` on every row, but no `jurisdictions[]`, `alternatives[]` or `as_of`; an entry we can't look up is a per-row `error` with no verdict; `checked_jurisdictions` and `_meta.stale_jurisdictions` once at the top. POST only: any other method is `405 method_not_allowed` with `Allow: POST`, key or no key. See [batch](https://igregulator.io/docs/batch/). |
| `GET /v1/jurisdictions/:code` | Single jurisdiction detail. The code is case-insensitive. |
| `GET /v1/jurisdictions/:code/operators` | Paginated operators holding at least one licence (any status) in that jurisdiction. `?limit=` 1–200 (default 50), `?offset=`. Ordered by internal operator id — stable across pages, not alphabetical. The code is case-insensitive (`ukgc`); an unknown code is `404 jurisdiction_not_found`, as on `/v1/jurisdictions/:code`, not an empty list. |
| `GET /v1/operators/:slug` | Full operator detail — metadata + licences + domains. Each licence carries `status_qualifier` and `license_reference_is_ours` (`true` for KH, TGC, IOM: `license_number` is our reference), with `issued_date` / `expiry_date` as `YYYY-MM-DD`. Each domain carries `verification_url` (the regulator's own page, where one exists) and `verification_page_status` / `verification_page_read_at` — what that page printed about the licence at our latest read, verbatim, and when; a word that isn't "in force" means the page is not proof ([verification_url](https://igregulator.io/docs/confidence/#verification_url--check-us-against-the-regulator)). The slug is case-insensitive. Supports `?as_of=`. |
| `GET /v1/operators/:slug/licenses` | Every licence for one operator (not paginated), with the same `status_qualifier` and `license_reference_is_ours`. Append `?include_history=true` for each licence's status-change log. |
| `GET /v1/operators/:slug/regulatory-actions` | Enforcement actions linked to one operator — fines, warnings, suspensions, revocations. `?limit=` 1–100 (default 20), `?offset=`, `?sort=date_desc` (default) / `date_asc` / `amount_desc` / `type_asc`. An empty list means none is *linked* to this operator, not a clean record: many published actions aren't matched to an operator yet (UKGC 107 of 107 linked, MGA 2 of 160, CW 0 of 18 on 2026-09-28). The response says so itself: `_meta.note` says what the list does and doesn't mean (on an empty list: that it is not a clean record), and `_meta.sources_read` lists the regulator publications we read. |
| `GET /v1/licenses/:license_id` | Licence by uuid. Useful when you want a pinned detail page. Carries the status's own provenance: `status_source_url`, `status_observed_at`, `last_listed_at`, `not_listed_since`; `status_qualifier`; `license_reference_is_ours`; `issued_date` / `expiry_date` as `YYYY-MM-DD`. Supports `?as_of=`. |
| `GET /v1/licenses/:license_id/history` | Status-change timeline for a single licence (the whole timeline, not paginated). Each event has a `note` and the evidence hash (`snapshot_sha256`, `snapshot_fetched_at`) — events are never deleted. **Only `corrected_at` means an event was withdrawn**: it is set, with `correction_note` saying why, when we later found the event wrong. A `correction_note` without `corrected_at` is a reworded note on an event that stands. |
| `GET /v1/export/:dataset` | **Pro and above.** A whole dataset — `licences`, `operators` or `domains` (links) — as one CSV or NDJSON file (`?format=csv\|json`), optionally sliced by `?jurisdiction=` / `?status=`; each row carries its status provenance. 10 exports a day on Pro, 100 on Business; below Pro → `402 export_requires_pro`. No `as_of`. See [export](https://igregulator.io/docs/export/). |

### Watchlist

| Endpoint | Use |
| --- | --- |
| `GET /v1/watchlist` | Summary: `{ count, limit, operators }` — your plan's cap and the 50 most recently added operators. |
| `GET /v1/watchlist/operators` | Your watched operators with a current licence status each. `?limit=` 1–200 (default 50), `?offset=`. |
| `POST /v1/watchlist/operators` | Add `{ "operator_slug": "…" }` → `201`. Already watched → `409`. |
| `DELETE /v1/watchlist/operators/:slug` | Remove → `204`, whether or not it was on the list. |
| `GET /v1/watchlist/events` | Poll for events (the webhook envelope, pulled). `?since=` or `?cursor=`, `?limit=` 1–500 (default 100); a per-hour poll ceiling per plan. See [watchlist](https://igregulator.io/docs/watchlist/). |

### Webhooks

| Endpoint | Use |
| --- | --- |
| `POST /v1/webhooks` | Create `{ url, events, watchlist_only?, description? }` → `201` with the signing `secret`, shown once. |
| `GET /v1/webhooks` | List your endpoints (no secrets). |
| `PATCH /v1/webhooks/:id` | Change `url`, `events`, `active`, `watchlist_only` or `description`. |
| `DELETE /v1/webhooks/:id` | Delete the endpoint → `204`. Its secrets and delivery history, pending deliveries included, go with it. |
| `POST /v1/webhooks/:id/rotate_secret` | Issue a new secret; the old one keeps signing for 7 days. |
| `GET /v1/webhooks/:id/deliveries` | Recent deliveries, newest first. `?status=all` / `pending` / `delivered` / `failed` / `abandoned`, `?limit=` 1–200 (default 100). |
| `POST /v1/webhooks/:id/test` | Send a signed `test.ping` now; returns `{ delivered, http_status, response_body, latency_ms, error }`. |

See [webhooks](https://igregulator.io/docs/webhooks/).

## System

| Endpoint | Use |
| --- | --- |
| `GET /v1/health` | Liveness probe. 200 if postgres + redis are reachable, 503 otherwise. |
| `GET /v1/stats` | Headline counts — licence records by status, per-jurisdiction licences and last register read (with the same fresh/stale rule as `/v1/health/coverage`), operators, linked domains and how many carry the regulator's own certificate or seal, brand pages, status changes read in the last 30 days, withdrawn events — each with its definition in the body (`definitions`). No key and no per-IP cap: one payload for everyone, recounted at most every 10 minutes. The igregulator.io homepage prints it. |
| `GET /openapi.json` | Canonical OpenAPI 3.1 spec. Consume it, generate a client, etc. |

## Response shape conventions

- **Paginated lists** take `?limit=` + `?offset=` and return
  `total`, `limit`, `offset` next to the rows:
  - `GET /v1/operators/search` → `{ q, total, limit, offset, operators, _meta }`
  - `GET /v1/jurisdictions/:code/operators` → `{ jurisdiction_code, total, limit, offset, operators }`
  - `GET /v1/operators/:slug/regulatory-actions` → `{ operator_slug, total, limit, offset, sort, regulatory_actions }`
  - `GET /v1/watchlist/operators` → `{ total, limit, offset, operators }`

  Defaults and maxima differ per endpoint — see
  [pagination](https://igregulator.io/docs/pagination/).
- **Unpaginated lists** return everything in one envelope:
  - `GET /v1/operators/:slug/licenses` → `{ operator_slug, licenses }`
  - `GET /v1/licenses/:license_id/history` → `{ license_id, license_number, history, _meta }`
  - `GET /v1/jurisdictions` → `{ jurisdictions }`; `GET /v1/webhooks` → `{ endpoints }`
- **`GET /v1/watchlist/events`** is cursor-paginated:
  `{ events, next_cursor, has_more }`.
- **Detail endpoints**: `GET /v1/jurisdictions/:code` and
  `GET /v1/licenses/:license_id` return the row itself (the licence with
  a `_meta` block). `GET /v1/operators/:slug` returns
  `{ operator, licenses, domains, _meta }` — its `licenses[]` and
  `domains[]` are complete, bare arrays; `/licenses` returns the same
  licences, plus optional history.
- Timestamps are ISO-8601 UTC (`2026-04-19T12:00:00.000Z`).
- Dates — a licence's `issued_date` and `expiry_date`, `/v1/check`'s
  `match.expires_at`, a regulatory action's `decision_date` — are `YYYY-MM-DD`
  strings: the register publishes a day, not an instant.
- `as_of` takes a `YYYY-MM-DD` date or an ISO-8601 datetime with a UTC offset,
  nothing else
  ([point-in-time](https://igregulator.io/docs/point-in-time/#date-semantics)).
- UUIDs are lowercase, hyphenated, v4.
- Operator slugs are lowercase; `/v1/operators/:slug…` resolves one in any case
  (`Flutter-UK-Limited` finds `flutter-uk-limited`). Jurisdiction codes are
  uppercase and resolve in any case too.
