Subscribing to Webhooks¶
Webhook delivery is managed through the Subscription Management API under base path /apis/v1/webhooks. A subscription is the unit of delivery: it ties one or more event types to a single delivery URL with its own signing secret.
Subscription Model¶
Each subscription has:
- A human-readable
name - One or more
event_types(all delivered to the same endpoint) — see Event Types - Its own
notification_url(delivery target) - Its own
signature_key(HMAC secret — shown only at creation and rotation) - An
api_versionpinning the payload format (2026-06-11is current) - An
is_activetoggle
One organisation can hold multiple subscriptions — e.g. one for patient.* events going to your EHR sync, another for appointment.* going to a scheduling integration, each with isolated credentials. You can also fan the same event out to several endpoints by creating multiple subscriptions with different notification_urls. You cannot register two active subscriptions that send the same event to the same notification_url — see Validation Rules.
Authentication¶
All /apis/v1/webhooks/* endpoints require a JWT Bearer token:
| Claim | Required | Used for |
|---|---|---|
organisation_id |
Yes | Identifies your organisation |
user_id |
No | Audit logging |
name / user_name |
No | Audit logging |
The first time you call POST /apis/v1/webhooks/subscriptions with a new organisation_id, the organisation is auto-provisioned from the JWT claims — there is no separate registration step.
Subscription Endpoints¶
| Method | Path | Description |
|---|---|---|
POST |
/apis/v1/webhooks/subscriptions |
Create a subscription. Returns signature_key once. |
GET |
/apis/v1/webhooks/subscriptions |
List your subscriptions. Optional ?is_active=true\|false. |
GET |
/apis/v1/webhooks/subscriptions/:id |
Get a single subscription (signature_key hidden). |
PATCH |
/apis/v1/webhooks/subscriptions/:id |
Update name, event_types, notification_url, api_version, or is_active. Replaces the whole event_types array. |
POST |
/apis/v1/webhooks/subscriptions/:id/subscribe |
Add event_types to a subscription, keeping the existing ones. |
POST |
/apis/v1/webhooks/subscriptions/:id/unsubscribe |
Remove specific event_types from a subscription, keeping the rest. |
DELETE |
/apis/v1/webhooks/subscriptions/:id |
Delete the subscription. |
POST |
/apis/v1/webhooks/subscriptions/:id/rotate-secret |
Generate a new signature_key. Returned once. |
POST |
/apis/v1/webhooks/subscriptions/:id/test |
Send a synthetic webhook to your notification_url to confirm wiring. |
Create a Subscription¶
Request Body¶
| Parameter | Type | Required | Description |
|---|---|---|---|
name |
string | Yes | Human-readable subscription name. Non-empty. |
event_types |
array | Yes | Non-empty array of event types. No duplicates. |
notification_url |
string | Yes | Valid delivery URL. Must use https:// outside of development. |
api_version |
string | No | Payload format version. Defaults to the latest (2026-06-11). Rejected with 400 INVALID_API_VERSION if unknown. |
Request¶
curl --location '{base_url}/apis/v1/webhooks/subscriptions' \
--header 'Authorization: Bearer JWT_TOKEN' \
--header 'Content-Type: application/json' \
--data '{
"name": "Patient + appointment webhook",
"event_types": ["patient.created", "patient.updated", "appointment.created"],
"notification_url": "https://example.com/webhooks/spry",
"api_version": "2026-06-11"
}'
Success Response¶
Code: 201 Created
{
"subscription": {
"id": "773aaca1-4e36-4efc-b425-1201341a1712",
"organisation_id": "3533728b-e152-4021-8e35-449adb9c0001",
"name": "Patient + appointment webhook",
"event_types": ["patient.created", "patient.updated", "appointment.created"],
"notification_url": "https://example.com/webhooks/spry",
"api_version": "2026-06-11",
"is_active": true,
"created_at": "2026-06-01T09:26:33.000Z",
"updated_at": "2026-06-01T09:26:33.000Z"
},
"signature_key": "<128 hex chars>",
"message": "Save this signature_key — it is used to verify webhook signatures and will not be shown again. Rotate via POST /subscriptions/:id/rotate-secret."
}
Response Fields¶
| Field | Type | Description |
|---|---|---|
subscription |
object | The created subscription record. |
subscription.id |
string | Subscription identifier — use it on all /subscriptions/:id routes. |
subscription.organisation_id |
string | Owning organisation. |
subscription.name |
string | Subscription name. |
subscription.event_types |
array | Subscribed event types. |
subscription.notification_url |
string | Delivery target. |
subscription.api_version |
string | Pinned payload format version. |
subscription.is_active |
boolean | Whether deliveries are enabled. |
subscription.created_at / updated_at |
string (date) | ISO 8601 timestamps. |
signature_key |
string | HMAC secret for signature verification. Returned only at creation and on rotation — store it immediately. |
message |
string | Reminder that the key will not be shown again. |
Error Response¶
Code: 400 Bad Request
Validation Rules¶
name— required, non-empty.event_types— required, non-empty array. Each value must be from Event Types. No duplicates.notification_url— required, valid URL. Must usehttps://outside of development.api_version— optional. Must be a supported version; defaults to the latest. Rejected with400 INVALID_API_VERSIONif unknown.- No duplicate endpoint per event — if you already have an active subscription delivering one of these
event_typesto this exactnotification_url, the request is rejected with409 DUPLICATE_SUBSCRIPTION(the response names the conflicting subscription and the overlapping event(s)). Update the existing subscription, or point this one at a different URL to fan the event out to a second endpoint. The same check runs onPATCHwhen you changenotification_url,event_types, or reactivate a subscription.
Managing a Subscription's Events¶
A subscription holds a set of event_types. There are two ways to change that set:
PATCH /subscriptions/:idwithevent_typesreplaces the whole list — whatever array you send becomes the complete set. Sending a partial list drops everything else. Use this when you want to set the exact set explicitly.POST /subscriptions/:id/subscribeandPOST /subscriptions/:id/unsubscribechange the set incrementally — add or remove just the events you name, leaving the rest untouched. Use these when you want to adjust one or two events without re-sending the whole array.
Each subscription is independent — changing one subscription's events never affects another.
Subscribe to More Events¶
Add one or more event types to an existing subscription, keeping its current events (and its notification_url / signature_key). Adding an event that's already present is a no-op.
curl --location '{base_url}/apis/v1/webhooks/subscriptions/773aaca1-4e36-4efc-b425-1201341a1712/subscribe' \
--header 'Authorization: Bearer JWT_TOKEN' \
--header 'Content-Type: application/json' \
--data '{ "event_types": ["case.discharged"] }'
The response is the updated subscription. Rejected with 409 DUPLICATE_SUBSCRIPTION if an added event would collide with another active subscription delivering to the same notification_url.
Unsubscribe from Events¶
Remove one or more event types from a subscription while keeping it (and its remaining events) active. Use this instead of PATCH when you just want to stop a few events without re-sending the whole event_types array.
curl --location '{base_url}/apis/v1/webhooks/subscriptions/773aaca1-4e36-4efc-b425-1201341a1712/unsubscribe' \
--header 'Authorization: Bearer JWT_TOKEN' \
--header 'Content-Type: application/json' \
--data '{ "event_types": ["case.updated", "appointment.cancelled"] }'
The response is the updated subscription.
- Event types you list that the subscription isn't subscribed to are ignored (the call is idempotent).
- If removing them would leave the subscription with no events, the request is rejected with
400 CANNOT_REMOVE_ALL_EVENTS— delete the subscription withDELETE /subscriptions/:idinstead.
Rotate the Signature Key¶
Generates a new signature_key, returned once in the response. The previous key stops signing immediately — including in-flight retries of earlier events, which are re-signed with the new key on each attempt. Update your endpoint to verify with the new key promptly to avoid rejecting retried deliveries.
Test a Subscription¶
Sends a synthetic webhook to your notification_url, signed with the subscription's current signature_key. The payload envelope includes "test": true so you can ignore test deliveries in production code paths. The response includes the dispatched payload and the HTTP result so you can debug from the API side.
Organisations¶
/apis/v1/webhooks/organisations exposes your organisation profile. Webhook credentials live on subscriptions, not on the organisation.
| Method | Path | Description |
|---|---|---|
GET |
/apis/v1/webhooks/organisations |
Get your organisation's profile. |
PATCH |
/apis/v1/webhooks/organisations |
Update name or is_active. |