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:
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}:
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.
| Category | Statuses | Meaning |
|---|---|---|
| Pending | Pending, InfoConfirmed, SiteConfirmed, MailOnly, Resubmitted | Not yet with the facility. |
| Processing | RequestSent, RequestConfirmed, NeedToReviewResults | With the facility, waiting on them, or records arrived and are being reviewed. |
| Needs resolution | AuthDenied, PaymentPending, NeedSiteSpecificAuth | Blocked on an action. |
| Completed | Completed, Partial | Records came back. |
| Records not found | PatientNotFound, ServiceDatesNotFound, SiteClosed, FacilityRefusal, NoRecordFound | Finished with nothing. |
| Cancelled | Canceled, Void, DuplicateRequest, WrongSiteName | Stopped. |
Eleven are terminal: Completed, Partial, Canceled, Void,
DuplicateRequest, WrongSiteName, PatientNotFound, ServiceDatesNotFound,
SiteClosed, FacilityRefusal, NoRecordFound.
Test the category, not one value.
Partialis terminal and carries real deliverables, so code waiting for exactlyCompletedwaits forever on a request whose documents already arrived. AndAuthDeniedis 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
Authorizationheader to a deliverableurl. 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 asEHR_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:
| Field | What it is | Address a file with it? |
|---|---|---|
fileName | Unique across the request; a repeat name reads as CCDA_80154372 (2).xml | Yes: GET/HEAD/DELETE .../deliverables/{fileName} |
folder | The display folder, /-joined, omitted at the top level | No. It is for display and grouping only. |
key | The real S3 key | No, 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 asiteIdif 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
- Receive webhook events: be told about every status change instead of polling.
- Query large tables: the query syntax behind
POST /v0/record-requests/browse.