Skip to content
Hermes Health API
API referenceOpenAPI spec

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.

Start a search on a patient you already created

Send the flag to PATCH /v0/companies/{companyId}/projects/{projectId}/patients/{patientId}/search-level:

Start a search
curl
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 purpose requires 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:

Poll the patient
curl
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);
searchStatusMeaning
InactiveNo search has been requested.
ProcessingA search is running.
CompletedA search finished and found something.
NoHitA 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 searchStatus alone. 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:

Read the visits
curl
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, not visit.siteId. An unknown key is accepted and ignored, so the wrong one returns 200 with 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