Partner API · v1
The home side of the chart, as an API.
Facilities, home-health agencies and health plans can read what a family records at home — glucose, blood pressure, weight, SpO₂, temperature, sleep, the medication list, the care plan, visit notes — and push their own readings and documents back. Only for patients whose families explicitly shared with you, only the scopes they chose, with every call logged.
Overview
Base URL https://www.expressagain.com/api/partner/v1. JSON in, JSON out, UTF-8. Every response carries an X-Request-Id — quote it when you write to us. Times are ISO 8601 UTC; glucose grid cells the family typed with a clock time come back as the family's local date-time with local: true. Units are fixed and US: mg/dL, mmHg, lb, %, °F, hours.
Consent-first
A family grants your organization access by share code and picks the scopes. No grant, no data — not even a 403 that says the patient exists.
Read and write
Read what the family and their aides log; write readings that show up as “from <your organization>”; push PDFs.
Audited and metered
Every call is logged (no PHI in the log) and counted per organization per day.
Consent model
- Your organization signs the BAA and receives a share code (six characters, like
7KQ2MW) and an API key. - A family opens Settings → Privacy → Share with an organization, types your code, sees your name, and picks what you may do: read readings, read medications, read the care plan, read visits (agency on record only), add readings, send documents. They also choose how much of the patient's name you see.
- The patient appears in
GET /patientsunder an opaque id (pt_…) that is stable for your organization and different for every other organization — partners cannot correlate patients by id. - The family can revoke at any time. Access ends immediately; you receive a
grant.revokedevent if you subscribed.
What a call may do is the intersection of the key's scopes and the grant's scopes. A missing scope answers 403 insufficient_scope and says whether the key or the family is the reason.
Authentication
Send your key as a bearer token. Keys look like ea_live_ followed by 40 characters, are shown once when issued, and are stored hashed (SHA-256) on our side. Rotate by asking for a new key and revoking the old one.
curl https://www.expressagain.com/api/partner/v1/me \
-H "Authorization: Bearer ea_live_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"
{
"apiVersion": "v1",
"organization": { "id": "…", "name": "Sunrise Home Health", "kind": "agency", "shareCode": "7KQ2MW" },
"plan": { "name": "standard", "includedCallsPerMonth": 250000, "includedPatients": 250 },
"key": { "id": "…", "scopes": ["read:observations", "write:observations", "webhooks"] },
"usage": { "callsToday": 41, "callsThisMonth": 1203, "patients": 12, "keys": 1, "webhooks": 1 },
"rateLimit": { "perMinute": 600 }
}Endpoints
| Method | Path | Scope | What |
|---|---|---|---|
| GET | /me | — | Your organization, plan, the key's scopes and this month's metered usage. |
| GET | /patients | — | Every family that granted your organization access: opaque id, name as the family allows, granted scopes. |
| GET | /patients/{id}/observations | read:observations | Readings from the family's own Health Tracking. ?type=glucose|bp|weight|spo2|temp|sleep &since= &until= &limit= |
| POST | /patients/{id}/observations | write:observations | Add one reading. It appears in the family's Health Tracking as "from <your organization>" and goes to the care team like any other reading. |
| GET | /patients/{id}/medications | read:medications | The structured medication rows and allergies the family keeps in Medical Info. |
| GET | /patients/{id}/care-plan | read:care-plan | The family's typed targets, the care team's instructions, and her day (the task sheet). |
| GET | /patients/{id}/visits | read:visits | The visit flow sheet and clock-ins/outs — only when the family marked you as their agency on record (403 otherwise). |
| POST | /patients/{id}/documents | write:documents | A PDF (base64, ≤ 5 MB) into the family's app. 501 on a deployment without document storage. |
| GET · POST | /webhooks | webhooks | List / register delivery URLs. The signing secret is returned once. |
| GET · DELETE | /webhooks/{id} | webhooks | One webhook with its last 20 deliveries; remove it. |
| GET | /fhir/Observation | read:observations | Beta — the same readings as a FHIR R4 searchset Bundle. ?patient= &code=<LOINC> &date=ge… &_count= |
List the patients who shared with you
GET https://www.expressagain.com/api/partner/v1/patients
{
"patients": [
{ "id": "pt_5f1c9e2ab7d0c4e8a19f3b62", "name": "Debbie", "scopes": ["read:observations", "read:medications", "write:observations"], "grantedAt": "2026-09-24T14:02:11.000Z", "agencyOnRecord": true }
],
"count": 1
}Read readings
GET https://www.expressagain.com/api/partner/v1/patients/pt_5f1c…/observations?type=glucose&since=2026-09-17T00:00:00Z
{
"patient": "pt_5f1c…",
"observations": [
{ "id": "g_…", "type": "glucose", "takenAt": "2026-09-23T07:30:00", "local": true, "value": 128, "unit": "mg/dL", "slot": "bb", "note": "", "recordedBy": "Dee", "recordedByKind": "owner", "source": "app" },
{ "id": "v_…", "type": "glucose", "takenAt": "2026-09-22T20:05:12.000Z", "value": 164, "unit": "mg/dL", "note": "from Sunrise Home Health", "recordedBy": "Sunrise Home Health", "recordedByKind": "partner", "source": "app" }
],
"count": 2,
"query": { "type": "glucose", "since": "2026-09-17T00:00:00.000Z", "until": null, "limit": 200 }
}Blood pressure rows carry systolic, diastolic and pulse; sleep rows may be an event (bed, wake, up) with no value. The app records and displays; it never interprets a reading — targets are the family's own words, on the care plan.
Write a reading
POST https://www.expressagain.com/api/partner/v1/patients/pt_5f1c…/observations
Content-Type: application/json
{ "type": "bp", "systolic": 132, "diastolic": 84, "pulse": 71, "takenAt": "2026-09-24T13:10:00Z", "note": "RN visit", "externalId": "visit-8812-bp" }
201 { "ok": true, "id": "v_…", "created": true, "summary": "BP 132/84, pulse 71 at 9:10 AM", "flagged": false, "recordedAs": "from Sunrise Home Health" }Bounds are the app's own typo guards (glucose 10–1000 mg/dL, systolic 40–300, diastolic 20–200, pulse 20–250, weight 20–1000 lb, SpO₂ 50–100 %, temp 90–110 °F, sleep 0–24 h). A repeated externalId updates the earlier row (200) instead of adding a second. flagged is true when the reading falls outside the family's typed targets — the same rule that pages their care team.
Send a document
POST https://www.expressagain.com/api/partner/v1/patients/pt_5f1c…/documents
{ "filename": "visit-summary-2026-09-24.pdf", "title": "RN visit summary", "contentType": "application/pdf", "base64": "JVBERi0xLjQK…" }
201 { "ok": true, "id": "…", "createdAt": "…", "filename": "visit-summary-2026-09-24.pdf", "bytes": 182311 }Webhooks
Register an https URL and the events you want. Events: observation.created, alert.raised (a reading outside the family's targets), admission (from a hospital ADT feed, where connected), grant.created, grant.revoked. You only receive events for patients whose grant includes the matching read scope.
POST https://www.expressagain.com/api/partner/v1/webhooks
{ "url": "https://hooks.agency.example/expressagain", "events": ["observation.created", "alert.raised", "grant.revoked"] }
201 { "ok": true, "id": "…", "secret": "whsec_…", "note": "Store the secret now — it is not shown again." }
# Each delivery
POST https://hooks.agency.example/expressagain
X-EA-Event: observation.created
X-EA-Delivery: 3d0d… # same id on every retry — dedupe on it
X-EA-Signature: t=1758722400,v1=<hex HMAC-SHA256(secret, "<t>.<raw body>")>
{ "id": "3d0d…", "event": "observation.created", "createdAt": "…",
"data": { "patient": "pt_5f1c…", "observation": { "id": "v_…", "type": "bp", "summary": "BP 132/84, pulse 71 at 9:10 AM", "takenAt": "…", "recordedBy": "Dee", "recordedByKind": "owner" } } }Verify: recompute the HMAC over t + "." + body with your secret, compare in constant time, reject if t is more than five minutes old. Answer 2xx within 8 s. We try three times (immediately, after 1.5 s, after 5 s); a 4xx other than 429 stops retries. Fifty consecutive failures pause the webhook. Readings a partner writes are delivered at once; readings the family types on their phone are picked up within minutes.
FHIR R4 read façade beta
If your integration engine already speaks FHIR, read the same readings as Observation resources in a searchset Bundle. LOINC-coded: 2339-0 glucose, 85354-9 BP panel (8480-6 / 8462-4, 8867-4 pulse), 29463-7 weight, 2708-6 SpO₂, 8310-5 temperature, 93832-4 sleep. Read-only; the write path stays on the plain endpoint.
GET https://www.expressagain.com/api/partner/v1/fhir/Observation?patient=pt_5f1c…&code=2339-0&date=ge2026-09-17&_count=100
Accept: application/fhir+json
{ "resourceType": "Bundle", "type": "searchset", "total": 2, "entry": [ { "resource": { "resourceType": "Observation", "status": "final",
"code": { "coding": [{ "system": "http://loinc.org", "code": "2339-0" }] }, "subject": { "reference": "Patient/pt_5f1c…" },
"effectiveDateTime": "2026-09-23T07:30:00", "valueQuantity": { "value": 128, "unit": "mg/dL", "system": "http://unitsofmeasure.org", "code": "mg/dL" } } } ] }Rate limits
600 calls per minute per key, continuously refilled. Every response carries X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset; a 429 adds Retry-After in seconds. Reads return up to 1,000 rows per call (limit, default 200); use since to poll incrementally, or webhooks instead of polling.
Errors
Errors are { "error": { "code", "message", … }, "requestId" }.
| Status | Code | Meaning |
|---|---|---|
| 401 | missing_key · invalid_key | No bearer key, malformed, unknown or revoked. |
| 403 | insufficient_scope | The key or the family's grant lacks the scope. `needed` names it. |
| 403 | not_agency_on_record | Visits are only for the agency the family marked as theirs. |
| 403 | org_suspended | The organization's access is paused. Contact ExpressAgain. |
| 404 | patient_not_found | No live grant names that id for your organization — including ids that exist for someone else. |
| 400 | bad_json · bad_query · bad_type · bad_unit · bad_value · bad_time · bad_slot · bad_url · bad_events | The message says what to fix. |
| 413 | too_large | Body over the limit (64 KB for readings, 5 MB decoded for documents). |
| 415 | bad_content_type · not_a_pdf | Documents must be PDFs. |
| 429 | rate_limited | Over 600 calls per minute on this key. Honour Retry-After. |
| 501 | partner_unavailable · documents_unavailable | Not enabled on this deployment. |
| 500 | server_error | Our fault. Quote the request id to support. |
Security & BAA
- Keys are hashed at rest, shown once, never logged. Revoke and reissue at any time.
- A grant from the family is the only path to data. Reads are scoped to the grant; the patient id is opaque and per-organization.
- Every call writes an audit row: organization, key, patient (internal id), endpoint, status, hashed IP, request id — never a value or a name. Families see when you last accessed and how often.
- Webhook deliveries are HMAC-signed; secrets are sealed at rest.
- TLS everywhere; data lives with the family's own record on our managed Postgres and private object storage — see Security & HIPAA.
- BAA: a partner organization is a business associate (or covered entity) exchanging PHI with ExpressAgain. You sign ExpressAgain's Business Associate Agreement before a key is issued; the template is in preparation and is sent with the reply to your request.
Pricing
Plans are sized to how you use it — per patient per month for agencies and plans with a panel, a flat organization fee for facilities, and metered calls for vendors. Trials include 5 patients and 10,000 calls a month. Usage is metered from day one so your invoice matches what you used. Contact us for a quote — pricing is finalized with the BAA.
Trial
5 patients · 10k calls/mo
Sandbox against your own test family; no charge.
Standard
up to 250 patients · 250k calls/mo
Per patient per month, or a flat monthly fee for facilities.
Enterprise
10,000 patients · 5M calls/mo
Volume pricing, SLA, dedicated support, HL7/ADT feeds.
Request access
Tell us who you are and what you want to do. A person replies with the BAA, pricing for your size, and a trial key once it's signed. Connecting an EHR instead? See interoperability.