Registration Service — API Contracts
Status: populated Owner: TBD Last updated: 2026-04-17 Companion: Service Template · 03 platform-services · 02 DDD
Common Headers
| Header | Value | Required |
|---|---|---|
Authorization | Bearer <JWT> (Keycloak RS256) | Yes |
Content-Type | application/json | Yes (mutations) |
Idempotency-Key | Opaque token (optional; same semantics as clientMutationId) | Optional on POST /patients |
X-Ghasi-Break-Glass-Reason | Free text (triggers break-glass audit path) | Optional |
X-Ghasi-Break-Glass-Reference | Incident/ticket id | Optional |
Module entitlement guard: ehr.registration required on all routes.
1. Patients
POST /api/v1/patients
Register a new patient.
| Attribute | Value |
|---|---|
| Auth scope | patient_record:write, FRONT_DESK, ADMIN, CLINICIAN |
| Idempotency | Idempotency-Key header or clientMutationId body field; cached 24h |
Request body (selected fields)
| Field | Type | Required | Notes |
|---|---|---|---|
names[] | array | Conditional | Required unless isUnidentified=true |
birthDate | YYYY-MM-DD | Conditional | Required unless isUnidentified=true |
sex | M|F|O|U | No | |
preferredLang | string | No | Must be in allowed language set |
identifiers[] | array | No | {system, value, typeCode, issuer?, verificationStatus?, confidence?} |
telecoms[] | array | No | {system, value, use} |
addresses[] | array | No | {country, stateProvince, district, city, line[], postalCode, use} |
nextOfKin[] | array | No | {relationship (code), name, phone?, relatedPatientId?, priority?} |
consentFlags[] | array | No | {type, granted, channel} |
isUnidentified | boolean | No | Emergency intake; bypasses mandatory field check |
isProvisional | boolean | No | Partial identity; mutually exclusive with isUnidentified |
isNewborn | boolean | No | Requires mother/guardian linkage |
intakeContext | object | No | {facilityId?, locationDescription?, arrivalContext?, encounterId?, notes?} |
Responses
| HTTP | Condition |
|---|---|
| 201 | Patient created — returns PatientResponseDto with id, stablePatientId, mrn, all nested arrays |
409 DUPLICATE_DETECTED | MPI score ≥ 85 (includes matches[] with id, mrn, score) |
409 IDENTIFIER_ALREADY_ASSIGNED | Same system+value exists on another patient |
400 MISSING_MANDATORY_FIELD | Tenant required fields not satisfied |
400 INVALID_NATIONAL_IDENTIFIER | National ID digit-length failed |
400 RELATIONSHIP_CODE_INVALID | NOK relationship code not in catalog |
GET /api/v1/patients
Search patients (tenant-scoped, minimum-necessary enforced).
| Attribute | Value |
|---|---|
| Auth scope | REGISTRATION_PATIENT_READ |
Query parameters
| Param | Type | Notes |
|---|---|---|
q | string | Free-text name search (≥3 chars counts as two weak factors) |
patientId | UUID | Exact internal ID (strong criterion) |
mrn | string | Exact MRN (strong criterion) |
identifier | system:value | Exact identifier (strong criterion) |
birthDate | YYYY-MM-DD | Weak factor |
sex | M|F|O|U | Weak factor |
page | number | 1-based |
limit | number | 1–100 |
Response 200
{
"data": [ { "id": "...", "stablePatientId": "...", "mrn": "...", "names": [], "identifiers": [], ... } ],
"pagination": { "total": 1, "page": 1, "pageSize": 20, "totalPages": 1 }
}
| HTTP | Error | Condition |
|---|---|---|
| 400 | PATIENT_SEARCH_INSUFFICIENT_CRITERIA | No strong criterion + fewer than two weak factors |
GET /api/v1/patients/kin-relationship-codes
Returns catalog of allowed NOK relationship codes.
| HTTP | Response |
|---|---|
| 200 | [{ "code": "MOTHER", "displayName": "Mother", "sortOrder": 10 }, ...] |
GET /api/v1/patients/unidentified-reconciliation
Lists active unidentified patient charts for review.
| Param | Notes |
|---|---|
slaBreachedOnly | boolean; requires REGISTRATION_UNIDENTIFIED_SLA_DAYS env |
GET /api/v1/patients/:id
Get patient by UUID.
| Param | Notes |
|---|---|
includeIncomingRelatedLinks | boolean; adds reverse NOK view |
| HTTP | Error | Condition |
|---|---|---|
| 200 | PatientResponseDto | — |
| 404 | PATIENT_NOT_FOUND | Not in tenant |
PUT /api/v1/patients/:id
Update patient demographics (optimistic lock).
Required body field: version (integer).
| HTTP | Error | Condition |
|---|---|---|
| 200 | Updated PatientResponseDto | |
| 409 | OPTIMISTIC_LOCK_CONFLICT | version mismatch |
| 409 | IDENTIFIER_ALREADY_ASSIGNED | system+value collision |
PATCH /api/v1/patients/:id/vital-status
Record or correct deceased status.
Auth: patient_record:record_vital_status (elevated; front-desk alone insufficient)
Request body
| Field | Type | Notes |
|---|---|---|
version | number | Required |
deceased | boolean | Required |
deceasedDateTime | ISO-8601 | Optional; must be omitted when deceased=false |
| HTTP | Error | Condition |
|---|---|---|
| 200 | PatientResponseDto | |
| 400 | DECEASED_DATETIME_WHEN_ALIVE | deceasedDateTime sent with deceased=false |
| 409 | OPTIMISTIC_LOCK_CONFLICT |
POST /api/v1/patients/:id/merge
Merge source into survivor :id.
Auth: REGISTRATION_PATIENT_MERGE
Request body: { "sourcePatientId": "uuid", "mergeReason": "string", "confirmUnidentifiedMerge?": boolean, "confirmProvisionalMerge?": boolean }
| HTTP | Error | Condition |
|---|---|---|
| 201 | Survivor PatientResponseDto | |
| 409 | MERGE_SELF | Survivor = source |
| 404 | PATIENT_NOT_FOUND |
POST /api/v1/patients/:id/unmerge
Reactivate source from survivor (requires REGISTRATION_UNMERGE_ENABLED=true).
Auth: REGISTRATION_PATIENT_MERGE
Request body: { "sourcePatientId": "uuid" }
| HTTP | Error | Condition |
|---|---|---|
| 200 | Survivor PatientResponseDto | |
| 403 | UNMERGE_DISABLED_BY_POLICY | env flag false |
| 400 | UNMERGE_INVALID_STATE | Source not recorded as merged into this survivor |
Portrait Endpoints
| Method | Path | Description | Auth |
|---|---|---|---|
| POST | /api/v1/patients/:id/portrait | Upload portrait; body: {contentType, dataBase64, consentGiven, clinicalDisplayAllowed, retentionDays?} | REGISTRATION_PORTRAIT_WRITE |
| GET | /api/v1/patients/:id/portrait | Download raw image bytes | REGISTRATION_PATIENT_READ |
| GET | /api/v1/patients/:id/portrait/history | Portrait version history metadata | REGISTRATION_PATIENT_READ |
Extension Schema Endpoints
| Method | Path | Description | Auth |
|---|---|---|---|
| GET | /api/v1/extension-schemas | List schema registry rows | REGISTRATION_EXTENSION_SCHEMA_ADMIN |
| GET | /api/v1/extension-schemas/standard-bundles | Canonical schemas for employment, marital, appearance | REGISTRATION_PATIENT_READ |
| POST | /api/v1/extension-schemas | Register new schema version | REGISTRATION_EXTENSION_SCHEMA_ADMIN |
| PUT | /api/v1/patients/:id/extensions/:bundleKey | Save validated extension payload | REGISTRATION_EXTENSION_INSTANCE_WRITE |
| GET | /api/v1/patients/:id/extensions | List extension history (masked by role) | REGISTRATION_EXTENSION_INSTANCE_READ |
2. Encounters
| Method | Path | Description | Auth |
|---|---|---|---|
| POST | /api/v1/encounters | Register encounter | patient_record:write |
| GET | /api/v1/encounters?patientId=&status= | List encounters | authenticated |
| GET | /api/v1/encounters/:id | Get by UUID | authenticated |
| PATCH | /api/v1/encounters/:id/status | Transition status | patient_record:write |
PATCH /encounters/:id/status body: { "status": "arrived", "version": 1, "note?": "string" }
Allowed transitions: planned→arrived, planned→cancelled, arrived→in-progress, arrived→cancelled, in-progress→finished, in-progress→cancelled.
| HTTP | Error | Condition |
|---|---|---|
| 200 | Updated EncounterResponseDto | |
| 409 | INVALID_STATUS_TRANSITION | |
| 409 | OPTIMISTIC_LOCK_CONFLICT | |
| 404 | ENCOUNTER_NOT_FOUND |
3. FHIR R4
Base path: /fhir/R4 (same auth + ehr.registration entitlement).
| Interaction | Path | Notes |
|---|---|---|
| Read | GET /fhir/R4/Patient/:id | Includes photo only when portrait active + consent + retention not expired |
| Search | GET /fhir/R4/Patient?family=&identifier=&birthdate=&_count= | Break-glass headers supported |
| Create | POST /fhir/R4/Patient | Maps to RegisterPatientUseCase |
| Update | PUT /fhir/R4/Patient/:id | Requires meta.versionId (optimistic lock) |
| Read | GET /fhir/R4/Encounter/:id | |
| Search | GET /fhir/R4/Encounter?patient=Patient/:id&status= | |
| Create | POST /fhir/R4/Encounter | |
| Update | PUT /fhir/R4/Encounter/:id |