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"])) < 300

The 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).