Pinning to a release
Terminology changes upstream, on somebody else's schedule, without a deployment on your side. The version you read is the one control you have over that, and the default is the wrong one.
Everything below elaborates this sentence.
The default reads drafts
Omit the version and, in the usual case, you read the working copy. That is correct when you are authoring terminology, since an editor needs to see the change they just made. It is wrong in every integration, because a draft concept that nobody approved can reach a patient-facing screen, be picked, and be submitted on a claim.
"In the usual case" is doing real work in that sentence. An unversioned read serves the working copy
only while the artefact is unpinned; under a manual pin it already serves that frozen version without
being asked. So omitting the version leaves you at the mercy of somebody else's pin setting. The
same call returns live rows on one vocabulary and frozen content on another, and nothing in your
code says which. Send serving and the question stops mattering.
Three ways to read the same call, in the order you should reach for them:
| Call | What it does | What you keep |
|---|---|---|
?version=serving | The version the artefact currently resolves to: the owner's pin if there is one, otherwise the newest released version effective today. A frozen read without having to look up a number first. This is what an integration sends on every runtime call. | the version it resolved to |
?version=v1.2.0 | Exactly that release, by its version_id (or its numeric id). Only released versions are honoured. A pinned read like this is cacheable indefinitely. | the version_id |
?version=draft | The pending draft, for reviewers. Gated on the reviewer permission, and not something an integration sends. | — |
omitted | Depends on the artefact: the working copy including drafts while it is unpinned, or the pinned version if somebody has set one. Right when authoring terminology, wrong when integrating with it, not because it is always live but because you cannot tell. | — |
What serving actually resolves to
"Latest" is the wrong mental model here, so this part is worth being precise about. Every versioned
artefact carries a pin_mode, and that decides the answer:
| Call | What it does | What you keep |
|---|---|---|
pin_mode: MANUAL | The owner has pinned a specific release. serving resolves to exactly that version and stays there, because a newer release does not move it. This is how a regulator holds a vocabulary steady while a later version is prepared, or holds consumers on a known-good release after a problem. | pinned_version |
pin_mode: AUTO_LATEST | No pin. serving resolves to the newest released version whose effective date has arrived. This is the common case, and it is what lets a new approved code reach your pickers without a deployment on your side. | the newest release |
Two things follow from that, and both are intended behaviour rather than quirks.
A future-dated release is not served early. Effective dates are wall-clock dates set by an author,
so a version dated next Monday is invisible to serving until Monday, with no coordination needed
on your side. "Today" means today in the deployment's own timezone rather than UTC. That is on
purpose: comparing against UTC would leave a release dated today unservable until three in the
morning local time.
A pin that can no longer be served does not break the read. If a pinned version is retired, the
read falls through to the newest release and says so: the artefact carries a pin_warning, and the
version ladder shows the state. You get terminology back rather than an error, along with a signal
that somebody's pin needs attention.
Reading which version you are on
Start with serving at the top of the response. It is the version_id that ?version=serving
resolves to, or null when nothing is servable. Beside it, pin_mode, pinned_version and
pin_warning say whether that answer was somebody's deliberate choice or simply the newest release.
Each row then carries is_serving and is_head_draft, so you never need a second call to work out
which is which.
version_id, is_serving, pin_modecurl -sS "https://ilm-dev.dha.go.ke/dev/ts/api/v2/code-systems/IDENTIFIER-TYPES/versions/" \
-H "authorization: Bearer $TS_TOKEN"Which to send where
serving and a fixed version answer different questions, and most integrations need both, in
different places.
| Call | What it does | What you keep |
|---|---|---|
Send serving on every runtime read | Pickers, validation, expansions. You want the terminology the owner is standing behind, and you want a newly approved code to appear without a release on your side. | serving |
Store the resolved version with the code | When you store a coded value, store which version you read it at. Interpreting a two-year-old claim means reading the vocabulary as it stood then, and a fixed ?version= is how you do that. | version_id, beside the code |
Send a fixed version when a run must be reproducible | A migration, a reconciliation, a regulatory extract. Pinning explicitly means running it again next month gives the same answer. The read is also cacheable indefinitely, which a long job notices. | the version_id |
That second habit is what makes an old record defensible. Without it, a code that has since been withdrawn looks like a data error rather than a code that was correct on the day.
Seeing what changed
The working copy against the serving version. A modified row carries both the before and after objects, so you can show what actually changed rather than that something did.
What a terminology update should look like on your side, with somebody reading a diff instead of being surprised by one.
cached at version 73 concepts1 conceptWithdrawn is not deleted
A withdrawn code still comes back, marked inactive, and this is deliberate: a record from last year that references it still has to render. Two rules follow, and they pull in opposite directions:
| Call | What it does | What you keep |
|---|---|---|
Filter for pickers | is_active: true and status PUBLISHED is what belongs in a dropdown. Offering a withdrawn code produces a rejection nobody can explain to the operator who picked it. | is_active |
Do not filter for history | When interpreting stored data, read everything. A filter applied here makes old records render as blanks, which looks like data loss. | the full set |
One list, two reads. If your terminology layer exposes a single getValueSet(system) that always
filters, the history path has no way to ask for what it needs.
Building so a release is not an incident
- 01Send version=serving on every runtime readIn one place, in your HTTP client for the terminology service, so no call site can forget it. A missing version parameter should be impossible rather than caught in review.
- 02Store the version with every code you storeOne extra column. It is the difference between explaining an old claim and guessing about it.
- 03Diff before you switch the cache overRead /versions/, fetch the new release by its version_id, and compare. The withdrawn half of the diff is what needs a human.
- 04Never auto-rewrite a clinical codeA withdrawn diagnosis on an unsubmitted claim is a clinical decision to revisit, not a mapping to apply. Flag it and let somebody look.
- 05Log the version you switched to, and whenWhen a coded value stops being accepted, the first question is which version you were reading and since when. Make that answerable in one query.
What breaks here
| Where it surfaces | What the desk sees | What actually went wrong |
|---|---|---|
| A picker, intermittently | "A code appeared that nobody approved" | The version parameter was omitted, so the read included drafts. Send serving. |
| Submission, after a quiet upstream release | "Claims started rejecting on Tuesday and we changed nothing" | That reading is usually right. You changed nothing, and the vocabulary did. Read /versions/ for the release and its effective date, then diff it. |
| Reporting on old claims | "Historical records show blanks where a diagnosis used to be" | The history path is filtering on is_active, so withdrawn codes resolve to nothing. Filter for pickers only. |
| An audit | "We cannot say which version of the vocabulary this claim was coded against" | The version was never stored beside the code. It cannot be reconstructed afterwards. |
| A sync job | "The cache silently replaced itself and four claims broke" | A sync that fetches and overwrites, with no diff step. The withdrawn concepts are the ones that need to be seen before the switch. |
Next: Getting the data you need · Validating a code · or the whole surface at the terminology service

