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.

Public API

Public facts about ExpressAgain, read-only and free, with no key: what it is, the Marketplace services, open agency jobs, the Help Center and every public page as markdown. JSON responses, CORS open for GET, 120 calls a minute per address. Nothing private is ever served: no patient information, no circle's record, no addresses, no applications.

Base URL https://www.expressagain.com/api/public/v1. Described by /openapi.json (OpenAPI 3.1), also listed in the API catalog.

GETWhat
/aboutWhat ExpressAgain is, when to use it and who it is for.
/servicesThe 13 Marketplace services and the condition modes, with their pages.
/jobs?state=&city=Open jobs posted by home care agencies: title, employment type, city, state, ZIP, the agency and the page to apply on. Pay only when the agency's posting states a rate; never a street address.
/help/search?q=Help Center articles: titles, one-line summaries and links.
/pages?path=Any public page as markdown, the same markdown the page answers to Accept: text/markdown.
/statusWhether the API is up.
curl "https://www.expressagain.com/api/public/v1/help/search?q=invite%20family"

{ "query": "invite family", "count": 3,
  "results": [ { "title": "…", "summary": "…", "topic": "Family and friends", "url": "https://www.expressagain.com/help/…", "status": "live" } ] }

Errors are RFC 9457 problem details (application/problem+json) with a code, a detail and a resolution: 400 for a bad parameter, 404 for an unknown address, 429 over the limit (with Retry-After), 501 where this deployment has no database.

Markdown: ask any public page with Accept: text/markdown and it answers markdown (curl -H 'Accept: text/markdown' https://www.expressagain.com/for-families). The site index for AI agents is /llms.txt.

MCP server and A2A agent

AI assistants can use the same read-only tools over the Model Context Protocol at https://www.expressagain.com/api/mcp (Streamable HTTP, no sign-in). Its card is at /.well-known/mcp/server-card.json. Tools: about, list_services, search_jobs, search_help, get_page.

Agents that speak A2A can send a message to https://www.expressagain.com/api/a2a; its agent card lists the same five skills, and the agent skills index explains each one.

AI agents can buy

An AI agent can buy the Individual or Family plan, monthly or yearly, for a person. The plan lands on the ExpressAgain account with the buyer's email (a new account gets a sign-in link), exactly as if the person had chosen it on the Upgrade page. Payment is a Stripe Shared Payment Token; agent keys are issued by ExpressAgain. Organizations are not sold online: an agent books a call, or starts an agency's free trial.

Discovery: ACP (Agentic Commerce Protocol, checkout at https://www.expressagain.com/api/acp), UCP (Universal Commerce Protocol, at https://www.expressagain.com/api/ucp), the product feed and the commerce OpenAPI. Requests carry a Bearer key and an Idempotency-Key; when a signing secret is issued, a Signature header is base64url HMAC-SHA256 over the Timestamp, a period and the RFC 8785 canonical JSON body.

Partner API 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, log home visits, 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 the day plan (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}/visitswrite:visitsLog one home visit from your own system (an ePCR): when, who, the vitals taken and the findings. Idempotent on externalId. Each vital lands in the family's Health Tracking; one line goes on the record's feed.
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.

Log a home visit

For a community paramedic, a visiting nurse or a nurse practitioner who charts in their own system: send the visit once and the family and everyone on the care team see what the clinician saw. Status: built. Keys are issued only after a signed BAA, and your ePCR vendor (or your own system) must agree to send.

POST https://www.expressagain.com/api/partner/v1/patients/pt_5f1c…/visits
Content-Type: application/json

{ "externalId": "pcr-2026-10-01-0042",
  "visitedAt": "2026-10-01T14:05:00Z",
  "clinician": { "name": "Sample Medic", "credential": "CP" },
  "vitals": { "systolic": 128, "diastolic": 76, "pulse": 72, "resp": 16, "spo2": 97,
              "temp": 98.4, "glucose": 118, "weight": 162, "pain": 2 },
  "findings": "Alert and talking. Walked to the kitchen with the walker. Reviewed the medication list with the family." }

201 { "ok": true, "created": true,
      "visit": { "id": "…", "externalId": "pcr-2026-10-01-0042", "visitedAt": "2026-10-01T14:05:00.000Z",
                 "clinician": { "name": "Sample Medic", "credential": "CP" }, "organization": "Sample Mobile Health",
                 "vitals": { "systolic": 128, "diastolic": 76, "pulse": 72, "resp": 16, "spo2": 97, "temp": 98.4, "glucose": 118, "weight": 162, "pain": 2 },
                 "findings": "Alert and talking. …",
                 "summary": "BP 128/76 · pulse 72 · resp 16 · SpO2 97% · temp 98.4 °F · sugar 118 · weight 162 lb · pain 2/10",
                 "recordedAs": "Sample Medic, CP · Sample Mobile Health", "createdAt": "…" } }

# The same externalId again → 200 { "ok": true, "created": false, "visit": { …the first report… } } — nothing new is written.

Scope write:visits, on your key and granted by the family. Every vital is optional, but send at least one vital or the findings (plain text, up to 2,000 characters); blood pressure needs both numbers. The bounds are the same typo guards as readings (respiratory rate 2–80 per minute, pain 0–10). visitedAt may not be in the future or more than a year ago. Each vital appears in the family's Health Tracking as from your clinician and organization and goes to the care team like any other reading; the visit fires visit.created. The app records and displays; it never interprets a visit. The clinician works within their own license and their organization's protocols.

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, visit.created (a home visit was logged — its vitals and whether findings were written, never the findings text). 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.
400empty · bad_bp · bad_findings · bad_clinician · bad_external_idHome visits: at least one vital or the findings; both blood pressure numbers; findings up to 2,000 characters; clinician.name required. `field` names the field.
409external_id_conflictHome visits: that externalId was already used for another patient.
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.