Scheduling Configuration Guide

A complete scheduling integration with NexHealth has two moving parts: (1) configuring when a provider is available and (2) retrieving the bookable slots that return. This guide walks through scheduling configurations from simplest to most complex. At each level, you'll see how to configure the schedule, what the API returns when you ask for slots, and what to look out for in the data.


The walkthrough starts with manual configuration - you create the schedule through the API, and the shape is identical across every health record system. Later on in this guide we will cover synced configuration at Level 2b, once you have an understanding of our scheduling endpoints.

📘

Default working hours configuration

Manual is the default for most integrations. Synced is opt-in per location and only available for supported systems.

Examples below use:
v20240412 (/working_hours, /available_slots) with the Nex-Api-Version: v20240412 header.
Reference: [/working_hours], [/available_slots].

Level 1 — One provider, one operatory, fixed weekly schedule

Throughout this guide, we will use the example office called Happy Smiles Dental to demonstrate working hour configurations.

This straightforward configuration is the simplest one most practices start with. Every other level adds a layer of complexity on top.

Level 1 — One provider, one operatory, fixed weekly schedule

Happy Smiles Dental - Configuration 1

Dr. Smith works Mondays through Thursdays, 9:00 AM to 5:00 PM, in Operatory 1. Same hours every week. No lunch break in the schedule. No exceptions.

This straightforward configuration is the simplest one most practices start with. Every other level adds a layer of complexity on top.

Configure

One POST. The provider's full week goes in the days array.

curl -X POST 'https://nexhealth.info/working_hours?subdomain=YOUR_SUBDOMAIN&location_id=YOUR_LOCATION_ID' \
  -H 'Nex-Api-Version: v20240412' \
  -H 'Authorization: Bearer YOUR_BEARER_TOKEN' \
  -H 'Content-Type: application/json' \
  -d '{
    "working_hour": {
      "active": true,
      "provider_id": YOUR_PROVIDER_ID,
      "operatory_id": YOUR_OPERATORY_ID,
      "begin_time": "09:00",
      "end_time": "17:00",
      "days": ["Monday", "Tuesday", "Wednesday", "Thursday"]
    }
  }'

The response echoes the record back with source: "manual" and label: null. That null label is the marker and it means "this is the base provider working hours" You'll see why that distinction matters in later levels.

Response (trimmed):

{
  "data": [
    {
      "source": "manual",
      "label": null
    }
  ]
}

Retrieve slots

Ask for Dr. Smith's bookable time on a Monday:

curl -X GET 'https://nexhealth.info/available_slots?subdomain=YOUR_SUBDOMAIN&start_date=2026-05-07&days=1&lids[]=YOUR_LOCATION_ID&pids[]=YOUR_PROVIDER_ID&slot_length=60&slot_interval=45&overlapping_operatory_slots=false&appointments_per_timeslot=1' \
  -H 'Nex-Api-Version: v20240412' \
  -H 'accept: application/vnd.Nexhealth+json;version=2' \
  -H 'Authorization: Bearer YOUR_BEARER_TOKEN'

The response returns a continuous run of bookable windows from 9:00 AM to 5:00 PM.


Response (trimmed):

{
  "data": [
    "lid": 21241,
      "pid": 158936024,
      "slots": [
        {
          "time": "2026-05-07T09:00:00.000-07:00",
          "end_time": "2026-05-07T17:00:00.000-07:00",
          "operatory_id": 1,
          "working_hour_label_id": null
        },
  ]
}

Slot length is determined by the appointment type or the specified slot_interval. The slots themselves are simply the working hour window cut into bookable chunks.

Gotchas

The POST succeeded but slots are still empty.

Configured working hours is a pre-requisite to utilizing the /available_slots endpoint.
Below is an example 200 response for an /available_slots call without configured availability:

{
    "code": true,
    "description": "Next available slot not found. Please confirm that the provider has active availabilities.",
    "error": null,
    "data": [
        {
            "lid": 21241,
            "pid": 158936024,
            "slots": [],
            "next_available_date": null
        }
    ],
    "count": 1
}

Level 2A — Multiple providers, multiple operatories, lunch breaks (manual)

Two new things at this level: additional working hour records (one per provider-operatory pair) and gaps in the day (lunch). Manual handles both by treating them as data structure problems — more POSTs, split records.

Level 2A — Multiple providers, multiple operatories, lunch breaks (manual)

Happy Smiles Dental - Configuration 2

The practice has two dentists and a hygienist. Dr. Smith works out of Operatory 3, Dr. Patel splits between Operatories 1 and 2, and Hygienist Lee floats across all three. Each provider has a break in the middle of their day. Hours are still fixed each week.

Configure

For Dr. Patel working two operatories, you POST twice where each working hour record binds a single operatory_id.
For lunch, you split the day into two records: a 9:00–12:00 morning block and a 1:00–5:00 afternoon block. There's no "lunch" concept in manual configuration. Lunch is just an absence between two records.

# Dr. Patel, Operatory 1, morning
curl -X POST 'https://nexhealth.info/working_hours?subdomain=YOUR_SUBDOMAIN&location_id=YOUR_LOCATION_ID' \
  -H 'Nex-Api-Version: v20240412' \
  -H 'Authorization: Bearer YOUR_BEARER_TOKEN' \
  -H 'Content-Type: application/json' \
  -d '{
    "working_hour": {
      "active": true,
    "provider_id": 158936025,
    "begin_time": "09:00",
    "end_time": "12:00",
    "operatory_id": 1,
    "days": ["Monday","Wednesday"]
    }
  }'

# Dr. Patel, Operatory 1, afternoon — second POST with begin_time 13:00, end_time 17:00
# Dr. Patel, Operatory 2, morning + afternoon — two more POSTs

A GET /working_hours call for the location returns the individual provider working hour records. For the practice above, that's roughly 18 records with one per provider, per operatory assignment, per half-day.

Response (trimmed)

"data": [
    {
      "id": 96878614,
      "provider_id": 158936025,
      "location_id": 21241,
      "operatory_id": 1,
      "begin_time": "09:00",
      "end_time": "12:00",
      "days": ["Monday","Wednesday"],
      "active": true,
      "source": "manual",
      "label": null
    },
    {
      "id": 96879940,
      "provider_id": 158936025,
      "location_id": 21241,
      "operatory_id": 2,
      "begin_time": "09:00",
      "end_time": "12:00",
      "days": ["Monday","Wednesday"],
      "active": true,
      "source": "manual",
      "label": null
    },
    {
      "id": 96878901,
      "provider_id": 158936025,
      "location_id": 21241,
      "operatory_id": 2,
      "begin_time": "13:00",
      "end_time": "17:00",
      "days": ["Monday","Wednesday"],
      "active": true,
      "source": "manual",
      "label": null
    },
    {
      "id": 96883945,
      "provider_id": 158936025,
      "location_id": 21241,
      "operatory_id": 1,
      "begin_time": "13:00",
      "end_time": "17:00",

Gotchas

One provider working day, multiple records.

A provider working two operatories returns two records, not one with multiple operatories. A provider with a lunch break returns two records per day, not one with a gap.


Level 2B - Calendar Unavailabilities

Level 2B - Calendar Unavailabilities

In Happy Smiles Dental - Configuration 2 you saw that lunch is modeled as the absence of a working hour. There is a 9:00 - 12:00 working hour, a gap in between, and a 1:00 - 5:00 working hour. That's one way to make time unbookable. There's another way to do this.

Practices can place dedicated calendar unavailabilities (often called events or blocks) on the schedule. These are discrete blocks of time on a provider's calendar that mark the time slot as unavailable and are configured directly in the target health record system.


Instead of splitting a working hour record, the office adds a block onto the calendar (a staff meeting, a team huddle, a recurring admin hour) and the Synchronizer API treats that block as an unavailable appointment. The slot is automatically excluded from /available_slots.

The Synchronizer API returns these blocks as an unavailable appointment (unavailable is true) through the /appointments endpoint as they are scheduled breaks on the calendar.

Example response from /appointments:
Response (trimmed)

{
  "code": true,
  "description": null,
  "error": null,
  "data": {
    "id": 1484450509,
    "patient_id": null,
    "provider_id": null,
    "provider_name": null,
    "start_time": "2026-07-07T16:00:00.000Z",
    "end_time": "2026-07-07T17:00:00.000Z",
    "unavailable": true,
    "cancelled": false,
    "location_id": 21241,
    "foreign_id": "block_47",
    "foreign_id_type": "remote_db-opendental-DataSource-9326",
    "misc": {},
  },
  "count": null
}

Two fields are the markers:

  • patient_id is null - no patient is associated with the block
  • unavailable is true - the slot is explicitly marked as unbookable

When /available_slots derives bookable time, it filters out any window that overlaps with a record where unavailable: true. You don't need to filter these out yourself.


This concept is source-agnostic. Calendar unavailabilities exist whether the practice uses manual or synced configuration.

Dentrix

For Dentrix event blocks are recognized as calendar unavailabilities.

Open dental

For Open dental non-scheduling blockouts and holidays are recognized as calendar unavailabilities.

Eaglesoft

For Eaglesoft, closed chairs (enabled upon configuration request) and time blocks are recognized as calendar unavailabilities.

Level 2C - Appointment Types

Level 2C - Appointment Types

A working hour record tells you when a provider is available. It doesn't tell you what they can be booked for. That's the job of appointment types.


For example, in Happy Smiles Dental - Configuration 2 the office needs to start booking specific kinds of appointments. They define Cleaning (30 minutes), Exam (15 minutes), and Root Canal (60 minutes) as NexHealth appointment types. When your product asks the patient "what are you scheduling for?", these are the categories it offers.

There are two related concepts here, and the distinction matters: NexHealth appointment types are categories defined only inside NexHealth. Appointment descriptors are the EHR's native appointment types and procedure codes, read directly from the practice's system.

NexHealth appointment types

These are a NexHealth construct. They are not read from or written to the target EHR. You create them through the API to define what kinds of appointments a provider can serve, how long each takes and when patients can book for certain appointment types.

Configure

POST to /appointment_types:

curl -X POST 'https://nexhealth.info/appointment_types?subdomain=YOUR_SUBDOMAIN' \
  -H 'Nex-Api-Version: v20240412' \
  -H 'Authorization: Bearer YOUR_BEARER_TOKEN' \
  -H 'Content-Type: application/json' \
  -d '{
    "appointment_type": {
      "name": "Cleaning",
      "minutes": 30,
      "bookable_online": false
    },
    "location_id": YOUR_LOCATION_ID
  }'

minutes is the duration the appointment occupies. This is what determines slot length when the type is requested.


NexHealth appointment types can then be assigned to a working hour record to define what types of appointments a provider can be booked for. You can bind these appointment types by including appointment_type_ids when creating or editing a working hour record.


For the full field reference, see the appointment types documentation.

Appointment descriptors (EHR-specific)

For supported health record systems, the API also surfaces the EHR's native appointment types and procedure codes through appointment descriptors. Descriptors are read from the EHR.


They have two use cases:

  1. Read the appointment types and procedure codes from the target health record system for a given appointment.
  2. Write appointments into the EHR with specific procedure codes or appointment types.

Descriptors matter when a posted appointment needs to land in the EHR with a specific code that the practice's downstream systems depend on. For example, a D1110 — Prophylaxis Adult code for the billing system, or a procedure code that drives recall messaging and patient outreach.

Retrieve

GET /locations/{id}/appointment_descriptors returns all the possible descriptors available for a given location. See the appointment descriptors documentation for the full reference.

Map an appointment descriptor to a NexHealth appointment type

Once you've identified the descriptors you want, PATCH the NexHealth appointment_type record to associate it. Subsequent posted appointments with the appointment_type_id included will include the EHR specific procedure codes or appointment types directly in their system.

curl -X PATCH 'https://nexhealth.info/appointment_types/YOUR_APPOINTMENT_TYPE_ID?subdomain=YOUR_SUBDOMAIN' \
  -H 'Nex-Api-Version: v20240412' \
  -H 'Authorization: Bearer YOUR_BEARER_TOKEN' \
  -H 'Content-Type: application/json' \
  -d '{
    "appointment_type": {
      "emr_appt_descriptor_ids": [9816179, 9815469]
    },
    "location_id": YOUR_LOCATION_ID
  }'

For a full guide please see: https://docs.nexhealth.com/v20240412/docs/access-procedure-codes

Example

Happy Smiles Dental - Configuration 2 needs to start booking specific kinds of appointments. They define Cleaning (30 minutes), Exam (15 minutes), and Root Canal (60 minutes) as NexHealth appointment types. When your product asks the patient "what are you scheduling for?", these are the categories it offers.


Inside the target health record system, the practice already has its own appointment types and procedure codes used for billing: D1110 — Prophylaxis Adult for the cleaning, D0150 — Comprehensive Oral Evaluation for the exam, D3310 — Endodontic Therapy for the root canal.

These codes are commonly shared across practices, but their NexHealth IDs may differ in the API by location. These are appointment descriptors that are read in from the target health record system, not created in NexHealth.


For example, you can configure NexHealth Appointment type Cleaning to map to the descriptor D1110 — Prophylaxis Adult so that when an appointment is posted, it lands in the target health record system with the correct billing code.

Gotchas

Appointment Types and Appointment Descriptors are different things, easy to conflate.

A name on a NexHealth appointment type ("Cleaning") and an EHR appointment type ("New Patient Cleaning") may overlap conceptually but they're separate records in separate systems. The NexHealth appointment type categorizes the appointment for scheduling; the descriptor controls how the appointment is written into the EHR. Treat them as independent entities.

Forgetting to map a descriptor.

An appointment POSTed without a mapped descriptor will land in the EHR using the practice's default appointment type, which may not match what your application intended and the office would need to manually make the appropriate changes in their target health record system. If the practice's billing depends on specific procedure codes you may want to include those in your posted appointments.

Level 2D - Under the hood: how slots are derived

Level 2D - Under the hood: how slots are derived

Going back to Dr. Patel from Level 2a, they have a 9:00–12:00 morning block and a 1:00–5:00 afternoon block in Operatory 1. Now layer on what a real day looks like: he has a booked appointment from 8:00–9:00, a calendar block from 9:00–11:00, a lunch break from 12:00–1:00, and the rest of the afternoon is open.

Three things drive what /available_slots returns:

1. The request window. start_date plus days defines the range. The endpoint looks at every working hour record that falls inside that window.
2. Working hours as the source of truth. Within the window, the endpoint uses Dr. Patels's /working_hours records to determine when he's working. Anything outside those records isn't a candidate.
3. appointment_type_id narrows scope and sets slot length. Passing appointment_type_id tells the endpoint two things: what slot length to use (the appointment type's minutes value), and which working hour records to consider (only those associated with that NexHealth appointment type).

Example

If the 9:00–12:00 block is assigned Cleaning and the 1:00–5:00 block is assigned Exam, a request for Cleaning availability only evaluates the 9:00–12:00 window. The afternoon never enters the calculation. With Cleaning set to 30 minutes, the endpoint returns 30-minute slots minus the 9:00–11:00 calendar unavailability. This means 11:00 and 11:30 come back as bookable from the /available_slots endpoint response.

Where appointment descriptors come in

Slot calculation is one thing. Writing the appointment to the EHR is another.

When a patient books one of those 11:00 cleaning slots, the appointment lands in the target health record system associated with the D1110 — Prophylaxis Adult descriptor that the Cleaning NexHealth appointment type was mapped to in Level 2c.

Nexhealth appointment types abstract the scheduling logic. Descriptors localize the EHR write. Slot calculation doesn't care about descriptors, they only matter once an appointment is being inserted into the target system.

Retrieve slots

A GET /available_slots request now spans more providers and more operatories. The response groups slots by pid (provider)and lid
(location id):

Response (trimmed)

"data": [
    {
      "lid": 21241,
      "pid": 158936025,
      "slots": [
        {
          "time": "2026-05-07T11:00:00.000-07:00",
          "end_time": "2026-05-07T11:30:00.000-07:00",
          "operatory_id": 1,
          "working_hour_label_id": null
        },
        {
          "time": "2026-05-07T11:30:00.000-07:00",
          "end_time": "2026-05-07T12:00:00.000-07:00",
          "operatory_id": 1,
          "working_hour_label_id": null
        }
      ]
    },
    }

Gotchas

Multiple provider operatory assignment

When you query /available_slots without specifying an operatory_id, the API returns slots for only the first available operatory, even if the provider has availability in multiple operatories. This means without specifying operatory_id then the /available_slots endpoint would only return Dr. Patels’s availability for one of his configured operatories. To get accurate, complete availability, always specify operatory_id in your query parameters if you need operatory-specific availability.

Working hour records have no appointment type assigned by default.

A request to /available_slots that includes appointment_type_id will return no available time slots if no working hour record is associated with that type. The endpoint isn't broken, it's correctly reporting that the provider has no working hours assigned to that appointment type. Map the type to the working hour record before searching for available time slots by appointment_type_id.

Level 3 - Synced working hours

So far, you've been the source of truth, every working hour record exists because your application POSTed it. For supported systems (Athena, Dentrix Enterprise, eClinicalWorks, Open Dental, Orthotrac, ModMed, NextGen Office, and Eaglesoft.), there's another way: NexHealth reads the schedule directly from the practice's health record system. The practice manages it through their existing workflow. Your application reads what the practice has already configured in their system.

Level 3 - Synced working hours

Manual working hours are useful if practices want to offer patients only a subset of availabilities that exist in the practice’s health record system, or if they want to offer patients ad hoc time slots that might not exist in the EHR. They’re necessary for the systems NexHealth doesn’t offer synced availability for.

Synced working hours on the other hand are the configured working hours defined by the office directly in the target health record system and are synced to the NexHealth system. These are useful when offices use the configured rules in the health record system as their source of truth (requires less configuration) and want to make their full calendar available for online booking.

⚠️

Pick one or the other for a given practice. While mixing manual and synced working hours are possible, it creates overlapping records that can affect available slots returned.

Open dental synced working hours

Synced working hours are readily available for Open Dental. No enablement step is required. The endpoint returns provider schedules and blockout data immediately.

Two types of records from the /working_hours endpoint are returned, distinguished by the label field:

  1. Provider working hours - the provider's base schedule as configured in Open Dental. The label field is null.
  2. Blockout / templated working hours - named time blocks the practice has designated for specific purposes (lunch, hygiene-only, new patients). The label object contains a name value.

Provider working hours (label is null):

This is the synced path. Same practice as Level 1. Dr. Smith works Mondays through Thursdays, 9:00 AM to 5:00 PM, in Operatory 1

Response (trimmed):

{
  "data": [
    {
      "id": 73091254,
      "provider_id": 518347930,
      "location_id": 445892,
      "operatory_id": 1,
      "begin_time": "09:00",
      "end_time": "17:00",
      "days": ["Monday"],
      "specific_date": "2025-09-29",
      "custom_recurrence": null,
      "tz_offset": "-0400",
      "active": true,
      "source": "synced",
      "label": null
    }
  ]
}

A null label represents time when the provider is in the office. Reference these hours when telling a patient "Dr. Smith is available on Mondays from 8 am to 5 pm."

In Open dental this corresponds to the configured provider’s schedule found in: Setup -> Schedules

Blockout working hours (label has a string value):

Blockout working hours are a secondary type of synced working hours. These define specific blocks of time on the health record system’s calendar for specific appointment types. The office will configure these so that the office staff knows which specific types of appointments a doctor will be available for booking on a given day. For example, and office can schedule Cleaning from 9am - 12 pm and then a non-scheduable blockout for lunch from 12pm - 1pm.

Response (trimmed):

{
  "data": [
    {
      "id": 72941563,
      "provider_id": 518347926,
      "location_id": 445892,
      "operatory_id": 287456,
      "begin_time": "09:00",
      "end_time": "11:00",
      "days": ["Monday"],
      "specific_date": "2025-09-29",
      "custom_recurrence": null,
      "tz_offset": "-0400",
      "active": true,
      "source": "synced",
      "label": {
        "id": 52187,
        "location_id": null,
        "name": "Exam"
      }
    }
  ]
}

The label name comes directly from how the practice named the blockout in Open Dental. Labels vary across practices.
In Open dental this corresponds to the defined blockouts found in: Setup -> Definitions -> Blockout types.

Eaglesoft synced working hours

The Eaglesoft provider schedule is determined by two things

  1. Default provider hours - The base set of time a provider works at the office
  2. Custom hours - Custom provider hours in Eaglesoft represent specific day overrides on top of a provider’s default hours

Default provider working hours

In Eaglesoft these are configured in List -> Provider/Staff -> Edit provider -> Hours

Custom hours

In Eaglesoft these are configured in List -> Provider/Staff -> Edit provider -> Hours -> Custom Hours

Gothcas

Custom schedule changes replace records, not edit them.

When a practice modifies a provider's schedule inside Eaglesoft, NexHealth deletes the existing working hour records for that provider and generates fresh ones to reflect the new schedule.