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.
| GET | What |
|---|---|
| /about | What ExpressAgain is, when to use it and who it is for. |
| /services | The 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. |
| /status | Whether 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.
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, log home visits, 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 the day plan (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}/visits | write:visits | Log 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}/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.
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" }.
| 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. |
| 400 | empty · bad_bp · bad_findings · bad_clinician · bad_external_id | Home 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. |
| 409 | external_id_conflict | Home visits: that externalId was already used for another patient. |
| 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.