Value sets and code systems
Every coded list in this documentation can be fetched from the terminology service. Copy one into a constant and it will be wrong the next time the list changes.
Everything below elaborates this sentence.
Why you fetch rather than embed
Coded lists move. Identifier types get added and withdrawn, document types grow as new interventions are onboarded, and diagnosis vocabularies are reversioned. A list you copy today will be quietly out of date in a few months, and when that happens the failure tends to look like something wrong with the patient's record rather than something wrong with your data.
Every list below is served by the terminology service, versioned, and safe to cache. Fetch on a
schedule, keep a local copy, and key that copy on the version you were handed.
This page covers which coded lists the rail uses and how to fetch one. The same list also comes in other shapes (paged JSON, FHIR, CSV, Excel, a bare code list, and the import path back in), and those are in getting the data you need. The rest of the service, meaning validating a code, walking a hierarchy, pinning to a released version, translating between vocabularies, or resolving a rate, is in the terminology service.
Which list do you need?
Every row below is a code system on the terminology service, and all of them are read with the
single call in Retrieving a list. Swap the
system parameter and nothing else changes. Each row links to the page where the list is
explained in context.
| Code system | What it is for | Taught at |
|---|---|---|
IDENTIFIER-TYPES | The document types a front desk may choose from when resolving a patient | 01 · Identify |
HEALTHCARE-WORKER-REGULATORS † | The bodies that register practitioners, which is half of a clinician's identifier | 01 · Identify |
PRACTITIONER-CADRES † | Discipline and speciality, where a benefit requires a named one | 01 · Identify |
ICD-11 | Diagnosis coding at preauthorisation, with an ICD-10 crosswalk for payers still on it | 5 · Preauth |
| Error codes | Every error the rail can return, published as terminology so you can generate an enum | Error dictionary |
† These two short codes are unconfirmed. They are the names we expect rather than names checked
against the live catalogue. IDENTIFIER-TYPES is confirmed and these two are not, so resolve them
before you wire them in, with GET /code-systems/?search=regulator and ?search=cadre. A 404 on
the detail route is a real answer, because the route accepts either a short code or an id.
Two habits apply whichever list you are using.
Pre-load rather than fetching during a visit. A pick-list fetched while a patient waits can also fail while that patient waits, so sync on a schedule instead.
Filter when you are building a picker, but not when you are reading history. Only
is_active: true belongs in a dropdown, and withdrawn codes still have to render when you are
interpreting a record from last year.
One call, whichever list you need
There is no per-vocabulary endpoint. Identifier types have no /identifier-type route of their
own. They are one code system among many on the terminology service, read through the same call as
every other list on this page. The only thing that changes between them is the system you pass.
Swap IDENTIFIER-TYPES for any short code in the table above and the rest of the call is
identical: same envelope, same paging, same version=serving. That is why a terminology layer in
your own code should take the system as an argument instead of carrying one function per list.
code, display, versioncurl -sS "https://ilm-dev.dha.go.ke/dev/ts/api/v2/concepts/?system=IDENTIFIER-TYPES&version=serving&page_size=50" \
-H "authorization: Bearer $TS_TOKEN"Every parameter this call accepts, in the terminology reference: filters for hierarchy, status
and concept class, and the full ?version= contract.
code, displayTwo things to notice in the envelope it returns.
status matters. A withdrawn code may still appear, marked inactive, so that historical records
referencing it stay readable. Filter on status: "active" when you populate a picker, and do not
filter when you are interpreting stored data.
version is your cache key. Compare it instead of re-fetching blindly, and log it. When a coded
value stops being accepted, the first question anyone asks is which version you were on.
Retrieving a list from the terminology service
Every coded list the rail uses is held in the terminology service as a code system, and every one
of them is fetched the same way: by API for a running integration, or as a spreadsheet for someone
who just needs to read it. None of it is specific to one vocabulary. Swap the system and the call
is identical.
Code
What needs a token, and what does not
The service splits in two, and knowing which half you are working in saves you an authentication problem that did not need to happen.
| Call | What it does | What you keep |
|---|---|---|
GET /code-systems/ | Which code systems exist, with their short codes and ids. Public, so it answers with no credential at all. | short_code, id |
GET /code-systems/{id}/ | One code system's detail. Also public. | canonical url |
GET /value-sets/ | Which value sets exist. Also public. | id |
GET /concepts/… | The codes themselves. Requires a token issued by the terminology service's own realm. | code, display |
GET /code-systems/{id}/versions/, /summary/, /changed/, $lookup, expansions | Everything else: versioned reads, FHIR operations, expansions. All of it needs that token. | — |
Two consequences follow. You can discover the catalogue before you hold any credentials, which is the quickest way to confirm a short code instead of guessing at it. And the public catalogue is filtered: without a token you see only what is published openly, so a system you were expecting may be invisible rather than missing.
By API: the call to build against
Code
version=serving is the part that matters. It resolves to the latest approved release of that
code system. Omit it and you get whatever is in the source, drafts included, which is what you want
while you are authoring terminology and wrong in every integration.
Code
What to read from each concept, and what to ignore:
| Call | What it does | What you keep |
|---|---|---|
code | The value you send on the wire, and the only thing you store | code |
display | The label you render, never stored and never sent | — |
is_active + status | Only a PUBLISHED and active concept belongs in a picker; a DRAFT never does | is_active |
parent_code, depth, child_count | Hierarchy, where the vocabulary has one. Render it as a tree rather than a flat list | parent_code |
next, total_pages | Paging. A sync that reads only the first page ships part of the vocabulary and still looks like it worked | next |
Some systems have a hierarchy and some do not. Identifier types are flat: every concept is
depth: 0. A diagnosis system is nested, so SYS-CARD (a Category) parents DIAG-CHF, which in
turn parents left- and right-sided failure. Use concept_class to tell a grouping node from a
selectable one, and render a category as a heading in your dropdown rather than as an option.
As a spreadsheet: the three-call release job
Excel and CSV do not come from /concepts/. They come from an asynchronous release job. You ask
for an export, poll until it finishes, then download it.
You need two ids: the code system's artifact_id, and a version_id.
Code
Code
Code
The export carries code | display | definition | concept_class | parent | status | effectiveTime, one row per concept.
Four things are worth knowing before you build this.
version_id is required. Omit it and the create returns
400 {"version_id":["This field is required."]}. Take it from step 1: draft_version while a
system is still being authored, or a released version id once one exists.
Do not cache output_file_url. It is a shortened presigned link that is regenerated on every
poll, so each poll hands you a different URL. Hit /release-jobs/{id}/download/ instead, which
redirects to the current one.
Polling needs no auth, but creating and downloading do. Job create needs
terminology-service.releasejobviewset.create and download needs .read. Reading job status is
open.
This path is for people, not for runtime. Use it to hand someone a list to review, or to seed a
mapping exercise. An integration reads /concepts/?version=serving rather than downloading
spreadsheets.
Diagnosis coding, and the crosswalk
Diagnoses are coded in ICD-11. Exactly one primary diagnosis is required on a preauthorisation, and duplicates are rejected.
Some payers are still on ICD-10. You do not code twice. A concept map on the terminology service
resolves it, through the same $translate operation any crosswalk uses:
The ICD-10 equivalent of an ICD-11 code, for payers that have not migrated.
the target code, and its equivalenceCode once in ICD-11 and let the map resolve the rest. A system that stores ICD-10 as its primary representation will have to migrate later.
Reading the answer takes a little more than lifting the target code out of it. The equivalence matters, and a negative result can mean two different things, which is covered in translating between vocabularies.
Standard codes and mapping
Before an item may be billed, it must be declared: you say what one of your price-list items maps to, and the rail validates that the standard code is real and recognised, then computes the paths onward to each payer's revenue codes.
This runs in one direction only. The rail does not infer what your item is; you declare it.
Anything never declared, or declared and rejected, stays off the claim entirely as
UNMAPPED_CODE.
Error codes as a value set
The error dictionary is itself published as terminology, so you can generate your error enum rather than typing it:
See the error dictionary for the readable version.
How to keep a local copy honest
- 01Sync on a schedule rather than on demand. A pick-list fetched during a patient interaction can also fail during that interaction.
- 02Key your cache on the returned version string, and log it with every coded value you store.
- 03Page properly. A sync written against the default first page ships part of the vocabulary and still looks like it worked.
- 04Filter on status for pickers, and do not filter when you are interpreting stored history. Withdrawn codes still need to render.
- 05Store the code and never the display text. A display string you stored is impossible to defend once the wording changes.
- 06Generate enums at build time from the published value sets, so a new code turns up when you compile instead of in production.
Next: Getting the data you need · Error dictionary · or see coded lists in use at 02 · Verify cover

