Docs

Webhooks

Verify, deduplicate, and process signed appointment and consultation document events at your HTTPS endpoint.

Doctronic sends a signed HTTP POST to your HTTPS endpoint when an appointment changes state or a consultation document is ready. Every delivery uses the same camelCase envelope:

{
  "eventId": "0f8fad5b-d9cb-469f-a165-70867728950e",
  "event": "appointment.waiting_room_ready",
  "occurredAt": "2026-08-25T18:30:00Z",
  "data": {
    "appointmentId": "dc_01K1EXAMPLE00000000000000",
    "userId": "mem_01K6JGGM2G2DB4Y9KB9H9MJJ6Q",
    "waitingRoomUrl": "https://example.com/waiting-room/dc_01K1EXAMPLE00000000000000"
  }
}
FieldMeaning
eventIdStable across every delivery attempt of one event. Use it as your deduplication key.
eventThe event name. Match it exactly; event names are case-sensitive.
occurredAtWhen the event happened, not when this attempt reached your endpoint.
dataEvent-specific fields, listed below.

Events

Eventdata fieldsMeaning
appointment.waiting_room_readyappointmentId, userId, waitingRoomUrlA join URL is ready. Replace any URL you stored earlier; waiting-room URLs expire.
appointment.practitioner_joinedappointmentId, userIdA practitioner joined the appointment.
appointment.call_endedappointmentId, userIdThe appointment call ended.
artifact.readyartifactId, artifactType, chatId, userIdA consultation document is ready to retrieve. userId can be null.

Doctronic sends only the events enabled for your organization. Acknowledge unrecognized event names with 2xx, so new events leave the ones you handle unaffected.

Verify the signature

X-Doctronic-Signature carries a lowercase hexadecimal HMAC-SHA256 digest of the exact request body bytes, keyed with your webhook secret. Verify it against the raw body before you parse the JSON, and compare in constant time. Verify the bytes exactly as received: reserializing the body changes them and breaks the signature. The SDKs do this in one call.

import { createHmac, timingSafeEqual } from 'node:crypto';

export async function verifyDoctronicWebhook(
  request: Request,
  secret: string,
) {
  const body = Buffer.from(await request.arrayBuffer());
  const supplied = request.headers.get('x-doctronic-signature') ?? '';
  const expected = createHmac('sha256', secret).update(body).digest('hex');

  const suppliedBytes = Buffer.from(supplied, 'utf8');
  const expectedBytes = Buffer.from(expected, 'utf8');
  const valid =
    suppliedBytes.length === expectedBytes.length &&
    timingSafeEqual(suppliedBytes, expectedBytes);

  if (!valid) throw new Error('Invalid Doctronic webhook signature');
  return JSON.parse(body.toString('utf8'));
}

Store the webhook secret the same way you store the API token: see keep the token on your backend.

Process a delivery

  1. Read and keep the raw request body.
  2. Verify X-Doctronic-Signature with a constant-time comparison.
  3. Validate the envelope and the data fields you expect for that event.
  4. Insert eventId into a durable uniqueness store. If it's already there, return 2xx and stop.
  5. Queue the work and return 2xx promptly.
  6. Apply the event only to the matching organization and patient records in your system.

Signature verification and eventId deduplication together are your replay protection. The signed occurredAt is the only timestamp, and a retried delivery can arrive several hours after it, so use it only for workflow-specific freshness checks.

Delivery behavior

Your responseWhat Doctronic does
2xxMarks the delivery successful.
Network failure, 429, or 5xxRetries with exponential backoff for up to six hours.
Any other 4xxTreats the delivery as a permanent failure and stops.

appointment.waiting_room_ready retries for up to five minutes and stops once the event is no longer useful for the appointment. Other appointment events can stop retrying when the appointment has ended. The attempt count varies, so keep the receiver available, idempotent, and fast.

Deliveries can arrive out of order. When the order matters, compare occurredAt or read the current state from GET /appointments/{appointment_id}/.

Test your receiver

Sign a sample body with your staging secret and post it to your local endpoint:

export DOCTRONIC_WEBHOOK_SECRET="replace-with-your-staging-webhook-secret"

body='{"eventId":"0f8fad5b-d9cb-469f-a165-70867728950e","event":"appointment.call_ended","occurredAt":"2026-08-25T19:05:00Z","data":{"appointmentId":"dc_01K1EXAMPLE00000000000000","userId":"mem_01K6JGGM2G2DB4Y9KB9H9MJJ6Q"}}'
signature=$(printf '%s' "$body" | openssl dgst -sha256 -hmac "$DOCTRONIC_WEBHOOK_SECRET" | sed 's/^.* //')

curl --fail "http://localhost:3000/webhooks/doctronic" \
  --header "Content-Type: application/json" \
  --header "X-Doctronic-Signature: $signature" \
  --data-binary "$body"

Then confirm, with signed staging deliveries as well as local ones, that:

  • A valid signature succeeds and a changed body fails verification.
  • A repeated eventId applies its side effect once.
  • An unknown event name returns 2xx and leaves other events unaffected.
  • Retries and out-of-order deliveries leave your records in the latest state.
  • A new waiting-room URL replaces the stored one.
  • Processing failures reach your on-call team, with no secrets or unrestricted patient content in the logs.

Limits

  • Doctronic configures webhooks manually. Endpoint registration, event subscriptions, secret delivery and rotation, delivery history, and event replay have no API or dashboard. To request any of them, see get access.
  • Doctronic delivers the signing secret through a secure channel. Plan rotation as a short cutover: accept the new secret at the agreed point, verify a signed test delivery, then remove the old secret from your systems. The SDKs' verifyWebhook() and verify_webhook() take one secret, so during the cutover verify with the new secret and, when that fails, with the old one.

On this page