03 · Consent
You send who, where and why. The rail picks the factor — fingerprint, one-time code, national eKYC or a guardian's consent — runs it, and returns a proof. You never choose the factor, integrate a scanner, or branch per payer. Render what you are given, and keep the proof.
Everything below elaborates this sentence.
What this stop does
Consent answers two things at once: is this really the person, and may this data be seen or this
action taken. One call does both. Which factor is legal — for this member, at this facility, on
this payer, for this purpose — is decided server-side and versioned there, so your code carries no
if minor, no if whitelisted, and no biometric vendor's name.
It stands on its own: you can ask for consent to read a record or check a cover without any claim or visit existing.
Biometric workstations need a companion application
You do not integrate a scanner, but the frame cannot reach one by itself. It runs in a browser, and a browser has no access to a fingerprint reader plugged into the machine. Every workstation that takes a biometric capture needs a companion application installed next to the browser: the hardware server. It runs on the workstation and talks to the reader, and the collection UI inside the frame talks to it.
That is what lets the rail use the scanners already deployed across SHA facilities. A facility does not buy new readers to go live, and your HMIS never calls the hardware server. The frame finds it.
Use the Advantage build. It is a Windows installer, published per environment:
| Environment | Hardware server, Advantage build |
|---|---|
| Development | Download the hardware server for development |
| Production | Download the hardware server for production |
The hardware server asks you to sign in once it is installed. Your contact person at Savannah can give you dummy credentials for logging in while you build and test.
What you send
Ask for an authentication. A thin body in, a decision and a next step out.
authentication_id + collection.urlThe subject is the patient in the path; the facility comes from the X-Facility-Id /
X-Facility-Id-Type headers, both required. Neither is read from the body.
| Call | What it does | What you keep |
|---|---|---|
purpose | Required. Why you are asking. It picks the rules and becomes the scope the proof may be spent on. | — |
agent_id | Required. The national ID of the operator asking — the audit note for who requested it, not routing. | — |
callback_url | Required. Where the outcome is delivered. | — |
selection_ref | Optional. The cover in scope, from covers/select at 02. It decides which payer's rules and factors apply; without it the default rules run. | — |
There is no network field. The network is derived from selection_ref, so a caller cannot
name the most permissive rules for itself; a network in the body is ignored.
Do not send hardware context — no workstation, agent or device identifiers. The collection UI captures those from the machine that does the capture.
What you get back
One shape, three fields. Intake only writes a pending record: no factor is chosen and no code is sent yet. The factor is picked and run inside the hosted surface, and a one-time code is sent from there.
| Call | What it does | What you keep |
|---|---|---|
authentication_id | The proof, as a resource. Persist it. | authentication_id |
status | Always pending at intake. | — |
collection.url | Embed it. Everything the member sees and does happens inside it: factor choice, capture, retries, fallbacks. Single use, and it expires at collection.expires_at, five minutes after issue. | collection.url |
There is no next_action, purpose, network or top-level expires_at. collection is left
out entirely when the deployment cannot mint a launch, or the call carried no bearer token; keep
the authentication_id and read the record.
Refusals are 400 with an AUTHN_* code: AUTHN_PURPOSE_REQUIRED, AUTHN_PURPOSE_UNKNOWN,
AUTHN_CALLBACK_REQUIRED, AUTHN_AGENT_REQUIRED. A selection_ref that cannot decide the
payer is 409 SELECTION_UNUSABLE (503 where the deployment cannot resolve one at all).
The outcome
Read the record for the result — never treat the capture window closing as success. A push arrives
on your callback_url; poll this only as the safety net.
Read the authentication back.
status + expires_at| Call | What it does | What you keep |
|---|---|---|
pending | No verdict yet. There is no factor and no expires_at. Not a failure, not something to retry. | — |
verified | The factor succeeded (factor OTP, SIL_BIOMETRICS or EKYC, with factor_class weak or strong). A freshness window starts at verified_at. | expires_at |
failed | Terminal, with no expires_at. Raise a new authentication rather than retrying this one. | error_code, error_detail |
The record also carries authentication_id, network, purpose, created_at and
updated_at. An unknown id is 404 AUTHN_NOT_FOUND; the id is looked up within your tenant and
is not checked against the patient in the path.
Consumption and expiry are not statuses: whether a proof can be spent is decided when you spend it. Attempt the redemption and handle the refusal rather than branching on a status.
Purpose is the scope
Purpose picks which rules run and bounds what the proof may later be spent on.
| Call | What it does | What you keep |
|---|---|---|
eligibility_check | Confirm cover, run an eligibility search. | — |
cover_view | View benefit or cover detail. | — |
visit_start | Open a visit, begin care. | — |
visit_end | Close a visit or discharge. Always needs its own authentication. | — |
pii_access | Read a patient summary or clinical record. | — |
A proof is a resource, not an event
A verified authentication is persisted, has a freshness window, and — depending on the factor —
can be spent more than once. Keep the authentication_id, not just the fact that it worked.
| Call | What it does | What you keep |
|---|---|---|
Strong factors | A fingerprint or national eKYC verification backs several intents while fresh — an eligibility search and, later, opening the visit. Do not re-authenticate defensively. | authentication_id |
Weak factors | A one-time code is spent once; a second redemption is refused. | — |
Standalone purposes | Some are never satisfied by an existing proof. Closing a visit is one — always take a fresh authentication. | a fresh one |
Sensitive data is rendered, not fetched
Inside the hosted surface
You do not call these in a normal integration — the surface renders inside the iframe and the browser posts to them. They are here because test harnesses drive the surface headlessly, and the failure below looks like a bug in your code when it is not.
Send the passcode for the launched authentication. Form-encoded in, HTML fragment out.
the re-rendered verify slotCheck the code the member read back. A refusal re-renders the slot with the executor's own message.
the receipt fragmentsend takes authentication_id and contact_id, plus unique_patient_id, purpose,
selection_ref and callback_url: it re-runs the consent decision, and contact_id must be a
number that decision offered for this member. verify reads only authentication_id,
contact_id and otp; the decision is not re-run. The session and tenant come from the
hie_session cookie and an X-Csrf-Token header (echoing hie_csrf), both scoped to the UI
mount path.
Age and guardian
What the baseline rules offer. They are versioned server-side, and a payer overlay can add to them.
| Call | What it does | What you keep |
|---|---|---|
Under 7 | No fingerprint. A guardian consents. | guardian identity |
7 to 17 | Fingerprint where the facility captures children; otherwise a guardian. A guardian is always offered as the second route. | guardian identity |
18 and over | National eKYC for a member with a national ID at a facility set up for it; a one-time code where the payer or facility allows it and the member has a contact on record; a guardian when neither is possible. | member contact |
Guardian consent is a modifier on the underlying factor and inherits its strength — a guardian's fingerprint is strong, a guardian's code is weak. The record carries who consented and on whose behalf.
Break-glass
Which factors a facility offers
Resolved for you, but the resolution explains most "the scanner is broken" tickets.
| Call | What it does | What you keep |
|---|---|---|
Facility enforces biometrics | The normal case — the strong factor first (eKYC for adults, fingerprint for children where captured), code where the member qualifies. | — |
Facility de-enforced | The one-time code becomes available to every adult there with a contact on record, whitelisted or not. | — |
Member individually whitelisted | The code path for this person, at any facility. | — |
Biometric no-match is normal (~3 adults in 10); the embedded UI falls back to the code path and raises a whitelist request where needed. Do not build a retry loop around the reader.
What breaks here
| Where it surfaces | What the desk sees | What actually went wrong |
|---|---|---|
| Intake | "The response has no collection.url" | The deployment could not mint a launch, or the call carried no bearer token. The authentication exists; read it by id. Intake never returns next_action, and a one-time code is sent from the surface, not at intake. |
| Embedded frame | "The frame loads with a doubled host and never renders" | collection.url is absolute. Use it as the iframe src verbatim — do not prefix a base URL. |
| Capture workstation | "The biometric path cannot reach the reader on a new workstation" | The hardware server is not installed there. The frame reaches the scanner only through it — install the Advantage build on every biometric workstation. |
| OTP surface (harness) | "'unique_patient_id is required' but we got a 200" | The hosted OTP endpoints answer 200 on failure with the message in the fragment. Read the fragment, not the status. |
| Any sensitive view | "The record was on the client before consent was granted" | Data was fetched, then permission asked. Ask first, render inside the viewer, never hold the payload. |
| Every capture | "We ask for a fingerprint twice in one visit" | A strong proof was treated as an event. A fresh one backs several purposes — keep authentication_id and spend it. |
| Discharge | "The proof from admission is refused at discharge" | Closing a visit is standalone. No proof taken for another purpose satisfies it. |
| Outcome | "We showed consent as granted and the claim was rejected for no authorisation" | The frame closing was treated as the answer. Only the record read or the signed callback establishes consent. |
Errors
Machine codes arrive in error. The AUTHN_* and PASSCODE_* families are this stop's own; the
decision codes come from the hosted surface, which renders them as its refusal page.
| HTTP | error | When |
|---|---|---|
| 400 | AUTHN_PURPOSE_REQUIRED, AUTHN_PURPOSE_UNKNOWN | Intake with no purpose, or one outside the five listed above. |
| 400 | AUTHN_CALLBACK_REQUIRED, AUTHN_AGENT_REQUIRED | Intake with no callback_url or no agent_id. |
| 400 | FACILITY_CONTEXT_MISSING | A facility header is missing or invalid. |
| 404 | AUTHN_NOT_FOUND | No authentication under that id in your tenant. |
| 409 | SELECTION_UNUSABLE | selection_ref is unknown, inactive or another patient's. 503 when the deployment cannot resolve one. |
| 409 | NO_MODALITY_AVAILABLE | No factor can work for this member here. Raised by the consent decision on the surface, not at intake. |
| 400 | PURPOSE_REQUIRED, PURPOSE_UNKNOWN | The same purpose refusals, as the consent decision words them on the surface. |
| 200 | AUTHN_CONTACT_REQUIRED, AUTHN_CONTACT_UNUSABLE, AUTHN_CONTACT_NOT_OFFERED | Passcode send or verify with no contact, a contact the payer cannot be asked about, or one the decision did not offer. Inside the fragment. |
| 200 | PASSCODE_INCORRECT, PASSCODE_ATTEMPTS_EXHAUSTED, PASSCODE_SENDS_EXHAUSTED | Wrong or expired code, too many wrong codes, too many codes raised. Inside the fragment. |
| 200 | AUTHN_OTP_REFUSED, AUTHN_OTP_UNAVAILABLE | The payer declined the request, or its passcode rail is unreachable. Inside the fragment. |
When a proof is later spent, the redemption refusals are AUTHN_NOT_VERIFIED, AUTHN_EXPIRED,
AUTHN_CONSUMED, AUTHN_SUBJECT_MISMATCH and AUTHN_PURPOSE_REFUSED; see 04 ·
Visit for how they surface.
Next
Next: 04 · Visit
A complete brief for this step — the calls to make, what to persist, and every failure path to handle. Nothing on this page is assumed.

