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.

HeaderValue
timestampISO8601 timestamp of when the message was sent
signatureHMAC signature using SHA256 to encode the message with the endpoint secret_key
content-typeapplication/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 nameWhen it fires
*_created, *_updated, *_deletedWhen NexHealth reads that change from the health record system. NexHealth reads on-premises systems every 10 to 15 minutes, so events arrive in bursts.
*_insertionAfter 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_changeWhen 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 sentWhen 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

  • 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

Join these three parts in order.

  • The timestamp from the timestamp header
  • The character .
  • The base64 encoded payload from step 1

Step 3: Work out the expected_signature

  • Compute an HMAC with the SHA256 hash function. Use the secret_key of the webhook endpoint as the key and the signed_payload string from step 2 as the message.

Step 4: Compare the signatures

  • Compare the expected_signature from step 3 with the signature header.

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.

RetryWait after the previous attemptTime since the first attempt
130 sec30 sec
22 min2.5 min
35 min7.5 min
415 min22.5 min
51 hour1 hour 23 min
63 hours4 hours 23 min
77 hours11 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 skipped and 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 active field to false, emails you and stops sending. Its subscriptions stay inactive when you set the endpoint back to active: true. Set each subscription back to active: true as well. See Edit webhook subscription.

What’s Next

Keep your own copy of the data current.

Did this page help you?
ChangelogMCP serverErrorsdevelopers@nexhealth.com