Skip to main content
A patient is a person a clinic keeps a record for: their name, how to reach them, a few details about their health and a few about how the clinic bills them. Almost everything else a clinic stores (visits, allergies, appointments, invoices) hangs off a patient, so this is usually the first resource an integration touches. If you have never worked with health records before, three ideas will save you time:
  • A patient has one id at each clinic. Every other route that asks for a patient_id wants this value. It is not the clinic’s medical record number (the mrn), 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

Only 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
(Each item is a full patient, shortened here.)

Create a patient

Adds a patient. Send an Idempotency-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 JSON null 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}
The body accepts the same fields as a create (all optional), except 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
The API cannot merge two patients. Staff decide that in the product. A test key always sees an empty list here.

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.
An empty 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 receives not_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.
A job’s 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}
A job that does not exist, or belongs to another clinic, answers not_found.

Import only the rows that passed

For a job in needs_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
The files behind a job are kept for 7 days. After that, or when the job is not in 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 new
  1. POST …/patients/search with their email and phone.
  2. If data has an item, use its id.
  3. If data is empty, POST …/patients with a fresh Idempotency-Key. If the answer carries duplicate_candidates, show them to a person.
Keep your own copy in step
  1. Page through GET …/patients?updated_since=LAST_TIME until has_more is false.
  2. Store the time you started the run, and use it next time.
Fix someone’s contact details
  1. PATCH …/patients/PATIENT_ID with the new phones set. Remember the array replaces the whole set, so send every number you want to keep.
Move a clinic’s old list across
  1. POST …/patient-imports with format and data.
  2. If the job is succeeded, you are done. If it is needs_review, read row_errors, fix the file and send it again, or POST …/run-valid to bring in only the good rows.
Book their next visit. Continue with Appointments.