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.

  1. Your organization signs the BAA and receives a share code (six characters, like 7KQ2MW) and an API key.
  2. 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.
  3. The patient appears in GET /patients under an opaque id (pt_…) that is stable for your organization and different for every other organization — partners cannot correlate patients by id.
  4. The family can revoke at any time. Access ends immediately; you receive a grant.revoked event 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

MethodPathScopeWhat
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}/observationsread:observationsReadings from the family's own Health Tracking. ?type=glucose|bp|weight|spo2|temp|sleep &since= &until= &limit=
POST/patients/{id}/observationswrite:observationsAdd 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}/medicationsread:medicationsThe structured medication rows and allergies the family keeps in Medical Info.
GET/patients/{id}/care-planread:care-planThe family's typed targets, the care team's instructions, and her day (the task sheet).
GET/patients/{id}/visitsread:visitsThe visit flow sheet and clock-ins/outs — only when the family marked you as their agency on record (403 otherwise).
POST/patients/{id}/documentswrite:documentsA PDF (base64, ≤ 5 MB) into the family's app. 501 on a deployment without document storage.
GET · POST/webhookswebhooksList / register delivery URLs. The signing secret is returned once.
GET · DELETE/webhooks/{id}webhooksOne webhook with its last 20 deliveries; remove it.
GET/fhir/Observationread:observationsBeta — 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" }.

StatusCodeMeaning
401missing_key · invalid_keyNo bearer key, malformed, unknown or revoked.
403insufficient_scopeThe key or the family's grant lacks the scope. `needed` names it.
403not_agency_on_recordVisits are only for the agency the family marked as theirs.
403org_suspendedThe organization's access is paused. Contact ExpressAgain.
404patient_not_foundNo live grant names that id for your organization — including ids that exist for someone else.
400bad_json · bad_query · bad_type · bad_unit · bad_value · bad_time · bad_slot · bad_url · bad_eventsThe message says what to fix.
413too_largeBody over the limit (64 KB for readings, 5 MB decoded for documents).
415bad_content_type · not_a_pdfDocuments must be PDFs.
429rate_limitedOver 600 calls per minute on this key. Honour Retry-After.
501partner_unavailable · documents_unavailableNot enabled on this deployment.
500server_errorOur 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.

No pricing to pick here — every organization is different, and we'd rather talk first. Nothing you type is shared with anyone else.