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.


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.jsonEndpoints 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
| Type | When | Sensitive |
|---|---|---|
request.created | A patient request passes its 5-second undo window | ✓ |
request.acknowledged, request.resolved, request.cancelled | Staff, API or the patient act on it | ✓ |
request.escalated | An escalation step | ✓ |
task.completed, task.confirmed | Patient marks a task done; staff confirm | ✓ |
encounter.stage_changed | The derived stage changes | ✓ |
encounter.access_activated | The first patient session on the visit | ✓ |
code.called, code.updated, code.cleared | Department code alerts | |
feed.state_changed | Feed healthy, delayed or down | |
ingest.dead_lettered, ingest.roster_quarantined | Ingest problems to fix | |
integration.credential_expiring | 30, 7 and 1 days before expiry | |
visit.communication_log_ready | Departure, when document write-back is on | ✓ |
webhook.test | The 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 timestampReceiver checklist
- Verify the signature against the raw body, in constant time. Reject timestamps more than 5 minutes from now.
- Drop messages whose
expiresAthas passed (answer 2xx). - Deduplicate on
webhook-idfor at least 7 days. - Keep the highest
versionper objectid; don't rely on arrival order orupdatedAt. - Answer 2xx within 15 s; do the work asynchronously.
Retries and failure
| Rule | Value |
|---|---|
| Success | Any 2xx within 15 s |
| Redirects | Not followed; a 3xx is a failure |
| Schedule | Immediately, 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, 504 | Retried; Retry-After honoured up to 1 hour |
| 410 Gone | The endpoint is disabled immediately |
| Auto-disable | No success for 5 days with 10 or more failures; owners are emailed |
| Throughput | At most 10 deliveries per second and 4 in flight per endpoint; a circuit breaker pauses a failing endpoint |
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.
Related
Last updated on