Skip to content

Webhooks

Receive signed EDFlow webhooks (Standard Webhooks): subscribe, verify signatures, deduplicate on webhook-id, order by version, expiry, retries and auto-disable, authenticating to your gateway, event types, and redelivery.

EDFlow delivers outbound events with Standard Webhooks, so you can verify them with an off-the-shelf library. Deliveries are at least once, unordered, thin (ids and structured fields, never free text), signed and time-limited.

Webhook endpoints: URL, subscribed event types, status and the signing secret (masked).
WebCaptured from build 5003605 on September 27, 2026

Subscribe

Create an endpoint in the portal or with the API: an https URL, the event types (or ["*"]), the sites, and optionally how EDFlow authenticates to your gateway. The signing secret (whsec_…) is shown once.

printf '%s' '{"eventTypes":["request.created"],"url":"https://10.0.0.5/hooks"}' > body.jsoncurl -sS -X POST 'https://api.sandbox.edflow.space/v1/webhook-endpoints' \  -H 'Content-Type: application/json' \  -H "Authorization: Bearer $EDFLOW_ACCESS_TOKEN" \  --data-binary @body.json

Endpoints must use https, a DNS name and port 443 or 1024–65535. Private, loopback and cloud-metadata addresses are refused. In customer test and live, deliveries come from fixed egress IP addresses listed by GET /v1/meta; expose your receiver through your DMZ or API gateway.

Event types

TypeWhenSensitive
request.createdA patient request passes its 5-second undo window✓
request.acknowledged, request.resolved, request.cancelledStaff, API or the patient act on it✓
request.escalatedAn escalation step✓
task.completed, task.confirmedPatient marks a task done; staff confirm✓
encounter.stage_changedThe derived stage changes✓
encounter.access_activatedThe first patient session on the visit✓
code.called, code.updated, code.clearedDepartment code alerts
feed.state_changedFeed healthy, delayed or down
ingest.dead_lettered, ingest.roster_quarantinedIngest problems to fix
integration.credential_expiring30, 7 and 1 days before expiry
visit.communication_log_readyDeparture, when document write-back is on✓
webhook.testThe test button

Sensitive types are delivered only when your app holds the matching read scope and the site's capability switch is on; each delivery is recorded as a disclosure for the patient's access report.

New event types are announced at least 60 days before they reach customer test or live. An endpoint subscribed to ["*"] only gets new types if you opt in with includeNewEventTypes.

The request

POST /hooks HTTP/1.1
Content-Type: application/json
webhook-id: msg_01J8ZR2K7Q4M9X3B5T1V6N8C0D
webhook-timestamp: 1789398764
webhook-signature: v1,K5oZfzN95Z9UVu1EsfQmfVNQhnkZ2pj9o9NDN/H/pI4=

{"id":"msg_01J8ZR2K7Q4M9X3B5T1V6N8C0D","type":"request.created","timestamp":"2026-09-14T14:12:44.310Z",
 "expiresAt":"2026-09-14T16:12:44.310Z","apiVersion":"2026-10-01","environment":"sandbox","site":"your-site-sbx",
 "data":{"object":"request","id":"req_4f8k2m9q1t7v","version":1,"encounterId":"enc_7k2m9q4t8v3w5x1y",
 "type":"blanket","category":"need","priority":"low","status":"open", "…": "…"}}

Verify

The signature is base64(HMAC-SHA256(secret, webhook-id + "." + webhook-timestamp + "." + rawBody)), sent as v1,<signature>. During a rotation, two space-separated signatures are sent.

import { Webhook } from "standardwebhooks";

const wh = new Webhook(process.env.EDFLOW_WEBHOOK_SECRET ?? ""); // "whsec_…"

export async function POST(req: Request) {
  const body = await req.text(); // raw body, never re-serialized JSON
  const msg = wh.verify(body, Object.fromEntries(req.headers)) as {
    id: string; type: string; expiresAt: string; data: { id: string; version: number };
  };
  if (Date.parse(msg.expiresAt) < Date.now()) return new Response(null, { status: 204 });
  if (await alreadyProcessed(msg.id)) return new Response(null, { status: 204 });
  await enqueue(msg); // apply only if data.version is newer than what you hold
  return new Response(null, { status: 204 });
}
from standardwebhooks.webhooks import Webhook

wh = Webhook(os.environ["EDFLOW_WEBHOOK_SECRET"])
msg = wh.verify(request.get_data(), dict(request.headers))  # raises on a bad signature or stale timestamp

Receiver checklist

  1. Verify the signature against the raw body, in constant time. Reject timestamps more than 5 minutes from now.
  2. Drop messages whose expiresAt has passed (answer 2xx).
  3. Deduplicate on webhook-id for at least 7 days.
  4. Keep the highest version per object id; don't rely on arrival order or updatedAt.
  5. Answer 2xx within 15 s; do the work asynchronously.

Retries and failure

RuleValue
SuccessAny 2xx within 15 s
RedirectsNot followed; a 3xx is a failure
ScheduleImmediately, then +5 s, +5 min, +30 min, +2 h, +5 h, +10 h, +14 h, +20 h, +24 h (about 3 days, with jitter), but never after expiresAt
429, 502, 503, 504Retried; Retry-After honoured up to 1 hour
410 GoneThe endpoint is disabled immediately
Auto-disableNo success for 5 days with 10 or more failures; owners are emailed
ThroughputAt most 10 deliveries per second and 4 in flight per endpoint; a circuit breaker pauses a failing endpoint
Screenshot pending: dev-console-webhook-deliveriesThis screenshot is captured automatically from the running app with sample data.
WebWebhook deliveries

Redelivery and recovery

The delivery log shows every attempt with its status code. Redeliver one delivery, or Recover everything since a time after an outage (within 30 days). Expired sensitive events are redelivered only if you ask explicitly.

Authenticating EDFlow to your gateway

Besides the signature, an endpoint can require OAuth 2.0 client credentials (you give EDFlow a client id, secret and token URL) or mutual TLS (EDFlow presents its outbound client certificate). Custom headers are stored encrypted.

Rotating the secret

Rotate secret issues a new secret; both sign during the overlap (1 to 168 hours, default 24), then the old one retires.

Last updated on

Was this page helpful?

On this page