Data pipelines quickstart
Load a practice's data once, then keep your copy current.
Some apps never book or post anything. They copy practice data into their own systems and keep it current.
The samples use the values from the Quickstart: NEX_TOKEN, NEX_SUBDOMAIN and NEX_LOCATION_ID. Node samples need Node 18 or later, saved as .mjs files. Python samples need the requests package.
1. Load everything once
List endpoints return records in pages. Ask for up to 1,000 records with per_page. Each response has a page_info object. Pass its end_cursor on the next request and stop when has_next_page is false.
CURSOR=""
while true; do
curl -s --request GET \
--url "https://nexhealth.info/patients?subdomain=$NEX_SUBDOMAIN&location_id=$NEX_LOCATION_ID&per_page=1000&end_cursor=$CURSOR" \
--header "Nex-Api-Version: v3.0.0" \
--header "Authorization: Bearer $NEX_TOKEN" > page.json
jq -c '.data[]' page.json >> patients.jsonl
[ "$(jq -r '.page_info.has_next_page' page.json)" = "true" ] || break
CURSOR=$(jq -r '.page_info.end_cursor' page.json)
doneimport { appendFileSync } from "node:fs";
const { NEX_TOKEN, NEX_SUBDOMAIN, NEX_LOCATION_ID } = process.env;
let cursor = "";
while (true) {
const url = `https://nexhealth.info/patients?subdomain=${NEX_SUBDOMAIN}&location_id=${NEX_LOCATION_ID}&per_page=1000&end_cursor=${cursor}`;
const res = await fetch(url, {
headers: {
"Nex-Api-Version": "v3.0.0",
Authorization: `Bearer ${NEX_TOKEN}`,
},
});
const page = await res.json();
for (const patient of page.data) {
appendFileSync("patients.jsonl", JSON.stringify(patient) + "\n");
}
if (!page.page_info.has_next_page) break;
cursor = page.page_info.end_cursor;
}import json
import os
import requests
cursor = ""
while True:
res = requests.get(
"https://nexhealth.info/patients",
params={
"subdomain": os.environ["NEX_SUBDOMAIN"],
"location_id": os.environ["NEX_LOCATION_ID"],
"per_page": 1000,
"end_cursor": cursor,
},
headers={
"Nex-Api-Version": "v3.0.0",
"Authorization": "Bearer " + os.environ["NEX_TOKEN"],
},
)
page = res.json()
with open("patients.jsonl", "a") as out:
for patient in page["data"]:
out.write(json.dumps(patient) + "\n")
if not page["page_info"]["has_next_page"]:
break
cursor = page["page_info"]["end_cursor"]"page_info": {
"has_previous_page": false,
"has_next_page": true,
"start_cursor": "AAAAA",
"end_cursor": "BBBBBB"
}Appointments work the same way, with one difference. GET /appointments needs a start and an end, so load them one date range at a time.
See Pagination.
2. Pick up changes
There are two ways to stay current. Webhooks are the better default.
| Way | How it works | Use it when |
|---|---|---|
| Webhooks | NexHealth calls your URL when a record is created or updated. | You want changes as they happen and fewer API calls. |
| Polling | You ask each list endpoint for records changed since your last run with updated_since. | You run a scheduled job, or you cannot receive webhooks. |
Webhooks. Subscribe to the created and updated events for each resource you keep, for example patient_created, patient_updated, appointment_created and appointment_updated. Charges, claims and payments also have deleted events. See Setting up webhooks and the full list in Webhook Subscriptions.
Polling. Save the time each run starts. On the next run, pass it as updated_since.
curl -s --request GET \
--url "https://nexhealth.info/patients?subdomain=$NEX_SUBDOMAIN&location_id=$NEX_LOCATION_ID&per_page=1000&updated_since=2026-12-01T00:00:00Z" \
--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/patients?subdomain=${NEX_SUBDOMAIN}&location_id=${NEX_LOCATION_ID}&per_page=1000&updated_since=2026-12-01T00:00:00Z`;
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/patients",
params={
"subdomain": os.environ["NEX_SUBDOMAIN"],
"location_id": os.environ["NEX_LOCATION_ID"],
"per_page": 1000,
"updated_since": "2026-12-01T00:00:00Z",
},
headers={
"Nex-Api-Version": "v3.0.0",
"Authorization": "Bearer " + os.environ["NEX_TOKEN"],
},
)
print(res.json())Charges and payments also take include_deleted, so your copy can drop records that were removed.
3. Stay inside the rate 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. Sleep and retry.
Large pages and webhooks keep you well under the limit. See Rate limiting.
4. Check how fresh the data is
NexHealth reads from the health record system on a cycle, so your copy can only be as fresh as the last read. GET /sync_status gives the read and write status for each sync and the time of the last read.
curl -s --request GET \
--url "https://nexhealth.info/sync_status?subdomain=$NEX_SUBDOMAIN" \
--header "Nex-Api-Version: v3.0.0" \
--header "Authorization: Bearer $NEX_TOKEN"const { NEX_TOKEN, NEX_SUBDOMAIN } = process.env;
const url = `https://nexhealth.info/sync_status?subdomain=${NEX_SUBDOMAIN}`;
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/sync_status",
params={
"subdomain": os.environ["NEX_SUBDOMAIN"],
},
headers={
"Nex-Api-Version": "v3.0.0",
"Authorization": "Bearer " + os.environ["NEX_TOKEN"],
},
)
print(res.json())A gap of up to 20 minutes during office hours is normal. Longer gaps overnight and on weekends are normal too. See Interpreting sync status.
Updated about 1 hour ago