Webhooks
Receive a notification on your server when a check finishes, and verify that each request is authentic.
A webhook notification is an HTTPS request that is sent to your server when a check finishes. It identifies the finished check, so your system can retrieve the result without polling the API.
Webhooks are configured by XUND on request. Partners do not configure anything themselves. On your side, an endpoint is needed that receives and verifies the requests.
Quick checklist
- Verify the signature. If verification fails, respond with
401. - Look for the same
webhook-idin your saved events (Repeated events). If it is found, respond with2xx. - Otherwise, save the event and respond with
2xx. If saving failed, respond with5xx. - Do slow work, such as loading the report, after responding.
Setup
To enable webhooks, send the following information to XUND:
- Webhook URL: the address on your server that receives the notifications. It must be a public
https://URL with a host name. IP addresses are not accepted. - Client Ids: the Client Ids whose checks trigger notifications. A check counts for the Client Id it was started with.
- Events: the events you want to receive (see Events).
XUND then sends you the signing secret once. It starts with whsec_ and is separate from your API key. The secret cannot be shown again, so store it safely. It is used to verify each request. To get a new secret, contact XUND. The old secret then stops working.
Events
| Event | Sent when |
|---|---|
check.finished.report | The check ends after its report is loaded for the first time, as JSON with GET /v1/chatbot/report or as PDF with GET /v1/chatbot/report/pdf, or the patient chooses "End check" on the screen shown when no illness could be suggested (no diagnosis). In the App, the report is loaded when it is shown to the patient, so this event fires at that point. Loading the report again does not send another notification. |
check.finished.emergency | The patient chose "End check" on the emergency warning. The check ends at that point. |
A check produces only one of these two events, never both.
The request
Each notification is a POST request to your webhook URL with a JSON body:
{
"type": "check.finished.report",
"timestamp": "2026-09-29T14:32:00.000Z",
"data": {
"checkId": "5e63f95e-ef3b-42ce-9027-ff6e021e19ef",
"state": "8b0e6c1a-1f7d-4c2e-9a53-2d6f0e7b4a91"
}
}| Field | Meaning |
|---|---|
type | The event: check.finished.report or check.finished.emergency. |
timestamp | When the event happened, in ISO 8601 format. The value does not change when the request is sent again. |
data.checkId | The id of the check. It is the same checkId that is used in the rest of the API. |
data.state | Optional. Only for checks started in the Symptom & Illness Check App with a state value. This is a UUID that you choose to identify the check in your system, for example a session or user id. It is returned unchanged with either event, so you can link the check to it. Checks run through the API never have this value, and the field is missing. See Webhooks and your own identifier. |
The notification only identifies the check that finished. It does not contain the result. To get the result, call the API with the checkId, for example GET /v1/chatbot/report.
Each request carries three headers:
| Header | Meaning |
|---|---|
webhook-id | The id of the event, for example msg_ae52425bd4f7114a2b14d5f9593146fbf4e8f9c8996e005110682dcad3529e8a. Every copy of the same event carries the same webhook-id. |
webhook-timestamp | When this request was signed, in Unix seconds. |
webhook-signature | The signature, for example v1,K5oZfzN95Z9UVu1EsfQmfVNQhnkZ2pj9o9NDN/H/pI4=. |
Verifying a request
The webhook URL is public, so anyone can send requests to it. Verify every request with your signing secret. If verification fails because the signature is wrong or the request is too old, respond with 401 and ignore the request.
Requests are signed according to the Standard Webhooks specification, so an official Standard Webhooks library can be used for verification. These libraries also reject requests that are older than 5 minutes. This protects against recorded requests being sent again (replay attacks). Keep your server clock correct. Otherwise, genuine requests are rejected as well.
Verify the raw request body, exactly the bytes that were received. Parsing the JSON and serialising it again changes the bytes, and verification then fails. In Express, for example, use express.raw({ type: 'application/json' }) for this route instead of express.json().
For Node.js, use the standardwebhooks package:
const { Webhook } = require('standardwebhooks')
const webhook = new Webhook(process.env.XUND_WEBHOOK_SECRET) // your signing secret, whsec_...
// rawBody: the request body as a string, exactly as received
// headers: the request headers (webhook-id, webhook-timestamp, webhook-signature)
function readWebhookRequest(rawBody, headers) {
// Throws if the signature is wrong or the request is too old. Returns the parsed body.
return webhook.verify(rawBody, headers)
}Libraries for other languages are listed in the Standard Webhooks repository.
To test your endpoint without live notifications, create signed requests with the same library. webhook.sign(webhookId, new Date(), rawBody) returns the complete value of the webhook-signature header (v1,...).
Responding to a request
The sender waits 30 seconds for a response. Respond with a 2xx status code only after the event has been saved. Do slow work, such as loading the report, after responding. If the event could not be saved, respond with 5xx so that the request is sent again. The response body is ignored.
| Your response | Result |
|---|---|
2xx | Delivery is complete. |
No response within 30 seconds, connection error, 408, 429, or 5xx | The request is sent again several times over a few minutes. After that, no further attempts are made. |
Any other status code, including redirects (3xx) | No further attempts are made. Redirects are not followed. |
Repeated events
The same event can arrive more than once, for example when a request is sent again. Every copy carries the same webhook-id: if an event with that webhook-id is already saved, respond with 2xx and do nothing else.
Last updated on
Content Library & Medical Services
When to reach for the three lookup families — Content Library, Medical Services, and Specializations — with one worked request and the practical rules for each.
Error handling
The JSON shape of every Symptom & Illness Check API error, the 401 rules, a code-by-code table of what went wrong and what to do, and which requests are safe to retry.