# 01 · Identify

  This is the step where you work out who is in front of you, using whatever documentation they
  carry. The patient first, then whoever treats them. Each becomes an identity the whole health
  system agrees on.

## What this step is

Someone arrives holding a national ID, a birth certificate, a refugee ID or a membership
number. Whatever it is, this step turns it into the one identifier the health system knows
them by.

Someone tells you who they are, and this step checks that against a national registry and
gives back one official answer. Patients are checked against the client registry, clinicians
against the health worker registry. It is the same question each time, asked of whichever
register is authoritative for it.

The patient's identity is the one everything after this step keys off. The clinician's is what
makes the claim defensible once care has been given.

The rule that makes it work is that an identifier without its type is not an identifier. It
is only a number, and a number resolves to nothing.

Three identities get resolved here: the patient, the clinician treating them, and the facility
they are in. The patient is the one that starts everything, so the rest of this section is
about them. The other two follow further down.

### The four moves of a lookup

The way you resolve a patient is the patient lookup, and it takes four moves:

<BuildSequence
  steps={[
    {
      do: "Pick the document type",
      detail: (
        <>
          The operator chooses what they are holding: National ID, birth certificate, refugee
          ID. That list is{" "}
          <a href="/rail/reference/Value-Sets?system=IDENTIFIER-TYPES">
            fetched from the terminology service
          </a>
          , rather than a constant in your code.
        </>
      ),
    },
    {
      do: "Send the pair",
      detail:
        "The identifier's value and its type travel together, in one call, inside your facility context.",
    },
    {
      do: "Read back one identity",
      detail:
        "The registry resolves the pair to a single record and returns the identifier the health system knows this person by, with minimal masked details so the operator can confirm the right human.",
    },
    {
      do: "Store it and carry it",
      detail:
        "Save that identifier against your own patient record. Use it for every later call in this encounter, and for every future visit by the same person.",
    },
  ]}
/>

Remember this much: <u>an identifier travels with its type, and what comes back is stored
once and reused forever.</u>

### What it looks like at the desk

Three ways to look at the same step: the screen an operator sees, the branches they can land
in, and which system answers whom.

<Wireframe
  title="Find patient"
  actor="Front desk"
  rows={[
    [
      { label: "Document type", kind: "field", span: 2, from: "IDENTIFIER-TYPES", note: "National ID · Alien ID · Refugee ID · Birth certificate · Birth notification · Mandate number · Temporary registration · Passport" },
      { label: "Number on the document", kind: "field", span: 1, note: "Scanned or typed" },
      { label: "Find", kind: "action", span: 1 },
    ],
  ]}
  caption={<>Always two inputs. A single free-text box is the commonest cause of a "patient not found" for a patient who is very much in the registry.</>}
/>

Filled in, with the values a real desk would see:

<Wireframe
  title="Find patient · 1 match"
  actor="Front desk · Example Medical Centre (FAC-2029)"
  rows={[
    [
      { label: "National ID", kind: "field", span: 2, from: "type = NATIONAL_ID", note: "Chosen from the picker" },
      { label: "33121466", kind: "field", span: 1, from: "value", note: "As printed on the card" },
      { label: "Find", kind: "action", span: 1 },
    ],
    [
      { label: "Found 1 patient with National ID 33121466", kind: "text", span: 4, note: "One match is the good case. None means re-check the type; several means ask for a second document." },
    ],
    [
      { label: "Wanjiru M****i M****i", kind: "text", span: 1, from: "name", note: "First name in full and the rest masked, enough to recognise but not enough to copy" },
      { label: "National ID · 3312****", kind: "text", span: 1, from: "matched_on", note: "Masked, and always shown with its type" },      { label: "Joined Oct 9, 2024", kind: "text", span: 1, from: "registered_on" },
      { label: "Alive", kind: "field", tone: "positive", span: 1, from: "is_alive", note: "The only status this step reports. Cover is asked at the next stop." },
    ],
    [
      { label: "SIL-UPI-0091774", kind: "text", span: 3, from: "unique_patient_id", note: "The one value to store. Every later call takes it as input." },
      { label: "View", kind: "action", span: 1, note: "Continue to cover selection" },
    ],
  ]}
  caption={<>The identifier you type is not the one you keep. You type a National ID and you store what the registry returns.</>}
/>

<Mermaid chart={`sequenceDiagram
    autonumber
    actor D as Front desk
    participant H as Your HMIS
    participant R as The rail
    participant C as Client registry

    D->>H: Picks document type, enters number
    H->>R: Lookup (type + value + facility context)
    R->>C: Resolve the identifier pair
    C-->>R: One record, or none, or several
    R-->>H: unique_patient_id, masked details, next_action
    H-->>D: Confirm the human, then continue
    Note over H,R: Store unique_patient_id now.<br/>A returning patient is never re-resolved.
`} />

<Mermaid chart={`flowchart TD
    A["Operator submits type + value"] --> B{Registry answered?}
    B -- "No, timed out" --> T["Pending, retry once,<br/>then let the desk continue"]
    B -- Yes --> C{How many records matched?}
    C -- None --> N["Not found<br/>Re-check the TYPE first"]
    C -- "More than one" --> X["Conflict<br/>Ask for a second document"]
    C -- Exactly one --> D{Is the person alive?}
    D -- No --> E["Stop. Escalate clinically."]
    D -- Yes --> F["Identity resolved<br/>store unique_patient_id"]
    F --> G["Carry it to 02 · Eligibility,<br/>where cover is asked"]
`} />

The request is the pair and nothing else. Facility travels in headers, on every call.

<Operation
  method="POST"
  path="/api/v2/patients/lookup"
  summary="Resolve an identifier pair to one unique_patient_id."
  params={[
    { name: "X-Facility-Id", type: "header", required: true, description: "The facility this call is made from." },
    { name: "X-Facility-Id-Type", type: "header", required: true, description: "How to read it: mfl, license-number, fr-code, registration-number or fid." },
    { name: "identifier.type", type: "string", required: true, description: "A code from the identifier-types value set." },
    { name: "identifier.value", type: "string", required: true, description: "As printed on the document." },
    { name: "lookup_ref", type: "string", required: false, description: "Echo a previous one to make a retry idempotent rather than a second lookup." },
  ]}
  request={`{
  "identifier": { "type": "NATIONAL_ID", "value": "33121466" }
}`}
  response={`{
  "lookup_ref": "LKP-2026-08-05-000512",
  "unique_patient_id": "SIL-UPI-0091774",
  "is_alive": true,
  "biodata": {
    "name": "Wanjiru M****i M****i",
    "date_of_birth": "2000-06-29",
    "gender": "MALE",
    "registered_on": "2024-10-09"
  },
  "matched_on": { "type": "NATIONAL_ID", "value": "3312****" },
  "identifiers": [
    { "type": "NATIONAL_ID",        "value": "3312****",          "verified": true },
    { "type": "CLIENT_REGISTRY_NO", "value": "CR61*************", "verified": true },
    { "type": "SHA_NUMBER",         "value": "SHA0*********",     "verified": true }
  ],
  "contacts": {
    "phones": [{ "value": "+254700****848", "verified": true }],
    "emails": [{ "value": "roy***@example.co.ke", "verified": false }]
  },
  "next_action": {
    "type": "select_cover",
    "endpoint": "/api/v2/patients/SIL-UPI-0091774/covers"
  }
}`}
/>

There is no consent block. Proving the member agreed is [3 · Consent](/rail/stops/Consent).
This call resolves an identity; it does not read a record.

#### What is redacted, and what is not

`identifiers[]` is the array the operator actually decides on. Each value shows enough to
recognise and not enough to reconstruct. The reveal is bounded so that at least the last four
characters of any identifier stay hidden, which stops the response becoming a guessing oracle.
Internal identifiers are absent rather than masked, because a masked internal id still tells a
caller the record exists in a system they have no business knowing about.

| Value | What comes back |
|---|---|
| Name | First name in full, later names keep first and last letter: `Wanjiru Mwangi Muturi` → `Wanjiru M****i M****i` |
| National ID | First 4: `33121466` → `3312****` |
| Phone | `+254700123848` → `+254700****848` |
| Email | Local part masked, domain kept: `roy.thande@example.co.ke` → `roy***@example.co.ke` |
| Registry / scheme identifiers | At most 4 leading characters: `CR6154087394882-8` → `CR61*************` |
| `unique_patient_id` | Raw. The one value you store is the one value not hidden |
| `date_of_birth` | Raw. Masking it left too little to recognise a person by |

Two habits follow. Do not mask again on top of this, or you will hide the very characters an
operator confirms against. And do not assume masking preserves length: name masking does,
phone masking does not.

#### Try it

<Postman collection="rail-01-identify.postman_collection.json" label="01 · Identify" />

Or run it directly. Set a token with Set API Token in the header, then open
[`patient.lookup` in the reference](/rail-api#tag/01--identify/post/api/v2/patients/lookup)
for the parameters, both failure bodies, and a Try it console for when the sandbox is live.

```bash
curl -sS -X POST "$RAIL_BASE_URL/api/v2/patients/lookup" \
  -H "Authorization: Bearer $RAIL_TOKEN" \
  -H "X-Facility-Id: FAC-2029" \
  -H "X-Facility-Id-Type: fr-code" \
  -H "Content-Type: application/json" \
  -d '{ "identifier": { "type": "NATIONAL_ID", "value": "33121466" } }'
```

#### The two answers that are not a 200

A `404` means re-check the type before re-typing the number. A `409` is a prompt rather than a
failure, and it carries what the desk needs to act:

```json
{
  "error": "IDENTIFIER_CONFLICT",
  "message": "National ID 33121466 matches 2 records. Ask for a second document.",
  "path": "identifier.value",
  "fix_stage": "identify",
  "detail": {
    "match_count": 2,
    "candidates": [
      { "biodata": { "name": "Wanjiru M****i M****i", "gender": "MALE" },
        "identifiers": [{ "type": "CLIENT_REGISTRY_NO", "value": "CR61*************" }],
        "contacts": { "phones": [{ "value": "+254700****848", "verified": true }] } },
      { "biodata": { "name": "Wanjiru M****a K****u", "gender": "MALE" },
        "identifiers": [{ "type": "CLIENT_REGISTRY_NO", "value": "CR77*************" }],
        "contacts": { "phones": [] } }
    ],
    "disambiguate_with": ["CLIENT_REGISTRY_NO", "PASSPORT_NO", "BIRTH_CERTIFICATE_NO"]
  }
}
```

A candidate carries no `unique_patient_id`. Until the operator has disambiguated, handing it
over would let a caller resolve a person by guessing, which is the whole reason the answer is
withheld. `disambiguate_with` names the types that would actually separate these two records,
which is more use than "collect a second identifier".

That covers one adult with one document.

  A child with no document of their own, the six situations a front desk actually meets, and what to
  do when the registry will not answer.

## Three identities, not one

A claim is never an assertion about one person. Every claim eventually has to say: *this
person, treated by that clinician, at this facility.* That is three identities and three
national registries, and they all resolve the same way, with an identifier plus what kind of
identifier it is.

What matters more than the count is the order you build them in.

<BuildSequence
  steps={[
    {
      do: "First · the patient",
      detail: (
        <>
          Resolved at the front desk, before anything clinical or financial happens. Nothing
          else can start without it: there is no cover to select, no consent to take and no
          visit to open for someone the registry has not named. Keep{" "}
          <code>unique_patient_id</code>.
        </>
      ),
    },
    {
      do: "Then · the clinician",
      detail: (
        <>
          Resolved when someone is chosen to deliver care, later in the encounter, and
          possibly more than once as staff change. It does not block the patient being
          identified, but it does block a defensible claim. Keep the{" "}
          <code>registration number</code> and the regulator that issued it.
        </>
      ),
    },
    {
      do: "Always · the facility",
      detail: (
        <>
          You never resolve this one. Your credential already carries it, and every call
          above is made inside it. Keep <code>facility_code</code> set once in your HTTP
          client rather than at each call site.
        </>
      ),
    },
  ]}
/>

Build them in that order. Nothing works without the patient. The clinician is what turns care
already given into a claim that survives review. The facility is configuration rather than
workflow.

Most integrations stop after the first, because the first is what unblocks a demo. The cost
shows up weeks later, in a rejection that names a practitioner instead of a patient.

  A patient is a number plus its document type, a clinician a number plus its regulator, a
  facility a code plus the registry it came from. Learn the pairing rule once and the other two
  cost you a form each. That is why this page teaches the patient in full and then moves fast.

Part A is the lookup you have just read. Parts B and C follow.

## Why this step exists

Everything downstream keys off one value, `unique_patient_id`: cover selection, eligibility,
authorisation, preauthorisation, reservation, billing and claims. This step is where that value
comes from, and it is the only place it comes from.

Lookup is kept separate from eligibility on purpose. A member holding three covers should not
wait on three balance checks before anyone has asked which cover they are using today. So this
call stays light, with enough to render a chooser and nothing that needs a balance.

Consent is not asked here. Resolving an identity is not the same as reading a record, and the
two are separated on purpose: proving the member agreed is [3 · Consent](/rail/stops/Consent),
once a cover is in view and there is something to consent *to*. What this call returns is
minimal and masked, enough to confirm you have the right human in front of you without handing
over the record.

## The call

Two calls make this step. One tells you which document types are currently accepted, the other
resolves the pair. Both live in the reference, with every parameter, every response code, and a
Try it console for when the sandbox is live.

<Wire
  rows={[
    { call: "GET · identifier types", does: "The document types a desk may choose from today. Fetch and cache it; never hard-code it.", keep: "code, version" },
    { call: "POST · lookup", does: "Resolves the identifier pair to one registry identity, inside your facility context.", keep: "unique_patient_id" },
  ]}
/>

Open [**01 · Identify in the Rail API reference**](/rail-api/01-identify) to read or run them.
The sidebar there is this same rail, 00 through 08, so the stop you are reading and the
endpoints you are calling stay in step. Connect once with the credentials widget in the header
and every call on that page becomes runnable.

The document types are not part of the rail's API at all. They are a coded list on the
terminology service, and this is the call:

<ValueSet
  system="IDENTIFIER-TYPES"
  label="Identifier types"
  what="The document types a front desk may choose from: national ID, birth certificate, refugee ID, and the rest. The list changes as types are added and withdrawn, so a hard-coded copy is correct the day you write it and quietly wrong months later."
/>

Three things the reference will not tell you, which is why this page exists.

<Wire
  rows={[
    { call: "Send the pair", does: "A value without its type resolves to nothing. The reference lists both fields; only this page says why sending one is the commonest false negative.", keep: "type + value" },
    { call: "Keep one field", does: "Every operation in the reference names the field worth persisting. Store it against your own patient record the first time you see it.", keep: "unique_patient_id" },
    { call: "Fetch, never embed", does: "The type list moves. A constant in your code is correct the day you write it and quietly wrong months later.", keep: "the cached version" },
  ]}
/>

## What you keep

<Wire
  rows={[
    {
      call: "POST /api/v2/patients/lookup",
      does: "Resolves the person to one identity, with masked details to confirm them",
      keep: "unique_patient_id",
    },
    {
      call: "GET /api/v1/terminology/concepts?code_system=IDENTIFIER-TYPES",
      does: "Tells you which identifier types are currently accepted",
      keep: "codes[].code",
    },
    {
      call: "GET /api/v1/professionals",
      does: "Resolves the attending clinician and returns a licence verdict",
      keep: "licence.status",
    },
  ]}
/>

`unique_patient_id` is the value that ties the rest together. Store it against your own patient
record the first time you see it. Everything after this step takes it as input, and re-resolving somebody you have
already resolved is the largest source of avoidable delay at a front desk.

Keep `lookup_ref` too, for the length of the interaction. It is the audit handle for this
resolution, and echoing it on a retry de-duplicates the attempt rather than recording a second
one.

## A · The patient

Covered above: an identifier pair resolves to one registry identity, and every later call hangs
off it.

One detail belongs here. The lookup also returns masked verified contacts, the phone and
email the member has actually confirmed. Those are the channels a one-time code goes to at
[3 · Consent](/rail/stops/Consent). When someone says "the code never arrived", start here. An
unverified channel is a dead end rather than a delay.

One habit is worth more than the rest: never store a name as free text where the registry gives
you an identifier. A typed name is only a label. The registry identifier is evidence, and
evidence is what you get asked for months later.

## B · The healthcare worker

Every billed line carries the clinician who performed it, their licence, and the body that regulates
them. Resolving that here makes the line defensible later, and it is the last cheap moment to
discover a lapsed licence.

The question is the same one asked of the patient, who are you and does your paperwork hold up,
and it is answered the same way: a number plus what kind of number it is. The regulator is half of
a registration number, exactly as a document type is half of a patient identifier.

It happens at a different moment, though, and usually on a different screen: the patient is resolved
at the desk, the clinician when care is assigned.

  The regulator picker, what a licence verdict looks like when it holds and when it does not, cadres
  and specialities, and the four things the health worker registry buys you, with the screens, the
  sequence and the workflow.

## C · The facility

  Facility resolution is real. The registry decides the tariff variant, whether a benefit is
  billable at your site at all, and which consent factors are offered. You do not call it to get
  started, though: your credential already carries a facility context, and every call on the
  rail is made inside it.

  The rest waits until the facility contracts settle. Two rules are safe to build on now. Never
  store a facility level. Read it each time, because levels and contracts change, and a stored
  level produces mispricing that is very hard to trace back. And expect a newly onboarded
  facility to be briefly absent, which is a transient to retry rather than a bug to report.

## Proving it is the same person

Resolving an identifier tells you a record exists. It does not tell you the person holding the
document is the person in the record. That proof happens at
[03 · Consent](/rail/stops/Consent), where you send a purpose and the rail
[decides the factor](/rail/stops/Consent#which-factors-a-facility-can-even-offer). What is
possible there is decided here, by flags this step returns, so it belongs on this page.

There are two paths, and they differ in kind rather than in degree.

<Wire
  rows={[
    { call: "Adults", does: "Verified against the national biometric database. Those prints were captured by the state, not by you, so template age and quality are outside anyone's control.", keep: "match or no match" },
    { call: "Minors, 7 to 17", does: "No national record exists, so they are enrolled at the point of care and the prints are sent to the biometric service. This is the only path where the prints originate with the provider.", keep: "enrolment outcome" },
  ]}
/>

  The adult match rate is around 70%. That number changes what you are building: a fallback
  used 3% of the time is an error screen, and a fallback used 30% of the time is a main path. Design
  the one-time-code route as a first-class flow with its own screens rather than an exception
  handler behind a failed scan.

Because minors are enrolled rather than matched, not every desk is an enrolment point. If your
integration serves paediatrics, find out whether the sites you are deploying to can enrol before
you assume the fingerprint path exists for them at all.

## What breaks here

Identity failures rarely announce themselves where the mistake was made. They surface later
disguised as something else, usually as "the patient's fault" or "the payer's fault".

**Patient identity**

<CostTable
  rows={[
    {
      where: "Lookup",
      symptom: "\"This patient is not in the system\"",
      cause:
        "The identifier was sent without its type, or with the wrong type. Genuine absence is rare; a mismatched type is not.",
    },
    {
      where: "Lookup",
      symptom: "\"It worked last month and now it does not\"",
      cause:
        "A hard-coded identifier type list. A type was withdrawn or renamed upstream, and the picker is still offering the old code.",
    },
    {
      where: "Cover selection",
      symptom: "\"They definitely have cover, but nothing shows\"",
      cause:
        "Resolved to the wrong person. The identifier matched more than one record and was silently disambiguated instead of being sent back for a second document.",
    },
    {
      where: "Cover selection",
      symptom: "\"The system says not covered, so we sent them away\"",
      cause:
        "A cover result treated as a lookup failure. The identity resolved; only the cover did not, and the reason and possible solution were dropped instead of shown.",
    },
    {
      where: "Consent",
      symptom: "\"The OTP never arrives\"",
      cause:
        "The contact on the registry is stale or unverified. The masked contacts returned at lookup are what consent will actually use, so offer only verified ones.",
    },
    {
      where: "Anywhere mid-encounter",
      symptom: "\"The same patient is on the bill twice\"",
      cause:
        "The patient was re-resolved partway through instead of the identity being carried forward, producing two identifiers for one person.",
    },
    {
      where: "Front desk queue",
      symptom: "\"Registration is slow at peak\"",
      cause:
        "Every visit triggers a fresh lookup. A returning patient should be read from your own record, because the registry identifier is stored once and reused for life.",
    },
    {
      where: "Claim",
      symptom: "\"Rejected, and we cannot see why\"",
      cause:
        "A name was stored as free text at intake instead of the registry identifier. A typed name is a label; the registry identifier is the evidence.",
    },
  ]}
/>

**Clinician identity**

<CostTable
  rows={[
    {
      where: "Consent",
      symptom: "\"The scan keeps failing for this patient\"",
      cause:
        "Adult prints are matched against the national database, and roughly three in ten do not match. This is the expected path rather than a fault, so fall back to a one-time code.",
    },
    {
      where: "Consent, paediatrics",
      symptom: "\"There is no fingerprint on file for this child\"",
      cause:
        "Minors have no national record and must be enrolled at the point of care. Not every site is an enrolment point.",
    },
    {
      where: "Clinician selection",
      symptom: "\"That registration number does not exist\"",
      cause:
        "The wrong regulator. The same digits can be valid under KMPDC and under another council, and the number is meaningless without the body that issued it.",
    },
    {
      where: "Billing",
      symptom: "\"Line rejected: practitioner not eligible\"",
      cause:
        "A lapsed or suspended licence, or the wrong speciality for the intervention. Licence status is checked live at billing, and checking it at selection turns this into a fixable conversation.",
    },
    {
      where: "Billing",
      symptom: "\"We cannot say who performed this\"",
      cause:
        "The clinician was picked from a typed internal list rather than the registry, so the line carries a name but no defensible registration.",
    },
  ]}
/>

Two patterns account for most of those rows. The first is an identifier sent without its type:
a patient number missing its document type, a registration number missing its regulator. The
second is a state that exists in the API but not on the screen: a conflict, an unusable cover, a
request nobody has sent yet. Neither is hard to fix, and both are expensive to find at a claim.

### Errors

| HTTP | `error` | When |
|------|---------|------|
| 404 | `MEMBER_NOT_FOUND` | The identifier does not resolve to any registry record. |
| 409 | `IDENTIFIER_CONFLICT` | It matches more than one record and cannot be disambiguated, so collect a second identifier. |

A 409 is not a failure to retry. It is a prompt to ask the person at the desk for something
else.

## What carries forward

You leave this step holding three things. Each is an input somewhere later, and losing one
means coming back for it, usually at the worst moment.

<Wire
  rows={[
    { call: "unique_patient_id", does: "The patient's identity. Taken as input by cover selection, consent, the visit, preauthorisation, billing and the claim.", keep: "Store on your patient record, permanently" },
    { call: "Registration number", does: "The attending clinician, with the regulator that issued it. Every billed line carries it; preauthorisation re-checks the licence.", keep: "Store on the encounter" },
    { call: "facility_code", does: "Where this is happening. It is already in your credential, but it scopes every rule that follows, including pricing.", keep: "Set once in your HTTP client" },
  ]}
/>

The rail tells you where to go next. Every response carries `next_action`: switch on its
`type`, then use the `endpoint` it hands you instead of building one, because it already carries
the identity you just resolved.

```json
"next_action": {
  "type": "select_cover",
  "endpoint": "/api/v2/patients/SIL-UPI-0091774/covers"
}
```

Constructing that URL yourself works right up until a path changes. Reading it from the
response never breaks, and it lets the sequence be reordered without touching your code. The
full contract is at [next_action](/rail/concepts/Next-Action).

**Next:** [02 · Verify cover](/rail/stops/Entitle). With an identity resolved, the question becomes
which cover pays.

<AgentPrompt title="Build Identify with an agent" filename="identify.instructions.md">{`You are integrating an HMIS with the Savannah unified payer rail.

Task: implement step 1 of 8, Identify. Three identities are resolved here.

A · THE PATIENT
  Fetch the accepted document types first, and cache them by version:
    GET /api/v1/terminology/concepts?code_system=IDENTIFIER-TYPES&version=serving
  Then resolve the person by identifier pair (value + type):
    POST /api/v2/patients/lookup   { "identifier": { "type": ..., "value": ... } }
  Send X-Facility-Id and X-Facility-Id-Type on every call. Set them in the HTTP client.
  Do NOT send a consent block here. Identity resolution takes no consent; proving the
  member agreed is stop 03.
  Persist from the response:
    unique_patient_id   // the thread; every later step takes this as input
    lookup_ref          // echo it on a retry so the attempt is de-duplicated
    contacts            // masked; these are the channels consent will use later
  Treat unique_patient_id as opaque: store it, send it, never parse it.
  Everything masked in the response is already safe to render. Do not mask it again, or you
  will hide the characters the operator confirms against.
  On 409, read detail.candidates and detail.disambiguate_with and ask for one of those
  document types. A candidate never carries a unique_patient_id.

B · THE HEALTHCARE WORKER
  Resolve the attending clinician by regulator + registration number. The regulator is part
  of the identifier: KMPDC, Clinical Officers Council, Nursing Council of Kenya.
  Check licence status live and refuse a suspended or lapsed practitioner at selection time.
  Do not attach them and discover it at billing.
  Persist the registration number and its regulator on the encounter.
  Where an intervention needs clinical sign-off, request approval and model these states
  explicitly: not sent, pending, approved, declined. Provide a way to re-check; the answer
  arrives out of band.

C · THE FACILITY
  Do not call for this. Your credential carries the facility context; set the facility headers
  once in your HTTP client rather than at individual call sites. Never store a facility level.
  Read it, because it changes and a stored level produces mispricing.

PROVING IDENTITY (decided here, exercised at Consent)
  Adults are matched against the national biometric database; expect roughly a 70% match
  rate and treat the one-time-code fallback as a MAIN path with its own screens, not an
  error handler.
  Minors aged 7-17 have no national record: they are enrolled at the point of care, and
  not every site can enrol. Do not assume a fingerprint path exists for paediatrics.
  Carry forward the flags this step returns (biometric enrolment and OTP eligibility)
  rather than re-deriving them at Consent.

RULES
- An identifier is always a pair. Never send a bare number: a patient identifier without its
  type, or a registration number without its regulator, is the commonest false "not found".
- Never hard-code a coded list. Fetch, cache by version, filter to active for pickers.
- Never store a patient name as free text where a registry identifier exists.
- Mask names, identifiers and contacts on screen.
- Handle a conflict by prompting for a second document, not by retrying the same call.
- A resolved patient with unusable cover is not a lookup failure: surface the reason and the
  possible solution, and let the encounter proceed as cash or on another cover.
- Every response carries next_action. Switch on next_action.type and take the endpoint from
  the response rather than constructing it. Do not hard-code the sequence.`}</AgentPrompt>
