Guide
Find where a patient was treated
Run Site Sonar, read the visits, and fix a search that finds nothing.
Site Sonar searches claims data to find the facilities a patient has been seen at, and returns them as visits. Use it when you do not already know where to ask for records.
Turn on the search
Set siteSonar on the patient body you send to
POST /v0/companies/{companyId}/projects/{projectId}/patients:
curl https://api.hermeshealth.ai/v0/companies/COMPANY_ID/projects/PROJECT_ID/patients \
-X POST \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"firstName": "Jane",
"lastNames": [
"Doe"
],
"dateOfBirth": "1990-04-12",
"sex": "female",
"zipCodes": [
"94110",
"02139"
],
"siteSonar": true
}'
import requests
response = requests.post(
"https://api.hermeshealth.ai/v0/companies/COMPANY_ID/projects/PROJECT_ID/patients",
headers={"Authorization": "Bearer YOUR_API_KEY"},
json={
"firstName": "Jane",
"lastNames": [
"Doe",
],
"dateOfBirth": "1990-04-12",
"sex": "female",
"zipCodes": [
"94110",
"02139",
],
"siteSonar": True,
},
)
response.raise_for_status()
print(response.json())
const response = await fetch("https://api.hermeshealth.ai/v0/companies/COMPANY_ID/projects/PROJECT_ID/patients", {
method: "POST",
headers: {
Authorization: "Bearer YOUR_API_KEY",
"Content-Type": "application/json",
},
body: JSON.stringify({
"firstName": "Jane",
"lastNames": [
"Doe"
],
"dateOfBirth": "1990-04-12",
"sex": "female",
"zipCodes": [
"94110",
"02139"
],
"siteSonar": true
}),
});
if (!response.ok) throw new Error(`Request failed: ${response.status}`);
const data = await response.json();
Omitting the flag means off. Two related flags can be set independently on the
same body: patientHistories retrieves clinical detail from the same claims —
see Get clinical history for a patient —
and ehrSearch searches connected EHR networks.
Start a search on a patient you already created
Send the flag to
PATCH /v0/companies/{companyId}/projects/{projectId}/patients/{patientId}/search-level:
curl https://api.hermeshealth.ai/v0/companies/COMPANY_ID/projects/PROJECT_ID/patients/PATIENT_ID/search-level \
-X PATCH \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"siteSonar": true
}'
import requests
response = requests.patch(
"https://api.hermeshealth.ai/v0/companies/COMPANY_ID/projects/PROJECT_ID/patients/PATIENT_ID/search-level",
headers={"Authorization": "Bearer YOUR_API_KEY"},
json={
"siteSonar": True,
},
)
response.raise_for_status()
print(response.json())
const response = await fetch("https://api.hermeshealth.ai/v0/companies/COMPANY_ID/projects/PROJECT_ID/patients/PATIENT_ID/search-level", {
method: "PATCH",
headers: {
Authorization: "Bearer YOUR_API_KEY",
"Content-Type": "application/json",
},
body: JSON.stringify({
"siteSonar": true
}),
});
if (!response.ok) throw new Error(`Request failed: ${response.status}`);
const data = await response.json();
This takes the same flags with patch semantics, and the difference matters: an
omitted flag here keeps its current value, where an omitted flag on create
means off. Sending { "siteSonar": true } will not turn patientHistories
off.
Get the identity right
The search matches on who the patient was at the time of care, so two fields do most of the work.
lastNames is every last name the patient has used, current one first, then
maiden and prior married names. A hyphenated name is one entry with the hyphen
typed in (Smith-Jones), never two.
zipCodes is where the patient lived at the time of each visit, not where
they live now. Claims carry the address of the time, so a current ZIP finds
current-address care and misses the rest. Pass every ZIP you know of, oldest
included.
Optional details on the patient body, such as socialSecurityNumber, help the
search match more claims. Send them when you have them.
If the project’s
purposerequires an authorization, no search runs until one is uploaded and approved. See Add a patient.
Know when it finished
Poll the patient with
GET /v0/companies/{companyId}/projects/{projectId}/patients/{patientId}. The
response wraps the patient in a patient object, so read
patient.searchStatus, and the request flags as patient.siteSonar and so on:
curl https://api.hermeshealth.ai/v0/companies/COMPANY_ID/projects/PROJECT_ID/patients/PATIENT_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",
headers={"Authorization": "Bearer YOUR_API_KEY"},
)
response.raise_for_status()
print(response.json()["patient"]["searchStatus"])
const response = await fetch("https://api.hermeshealth.ai/v0/companies/COMPANY_ID/projects/PROJECT_ID/patients/PATIENT_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.patient.searchStatus);
searchStatus | Meaning |
|---|---|
Inactive | No search has been requested. |
Processing | A search is running. |
Completed | A search finished and found something. |
NoHit | A search finished and found nothing. |
Or subscribe a webhook to patient / columnChanged on siteSonar,
patientHistories and ehrSearch. Each flag changes from true to false
when its run finishes, and that change is the signal. See
Receive webhook events.
Don’t subscribe to
searchStatusalone. A repeat run that ends in the same status changes nothing, so it sends nothing.
In a sandbox project, a search finishes on its own in about 30 seconds.
Read the visits
Query them with POST /v0/visits/browse:
curl https://api.hermeshealth.ai/v0/visits/browse \
-X POST \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"where": {
"patient": {
"id": [
{
"equals": "<patient_id>"
}
]
},
"project": {
"id": [
{
"equals": "<project_id>"
}
]
}
},
"orderBy": [
{
"visit": {
"earliestServiceDate": "asc"
}
}
]
}'
import requests
response = requests.post(
"https://api.hermeshealth.ai/v0/visits/browse",
headers={"Authorization": "Bearer YOUR_API_KEY"},
json={
"where": {
"patient": {
"id": [
{
"equals": "<patient_id>",
},
],
},
"project": {
"id": [
{
"equals": "<project_id>",
},
],
},
},
"orderBy": [
{
"visit": {
"earliestServiceDate": "asc",
},
},
],
},
)
response.raise_for_status()
print(response.json()["table"])
const response = await fetch("https://api.hermeshealth.ai/v0/visits/browse", {
method: "POST",
headers: {
Authorization: "Bearer YOUR_API_KEY",
"Content-Type": "application/json",
},
body: JSON.stringify({
"where": {
"patient": {
"id": [
{
"equals": "<patient_id>"
}
]
},
"project": {
"id": [
{
"equals": "<project_id>"
}
]
}
},
"orderBy": [
{
"visit": {
"earliestServiceDate": "asc"
}
}
]
}),
});
if (!response.ok) throw new Error(`Request failed: ${response.status}`);
const data = await response.json();
console.log(data.table);
Rows carry visit, patient, site, payor, company and project. The
full query syntax is in Query large tables.
Take site.id from the visits you care about. It identifies the facility
when you request records, which is the reason to search at all: Hermes already
knows that site’s address, NPI and submission preferences, so a request built
from it needs nothing else.
Filter through the joined entity, not an ID column on the root:
site.id, notvisit.siteId. An unknown key is accepted and ignored, so the wrong one returns200with the wrong rows rather than an error.
When a search finds nothing
NoHit means the search ran and matched nothing. That is an answer, not a
failure, and re-running the same patient unchanged returns it again. Fix the
inputs, then request a new search with PATCH .../search-level.
Almost always it is one of the two identity problems above: the ZIPs are current rather than historical, or a maiden name is missing.
When the flag never clears
A run that finishes clears its flag from true to false. A run that
fails does not: the flag stays true, the run is retried quietly, and no
completion edge fires until an attempt succeeds.
So a patient sitting at siteSonar: true for far longer than a search normally
takes has not necessarily stalled — it may be on a later attempt. This is also
why a webhook on the flags can stay silent for a while.
If the project requires an authorization and none is uploaded, the flag is set
and the search never starts. Check authorizationStatus before assuming the
search itself is at fault.
Sandbox will not reproduce any of this: it auto-completes against sample data whatever names and ZIPs you send, so it proves your integration works but says nothing about whether your patient identity is good enough to match real claims.
Next steps
- Request records from a facility: ask a site you found for the patient’s documents.
- Get clinical history for a patient: read what happened at those visits.