What a cover pays for
A cover pays for things arranged as a tree rather than a flat catalogue. The tree starts at the benefit, and every level takes the code you chose at the level above it.
Everything below elaborates this sentence.
This page picks up where 02 · Verify cover leaves off. That page answers which cover pays. Once a cover is selected, this one answers what it pays for, here, today.
Nothing below works without a selection. The tree is read against a selection_ref, which is
resolved per member and per cover, so the same query with a different selection comes back
different. The facility headers are required here as on every v2 call.
The next three views show the same structure: the screens an operator works through, the shape itself, and the calls behind them.
What an operator does: three drill-downs, and a chain your client remembers.
coversearchnodes[].codenodes[].codebreadcrumbnodes[].codeaccess_pointyour own chainnodes[].codeauthorization.needs_preauthnext_actionnodes[].codeauthorization.needs_preauthnext_actioncoverageWhichever view you read, two things matter more than the levels themselves.
Balances are not a level of the tree, and they are not a separate call either. Every node carries
its own coverage, so the figure arrives on the row you are already looking at. Walking further
down never changes a balance you have read; it only shows the balances of the children.
Fund, access point, tariff and authorisation are not levels either. They are attributes that hang on whichever node the rail chose to attach them to. That is the part that trips people who have integrated one payer and assume the next behaves the same way.
Some things are on every node, some only on the last
Of everything on this page, this is the part to hold on to, and it is why a hard-coded traversal breaks on the second payer you integrate.
| Call | What it does | What you keep |
|---|---|---|
On every node | code, name, kind, access_point, fund, coverage and next_action. coverage is there on a group as well as a line, because a group can be exhausted as a whole. | coverage |
Only on a service line | tariff, copay, authorization and limits. A payer may know them further up its tree, but the rail publishes them only at the billable node, so that one component renders every payer's walk. | the flags, read at the leaf |
Only when the node cannot be used | status: blocked, blocked_by and detail. A blocked group is not walkable: a desk should not drill to the bottom of a benefit the facility may not use. | blocked_by |
So "does this need preauthorisation?" is answered on the service line, in authorization, whichever
level the payer declared it at. You do not walk the chain to work it out. What you do keep is the
chain itself, because the node that blocked a walk, or the group whose balance ran out, is the thing a
desk will be asked about.
You walk it one level at a time
No call returns the whole tree, and that is not a limitation to route around. The tree is too large to hand over, and the national scheme's service-line list alone runs to thousands of codes.
So every level takes the code chosen at the level above it, passed to the same route as
parent_code rather than to a route of its own. You do not compose that call. Each node's
next_action already carries it.
- 01Ask for the benefits on the selectionThe call with no parent_code. This is the only level you can ask for without already having a code in hand.
- 02Follow filter_benefitsA node whose next_action.type is filter_benefits has more beneath it. GET its endpoint, which is this route with parent_code set to the node's code.
- 03Stop on any other typeobtain_consent, start_visit, add_service_line, resolve_open_visits and none all end the walk on that node. They are hand-offs, not more tree.
- 04Read the balance off the nodeNothing further to call. coverage is on the node you are standing on.
The end of the walk is next_action.type, not an empty level. A billable node comes back with a
next action of its own, and a blocked one comes back with none. If you go looking for the level
beneath a service line by passing its code as parent_code, the answer is not an empty page but
404 BENEFIT_NOT_FOUND. A client that stops "when a level comes back empty" will therefore never
stop on its own, and one that hard-codes three calls is correct for exactly the payers it was
written against.
That has three consequences for the picker you build.
Take the depth from the response instead of assuming it. One payer's tree ends at the service line
under a sub-benefit, another's ends at the sub-benefit. Follow what comes back, and offer a select
action on any node whose type is not filter_benefits.
A kind of group or service_line is for display. next_action is authoritative about what can
follow, and a client that branches on kind will eventually branch wrongly on a payer whose tree
is a different shape.
Search is per level. A clinician looking for "dialysis" can filter the level they are on, and the filter is forwarded to that level's payer call. It does not cross levels, and SHA ignores it at the top level entirely. Pair the walk with your own search over levels you have already fetched if a clinician needs to find a leaf without knowing where it sits, and keep the chain for each hit.
One shape, several vocabularies
The words change per payer while the structure stays the same, and the rail normalises to the left-hand column.
| On the rail | SHA calls it | Others may call it |
|---|---|---|
| Benefit | Benefit (SHA-07, PMF-07) | Package, category, class |
| Sub-benefit | Sub-benefit, or "sub-package" (SHA-07-SC-01) | Sub-package, benefit group |
| Service line | Intervention (SHA-07-001) | Service, item, tariff line, product |
The traversal is what an operator sees as three screens. In your code it is one graph, and the same query with a different member, facility or cover comes back different.
The call that walks it
Every level of the tree. Omit parent_code for the top level, then follow each node's next_action to descend. search filters one level, and access_point narrows a level to what is payable in the visit you are in.
nodes[].codeThe handle is the selection_ref from covers/select, not an eligibility reference: v2 has none.
Reading the tree also takes no consent proof. The proof is redeemed later, at
POST /api/v2/claims/authorize.
/api/v2/patients/{unique_patient_id}/eligibility/{selection_ref}/benefitsOne level of the benefit tree for a committed selection.
| Parameter | Type | Description |
|---|---|---|
X-Facility-Idrequired | header | The facility this call is made from. |
X-Facility-Id-Typerequired | header | How to read it: mfl, license-number, fr-code, registration-number or fid. |
unique_patient_idrequired | path | From the lookup. |
selection_refrequired | path | From covers/select. |
parent_code | query | The node to descend into. Omit for the top level. |
search | query | Filter this level by name or code. SHA ignores it at the top level. |
access_point | query | Filter billable lines to the access point of the current visit. |
page | query | 1-based. An unreadable value falls back to 1. |
page_size | query | Default 50, capped at 200. An unreadable value falls back to the default. |
{
"unique_patient_id": "2029090000000046",
"selection_ref": "cov-8f3a21c4",
"cover": {
"payer_code": "2029",
"payer_name": "Social Health Authority",
"scheme_codes": ["1", "2"],
"scheme_names": ["SHIF", "UHC"],
"member_identifier": { "type": "CLIENT_REGISTRY_NO", "value": "CR56********" },
"valid_to": "2034-09-30"
},
"facility": { "id": "12345", "id_type": "fr-code" },
"parent_code": "SHA-07-SC-01",
"breadcrumb": [{ "code": "SHA-07-SC-01", "name": "" }],
"nodes": [
{
"code": "SHA-07-001",
"name": "Management of medical cases",
"kind": "service_line",
"access_point": "IP",
"fund": "SHIF",
"tariff": { "applicable": "3500", "basis": "payer_overall_for_facility" },
"authorization": {
"needs_preauth": false,
"needs_doctor_authorization": false,
"needs_manual_approval": false
},
"coverage": { "uses_left": null, "amount_left": null, "known": false },
"next_action": {
"type": "obtain_consent",
"method": "POST",
"endpoint": "/api/v2/patients/2029090000000046/authentications",
"then": {
"type": "start_visit",
"method": "POST",
"endpoint": "/api/v2/visits?selection_ref=cov-8f3a21c4&service_code=SHA-07-001",
"requires": [
{ "field": "service_type", "note": "INPATIENT or OUTPATIENT — the desk's own call, which no earlier stop decides." }
]
}
}
}
],
"page": { "number": 1, "size": 50, "total": 1 },
"as_of": "2026-08-14T09:15:20Z",
"unavailable": []
}curl -X GET "$RAIL_BASE_URL/api/v2/patients/{unique_patient_id}/eligibility/{selection_ref}/benefits" \
-H "Authorization: Bearer $RAIL_TOKEN"The top of the response is the context of the selection, carried on every level so a response is
self-contained. cover shows which cover is being spent, with the member identifier masked.
facility is where the call was made from, and id_type is what the benefits system actually
accepted, which is not always what you sent. page carries next and previous, which are this
service's own endpoints and never an upstream URL, and only when there is a page to go to.
Read three things off that response.
The tree is per member. The benefits system is keyed on the member, so two members at the same facility get different trees, and caching one member's tree as "the benefit catalogue" is how a patient gets offered something they are not entitled to.
Several funds can share one tree. Nodes below the top carry a fund where the payer states one, so group by it and label the fund
on screen, because a clinician choosing "Inpatient" needs to know whether they are spending the
national benefit or the employer fund.
The pages are small by default: 50 rows, at most 200. Read every page, following page.next,
because a sync that reads page one only ships part of a tree and looks like it worked.
What next_action can be
Every node ends in a next_action of the shape {type, method, endpoint, requires[], then}, and
which type it is depends on what the node is, and on whether the patient has a visit open.
| Call | What it does | What you keep |
|---|---|---|
filter_benefits | A group with more beneath it. GET the endpoint: this same route with parent_code set to the node's code. | endpoint |
obtain_consent | A billable line, and the patient has no visit open. POST the endpoint, the consent ceremony. then is the start_visit that follows it. | endpoint, then |
start_visit | A billable line, offered directly when the rail could not find out whether a visit is open. POST /api/v2/visits. requires names consent_ref and service_type. | endpoint, requires |
add_service_line | A billable line, and the patient already has one visit open. POST the service-lines endpoint of that visit. requires names the code and cover, and consent_ref when that cover holds no authorised line on the visit yet. | endpoint, requires |
resolve_open_visits | The patient has more than one open visit. No endpoint, because no call fixes it: someone has to close the visits that are finished. | — |
none | The node is blocked. No endpoint. status is blocked and blocked_by says why. | blocked_by |
method says how to call endpoint, and it is published because the reasonable guess is wrong as
often as it is right: the visit endpoints are POST-only. requires names inputs the call needs that
this response does not carry, and where to get each. then is the call that follows this one when
this action is a step on the way somewhere rather than the destination.
A node that cannot be used
A node the facility cannot use is returned, not hidden, so a desk can say why.
Code
blocked_by is one of PROVIDER_SUSPENDED_FOR_PAYER, OUT_OF_PANEL, SCHEME_RULE_NOT_SET,
BENEFIT_NOT_BILLABLE or BENEFIT_EXHAUSTED. The first four are the payer's verdict on the facility,
and they win over the last: a suspended or off-panel facility is not reported as exhausted, because
that would send a desk to chase a balance that was never the problem. For an
exhausted node detail is the payer's own words when it sent any, and otherwise the coverage
object. The visit stop checks the balance again: hiding an action is presentation, refusing the
write is the control.
Failures on this call
| Status | error | Meaning |
|---|---|---|
| 400 | FACILITY_NOT_IN_BENEFITS | The benefits system does not recognise the facility. path is X-Facility-Id. Check both headers; retrying will not help. |
| 400 | FACILITY_CONTEXT_MISSING | A facility header is absent or invalid. |
| 404 | SELECTION_NOT_FOUND | No selection is known by that reference, including one that belongs to another patient. detail echoes the reference. |
| 404 | BENEFIT_NOT_FOUND | parent_code names nothing in this member's tree, or is a terminal node with nothing beneath it. Walk down from the top again. |
| 409 | SELECTION_LAPSED | The cover has ended or the record is inactive. Fetch the covers and select again. |
| 409 | SELECTION_UNUSABLE | The stored selection names no payer, so there is no benefits system to ask. Select the cover again. |
| 502 | BENEFITS_UNAVAILABLE | The benefits system could not be reached, or none answers for this payer. The tree is unknown, not empty: retry. detail carries unavailable[] when every source failed. |
| 503 | BENEFITS_UNAVAILABLE | This deployment has no clinical store wired to read the selection from. |
Do not read an empty nodes as "nothing is covered". A benefits system that could not answer is a
502, not an empty page.
What a service line declares
This is the densest object in the step. Every field on it changes behaviour somewhere later, and it
appears only on a node whose kind is service_line.
| Call | What it does | What you keep |
|---|---|---|
tariff.applicable | What the payer pays, already resolved for the facility that asked, as a decimal string. The rail does not choose among level tariffs for you and cannot choose wrongly. tariff.basis says where it came from, so a queried amount can be explained. | the applicable tariff |
tariff.fallback | What applies once the primary is spent, when the payer states one. | if present |
authorization.needs_preauth | Sends you to 06 · Preauthorize before the work may be done. Readable here, hours before a patient is on a table. | the flag |
authorization.needs_manual_approval | A human has to approve, not just a system. That is a different waiting time and should be a different message on screen. | the flag |
authorization.needs_doctor_authorization, doctors_required | A doctor's sign-off, and how many doctors. Absent from most lines. | the flags |
authorization.specialised | The extra preauth queues this line needs, such as renal or oncology, as a list of names. Absent from most lines, decisive on the ones that carry it. | the list |
authorization.required_for_preauth, required_for_claim | The documents needed before the service and at claim time. Missing one is the commonest reason a valid claim is rejected. | the lists |
copay | What the member pays on top: type FLAT or PERCENTAGE, a decimal value, and applies_to naming the charge categories. A percentage applies to the claim total. Absent means the payer stated none, which is not the same as zero. | type, value |
limits | How often a line may be used: annual_quantity, global_period_days, is_multisession. These are definitions, not balances. is_multisession is set only where the payer reports it, and the SHA source does not carry it. | the definitions |
Reading the balance
Every node carries coverage, and it is two numbers on purpose, both computed by the rail so that
two clients cannot disagree about whether a member can be treated.
| Call | What it does | What you keep |
|---|---|---|
uses_left | How many more times this may be used. null when unknown; 0 blocks the node. | the count |
amount_left | The money left, as a decimal string so it cannot pick up a float's rounding. null when unknown; a zero of any spelling (0, 0.00) blocks the node. | the amount |
known | False when neither number could be established. Say "balance unknown" rather than rendering two nulls as zeroes. | the flag |
amount_limit | The most this node may draw, which is not the same fact as the amount left. A sub-benefit can sit under a pool of three million and still be capped at two hundred and fifty thousand of it. Absent when the payer stated no cap of its own. | if present |
shares_parent_limit | True when amount_left belongs to a pool this node draws from rather than to the node itself. The difference between "you have this" and "you have this until a sibling spends it". | if true |
Most limits count visits rather than money
Anyone arriving from private insurance has to unlearn this one. A limit here is as likely to be a number of times as a sum of money.
| Call | What it does | What you keep |
|---|---|---|
Count-based | One per year, one per week, dialysis twice weekly up to a yearly maximum. The limit is a tally of sessions or visits. | uses_left |
Money-based | A monetary ceiling per benefit, the shape private insurance uses throughout. | amount_left |
Both, on one member | Public-officer add-on covers behave like traditional insurance, so a member can hold count-based limits on their standard cover and money-based limits on the add-on, at the same time. | both, per cover |
A balance screen that only knows how to render currency will show nothing useful for most benefits. Read which kind you are holding before you format it.
There is also a separate patient-level utilisation figure, by intervention. It is a different
question from the one coverage answers, and is not needed to read a balance.
What has been used against a benefit, by patient and intervention code. It takes patient_id and intervention_code as query parameters and answers 400 without both.
utilised, allocatedWhat you keep
| Call | What it does | What you keep |
|---|---|---|
The service line code | The unit everything later prices against, with its access point and authorization needs. | nodes[].code, access_point |
The chain you walked | Which benefit and sub-benefit led here. It is what lets you explain a refusal rather than report one, and the response will not give it back to you. | benefit → sub-benefit → service line |
The balance you read | Where one exists. Note whether it was a count or an amount, and whether it is shared with a pool. | coverage |
What breaks here
| Where it surfaces | What the desk sees | What actually went wrong |
|---|---|---|
| Bill | "The line was refused and the code looks correct" | Access point mismatch. An inpatient-only service line was selected during an outpatient visit, and nothing failed at selection time. |
| Preauth | "Nobody told us this needed approving" | authorization was on the service line and was not read. It is present on the billable node only, so a client that stops at the group never sees it. |
| Any picker | "The walk never ends" or "the call for the next level returns 404" | The client is waiting for an empty level. The walk ends on next_action.type, and asking for a service line's code as parent_code is 404 BENEFIT_NOT_FOUND. |
| Any picker | "A clinician cannot find dialysis" | Search filters one level, and SHA ignores it at the top. Drill down, or search over levels you have already fetched. |
| Bill | "It said covered, and the amount came back zero" | A null amount_left rendered as zero, or a shared pool already spent by a sibling. Null is not zero, and not every benefit is balance-tracked. |
| Any picker | "Some benefits are missing or greyed out" | The node is blocked, not gone: read blocked_by. OUT_OF_PANEL and PROVIDER_SUSPENDED_FOR_PAYER are facility problems, BENEFIT_EXHAUSTED is a balance. |
What carries forward
The service line code and its access point go to 06 · Preauthorize where approval is required, and to 07 · Bill where it is priced.
Back to02 · Verify coverWhich cover pays: the covers a member holds, who contributes to each, what selecting one records, and how the selection_ref lapses.
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.

