Skip to main content

Subscribing to webhooks

There are two noteworthy properties of a Subscription:

  • Criteria: The rules that the server should use to determine when to generate notifications for this subscription. The Criteria is specified using FHIR search semantics.
  • Channel: Details where to send notifications when resources are received that meet the criteria. This is where you specify the HTTPS destination.

For example:

curl -X POST https://api.kit.com/fhir/R4/Subscription \
-H "Authorization: Bearer $MY_ACCESS_TOKEN" \
-H "Content-Type: application/fhir+json" \
-d '{
"resourceType":"Subscription",
"criteria":"DiagnosticReport?status=completed",
"channel":{
"type": "rest-hook",
"endpoint": "https://example.com/webhook",
"header": ["Authorization: Bearer secret-token-abc-123"]
}
}'

Webhooks can optionally use a FHIR extension to enable an HMAC signature. To enable HMAC signatures, use the extension “url” or “https://www.medplum.com/fhir/StructureDefinition-subscriptionSecret” and “valueString” of a cryptographically secure secret.

{
"resourceType": "Subscription",
"criteria": "DiagnosticReport?status=completed",
"status": "active",
"channel": {
"type": "rest-hook",
"endpoint": "https://example.com/webhook"
},
"extension": [
{
"url": "https://www.medplum.com/fhir/StructureDefinition-subscriptionSecret",
"valueString": "abc"
}
]
}

The valueString will be used to generate a signature. The signature is a timestamp followed by a Hashed Message encoded using SHA-256 (otherwise known as an HMAC). The timestamp is included to ensure a match between when the payload was generated and when it was encoded. This will help prevent bad actors from tampering with the message. The signature is encoded by concatenating the time_stamp with the body payload using a period . to separate the two. The signature is the payload encoded using SHA-256 (otherwise known as an HMAC). The key for the hash will be the valueString from the FHIR extension. API consumers are encouraged to encode the payload with the secret key and compare the signatures.

Example: TypeScript / Express

app.post("/webhook", (req, res) => {
const secret = "..."; // Created separately
const signature = crypto
.createHmac("sha256", secret)
.update(JSON.stringify(req.body))
.digest("hex");
console.log("Signature:", req.headers["x-signature"]);
console.log("Expected:", signature);
console.log("Received:", req.body);
res.sendStatus(200);
});

Example: Python / Flask

@app.route("/webhook", methods=["POST"])
def handle_webhook():
secret = b'...' # Created separately
message = flask.request.get_data()
signature = hmac.new(secret, message, hashlib.sha256).hexdigest()
log('Expected: ' + signature)
log('Received: ' + flask.request.headers.get('x-signature'))
return {"ok":True}

All successful post backs should return a 2XX response. Kit should assume failure in the event of any HTTP response that is not a 2XX. In this case, Kit should attempt to retry the call back using exponential backoff (e.g., retry after 1s, 2s, 4s, 8s, 16s, 32s, etc). This should be capped at 3 days, at which point the event should be assumed to have failed.

For more information about FHIR Subscriptions: