- A patient has one
idat each clinic. Every other route that asks for apatient_idwants this value. It is not the clinic’s medical record number (themrn), which is a separate, human-friendly label. - Nothing is ever deleted. You can create, change and archive a patient. There is no delete route.
- Duplicates are flagged, not blocked. Real front desks create the same person twice. We create the record and tell staff to review it, rather than guess.
Who can use it
This is patient information, so it has more conditions than most routes. Read Patient data access first: it explains the API add-on, the agreement, and for an organization the approval for patient data. Then Permissions for how a key gets its scopes.
A test key (
ehr_test_…) reaches only your workspace’s sandbox, which holds invented patients, so you can try
every request on this page without touching a real chart. See Authentication.
A patient the clinic has restricted never appears in a list or a search. Asking for one directly, or one in another
clinic, answers 404 with code: "not_found": absent and not-permitted look identical on purpose. See Errors.
What a patient looks like
id, first_name and last_name are always present. Everything else can be null. The fields you will use most:
Allergies, current medications and chronic conditions are returned when you read a patient, but you cannot write
them here. They are the clinic’s reviewed clinical record. Send allergies with Allergies and
conditions with Conditions; a clinician confirms each before it reaches the chart. Sending one of these fields on a create, an update or an import is
refused, naming the field.
List patients
Returns the patients your key may see, a page at a time. There is deliberately no search by name: a name search over an API would be a directory of people. Find a specific person with the exact filters below, or with Search by identifiers.GET /v1/clinics/{clinic_id}/patients
Create a patient
Adds a patient. Send anIdempotency-Key header, or the request is refused. See
Retries and duplicates.
POST /v1/clinics/{clinic_id}/patients
A field that is not listed is refused with
invalid_request, naming it. That includes every read-only field
(mrn, client_status, insurance, pharmacies, care_providers, couple, portal_access_enabled,
admission_status, profile_image), and allergies, current_medications and chronic_conditions.
notification_settings is refused with its own code, notification_settings_not_writable: it is the patient’s own consent record.
duplicate_candidates appears only when the clinic already has someone who looks like a match. The patient is
created anyway. Show the match to a person, do not try to merge. Retrying with the same key returns the same patient and does not create a second one.
Read one patient
GET /v1/clinics/{clinic_id}/patients/{patient_id}
Change a patient
Send only what changes. A field you leave out is left alone. A field you send as JSONnull is cleared, where the
field can be empty (first_name and last_name can never be cleared; that is refused naming the field). emails,
phones and guardians replace the whole set when present, and next_of_kin, employment and billing replace as a whole.
PATCH /v1/clinics/{clinic_id}/patients/{patient_id}
run_automations. Differences: client_type can be
adult or minor but never couple, and mrn, client_status and the other read-only fields are refused. An update
needs no Idempotency-Key: sending the same change twice leaves the same result.
Archive a patient
Archiving marks the patient as no longer current, the same as the clinic’s own archive button. It never removes a record. Archiving someone who is already archived succeeds again, so it is safe to retry. There is no request body.POST /v1/clinics/{clinic_id}/patients/{patient_id}/archive
See possible duplicates
When a create or an import finds a close match, the clinic’s staff are shown the pair. This route lists those pairs. It returns ids and a similarity score only, never a name. Read each side with Read one patient.GET /v1/clinics/{clinic_id}/patients/duplicate_flags
Search by identifiers
Looks for an exact match on two or more different identifiers. Use it when you hold an email and a phone number (or a record number) and want to know whether the person is already a patient. The identifiers go in the body, never in a URL.POST /v1/clinics/{clinic_id}/patients/search
At most five patients come back. There is no name-only or partial match: that is what stops this route being used to
walk through the clinic’s patients one field at a time.
data list means “no match” or “not allowed to see the match”, and the two are never told apart.
It is 200, not 404.
Import a list of patients
Brings in up to 100 patients at once from another system. Only a clinic’s own key may import; an organization connected to the clinic may not (it receivesnot_found). The call needs patients:write, the API add-on and the accepted agreement.
POST /v1/clinics/{clinic_id}/patient-imports
The whole file is checked before anything is written. If every row is valid, the import runs at once and the answer
is the finished job. If any row fails, nothing is written and the job comes back as
needs_review with a
row_errors list (row number, field, reason; never the value). Fix the file and send it again, or choose to import
just the rows that passed (see below). No Idempotency-Key is needed: a file with exactly the same content that was already imported is refused with
already_imported, so retrying a timed-out call is safe. An import never contacts a patient and never starts an
automation. A possible duplicate is still created and counted in flagged_duplicate.
status is validating, needs_review, running, succeeded or failed. A row that carries allergies,
current_medications or chronic_conditions fails like any other invalid row. Files larger than 10 MiB, or with more than 100 rows, are refused before any row is read (413, payload_too_large, for size).
List import jobs
The clinic’s import jobs, oldest first. It shows progress and outcome only, never a file or a patient.GET /v1/clinics/{clinic_id}/patient-imports
The answer is
{ "data": [ …jobs… ], "has_more": false, "next_cursor": null }, each job shaped as above.
Read one import job
GET /v1/clinics/{clinic_id}/patient-imports/{job_id}
not_found.
Import only the rows that passed
For a job inneeds_review, this imports the valid rows and leaves the failed ones out. It is a deliberate choice,
recorded on the job. There is no request body. The failed rows stay listed in row_errors for reference.
POST /v1/clinics/{clinic_id}/patient-imports/{job_id}/run-valid
needs_review, this route refuses (see below).
Things to know
Refusals you may meet
Every error has the shape described in Errors.
Idempotency.
POST create needs an Idempotency-Key. PATCH, archive and the import routes do not. See
Retries and duplicates.
Pagination. The list, the duplicate list and the import-job list use limit, starting_after, has_more and
next_cursor. See Pagination.
FHIR. Add Accept: application/fhir+json to the list or the single read and you get a Patient (or a Bundle of
them). You can also create (POST) or change (PATCH) a patient by sending a FHIR Patient with
Content-Type: application/fhir+json; the answer is still our JSON shape. Some FHIR elements that have no home in a
clinic record (for example deceasedBoolean or Patient.link) are refused rather than dropped. The import also
accepts a FHIR Bundle. See FHIR R4.
Organization keys and connections. An organization key sees only the patients its connection reaches. See
Connections.
Common tasks
Find a patient, or create one if they are newPOST …/patients/searchwith their email and phone.- If
datahas an item, use itsid. - If
datais empty,POST …/patientswith a freshIdempotency-Key. If the answer carriesduplicate_candidates, show them to a person.
- Page through
GET …/patients?updated_since=LAST_TIMEuntilhas_moreisfalse. - Store the time you started the run, and use it next time.
PATCH …/patients/PATIENT_IDwith the newphonesset. Remember the array replaces the whole set, so send every number you want to keep.
POST …/patient-importswithformatanddata.- If the job is
succeeded, you are done. If it isneeds_review, readrow_errors, fix the file and send it again, orPOST …/run-validto bring in only the good rows.