Set up Webhooks

Webhooks allow the NexHealth API to send messages to you when certain events occur.

Registering an Endpoint

The first step to begin consuming NexHealth webhooks is to register a URL with us. Do this by making a POST request to /webhook_endpoints

curl --request POST \
     --url 'https://nexhealth.info/webhook_endpoints' \
     --header 'Accept: application/vnd.Nexhealth+json; version=2' \
     --header 'Content-Type: application/json' \
     --data '
{
     "target_url": "https://yoururl.com/hooks"
}
'

Since our webhook messages may contain PHI, you must register a valid URL using HTTPS.

Upon successful registration, we will respond with the id of the new registered endpoint. Since your server will be listening on a public endpoint, the registered endpoint includes a secret key that will allow you to cryptographically verify the authenticity of the messages you receive.

Creating a Subscription

Once your server is listening to HTTPS requests on the registered route, you need to define what kind of messages you are interested in. This is called subscribing. Subscriptions are always defined as a dependent sub-resource to the endpoint that you registered previously. See the request below as an example of how to create your first subscription.

curl --request POST \
     --url 'https://nexhealth.info/webhook_endpoints/id/webhook_subscriptions?subdomain=test' \
     --header 'Accept: application/vnd.Nexhealth+json; version=2' \
     --header 'Content-Type: application/json' \
     --data '
{
     "resource_type": "appointment",
     "event": "appointment_insertion"
}
'

Notice, you are required to provide a subdomain to scope which institution's events you are subscribing to. The other required fields are the resource_type and the event. See the WebhookSubscriptions resource page for more details on these parameters.

Event Messages

Now that you have registered a URL and created a subscription with that URL, we will begin sending you messages anytime an event you subscribed to occurs within the subscribed subdomain. Hooks are sent via POST requests to the registered URL with the following headers:

Headervalue
timestampISO8601 Timestamp of sending the message
signatureHMAC signature using SHA256 to encode the message with the endpoint secret_key
content-typeapplication/json

The JSON payload is structured in the following manner using a completed appointment insertion as an example:

{
  "resource_type": "appointment",
  "event_name": "appointment_insertion.complete",
  "event_time": "2021-12-07T05:47:21.214+00:00",
  "data": {
    "appointment": {
      "id": 1136829,
      "patient_id": 55476535,
      "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": "Caleb Orozco has some concerns they want to discuss about their condition",
      "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": "Caleb Orozco",
      "provider_name": "Dr. Crawford",
      "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
    }
  ]
}

Checking the webhook signature

NexHealth will always sign webhook events sent to your configured endpoints by including a signature in the signature header, as well as a timestamp in the timestamp header.

In order to verify the signature of the webhook request, you'll need to retrieve the webhook secret_key given to you when you first created your webhook endpoint. If you don't have this, you'll need to recreate your webhook endpoint.

Step 1: Encode the payload for use in the signed_payload string

  • Using any library of your choice, base64 encode the payload body that was sent to you in the webhook event

Step 2: Recreate the signed_payload string by concatenating

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

Step 3: Determine the expected_signature

  • Compute an HMAC with the SHA256 hash function using the webhook endpoint's secret_key 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 to the signature header

Examples

import json
import hashlib
import hmac
import base64

def generate_signature(payload: dict, timestamp: str, secret_key: str):
    # Separators are used to remove whitespaces
    payload_str = json.dumps(payload, separators=(',', ':'))
    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()

📘

Webhook Retries

If your application does not respond with a 2xx status code we will retry using the following schedule. If we continue to receive errors for 48 hours we will update the webhook endpoint "active" field to false and stop sending webhooks.

Retry #Total Time
130 sec
21.5 min
33.5 min
410 min
530 min
62 hours
75 hours
810 hours
924 hours
1048 hours