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.
Lifecycle
Section titled “Lifecycle”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.
Creating one
Section titled “Creating one”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.
Rejection at creation
Section titled “Rejection at creation”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.
Downgraded delivery
Section titled “Downgraded delivery”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).
Acknowledging receipt
Section titled “Acknowledging receipt”The recipient tenant acknowledges a delivered exchange:
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.
Cancelling
Section titled “Cancelling”Any non-terminal exchange can be cancelled by its sender:
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.