Setting up webhooks
Register an endpoint, subscribe to events and check the signature on each message.
Webhooks let NexHealth send your app a message when an event happens.
The samples use the values from the Quickstart: NEX_TOKEN and NEX_SUBDOMAIN. Node samples need Node 18 or later, saved as .mjs files. Python samples need the requests package.
Registering an endpoint
First, register a URL. Send a POST request to /webhook_endpoints.
curl -s --request POST \
--url "https://nexhealth.info/webhook_endpoints" \
--header "Nex-Api-Version: v3.0.0" \
--header "Authorization: Bearer $NEX_TOKEN" \
--header "Content-Type: application/json" \
--data '{ "target_url": "https://your-app.example.com/hooks" }'const { NEX_TOKEN } = process.env;
const url = "https://nexhealth.info/webhook_endpoints";
const res = await fetch(url, {
method: "POST",
headers: {
"Nex-Api-Version": "v3.0.0",
Authorization: `Bearer ${NEX_TOKEN}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
target_url: "https://your-app.example.com/hooks",
}),
});
console.log(await res.json());import os
import requests
res = requests.post(
"https://nexhealth.info/webhook_endpoints",
headers={
"Nex-Api-Version": "v3.0.0",
"Authorization": "Bearer " + os.environ["NEX_TOKEN"],
},
json={
"target_url": "https://your-app.example.com/hooks",
},
)
print(res.json())Webhook messages may contain PHI, so you must register a valid HTTPS URL.
The response holds the ID of the new endpoint. Save it as NEX_WEBHOOK_ID. The next request uses it. Your server listens on a public endpoint, so the registered endpoint also comes with a secret key. Use it to verify that each message you receive is authentic.
Creating a subscription
Once your server accepts HTTPS requests on the registered route, choose which messages you want. This is called subscribing. A subscription always belongs to an endpoint you registered. This request creates your first subscription.
curl -s --request POST \
--url "https://nexhealth.info/webhook_endpoints/$NEX_WEBHOOK_ID/webhook_subscriptions?subdomain=$NEX_SUBDOMAIN" \
--header "Nex-Api-Version: v3.0.0" \
--header "Authorization: Bearer $NEX_TOKEN" \
--header "Content-Type: application/json" \
--data '{ "resource_type": "Appointment", "event": "appointment_insertion" }'const { NEX_WEBHOOK_ID, NEX_TOKEN, NEX_SUBDOMAIN } = process.env;
const url = `https://nexhealth.info/webhook_endpoints/${NEX_WEBHOOK_ID}/webhook_subscriptions?subdomain=${NEX_SUBDOMAIN}`;
const res = await fetch(url, {
method: "POST",
headers: {
"Nex-Api-Version": "v3.0.0",
Authorization: `Bearer ${NEX_TOKEN}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
resource_type: "Appointment",
event: "appointment_insertion",
}),
});
console.log(await res.json());import os
import requests
res = requests.post(
"https://nexhealth.info/webhook_endpoints/" + os.environ["NEX_WEBHOOK_ID"] + "/webhook_subscriptions",
params={
"subdomain": os.environ["NEX_SUBDOMAIN"],
},
headers={
"Nex-Api-Version": "v3.0.0",
"Authorization": "Bearer " + os.environ["NEX_TOKEN"],
},
json={
"resource_type": "Appointment",
"event": "appointment_insertion",
},
)
print(res.json())The subdomain goes in the query string, as in the examples. It sets which practice's events you subscribe to. Put it in the JSON body instead and the call returns 403. resource_type and event go in the body and must match the names in the Webhook Subscriptions reference exactly, including case: Appointment, not appointment. A wrong value returns 400 Invalid value for parameter resource_type.
Event messages
With a URL registered and a subscription created, NexHealth sends you a message every time a subscribed event happens in the subscribed subdomain. Each webhook is a POST request to the registered URL with these headers.
| Header | Value |
|---|---|
| timestamp | ISO8601 timestamp of when the message was sent |
| signature | HMAC signature using SHA256 to encode the message with the endpoint secret_key |
| content-type | application/json |
The JSON payload looks like this. The example is a completed appointment insertion.
{
"institution_id": 1,
"subdomain": "demo-practice",
"webhook_subscription_id": 2,
"resource_type": "Appointment",
"event_name": "appointment_insertion.complete",
"event_time": "2021-12-07T05:47:21.214Z",
"data": {
"appointment": {
// Fields shown are meant as a non-comprehensive example.
// Reference /reference/webhook-subscriptions for current details
"id": 1136829,
"patient_id": 4102,
"provider_id": 415858,
"start_time": "2021-12-01T09:30:00.000Z",
"confirmed": true,
"patient_missed": false,
"created_at": "2021-12-05T16:30:53.471Z",
"updated_at": "2021-12-05T16:30:53.488Z",
"note": "Patient asked for a morning time",
"end_time": "2021-12-01T09:45:00.000Z",
"unavailable": false,
"cancelled": false,
"timezone": "America/New_York",
"institution_id": 1,
"appointment_type_id": 1112,
"checkin_at": "2021-12-01T09:32:00.000Z",
"foreign_id": null,
"foreign_id_type": "nex",
"location_id": 75,
"last_sync_time": null,
"patient_confirmed": true,
"created_by_user_id": null,
"is_guardian": false,
"insurance_choice_type": null,
"insurance_plan_id": null,
"insurance_carrier_id": null,
"patient_confirmed_at": "2021-12-05T16:30:53.501Z",
"cancelled_at": null,
"is_new_clients_patient": null,
"confirmed_at": null,
"sooner_if_possible": false,
"operatory_id": 3,
"checked_out": false,
"checked_out_at": null,
"referrer": null,
"patient_name": "John Smith",
"provider_name": "Dr. Lee",
"operatory_name": "Room 3",
"is_past": true,
"status": "paid",
"is_past_patient": true,
"insurance_description": null,
"created_at_with_tz": "2021-12-05T11:30:53.471-05:00",
"start_time_with_tz": "2021-12-01T04:30:00.000-05:00",
"end_time_with_tz": "2021-12-01T04:45:00.000-05:00",
"timezone_offset": "-05:00"
}
},
"delivery_errors": [
{
"code": "500",
"timestamp": "2021-12-06T05:53:44.844Z",
"message": "500 Internal Server Error",
"attempts": 1
}
]
}Every event
The Webhook subscriptions reference lists every resource_type and event, with an example payload for each. Event names follow one pattern.
| Event name | When it fires |
|---|---|
*_created, *_updated, *_deleted | When NexHealth reads that change from the health record system. NexHealth reads on-premises systems every 10 to 15 minutes, so events arrive in bursts. |
*_insertion | After you create a record through the API, when the write into the health record system finishes. event_name carries the result: appointment_insertion.complete, appointment_insertion.failed or appointment_insertion.timed_out. The reason for a failure is not in the payload. |
sync_status_read_change, sync_status_write_change | When a sync's reads or writes change state: .red when they go down, .green when they come back. read_status_at and write_status_at on GET /sync_status carry the timestamps. |
appointment_updated after a write you sent | When a cancel, confirm, check-in or reschedule you sent through PATCH /appointments finishes in the health record system. event_name is appointment_updated on success and appointment_updated.failed, appointment_updated.timed_out or appointment_updated.incomplete otherwise, with previous_attributes showing the field you changed. |
Payloads carry no event ID. Each event is sent once. It is sent again only when your endpoint does not answer with a 2xx.
Checking the webhook signature
NexHealth signs every webhook event it sends to your endpoints. The signature is in the signature header and a timestamp is in the timestamp header.
To verify the signature, you need the secret_key you received when you created the webhook endpoint. If you do not have it, create the webhook endpoint again.
Step 1: Encode the payload for the signed_payload string
signed_payload string- Base64 encode the body of the request exactly as you received it. Do not parse and re-serialize it first: a byte of difference changes the signature.
Step 2: Build the signed_payload string
signed_payload stringJoin these three parts in order.
- The timestamp from the
timestampheader - The character
. - The base64 encoded payload from step 1
Step 3: Work out the expected_signature
expected_signature- Compute an HMAC with the SHA256 hash function. Use the
secret_keyof the webhook endpoint as the key and thesigned_payloadstring from step 2 as the message.
Step 4: Compare the signatures
- Compare the
expected_signaturefrom step 3 with thesignatureheader.
Example
import json
import hashlib
import hmac
import base64
def generate_signature(payload: dict, timestamp: str, secret_key: str):
# Separators are used to remove whitespaces;
# ensure_ascii=False will save the string as UTF-8 instead of the escaped sequence
payload_str = json.dumps(payload, separators=(',', ':'), ensure_ascii=False)
b64encoded = base64.b64encode(payload_str.encode())
# When concatenating don't forget to decode everything first
message = f"{timestamp}.{b64encoded.decode()}"
return hmac.new(secret_key.encode(), message.encode(), hashlib.sha256).hexdigest()Retries and failing endpoints
If your app does not answer with a 2xx, NexHealth retries the delivery seven times over about 11 hours, then marks it failed.
| Retry | Wait after the previous attempt | Time since the first attempt |
|---|---|---|
| 1 | 30 sec | 30 sec |
| 2 | 2 min | 2.5 min |
| 3 | 5 min | 7.5 min |
| 4 | 15 min | 22.5 min |
| 5 | 1 hour | 1 hour 23 min |
| 6 | 3 hours | 4 hours 23 min |
| 7 | 7 hours | 11 hours 23 min |
An endpoint that keeps failing is protected in two steps.
- Pause. After more than 10 failed attempts in a row, NexHealth stops sending to the endpoint for 60 minutes. Events due in that hour are marked
skippedand are not sent later. You get a warning email, at most one a day. One successful delivery resets the count. - Deactivation. When a delivery runs out of retries and the last 100 finished deliveries to the endpoint (successful or failed) all failed, NexHealth sets the endpoint's
activefield tofalse, emails you and stops sending. Its subscriptions stay inactive when you set the endpoint back toactive: true. Set each subscription back toactive: trueas well. See Edit webhook subscription.
Updated 17 days ago