Skip to main content

Registration Service — API Contracts

Status: populated Owner: TBD Last updated: 2026-04-17 Companion: Service Template · 03 platform-services · 02 DDD

Common Headers

HeaderValueRequired
AuthorizationBearer <JWT> (Keycloak RS256)Yes
Content-Typeapplication/jsonYes (mutations)
Idempotency-KeyOpaque token (optional; same semantics as clientMutationId)Optional on POST /patients
X-Ghasi-Break-Glass-ReasonFree text (triggers break-glass audit path)Optional
X-Ghasi-Break-Glass-ReferenceIncident/ticket idOptional

Module entitlement guard: ehr.registration required on all routes.

1. Patients

POST /api/v1/patients

Register a new patient.

AttributeValue
Auth scopepatient_record:write, FRONT_DESK, ADMIN, CLINICIAN
IdempotencyIdempotency-Key header or clientMutationId body field; cached 24h

Request body (selected fields)

FieldTypeRequiredNotes
names[]arrayConditionalRequired unless isUnidentified=true
birthDateYYYY-MM-DDConditionalRequired unless isUnidentified=true
sexM|F|O|UNo
preferredLangstringNoMust be in allowed language set
identifiers[]arrayNo{system, value, typeCode, issuer?, verificationStatus?, confidence?}
telecoms[]arrayNo{system, value, use}
addresses[]arrayNo{country, stateProvince, district, city, line[], postalCode, use}
nextOfKin[]arrayNo{relationship (code), name, phone?, relatedPatientId?, priority?}
consentFlags[]arrayNo{type, granted, channel}
isUnidentifiedbooleanNoEmergency intake; bypasses mandatory field check
isProvisionalbooleanNoPartial identity; mutually exclusive with isUnidentified
isNewbornbooleanNoRequires mother/guardian linkage
intakeContextobjectNo{facilityId?, locationDescription?, arrivalContext?, encounterId?, notes?}

Responses

HTTPCondition
201Patient created — returns PatientResponseDto with id, stablePatientId, mrn, all nested arrays
409 DUPLICATE_DETECTEDMPI score ≥ 85 (includes matches[] with id, mrn, score)
409 IDENTIFIER_ALREADY_ASSIGNEDSame system+value exists on another patient
400 MISSING_MANDATORY_FIELDTenant required fields not satisfied
400 INVALID_NATIONAL_IDENTIFIERNational ID digit-length failed
400 RELATIONSHIP_CODE_INVALIDNOK relationship code not in catalog

GET /api/v1/patients

Search patients (tenant-scoped, minimum-necessary enforced).

AttributeValue
Auth scopeREGISTRATION_PATIENT_READ

Query parameters

ParamTypeNotes
qstringFree-text name search (≥3 chars counts as two weak factors)
patientIdUUIDExact internal ID (strong criterion)
mrnstringExact MRN (strong criterion)
identifiersystem:valueExact identifier (strong criterion)
birthDateYYYY-MM-DDWeak factor
sexM|F|O|UWeak factor
pagenumber1-based
limitnumber1–100

Response 200

{
"data": [ { "id": "...", "stablePatientId": "...", "mrn": "...", "names": [], "identifiers": [], ... } ],
"pagination": { "total": 1, "page": 1, "pageSize": 20, "totalPages": 1 }
}
HTTPErrorCondition
400PATIENT_SEARCH_INSUFFICIENT_CRITERIANo strong criterion + fewer than two weak factors

GET /api/v1/patients/kin-relationship-codes

Returns catalog of allowed NOK relationship codes.

HTTPResponse
200[{ "code": "MOTHER", "displayName": "Mother", "sortOrder": 10 }, ...]

GET /api/v1/patients/unidentified-reconciliation

Lists active unidentified patient charts for review.

ParamNotes
slaBreachedOnlyboolean; requires REGISTRATION_UNIDENTIFIED_SLA_DAYS env

GET /api/v1/patients/:id

Get patient by UUID.

ParamNotes
includeIncomingRelatedLinksboolean; adds reverse NOK view
HTTPErrorCondition
200PatientResponseDto
404PATIENT_NOT_FOUNDNot in tenant

PUT /api/v1/patients/:id

Update patient demographics (optimistic lock).

Required body field: version (integer).

HTTPErrorCondition
200Updated PatientResponseDto
409OPTIMISTIC_LOCK_CONFLICTversion mismatch
409IDENTIFIER_ALREADY_ASSIGNEDsystem+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

FieldTypeNotes
versionnumberRequired
deceasedbooleanRequired
deceasedDateTimeISO-8601Optional; must be omitted when deceased=false
HTTPErrorCondition
200PatientResponseDto
400DECEASED_DATETIME_WHEN_ALIVEdeceasedDateTime sent with deceased=false
409OPTIMISTIC_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 }

HTTPErrorCondition
201Survivor PatientResponseDto
409MERGE_SELFSurvivor = source
404PATIENT_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" }

HTTPErrorCondition
200Survivor PatientResponseDto
403UNMERGE_DISABLED_BY_POLICYenv flag false
400UNMERGE_INVALID_STATESource not recorded as merged into this survivor

Portrait Endpoints

MethodPathDescriptionAuth
POST/api/v1/patients/:id/portraitUpload portrait; body: {contentType, dataBase64, consentGiven, clinicalDisplayAllowed, retentionDays?}REGISTRATION_PORTRAIT_WRITE
GET/api/v1/patients/:id/portraitDownload raw image bytesREGISTRATION_PATIENT_READ
GET/api/v1/patients/:id/portrait/historyPortrait version history metadataREGISTRATION_PATIENT_READ

Extension Schema Endpoints

MethodPathDescriptionAuth
GET/api/v1/extension-schemasList schema registry rowsREGISTRATION_EXTENSION_SCHEMA_ADMIN
GET/api/v1/extension-schemas/standard-bundlesCanonical schemas for employment, marital, appearanceREGISTRATION_PATIENT_READ
POST/api/v1/extension-schemasRegister new schema versionREGISTRATION_EXTENSION_SCHEMA_ADMIN
PUT/api/v1/patients/:id/extensions/:bundleKeySave validated extension payloadREGISTRATION_EXTENSION_INSTANCE_WRITE
GET/api/v1/patients/:id/extensionsList extension history (masked by role)REGISTRATION_EXTENSION_INSTANCE_READ

2. Encounters

MethodPathDescriptionAuth
POST/api/v1/encountersRegister encounterpatient_record:write
GET/api/v1/encounters?patientId=&status=List encountersauthenticated
GET/api/v1/encounters/:idGet by UUIDauthenticated
PATCH/api/v1/encounters/:id/statusTransition statuspatient_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.

HTTPErrorCondition
200Updated EncounterResponseDto
409INVALID_STATUS_TRANSITION
409OPTIMISTIC_LOCK_CONFLICT
404ENCOUNTER_NOT_FOUND

3. FHIR R4

Base path: /fhir/R4 (same auth + ehr.registration entitlement).

InteractionPathNotes
ReadGET /fhir/R4/Patient/:idIncludes photo only when portrait active + consent + retention not expired
SearchGET /fhir/R4/Patient?family=&identifier=&birthdate=&_count=Break-glass headers supported
CreatePOST /fhir/R4/PatientMaps to RegisterPatientUseCase
UpdatePUT /fhir/R4/Patient/:idRequires meta.versionId (optimistic lock)
ReadGET /fhir/R4/Encounter/:id
SearchGET /fhir/R4/Encounter?patient=Patient/:id&status=
CreatePOST /fhir/R4/Encounter
UpdatePUT /fhir/R4/Encounter/:id