Data pipelines configuration guide

Plan a full load, run it across locations, and keep every resource current without hitting the limits.

This guide covers the choices behind a data pipeline. Data pipelines quickstart shows the basic load and the two ways to pick up changes.

The samples use the values from the Quickstart: NEX_TOKEN, NEX_SUBDOMAIN and NEX_LOCATION_ID, plus NEX_WEBHOOK_ID from Setting up webhooks. Node samples need Node 18 or later, saved as .mjs files. Python samples need the requests package.

1. Map the locations

Your key reaches one or more institutions. Each institution has a subdomain and one or more locations. Every list call after this one takes subdomain and location_id, so start by listing what you can reach. GET /locations returns each institution with its locations. Leave subdomain off to see every institution your key can reach.

curl -s --request GET \
  --url "https://nexhealth.info/locations?subdomain=$NEX_SUBDOMAIN&inactive=false" \
  --header "Nex-Api-Version: v3.0.0" \
  --header "accept: application/vnd.Nexhealth+json;version=2" \
  --header "Authorization: Bearer $NEX_TOKEN"
const { NEX_TOKEN, NEX_SUBDOMAIN } = process.env;

const url = `https://nexhealth.info/locations?subdomain=${NEX_SUBDOMAIN}&inactive=false`;

const res = await fetch(url, {
  headers: {
    "Nex-Api-Version": "v3.0.0",
    accept: "application/vnd.Nexhealth+json;version=2",
    Authorization: `Bearer ${NEX_TOKEN}`,
  },
});
console.log(await res.json());
import os

import requests

res = requests.get(
    "https://nexhealth.info/locations",
    params={
        "subdomain": os.environ["NEX_SUBDOMAIN"],
        "inactive": "false",
    },
    headers={
        "Nex-Api-Version": "v3.0.0",
        "accept": "application/vnd.Nexhealth+json;version=2",
        "Authorization": "Bearer " + os.environ["NEX_TOKEN"],
    },
)
print(res.json())

Two fields on each location shape the rest of the pipeline.

  • tz is the practice's time zone. Appointment windows and updated_since values are UTC. Convert at the edges.
  • last_sync_time is the last time the location was refreshed from the health record system.

Several locations can share one health record system server. Patients then belong to the server, not to one location, which is why GET /patients searches across locations unless you send location_strict=true.

2. Load in the right order

Load resources in the order their IDs are needed, so every record you store can be joined.

OrderResourceEndpointWhat the load needs
1Providers, operatoriesGET /providers, GET /operatorieslocation_id.
2Appointment typesGET /appointment_typessubdomain. A type belongs to the institution or to one location, and parent_type says which. Send location_id when types are location specific.
3PatientsGET /patientslocation_strict=true when the server is shared, so you load each location's own patients.
4AppointmentsGET /appointmentsA start and end. Load one date range at a time.
5Charges, payments, adjustmentsGET /charges, GET /payments, GET /adjustmentsAt least one filter. Use updated_since with a date before the practice went live. Financial data has to be turned on for the key.
6Balances, claims, insuranceGET /guarantor_balances, GET /claims, GET /insurance_coveragesupdated_since for balances.

Ask for 1,000 records a page with per_page, follow end_cursor until has_next_page is false, and store the id and updated_at of every record. Both are needed later.

Appointments need a window. GET /appointments returns appointments starting between start and end. Load one date range at a time, for example one month.

curl -s --request GET \
  --url "https://nexhealth.info/appointments?subdomain=$NEX_SUBDOMAIN&location_id=$NEX_LOCATION_ID&start=2026-12-01T00:00:00Z&end=2026-12-31T23:59:59Z&per_page=1000" \
  --header "Nex-Api-Version: v3.0.0" \
  --header "Authorization: Bearer $NEX_TOKEN"
const { NEX_TOKEN, NEX_SUBDOMAIN, NEX_LOCATION_ID } = process.env;

const url = `https://nexhealth.info/appointments?subdomain=${NEX_SUBDOMAIN}&location_id=${NEX_LOCATION_ID}&start=2026-12-01T00:00:00Z&end=2026-12-31T23:59:59Z&per_page=1000`;

const res = await fetch(url, {
  headers: {
    "Nex-Api-Version": "v3.0.0",
    Authorization: `Bearer ${NEX_TOKEN}`,
  },
});
console.log(await res.json());
import os

import requests

res = requests.get(
    "https://nexhealth.info/appointments",
    params={
        "subdomain": os.environ["NEX_SUBDOMAIN"],
        "location_id": os.environ["NEX_LOCATION_ID"],
        "start": "2026-12-01T00:00:00Z",
        "end": "2026-12-31T23:59:59Z",
        "per_page": 1000,
    },
    headers={
        "Nex-Api-Version": "v3.0.0",
        "Authorization": "Bearer " + os.environ["NEX_TOKEN"],
    },
)
print(res.json())

3. Pick up changes

Webhooks are the better default. Subscribe to the created, updated and deleted events for each resource you keep, one subscription per event per institution. Appointments, patients and the ledger resources all have them. Charges, claims and payments also have deleted events.

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": "Charge", "event": "charge_updated" }'
const { NEX_TOKEN, NEX_SUBDOMAIN, NEX_WEBHOOK_ID } = 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: "Charge",
    event: "charge_updated",
  }),
});
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": "Charge",
        "event": "charge_updated",
    },
)
print(res.json())

Three rules make webhooks reliable.

  • Answer with a 2xx right away and do the work after. NexHealth sends an event again only when your endpoint does not answer with a 2xx, and deactivates an endpoint that keeps failing.
  • Make the handler idempotent. Payloads carry no event ID. Store the record's id and updated_at, and skip an event you have already applied.
  • Expect bursts. NexHealth reads on-premises systems every 10 to 15 minutes, so events arrive in groups.

If you poll instead, ask each list endpoint for records changed since your last run. updated_since returns records updated at or after the time you send, so overlap by a few minutes rather than cutting it exact, and sort by updated_at. Add include_deleted=true on charges, payments and adjustments so your copy drops records the office removed.

curl -s --request GET \
  --url "https://nexhealth.info/charges?subdomain=$NEX_SUBDOMAIN&location_id=$NEX_LOCATION_ID&updated_since=2026-12-01T00:00:00Z&include_deleted=true&sort=updated_at&per_page=1000" \
  --header "Nex-Api-Version: v3.0.0" \
  --header "Authorization: Bearer $NEX_TOKEN"
const { NEX_TOKEN, NEX_SUBDOMAIN, NEX_LOCATION_ID } = process.env;

const url = `https://nexhealth.info/charges?subdomain=${NEX_SUBDOMAIN}&location_id=${NEX_LOCATION_ID}&updated_since=2026-12-01T00:00:00Z&include_deleted=true&sort=updated_at&per_page=1000`;

const res = await fetch(url, {
  headers: {
    "Nex-Api-Version": "v3.0.0",
    Authorization: `Bearer ${NEX_TOKEN}`,
  },
});
console.log(await res.json());
import os

import requests

res = requests.get(
    "https://nexhealth.info/charges",
    params={
        "subdomain": os.environ["NEX_SUBDOMAIN"],
        "location_id": os.environ["NEX_LOCATION_ID"],
        "updated_since": "2026-12-01T00:00:00Z",
        "include_deleted": "true",
        "sort": "updated_at",
        "per_page": 1000,
    },
    headers={
        "Nex-Api-Version": "v3.0.0",
        "Authorization": "Bearer " + os.environ["NEX_TOKEN"],
    },
)
print(res.json())

4. Handle deletes and merges

  • Charges, payments and adjustments come back with deleted_at set when you send include_deleted=true. The *_deleted webhooks fire for them too.
  • There is no appointment_deleted event. Store cancelled and cancelled_at from appointment_updated.
  • A patient created through the API is a placeholder until the office makes them a patient of record. When NexHealth reads the real record, it links the two and deletes the placeholder. Subscribe to patient_created and patient_updated so your copy follows.

5. Stay inside the limits

  • Each API key is limited to 1,000 requests a minute on the patients and appointments endpoints and 2,000 a minute on the others. Past the limit you get 429 with You've exceeded our rate limit. Please wait and retry. The limit covers every location the key reaches, so a parallel load across locations hits it first.
  • Spread calls over 10 to 60 seconds and retry with backoff. Large pages keep the count low.
  • Cache appointment types, providers and operatories instead of reading them on every webhook.
  • Keep updated_since windows short on large reads and sort by updated_at. A read that takes too long answers with Gateway Time-out.

See Rate limiting.

6. Know how fresh the data is

Your copy is only as fresh as NexHealth's last read. GET /sync_status returns read_status and write_status per sync, each green or red, with read_status_at and write_status_at.

  • A gap of up to 20 minutes during office hours is normal. Overnight and on weekends the gap is longer.
  • sync_status_read_change fires when reads come back from red to green. It does not fire on the way down, so poll read_status_at to catch an outage.
  • On an on-premises system a long gap usually means the office server is off or the NexHealthSync service has stopped.

See Interpreting sync status.

When something goes wrong

See Troubleshooting data pipelines. Every message the API returns is in the Errors reference.


Did this page help you?