A throwaway endpoint for FHIR rest-hook notifications
Point a FHIR Subscription at the URL you get below, and every notification your server sends is captured in full, validated against R4B/R5, and shown live.
Leave blank for a random id. Letters, digits, and - . _ ~ only; up to 64 characters.
Nothing is authenticated and data is kept in memory only: anyone who knows an endpoint's id can read everything sent to it, and a chosen id is only as private as it is hard to guess. A debugging tool, not a destination for real patient data.
Testing a rest-hook subscription normally means standing up a public server just to see what your FHIR server is actually sending. Notifyr replaces that with a URL you create in one click.
Every request that arrives is:
- captured in full — the raw body exactly as sent, even when it is malformed;
- validated — JSON, then FHIR structure and value sets, with every problem located and explained;
- counted by type — handshakes, heartbeats and event-notifications tallied separately, so you can confirm each part of the subscription lifecycle actually fired.
How to test with fhir-candle
A five-step walkthrough against a real FHIR server, with the Subscription and Encounter to paste.
fhir-candle is a small in-memory FHIR server with a built-in Subscriptions reference implementation, which makes it the quickest way to see real notifications land here.
No interest in running anything locally? subscriptions.argo.run hosts the same fhir-candle software, with SubscriptionTopic/encounter-complete already loaded at /fhir/r4b — skip straight to step 2 below with that base URL. It's shared, third-party infrastructure Notifyr doesn't control, so the local walkthrough is the one that's always reproducible and gives you server-side logs when something doesn't arrive.
Create an endpoint first — the sample in step 2 then arrives pre-filled with its own URL, on its dashboard.
- 1
Start fhir-candle with the subscriptions reference implementation
fhir-candle --reference-implementation subscriptions -o
It serves R4B at
http://localhost:5826/fhir/r4band-oopens its UI in a browser. - 2
Create a Subscription pointing at your webhook URL
PUT http://localhost:5826/fhir/r4b/Subscription/notifyr-test{ "resourceType": "Subscription", "id": "notifyr-test", "status": "active", "end": "2027-01-01T00:00:00Z", "reason": "Testing rest-hook notifications with Notifyr", "criteria": "http://example.org/FHIR/SubscriptionTopic/encounter-complete", "_criteria": { "extension": [ { "url": "http://hl7.org/fhir/uv/subscriptions-backport/StructureDefinition/backport-filter-criteria", "valueString": "Encounter?patient=Patient/example" } ] }, "channel": { "extension": [ { "url": "http://hl7.org/fhir/uv/subscriptions-backport/StructureDefinition/backport-heartbeat-period", "valueInteger": 120 } ], "type": "rest-hook", "endpoint": "https://your-notifyr-host/hook/your-hook-id", "payload": "application/fhir+json", "_payload": { "extension": [ { "url": "http://hl7.org/fhir/uv/subscriptions-backport/StructureDefinition/backport-payload-content", "valueCode": "id-only" } ] } } }Replace
channel.endpointwith your own hook URL. The heartbeat extension is set to 120 seconds, so a heartbeat arrives every two minutes. Keependin the future or the subscription expires. - 3
Watch the handshake arrive
As soon as the Subscription becomes active, fhir-candle sends a handshake. It should appear here within a couple of seconds, and the Handshake counter should read 1. If it does not, the server could not reach your URL.
- 4
Create an Encounter with status active
PUT http://localhost:5826/fhir/r4b/Encounter/example{ "resourceType": "Encounter", "id": "example", "text": { "status": "generated", "div": "<div xmlns=\"http://www.w3.org/1999/xhtml\">Encounter with patient @example</div>" }, "status": "active", "class": { "system": "http://terminology.hl7.org/CodeSystem/v3-ActCode", "code": "IMP", "display": "inpatient encounter" }, "subject": { "reference": "Patient/example" } }Nothing is notified yet — the topic fires on completion, not creation.
- 5
Update the Encounter to status finished
PUT the same Encounter again with
"status": "finished". That satisfies theencounter-completetopic and triggers an event-notification.
You should end up with a handshake, an event-notification, and a heartbeat every two minutes for as long as the subscription stays active. Click any row to see its raw body and validation results.
How Notifyr validates
The three validation tiers, and why Subscription notifications need handling of their own.
Every request runs through three tiers. Each one gates the next, so a message is only judged on rules it could plausibly satisfy.
- 1. Does the body parse as JSON?
If not, the message is invalid and answered
400. The raw body is still stored exactly as sent — an unparseable payload is usually the one you most need to look at. - 2. Is it a JSON object with a
resourceType?An array, a scalar, or an object without
resourceTypecannot be a FHIR resource. Invalid, answered422. - 3. Does it pass FHIR structural and value-set validation?
Run through the
fhirpackage against R4. Unknown elements, missing required properties and codes outside their value set are reported with the exact path, e.g.Patient.gender. Invalid, answered422.
Subscription notifications
These need special handling. The fhir package ships R4 conformance only, and SubscriptionStatus was introduced in R4B — so left alone the validator rejects the resource type outright and marks every handshake and heartbeat invalid.
Notifyr therefore validates SubscriptionStatus entries itself against the R4B/R5 definition, and hands the rest of the Bundle to the library untouched, so a broken sibling resource is still caught.
- Errors (make it invalid):
typepresent and one of the five notification types;subscriptionpresent with a reference;statusa known code;topica string. - Warnings (shown, still valid):
Bundle.typeother thanhistory; a missingBundle.timestamp;SubscriptionStatusnot first; aquery-statuscarrying event information, which the spec prohibits. - A handshake or heartbeat may carry
notificationEvententries and is not flagged for it — R4B marks the element "Special" for those types, so a server catching a client up after a reconnect is behaving correctly.
eventsSinceSubscriptionStart is accepted both as a string (R4B) and as a number (R5).
Warnings never make a message invalid, and only valid notifications are counted by type — a Bundle that failed validation has not established what kind of notification it was.
References
The specification pages every rule here is checked against.
Specification
- Subscription (R4B)
The resource itself, plus the Channels section that defines rest-hook delivery and endpoint validation.
- SubscriptionStatus (R4B)
What every notification carries. Defines type (handshake, heartbeat, event-notification, query-status, query-event) as 1..1, and eventsSinceSubscriptionStart as a string.
- SubscriptionTopic (R4B)
Describes which events trigger a notification and what the payload should contain.
- Topic-Based Subscriptions Framework (R5)
The full framework, including the notification Bundle shape. R5 types eventsSinceSubscriptionStart as an integer64 rather than a string.
- Subscriptions R5 Backport IG
How R4 servers implement the R5 model, and the source of the backport-heartbeat-period, backport-filter-criteria and backport-payload-content extensions.
- Notification payloads (Backport IG)
What empty, id-only and full-resource each require a notification Bundle to carry.
Tooling
- fhir-candle
In-memory FHIR server with a subscriptions reference implementation. Used in the walkthrough above.
- fhir (npm)
The R4 validator Notifyr runs your payloads through.