Quickstart: book your first appointment
Go from a new account to a booked appointment in your sandbox in about 10 minutes.
You do not need a health record system. You paste in one value, your API key. Every other value comes from the API.
Before you start
- Create a developer account. NexHealth creates a demo practice for you with sample data.
- In the Developer Portal, open the API Key page and click Create sandbox API key. The key is shown once, so save it. Only administrators see this page. Each organization has one sandbox key and one production key. You can rotate a key from the same page.
- Pick a language. Every step has the same call in cURL, Node and Python.
- cURL. Install
curlandjq. The samples usejqto pull values out of each response. Run each step in your terminal. - Node. Use Node 18 or later. Add each step to one file,
quickstart.mjs, and run it after the last step. - Python. Install the
requestspackage. Add each step to one file,quickstart.py, and run it after the last step.
- cURL. Install
Save your key in your terminal. All three languages read it from there.
export NEX_API_KEY="your-sandbox-api-key"1. Get a token
Trade your API key for a bearer token. Every other call uses the token.
export NEX_TOKEN=$(curl -s --request POST \
--url "https://nexhealth.info/authenticates" \
--header "Nex-Api-Version: v3.0.0" \
--header "Authorization: $NEX_API_KEY" | jq -r '.data.token')
echo $NEX_TOKEN// Save as quickstart.mjs and run: NEX_API_KEY=your-key node quickstart.mjs
// Needs Node 18 or later.
const BASE = "https://nexhealth.info";
async function call(method, path, opts = {}) {
const url = new URL(BASE + path);
for (const [key, value] of Object.entries(opts.query || {})) {
url.searchParams.set(key, value);
}
const res = await fetch(url, {
method,
headers: {
"Nex-Api-Version": "v3.0.0",
"Content-Type": "application/json",
Authorization: opts.token ? "Bearer " + opts.token : process.env.NEX_API_KEY,
},
body: opts.body ? JSON.stringify(opts.body) : undefined,
});
const json = await res.json();
if (!res.ok || json.code === false) {
throw new Error(method + " " + path + " failed: " + res.status + " " + JSON.stringify(json.error));
}
return json.data;
}
// 1. Get a token
const { token } = await call("POST", "/authenticates");# Save as quickstart.py and run: NEX_API_KEY=your-key python quickstart.py
# Needs the requests package: pip install requests
import os
from datetime import datetime, timedelta, timezone
import requests
BASE = "https://nexhealth.info"
def call(method, path, token=None, params=None, body=None):
res = requests.request(
method,
BASE + path,
params=params,
json=body,
headers={
"Nex-Api-Version": "v3.0.0",
"Authorization": "Bearer " + token if token else os.environ["NEX_API_KEY"],
},
)
payload = res.json()
if not res.ok or payload.get("code") is False:
raise RuntimeError(f"{method} {path} failed: {res.status_code} {payload.get('error')}")
return payload["data"]
# 1. Get a token
token = call("POST", "/authenticates")["token"]The Node and Python samples start with one helper, call. It sends every request, adds the version header and stops if a call fails.
The API returns the token in data.token.
{
"code": true,
"data": {
"token": "eyJhbGciOiJIUzI1NiJ9..."
},
"description": "Authenticated",
"error": []
}A token lasts 24 hours in the sandbox and 1 hour in production. Calls without a valid token return 401. Request a new token the same way.
2. Find your demo practice
An institution is a practice, or a group of practices under one brand. Its subdomain identifies it. A location is one office. Most calls need both.
List the institutions your key can reach and save the first subdomain and location ID.
curl -s --request GET \
--url "https://nexhealth.info/institutions" \
--header "Nex-Api-Version: v3.0.0" \
--header "Authorization: Bearer $NEX_TOKEN" > institutions.json
export NEX_SUBDOMAIN=$(jq -r '.data[0].subdomain' institutions.json)
export NEX_LOCATION_ID=$(jq -r '.data[0].locations[0].id' institutions.json)
echo $NEX_SUBDOMAIN $NEX_LOCATION_ID// 2. Find your demo practice
const [institution] = await call("GET", "/institutions", { token });
const subdomain = institution.subdomain;
const locationId = institution.locations[0].id;# 2. Find your demo practice
institution = call("GET", "/institutions", token)[0]
subdomain = institution["subdomain"]
location_id = institution["locations"][0]["id"]You can also copy both values from the Institutions page of the portal. See Institutions, syncs, and locations.
3. Find a patient, a provider and an operatory
A provider is the doctor or hygienist who sees the patient. An operatory is the room or chair. Some health record systems book by operatory and some do not. Your demo practice has both.
export NEX_PATIENT_ID=$(curl -s --request GET \
--url "https://nexhealth.info/patients?subdomain=$NEX_SUBDOMAIN&location_id=$NEX_LOCATION_ID&per_page=5" \
--header "Nex-Api-Version: v3.0.0" \
--header "Authorization: Bearer $NEX_TOKEN" | jq -r '.data[0].id')
export NEX_PROVIDER_ID=$(curl -s --request GET \
--url "https://nexhealth.info/providers?subdomain=$NEX_SUBDOMAIN&location_id=$NEX_LOCATION_ID&per_page=5" \
--header "Nex-Api-Version: v3.0.0" \
--header "Authorization: Bearer $NEX_TOKEN" | jq -r '.data[0].id')
export NEX_OPERATORY_ID=$(curl -s --request GET \
--url "https://nexhealth.info/operatories?subdomain=$NEX_SUBDOMAIN&location_id=$NEX_LOCATION_ID&per_page=5" \
--header "Nex-Api-Version: v3.0.0" \
--header "Authorization: Bearer $NEX_TOKEN" | jq -r '.data[0].id')
echo $NEX_PATIENT_ID $NEX_PROVIDER_ID $NEX_OPERATORY_ID// 3. Find a patient, a provider and an operatory
const scope = { subdomain, location_id: locationId, per_page: 5 };
const [patient] = await call("GET", "/patients", { token, query: scope });
const [provider] = await call("GET", "/providers", { token, query: scope });
const [operatory] = await call("GET", "/operatories", { token, query: scope });# 3. Find a patient, a provider and an operatory
scope = {"subdomain": subdomain, "location_id": location_id, "per_page": 5}
patient = call("GET", "/patients", token, scope)[0]
provider = call("GET", "/providers", token, scope)[0]
operatory = call("GET", "/operatories", token, scope)[0]Each call returns a list in data. This is the patient list, trimmed.
{
"code": true,
"data": [
{
"id": 415,
"first_name": "John",
"last_name": "Smith",
"name": "John Smith"
}
],
"page_info": {
"has_previous_page": false,
"has_next_page": true,
"start_cursor": "AAAAA",
"end_cursor": "BBBBBB"
}
}4. Book the appointment
Send the patient, the provider, the operatory and a time. Times are in UTC. In the cURL sample, change the date to a day in the future. The Node and Python samples book tomorrow at 15:00 UTC.
cat > appointment-request.json <<EOF
{
"appt": {
"patient_id": $NEX_PATIENT_ID,
"provider_id": $NEX_PROVIDER_ID,
"operatory_id": $NEX_OPERATORY_ID,
"start_time": "2026-12-01T15:00:00Z",
"end_time": "2026-12-01T15:30:00Z"
}
}
EOF
curl -s --request POST \
--url "https://nexhealth.info/appointments?subdomain=$NEX_SUBDOMAIN&location_id=$NEX_LOCATION_ID¬ify_patient=false" \
--header "Nex-Api-Version: v3.0.0" \
--header "Authorization: Bearer $NEX_TOKEN" \
--header "Content-Type: application/json" \
--data @appointment-request.json > appointment.json
cat appointment.json
export NEX_APPOINTMENT_ID=$(jq -r '.data.appt.id' appointment.json)// 4. Book the appointment for tomorrow at 15:00 UTC
const start = new Date();
start.setUTCDate(start.getUTCDate() + 1);
start.setUTCHours(15, 0, 0, 0);
const end = new Date(start.getTime() + 30 * 60 * 1000);
const { appt } = await call("POST", "/appointments", {
token,
query: { subdomain, location_id: locationId, notify_patient: false },
body: {
appt: {
patient_id: patient.id,
provider_id: provider.id,
operatory_id: operatory.id,
start_time: start.toISOString(),
end_time: end.toISOString(),
},
},
});
console.log("Booked appointment", appt.id);# 4. Book the appointment for tomorrow at 15:00 UTC
start = (datetime.now(timezone.utc) + timedelta(days=1)).replace(hour=15, minute=0, second=0, microsecond=0)
end = start + timedelta(minutes=30)
appt = call(
"POST",
"/appointments",
token,
{"subdomain": subdomain, "location_id": location_id, "notify_patient": "false"},
{
"appt": {
"patient_id": patient["id"],
"provider_id": provider["id"],
"operatory_id": operatory["id"],
"start_time": start.strftime("%Y-%m-%dT%H:%M:%SZ"),
"end_time": end.strftime("%Y-%m-%dT%H:%M:%SZ"),
}
},
)["appt"]
print("Booked appointment", appt["id"])A 201 response means NexHealth accepted the appointment.
{
"code": true,
"data": {
"appt": {
"id": 1822,
"patient_id": 415,
"provider_id": 104,
"operatory_id": 181,
"start_time": "2026-12-01T15:00:00.000Z",
"end_time": "2026-12-01T15:30:00.000Z",
"foreign_id": null,
"last_sync_time": null
}
}
}5. Read it back
curl -s --request GET \
--url "https://nexhealth.info/appointments/$NEX_APPOINTMENT_ID?subdomain=$NEX_SUBDOMAIN" \
--header "Nex-Api-Version: v3.0.0" \
--header "Authorization: Bearer $NEX_TOKEN"// 5. Read it back
const saved = await call("GET", "/appointments/" + appt.id, { token, query: { subdomain } });
console.log(saved);# 5. Read it back
print(call("GET", "/appointments/" + str(appt["id"]), token, {"subdomain": subdomain}))You booked an appointment through the API.
6. Subscribe to the write webhook
At a real practice, NexHealth accepts the appointment first and writes it into the health record system afterwards. The appointment_insertion webhook tells you whether that write succeeded. Set it up now so it is ready when you connect a real system.
Register an HTTPS URL, then subscribe it to the event for your institution.
export NEX_WEBHOOK_ID=$(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" }' | jq -r '.data.id')
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" }'// 6. Subscribe to the write webhook
const endpoint = await call("POST", "/webhook_endpoints", {
token,
body: { target_url: "https://your-app.example.com/hooks" },
});
await call("POST", "/webhook_endpoints/" + endpoint.id + "/webhook_subscriptions", {
token,
query: { subdomain },
body: { resource_type: "Appointment", event: "appointment_insertion" },
});
console.log("Webhook secret key", endpoint.secret_key);# 6. Subscribe to the write webhook
endpoint = call("POST", "/webhook_endpoints", token, body={"target_url": "https://your-app.example.com/hooks"})
call(
"POST",
"/webhook_endpoints/" + str(endpoint["id"]) + "/webhook_subscriptions",
token,
{"subdomain": subdomain},
{"resource_type": "Appointment", "event": "appointment_insertion"},
)
print("Webhook secret key", endpoint["secret_key"])The first response also holds a secret_key. Save it. You use it to check the signature on every message. See Setting up webhooks.
Your demo practice will not send this eventThe demo practice is sample data with no health record system behind it, so the write never happens and
foreign_idstaysnull. To seeappointment_insertion.completearrive, connect a free Open Dental demo to your sandbox. See Sandbox and test mode.
Read How a write reaches the health record system before you build on writes.
Prefer Postman?
The Postman collection has every endpoint ready to send. Fork the collection to copy it and its environment to your Postman account. In the NexHealth API Environment, fill in your API key, subdomain and location ID. Then send post Authenticates. A script in the collection saves the bearer token, so every other request works from there.
Next steps
| If you want to | Go to |
|---|---|
| Show open times and book into them | Scheduling quickstart |
| Read charges and post payments | Payments quickstart |
| Pull practice data into your own systems | Data pipelines quickstart |
| Watch an appointment land in a real system | Sandbox and test mode |
| Put a real practice live | Go-live checklist |
Updated 30 minutes ago