How To: Webhooks
This guide explains how to register webhook subscriptions and how to verify that incoming webhook payloads are from Semble.
Prerequisites
- A valid API token with seeWebhooks, createWebhooks, editWebhooks, and/or deleteWebhooks permissions as needed
- Webhooks enabled for your practice (contact Semble if not visible)
- An HTTPS endpoint that can receive POST requests
Step 1: Register a webhook subscription
Use the createWebhook mutation to register a destination URL and the events you want to receive.
Required:
- url – Your HTTPS endpoint (e.g.
https://your-app.com/webhooks/semble) - eventTypes – Array of event type enum values (see Event types)
- label – Short name to identify this subscription (e.g. in your admin UI)
Optional:
- headers – Key-value pairs (e.g.
Authorization,X-Request-Id) that Semble will send with each webhook delivery
Response: The mutation returns the webhook ID and the secret. Store the secret securely; you will need it to verify incoming webhooks once delivery is available.
Example:
mutation CreateWebhook($input: CreateWebhookInput!) {
createWebhook(input: $input) {
data {
id
secret
url
label
eventTypes
headers { key value }
version
}
error
}
}
Variables:
{
"input": {
"url": "https://your-app.com/webhooks/semble",
"eventTypes": ["PATIENT_CREATED", "PATIENT_DELETED"],
"label": "Main integration",
"headers": [
{ "key": "X-Custom-Header", "value": "your-value" }
]
}
}
Step 2: Update a webhook subscription
Use the updateWebhook mutation to change the URL, event types, label, custom headers, or delivery pause state (deliveryPaused) for an existing subscription. You need a token with editWebhooks permission.
Partial input: Only include fields you want to change.
- url – Must remain HTTPS if provided
- eventTypes – At most 10 event types per subscription
- label – At most 256 characters
- headers – Same shape as create; send an empty array to clear headers
- deliveryPaused – Set to
trueto pause outbound delivery (e.g. maintenance) orfalseto resume. This is separate from soft-delete: usedeleteWebhookto soft-delete a subscription (deletedbecomes true).deletedcannot be changed viaupdateWebhook.
Response: Returns the subscription id, full configuration (including deliveryPaused and deleted as stored), and the persisted secret (same as on create) so you can confirm you are updating the correct subscription. List and get queries still do not return the secret.
mutation UpdateWebhook($id: ID!, $input: UpdateWebhookInput!) {
updateWebhook(id: $id, input: $input) {
data {
id
secret
url
label
eventTypes
deleted
deliveryPaused
headers { key value }
version
createdAt
updatedAt
}
error
}
}
Duplicate subscriptions (same URL and overlapping event types as another active subscription for your practice) are rejected with a conflict error, same as on create.
Event types
Use the WebhookEventType enum values in eventTypes when creating or updating a subscription.
| Enum value | Description |
|---|---|
| PATIENT_CREATED | A patient was created |
| PATIENT_UPDATED | A patient was updated |
| PATIENT_DELETED | A patient was deleted |
Duplicate subscriptions (same URL and same event type for your practice) are rejected.
Step 3: Unregister a webhook
To stop receiving events for a subscription, use the deleteWebhook mutation with the webhook id returned when you created it. You need a token with deleteWebhooks permission. Only the practice that created the subscription can delete it.
The subscription is soft-deleted (marked as deleted); it is not removed from the database so that webhook log history is retained. Events will no longer be sent to that URL for the deleted subscription.
mutation DeleteWebhook($id: ID!) {
deleteWebhook(id: $id) {
data { id }
error
}
}
If the webhook is not found (wrong id or already deleted), the mutation returns data: null and an error with a 404 response.
Step 4: List and get webhooks
You need a token with seeWebhooks permission.
List all webhooks for the current practice with the webhooks query. By default only non-deleted subscriptions are returned. Pass includeDeleted: true to include soft-deleted ones.
query ListWebhooks($includeDeleted: Boolean) {
webhooks(includeDeleted: $includeDeleted) {
id
url
label
eventTypes
deleted
deliveryPaused
version
createdAt
updatedAt
headers { key value }
}
}
Get a single webhook by id with the webhook query. Returns the subscription if it belongs to your practice (including when deleted), or null if not found.
query GetWebhook($id: ID!) {
webhook(id: $id) {
id
url
label
eventTypes
deleted
deliveryPaused
version
createdAt
updatedAt
headers { key value }
}
}
Fields: id, url, label, eventTypes, deleted (true if soft-deleted), version, createdAt, updatedAt, headers. The secret is only returned when you create or update a subscription, not in list or get.
Verifying the trusted sender (WIP)
Semble will sign each webhook request so you can verify it was sent by us and not modified in transit. The secret returned when you create or update a subscription is used to produce a signature over the request body; only someone with that secret can generate a valid signature.
Status: The exact signing scheme (header name, format, and algorithm) is not yet finalised. The guidance below is generic so you can prepare your endpoint to verify payloads once delivery is implemented.
Why store the secret?
- Authenticity – You can check that the request came from Semble (we are the only ones with your secret).
- Integrity – You can detect if the body was changed in transit (the signature will not match).
When Semble delivers a webhook, we will send a signature (e.g. in a header) computed from the raw body and your secret. On your server you will recompute the same value and compare it in constant time. Do not process the payload until verification succeeds.
Generic verification steps (when delivery is available)
- Read the raw request body as received (before parsing JSON). Do not modify or re-serialize it.
- Retrieve the secret for the webhook subscription (e.g. by subscription ID in the payload or header).
- Compute the expected signature using the algorithm and encoding documented in the API (e.g. HMAC-SHA256 of raw body with secret, then hex or base64).
- Compare with the value in the signature header using constant-time comparison to avoid timing attacks. Reject (e.g. 401 or 403) if missing or invalid.
Security notes
- Keep the secret private. Do not log it or expose it in client-side code.
- Always verify before processing the payload. Reject requests with a missing or invalid signature.
- Use the raw body for verification. If your framework parses the body automatically, configure it to expose the raw bytes for the signature check first.
For the exact header name, format, and algorithm used in production, refer to the latest API or webhook delivery documentation when available.