AnpherosAnpheros PlatformDevelopers

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
From a write to your webhook
Your codeAnpherosYour server1. POST /v1/webhooks · url, events2. id + whsec_… secret (once)3. a resource is written4. event + Anpheros-Signature5. verify HMAC, ≤ 300 s6. 2xx within 10 s7. GET the resource with your key

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.

Try it free in the sandbox · first FHIR call in 5 minutesFree · 30 synthetic patients · no cardYour first API call in 2 minutes →

Product updates by email

New features, guides and the end of the beta. One email a month at most.

Send me occasional Anpheros Platform updates by email (new features, API changes, guides). I'll confirm by email and can unsubscribe in one click from any email. Privacy