---
title: "Create a webhook subscription"
method: POST
path: "/v1/webhooks"
tags: ["Webhooks"]
---

# Create a webhook subscription

`POST /v1/webhooks`

## Request

- Base URL: `https://api.registrum.co.uk`
- URL: `https://api.registrum.co.uk/v1/webhooks`
- Auth: API key in header `X-API-Key`

Create a webhook subscription to watch UK companies for changes.

Registrum checks each watched company roughly every 15 minutes. When a change
is detected (e.g., status becomes dissolved, accounts go overdue), a signed
POST is delivered to your URL.

**Supported events:**
- `status_change` - company_status changes (e.g., active → dissolved)
- `accounts_overdue` - accounts filing deadline missed
- `confirmation_overdue` - confirmation statement deadline missed
- `directors_changed` - a director is appointed or resigned (checked every 6 hours)
- `pscs_changed` - a PSC is added, ceased, or their ECCTA verification status changes (every 6 hours)
- `compliance_risk_changed` - the company's overall ECCTA risk rating changes, e.g., compliant → partial
  (every 6 hours). **Requires Pro/Enterprise or the Compliance Monitoring add-on.**

**Webhook watch caps** - the number of *distinct* companies you can watch across all
your webhooks (existing + this request combined), by plan:

| Plan | Companies watched |
|------|--------------------|
| Free | 1 |
| Web | 5 |
| Pro | 25 |
| Enterprise | 250 |

Exceeding your cap, or requesting `compliance_risk_changed` without Pro/Enterprise,
returns `403 PLAN_REQUIRED`. The **Compliance Monitoring add-on** raises your cap to
100 companies and unlocks `compliance_risk_changed` on any plan.

**Payload delivered to your URL:**
```json
{
  "event": "company_updated",
  "company_number": "00445790",
  "changes": {
    "status_change": {"from": "active", "to": "dissolved"}
  },
  "triggered_at": "2026-03-11T10:00:00Z"
}
```

**Verifying signatures:**
Every delivery includes an `X-Registrum-Signature: sha256=<hex>` header.
Verify it with HMAC-SHA256 using the `secret` returned at creation time:
```python
import hmac, hashlib
expected = "sha256=" + hmac.new(secret.encode(), request.body, hashlib.sha256).hexdigest()
assert hmac.compare_digest(expected, request.headers["X-Registrum-Signature"])
```

**Note:** The `secret` is returned once at creation - store it securely.

## Request body

- WebhookCreateRequest: Request body for creating a webhook subscription.
  - `url` string, uri, required: HTTPS URL that will receive POST requests when a watched company changes.
  - `company_numbers` string[], required: List of Companies House company numbers to watch (1–50).
  - `events` string[]: 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.

## Response `200`

Webhook created. Response includes the signing secret - store it securely.

- WebhookEnvelope: Response envelope for POST /v1/webhooks and GET /v1/webhooks/{webhook_id}.
  - `status` string: 'success' or 'error'
  - `data` WebhookSubscription: A webhook subscription row.
    - `id` string, required: Webhook ID (UUID). Use it with GET and DELETE /v1/webhooks/{webhook_id}
    - `api_key_id` string, nullable: ID of the API key that owns this webhook
    - `url` string, required: URL that receives signed POST deliveries when a watched company changes
    - `company_numbers` string[], required: Companies House numbers being watched
    - `events` string[], required: Event types subscribed to, e.g. `status_change`, `accounts_overdue`
    - `secret` string, required: HMAC-SHA256 signing secret for verifying the `X-Registrum-Signature` header. Store it securely
    - `is_active` boolean, required: True while the subscription is delivering
    - `created_at` string, nullable: ISO 8601 timestamp when the webhook was created
    - `last_triggered_at` string, nullable: ISO 8601 timestamp of the last delivery attempt (null if never triggered)
    - `failure_count` integer, nullable: Consecutive delivery failures recorded for this webhook
  - `cached` boolean: True if this response was served from cache rather than fetched live
  - `cache_age_seconds` integer, nullable: How many seconds ago this data was fetched from Companies House (null if not cached)
  - `fetched_at` string, nullable: 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.
  - `data_source` string: '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
  - `credits_used` integer: Number of API credits consumed by this request
  - `credits_remaining` integer, nullable: API credits remaining this month (null in dev mode)

## Other responses

- `400`: Invalid event type or company number
- `401`: Missing or invalid API key
- `403`: 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`: Validation error
- `503`: Storage unavailable (Supabase not configured)

---

[API](https://skmtc.dev/registrum/apis/registrum-api.md) · [All operations](https://skmtc.dev/registrum/apis/registrum-api/llms.txt) · [OpenAPI document](https://skmtc.dev/registrum/apis/registrum-api/revisions/2bf0ecf7cc02?raw)
