POST /v4/patients/history reads the appointment and bill history of up to 25
charts at once. It exists for apps that decide something from a patient’s
whole history — a recall campaign, a gap-in-care check — where a missed row is
a wrong decision, and where waiting for a clinic-wide import to finish is not an
option.
Scopes: read:patients, plus read:appointments and/or read:bills for the
resources you ask for.
The recipe for one linked group (one person’s EZDerm and ModMed charts):
POST /v4/patients/history/refreshwith the group’s EZDerm charts (optional, see Per-patient refresh).POST /v4/patients/historywith every chart in the group and thecoveragewindow you care about.- Act only when
groups[].appointments.statusiscomplete. Otherwise look at each chart’sreadinessto see what is missing. - Stay current from
GET /v4/changes: a new booking on either chart arrives asappointment.changed; re-read that group.
Request
Unknown fields are a 400, not ignored. A cursor issued to another
organization, malformed, repeated, or naming a chart or resource not in this
request is a 400.
Response
{ "patientId": …, "found": false } and nothing else.
Linked charts are returned separately, each with its own EHR, MRN, facility ids
and contacts; nothing is merged. To find a group’s latest qualifying
appointment, take the latest across every member chart’s items, skipping rows
with removedFromEhrAt.
Consistency: snapshot, version, cursor
- One snapshot per response. Every chart and resource in a response is read
in one
REPEATABLE READtransaction atsnapshotAt. - Paging is per (chart, resource). Rows are ordered by id, which never
changes. A large chart returns
hasMore: trueand anextCursorwhile the others in the batch finish; send only the cursors you still need. versionis a content hash of every row in that chart’s resource at the snapshot (bills: bills plus coding groups, line items and diagnoses). Any insert, update or delete changes it, including writes that move no timestamp.- A cursor only continues the set it was cut from. If the version changed,
the resource returns
status: "snapshot_changed"with no items: restart that (chart, resource) without a cursor. A walk either reads one unchanged set end to end or is told to restart; it never skips or repeats a row silently. The hash covers sync bookkeeping too, so a chart being re-synced while you page can restart more than once. Most charts fit one page and never need a cursor.
Readiness
items: [] means “the mirror has no rows”. readiness says whether that can be
trusted. Paging to the end proves nothing about the EHR; only readiness does.
Appointments
Readiness rests on one of two kinds of evidence (basis):
window — facility-day sweeps. The appointment sync reads one facility-local
week per call (ModMed per facility, EZDerm per practice) and records each day of
the week only after every page of that week was read and written (an appointment
the EHR returns without a patient is not mirrored, so it cannot be part of any chart’s history). A week the
EHR did not answer records a failure on its days instead. The rolling sync reads
one week back through the booking horizon every five minutes; history older than
that is read by the historical backfill (see Rollout).
patient — per-patient refresh. A refresh that read
the chart’s whole history in one patient-scoped EHR call makes that chart
complete on its own for a day (stale after), whatever the facility sweeps
say — so one linked group can become ready while the rest of the clinic is
still importing. patientRefresh shows that evidence even when basis is
window.
asOf is the EHR state the evidence reflects: an EHR change after it is not
covered. excludes lists appointment kinds the sync never imports for this EHR
(virtual_encounters on EZDerm).
Bills
The bill sync keeps no completeness evidence, so the platform never calls a bill history complete:Groups
groups[] has one entry per linked identity among the requested charts. Its
status is the least ready member’s, or missing_members while a chart linked to
the identity in your organization was not in patientIds — a group’s history is
not whole until every member chart is read.
What the evidence does not guarantee
- A day that left the rolling sync’s one-week lookback is not re-read; an EHR change to an appointment more than a week after its date is not reflected until a backfill or per-patient refresh re-reads it.
- Only facilities linked to a live connector are swept. A ModMed facility no
connector is linked to is outside
scopes; a facility whose only connector is disconnected reads as not covered. - EZDerm sweeps and refreshes request non-virtual encounters only (
excludes). - EZDerm’s encounter search is unpaginated with an undocumented cap. A week of
400+ results is re-read day by day; a day of 400+, or a patient with 400+, is
written but never verified (
possible-truncation). - A sweep that crashes records nothing for the weeks it did not finish; their
verification stops advancing and
asOf/liveAsOfshow it.
Appointments the EHR no longer has
When a fully read week (or a patient refresh) no longer returns a mirrored appointment, the row is kept withremovedFromEhrAt set, and an
appointment.removed change is published. A later read that returns it again
clears the field. A rescheduled appointment can read as removed until the week
it moved to is read. Do not count removed rows as visits or bookings.
Per-patient refresh
- Scope
read:appointments. Up to 25 charts.already_queuedwhile a refresh for the chart is pending (a pending refresh older than ten minutes may be queued again). Foreign charts answernot_found. - EZDerm only. The refresh uses EZDerm’s patient-scoped encounter search,
the call the platform already uses for a patient’s upcoming appointments.
ModMed has no patient-scoped appointment call verified against live traffic,
so ModMed charts are always
unsupportedand rely on facility sweeps plus the historical backfill. - Whose login reads. The body may carry
sessionToken, the App Bridge session token of the logged-in user making the request. The refresh picks which EHR login reads the chart, in order: your organization’s office (system) EZDerm connection on the patient’s practice; if it has none live, the personal EZDerm connection of the logged-in user who made the request, when you send their App BridgesessionToken(verified; a token that does not verify for your app and organization is a401for the whole request, and nothing is queued); otherwise none. Another staff member’s personal connection, and the practice’s autosubmission connection, are never used. With none, the chart is stillqueuedhere and its refresh then fails withno-live-connector. A read through a personal connection writes what that login can see but never marks appointments removed and never makes the chartcomplete: readiness reportsfailedwithlastFailureReasonread-through-personal-login. That is final for that login; retrying reads the same partial view again. - A chart whose refresh is still pending answers
already_queued, whoever queued it; that run reads with the login it was queued with. Once it has run, a request with yoursessionTokenqueues a new read. - An empty answer for a chart the mirror holds appointments for is treated as a
failure (
empty-result-contradicts-mirror), never as a wiped history. - The refresh writes every returned appointment, marks the chart’s mirrored appointments the EHR no longer returns, then records the evidence. If it crashes before the last step the chart stays unverified; re-running is safe.
Identity
chart is one EHR chart. Charts of the same person share masterPatientId and
list each other in duplicatePatientIds; identityLinks gives the evidence for
each link:
Shared phone or email alone never links charts. MRNs are per EHR and are not
used to link charts across EHRs. Existing links are not rewritten; review
legacy_unverified links in the patient registry.
A link or unlink changes every member chart’s updatedAt and publishes
patient.identity_changed for each on GET /v4/changes.
Charts waiting for identity review
identityReview says whether a chart is linked, pending_review or unmatched.
A chart is pending_review when matching found identities it would not link on its
own: some identifiers agreed and others did not, for example the same name and date
of birth with a different phone. It stays unlinked, with no masterPatientId, until
a person decides. candidates names the identities in your organization it might
belong to and which identifiers agreed (name_dob, phone, email). A candidate is
not a link: do not merge charts on it, and treat a phone or email match without
name_dob as a possible relative. A chart may be pending_review with no candidates
listed.
A reviewer’s decision arrives as an ordinary link: the chart joins the group with
basis manual, and every member gets a new updatedAt and a
patient.identity_changed event.
Contacts: with include=contact on GET /v4/patients/{id}, each phone number
carries source (ehr_chart or intake) and sourceUpdatedAt. Numbers are
never copied between linked charts, so you can prefer one EHR’s chart.
Linked charts can disagree on phone, email and address. Being linked says the
charts are the same person; it does not say which chart’s contact details are
current. The API does not tell you which contact is newer:
updatedAtand anehr_chartnumber’ssourceUpdatedAtadvance together for every member of a group when the group changes, and for any demographic change on the chart. They order chart versions, not contact edits.- No field records when the EHR last changed a contact, or which contact the practice prefers.
Reviewed contact sources
When linked charts disagree, a person can record which chart holds the current phone, email or mailing address. Withinclude=contact (which needs
read:patient_pii), GET /v4/patients/{id} and GET /v4/patients return:
identityMembershipVersion (also returned with include=identity and on every
history chart) changes whenever a chart joins or leaves the identity.
contactVersions is a version of this chart’s own contact values. Both are opaque:
compare them as strings.
unresolved: no reviewed choice is recorded. This is the normal state for most identities and does not mean the contact needs review: where the charts agree, or your own rule clearly prefers one chart, use it. A review is only for contacts the charts dispute. Never settle a dispute by chart id orupdatedAt.resolved: the choice still holds. The source chart is still a member, the identity’s members are the ones the reviewer saw, and the source chart’s value is the one reviewed.valueis that chart’s current value.stale: the choice no longer holds (source_unlinked,membership_changed,value_missingorvalue_changed). Treat it as unresolved until a person reviews it again.
decisionId, valueVersion and membershipVersion. Use them
only while the same response shows status: "resolved" and membershipVersion
equal to identityMembershipVersion. A choice is the same on every chart of the
identity and appears only to your organization. It records where the contact
comes from, not consent: your opt-out and do-not-contact rules still apply.
Nothing is chosen automatically, including for charts linked before this existed.
Limits
- 25 charts per request, 200 rows per chart per resource, 2,500 rows per response.
- Requests count against the app’s normal rate limit.
Rollout and backfill
- On deploy the coverage grid is empty: every chart reads
not_importeduntil the rolling sync’s first verified weeks (within five minutes per connector), and history older than the lookback readspartialuntil backfilled. - Historical backfill is the
appointments-coverage-backfilltask, run per connector with{ "connectorId", "from": "YYYY-MM-DD" }. It reads unverified weeks newest-first, four per run, re-queues itself, resumes from the grid after any interruption, and reportsstalledweeks the EHR refused three times. It is started by an operator, not on a schedule. - EZDerm groups can be made ready immediately with a per-patient refresh, without waiting for the backfill.
Not provided
- Bill completeness. No connector records bill discovery completeness.
- ModMed per-patient history. Needs a verified patient-scoped appointment
endpoint (the spec shows
GET /ema/ws/v3/appointments?where=patient==…, but it is unverified against live data). - Notes are not part of this endpoint.
