Skip to main content
Two reference endpoints back the pickers a scheduling or reactivation app needs:
  • GET /v4/appointment-types
  • GET /v4/cancel-reasons
Both are org-scoped, unpaginated (a practice configures tens of these, not thousands), and require the read:appointments scope.

Appointment types

Pass ?inUseOnly=true to get only the types that appear on at least one synced appointment — useful when a picker should not offer a filter that would match nothing. Pass ?activeOnly=true to get only the types the practice can still book. Retired types are returned by default, because historical appointments still reference them and a picker that cannot name one renders that appointment’s type as blank. ModMed only — EZDerm reports no status, so EZDerm types have active: null and are excluded by this filter.

Completeness differs by EHR

For EZDerm, a type nobody has booked inside the sync window is absent from the catalogue. Treat the EZDerm list as a floor, not a complete configuration dump.

Filtering appointments by type

Filter on id, not externalId.externalId is the EHR’s own id, and ModMed hands out practice-local integers like 6054. On an organization with two connected EHR instances the same externalId names two different types, so a filter keyed on it would return the other location’s appointments too. id is Max AI’s own id and is unique.Ids that no longer resolve are ignored. If none of the ids in a filter resolve, the result is empty — not unfiltered. That is deliberate: silently dropping a broken filter would turn “these three appointment types” into “every appointment in the practice”.
On an appointment, appointmentType.externalId matches this catalogue’s externalId. For an organization with a single connected EHR instance — the common case — that mapping is exact. With two instances, two catalogue rows can share an externalId. The appointment carries instanceId for exactly this reason: pair it with appointmentType.externalId and match against the catalogue row’s own instanceId + externalId. Filtering still takes the catalogue id, which is unique on its own.
lastSyncedAt on these two catalogues is a last-seen-at, not a changed-at: it advances on every sync in which the type is still present in the practice’s configuration, so you can tell a stale catalogue from a healthy one. A RETIRED type stops being observed, so its stamp freezes at the last sync that saw it — do not read min(lastSyncedAt) across the catalogue as connector staleness. That is the opposite of lastSyncedAt on an appointment, which only moves when the row’s data actually changed. Do not use it to detect catalogue edits — compare name / active instead.

Cancellation reasons

Retired reasons are returned by default — historical appointments still reference them, and a picker that cannot name one would render that appointment’s reason blank. Pass ?activeOnly=true for only the reasons the practice can currently select. Sourced from ModMed’s cancel-reason configuration. EZDerm exposes no equivalent reference surface, so an EZDerm-only organization gets an empty list.

Cancellation reasons on an appointment: current limitation

Today cancelReason is null on every scraped appointment.Neither ModMed’s scheduler feed nor EZDerm’s encounter feed returns a cancellation reason when reading appointments — ModMed only accepts a reason on the write (cancel) call. So a null here means “not reported by the EHR”, not “cancelled for no reason”.What this means for you:
  • You can configure which reasons should suppress outreach now, from GET /cancel-reasons. That configuration starts taking effect automatically the day a vendor read feed carries the value — no client change needed.
  • Do not write a suppression rule that treats null as a specific reason (e.g. “null means the patient cancelled”). It would misfire on every appointment today and change meaning later.
  • ?cancelReasonIds= filters correctly, but currently matches nothing, for the same reason.