Developers
Webhooks
Subscribe to lifecycle events instead of polling. Every delivery is signed — verify the signature against the raw body before trusting anything in the payload.
Payload
Example delivery
{
"id": "evt_01H…",
"type": "transcription.completed",
"created_at": "2026-09-21T09:14:22Z",
"api_version": "v1",
"data": {
"consultation_id": "b0f1…",
"external_consultation_id": "clinic-4821",
"job_id": "job_01H…"
}
}- Identifiers only
- Payloads never contain transcripts, note bodies, patient names, contacts or any identifying content. Fetch content afterwards with a scoped API call.
- api_version
- Pinned to the API version the subscription was created under, so a future v2 cannot silently change your payload shape.
- Delivery
- Up to six attempts with exponential backoff. Respond 2xx quickly and process asynchronously.
- Idempotency
- Deduplicate on the event id — retries reuse it.
Signature verification
The x-scribemd-signature header is formatted t=<unix timestamp>,v1=<hex HMAC-SHA256 of `${timestamp}.${rawBody}`>. Always compare in constant time and reject stale timestamps.
Node
import crypto from "node:crypto";
// Read the RAW body — parsing it first breaks the signature.
export function verify(rawBody, header, secret) {
const parts = Object.fromEntries(
header.split(",").map((p) => p.split("=")),
);
const expected = crypto
.createHmac("sha256", secret)
.update(`${parts.t}.${rawBody}`)
.digest("hex");
const a = Buffer.from(parts.v1 ?? "", "hex");
const b = Buffer.from(expected, "hex");
if (a.length !== b.length || !crypto.timingSafeEqual(a, b)) return false;
// Reject replays older than five minutes.
return Math.abs(Date.now() / 1000 - Number(parts.t)) < 300;
}Python
import hmac, hashlib, time
def verify(raw_body: bytes, header: str, secret: str) -> bool:
parts = dict(p.split("=", 1) for p in header.split(","))
signed = f"{parts['t']}.".encode() + raw_body
expected = hmac.new(secret.encode(), signed, hashlib.sha256).hexdigest()
if not hmac.compare_digest(expected, parts.get("v1", "")):
return False
return abs(time.time() - int(parts["t"])) < 300The signing secret is generated when you register an endpoint and displayed once. Endpoints must be HTTPS.
Event catalogue
consultation.created
consultation.processing
transcription.completed
transcription.failed
transcription.partial
soap.generated
quality_review.completed
consultation.completed
consultation.failed
clinical.note.created
clinical.note.updated
subscription.updated
payment.completed
academy.course.completed
academy.certificate.issued
early_warning.signal.created
- consultation.created
- An encounter record was opened through the API.
- transcription.completed
- Transcription finished and the transcript can be fetched.
- transcription.failed
- Transcription could not be produced; no content is available for that encounter.
- soap.generated / clinical.note.created
- An AI draft note was generated and awaits clinician review.
- clinical.note.updated
- An existing note changed state — including clinician approval.
- quality.flagged
- Automated quality review raised a flag on a draft.
- payment.completed
- A verified subscription payment succeeded (reference, amount, currency only).
- subscription.updated
- A subscription plan or expiry date changed after payment.
- academy.course.completed
- A learner finished every module of an Academy track.
- academy.certificate.issued
- A verifiable Academy credential was issued (credential ID and track).
- early_warning.signal.created
- A de-identified surveillance signal was recorded (syndrome, age band, day, coarse area).