Webhooks
Signed, retried, thin events for resource writes, consent changes and finalized documents, with signature verification in TypeScript, Dart and Python.
Anpheros Platform can call your server when something happens in your project: a resource was written, a person granted or revoked access, a document was finalized. Events are thin: ids, type, version — never clinical content. Read the resource through the API if you need it. Every delivery is signed; verify the signature before trusting it.
Register an endpoint
curl -X POST $BASE/v1/webhooks -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
-d '{"url": "https://your.app/anpheros/hook", "events": ["resource.created", "consent.revoked"], "description": "prod"}'
The response contains secret (whsec_…) once. Store it; you need it to verify signatures.
events accepts ["*"] or any of:
| type | when | data |
|---|---|---|
resource.created / resource.updated / resource.deleted |
your project wrote a FHIR resource (REST v1, FHIR API or a bundle) — or another project wrote into a record you have access to (you created the patient, or you hold an active grant); then data.external is true and data.source says who wrote it: author_type (patient / practitioner / device / import / ai), organization (the display name the writer acted on behalf of), practitioner (name from the directory, when known), source_system |
resource_type, id, patient (the id you see), version, last_updated, and for external writes external, source |
consent.granted |
a person granted your app access from the consent page (or a trusted app linked its record) | grant_id, patient, application_id, scopes, purpose, expires_at |
consent.revoked |
the person (or the platform) ended a grant | same as above plus revoked_at, reason |
document.finalized |
a document upload was finalized and checksummed | id, patient, content_type, size, sha256 |
ping |
you asked for a test delivery | endpoint |
Endpoints are per project and environment: a sk_test_ key manages sandbox endpoints,
a sk_live_ key production ones. URLs must be https:// (in sandbox, http://localhost is fine).
Up to 10 endpoints per project and environment.
The delivery
POST https://your.app/anpheros/hook
Content-Type: application/json
User-Agent: Anpheros-Webhooks/1.0
Anpheros-Event: resource.created
Anpheros-Event-Id: evt_…
Anpheros-Delivery: whd_…
Anpheros-Signature: t=1789800000,v1=5f3a…
{"id":"evt_…","type":"resource.created","created_at":"2026-09-19T12:00:00.000Z","environment":"production",
"project":"proj_…","data":{"resource_type":"Observation","id":"…","patient":"…","version":1,"last_updated":"…"}}
Respond with any 2xx within 10 seconds. Anything else (or a timeout) is a failure and the
delivery is retried with backoff for up to 24 hours (10 attempts), then marked exhausted.
Deliveries are at-least-once and may arrive out of order: use Anpheros-Event-Id to
de-duplicate and data.version to ignore stale updates.
Verify the signature
v1 is HMAC-SHA256 with your secret over <t>.<raw body>; reject if t is older than 5 minutes.
import { verifyWebhookSignature } from '@anpheros/sdk';
app.post('/anpheros/hook', express.raw({ type: 'application/json' }), async (req, res) => {
const ok = await verifyWebhookSignature({ secret: process.env.ANPHEROS_WEBHOOK_SECRET!, header: req.header('anpheros-signature'), body: req.body });
if (!ok) return res.status(400).end();
const event = JSON.parse(req.body.toString());
// … enqueue and return quickly
res.status(204).end();
});
import 'package:anpheros_sdk/anpheros_sdk.dart';
final ok = verifyWebhookSignature(secret: secret, header: request.headers['anpheros-signature'], body: rawBodyBytes);
import hmac, hashlib, time
def verify(secret: str, header: str, body: bytes, tolerance=300) -> bool:
parts = dict(p.split("=", 1) for p in header.split(","))
if abs(time.time() - int(parts["t"])) > tolerance:
return False
expected = hmac.new(secret.encode(), f"{parts['t']}.".encode() + body, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, parts["v1"])
Operate
GET /v1/webhooks/{id}/deliveries?status=failed— the log, newest first, with status code and error.POST /v1/webhooks/{id}/deliveries/{delivery_id}/redeliver— send one again now.POST /v1/webhooks/{id}/ping— a test event.POST /v1/webhooks/{id}/rotate— new secret (the old one stops immediately; update your server first if you can't afford a gap: register a second endpoint, then delete the old one).PATCH /v1/webhooks/{id}with{"active": false}pauses deliveries; pending ones are dropped when you delete the endpoint.
Both SDKs expose this under webhooks (create, list, get, update, delete, rotateSecret, ping, deliveries, redeliver).