Skip to content
Hermes Health API
API referenceOpenAPI spec

Guide

Request records from a facility

Create a request, follow it to completion, and download what came back.

A record request asks one facility for one patient’s records, bounded by a range of service dates. A search discovers data; a request retrieves documents. A patient usually needs several requests, one for each place they were treated.

Create the request

Create it with POST /v0/companies/{companyId}/projects/{projectId}/patients/{patientId}/record-requests:

Create the request
curl
curl https://api.hermeshealth.ai/v0/companies/COMPANY_ID/projects/PROJECT_ID/patients/PATIENT_ID/record-requests \
  -X POST \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "siteId": "<site_id>",
  "medicalRecord": true,
  "billing": false,
  "imaging": false,
  "ccda": false,
  "startDateOfService": "2024-01-15",
  "endDateOfService": "2024-02-20"
}'
import requests

response = requests.post(
    "https://api.hermeshealth.ai/v0/companies/COMPANY_ID/projects/PROJECT_ID/patients/PATIENT_ID/record-requests",
    headers={"Authorization": "Bearer YOUR_API_KEY"},
    json={
        "siteId": "<site_id>",
        "medicalRecord": True,
        "billing": False,
        "imaging": False,
        "ccda": False,
        "startDateOfService": "2024-01-15",
        "endDateOfService": "2024-02-20",
    },
)
response.raise_for_status()
print(response.json()["requests"])
const response = await fetch("https://api.hermeshealth.ai/v0/companies/COMPANY_ID/projects/PROJECT_ID/patients/PATIENT_ID/record-requests", {
    method: "POST",
    headers: {
        Authorization: "Bearer YOUR_API_KEY",
        "Content-Type": "application/json",
    },
    body: JSON.stringify({
      "siteId": "<site_id>",
      "medicalRecord": true,
      "billing": false,
      "imaging": false,
      "ccda": false,
      "startDateOfService": "2024-01-15",
      "endDateOfService": "2024-02-20"
    }),
});
if (!response.ok) throw new Error(`Request failed: ${response.status}`);
const data = await response.json();
console.log(data.requests);

medicalRecord, billing, imaging and both service dates are required. ccda is optional and defaults to false.

The response is RecordRequestMany { requests: [...] }, with one entry per flag you turned on: each record type goes to the facility as its own request. Keep every request.id.

Identify the facility

Two ways.

By siteId (preferred), taken from a visit row after a search. See Find where a patient was treated. Hermes already knows that site’s address, NPI and submission preferences.

By name and address, when you already know where to ask and have no site ID. Pass siteName, siteAddressLine1, siteCity, siteState and siteZip (plus optional siteAddressLine2), and Hermes resolves the facility.

Track it

Poll each request with GET /v0/companies/{companyId}/projects/{projectId}/patients/{patientId}/record-requests/{recordRequestId}:

Poll a record request
curl
curl https://api.hermeshealth.ai/v0/companies/COMPANY_ID/projects/PROJECT_ID/patients/PATIENT_ID/record-requests/RECORD_REQUEST_ID \
  -H "Authorization: Bearer YOUR_API_KEY"
import requests

response = requests.get(
    "https://api.hermeshealth.ai/v0/companies/COMPANY_ID/projects/PROJECT_ID/patients/PATIENT_ID/record-requests/RECORD_REQUEST_ID",
    headers={"Authorization": "Bearer YOUR_API_KEY"},
)
response.raise_for_status()
print(response.json()["request"]["status"])
const response = await fetch("https://api.hermeshealth.ai/v0/companies/COMPANY_ID/projects/PROJECT_ID/patients/PATIENT_ID/record-requests/RECORD_REQUEST_ID", {
    method: "GET",
    headers: {
        Authorization: "Bearer YOUR_API_KEY",
    },
});
if (!response.ok) throw new Error(`Request failed: ${response.status}`);
const data = await response.json();
console.log(data.request.status);

request.status is one of twenty values rolling up into six categories, which is the useful way to read them.

CategoryStatusesMeaning
PendingPending, InfoConfirmed, SiteConfirmed, MailOnly, ResubmittedNot yet with the facility.
ProcessingRequestSent, RequestConfirmed, NeedToReviewResultsWith the facility, waiting on them, or records arrived and are being reviewed.
Needs resolutionAuthDenied, PaymentPending, NeedSiteSpecificAuthBlocked on an action.
CompletedCompleted, PartialRecords came back.
Records not foundPatientNotFound, ServiceDatesNotFound, SiteClosed, FacilityRefusal, NoRecordFoundFinished with nothing.
CancelledCanceled, Void, DuplicateRequest, WrongSiteNameStopped.

Eleven are terminal: Completed, Partial, Canceled, Void, DuplicateRequest, WrongSiteName, PatientNotFound, ServiceDatesNotFound, SiteClosed, FacilityRefusal, NoRecordFound.

Test the category, not one value. Partial is terminal and carries real deliverables, so code waiting for exactly Completed waits forever on a request whose documents already arrived. And AuthDenied is not terminal: the request moves again once the authorization is fixed.

Be told instead of polling

Subscribe a webhook to recordRequest / columnChanged on status. Hermes posts on every transition and data.status carries the new value.

The payload holds only your subscribed columns, not the full request. When the status reaches the Completed category, GET the path in the payload’s resource field to read the deliverables. See Receive webhook events.

Download the deliverables

Once the status is Completed or Partial, the deliverables array on that same GET is populated. There is no separate fetch: the polling response is the download manifest.

Each entry carries a download url. It is valid for about an hour, so download from it rather than storing it.

Send no Authorization header to a deliverable url. The URL is already signed, and an extra header makes the download fail.

Two fields hold documents, and they are different things:

  • deliverables (plural) lists every raw document that came back.
  • deliverable (singular) is the one combined chart Hermes generates, once it has published one, downloaded as EHR_Charts_Combined.pdf. It is not the first entry of the array.

The listing presents the request the way the web app does, not the way S3 stores it. Three rules follow from that:

FieldWhat it isAddress a file with it?
fileNameUnique across the request; a repeat name reads as CCDA_80154372 (2).xmlYes: GET/HEAD/DELETE .../deliverables/{fileName}
folderThe display folder, /-joined, omitted at the top levelNo. It is for display and grouping only.
keyThe real S3 keyNo, though it is unchanged: nothing here moves a stored file.

Renaming is the one exception. PATCH .../deliverables/{path} takes the stored path, which is the part of key after /deliverables/ and may have several segments, because it moves the file rather than reading it.

When a request fails

Three are blocked rather than finished, and can move again:

  • AuthDenied: the facility rejected the authorization on file. Usually a missing signature, an expired date, or a form that does not name the facility. Fix it and the request proceeds.
  • NeedSiteSpecificAuth: this facility wants its own authorization form.
  • PaymentPending: the facility charges for records and has not been paid.

Five are terminal and produce nothing:

  • PatientNotFound: no record of this patient there, or the identity you sent does not match theirs.
  • ServiceDatesNotFound: they have the patient but nothing in your date range. Widen it and raise a new request.
  • SiteClosed: the facility has shut down. Records may sit with a successor organisation, which is a different site and a new request.
  • FacilityRefusal: they declined to release.
  • NoRecordFound: the request reached the right place and came back empty.

Four are stopped:

  • WrongSiteName: the facility named is not the one holding the records. Raise a new request against the right site, using a siteId if you have one.
  • DuplicateRequest: an identical request already exists. Find the original.
  • Canceled: cancelled.
  • Void: voided, and will not be worked.

Partial is not a failure. It is terminal, in the Completed category, and the facility sent some of what was asked for. Those deliverables are all you will get from that request; chase the gap with a new one.

Watch many at once

POST /v0/record-requests/browse takes the same query syntax as the clinical tables (see Query large tables), so one call finds everything sitting in Needs resolution across a project. That is the set worth a human’s attention, since the terminal ones will not change whatever you do.

In a sandbox project

Requests auto-complete shortly after creation with a stub deliverable, so the whole path (create, poll, read the array, download a file) can be proven before a real facility is contacted.

Next steps