Skip to content

Exchanges

An exchange is one referral, letter, result, discharge summary, or summary request moving from a sender to a recipient. It always passes through consent-policy evaluation and the audit ledger — there is no bypass path, not even for Railflo’s own internal tools.

Rendered directly from the state machine’s own transition table (packages/domain/src/exchange.ts) — this diagram cannot drift from the actual code, because it’s read off the same array the state machine folds over.

stateDiagram-v2
[*] --> created
created --> validated: validate_ok
created --> rejected: validate_fail
validated --> policy_checked: policy_permit
validated --> rejected: policy_deny
policy_checked --> routed: route_assigned
routed --> delivered: deliver_ok
routed --> failed: deliver_fail
delivered --> acknowledged: ack_received
created --> cancelled: cancel
validated --> cancelled: cancel
policy_checked --> cancelled: cancel
routed --> cancelled: cancel
delivered --> cancelled: cancel
rejected --> [*]
failed --> [*]
cancelled --> [*]
acknowledged --> [*]

rejected, failed, cancelled, and acknowledged are terminal — nothing transitions out of them.

Terminal window
curl -X POST https://api.sandbox.railflo.com/v1/referrals \
-H "Authorization: Bearer railflo_sk_test_..." \
-H 'content-type: application/json' \
-d '{
"sender": { "practitioner_id": "prac_..." },
"recipient": { "kind": "railflo_org", "org_id": "org_..." },
"patient": "pat_...",
"purpose_of_use": "treatment"
}'

/v1/letters and the generic /v1/exchanges (with an explicit type) work the same way. recipient.kind is either railflo_org (another Railflo tenant — native delivery) or external (a display name + an address channel — see Downgraded delivery below).

purpose_of_use matters: it’s what the consent policy decision point evaluates against (see Consent & policy) — treatment and transfer_of_care are permitted by default under Irish jurisdiction rules, most other purposes are not, absent an explicit policy.

If policy evaluation denies the request, the exchange is created directly into rejected state — this is not an HTTP error, it’s a legitimate business outcome: the request succeeded, the exchange was refused. Check the state and rejection_reason fields on the response.

When the recipient isn’t a Railflo tenant (recipient.kind: "external"), delivery downgrades to a governed fallback: no clinical content ever leaves via email. The recipient gets a message naming the sender and containing only the patient’s initials and birth year, plus a one-time secure link. The actual document is rendered and released only once the recipient proves who they are by entering the patient’s date of birth on that link (locking after 5 wrong attempts).

The recipient tenant acknowledges a delivered exchange:

Terminal window
curl -X POST https://api.sandbox.railflo.com/v1/exchanges/{id}/ack \
-H "Authorization: Bearer railflo_sk_test_..."

This is the one recipient-initiated write in the whole lifecycle — every other transition is driven by the sender’s request or the routing engine.

Any non-terminal exchange can be cancelled by its sender:

Terminal window
curl -X POST https://api.sandbox.railflo.com/v1/exchanges/{id}/cancel \
-H "Authorization: Bearer railflo_sk_test_..." \
-d '{"reason": "sent to the wrong recipient"}'

A reason is required — it’s audited alongside the cancellation.