XUND
Guides

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

  1. Verify the signature. If verification fails, respond with 401.
  2. Look for the same webhook-id in your saved events (Repeated events). If it is found, respond with 2xx.
  3. Otherwise, save the event and respond with 2xx. If saving failed, respond with 5xx.
  4. 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

EventSent when
check.finished.reportThe 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.emergencyThe 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"
  }
}
FieldMeaning
typeThe event: check.finished.report or check.finished.emergency.
timestampWhen the event happened, in ISO 8601 format. The value does not change when the request is sent again.
data.checkIdThe id of the check. It is the same checkId that is used in the rest of the API.
data.stateOptional. 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:

HeaderMeaning
webhook-idThe id of the event, for example msg_ae52425bd4f7114a2b14d5f9593146fbf4e8f9c8996e005110682dcad3529e8a. Every copy of the same event carries the same webhook-id.
webhook-timestampWhen this request was signed, in Unix seconds.
webhook-signatureThe 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 responseResult
2xxDelivery is complete.
No response within 30 seconds, connection error, 408, 429, or 5xxThe 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

On this page