Try it free in the sandbox · first FHIR call in 5 minutesFree · 30 synthetic patients · no cardYour first API call in 2 minutes →
Receive your first webhook
Step 4: register a webhook endpoint, verify the Anpheros-Signature HMAC in Node and Python, send a test ping, and handle retries, the delivery log and redelivery.
Step 4 of 5. Instead of asking the API every few minutes whether a record changed, register an address and Anpheros sends you a short, signed event when something happens: a resource written, a consent granted or revoked, a document finalized. Your server verifies the signature, then reads the resource itself with its own credentials.
In short:
| Register | POST /v1/webhooks with url and events (key with write scope) |
| Secret | whsec_…, returned once, when you create the endpoint or rotate it |
| Signature | header Anpheros-Signature: t=<unix time>,v1=<hex>, HMAC-SHA256 of "<t>." + raw body |
| Accept within | 300 seconds of t |
| Test | POST /v1/webhooks/{id}/ping sends a ping event |
| Retries | up to 10 attempts with growing delays; the endpoint is disabled after 10 failures in a row |
1. Get a public HTTPS address
The address must be public and use https. While you develop, a tunnel works: run your server on
localhost:3000 and expose it with a tunnelling tool of your choice, then use the https://…
address it gives you.
2. Register the endpoint
curl -X POST https://platform.anpheros.com/v1/webhooks \
-H "Authorization: Bearer $ANPHEROS_KEY" -H 'content-type: application/json' -d '{
"url": "https://your-tunnel.example/anpheros/webhook",
"events": ["resource.created", "resource.updated", "consent.revoked"],
"description": "dev laptop"
}'
The answer contains id and secret (whsec_…). Save the secret now: it is shown only once.
Leave events out to receive every event (["*"]). A project can have up to 10 endpoints per
environment. The list of event names: GET /v1/webhooks/event-types.
| Event | When |
|---|---|
resource.created, resource.updated, resource.deleted |
a FHIR resource in a record you can see was written |
consent.granted, consent.revoked |
a person approved or withdrew access for your app |
document.finalized |
an uploaded document is complete |
ping |
you asked for a test event |
3. Verify and handle the event
The event body is thin: it says what changed, not the medical data.
{"id":"evt_…","type":"resource.created","created_at":"2026-11-08T10:15:02Z","environment":"sandbox","project":"proj_…","data":{"resource_type":"Observation","id":"…","patient":"…","version":1,"last_updated":"…"}}
Verify the signature on the raw body, before parsing it. Node (Express):
import crypto from 'node:crypto';
import express from 'express';
const SECRET = process.env.ANPHEROS_WEBHOOK_SECRET; // whsec_…
const app = express();
app.post('/anpheros/webhook', express.raw({ type: 'application/json' }), (req, res) => {
const parts = Object.fromEntries(String(req.get('Anpheros-Signature') || '').split(',').map(p => p.split('=')));
const t = Number(parts.t);
const expected = crypto.createHmac('sha256', SECRET).update(`${parts.t}.${req.body}`).digest('hex');
const ok = parts.v1 && expected.length === parts.v1.length &&
crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(parts.v1)) && Math.abs(Date.now() / 1000 - t) <= 300;
if (!ok) return res.status(400).send('bad signature');
const event = JSON.parse(req.body);
res.sendStatus(204); // answer quickly, then work
if (event.type === 'resource.created') {
// read the resource with your own key: GET /fhir/R4/{resource_type}/{id}
}
});
app.listen(3000);
Python (Flask):
import hashlib, hmac, json, os, time
from flask import Flask, request
SECRET = os.environ["ANPHEROS_WEBHOOK_SECRET"].encode()
app = Flask(__name__)
@app.post("/anpheros/webhook")
def webhook():
raw = request.get_data() # bytes, exactly as sent
parts = dict(p.split("=", 1) for p in request.headers.get("Anpheros-Signature", "").split(",") if "=" in p)
expected = hmac.new(SECRET, parts.get("t", "").encode() + b"." + raw, hashlib.sha256).hexdigest()
if not hmac.compare_digest(expected, parts.get("v1", "")) or abs(time.time() - int(parts.get("t", 0))) > 300:
return "bad signature", 400
event = json.loads(raw)
print(event["type"], event["data"])
return "", 204
The TypeScript and Dart SDKs have a ready verifier with the same rules: see Webhooks.
Other headers on each delivery: Anpheros-Event (the type), Anpheros-Event-Id (the same as id;
store it and ignore repeats) and Anpheros-Delivery.
4. Send yourself a test event
curl -X POST https://platform.anpheros.com/v1/webhooks/$WEBHOOK_ID/ping -H "Authorization: Bearer $ANPHEROS_KEY"
Your server should log a ping event. Then write an observation (see Your first write)
and watch the resource.created event arrive.
5. When deliveries fail
Answer with a 2xx within 10 seconds. Anything else, or no answer, is retried with growing delays,
up to 10 attempts; after 10 failed deliveries in a row the endpoint is disabled.
# what was delivered, and what failed
curl "https://platform.anpheros.com/v1/webhooks/$WEBHOOK_ID/deliveries?limit=50" -H "Authorization: Bearer $ANPHEROS_KEY"
# send the failed ones again, after you fixed your server
curl -X POST https://platform.anpheros.com/v1/webhooks/$WEBHOOK_ID/deliveries/redeliver-exhausted -H "Authorization: Bearer $ANPHEROS_KEY"
# turn it back on, or change the events
curl -X PATCH https://platform.anpheros.com/v1/webhooks/$WEBHOOK_ID -H "Authorization: Bearer $ANPHEROS_KEY" \
-H 'content-type: application/json' -d '{"active": true}'
# new secret (the old one stops working)
curl -X POST https://platform.anpheros.com/v1/webhooks/$WEBHOOK_ID/rotate -H "Authorization: Bearer $ANPHEROS_KEY"
Common errors
| Symptom | Fix |
|---|---|
| Every event fails the signature check | You verified the parsed and re-serialized JSON: verify the raw bytes, as above |
400 when registering |
The url is not public https, or an event name is misspelled; see GET /v1/webhooks/event-types |
| Events stopped arriving | The endpoint was disabled after 10 failures: check the deliveries, fix, then PATCH {"active": true} |
| The same event twice | Deliveries can repeat: keep the Anpheros-Event-Id you processed and skip repeats |
Next step
Step 5: Go live, from the sandbox to production with real patients.
Frequently asked questions
Why does the event not contain the data itself?
So that medical data never travels to an address unless your server asks for it with its own credentials, and so an old event never carries data the person has since withdrawn.
Can I receive events for patients who use my app through OAuth?
Yes. Events cover the records your project can see, including those shared with your app by consent, and consent.revoked tells you when access ends.
How do I test without a public server?
Use a tunnel to your laptop, as in step 1, and the ping endpoint to send test events whenever you want.
Ask your AI
Paste this into Claude, ChatGPT, Cursor or Claude Code. Your assistant reads this guide and adapts it to your code.
Read https://developers.anpheros.com/guides/first-webhook.md and help me receive and verify Anpheros webhooks in my backend. Use the Anpheros sandbox (base URL https://platform.anpheros.com) with my API key from the ANPHEROS_KEY environment variable, never written in the code. Give me complete, runnable code, the response I should expect, and how to fix the errors in https://developers.anpheros.com/guides/troubleshooting.md if a call fails.Open in ClaudeOpen in ChatGPTThis guide as MarkdownBuild the whole integration with an AI coding tool
Try it free in the sandbox · first FHIR call in 5 minutesFree · 30 synthetic patients · no cardYour first API call in 2 minutes →