Migration Guide to Stable
Migrating from v2.2.2 to v20240412
This guide walks through every endpoint of the Synchronizer API v2.2.2 and documents the breaking changes you need to handle to move to the NexHealth API v20240412, along with concrete alternatives for behavior that was removed (most notably include values on list endpoints, and the pagination scheme).
How to read this guide:
- Read the cross-cutting sections first - version selection, pagination, and includes. They apply to many endpoints.
- Then use the endpoint-by-endpoint reference as a checklist for the endpoints you actually call.
Legend used throughout:
| Status | Meaning |
|---|---|
| ✅ Unchanged | Same request and response contract in v20240412. You only need to switch the version header. |
| ⚠️ Changed | Endpoint exists in v20240412 but has breaking changes (params, response shape, semantics, pagination). |
| ❌ Removed | Endpoint does not exist in v20240412. A replacement endpoint is documented. |
| ✨ New | Endpoint only exists in v20240412 (useful as a replacement/alternative). |
1. Selecting the API version
Versioning moved from a vendored Accept header to the dedicated Nex-Api-Version header.
v2.2.2 request:
curl "https://nexhealth.info/patients?subdomain=demo&location_id=1" \
-H "Accept: application/vnd.Nexhealth+json;version=2" \
-H "Authorization: Bearer <token>"v20240412 request:
curl "https://nexhealth.info/patients?subdomain=demo&location_id=1" \
-H "Nex-Api-Version: v20240412" \
-H "Authorization: Bearer <token>"Details:
- Send
Nex-Api-Version: v20240412on every request, including requests to endpoints that are unchanged from v2.2.2. - If the header is absent, requests with the legacy
Accept: application/vnd.Nexhealth+json;version=2header (or no version at all) resolve to v2.2.2. Migrating means adding the header everywhere, not swapping URLs - paths stay the same. v3.0.0is accepted as a SemVer alias ofv20240412; the docs and this guide usev20240412.- Authentication is unchanged:
POST /authenticateswith your API key still returns the Bearer token used on subsequent calls. - Endpoints marked ✅ below are served by the exact same implementation in both versions - v20240412 falls through to them transparently. You do not need to wait to switch the header for those.
2. Response envelope
The success envelope keeps its overall shape, with one change on cursor-paginated list endpoints:
| Field | v2.2.2 | v20240412 |
|---|---|---|
code | true/false | unchanged |
description | informational messages (ignored params, deprecations) | unchanged - watch this field: deprecation notices and payload-change notes are surfaced here |
error | error messages | unchanged |
data | resource or array | unchanged, except GET /patients (see Patients) |
count | total number of matching records on paginated lists | removed on cursor-paginated lists (kept only on GET /patient_recalls) |
page_info | - | new on cursor-paginated lists (see below) |
3. Pagination: limit/offset → cursor
This is the most widespread breaking change. In v2.2.2, list endpoints used offset pagination (page + per_page).
In v20240412, redefined list endpoints use cursor (keyset) pagination.
v2.2.2 (offset)
GET /appointments?...&page=3&per_page=100
page- 1-based page number.per_page- number of records per page.- Response contains
countwith the total number of matching records.
v20240412 (cursor)
GET /appointments?...&per_page=100 # first page
GET /appointments?...&per_page=100&end_cursor=MTA0OA # next page
GET /appointments?...&per_page=100&start_cursor=MTAyNA # previous page
per_page- number of records per page.start_cursor/end_cursor- opaque cursor strings. Mutually exclusive; passing both is a 400.- Response gains a
page_infoobject and (generally) losescount:
{
"code": true,
"data": [ ... ],
"page_info": {
"has_previous_page": false,
"has_next_page": true,
"start_cursor": "MTAyNA",
"end_cursor": "MTA0OA"
}
}How to iterate
To walk a collection forward, repeat the same request, each time passing the previous response's page_info.end_cursor value as the end_cursor query parameter, until has_next_page is false.
Yes - end_cursor fetches the next page. The cursors name the boundaries of the page you already have (start_cursor = its first record, end_cursor = its last), so continuing forward means resuming from the end boundary, and going backward means resuming from the start boundary.
First request - no cursor params:
GET /payments?subdomain=demo&location_id=1&updated_since=2024-04-12T00:00:00Z&per_page=100
Nex-Api-Version: v20240412
Authorization: Bearer <token>
{
"code": true,
"data": [ ...100 payments... ],
"page_info": {
"has_previous_page": false,
"has_next_page": true,
"start_cursor": "MTAyNA",
"end_cursor": "MTA0OA"
}
}Next page - same request plus the end_cursor value returned above:
GET /payments?subdomain=demo&location_id=1&updated_since=2024-04-12T00:00:00Z&per_page=100&end_cursor=MTA0OA
Nex-Api-Version: v20240412
Authorization: Bearer <token>
Repeat until a response comes back with "has_next_page": false. To walk backward instead, pass the response's page_info.start_cursor value as the start_cursor query parameter.
Migration notes
- You cannot jump to page N and you cannot compute "total pages":
countis no longer returned on cursor endpoints. If your integration displayed totals or random-accessed pages, redesign around sequential iteration (has_next_page). - Cursors are opaque. Don't construct or parse them; only echo back the values from
page_info. Requests with malformed cursors are rejected (400). - Cursors encode the sort position. Don't change the
sortparam (or filters) mid-iteration; cursors from one ordering are not valid for another. - One exception to the general story:
GET /payment_plansalready uses cursor pagination in v2.2.2 - nothing changes there. GET /patient_recalls(cursor in v20240412) still returnscountalongsidepage_info, and temporarily falls back to offset behavior if you pass a legacypageparam. Treat that fallback as transitional - move to cursors.
Every endpoint in the reference section carries a Pagination note stating whether and how this applies to it.
4. include parameters: what changed, and the alternatives
include parameters: what changed, and the alternativesv20240412 deliberately removes include support from most list (index) endpoints - embedding related records on every row of a collection was the main source of slow, oversized list responses. The general pattern:
- List endpoints return flat objects with foreign keys (
patient_id,provider_id,charge_id, …) and no embedded records. - Detail endpoints (
GET /<resource>/{id}) keepincludesupport - in most cases with more include values than v2.2.2 had. - Dedicated endpoints exist for most relations you previously embedded - procedures, insurance coverages, charges, payments, adjustments, appointments. None of them requires a per-patient filter, so you can crawl the whole collection and join it to its parents client-side - no need for one request per patient.
When you migrate a call that used include on a list endpoint, pick one of:
-
Fetch the related collection in bulk from its dedicated endpoint and join client-side on the foreign key (
patient_id,appointment_id, …). Best for syncs. What each endpoint minimally requires:- ✨
GET /insurance_coverages- no filters at all. GET /procedures?updated_since=…-updated_sincealone is a valid filter (pass your last sync time, or an
old date for a full backfill).GET /charges|payments|adjustments?location_id=…&updated_since=…- same idea, scoped per location.GET /appointments?location_id=…&start=…&end=…- time-window based.
Per-record filters (
patient_id,patient_ids[],appointment_id, …) remain available for when you want a single parent's records on demand - they are optional, not required. - ✨
-
Fetch details per record via
GET /<resource>/{id}?include[]=…. Best when you drill into a handful of records, not whole collections.
Index endpoints that kept their includes (unchanged from v2.2.2): GET /providers (locations, availabilities, appointment_types), GET /operatories (appointment_types, appt_categories), and GET /appointment_types (descriptors).
The per-endpoint sections below map every removed include value to its concrete replacement.
5. Webhooks
The /webhook_endpoints CRUD is unchanged. The /webhook_endpoints/{id}/webhook_subscriptions endpoints changed in ways that matter for migration:
- Subscriptions are pinned to the API version they were created with. A subscription created via v2.2.2 keeps delivering v2.2.2-shaped payloads. To receive v20240412-shaped payloads (e.g. the flat appointment entity), create a new subscription using
Nex-Api-Version: v20240412and delete the v2.2.2 one once you've cut over. - The v20240412 event catalog is a superset of v2.2.2's. Every v2.2.2
resource_type/eventstill exists. New resources:
Adjustment,AdjustmentType,Charge,Claim,FeeSchedule,Message,Onboarding,Payment,PaymentType,TreatmentPlan(with*_created/*_updated/*_deleted, plusadjustment_insertion,payment_insertion, andonboarding_availability_ready). GET …/webhook_subscriptions:subdomainis now optional. Omit it to list every subscription on the endpoint (including non-institution subscriptions); pass it to scope to one institution.POST …/webhook_subscriptions:subdomainmust be omitted for resources not owned by an institution (currentlyOnboarding) - passing it returns 400. For all other resources it remains required.
6. Endpoint-by-endpoint reference
Summary table (all public v2.2.2 endpoints)
| v2.2.2 endpoint | Status | v20240412 |
|---|---|---|
POST /authenticates | ✅ | unchanged |
GET /institutions, GET /institutions/{id} | ✅ | unchanged |
GET /locations, GET /locations/{id}, GET /locations/{id}/appointment_descriptors | ✅ | unchanged |
GET /patients | ⚠️ | cursor pagination, envelope + entity changes, includes removed |
POST /patients | ⚠️ (additive) | return_existing_if_match option |
GET /patients/{id} | ⚠️ | entity changes; includes kept |
GET /patients/{id}/insurance_coverages | ❌ | → GET /insurance_coverages?patient_id=… |
GET /appointments | ⚠️ | cursor, patient_ids[], includes removed, cancelled/deleted semantics |
POST /appointments | ⚠️ | working-hours validation removed; appointments_per_timeslot |
GET /appointments/{id} | ✅ | unchanged (includes intact) |
PATCH /appointments/{id} | ⚠️ (additive) | reschedule/check-in/operatory/note updates |
GET /appointments/{id}/appointment_descriptors | ✅ | unchanged |
GET /appointment_slots | ❌ | → GET /available_slots |
GET/POST /availabilities, GET/PATCH/DELETE /availabilities/{id} | ❌ | → /working_hours |
GET/POST /appointment_types, GET/PATCH/DELETE /appointment_types/{id}, GET /appointment_types/{id}/appointment_descriptors | ✅ | unchanged |
GET /providers, GET /providers/{id} | ⚠️ | cursor + availability entity change |
GET /operatories | ⚠️ | cursor pagination only |
GET /operatories/{id} | ✅ | unchanged |
GET /procedures | ⚠️ | updated_after→updated_since, new date filters, cursor |
GET /patient_recalls | ⚠️ | cursor pagination |
GET /patient_recalls/{id} | ✅ | unchanged |
GET /recall_types, GET /recall_types/{id} | ✅ | unchanged |
GET /document_types, GET /document_types/{id} | ✅ | unchanged |
GET /insurance_plans | ⚠️ | includes removed, cursor, new filters |
GET /insurance_plans/{id} | ⚠️ (mostly additive) |
|
GET /adjustments, GET /charges, GET /payments (+ /{id} variants) | ⚠️ | rebuilt for v20240412 - treat as new endpoints |
GET /payment_plans, GET /payment_plans/{id} | ✅ | unchanged (already cursor-paginated) |
GET /sync_status | ✅ | unchanged |
GET/POST /webhook_endpoints, PATCH/DELETE /webhook_endpoints/{id} | ✅ | unchanged |
GET/POST …/webhook_subscriptions, PATCH/DELETE …/webhook_subscriptions/{subscription_id} | ⚠️ | version pinning, subdomain rules, new events |
Authenticates
POST /authenticates - ✅ Unchanged
POST /authenticates - ✅ Unchanged- Same request/response. Keep sending your API key in the
Authorizationheader; the returned Bearer token works for v20240412 requests exactly as for v2.2.2. - Pagination: not paginated (unchanged).
Institutions
GET /institutions, GET /institutions/{id} - ✅ Unchanged
GET /institutions, GET /institutions/{id} - ✅ Unchanged- Identical contract in v20240412.
- Pagination: not paginated in either version.
Locations
GET /locations, GET /locations/{id}, GET /locations/{id}/appointment_descriptors - ✅ Unchanged
GET /locations, GET /locations/{id}, GET /locations/{id}/appointment_descriptors - ✅ Unchanged- Identical contract in v20240412.
- Pagination: not paginated in either version.
Patients
GET /patients - ⚠️ Changed
GET /patients - ⚠️ ChangedRequest changes
| Change | v2.2.2 | v20240412 |
|---|---|---|
| Pagination | page / per_page | start_cursor / end_cursor / per_page (cursor) |
include[] | upcoming_appts, procedures, insurance_coverages, adjustments, charges, payments, address | removed entirely |
appointment_date_start / appointment_date_end | embedded matching appointments in the response | removed |
sort | fields name, created_at; default name | fields id, updated_at; default id |
Unchanged filters: subdomain (required), location_id (required), name, email, phone_number, date_of_birth, inactive, foreign_id, updated_since, new_patient, non_patient, location_strict.
Response changes
- Envelope:
datais now the array of patient objects directly. In v2.2.2 it was a nested map:
{"data": {"patients": [...]}}→{"data": [...]}. - Removed from the patient object:
location_ids,balance(performance),preferred_locale(usepreferred_language). - Added:
hipaaobject (privacy,consent,authorization+ their dates). countis gone; usepage_info.
Include alternatives
v2.2.2 include value | v20240412 alternative |
|---|---|
address | already in the response - each patient's bio field carries the address (address_line_1, address_line_2, city, state, zip_code); GET /patients/{id}?include[]=address also returns it as a structured address object |
upcoming_appts | GET /appointments?location_id=…&start=<now>&end=<horizon> in bulk, grouped by patient_id; or GET /patients/{id}?include[]=upcoming_appts per patient |
procedures | GET /procedures?updated_since=… in bulk, joined on patient_id (patient_id filter available for a single patient) |
insurance_coverages | GET /insurance_coverages in bulk - no filters required - joined on patient_id |
adjustments | GET /adjustments?location_id=…&updated_since=… in bulk, joined on patient_id |
charges | GET /charges?location_id=…&updated_since=… in bulk, joined on patient_id |
payments | GET /payments?location_id=…&updated_since=… in bulk, joined on patient_id |
(appointments via appointment_date_start/end) | GET /appointments?location_id=…&start=…&end=… (optionally narrowed with patient_ids[]) |
- Pagination: offset → cursor (breaking). Total
countno longer returned.
POST /patients - ⚠️ Changed (additive, non-breaking)
POST /patients - ⚠️ Changed (additive, non-breaking)- New optional
return_existing_if_match(defaultfalse). Default behavior matches v2.2.2: creating a patient that matches an existing one (date of birth + name + phone number) returns 400. Withtrue, the existing patient is returned with 200 instead (a fresh create still returns 201). Adopt it if you currently parse the 400 to find existing patients. - Request body and response shape otherwise unchanged.
- Pagination: not applicable.
GET /patients/{id} - ⚠️ Changed
GET /patients/{id} - ⚠️ Changed- Includes kept - all v2.2.2 values still work:
upcoming_appts,last_visited_appointment,children,guarantor,provider,procedures,insurance_coverages,adjustments,charges,payments,patient_alerts,address. - Removed from the patient object:
balance(use ✨GET /guarantor_balances?...for balance data),preferred_locale(usepreferred_language). - Added:
hipaaobject. - Changed within includes: the
adjustments,charges, andpaymentsincludes return the new v20240412 transaction shapes (see the Adjustments, Charges and Payments API reference);proceduresitems gaintreatment_plan_ids. - Pagination: not paginated (unchanged).
GET /patients/{id}/insurance_coverages - ❌ Removed
GET /patients/{id}/insurance_coverages - ❌ RemovedReplaced by the dedicated Insurance endpoints:
- ✨
GET /insurance_coverages?patient_id={id}- same data, plusupdated_sinceandactivefilters (activedefaults totrue, matching the old endpoint which only returned active coverages). Thepatient_idfilter is optional: drop it to crawl every coverage for the institution in one paginated sweep instead of requesting per patient. - ✨
GET /insurance_coverages/{id}- single coverage with embedded subscriber details. - Pagination: the old endpoint was offset-paginated; the replacement is cursor-paginated.
Appointments
GET /appointments - ⚠️ Changed
GET /appointments - ⚠️ ChangedRequest changes
| Change | v2.2.2 | v20240412 |
|---|---|---|
| Pagination | page / per_page | start_cursor / end_cursor / per_page (cursor) |
patient_id | single integer | deprecated - still accepted but no longer documented; responses carry a deprecation notice in description. Use patient_ids[] (max 25 ids) |
location_id | required | optional - but at least one of location_id or foreign_id is required; omitting location_id searches all locations your token can access |
include[] | patient (default!), guarantor, descriptors, booking_details, procedures, operatory, appointment_type | removed entirely |
sort | - | new; field updated_at (sort=-updated_at for descending). Default order: created_at ascending |
Unchanged filters: start, end (required datetimes), timezone, unavailable, nex_only, updated_since, appointment_type_id, foreign_id, provider_ids[], operatory_ids[], created_by.
cancelled filter semantics changed - appointments deleted in the EHR are treated as cancelled:
cancelled param | v2.2.2 returned | v20240412 returns |
|---|---|---|
| omitted | active + cancelled (EHR-deleted always excluded) | everything - active + cancelled + EHR-deleted |
true | cancelled and not EHR-deleted | cancelled or EHR-deleted |
false | active only | active only (same) |
If you relied on v2.2.2's default, the closest v20240412 equivalent is cancelled=false (you lose the cancelled-but-not-deleted rows; fetch them with a second cancelled=true call if needed).
Response changes
- Rows are now flat appointment objects (id,
patient_id,provider_id,provider_name,start_time,end_time,confirmed,patient_missed,note,unavailable, timestamps, …). The v2.2.2 default embeddedpatientobject, and every other embed, is gone. countis gone; usepage_info.
Include alternatives
v2.2.2 include value | v20240412 alternative |
|---|---|
patient (v2.2.2 default) | fetch patients in bulk via GET /patients and join on patient_id; or GET /appointments/{id}?include[]=patient when drilling into one appointment |
guarantor | GET /appointments/{id}?include[]=guarantor, or GET /patients/{patient_id}?include[]=guarantor |
descriptors | GET /appointments/{id}/appointment_descriptors (dedicated endpoint, unchanged from v2.2.2) |
booking_details | GET /appointments/{id}?include[]=booking_details |
procedures | GET /procedures?updated_since=… in bulk, joined on appointment_id; or GET /procedures?appointment_id={id} for a single appointment |
operatory | GET /operatories/{operatory_id} |
appointment_type | GET /appointment_types/{appointment_type_id} |
GET /appointments/{id} still supports all seven include values at once - it is the drop-in way to rehydrate a full v2.2.2-shaped appointment.
- Pagination: offset → cursor (breaking).
POST /appointments - ⚠️ Changed
POST /appointments - ⚠️ Changed- Working-hours validation removed. v2.2.2 rejected bookings whose time fell outside the provider's synced working hours; v20240412 does not run this check - out-of-hours bookings are accepted and written back to the EHR. If your UX depended on the API rejecting invalid slots, validate against ✨
GET /available_slotsbefore booking. - New optional
appointments_per_timeslot(1-5, default 1): allows multiple bookings on one slot. Only for EHRs that support multi-booking (eClinicalWorks, NextGen); other EHRs return 400 when the value exceeds 1. - Body (
appt{…}),notify_patient, response shape and201status are unchanged. - Pagination: not applicable.
GET /appointments/{id} - ✅ Unchanged
GET /appointments/{id} - ✅ Unchanged- Same contract, including
include[](patientdefault,guarantor,descriptors,booking_details,procedures,operatory,appointment_type). - Pagination: not applicable.
PATCH /appointments/{id} - ⚠️ Changed (additive, non-breaking)
PATCH /appointments/{id} - ⚠️ Changed (additive, non-breaking)v2.2.2 could only write back confirmed and cancelled (both keep their v2.2.2 behavior). v20240412 adds to the appt body:
start_time+end_time- reschedule (must be provided together),operatory_id- move the appointment,note- update the EHR note,checkin_at- write back check-in status.
EHR support for the new fields is limited:
-
start_time/end_time,operatory_idandnoteare only supported when the location's EHR supports appointment updates - Denticon, Dentrix, Dentrix Enterprise, Eaglesoft and Open Dental. For other EHRs, a request changing any of these fields is rejected with a 400 ("EMR does not support appointment update"). -
checkin_atis written back to the EHR for Denticon, Dentrix, Dentrix Enterprise, Eaglesoft, ModMed, NextGen and Open Dental. -
Pagination: not applicable.
GET /appointments/{id}/appointment_descriptors - ✅ Unchanged
GET /appointments/{id}/appointment_descriptors - ✅ Unchanged- Same contract (
descriptor_typefilter). - Pagination: not paginated in either version.
Appointment Slots → Available Slots
GET /appointment_slots - ❌ Removed → ✨ GET /available_slots
GET /appointment_slots - ❌ Removed → ✨ GET /available_slotsThe endpoint was renamed; the request and response contracts are otherwise compatible. To migrate without adopting any new features:
- Change the path from
GET /appointment_slotstoGET /available_slots(with theNex-Api-Version: v20240412header). - Keep your query parameters exactly as they are. Every v2.2.2 parameter is accepted unchanged:
start_date,days,lids[],pids[], and the optionaloperatory_ids[],appointment_type_id,slot_length,slot_interval,overlapping_operatory_slots. - Do not send the new optional parameters (
working_hour_label_id,working_hour_source,appointments_per_timeslot) - leaving them out preserves the existing behavior. - Read the response as before - same structure (per-location
slots[]+next_available_date). Each slot carries one new field,working_hour_label_id, which you can ignore.
One requirement was relaxed, which does not affect existing callers: pids[] is no longer strictly required - v20240412 accepts either pids[] or appointment_type_id. Requests that already send pids[] behave as in v2.2.2.
- Pagination: not paginated in either version (bounded by
days).
Availabilities → Working Hours
GET/POST /availabilities, GET/PATCH/DELETE /availabilities/{id} - ❌ Removed → ✨ /working_hours
GET/POST /availabilities, GET/PATCH/DELETE /availabilities/{id} - ❌ Removed → ✨ /working_hoursThe resource was renamed; the model gained labels and an explicit source. Mapping:
| v2.2.2 | v20240412 |
|---|---|
GET /availabilities | GET /working_hours |
POST /availabilities | POST /working_hours |
GET /availabilities/{id} | GET /working_hours/{id} |
PATCH /availabilities/{id} | PATCH /working_hours/{id} |
DELETE /availabilities/{id} | DELETE /working_hours/{id} |
Request changes
- Body wrapper key renames from
availability{…}toworking_hour{…}. The fields inside are identical (provider_id,operatory_id,begin_time,end_time,days[],specific_date,custom_recurrence{…},appointment_type_ids[],active). - Index filters:
location_id,provider_id,operatory_id,active,ignore_past_datesunchanged; newlabel_idandsource(manual|synced) filters. include[]=appointment_typesworks on index and show, as in v2.2.2.
Response changes
-
synced(boolean) is removed → replaced bysource("manual"= created in NexHealth,"synced"= from the EHR) and alabelobject (id,location_id,name). -
✨
GET /working_hour_labelslists the labels usable in filters. -
Pagination: v2.2.2 index was offset-paginated;
GET /working_hoursis cursor-paginated. The other verbs are not paginated.
Appointment Types
GET/POST /appointment_types, GET/PATCH/DELETE /appointment_types/{id}, GET /appointment_types/{id}/appointment_descriptors - ✅ Unchanged
GET/POST /appointment_types, GET/PATCH/DELETE /appointment_types/{id}, GET /appointment_types/{id}/appointment_descriptors - ✅ Unchanged- Identical contract in v20240412, including
include[]=descriptorson the two GET endpoints. - Pagination: not paginated in either version.
Providers
GET /providers - ⚠️ Changed
GET /providers - ⚠️ Changed- Pagination: offset (
page/per_page) → cursor (breaking).countremoved. - New
sortparam (fieldupdated_at). include[]kept:locations(default),availabilities,appointment_types. Filters unchanged (location_id,ids[],foreign_id,requestable,inactive,updated_since).- Response change inside
availabilitiesinclude: each availability losessyncedand gainssource(manual/synced) and alabelobject - same change as the working-hours model above. - For OAuth applications: the
appointmentsscope no longer grants access; onlyappointments:book_onlinedoes. (API-key auth is unaffected.)
GET /providers/{id} - ⚠️ Changed (entity only)
GET /providers/{id} - ⚠️ Changed (entity only)- Same params and includes as v2.2.2. Only the
availabilitiesentity change above applies. - Pagination: not applicable.
Operatories
GET /operatories - ⚠️ Changed
GET /operatories - ⚠️ Changed- Pagination: offset → cursor (breaking).
countremoved. Everything else - filters (search_name,foreign_id,updated_since) andinclude[](appointment_types,appt_categories; defaultappt_categories) - is unchanged. - Same OAuth-scope narrowing as providers (
appointments:book_onlineonly) for OAuth applications.
GET /operatories/{id} - ✅ Unchanged
GET /operatories/{id} - ✅ Unchanged- Identical contract (includes intact).
- Pagination: not applicable.
Procedures
GET /procedures - ⚠️ Changed
GET /procedures - ⚠️ Changed| Change | v2.2.2 | v20240412 |
|---|---|---|
| Pagination | page / per_page | cursor |
updated_after | filter on update time | renamed to updated_since (inclusive, UTC) |
| Date-range filters | - | new: started_after, started_before, ended_after, ended_before (inclusive, on start_date/end_date) |
sort | - | new; field updated_at |
| At-least-one-filter rule | one of provider_id, patient_id, appointment_id, updated_after | one of provider_id, patient_id, appointment_id, updated_since, started_after, started_before, ended_after, ended_before |
location_id still cannot be the only filter. The list response entity is unchanged.
- ✨ New:
GET /procedures/{id}- fetch a single procedure. Caveat (documented on the endpoint): the list endpoint is not filtered by your token's location access, so an id from the list can 404 on the detail endpoint if the procedure belongs to an inaccessible location. - Pagination: offset → cursor (breaking).
Patient Recalls
GET /patient_recalls - ⚠️ Changed
GET /patient_recalls - ⚠️ Changed- Pagination: offset → cursor (breaking) - with two transitional quirks: the response still includes
count, and a legacypageparam still triggers old-style offset pagination. Don't rely on the fallback. - Filters unchanged:
recall_id,patient_id,foreign_id,updated_since,due_after,sort(fielddate_due).
GET /patient_recalls/{id} - ✅ Unchanged
GET /patient_recalls/{id} - ✅ Unchanged- Pagination: not applicable.
Recall Types
GET /recall_types, GET /recall_types/{id} - ✅ Unchanged
GET /recall_types, GET /recall_types/{id} - ✅ Unchanged- Pagination: not paginated in either version.
Document Types
GET /document_types, GET /document_types/{id} - ✅ Unchanged
GET /document_types, GET /document_types/{id} - ✅ Unchanged- Pagination: not paginated in either version.
Insurance Plans
GET /insurance_plans - ⚠️ Changed
GET /insurance_plans - ⚠️ Changed| Change | v2.2.2 | v20240412 |
|---|---|---|
| Pagination | page / per_page | cursor |
include[] | patient_coverages, subscribers | removed |
updated_since | - | new (inclusive, UTC) |
include_deleted | - | new (soft-deleted plans excluded by default) |
sort | - | new; field updated_at |
payer_id and group_num filters are unchanged. The plan entity gains deleted_at.
Include alternatives
v2.2.2 include value | v20240412 alternative |
|---|---|
patient_coverages | ✨ GET /insurance_coverages - whole collection, no filters required (patient_id, updated_since optional) - or GET /insurance_plans/{id}?include[]=patient_coverages per plan |
subscribers | GET /insurance_plans/{id}?include[]=subscribers; subscriber details also come embedded on ✨ GET /insurance_coverages/{id} |
- Pagination: offset → cursor (breaking).
GET /insurance_plans/{id} - ⚠️ Changed (additive)
GET /insurance_plans/{id} - ⚠️ Changed (additive)include[]keepspatient_coveragesandsubscribers, and addsfee_schedule.- Entity gains
deleted_at; response includes the plan's fee schedule when requested. - Pagination: not applicable.
Adjustments, Charges & Payments
The financial endpoints (/adjustments, /charges, /payments) were rebuilt from the ground up for v20240412 and have not been available in v2.2.2 for some time, so there is no working v2.2.2 integration to migrate - build against the v20240412 API reference directly. In short:
-
GET /adjustments,GET /charges,GET /payments- list endpoints; each requires at least one filter and supportsupdated_since+include_deletedfor incremental syncs. -
GET /adjustments/{id},GET /charges/{id},GET /payments/{id}- detail endpoints withinclude[]support for related records (patient, provider, guarantor, claim, charge, and type/plan resources). -
✨
POST /adjustmentsand ✨POST /payments- new write-back endpoints (asynchronous202 Accepted; the outcome is delivered via theadjustment_insertion/payment_insertionwebhooks). -
Supporting lookups: ✨
GET /adjustment_types, ✨GET /payment_types, ✨GET /claims, ✨GET /guarantor_balances, ✨GET /insurance_balances. -
Pagination: all three list endpoints are cursor-paginated (see section 3).
Payment Plans
GET /payment_plans, GET /payment_plans/{id} - ✅ Unchanged
GET /payment_plans, GET /payment_plans/{id} - ✅ Unchanged- Same contract in both versions, including the index's required-filter rule (one of
patient_id,guarantor_id,updated_since) and the show endpoint'sinclude[](patient,guarantor). - Pagination: the index endpoint is cursor-paginated in v2.2.2 as well - it launched with the new scheme. Nothing changes.
Sync Status
GET /sync_status - ✅ Unchanged
GET /sync_status - ✅ Unchanged- Pagination: not paginated in either version.
Webhook Endpoints
GET/POST /webhook_endpoints, PATCH/DELETE /webhook_endpoints/{id} - ✅ Unchanged
GET/POST /webhook_endpoints, PATCH/DELETE /webhook_endpoints/{id} - ✅ Unchanged- Identical contract in v20240412.
- Pagination: not paginated in either version.
Webhook Subscriptions
See section 5 for the version-pinning model - it is the key migration concern here.
GET /webhook_endpoints/{id}/webhook_subscriptions - ⚠️ Changed
GET /webhook_endpoints/{id}/webhook_subscriptions - ⚠️ Changedsubdomainrequired → optional (omit to list all subscriptions on the endpoint, including non-institution ones such as Onboarding).resource_type/eventfilter enums expanded (superset of v2.2.2 - see section 5).- Pagination: not paginated in either version.
POST /webhook_endpoints/{id}/webhook_subscriptions - ⚠️ Changed
POST /webhook_endpoints/{id}/webhook_subscriptions - ⚠️ Changed- Subscriptions created here are pinned to v20240412 payloads - this is how you upgrade webhook payload shapes.
subdomain: still required for institution-owned resources, but must be omitted forOnboardingsubscriptions (400 otherwise).resource_type/eventaccept the expanded v20240412 catalog.- Pagination: not applicable.
PATCH /webhook_endpoints/{id}/webhook_subscriptions/{subscription_id} - ⚠️ Changed
PATCH /webhook_endpoints/{id}/webhook_subscriptions/{subscription_id} - ⚠️ Changedsubdomainis now required (v2.2.2 took no subdomain on this endpoint). When the subscription isn't institution-scoped (e.g. Onboarding), pass the subdomain of any institution your API user can access. Body (new_endpoint_id,active) unchanged. After moving a subscription withnew_endpoint_id, address it via the new endpoint id in the path.- Pagination: not applicable.
DELETE /webhook_endpoints/{id}/webhook_subscriptions/{subscription_id} - ✅ Unchanged (204)
DELETE /webhook_endpoints/{id}/webhook_subscriptions/{subscription_id} - ✅ Unchanged (204)- Pagination: not applicable.
7. New in v20240412 (no v2.2.2 equivalent)
Beyond the endpoints already covered above (/available_slots, /working_hours, /working_hour_labels, /insurance_coverages, the rebuilt financial endpoints, GET /procedures/{id}), v20240412 adds - all cursor-paginated on their index endpoints:
| Endpoint(s) | Purpose |
|---|---|
GET /adjustment_types, GET /adjustment_types/{id} | Adjustment types (needed for POST /adjustments) |
GET /payment_types, GET /payment_types/{id} | Payment types (needed for POST /payments) |
GET /claims, GET /claims/{id} | Insurance claims (includes: patient, provider, guarantor, charges, charge_payouts) |
GET /guarantor_balances, GET /guarantor_balances/{id} | Guarantor account balances (replaces the removed patient balance attribute) |
GET /insurance_balances, GET /insurance_balances/{id} | Insurance accounts-receivable balances |
GET /fee_schedules, GET /fee_schedules/{id}, GET /fee_schedule_procedures | Fee schedules and per-procedure fees |
GET /clinical_notes | Clinical notes for patient encounters |
GET /treatment_plans, GET /treatment_plans/{id} | Treatment plans (procedures reference them via treatment_plan_ids) |
GET/POST /onboardings, GET /onboardings/{hashid} | Programmatic location onboarding |
8. Migration checklist
- Switch the version header: replace
Accept: application/vnd.Nexhealth+json;version=2withNex-Api-Version: v20240412on every request. - Convert pagination loops on every list endpoint you use: drop
pageincrements, iterate withpage_info.end_cursor/has_next_page, and remove any reliance oncounttotals. - Replace list-endpoint
includes using the per-endpoint alternative tables (detail-endpoint includes or dedicated endpoints joined by foreign keys). - Appointments: switch
patient_id→patient_ids[]; ensure at least one oflocation_id/foreign_id; decide how to handle the new cancelled/deleted default (cancelled=falsefor active-only); stop expecting embeddedpatientobjects on the index; re-add server-side slot validation via/available_slotsif you relied on the working-hours booking rejection. - Scheduling: move
/appointment_slotscalls to/available_slotsand/availabilitiesCRUD to/working_hours(body keyavailability→working_hour;synced→source/label). - Patients: unwrap
data.patients→data, pick uppreferred_languageinstead ofpreferred_locale, source balances from/guarantor_balances, and re-point patient-coverage reads to/insurance_coverages. - Webhooks: re-create your subscriptions under
Nex-Api-Version: v20240412so payloads use the new entity shapes, then delete the v2.2.2-pinned subscriptions after cutover. - Sort params: re-check every
sortyou send - several endpoints changed fields/defaults (e.g. patients:
name/created_at→id/updated_at).
Based on the v2.2.2 and v20240412 API specifications as of 2026-07-09.