Skip to main content

Terminology Service — API Contracts

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


1. Authentication

Route prefixAuth mechanism
GET /v1/terminology/*Keycloak JWT (Authorization: Bearer <token>) — no anonymous access (FR-TERM-010)
POST/PUT/DELETE /v1/terminology/concepts*JWT + role guard (platform:admin or tenant:admin)
GET /internal/terminology/*Optional X-Terminology-Internal-Token header or network mesh trust
GET /fhir/R4/*JWT (proxied via interop-service; same Keycloak realm)

2. Concept Query APIs

2.1 Full-Text Search — FR-TERM-001

GET /v1/terminology/search?system=&query=&limit=&offset=

Query params:

ParamRequiredDescription
systemYesURI of the coding system
queryYesFree-text search term (code prefix or display substring)
limitNoMax results (default 20, max 100)
offsetNoPagination offset (default 0)

Response 200:

{
"data": [
{
"id": "CON_01JXXX",
"system": "http://loinc.org",
"code": "2345-7",
"display": "Glucose [Mass/volume] in Serum or Plasma",
"active": true
}
],
"total": 42,
"limit": 20,
"offset": 0
}

Only active = true concepts returned. Tenant-scoped concepts included when caller's tenantId matches.


2.2 Exact Code Lookup — FR-TERM-002

GET /v1/terminology/lookup?system=&code=

Response 200:

{
"id": "CON_01JXXX",
"system": "http://loinc.org",
"code": "2345-7",
"display": "Glucose [Mass/volume] in Serum or Plasma",
"definition": "...",
"active": true
}

Response 404:

{ "found": false, "system": "http://loinc.org", "code": "INVALID-9" }

2.3 Code Validation — FR-TERM-003

GET /v1/terminology/validate?system=&code=

Response 200:

{ "valid": true, "active": true, "system": "http://loinc.org", "code": "2345-7" }

Returns valid: true and active: false if the code exists but is retired. Returns valid: false if the code does not exist.


2.4 ValueSet Expansion — FR-TERM-004

GET /v1/terminology/expand?valueSetUrl=&count=&offset=

Response 200:

{
"valueSetUrl": "http://ghasi.health/fhir/vs/lab-observation-categories",
"total": 8,
"timestamp": "2026-04-18T12:00:00Z",
"expansion": [
{ "system": "http://loinc.org", "code": "2345-7", "display": "Glucose [Mass/vol] Serum" }
]
}

3. Clinical Decision Support APIs

3.1 Drug Class Lookup — FR-TERM-005

GET /v1/terminology/drug-class?rxnormCode=

Response 200:

{
"rxnormCode": "83367",
"drugClasses": [
{ "className": "beta-blocker", "classSystem": "NDF-RT" },
{ "className": "antihypertensive", "classSystem": "NDF-RT" }
]
}

3.2 Drug-Drug Interaction Check — FR-TERM-006

GET /v1/terminology/interactions?rxnormCodes=A,B,C

Query params:

ParamDescription
rxnormCodesComma-separated list of RxNorm codes (2–20 codes)

Response 200:

{
"checkedCodes": ["83367", "29046", "18631"],
"interactions": [
{
"drug1Code": "83367",
"drug2Code": "29046",
"severity": "HIGH",
"description": "Atenolol + Verapamil: additive negative chronotropic effect; monitor heart rate.",
"source": "multum"
}
]
}

Empty interactions array means no known interactions.


3.3 Duplicate Therapy Detection — FR-TERM-011

GET /v1/terminology/duplicate-therapy?rxnormCodes=A,B,C

Response 200:

{
"checkedCodes": ["83367", "68180", "29046"],
"duplicates": [
{
"drug1Code": "83367",
"drug2Code": "68180",
"sharedClasses": ["beta-blocker"]
}
]
}

3.4 Drug-Condition Contraindications — FR-TERM-012

GET /v1/terminology/contraindications?rxnormCode=A&icd10Codes=B,C,D

Response 200:

{
"rxnormCode": "29046",
"checkedConditions": ["I10", "J45"],
"contraindications": [
{
"icd10Code": "J45",
"severity": "RELATIVE",
"description": "ACE inhibitors: use with caution in patients with severe asthma — may exacerbate bronchoconstriction."
}
]
}

4. FHIR Terminology Operations

All FHIR operations accept and return application/fhir+json.

4.1 CodeSystem/$lookup

GET /fhir/R4/CodeSystem/$lookup?system=http://loinc.org&code=2345-7

Returns FHIR Parameters resource with name, display, active.

4.2 CodeSystem/$validate-code

POST /fhir/R4/CodeSystem/$validate-code
Body: FHIR Parameters { system, code }

Returns FHIR Parameters with result (boolean), message.

4.3 ValueSet/$expand

GET /fhir/R4/ValueSet/{id}/$expand?count=50&offset=0

Returns FHIR ValueSet with populated expansion.contains[].

4.4 ConceptMap/$translate

POST /fhir/R4/ConceptMap/$translate
Body: FHIR Parameters { system, code, targetSystem }

Returns FHIR Parameters with result, match[] (equivalence + concept).


5. Admin APIs

5.1 Create Concept — FR-TERM-008

POST /v1/terminology/concepts
Authorization: platform:admin or tenant:admin

Request body:

{
"system": "urn:ghasi:tenant:TENANT_01J",
"code": "CUSTOM-001",
"display": "Custom Lab Panel",
"definition": "Site-specific lab panel definition",
"tenantId": "TENANT_01J"
}

Response 201:

{ "id": "CON_01JXXX", "system": "...", "code": "CUSTOM-001", "display": "..." }

Response 409: Code already exists for the system/tenant combination.


5.2 Update Concept — FR-TERM-008

PUT /v1/terminology/concepts/:id

Updatable fields only: display, definition. system, code, tenantId are immutable.


5.3 Deactivate Concept — FR-TERM-008

DELETE /v1/terminology/concepts/:id

Sets active = false. Does not physically delete. Returns 200 with updated concept.


5.4 Bulk CSV Import — FR-TERM-009

POST /v1/terminology/concepts/import
Content-Type: multipart/form-data

Disabled when TERMINOLOGY_CONCEPT_IMPORT_ENABLED=false (returns 403).

Request: multipart file upload of CSV with columns: system,code,display,definition

Response 202:

{ "importJobId": "IMP_01JXXX", "statusUrl": "/v1/terminology/import/IMP_01JXXX" }

Response 200 (poll):

{
"importJobId": "IMP_01JXXX",
"status": "completed",
"conceptsAdded": 142,
"conceptsUpdated": 8,
"conceptsSkipped": 3,
"errors": []
}

6. Internal API

GET /internal/terminology/lookup?system=&code=
GET /internal/terminology/interactions?rxnormCodes=
GET /internal/terminology/drug-class?rxnormCode=

Same response shape as public APIs but skips JWT validation. Protected by network policy or X-Terminology-Internal-Token header. Used by @ghasi/terminology-client for service-to-service calls.


7. Error Codes

HTTPCodeMeaning
400VALIDATION_ERRORMissing or invalid query parameters
401UNAUTHORIZEDJWT missing or invalid
403FORBIDDENRole insufficient or tenant scope violation
404NOT_FOUNDConcept or ValueSet not found
409DUPLICATE_CONCEPTCode already exists for system/tenant
422IMPORT_VALIDATION_ERRORCSV row failed validation; import aborted or partial
503SERVICE_UNAVAILABLEDatabase unavailable

8. Pagination Envelope

{
"data": [...],
"total": 300,
"limit": 20,
"offset": 40
}

Used by all list endpoints (/search, /import/{id} error list).