Patient Activity Notifications
Get one webhook per patient when new data lands in a Zus record, then pull the changes with an incremental Group $export.
Beta featurePatient activity notifications are designed to work with Bulk FHIR Export, which is in private beta and may change or experience performance issues at any time.
1. Overview
Patient activity subscriptions notify your application when new data is added to a patient record visible to your builder. Instead of sending a webhook for every new resource, Zus sends one notification per patient per configured interval. Use the patient's Universal Patient Identifier (UPID) from the notification to run an incremental Group $export.
Key behavior
- One notification per patient per interval. The interval starts when the first new resource arrives. Additional writes during that interval do not reset the timer.
- Any write counts. New or updated Conditions, Observations, documents, Encounters, medications, and other resources that carry the patient's UPID trigger a notification. Resources without a UPID, such as Practitioners and Organizations, do not.
- No resource filters. Filters are not currently supported for this subscription type.
- No clinical data in the webhook. The payload contains only the patient's
upidandgroupreference. - Not a FHIR Subscription resource. Zus configures and manages patient activity subscriptions separately from FHIR Subscription resources.
Choosing a subscription type
| Subscription type | Fires on | Trade-offs |
|---|---|---|
| Resource-level subscription | A specific resource type, such as Encounter | Can send several notifications for a single ingest. Misses updates that do not include that resource type. |
| Patient activity subscription | Any resource type that carries the patient's UPID | One notification per patient per interval. The webhook carries no resource, so you run $export to get the data. |
Notification flow
- A resource for the patient is written in Zus.
- If no interval is open for that patient and subscription, Zus starts one.
- When the interval ends, Zus sends a POST to your webhook.
- Your application runs
$exportusing the patient's saved_sincebookmark. - After a successful export, your application saves the manifest's
transactionTimeas the patient's next_sincebookmark. See Sync with $export.
2. Prerequisites
| Item | What is needed | Notes |
|---|---|---|
| Zus builder | A Zus builder in sandbox and production | The subscription is scoped to the builder specified during setup. |
| API credentials | A machine-to-machine app client (client ID and secret) | Tokens last 1 hour. Token requests are limited to 200 per hour per app client. See Creating App Clients. |
| Webhook endpoint | A public HTTPS URL that accepts POST with a JSON body | HTTP is rejected at subscription time. Return any 2xx within 120 seconds. |
| Webhook credentials | Basic auth (username and password) or OAuth 2.0 client credentials (token URL, client ID, client secret) | Unauthenticated endpoints are not allowed in sandbox or production. |
| Export pipeline | A job that runs $export for a patient group with _since and stores a per-patient bookmark | Notifications identify patients. Use $export to retrieve their data. |
| Patient visibility | Patients must be visible to the builder the subscription is scoped to | Alerts fire for any patient the builder can see, including patients it did not create. |
Hosts
| Environment | Export host | Token endpoint |
|---|---|---|
| Sandbox | https://fqs.sandbox.zusapi.com | https://auth.sandbox.zusapi.com/oauth/token |
| Production | https://fqs.zusapi.com | https://auth.zusapi.com/oauth/token |
Use your app client's credentials to get an access token from the token endpoint, and send it as Authorization: Bearer <token> on $export requests. You do not need access to the subscriptions API.
3. Request a subscription
Zus sets up subscriptions on request. Contact your Zus account manager and provide the following for each environment. Zus confirms when the subscription is active.
| Field | What to provide | Notes |
|---|---|---|
| Subscription name | A short name, such as patient-export-sync | Use the same name when requesting a change or removal. |
| Notification interval | A duration, such as 30m or 1h | Minimum 30 minutes. Notifications are grouped per patient. |
suppressOwnWrites | true or false | When true, writes made by your own builder do not trigger notifications. |
| Webhook URL | A public HTTPS endpoint for each environment | Zus sends POST notifications to this address. |
| Authentication type | Basic or OAuth 2.0 client credentials | Zus authenticates to your webhook with the chosen method. |
| Webhook credentials | Basic: username and password. OAuth 2.0: token URL, client ID, client secret, plus audience and scopes if needed | Share these separately from the setup request. |
| Environment and builder | Sandbox or production, and the relevant builder | Request and test sandbox first, then coordinate production. |
Share webhook passwords and OAuth client secrets with Zus over an approved secure channel. Never send them in plain-text email.
4. Receive and verify notifications
Zus sends a POST to your webhook for each patient with activity during the interval.
POST https://webhooks.example.com/zus/patient-activity
Content-Type: application/json
Authorization: Basic <base64(username:password)>
{
"upid": "c4b1d2e9-6bb4-4df5-b5c2-7b4761ace72e",
"group": "Group/c4b1d2e9-6bb4-4df5-b5c2-7b4761ace72e"
}With OAuth 2.0, the header is Authorization: Bearer <token>. The body is the same for both methods.
| Field | Always present | Meaning |
|---|---|---|
upid | Yes | The patient's Universal Patient Identifier. Every Zus resource for this patient carries this UPID. |
group | Yes | The same patient written as Group/<upid>, the identifier $export takes. |
Ignore any unrecognized fields so your handler stays compatible with future additions.
Verify that the call came from Zus
Notifications are not signed. Verify the Authorization header on every request.
| Auth type | What arrives | What to verify |
|---|---|---|
| Basic | Authorization: Basic <base64(username:password)> | Compare the decoded username and password to the pair you registered, using a constant-time comparison. |
| OAuth 2.0 | Authorization: Bearer <token> issued by your token URL | Validate the token as you would any client credentials token from your identity provider: signature, issuer, expiry, and the audience or scopes you asked Zus to request. |
Return 401 for invalid credentials. Zus retries a 401 like any other non-2xx response.
Treat webhook input as untrusted. Validate that upid is a UUID before using it in an export URL, and authenticate the request before scheduling any work.
Timing
- The alert is sent within about a minute of the interval closing. The interval starts at the first new resource for the patient, not the last.
- There is no alert for a patient with no new data.
Rotate webhook credentials
Contact Zus and provide the replacement credentials over a secure channel. Zus confirms when the new credentials are active. Until then, and for a short time afterward, accept both the old and new credentials so in-flight and retried requests can complete.
5. Process notifications and sync with $export
Recommended handler
- Verify the
Authorizationheader. Return401on failure. - Parse the body and validate that
upidis a UUID. Return400on failure. - Enqueue an export for
upid. If an export for that UPID is already queued or running, do not schedule a duplicate. - Return
200or202once the work is queued, well within the 120 second timeout. Zus ignores the response body.
Do not wait for $export to finish before responding.
Delivery semantics
| Behavior | What Zus does | What your application should do |
|---|---|---|
| Duplicate alerts | At-least-once delivery. If a delivery times out or returns an error, even after your endpoint processed it, the same UPID can be sent again later. | Deduplicate queued jobs by UPID. |
| Ordering | No ordering across patients. One patient's alerts are at least one interval apart. | Do not infer sequence from arrival order. |
| New data during delivery | If more data lands while an alert is in flight, a fresh window starts and another alert follows after the next interval. | Nothing. The next export picks it up. |
| Failed delivery (non-2xx, timeout, connection error) | Retries with an increasing backoff over several hours. | Monitor and fix webhook errors. Retries stop after a successful delivery. |
| Repeated failures | After repeated failed attempts, Zus pauses notifications for that patient on that subscription until the issue is resolved. | Contact Zus support with the subscription name, and run a full export for patients whose notifications may have been paused. |
Sync with $export
For each notification, run POST /Group/{upid}/$export?_since=<bookmark> against the export host. See Bulk FHIR Export for parameters, polling, and downloading.
- Keep a per-patient
_sincebookmark, independent of notifications. The notification has no change timestamp, and a notification can be delayed or retried. - Update the bookmark only after a successful export.
- Save the manifest's
transactionTimeas the next bookmark, and send it as_sinceon the patient's next export. Do not use the time you started the export job. - Resources written in the 15 minutes before an export request are always included, so consecutive incremental exports overlap. Upsert by resource ID.
- On
409 Conflict, an export is already running for this patient and builder. Continue with the job in theContent-Locationheader.
6. What exports will not show
A notification can arrive for a change that the export does not contain:
- Restricted-access data. A resource with a specially regulated security label triggers a notification but is withheld from the export.
- Binary resources. The export never emits Binary resources. Document content is delivered through DocumentReference.
A notification with no new export data can also be a duplicate that arrived after an earlier export already picked up the change.
7. Change or remove a subscription
Contact your Zus account manager with the environment, builder, subscription name, and the requested change. Share any new webhook credentials over an approved secure channel.
- Interval changes apply to notification windows opened after the update. Windows already in progress keep the previous interval.
suppressOwnWriteschanges apply to subsequent writes.- Webhook URL and credential changes take effect shortly after Zus applies them, including for retries. Zus confirms when the change is active.
- Removal. Zus confirms when the subscription is removed. Notifications that were already pending may still arrive for a short time afterward.
8. Test in sandbox and go live
Sandbox test procedure
- Get a sandbox token with your sandbox app client. The audience is
https://api.sandbox.zusapi.com. - Send Zus the sandbox configuration from Request a subscription, with an interval of
30mand your sandbox webhook URL. Wait for Zus to confirm the subscription is active. - Pick a sandbox patient visible to your builder and note its UPID.
- Write a resource for the patient through the sandbox FHIR API at
https://api.sandbox.zusapi.com/fhir, for examplePOST /fhir/Observation. IfsuppressOwnWritesistrue, your own writes will not trigger a notification. Coordinate with Zus to use a test configuration with that setting disabled, or ask Zus to load test data. - Allow about 31 minutes for the first notification. Write a second resource during the same interval and confirm you receive only one notification for the patient.
- Run
$exportagainsthttps://fqs.sandbox.zusapi.comwith_sinceset to a time before your writes, and confirm the new resources are in the output. - Return a
500from your endpoint for one alert, and confirm that Zus retries the delivery.
If you need a connectivity or authentication check first, ask Zus to help validate webhook delivery. Test your payload parser with a real patient activity notification, since a generic test payload may have a different format.
Go-live checklist
- Production app client created and token flow working against
https://auth.zusapi.com/oauth/token - Production webhook reachable over HTTPS and accepting the credentials shared with Zus
- Handler verifies the
Authorizationheader and validates the UPID - Handler returns 2xx within a few seconds and queues the export asynchronously
- Export jobs coalesce by UPID, honor
Retry-After, handle409, and use per-patient bookmarks - Alerting on a sustained rate of non-2xx responses, so an outage is fixed before notifications are paused
- Initial full export and per-patient bookmarks ready before Zus enables production notifications
- Zus has confirmed the production subscription is active with the requested interval and
suppressOwnWritessetting
9. Limits and FAQ
| Limit | Value |
|---|---|
| Minimum interval | 30 minutes |
| Webhook response timeout | 120 seconds |
| Delivery retries | Increasing backoff over several hours, then notifications pause |
| Alert latency after the window closes | About 1 minute |
| Token requests | 200 per hour per app client. Tokens last 1 hour. See Rate Limits. |
| Exports in flight per patient per builder | 1 |
| Export output retention | 7 days. Download links are valid for 15 minutes. |
Why did I get a notification but no new export data?
The change may have come from your own builder with suppressOwnWrites set to false. The resource may carry a specially regulated security label, which triggers a notification but is withheld from the export. Or the notification may be a duplicate that arrived after an export already processed the change.
Why didn't a patient with new data trigger a notification?
Check whether the patient is visible to your builder, whether suppressOwnWrites excluded the write, and whether the interval has ended. Repeated webhook failures can also pause a patient's notifications. Review your webhook logs and ask Zus to investigate if needed.
Can the webhook include the resource instead of requiring an export?
Not with patient activity subscriptions. For resource-level notifications, use a filtered resource subscription. Those send a notification per resource rather than once per patient per interval.
When does the interval start?
When the first qualifying resource is written. Additional resources during the interval do not extend it.
What happens when patient records are merged or split?
Notifications do not currently identify merges or splits, or map between UPIDs. After a merge, the resulting patient record may receive later notifications under its own UPID. A dedicated merge and split signal is on the roadmap.
Is this a FHIR Subscription resource?
No. Zus configures and manages patient activity subscriptions separately from FHIR Subscription resources. The webhook identifies the patient as Group/<upid> for use with $export.
Support
Contact your Zus account manager or [email protected] with the environment, subscription name, UPID, approximate notification time, and any relevant webhook response codes or error messages. Zus can investigate delivery attempts and resume paused notifications.
Related pages
Updated 30 minutes ago