Skip to main content
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):
  1. POST /v4/patients/history/refresh with the group’s EZDerm charts (optional, see Per-patient refresh).
  2. POST /v4/patients/history with every chart in the group and the coverage window you care about.
  3. Act only when groups[].appointments.status is complete. Otherwise look at each chart’s readiness to see what is missing.
  4. Stay current from GET /v4/changes: a new booking on either chart arrives as appointment.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

A chart outside your organization and a chart that does not exist both answer { "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 READ transaction at snapshotAt.
  • Paging is per (chart, resource). Rows are ordered by id, which never changes. A large chart returns hasMore: true and a nextCursor while the others in the batch finish; send only the cursors you still need.
  • version is 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/liveAsOf show 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 with removedFromEhrAt 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_queued while a refresh for the chart is pending (a pending refresh older than ten minutes may be queued again). Foreign charts answer not_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 unsupported and 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 Bridge sessionToken (verified; a token that does not verify for your app and organization is a 401 for 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 still queued here and its refresh then fails with no-live-connector. A read through a personal connection writes what that login can see but never marks appointments removed and never makes the chart complete: readiness reports failed with lastFailureReason read-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 your sessionToken queues 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:
  • updatedAt and an ehr_chart number’s sourceUpdatedAt advance 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.
Keep each chart’s contacts separate. Do not pick a current contact by matching phone numbers or by ordering chart ids.

Reviewed contact sources

When linked charts disagree, a person can record which chart holds the current phone, email or mailing address. With include=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 or updatedAt.
  • 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. value is that chart’s current value.
  • stale: the choice no longer holds (source_unlinked, membership_changed, value_missing or value_changed). Treat it as unresolved until a person reviews it again.
To pin a choice, keep 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_imported until the rolling sync’s first verified weeks (within five minutes per connector), and history older than the lookback reads partial until backfilled.
  • Historical backfill is the appointments-coverage-backfill task, 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 reports stalled weeks 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.