Terminology Service — API Contracts
Status: populated Owner: TBD Last updated: 2026-04-18 Companion: Service Template · 03 platform-services · 02 DDD
1. Authentication
| Route prefix | Auth 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:
| Param | Required | Description |
|---|---|---|
system | Yes | URI of the coding system |
query | Yes | Free-text search term (code prefix or display substring) |
limit | No | Max results (default 20, max 100) |
offset | No | Pagination 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:
| Param | Description |
|---|---|
rxnormCodes | Comma-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
| HTTP | Code | Meaning |
|---|---|---|
| 400 | VALIDATION_ERROR | Missing or invalid query parameters |
| 401 | UNAUTHORIZED | JWT missing or invalid |
| 403 | FORBIDDEN | Role insufficient or tenant scope violation |
| 404 | NOT_FOUND | Concept or ValueSet not found |
| 409 | DUPLICATE_CONCEPT | Code already exists for system/tenant |
| 422 | IMPORT_VALIDATION_ERROR | CSV row failed validation; import aborted or partial |
| 503 | SERVICE_UNAVAILABLE | Database unavailable |
8. Pagination Envelope
{
"data": [...],
"total": 300,
"limit": 20,
"offset": 40
}
Used by all list endpoints (/search, /import/{id} error list).