{"openapi":"3.1.0","info":{"title":"Registrum API","description":"\n# Registrum API\n\nUK company intelligence — structured financials, director networks, and intelligent caching — from a single developer-friendly REST API.\n\n---\n\n## Quick Start\n\n### 1. Get your API key\n\nSign up to receive your API key.\n\n### 2. Authenticate your requests\n\nInclude your API key in the **`X-API-Key`** header with every request:\n\n```\nX-API-Key: reg_live_your_key_here\n```\n\n> **Using the interactive docs below?** Click the green **Authorize** button at the top of this page, paste your API key, and click **Authorize**. All subsequent \"Try it out\" requests will include it automatically.\n\n### 3. Make your first request\n\nSearch for a company:\n```\nGET /v1/search?q=tesco\n```\n\nGet an enriched company profile:\n```\nGET /v1/company/00445790\n```\n\n### Client libraries & examples\n\n- **Python**: [companies-house-api-python-starter](https://github.com/RegistrumUK/companies-house-api-python-starter) — auth, rate-limit backoff, pagination, and the iXBRL-vs-PDF gotcha, in one file\n- **Node.js**: [companies-house-api-node-starter](https://github.com/RegistrumUK/companies-house-api-node-starter) — same, using the built-in `fetch`\n- **MCP (Claude, Cursor)**: [registrum-mcp](https://github.com/vdmeu/registrum-mcp) server, with a [prompt cookbook and example scripts](https://github.com/RegistrumUK/registrum-mcp-examples)\n\n---\n\n## How It Works\n\nThis API sits on top of the [Companies House public API](https://developer.company-information.service.gov.uk/) and adds:\n\n- **Intelligent caching** — responses are cached so repeat queries are instant and you never hit rate limits\n- **Enriched profiles** — company age, accounts status, size classification, and more in a single call\n- **Director networks** — discover all companies connected through shared directors\n- **Structured financials** — parsed iXBRL filings returned as clean JSON with current + prior year\n\n## Upstream Call Costs & Throughput\n\nEach endpoint makes a different number of calls to the Companies House API on your behalf.\n**Cached responses cost nothing** — once a company is fetched, repeat requests within the cache window are instant.\n\n| Endpoint | Upstream calls (cold cache) | Cache duration |\n|---|---|---|\n| `GET /v1/search` | 1 per unique query | 1 hour |\n| `GET /v1/company/{n}` | 1 per company | 30 days |\n| `GET /v1/company/{n}/directors` | ~N+2 (N = number of directors) | 30 days |\n| `GET /v1/company/{n}/network?depth=1` | ~N+2 | 30 days |\n| `GET /v1/company/{n}/network?depth=2` | 50–200 (capped at 30 connected companies) | 30 days |\n| `GET /v1/company/{n}/financials` | 2–3 (filing history + doc metadata + iXBRL download) | 90 days |\n| `GET /v1/health` | 0 | — |\n| `GET /v1/health/deep` | 1 | — |\n| `GET /v1/health/probes` | 0 | — |\n\n**Safe throughput on cold cache (approximate, across all customers):**\n- Search and company profiles: up to ~100 unique queries per minute\n- Directors and network (depth=1): up to ~8 different companies per minute\n- Network (depth=2): up to ~2–3 different companies per 5 minutes\n\nIf these rates are exceeded, responses slow briefly (typically 5–15 seconds) while upstream capacity\nrecovers — you won't receive errors, just slightly higher latency. The cache eliminates this concern\nfor any company queried more than once within the cache window.\n\n## Response Format\n\nEvery endpoint returns responses in this standard format:\n\n```json\n{\n  \"status\": \"success\",\n  \"data\": { ... },\n  \"cached\": true,\n  \"cache_age_seconds\": 3421,\n  \"credits_used\": 1,\n  \"credits_remaining\": 487\n}\n```\n\n## Response Headers\n\nEvery response includes:\n- `X-Request-Id` — unique ID for this request (useful for debugging/support)\n- `X-API-Version` — current API version\n- `X-Data-Stale: true` — only present when serving expired cache during an upstream outage\n\n## Rate Limits\n\n| Tier | Price | Calls/month | Calls/day | Burst |\n|------|-------|-------------|-----------|-------|\n| Free | Free | 50 | 15 | 10/min |\n| Web | £9/mo | 500 | 50 | 30/min |\n| Pro | £49/mo | 4,000 | 400 | 100/min |\n| Enterprise | £149/mo | Unlimited | Unlimited | 150/min |\n\n**Pro/Enterprise-only features:** `network?depth=2`, PSC chain traversal, and ECCTA compliance monitoring.\nFree/Web requests to these return `403 PLAN_REQUIRED` with an `upgrade_url`.\n\n## Errors\n\nErrors return a JSON object with `status: \"error\"` and a human-readable `detail` message:\n\n```json\n{\n  \"status\": \"error\",\n  \"detail\": \"Company 99999999 not found\",\n  \"request_id\": \"a1b2c3d4-...\"\n}\n```\n\n| Status Code | Meaning |\n|-------------|---------|\n| 400 | Invalid input (e.g., bad company number format) |\n| 401 | Missing or invalid API key |\n| 404 | Company not found |\n| 422 | Validation error (e.g., search query too long) |\n| 429 | Rate limit exceeded |\n| 502 | Companies House API is temporarily unavailable |\n| 500 | Unexpected server error |\n\n## Usage Dashboard\n\nTrack your API calls, quota, and reset date at **[registrum.co.uk/dashboard](https://registrum.co.uk/dashboard)**.\nSign in with the email you used to get your key — no password needed.\n\nOr call `GET /v1/usage` (free, no credit consumed) to read quota programmatically.\n\n## Need Help?\n\nInclude the `X-Request-Id` from your response headers when contacting support — it helps us trace exactly what happened.\n","version":"0.1.0"},"servers":[{"url":"https://api.registrum.co.uk","description":"Production"}],"paths":{"/v1/health":{"get":{"tags":["Health"],"summary":"Liveness check","description":"Check if the API server is running.\n\nReturns 200 with uptime and version info. No authentication required.\nUse this endpoint for uptime monitoring (e.g., UptimeRobot, Better Uptime).\n\n**Upstream usage:** 0 — no upstream calls made. Safe to poll at any frequency.","operationId":"health_v1_health_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HealthResponse"}}}}},"security":[]}},"/v1/health/deep":{"get":{"tags":["Health"],"summary":"Dependency health check","description":"Check that all upstream dependencies are reachable.\n\nTests connectivity to the Companies House API and Supabase database,\nand reports per-dependency status and response times. No authentication required.\n\nAlso reports **`api_self_check`** — not an upstream, but this API's own\nself-check probe: whether it is still running here, and what it last saw\nwhen it called `GET /v1/company/{n}` through the public hostname. An\nexternal uptime monitor watching this endpoint for the word `unhealthy`\ntherefore also catches the case where our internal monitoring has stopped.\n\n- **healthy** — all dependencies responding normally\n- **degraded** — one or more dependencies are slow or unreachable (the API will still serve cached data)\n\n**Upstream usage:** 1 upstream call to the Companies House API (a lightweight search probe).\nDo not poll this endpoint at high frequency — once per minute is sufficient for monitoring.","operationId":"health_deep_v1_health_deep_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DeepHealthResponse"}}}}},"security":[]}},"/v1/health/probes":{"get":{"tags":["Health"],"summary":"Latest background health probe results","description":"Return the latest result from each background health probe.\n\nProbes run automatically on a schedule. The headline ones:\n\n- **api_self_check** (every 5m) — calls `GET /v1/company/{reference}` on the\n  public hostname, with a real API key, and requires all three of: HTTP 200,\n  the right company in the body, and a response inside a 5-second budget.\n  This is the only probe that asks the customer's own question. A 200 alone\n  is not a pass: during the 2026-08-13 outage `/v1/health` answered 200 at\n  12-15 seconds while ~90% of real traffic was 499/502.\n\n- **event_loop_lag** (every 60s) — measures how long the API's event loop\n  takes to reschedule a sleeping task. The API runs a single event loop, so\n  this delay is paid by *every* concurrent request, including this one.\n  Unlike per-request `duration_ms`, it sees work that blocks the loop after\n  a response is sent or inside a background job (CH-Api#110).\n  `max_lag_ms` under 250 is healthy; over 1000 means requests are queueing.\n\n- **ch_api_profile** (every 6h) — fetches a real company profile and verifies\n  the response fields our code depends on are still present and correctly typed.\n  Detects silent CH API schema changes before they break customers.\n\n- **rate_limit** (every 1h) — checks the `X-Ratelimit-Remain` header and\n  alerts when the budget drops below 20% of the 600-per-5-minutes allowance.\n\n- **document_api** (every 12h) — fetches filing history and hits the CH\n  Document API to confirm iXBRL downloads are still accessible.\n\nPossible statuses:\n- **pass** — probe succeeded with no issues\n- **degraded** — probe completed but detected a warning condition\n- **fail** — probe failed (connection error, unexpected HTTP status, missing fields)\n\n**Note:** Results are stored in memory. After a server restart, probes run once\nimmediately at startup and then on their normal schedule.\n\nNo authentication required. Safe to poll at low frequency (once per minute max).\n\n**Upstream usage:** 0 — reads from memory only. No upstream calls made.","operationId":"health_probes_v1_health_probes_get","responses":{"200":{"description":"Probe results returned (may be empty if probes haven't run yet)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProbesResponse"}}}}},"security":[]}},"/v1/search":{"get":{"tags":["Search"],"summary":"Search for companies by name","description":"Search for UK companies by name.\n\nReturns a list of matching companies with basic details. Results are cached for 1 hour.\n\n**Example:** `/v1/search?q=tesco` returns Tesco PLC, Tesco Stores Limited, etc.\n\n**With enrichment:** `/v1/search?q=tesco&enrich=true` also includes company age,\naccounts status, and size classification for each result (capped at 10 enriched results).\n\n---\n\n**Upstream usage:** 1 upstream call per unique search query (cached for 1 hour).\nRepeating the same query within the hour is free — no upstream call is made.\nAt high volume, you can safely issue ~100 unique searches per minute before\nresponses begin to slow. Beyond that, requests queue briefly (typically a few seconds)\nwhile upstream capacity recovers.","operationId":"search_companies_v1_search_get","parameters":[{"name":"q","in":"query","required":true,"schema":{"type":"string","minLength":1,"maxLength":200,"description":"Company name to search for. Supports partial/fuzzy matching.","examples":["tesco","barclays","rolls royce"],"title":"Q"},"description":"Company name to search for. Supports partial/fuzzy matching."},{"name":"items_per_page","in":"query","required":false,"schema":{"type":"integer","maximum":100,"minimum":1,"description":"Number of results to return per page (1-100).","default":20,"title":"Items Per Page"},"description":"Number of results to return per page (1-100)."},{"name":"start_index","in":"query","required":false,"schema":{"type":"integer","minimum":0,"description":"Offset for pagination. Use 0 for the first page, 20 for the second (if items_per_page=20), etc.","default":0,"title":"Start Index"},"description":"Offset for pagination. Use 0 for the first page, 20 for the second (if items_per_page=20), etc."},{"name":"enrich","in":"query","required":false,"schema":{"type":"boolean","description":"If true, returns full enriched profiles for each result instead of basic search data. Uses more API credits (1 per enriched result, capped at 10).","default":false,"title":"Enrich"},"description":"If true, returns full enriched profiles for each result instead of basic search data. Uses more API credits (1 per enriched result, capped at 10)."}],"responses":{"200":{"description":"Search results returned successfully","content":{"application/json":{"schema":{"$ref":"#/components/schemas/APIResponse"}}}},"401":{"description":"Missing or invalid API key"},"422":{"description":"Validation error (e.g., query too long or missing)"},"502":{"description":"Companies House API is temporarily unavailable"},"503":{"description":"Request shed to protect shared Companies House capacity; retry after the seconds given in the Retry-After header"}}}},"/v1/company/{company_number}":{"get":{"tags":["Company"],"summary":"Get enriched company profile","description":"Get an enriched company profile by company number.\n\nReturns the company's core details plus derived fields that aren't available\nfrom the raw Companies House data:\n\n- **company_age_years** — calculated from incorporation date\n- **company_category** — inferred size (micro/small/medium/large) based on accounts type\n- **accounts.overdue** — whether annual accounts are past their filing deadline\n- **confirmation_statement.overdue** — whether the confirmation statement is overdue\n\nData is cached for 30 days. If the Companies House API is down, stale cached data\nis returned with `cached: true` in the response.\n\n**Example:** `/v1/company/00445790` returns the enriched profile for Tesco PLC.\n\n---\n\n**Upstream usage:** 1 upstream call per unique company on first fetch (cached for 30 days).\nRepeating a lookup for the same company within 30 days is free — no upstream call is made.\nAt high volume, you can safely look up ~100 different companies per minute before responses\nbegin to slow. Beyond that, requests queue briefly (typically a few seconds) while upstream\ncapacity recovers.","operationId":"get_company_v1_company__company_number__get","parameters":[{"name":"company_number","in":"path","required":true,"schema":{"type":"string","description":"The Companies House company number. 8 alphanumeric characters (e.g., `00445790` for Tesco PLC, `SC123456` for a Scottish company). Numeric-only numbers are automatically zero-padded, so `445790` and `00445790` both work.","examples":["00445790","SC123456","445790"],"title":"Company Number"},"description":"The Companies House company number. 8 alphanumeric characters (e.g., `00445790` for Tesco PLC, `SC123456` for a Scottish company). Numeric-only numbers are automatically zero-padded, so `445790` and `00445790` both work."}],"responses":{"200":{"description":"Enriched company profile returned successfully","content":{"application/json":{"schema":{"$ref":"#/components/schemas/APIResponse"}}}},"400":{"description":"Invalid company number format"},"401":{"description":"Missing or invalid API key"},"404":{"description":"Company not found at Companies House"},"502":{"description":"Companies House API is temporarily unavailable"},"503":{"description":"Request shed to protect shared Companies House capacity; retry after the seconds given in the Retry-After header"},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/company/{company_number}/directors":{"get":{"tags":["Directors"],"summary":"Get company directors with their other appointments","description":"Get all directors for a company, split into current and past, with their other board appointments.\n\nFor each director, the response includes every other company they sit on (or have sat on),\nenabling you to map connections between companies through shared board members.\n\n- **current_directors** — officers with no resignation date, including secretaries\n  (never filtered — a secretary is a real officer that KYB checks need to see)\n- **past_directors** — officers who have resigned\n- **other_appointments** — every other company each director is or was appointed to\n\nEach director carries ECCTA identity verification state: **verification_status**\n(`verified` | `pending` | `overdue` | `unknown` | `not_applicable`),\n**verification_deadline** (this person's own CH-authoritative deadline, only set\nwhen pending or overdue), and **is_board_director** (true for every officer except\nsecretaries — a `corporate-director` IS a board director even though it cannot\npersonally verify an identity, so `verification_status` is `not_applicable` for it\nwhile `is_board_director` stays `true`). `pending` means a deadline has been\nrecorded but has NOT passed — nothing has been missed. Top-level counts\n(`directors_verified`, `directors_pending`, `directors_overdue`, `directors_unknown`,\n`directors_verification_required`) exclude officers ECCTA does not apply to\n(corporate officers and secretaries), so they can be smaller than `total_current`.\n\nData is cached for 30 days. If the Companies House API is down, stale cached data\nis returned with `cached: true` in the response.\n\n**Example:** `/v1/company/00445790/directors` returns all Tesco PLC directors.\n\n---\n\n**Upstream usage:** This endpoint is upstream-intensive on first fetch.\nIt makes **1 call per director** (to retrieve their appointment history), plus 1–2 calls\nfor the officer list itself. A company with 10 directors uses ~12 upstream calls;\na company with 50 directors uses ~52. After the first fetch, the result is cached for\n30 days and subsequent calls are free.\n\nAt high volume, **querying more than ~8 different companies per minute on cold cache**\nwill cause responses to slow while upstream capacity recovers (typically 5–15 seconds).\nFor bulk use cases, space requests out or pre-warm the cache during off-peak hours.","operationId":"get_directors_v1_company__company_number__directors_get","parameters":[{"name":"company_number","in":"path","required":true,"schema":{"type":"string","description":"The Companies House company number. 8 alphanumeric characters (e.g., `00445790` for Tesco PLC). Numeric-only numbers are automatically zero-padded.","examples":["00445790","SC123456","445790"],"title":"Company Number"},"description":"The Companies House company number. 8 alphanumeric characters (e.g., `00445790` for Tesco PLC). Numeric-only numbers are automatically zero-padded."}],"responses":{"200":{"description":"Directors list returned successfully, split into current and past","content":{"application/json":{"schema":{"$ref":"#/components/schemas/APIResponse"}}}},"400":{"description":"Invalid company number format"},"401":{"description":"Missing or invalid API key"},"404":{"description":"Company not found at Companies House"},"502":{"description":"Companies House API is temporarily unavailable"},"503":{"description":"Request shed to protect shared Companies House capacity; retry after the seconds given in the Retry-After header"},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/company/{company_number}/network":{"get":{"tags":["Directors"],"summary":"Map connected companies via shared directors","description":"Discover all companies connected to the target through shared board members.\n\nThis is the key differentiator of this API: starting from one company, you can see\nevery other company that shares at least one current director, revealing hidden\ngovernance links and corporate group structures.\n\n- **connections** — each connected company with the list of shared director names\n- **connection_strength** — number of shared directors (higher = stronger link)\n- **truncated** — `true` if depth=2 traversal hit the 30-company cap (see below)\n- Results are sorted by connection strength (strongest first)\n\nOnly genuine board directors contribute to connections — corporate secretaries\n(e.g. a company secretarial provider shared by thousands of client companies) are\nexcluded, so a shared corporate-secretary provider never creates a phantom\n\"connection\" between otherwise unrelated companies.\n\n**Depth 1** (default): direct connections only — companies sharing a director with the target.\n**Depth 2**: also traverses each connected company's directors to find second-degree connections.\nThis reveals wider networks but is upstream-intensive on cold cache.\n\nData is cached for 30 days (cache key includes depth). If the Companies House API is\ndown, stale cached data is returned with `cached: true`.\n\n**Plan gating:** `depth=2` requires a Pro or Enterprise plan (403 for Free/Web). `depth=1`\nis available on all plans.\n\n**Example:** `/v1/company/00445790/network?depth=1` shows companies connected to Tesco PLC.\n\n---\n\n**Upstream usage:** This is the most upstream-intensive endpoint.\n\n- **Depth 1 (cold cache):** same as `/directors` — ~N+2 upstream calls, where N is the number\n  of directors. A 10-director company uses ~12 calls; a 50-director company uses ~52 calls.\n  After first fetch, cached for 30 days at no cost.\n\n- **Depth 2 (cold cache):** traverses each connected company's directors as well. To protect\n  service quality for all customers, traversal is **capped at 30 connected companies** per request.\n  Typical upstream cost: 50–200 calls. If the cap is hit, the response includes `truncated: true`\n  and `truncated_at: 30` — this means more connections exist but were not explored.\n\n**Practical guidance:** Depth=2 on cold cache is slow by design. On a warm cache (after the\nfirst request), depth=2 is instant and free. If you need depth=2 across many companies,\ncall `/directors` for each first to warm the cache, then request the network.\n\nAt high volume, **querying more than ~8 different companies per minute at depth=1** or\n**more than 2–3 different companies per 5 minutes at depth=2** on cold cache will cause\nresponses to slow while upstream capacity recovers.","operationId":"get_network_v1_company__company_number__network_get","parameters":[{"name":"company_number","in":"path","required":true,"schema":{"type":"string","description":"The Companies House company number. 8 alphanumeric characters (e.g., `00445790` for Tesco PLC). Numeric-only numbers are automatically zero-padded.","examples":["00445790","SC123456","445790"],"title":"Company Number"},"description":"The Companies House company number. 8 alphanumeric characters (e.g., `00445790` for Tesco PLC). Numeric-only numbers are automatically zero-padded."},{"name":"depth","in":"query","required":false,"schema":{"type":"integer","maximum":2,"minimum":1,"description":"Network traversal depth. `1` = companies sharing directors with the target (default). `2` = also includes companies connected to those companies (slower, more results).","examples":[1,2],"default":1,"title":"Depth"},"description":"Network traversal depth. `1` = companies sharing directors with the target (default). `2` = also includes companies connected to those companies (slower, more results)."}],"responses":{"200":{"description":"Network map returned successfully, sorted by connection strength","content":{"application/json":{"schema":{"$ref":"#/components/schemas/APIResponse"}}}},"400":{"description":"Invalid company number format or depth value"},"401":{"description":"Missing or invalid API key"},"403":{"description":"Pro or Enterprise plan required for depth=2"},"404":{"description":"Company not found at Companies House"},"502":{"description":"Companies House API is temporarily unavailable"},"503":{"description":"Request shed to protect shared Companies House capacity; retry after the seconds given in the Retry-After header"},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/company/{company_number}/financials":{"get":{"tags":["Financials"],"summary":"Get structured financial data from filed accounts","description":"Get structured financial data extracted from the company's latest filed iXBRL accounts.\n\nReturns balance sheet and profit & loss figures for the current and prior reporting year,\nwhere available. All monetary values are in actual GBP (e.g. `5200000` = £5,200,000).\n\n**What you get:**\n\n- **profit_and_loss** — turnover, gross profit, operating profit, profit before/after tax,\n  depreciation. Only available for companies that file full accounts.\n- **balance_sheet** — fixed assets, current assets, creditors, net assets, equity.\n  Available for virtually all UK companies (legally mandated).\n- **other** — average number of employees during the period.\n- **data_quality** — tells you exactly what was extracted, what's missing, and why.\n  Use `accounts_type` to understand field availability, and `filing_type` / `filed_on` /\n  `period_end_source` to audit *which* Companies House filing the figures came from.\n\n**Which filing is used:** the most recent filing that is genuinely a set of annual accounts\nfor this company (`AA`, `AAMD`, legacy `AC(NI)`). Companies House files change-of-accounting-\nreference-date forms (`AA01`) and similar administrative documents in the same filing-history\ncategory, and they can be filed after the last real accounts — those are skipped.\n`data_quality.period_end` is the accounting period end, not the date the filing was\nsubmitted; the submission date is `data_quality.filed_on`.\n\n**Why some fields are null:**\n\nUK law allows smaller companies to withhold P&L data:\n- **micro** (`accounts_type: \"micro\"`) — balance sheet only; turnover and profit are legally\n  exempt from disclosure. ~50% of UK companies file micro accounts.\n- **abbreviated** (`accounts_type: \"abbreviated\"`) — balance sheet only; small company\n  exemption from P&L disclosure.\n- **full** (`accounts_type: \"full\"`) — all fields available (medium/large companies).\n\nIf a field is null, check `data_quality.missing_fields` for the list and\n`data_quality.has_profit_loss` to understand whether P&L was filed at all.\n\n**Sources:**\n\nData is extracted from whichever format is available in the filed accounts:\n- **iXBRL** (inline XBRL) — structured markup, high precision, `data_quality.source: \"ixbrl\"`.\n  Covers most UK companies since CH made iXBRL filing mandatory for most company types.\n- **PDF** — text/table extraction attempted as a fallback. In practice, PDF annual accounts\n  on Companies House are rendered as image-based PDFs (from design software or scanning)\n  and yield no extractable text. Returns 404 when image-based PDF is detected.\n\nIf no parseable filing is available (no iXBRL filing, or image-based PDF), returns HTTP 200\nwith `available: false` and accounts metadata from the company profile. This lets pipelines\nbranch cleanly without exception handling. HTTP 404 is only returned when the company number\nitself does not exist in Companies House.\n\nData is cached for 90 days. Financial filings don't change between annual submissions,\nso cached data is reliably current.\n\n**Example:** `/v1/company/00445790/financials` returns Tesco PLC's latest filed accounts.\n\n---\n\n**Upstream usage:** 2–3 upstream calls on cold cache (filing history + document metadata +\ndocument download). After first fetch, cached for 90 days at no cost. On cold cache,\nlimit to ~30 different companies per minute to stay within upstream capacity.","operationId":"get_financials_v1_company__company_number__financials_get","parameters":[{"name":"company_number","in":"path","required":true,"schema":{"type":"string","description":"Companies House company number (e.g. `01234567` for a typical limited company). Numeric-only numbers are automatically zero-padded.","examples":["01234567","00445790"],"title":"Company Number"},"description":"Companies House company number (e.g. `01234567` for a typical limited company). Numeric-only numbers are automatically zero-padded."}],"responses":{"200":{"description":"Financial data extracted and returned successfully","content":{"application/json":{"schema":{"$ref":"#/components/schemas/APIResponse"}}}},"400":{"description":"Invalid company number format"},"401":{"description":"Missing or invalid API key"},"404":{"description":"Company not found (company number does not exist in Companies House)."},"502":{"description":"Companies House API or Document API temporarily unavailable"},"503":{"description":"Request shed to protect shared Companies House capacity; retry after the seconds given in the Retry-After header"},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/company/{company_number}/psc":{"get":{"tags":["PSC"],"summary":"Get persons with significant control","description":"Get the PSC (Persons with Significant Control) register for a company.\n\nReturns all active and ceased PSCs, split into categories. `kind` is a closed\nenum decided by an explicit allow-list of every Companies House PSC kind:\n\n- **individual** — a natural person with significant control\n- **corporate-entity** — a company with significant control\n- **legal-person** — a legal person (e.g., government body) with significant control\n- **super-secure** — a person whose details are protected by court order\n- **unknown** — Companies House returned a kind we have not classified. Nothing\n  is inferred from it: no verification status, no chain traversal.\n\nEvery PSC also carries **`kind_raw`** — the verbatim Companies House `kind`\nstring (e.g. `individual-beneficial-owner`). Before 2026-08-01 an\nunrecognised kind was echoed into `kind` itself; it is now normalised to\n`unknown` and the original value is always available in `kind_raw`.\n\n**Corporate-entity PSCs — which register the number belongs to.** Companies\nHouse stores a corporate PSC's number as the entity's number at whatever\nregistry `identification.place_registered` names, which is often not\nCompanies House (Guernsey Registry, Jersey Financial Services Commission,\nCommercial Register Of Liechtenstein, ...). Four fields make this explicit:\n\n- `registry_number` — the number exactly as filed, at whatever registry issued it\n- `registry_name` — that registry's name as filed\n- `registry_is_companies_house` — `true` (confirmed UK register), `null` (a\n  number is present but cannot be tied to the UK register), `false` (no\n  registration number filed at all)\n- `company_number` — populated **only** when `registry_is_companies_house`\n  is `true`, and always in canonical Companies House form (zero-padded,\n  upper-cased), so it can be passed straight back to this API\n\nBefore 2026-08-01 `company_number` carried the raw filed value whatever\nregister it came from, so a foreign number could zero-pad onto an unrelated\nUK company (CH-Api#59). Clients that need the old raw behaviour should read\n`registry_number`.\n\nEach PSC includes decoded `natures_of_control_decoded` with human-readable descriptions\nof the control type (e.g., \"Owns 25-50% of shares\" instead of `ownership-of-shares-25-to-50-percent`).\n\nCompanies exempt from PSC filing (e.g., listed PLCs on regulated markets) return\n`has_psc_exemption: true` with empty PSC lists.\n\nData is cached for 30 days. If the Companies House API is down, stale cached data\nis returned with `cached: true` in the response.\n\n**Example:** `/v1/company/12345678/psc`\n\n---\n\n**Upstream usage:** 1 upstream call per unique company on first fetch (cached for 30 days).\nCompanies with >100 PSCs require additional pagination calls.","operationId":"get_psc_v1_company__company_number__psc_get","parameters":[{"name":"company_number","in":"path","required":true,"schema":{"type":"string","description":"The Companies House company number. 8 alphanumeric characters (e.g., `00445790` for Tesco PLC). Numeric-only numbers are automatically zero-padded.","examples":["00445790","12345678"],"title":"Company Number"},"description":"The Companies House company number. 8 alphanumeric characters (e.g., `00445790` for Tesco PLC). Numeric-only numbers are automatically zero-padded."}],"responses":{"200":{"description":"PSC register returned successfully","content":{"application/json":{"schema":{"$ref":"#/components/schemas/APIResponse"}}}},"400":{"description":"Invalid company number format"},"401":{"description":"Missing or invalid API key"},"404":{"description":"Company not found at Companies House"},"502":{"description":"Companies House API is temporarily unavailable"},"503":{"description":"Request shed to protect shared Companies House capacity; retry after the seconds given in the Retry-After header"},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/company/{company_number}/psc/chain":{"get":{"tags":["PSC"],"summary":"Resolve PSC ownership chain to find ultimate beneficial owners","description":"Resolve the PSC ownership chain for a company, traversing corporate entity PSCs recursively.\n\nStarting from the given company, fetches its PSCs. When a PSC is a corporate entity\nregistered at Companies House, follows it to find *that* company's PSCs, and so on,\nuntil reaching natural persons, foreign entities, or the depth limit.\n\n**Terminal reasons** explain why a branch stopped:\n\n- `natural_person` — reached an individual (ultimate beneficial owner)\n- `legal_person` — reached a legal person (e.g., government body)\n- `super_secure` — person's details protected by court order\n- `foreign_entity` — corporate entity with no registration number filed at all\n- `unverified_registry` — a registration number is filed, but it cannot be\n  tied to the Companies House register (a foreign registry, or a registry\n  name we do not recognise). The branch stops rather than following a number\n  that may belong to an unrelated UK company. `registry_number` and\n  `registry_name` on the node say what was filed.\n- `depth_limit` — max traversal depth reached\n- `not_found` — company not found or API error\n- `cycle_detected` — circular ownership detected\n- `psc_exempt` — company exempt from PSC filing (e.g., listed PLC)\n- `unknown_kind` — Companies House returned a PSC kind we have not classified.\n  The branch stops rather than guessing; `kind_raw` carries the raw value.\n\n**ECCTA identity verification.** Every `individual` node in the tree — at any\ndepth, including the ultimate beneficial owners the chain exists to find —\ncarries the same four fields as `/company/{number}/psc`, with the same\nmeanings:\n\n- `verification_status` — `verified` | `pending` | `overdue` | `unknown`\n- `identity_verified` — `true` (verified), `false` (**overdue only**), or\n  `null` for pending and unknown. A pending person has missed nothing.\n- `identity_verified_on` — set only when `verification_status` is `verified`\n- `verification_deadline` — that person's own Companies House deadline, set\n  only when `pending` or `overdue`. Companies House's open-ended-window\n  sentinel (9999-12-31) is never surfaced; such a person is `pending` with a\n  `null` deadline.\n\nCorporate-entity, legal-person, super-secure and unknown nodes omit these\nfields entirely — a company has no personal identity to verify, so a status\non one would imply an obligation that does not exist.\n\nBefore 2026-08-17 the chain reported no verification data at all, so the same\nperson could appear verified on `/psc` and blank here (CH-Api#104). Every\nperson in the tree is classified against a single date fixed at the start of\nthe traversal.\n\n**Credits:** Each company resolved in the chain costs 1 upstream API call.\nThe `chain_metadata.total_credits` field shows the total cost.\n\n**Example:** `/v1/company/12345678/psc/chain?max_depth=3`","operationId":"get_psc_chain_v1_company__company_number__psc_chain_get","parameters":[{"name":"company_number","in":"path","required":true,"schema":{"type":"string","description":"The Companies House company number to start the chain from.","examples":["12345678"],"title":"Company Number"},"description":"The Companies House company number to start the chain from."},{"name":"max_depth","in":"query","required":false,"schema":{"type":"integer","maximum":10,"minimum":1,"description":"Maximum depth to traverse the ownership chain (1-10). Each level costs 1 upstream API call per corporate entity.","default":5,"title":"Max Depth"},"description":"Maximum depth to traverse the ownership chain (1-10). Each level costs 1 upstream API call per corporate entity."}],"responses":{"200":{"description":"PSC chain resolved successfully","content":{"application/json":{"schema":{"$ref":"#/components/schemas/APIResponse"}}}},"400":{"description":"Invalid company number format"},"401":{"description":"Missing or invalid API key"},"403":{"description":"Pro or Enterprise plan required"},"404":{"description":"Company not found at Companies House"},"502":{"description":"Companies House API is temporarily unavailable"},"503":{"description":"Request shed to protect shared Companies House capacity; retry after the seconds given in the Retry-After header"},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/company/{company_number}/compliance":{"get":{"tags":["Compliance"],"summary":"Get ECCTA identity verification compliance snapshot","description":"ECCTA identity verification compliance snapshot for a company.\n\nReturns a verification health report for all current directors and active\nindividual PSCs under the Economic Crime and Corporate Transparency Act 2023.\nMandatory identity verification was introduced in November 2025, with enforcement\nbeginning 18 November 2026.\n\n**Fields:**\n\n- `directors_total` / `pscs_total` — count of persons ECCTA identity verification\n  actually applies to: current (non-resigned) directors minus corporate officers\n  and secretaries, and active individual PSCs minus corporate/legal-person PSCs.\n  **Not the board size.** It is smaller than `/company/{number}/directors`'\n  `total_current`, which counts every current officer including secretaries and\n  corporate officers.\n- `directors_verified` / `pscs_verified` — count with a confirmed CH\n  `identity_verified_on` date.\n- `directors_pending` / `pscs_pending` — count with a verification deadline that has\n  NOT yet passed. Nothing has been missed; during the Nov 2025 - Nov 2026 transition\n  this is the majority state for UK directors.\n- `directors_overdue` / `pscs_overdue` — count whose deadline has passed with no\n  verification recorded.\n- `directors_unknown` / `pscs_unknown` — count where `identity_verification_details`\n  is entirely absent from the CH API response (pre-ECCTA data, not evidence of\n  non-compliance).\n- `directors_unverified` / `pscs_unverified` — **deprecated aliases**, kept only for\n  backwards compatibility. They now mean exactly `overdue` (CH-Api#52, fixed\n  2026-07-30); before that fix they also counted `pending` people, which branded\n  roughly 70% of UK directors non-compliant during the transition. Use\n  `directors_pending`/`directors_overdue` (or the PSC equivalents) instead.\n- `verification_rate` — `verified / (verified + pending + overdue)`, **directors and\n  PSCs combined into one figure**. Unknowns are excluded from both the numerator and\n  the denominator. `0.0` when nobody is verified, pending or overdue.\n- `verification_risk` — `compliant` | `pending` | `partial` | `high_risk` |\n  `unknown`. A future deadline is deliberately not treated as risk on its own\n  (`pending`); only a passed deadline with no verification drives `high_risk`.\n- `risk_score` — `1.0 - verification_rate`, or `null` when no person has a known\n  status (verified, pending or overdue) - i.e. everyone is `unknown`.\n- `unverified_persons` — every director/PSC who is `pending` **or** `overdue` (both,\n  not overdue-only), each carrying its own `status` (`pending` | `overdue`) and\n  `deadline`. **Read `status` before describing anyone here as non-compliant** - a\n  `pending` entry has missed nothing. `deadline` may be `null` (an open-ended\n  verification window with no printable date); never substitute a fallback date for\n  a null one. **This list is longer than the `*_unverified` counts by design**: the\n  deprecated aliases count `overdue` only, this list carries `pending` too -\n  comparing `len(unverified_persons)` to `directors_unverified + pscs_unverified`\n  and expecting them to match is the mistake to avoid.\n- `eccta_enforcement_deadline` — 2026-11-18\n\nCorporate entity and legal person PSCs are exempt from individual verification and\nare excluded from all counts.\n\n**Plan gating:** Pro or Enterprise only. Free and Web plan requests return 403.\n\n**Cache:** 30-day TTL. Officers and PSC are fetched concurrently.\n\n**Credits:** 1 credit per call.\n\n**Example:** `/v1/company/00445790/compliance`","operationId":"get_compliance_v1_company__company_number__compliance_get","parameters":[{"name":"company_number","in":"path","required":true,"schema":{"type":"string","description":"Companies House company number (e.g. `00445790` for Tesco PLC). Numeric-only numbers are zero-padded.","examples":["00445790","10892514"],"title":"Company Number"},"description":"Companies House company number (e.g. `00445790` for Tesco PLC). Numeric-only numbers are zero-padded."}],"responses":{"200":{"description":"Compliance snapshot returned successfully","content":{"application/json":{"schema":{"$ref":"#/components/schemas/APIResponse"}}}},"400":{"description":"Invalid company number format"},"401":{"description":"Missing or invalid API key"},"403":{"description":"Pro or Enterprise plan required"},"404":{"description":"Company not found at Companies House"},"502":{"description":"Companies House API is temporarily unavailable"},"503":{"description":"Request shed to protect shared Companies House capacity; retry after the seconds given in the Retry-After header"},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/enrich":{"post":{"tags":["Enrich"],"summary":"Bulk enrich up to 50 companies in one request","description":"Enrich up to 50 UK companies in a single request.\n\nAccepts a JSON body with a list of company numbers and returns an enriched profile\nfor each one. Results are fetched in parallel and served from the 30-day cache\nwhere available.\n\n**Partial success** — each item in the response has its own `status` field. If one\ncompany number is invalid or not found, the rest still succeed. Inspect each item\nindividually.\n\n**Credits** — this endpoint charges `credits_used = N` where N is the number of\ncompany numbers in the request, regardless of cache hits or errors.\n\n**Example request:**\n```json\nPOST /v1/enrich\n{\"company_numbers\": [\"00445790\", \"03547512\"]}\n```\n\n**Example response (one success, one not found):**\n```json\n{\n  \"status\": \"success\",\n  \"credits_used\": 2,\n  \"data\": [\n    {\"company_number\": \"00445790\", \"status\": \"success\", \"data\": {...}},\n    {\"company_number\": \"99999999\", \"status\": \"error\", \"error\": \"Company 99999999 not found\"}\n  ]\n}\n```\n\n---\n\n**Cache behaviour:** Companies already in cache are returned instantly at no upstream cost.\nCache TTL is 30 days (same as `GET /v1/company/{n}`).","operationId":"bulk_enrich_v1_enrich_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/EnrichRequest"}}},"required":true},"responses":{"200":{"description":"Bulk enrichment results returned (partial success is possible)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/APIResponse"}}}},"400":{"description":"No valid company numbers provided, or more than 50 requested"},"401":{"description":"Missing or invalid API key"},"422":{"description":"Validation error in request body"}}}},"/v1/webhooks":{"get":{"tags":["Webhooks"],"summary":"List webhook subscriptions","description":"List all active webhook subscriptions for the authenticated API key.","operationId":"list_webhooks_v1_webhooks_get","responses":{"200":{"description":"List of active webhook subscriptions for this API key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/APIResponse"}}}},"401":{"description":"Missing or invalid API key"}}},"post":{"tags":["Webhooks"],"summary":"Create a webhook subscription","description":"Create a webhook subscription to watch UK companies for changes.\n\nRegistrum checks each watched company roughly every 15 minutes. When a change\nis detected (e.g., status becomes dissolved, accounts go overdue), a signed\nPOST is delivered to your URL.\n\n**Supported events:**\n- `status_change` — company_status changes (e.g., active → dissolved)\n- `accounts_overdue` — accounts filing deadline missed\n- `confirmation_overdue` — confirmation statement deadline missed\n- `directors_changed` — a director is appointed or resigned (checked every 6 hours)\n- `pscs_changed` — a PSC is added, ceased, or their ECCTA verification status changes (every 6 hours)\n- `compliance_risk_changed` — the company's overall ECCTA risk rating changes, e.g., compliant → partial\n  (every 6 hours). **Requires Pro/Enterprise or the Compliance Monitoring add-on.**\n\n**Webhook watch caps** — the number of *distinct* companies you can watch across all\nyour webhooks (existing + this request combined), by plan:\n\n| Plan | Companies watched |\n|------|--------------------|\n| Free | 1 |\n| Web | 5 |\n| Pro | 25 |\n| Enterprise | 250 |\n\nExceeding your cap, or requesting `compliance_risk_changed` without Pro/Enterprise,\nreturns `403 PLAN_REQUIRED`. The **Compliance Monitoring add-on** raises your cap to\n100 companies and unlocks `compliance_risk_changed` on any plan.\n\n**Payload delivered to your URL:**\n```json\n{\n  \"event\": \"company_updated\",\n  \"company_number\": \"00445790\",\n  \"changes\": {\n    \"status_change\": {\"from\": \"active\", \"to\": \"dissolved\"}\n  },\n  \"triggered_at\": \"2026-03-11T10:00:00Z\"\n}\n```\n\n**Verifying signatures:**\nEvery delivery includes an `X-Registrum-Signature: sha256=<hex>` header.\nVerify it with HMAC-SHA256 using the `secret` returned at creation time:\n```python\nimport hmac, hashlib\nexpected = \"sha256=\" + hmac.new(secret.encode(), request.body, hashlib.sha256).hexdigest()\nassert hmac.compare_digest(expected, request.headers[\"X-Registrum-Signature\"])\n```\n\n**Note:** The `secret` is returned once at creation — store it securely.","operationId":"create_webhook_v1_webhooks_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookCreateRequest"}}},"required":true},"responses":{"200":{"description":"Webhook created. Response includes the signing secret — store it securely.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/APIResponse"}}}},"400":{"description":"Invalid event type or company number"},"401":{"description":"Missing or invalid API key"},"403":{"description":"Plan required: either the request would exceed your plan's webhook watch cap, or compliance_risk_changed was requested without Pro/Enterprise or the Compliance Monitoring add-on."},"422":{"description":"Validation error"},"503":{"description":"Storage unavailable (Supabase not configured)"}}}},"/v1/webhooks/{webhook_id}":{"get":{"tags":["Webhooks"],"summary":"Get a webhook subscription","description":"Get a single webhook subscription by ID.","operationId":"get_webhook_v1_webhooks__webhook_id__get","parameters":[{"name":"webhook_id","in":"path","required":true,"schema":{"type":"string","description":"The webhook's ID, as returned by POST /v1/webhooks when it was created.","examples":["whk_3f8a21c0"],"title":"Webhook Id"},"description":"The webhook's ID, as returned by POST /v1/webhooks when it was created."}],"responses":{"200":{"description":"Webhook subscription details","content":{"application/json":{"schema":{"$ref":"#/components/schemas/APIResponse"}}}},"401":{"description":"Missing or invalid API key"},"404":{"description":"Webhook not found or does not belong to this API key"},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"delete":{"tags":["Webhooks"],"summary":"Delete a webhook subscription","description":"Delete a webhook subscription. Deliveries will stop immediately.","operationId":"delete_webhook_v1_webhooks__webhook_id__delete","parameters":[{"name":"webhook_id","in":"path","required":true,"schema":{"type":"string","description":"The webhook's ID, as returned by POST /v1/webhooks when it was created.","examples":["whk_3f8a21c0"],"title":"Webhook Id"},"description":"The webhook's ID, as returned by POST /v1/webhooks when it was created."}],"responses":{"200":{"description":"Webhook deleted","content":{"application/json":{"schema":{"$ref":"#/components/schemas/APIResponse"}}}},"401":{"description":"Missing or invalid API key"},"404":{"description":"Webhook not found or does not belong to this API key"},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/company/{company_number}/kyb-report":{"get":{"tags":["KYB"],"summary":"Get a full Know Your Business (KYB) report","description":"Get a comprehensive Know Your Business (KYB) report for a UK company.\n\nAggregates the company profile, financial data, director information, and a computed\nset of risk flags into a single response. Suitable for onboarding, due diligence,\nand automated KYB workflows.\n\n**Report sections:**\n\n- **profile** — enriched company profile (status, age, address, SIC codes)\n- **financials** — latest filed accounts (balance sheet + P&L). `null` if no digital filing exists.\n- **directors** — current and past directors with their other appointments\n- **psc_chain** — PSC ownership chain resolved to ultimate beneficial owners. Each node has a\n  `terminal_reason` (natural_person, foreign_entity, legal_person, psc_exempt, depth_limit,\n  not_found, cycle_detected). `null` if chain resolution fails.\n- **risk_flags** — boolean flags derived from the profile and filing data:\n  - `accounts_overdue` — accounts past their filing deadline\n  - `confirmation_overdue` — confirmation statement overdue\n  - `has_charges` — company has registered charges (e.g., mortgages)\n  - `has_insolvency_history` — any insolvency history on record\n  - `not_active` — company is not in active status (dissolved, liquidation, etc.)\n  - `recently_incorporated` — less than 1 year old\n  - `financials_unavailable` — no parseable digital accounts filing exists\n  - `no_accounts_type` — no accounts type recorded (dormant or never filed)\n- **summary_md** — LLM-ready Markdown combining profile + financials summaries\n\n**Credits:** 3 base credits (profile + financials + directors) plus 1 per company resolved\nin the PSC chain. Total is reported in `credits_used` and in `psc_chain.chain_metadata.total_credits`.\n\n**Cache behaviour:** All underlying data sources are served from cache where available.\nProfile: 30-day TTL. Financials: 90-day TTL. Directors: 30-day TTL. PSC: fetched fresh per call.\n\n**Example:** `/v1/company/00445790/kyb-report` returns a full KYB report for Tesco PLC.","operationId":"get_kyb_report_v1_company__company_number__kyb_report_get","parameters":[{"name":"company_number","in":"path","required":true,"schema":{"type":"string","description":"Companies House company number (e.g. `00445790` for Tesco PLC).","examples":["00445790"],"title":"Company Number"},"description":"Companies House company number (e.g. `00445790` for Tesco PLC)."}],"responses":{"200":{"description":"KYB report returned. Financials and directors sections may be null if unavailable.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/APIResponse"}}}},"400":{"description":"Invalid company number format"},"401":{"description":"Missing or invalid API key"},"404":{"description":"Company not found"},"502":{"description":"Companies House API unavailable"},"503":{"description":"Request shed to protect shared Companies House capacity; retry after the seconds given in the Retry-After header"},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/batch":{"post":{"tags":["Batch"],"summary":"Submit a batch of up to 500 companies for async enrichment","description":"Submit a list of up to 500 UK company numbers for background enrichment.\n\nUnlike `POST /v1/enrich` (which processes up to 50 companies synchronously),\nthis endpoint accepts much larger lists and processes them in the background,\nautomatically respecting the shared Companies House API rate limit.\n\n**How it works:**\n1. Submit your list — receive a `batch_id` immediately.\n2. Poll `GET /v1/batch/{batch_id}` every few seconds until `status` is terminal.\n3. Read `results` for successful enrichments and `errors` for any failures.\n\n**Status lifecycle:**\n```\nqueued → processing → complete\n                   ↘ partial          (some successes, some errors)\n                   ↘ failed           (all companies errored)\n                   ↘ quota_exceeded   (monthly credit limit hit mid-batch)\n```\n\n**Credits:**\n1 credit is charged per company successfully processed (or attempted).\nCredits are deducted as processing proceeds, not upfront.\nPolling `GET /v1/batch/{batch_id}` is always free.\n\n**Expiry:**\nJob results are kept for 7 days after creation and then automatically deleted.\n\n**Example request:**\n```json\nPOST /v1/batch\n{\"company_numbers\": [\"00445790\", \"03547512\", \"SC123456\"]}\n```\n\n**Example immediate response:**\n```json\n{\n  \"batch_id\": \"3fa85f64-5717-4562-b3fc-2c963f66afa6\",\n  \"status\": \"queued\",\n  \"total_companies\": 3,\n  \"estimated_wait_seconds\": 10\n}\n```\n\n**Poll until complete:**\n```\nGET /v1/batch/3fa85f64-5717-4562-b3fc-2c963f66afa6\n```","operationId":"submit_batch_v1_batch_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/BatchSubmitRequest"}}},"required":true},"responses":{"200":{"description":"Batch accepted and queued — use batch_id to poll for results","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BatchSubmitResponse"}}}},"400":{"description":"Empty list or list exceeds 500 companies"},"401":{"description":"Missing or invalid API key"},"422":{"description":"Validation error in request body"},"503":{"description":"Database unavailable — try again shortly"}}}},"/v1/batch/{batch_id}":{"get":{"tags":["Batch"],"summary":"Poll an async batch job for status and results","description":"Poll for the status and results of a previously submitted batch job.\n\n**This endpoint is free** — polling does not consume API credits.\n\nPoll every 2–5 seconds until `status` reaches a terminal state:\n`complete`, `partial`, `failed`, or `quota_exceeded`.\n\nThe `results` array grows as companies are enriched. `errors` accumulates\nany failures. Once `status` is terminal, `results` and `errors` are final.\n\n**Suggested polling interval:**\n- Small batches (≤50): every 5 seconds\n- Medium batches (51–200): every 10 seconds\n- Large batches (201–500): every 15–30 seconds\n\n**Example response (in-progress):**\n```json\n{\n  \"batch_id\": \"3fa85f64-...\",\n  \"status\": \"processing\",\n  \"total_companies\": 100,\n  \"completed_companies\": 30,\n  \"credits_charged\": 30,\n  \"results\": [...],\n  \"errors\": {},\n  \"created_at\": \"2026-03-17T10:00:00Z\",\n  \"expires_at\": \"2026-03-24T10:00:00Z\"\n}\n```","operationId":"get_batch_v1_batch__batch_id__get","parameters":[{"name":"batch_id","in":"path","required":true,"schema":{"type":"string","description":"The batch job's ID, as returned by POST /v1/batch when the job was submitted.","examples":["bat_9c14e7d2"],"title":"Batch Id"},"description":"The batch job's ID, as returned by POST /v1/batch when the job was submitted."}],"responses":{"200":{"description":"Batch job status and partial/full results","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BatchJobStatus"}}}},"401":{"description":"Missing or invalid API key"},"404":{"description":"Batch job not found or does not belong to your API key"},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"x-no-credit-charge":true}},"/v1/plans":{"get":{"tags":["Account"],"summary":"Get canonical plan quotas, pricing, and features","description":"Single source of truth for plan quotas, pricing, and feature lists.\n\nPublic, unauthenticated endpoint — the website, customer dashboard, and\ntransactional email copy should fetch this instead of hardcoding plan\nnumbers, so they can't silently drift from what the API actually\nenforces. See `docs/config-centralization-audit-2026-06-22.md`.\n\nAlso includes `watched_companies_limit` — the number of distinct companies\na plan may watch via webhooks (null = unlimited). A key's individual limit\nmay be higher than its plan default if the Compliance Monitoring add-on is\nactive (see `POST /v1/webhooks`).\n\nExample: `GET /v1/plans`","operationId":"get_plans_v1_plans_get","responses":{"200":{"description":"Plan economics for all four tiers.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PlansResponse"}}}},"502":{"description":"Plan configuration could not be loaded upstream."}},"security":[]}},"/v1/usage":{"get":{"tags":["Account"],"summary":"Get your current usage and quota","description":"Check how many API calls you've used this month and how many remain.\n\nReturns your current plan, calls used, monthly limit, and the UTC timestamp\nwhen your counter resets (the 1st of next month).\n\n**This call is free** — it does not consume a credit.\n\n**Tip:** Every API response also includes `credits_remaining` in the JSON body\nand `X-RateLimit-Remaining` / `X-RateLimit-Reset` in the response headers, so\nyou can monitor usage inline without making a separate request.","operationId":"get_usage_v1_usage_get","responses":{"200":{"description":"Usage stats for the authenticated key.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UsageResponse"}}}},"401":{"description":"Missing or invalid API key."}}}},"/v1/feedback":{"post":{"tags":["Feedback"],"summary":"Submit a bug report or feature request","description":"Submit a bug report, feature request, or general feedback.\n\nFeedback is stored and reviewed by the Registrum team.\nInclude `endpoint` and `company_number` when relevant — it helps reproduce the issue faster.\n\n**Types:**\n- `bug` — something is broken or returning unexpected data\n- `request` — a new endpoint, field, or capability you'd like to see\n- `other` — anything else\n\n**Example:**\n```\nPOST /v1/feedback\n{\n  \"type\": \"bug\",\n  \"message\": \"The financials endpoint returns 502 for company 00445790.\",\n  \"endpoint\": \"/v1/company/00445790/financials\",\n  \"company_number\": \"00445790\"\n}\n```","operationId":"submit_feedback_v1_feedback_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/FeedbackRequest"}}},"required":true},"responses":{"200":{"description":"Feedback received.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/APIResponse"}}}},"400":{"description":"Invalid feedback type. Must be 'bug', 'request', or 'other'."},"401":{"description":"Missing or invalid API key."},"422":{"description":"Validation error — message must be 10–2000 characters."},"500":{"description":"Internal error saving feedback."}}}}},"components":{"schemas":{"APIResponse":{"properties":{"status":{"type":"string","title":"Status","description":"'success' or 'error'","default":"success"},"data":{"anyOf":[{"type":"object"},{"items":{},"type":"array"},{"type":"null"}],"title":"Data","description":"The response payload (varies by endpoint)"},"cached":{"type":"boolean","title":"Cached","description":"True if this response was served from cache rather than fetched live","default":false},"cache_age_seconds":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Cache Age Seconds","description":"How many seconds ago this data was fetched from Companies House (null if not cached)"},"fetched_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Fetched At","description":"ISO 8601 instant (UTC) when this data was actually fetched from Companies House. Use this, not cache_age_seconds, to show a user when the data was last checked: an age is measured at the moment this response was produced, so anything that caches the response downstream will overstate freshness by however long it held it. This instant does not drift. Null when unknown - render nothing rather than a guessed date.","examples":["2026-08-06T09:15:00+00:00"]},"data_source":{"type":"string","title":"Data Source","description":"'live' — fetched from Companies House right now; 'cached' — served from local cache (data is recent, no upstream call needed); 'cached_rate_conserved' — served from cache because the CH API budget is above 70% utilisation; data may be up to 30 days old in this state","default":"live"},"credits_used":{"type":"integer","title":"Credits Used","description":"Number of API credits consumed by this request","default":1},"credits_remaining":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Credits Remaining","description":"API credits remaining this month (null in dev mode)"}},"type":"object","title":"APIResponse","description":"Standard wrapper for all API responses.\n\nEvery endpoint returns data in this format. The `data` field contains\nthe endpoint-specific payload. Cache metadata tells you whether the\nresponse came from cache and how fresh it is.","examples":[{"cache_age_seconds":3421,"cached":true,"credits_remaining":487,"credits_used":1,"data":{"company_age_years":78,"company_name":"TESCO PLC","company_number":"00445790","company_status":"active","company_type":"plc"},"data_source":"cached","status":"success"}]},"BatchJobStatus":{"properties":{"batch_id":{"type":"string","title":"Batch Id","description":"UUID of this batch job."},"status":{"type":"string","title":"Status","description":"Current processing state:\n- `queued` — waiting to start\n- `processing` — actively being enriched\n- `complete` — all companies successfully enriched\n- `partial` — finished with some errors (see `errors`)\n- `quota_exceeded` — your monthly quota ran out mid-batch; remaining companies are in `errors`\n- `failed` — all companies failed (upstream issue)"},"total_companies":{"type":"integer","title":"Total Companies","description":"Total number of companies in this batch."},"completed_companies":{"type":"integer","title":"Completed Companies","description":"Number of companies processed so far (successes + errors)."},"credits_charged":{"type":"integer","title":"Credits Charged","description":"Credits consumed so far. Final value once status is terminal."},"results":{"items":{"type":"object"},"type":"array","title":"Results","description":"Enriched profiles for successfully processed companies. Each item has `company_number`, `status: 'success'`, and `data` (the enriched profile). Order matches the original request."},"errors":{"type":"object","title":"Errors","description":"Map of company_number → error message for companies that could not be enriched. Empty if all succeeded."},"created_at":{"type":"string","title":"Created At","description":"ISO 8601 timestamp when the batch was submitted."},"expires_at":{"type":"string","title":"Expires At","description":"ISO 8601 timestamp when this job and its results will be automatically deleted (7 days after creation)."}},"type":"object","required":["batch_id","status","total_companies","completed_companies","credits_charged","created_at","expires_at"],"title":"BatchJobStatus","description":"Full status and results for a batch job — returned by GET /v1/batch/{batch_id}.","examples":[{"batch_id":"3fa85f64-5717-4562-b3fc-2c963f66afa6","completed_companies":3,"created_at":"2026-03-17T10:00:00Z","credits_charged":3,"errors":{},"expires_at":"2026-03-24T10:00:00Z","results":[{"company_number":"00445790","data":{"company_name":"TESCO PLC","company_status":"active"},"status":"success"}],"status":"complete","total_companies":3}]},"BatchSubmitRequest":{"properties":{"company_numbers":{"items":{"type":"string"},"type":"array","maxItems":500,"minItems":1,"title":"Company Numbers","description":"Ordered list of UK company numbers to enrich. Accepts 1–500 entries per batch. Numbers are zero-padded automatically (e.g. '445790' → '00445790'). Results are delivered in the same order.","examples":[["00445790","03547512","SC123456"]]}},"type":"object","required":["company_numbers"],"title":"BatchSubmitRequest","description":"Request body for POST /v1/batch.","examples":[{"company_numbers":["00445790","03547512","SC123456"]}]},"BatchSubmitResponse":{"properties":{"batch_id":{"type":"string","title":"Batch Id","description":"UUID of this batch job. Poll GET /v1/batch/{batch_id} for results."},"status":{"type":"string","title":"Status","description":"Initial job status — always 'queued' on submission.","default":"queued"},"total_companies":{"type":"integer","title":"Total Companies","description":"Number of company numbers accepted into this batch."},"estimated_wait_seconds":{"type":"integer","title":"Estimated Wait Seconds","description":"Rough upper-bound estimate in seconds. In practice most batches complete faster if the cache hit rate is high."}},"type":"object","required":["batch_id","total_companies","estimated_wait_seconds"],"title":"BatchSubmitResponse","description":"Returned immediately after a batch job is accepted.","examples":[{"batch_id":"3fa85f64-5717-4562-b3fc-2c963f66afa6","estimated_wait_seconds":30,"status":"queued","total_companies":250}]},"DeepHealthResponse":{"properties":{"status":{"type":"string","title":"Status","description":"Overall status: 'healthy' if all deps are up, 'degraded' if any are down","default":"healthy"},"uptime_seconds":{"type":"number","title":"Uptime Seconds","description":"Seconds since the server started","default":0},"version":{"type":"string","title":"Version","description":"API version number","default":""},"dependencies":{"items":{"$ref":"#/components/schemas/DependencyHealth"},"type":"array","title":"Dependencies","description":"Per-dependency health status"}},"type":"object","title":"DeepHealthResponse","description":"Checks all upstream dependencies (Companies House API, Supabase) and reports their status."},"DependencyHealth":{"properties":{"name":{"type":"string","title":"Name","description":"Dependency name (e.g., 'companies_house_api', 'supabase')"},"status":{"type":"string","title":"Status","description":"'healthy', 'unhealthy', or 'degraded'"},"response_time_ms":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Response Time Ms","description":"Response time in milliseconds"},"error":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Error","description":"Error message if unhealthy"}},"type":"object","required":["name","status"],"title":"DependencyHealth","description":"Health status of a single upstream dependency."},"EnrichRequest":{"properties":{"company_numbers":{"items":{"type":"string"},"type":"array","maxItems":50,"minItems":1,"title":"Company Numbers","description":"List of Companies House company numbers to enrich (1–50 per request). Numeric-only numbers are automatically zero-padded to 8 digits.","examples":[["00445790","03547512","SC123456"]]}},"type":"object","required":["company_numbers"],"title":"EnrichRequest","description":"Request body for the bulk enrichment endpoint.","examples":[{"company_numbers":["00445790","03547512"]}]},"FeedbackRequest":{"properties":{"type":{"type":"string","title":"Type","description":"Feedback type. One of: `bug`, `request`, `other`.","examples":["bug"]},"message":{"type":"string","maxLength":2000,"minLength":10,"title":"Message","description":"Human-readable description of the issue or request.","examples":["The /v1/company/{n}/financials endpoint returns 502 for company 00445790."]},"endpoint":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Endpoint","description":"The API endpoint where you encountered the issue, if applicable.","examples":["/v1/company/00445790/financials"]},"company_number":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Company Number","description":"UK company number you were querying, if applicable.","examples":["00445790"]},"request_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Request Id","description":"The X-Request-Id header value from the failed response. Including this lets us trace the exact request in our logs.","examples":["a1b2c3d4-e5f6-7890-abcd-ef1234567890"]}},"type":"object","required":["type","message"],"title":"FeedbackRequest","description":"Feedback payload.","examples":[{"company_number":"00445790","endpoint":"/v1/company/00445790/financials","message":"The financials endpoint returns 502 for company 00445790.","request_id":"a1b2c3d4-e5f6-7890-abcd-ef1234567890","type":"bug"}]},"HTTPValidationError":{"properties":{"detail":{"items":{"$ref":"#/components/schemas/ValidationError"},"type":"array","title":"Detail"}},"type":"object","title":"HTTPValidationError"},"HealthResponse":{"properties":{"status":{"type":"string","title":"Status","description":"'healthy' if the server is running","default":"healthy"},"uptime_seconds":{"type":"number","title":"Uptime Seconds","description":"Seconds since the server started","default":0},"version":{"type":"string","title":"Version","description":"API version number","default":""},"environment":{"type":"string","title":"Environment","description":"Runtime environment (development/production)","default":""}},"type":"object","title":"HealthResponse","description":"Basic liveness check. Returns 200 if the API server is running."},"PlanDetail":{"properties":{"monthly_limit":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Monthly Limit","description":"API calls allowed per month. null = unlimited."},"daily_limit":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Daily Limit","description":"API calls allowed per day. null = unlimited."},"burst_limit":{"type":"integer","title":"Burst Limit","description":"API calls allowed per minute."},"price_gbp":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Price Gbp","description":"Monthly price in GBP. null = custom pricing (contact us)."},"features":{"items":{"type":"string"},"type":"array","title":"Features","description":"Marketing-facing feature list for this tier."},"watched_companies_limit":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Watched Companies Limit","description":"Distinct companies this plan can watch via webhooks. null = unlimited."}},"type":"object","required":["monthly_limit","daily_limit","burst_limit","price_gbp","features","watched_companies_limit"],"title":"PlanDetail"},"PlansResponse":{"properties":{"status":{"type":"string","title":"Status","description":"Always 'success'."},"plans":{"additionalProperties":{"$ref":"#/components/schemas/PlanDetail"},"type":"object","title":"Plans","description":"Keyed by plan tier: free, web, pro, enterprise."}},"type":"object","required":["status","plans"],"title":"PlansResponse"},"ProbeResult":{"properties":{"probe_name":{"type":"string","title":"Probe Name","description":"Probe identifier: 'event_loop_lag', 'ch_api_profile', 'rate_limit', or 'document_api'"},"status":{"type":"string","title":"Status","description":"'pass', 'degraded', or 'fail'"},"response_time_ms":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Response Time Ms","description":"How long the probe took in milliseconds"},"details":{"type":"object","title":"Details","description":"Probe-specific result data or error message"},"checked_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Checked At","description":"ISO 8601 timestamp of when this probe last ran"}},"type":"object","required":["probe_name","status"],"title":"ProbeResult","description":"Result of a single health probe run."},"ProbesResponse":{"properties":{"probes":{"items":{"$ref":"#/components/schemas/ProbeResult"},"type":"array","title":"Probes","description":"One entry per probe, ordered by name"},"overall":{"type":"string","title":"Overall","description":"'pass' if all probes passed, 'degraded' if any degraded, 'fail' if any failed"}},"type":"object","required":["overall"],"title":"ProbesResponse","description":"Latest results from all background health probes."},"UsageResponse":{"properties":{"status":{"type":"string","title":"Status","description":"Always 'success'."},"plan":{"type":"string","title":"Plan","description":"Your current plan tier (free, web, pro, enterprise)."},"calls_used":{"type":"integer","title":"Calls Used","description":"API calls made in the current billing month."},"calls_limit":{"type":"integer","title":"Calls Limit","description":"Total calls allowed in the current billing month."},"credits_remaining":{"type":"integer","title":"Credits Remaining","description":"Calls remaining before your monthly quota is exhausted."},"reset_at":{"type":"string","title":"Reset At","description":"ISO 8601 UTC timestamp when your monthly counter resets."}},"type":"object","required":["status","plan","calls_used","calls_limit","credits_remaining","reset_at"],"title":"UsageResponse","example":{"calls_limit":2000,"calls_used":142,"credits_remaining":1858,"plan":"pro","reset_at":"2026-05-01T00:00:00+00:00","status":"success"}},"ValidationError":{"properties":{"loc":{"items":{"anyOf":[{"type":"string"},{"type":"integer"}]},"type":"array","title":"Location"},"msg":{"type":"string","title":"Message"},"type":{"type":"string","title":"Error Type"},"input":{"title":"Input"},"ctx":{"type":"object","title":"Context"}},"type":"object","required":["loc","msg","type"],"title":"ValidationError"},"WebhookCreateRequest":{"properties":{"url":{"type":"string","maxLength":2083,"minLength":1,"format":"uri","title":"Url","description":"HTTPS URL that will receive POST requests when a watched company changes.","examples":["https://yourapp.example.com/hooks/registrum"]},"company_numbers":{"items":{"type":"string"},"type":"array","maxItems":50,"minItems":1,"title":"Company Numbers","description":"List of Companies House company numbers to watch (1–50).","examples":[["00445790","03547512"]]},"events":{"items":{"type":"string"},"type":"array","title":"Events","description":"Event types to subscribe to. Valid values: ['accounts_overdue', 'company_updated', 'compliance_risk_changed', 'confirmation_overdue', 'directors_changed', 'pscs_changed', 'status_change']. Default: status_change + accounts_overdue.","default":["status_change","accounts_overdue"],"examples":[["status_change","accounts_overdue","confirmation_overdue"]]}},"type":"object","required":["url","company_numbers"],"title":"WebhookCreateRequest","description":"Request body for creating a webhook subscription.","examples":[{"company_numbers":["00445790"],"events":["status_change","accounts_overdue"],"url":"https://yourapp.example.com/hooks/registrum"}]}},"securitySchemes":{"ApiKeyAuth":{"type":"apiKey","in":"header","name":"X-API-Key","description":"Your API key. Include it in the `X-API-Key` header.\n\nUse your assigned production key."}}},"tags":[{"name":"Search","description":"Find UK companies by name. Supports fuzzy matching and pagination. Results are cached for 1 hour."},{"name":"Company","description":"Get enriched company profiles by company number. Profiles include derived fields like company age, size classification, and accounts overdue status. Data is cached for 30 days."},{"name":"Directors","description":"Discover company directors and map corporate networks through shared board members. Each director includes their other appointments, enabling you to trace governance links between companies. Data is cached for 30 days."},{"name":"Financials","description":"Structured financial data extracted from filed iXBRL accounts. Returns balance sheet and P&L figures (where available) for current and prior year. All values in actual GBP. Data is cached for 90 days."},{"name":"PSC","description":"Persons with Significant Control (PSC) register. Returns individuals, corporate entities, and legal persons who hold significant control over a company. Includes decoded control types and exemption detection for listed PLCs. Data is cached for 30 days."},{"name":"Compliance","description":"ECCTA identity verification compliance snapshots under the Economic Crime and Corporate Transparency Act 2023. Returns a verification health report for all current directors and active individual PSCs — who's verified, who isn't, and an overall risk rating. Pro and Enterprise plan only. Cached for 30 days."},{"name":"KYB","description":"Know Your Business report. Aggregates company profile, financials, directors, and computed risk flags into a single endpoint. Suitable for onboarding, due diligence, and automated compliance workflows."},{"name":"Webhooks","description":"Subscribe to company change events. Registrum polls watched companies every 15 minutes and delivers a signed POST to your URL when a status, accounts, or confirmation-statement change is detected. Deliveries are signed with HMAC-SHA256. Distinct companies watched are capped by plan (free=1, web=5, pro=25, enterprise=250); the Compliance Monitoring add-on raises the cap to 100 and unlocks the compliance_risk_changed event on any plan."},{"name":"Enrich","description":"Bulk enrichment endpoint. Enrich up to 50 UK companies in a single POST request. Each company is looked up in parallel with a cache-first strategy. Partial success is supported — individual items can fail without failing the whole batch."},{"name":"Batch","description":"Async batch enrichment. Submit a list of up to 500 UK company numbers and Registrum enriches them in the background, automatically managing the Companies House API rate limit. Poll `GET /v1/batch/{batch_id}` for progress — polling is always free. Results are retained for 7 days. Ideal for bulk data pipelines and list processing."},{"name":"Health","description":"Service health checks. These endpoints do **not** require authentication and are safe to use for uptime monitoring."},{"name":"Account","description":"Check your API key's usage and quota. `GET /v1/usage` is free — it does not consume a credit."},{"name":"Feedback","description":"Submit bug reports or feature requests directly from your integration. Feedback is reviewed by the Registrum team and helps prioritise fixes and improvements. Authenticated with your API key — no extra setup required."}],"security":[{"ApiKeyAuth":[]}]}