Webhooks
Webhooks let you integrate SauBit with medical records (HIS/EMR) and internal hospital automations.
When configured, SauBit sends events via POST to a hospital endpoint (e.g. https://hospital.example.com/webhooks/events).
How to configure
Webhook registration is done in the Admin (organization):
- Destination URL
- Desired events (or all)
- Secret (generated and shown only once)
Sent headers
X-SauBit-Event: event typeX-SauBit-Timestamp: epoch secondsX-SauBit-Signature: HMAC signature
Signature (HMAC)
The body is signed with:
HMAC_SHA256(secret, "{timestamp}.{raw_body}")
Header format:
X-SauBit-Signature: sha256=<hex>
Events
analysis.completed
Fired when an analysis finishes (check or recheck).
{
"event": "analysis.completed",
"data": {
"analysis_id": "uuid",
"endpoint": "/interactions/check",
"cached": false,
"recheck_of": "optional-uuid"
}
}
chat.message
Fired when the hospital sends a chat message.
{
"event": "chat.message",
"data": {
"session_id": "abc123",
"analysis_id": "optional-uuid"
}
}
Verification example (Node.js)
import crypto from "crypto";
// Accepts the current secret and, during rotation, the previous one (X-SauBit-Signature-Previous).
export function verifySauBitWebhook(req, secrets) {
const timestamp = req.headers["x-saubit-timestamp"];
const rawBody = req.rawBody; // raw body (bytes/string)
const msg = `${timestamp}.${rawBody}`;
const candidates = [
req.headers["x-saubit-signature"],
req.headers["x-saubit-signature-previous"],
].filter(Boolean);
for (const secret of [].concat(secrets)) {
const expected = `sha256=${crypto.createHmac("sha256", secret).update(msg).digest("hex")}`;
const expectedBuf = Buffer.from(expected);
for (const sig of candidates) {
const sigBuf = Buffer.from(String(sig));
// timingSafeEqual throws if lengths differ — check first.
if (sigBuf.length === expectedBuf.length && crypto.timingSafeEqual(sigBuf, expectedBuf)) {
return true;
}
}
}
return false;
}
Verification example (Python)
import hmac
import hashlib
def verify_saubit(signature: str, timestamp: str, raw_body: bytes, secret: str) -> bool:
msg = timestamp.encode() + b"." + raw_body
digest = hmac.new(secret.encode(), msg, hashlib.sha256).hexdigest()
expected = f"sha256={digest}"
return hmac.compare_digest(signature, expected)
Event envelope
Each delivery carries a stable envelope:
{
"event_id": "evt_uuid",
"event": "analysis.completed",
"schema_version": "1.0",
"created_at": "2026-06-30T12:00:00Z",
"delivery_id": "dlv_uuid",
"attempt": 1,
"data": { "...": "..." }
}
- At-least-once delivery: the same
event_idmay arrive duplicated — dedupe byevent_id.delivery_id/attemptidentify the attempt. - Events may arrive out of order — do not assume ordering; use
created_at.
Delivery, retry and replay
Each event is delivered with up to 3 attempts (retry on network error or non-2xx response, with backoff). Every delivery — success or failure — is logged (event, URL, HTTP status, attempt count, last error). Respond 2xx to acknowledge receipt.
Manual replay of a past delivery, listing the delivery log and a test send
(webhook.test) are admin operations, in the Admin Swagger (/docs/admin).
Secret rotation (two valid secrets)
When the secret is rotated (admin), the previous one stays valid during the window: each
delivery includes X-SauBit-Signature (current) and X-SauBit-Signature-Previous
(previous). Validate against either — the example above already does — to rotate without
dropping deliveries.