Export
FHIR R4 bundles validated against US Core in CI, HL7 v2 ORU over MLLP, the Epic adapter proven against Epic's developer sandbox, and a generic FHIR R4 server target.
An episode can be sent onward as a FHIR R4 transaction bundle or an HL7 v2.5.1 ORU^R01 message. Exports are declared on the program, fire on triggers, run through a coordinator with retries, and leave one status row per attempt. This page documents the targets that exist, how a program binds to them, and how to read the result.
Targets
| Target | kind | Transport | Status |
|---|---|---|---|
| FHIR R4 server | fhir_medplum | Transaction bundle over HTTPS with an OAuth client credential; idempotent Observation writes | Shipped |
| Epic | fhir_epic | Transaction bundle over HTTPS with SMART Backend Services; Patient.Search before write | Proven against Epic's developer sandbox; production use requires your organization's Epic grant |
| HL7 v2 interface engine | hl7v2_mllp | ORU^R01 v2.5.1 over MLLP with ACK handling | Shipped |
| Read-only episode bundle | none | GET /api/v1/episodes/{id}/fhir | Shipped |
Exports go to a FHIR server or an HL7 interface; no export target posts JSON to an arbitrary URL you own. The webhooks in this section are inbound (devices to AnyBio); see device ingest.
The FHIR bundle
GET /api/v1/episodes/{id}/fhir (Read scope) returns the episode as a FHIR R4 Bundle. The same builder feeds both FHIR adapters. The bundle contains:
- Patient binding - by default no
Patientresource is emitted: each Observation'ssubjectis a logicalidentifierreference (system and value captured at enrollment and held per endpoint), and the receiving system resolves it to its own patient. AnyBio never creates or matches patients in a provider's system. An endpoint can require that binding, in which case an export for a patient without one is refused rather than sent unbound. A separate mode emits a literalPatientfor a demonstration server that owns none. - Observation - one per linked observation, with the biosignal's LOINC code and unit, a business
identifierunderhttps://api.anybio.io/fhir/sid/observationso a re-export can be a conditional create rather than a duplicate, andmeta.profileset from the biosignal catalog's declared profiles (for example US Core Heart Rate). A blood-pressure panel is one Observation with systolic and diastolic components. - DocumentReference - a telemetry strip rendered as PDF, when the episode produced one.
- Provenance - who produced what, with in-bundle targets.
- Organization - the exporting organization.
Per export, a program binding can shape what a FHIR target receives: shaping.mode is raw (default, everything in the window) or decimate with decimate_seconds (at most one observation per biosignal per bucket). Shaping thins what one opted-in export sends by time; it never drops a value and never touches what is stored.
Validation in CI
The backend's CI runs the official HL7 FHIR validator (FHIR 4.0.1, hl7.fhir.us.core#6.1.0) on every change. Two gates block the build: a hand-written known-good fixture must validate with zero errors, and a deliberately malformed fixture must be rejected. A third gate validates the bundle AnyBio actually generates against a checked-in list of known findings; a new finding fails the build.
The HL7 v2 message
The HL7 adapter assembles an ORU^R01 (v2.5.1) from the episode: PID and PV1 from the patient and episode, OBR-4 naming the program, and OBX segments from three sources - the linked biosignal observations as numeric results, condition_detected timeline events as text, and agent_insight events as text. A telemetry strip is referenced with a placeholder OBX pointing at the document, whose delivery is the FHIR adapter's job. Sending is over MLLP; the adapter parses the ACK and maps AA to success, AE and AR to a permanent failure, and a connect or ACK timeout to a retry.
Configure a target
FHIR endpoints
POST /api/v1/fhir-medplum-configs (Write scope), also GET, GET /{id}, PATCH /{id}, DELETE /{id}. Body:
{
"name": "Sandbox FHIR server",
"base_url": "https://fhir.example.org/fhir/R4/",
"oauth_provider_id": 12,
"project_id": 7,
"epic_token_url": null,
"enabled": true
}oauth_provider_id names the credential row configured under Integrations, Data Destinations. epic_token_url is the SMART Backend Services token endpoint and is set only for an Epic row; it is distinct from base_url, the FHIR resource base. Both URLs pass the same guard at write time and again before any network call: HTTPS only, no user info, no IP literals, no localhost, no dotted-quad embedded in the host, no base that already ends in a resource type or metadata, and the host must be on the deployment's export host allowlist. An empty allowlist fails closed.
HL7 endpoints
POST /api/v1/hl7-endpoints (Write scope), also GET, GET /{id}, PATCH /{id}, DELETE /{id}. Body:
{
"name": "Interface engine (test)",
"host": "interface.example.org",
"port": 6661,
"sending_application": "ANYBIO",
"sending_facility": "ANYBIO.IO",
"receiving_application": "ENGINE",
"receiving_facility": "CLINIC",
"project_id": 7,
"hl7_version": "2.5.1",
"connect_timeout_ms": 5000,
"ack_timeout_ms": 10000,
"auto_submit_on_episode_close": false,
"enabled": true
}Optional: encoding_characters, processing_id, metadata.
Data exports
A data export names a target and a kind. POST /api/v1/data-exports (Write scope), also GET, GET /{id}, PATCH /{id}, DELETE /{id} (disable):
{
"name": "Clinic interface",
"kind": "hl7v2_mllp",
"project_id": 7,
"target_ref": { "kind": "hl7v2_mllp", "hl7_endpoint_id": "<uuid>" },
"enabled": true
}target_ref shapes: {"kind":"hl7v2_mllp","hl7_endpoint_id":"<uuid>"}; {"kind":"fhir_medplum","fhir_medplum_config_id":"<uuid>"}; the Epic adapter reads the same endpoint id key from a row whose epic_token_url is set.
POST /api/v1/data-exports/{id}/test sends a synthetic test message through the configured destination. The HL7 adapter forces MSH-11 = T (training) so a downstream reviewer cannot mistake it for clinical traffic.
Bind an export to a program
A program lists its exports in spec.exports. Each binding names a data export and the triggers that fire it:
"exports": [
{
"data_export_id": "<data export uuid>",
"enabled": true,
"triggers": [
{ "type": "on_episode_start" },
{ "type": "on_schedule", "cron": "0 7 * * *", "timezone": "America/Chicago", "require_new_data": true },
{ "type": "on_attention_crossed", "threshold": 61, "debounce_seconds": 3600 },
{ "type": "on_timeline_event", "event_type": "condition_detected" },
{ "type": "on_episode_end" }
],
"shaping": { "mode": "decimate", "decimate_seconds": 300 }
}
]| Trigger | Fires |
|---|---|
on_episode_start | Once, at enrollment |
on_episode_end | Once, if the episode closes (a long-lived program episode may never close) |
on_timeline_event | Per matching episode_events row; optional predicate is a JSON subset match on the payload |
on_schedule | On a five-field cron in a timezone (user_local allowed); require_new_data skips empty windows |
on_attention_crossed | When attention rises above threshold, rate-limited by debounce_seconds (default 3600) |
on_demand | Only by POST /api/v1/episodes/{episode_id}/data-exports/{data_export_id} |
Read the result
Every fire creates an episode_data_exports row. GET /api/v1/episodes/{episode_id}/data-exports lists them; GET /api/v1/episode-data-exports/{id} returns one with kind, trigger_reason, export_status, started_at, completed_at, protocol_control_id, protocol_ack, error_message, retry_count, max_retries, next_retry_at, payload_ref, payload_bytes, window_start, window_end.
Statuses: pending, in_progress, success, failed, retrying. A transient failure (network, 5xx, redirect, token acquisition, MLLP timeout) schedules a retry at 1, 5 and 15 minutes and reuses the original window; a permanent failure (4xx, AE or AR, a malformed frame, a disabled endpoint, a bundle the builder could not produce) stops. POST /api/v1/episode-data-exports/{id}/retry re-queues a failed row by hand.
The Epic adapter, precisely
What is proven on demand: GET /api/v1/integrations/epic/sandbox-demo runs a read-only round-trip against Epic's public developer sandbox and its synthetic patients - SMART Backend Services authentication (an RS384 client assertion exchanged for a token), Patient.Read, Patient.Search by identifier, and a best-effort Observation.Search. The sandbox URLs are hard-coded in the service; it never reads a configured endpoint and never accepts a URL from the request, so it cannot touch a real Epic instance. When no signing key is configured for the environment it answers available: false with a static reason.
How the write adapter behaves (implemented, exercised in the test suite): it resolves the patient with Patient.Search on the logical identifier and refuses on zero or more than one result (it never creates a Patient), rewrites every in-bundle subject to the resolved Patient/<id>, posts the transaction bundle, and surfaces any non-2xx per-entry response as an error with Epic's diagnostic.
The Epic adapter is validated against Epic's developer sandbox; production use requires your organization's Epic grant.
Care hand-offs API
The staff-only follow-up queue - list and detail, claim, contact attempt, resolve with a disposition, resume automation - with optimistic versioning and the messaging policy that routes it.
Device ingest
The cellular device webhook (shared secret, per project), image ingest, waveform captures, and the provenance fields every observation carries - measured time versus received time.