Integrations

EMR & charts

Five adapters onto one canonical patient model — an honest per-EMR capability matrix, least-privilege scopes, order write-back into the chart, and the read-only degrade that never fails silently.

Connect the chart your clinicians already live in, and an EMR-sourced order becomes indistinguishable from a widget-sourced one by the time it reaches a pharmacy. That's the whole design: an EMR is a new inbound source, not a new pipeline. A pull populates the same canonical order object the rest of the rail already consumes, so routing, approval, failover, and webhooks are untouched by which chart it came from.

Gated by emr_integrations (dark by default, revealed once a per-vendor BAA is on file). Write-back adds emr_order_writeback on top.

The five adapters#

Adapter adapterType Auth Subscribe
Healthie healthie API key Webhook
Elation elation OAuth2 Webhook
DrChrono drchrono OAuth2 Webhook
FHIR R4 fhir_r4 SMART bearer / OAuth2 Poll (webhook where the endpoint supports it)
Sandbox mock none Webhook

fhir_r4 is the generic path, not a fallback — it's how you connect any conformant R4 endpoint, including systems that expose FHIR as their integration story rather than a proprietary REST API. You set adapterVendor to name the concrete system (athenahealth, canvas, whatever it is) so logs and the capability matrix reflect the real endpoint rather than "FHIR".

mock is the sandbox adapter. It answers every verb against fixtures so an integration round-trips before you have credentials. In live mode it is refused — if a real tenant's connection resolves to the mock because no credential was found, the call throws rather than quietly returning fixture data. Fixture data written into a real chart is the same class of failure as writing a fake patient into it.

HL7 v2 is deliberately not an adapter. An interface-engine integration is a per-site MLLP listener, not a self-serve connection, and pretending otherwise would put a picker entry in front of clinics who can't actually use it.

The capability matrix#

Every adapter reports what it can honestly do, independent of whether credentials happen to be present. The clinic UI renders this matrix, and the router expects it — a capability reported false is a supported state, not an error.

Demographics Intake Medications Encounters Push Writes as
Healthie Task
Elation Non-visit note
DrChrono Task
FHIR R4 profile-dependent MedicationRequest or Communication

Two entries deserve the explanation.

Elation reports no intake pull. Elation's forms model doesn't expose a structured questionnaire response the way a FormAnswerGroup or a FHIR QuestionnaireResponse does. Rather than scrape a note body and pretend the result is structured intake, the adapter reports intake: false and the clinical questionnaire is captured through neolife's own intake widget. You still get the certificate; it just isn't sourced from the chart.

FHIR R4 defaults to no push. A conformant R4 server may be entirely read-only, and assuming otherwise means discovering it at the moment a real order needs to be written. The static default is push: false; a per-connection profile enables writes once the endpoint is confirmed to accept MedicationRequest. The connection-specific matrix — not the static one — is what the UI shows you.

Least-privilege scopes#

The scopes you grant are recorded on the connection and are visible next to the capability matrix, so "what can this integration reach" is answerable without opening the vendor's admin console. Grant the narrowest set that covers the capabilities you actually turned on.

For a SMART-on-FHIR connection doing pull only:

patient/Patient.read
patient/QuestionnaireResponse.read
patient/MedicationRequest.read
patient/Encounter.read

Add exactly one scope to enable write-back:

patient/MedicationRequest.write

Two grants people add reflexively and shouldn't: a wildcard (patient/*.read) hands neolife every resource class the endpoint exposes, including ones no capability here reads; and user/ scopes broaden the grant from the patient's compartment to the practitioner's whole caseload. Neither is needed by anything on this page.

Rule: narrowing a scope is safe. If a scope is revoked and a capability depended on it, the next health check degrades the connection with a specific error code — it does not silently return empty results that look like a patient with no medications.

Pulling context#

curl -X POST https://api.neolife.health/integrations/emr/connections/{id}/pull \
  -H "Authorization: Bearer $NEOLIFE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "emrId": "pat_88213" }'

The pull runs server-side inside the regulated plane and returns a canonical order draft. Demographics, intake answers, medications, and encounter diagnosis codes are normalized into the same shapes the rest of the platform uses, so an EMR-sourced draft carries the ICD-10 code an order line needs without your integration knowing which chart it came from.

The join key is always the EMR-native id, never a name-and-date-of-birth match. A fuzzy demographic match across two systems is how one patient's medication list ends up on another patient's order; the identity map holds the vendor's own identifier and nothing else is trusted.

Write-back, and the read-only degrade#

Once an order is routed you can write it back into the chart so the clinician sees it where they already work.

curl -X POST https://api.neolife.health/integrations/emr/connections/{id}/push-order \
  -H "Authorization: Bearer $NEOLIFE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "order": { ... }, "patient": { "emrId": "pat_88213" }, "trackingUrl": "https://..." }'

What gets written depends on the chart: a Task in Healthie and DrChrono, a non-visit note in Elation, a MedicationRequest on a FHIR endpoint that accepts writes, and a Communication on one that doesn't take medication orders but does take messages.

When the chart can't accept the write, the order is held and the provider is notified in-app. The push response comes back with degraded: true, an entityType of held, and a reason. It is never a silent success and never a dropped order. Three things land you there: the emr_order_writeback flag is off, the connection's honest capability matrix reports push: false, or the endpoint rejected the write at call time.

That degrade is the reason the capability matrix is honest rather than optimistic. A matrix that claims a capability the endpoint lacks turns a predictable held order into a lost one.

Events flowing the other way#

Adapters that support webhooks parse the vendor payload into a vendor-neutral event — patient.created, patient.updated, intake.submitted — carrying the EMR-native id, an entity type, and a dedupe key. Vendors replay. The dedupe key is what makes a replayed event a no-op instead of a duplicate draft; the same discipline as idempotency on the write path.

FHIR endpoints without a subscription mechanism are polled on an _lastUpdated watermark that walks forward, so a poll resumed after an outage picks up where it stopped rather than replaying the corpus.

Next steps#