# Translating between vocabularies

  Your clinicians code a diagnosis once. Some payers want ICD-11, others are still on ICD-10, and
  what bridges those two facts is a concept map, not a column in your database.

## The situation this solves

A clinician records cholera. Your system stores `1A00`, the ICD-11 code, because that is the
vocabulary the rail codes diagnoses in. Then a claim goes to a payer whose adjudication engine only
understands ICD-10, where the same condition is `A00.9`.

There are three ways to handle that. Two of them go wrong later.

<Wire
  rows={[
    { call: "Store both codes", does: "Two columns, filled at capture time. It works until either vocabulary is revised, at which point your stored pairs disagree with the published crosswalk and nobody can say which is right.", keep: "don't" },
    { call: "Maintain your own mapping table", does: "A spreadsheet somebody owns. It is correct on the day it is written and quietly wrong after that. It is also the same work the terminology owner is already doing, done less well.", keep: "don't" },
    { call: "Store the primary code, translate on demand", does: "One code in your record. When a payer needs the other vocabulary, ask the service. The mapping stays owned by the people who curate it.", keep: "the ICD-11 code" },
  ]}
/>

  Store the vocabulary you captured in, and treat every translation as derived. A system that keeps
  the ICD-10 code as its primary representation, because that is what one payer wanted, has signed up
  for a migration nobody has scheduled, and it can no longer say what the clinician actually chose.

## What a concept map is

A concept map is a named, versioned set of rows with an owner. Each row says that this source code
corresponds to that target code, and how strong the correspondence is.

The strength is the part people skip, and it is what decides whether a translation is safe to apply
unattended.

<Wire
  rows={[
    { call: "equivalent", does: "The same thing. Safe to substitute without anyone looking.", keep: "use it" },
    { call: "narrower", does: "The source is more specific than the target. Translating loses detail, which is usually acceptable because the target still covers the case.", keep: "use it, know what was lost" },
    { call: "broader", does: "The source is more general than the target. Translating adds specificity nobody recorded, so surface this one rather than applying it silently.", keep: "flag it" },
    { call: "related", does: "Connected, but not a substitution. Not a translation you can bill on.", keep: "a human decides" },
    { call: "notrelated", does: "Explicitly not a match. Recorded so nobody proposes it again.", keep: "do not use" },
    { call: "nomap", does: "A deliberate statement that this code has no target in this map, with a reason. Somebody looked and decided. See the warning below.", keep: "the reason" },
    { call: "unmapped", does: "Nobody has decided yet. The work has not been done, which is not the same as a conclusion.", keep: "chase it" },
  ]}
/>

`wider` and `unmatched` are accepted spellings of `broader` and `notrelated`, kept for callers written
against the older vocabulary. Send the current names.

  Translating on a `broader` row means the code you send is more specific than the one your clinician
  chose. That can change what a payer thinks was treated. Where a map row is `broader`, put the
  translation in front of a person before it goes on a claim. Where volume makes that impossible, at
  least log that it happened, so a query about the claim can be answered.

## Doing it

<Wireframe
  title="Diagnosis"
  actor="Clinician"
  rows={[
    [{ label: "Primary diagnosis", from: "1A00 · Cholera", kind: "field", tone: "positive", span: 4, note: "ICD-11. The only code stored on the encounter." }],
    [{ label: "Payer requires ICD-10", from: "A00.9 · Cholera, unspecified", kind: "text", span: 3, note: "Resolved by the crosswalk at submission. Equivalence: broader" },
     { label: "equivalent?", from: "broader", tone: "attention", span: 1, note: "Less specific than what was recorded, so the screen shows it" }],
    [{ label: "Submit claim", kind: "action", span: 4 }],
  ]}
  caption="The clinician never picks an ICD-10 code and never sees a second dropdown. The screen surfaces only what a person should judge, which is that this particular translation is not an exact correspondence."
/>

<Mermaid chart={`sequenceDiagram
    autonumber
    participant H as Your HMIS
    participant T as Terminology service
    participant P as Payer

    Note over H,T: Once, at configuration time
    H->>T: GET /concept-maps/?source_code_system=..&target_code_system=..
    T-->>H: The maps that exist, with their short codes
    Note over H: Put the short code in configuration.<br/>Do not resolve it per claim.

    Note over H,T: Per code, at submission
    H->>T: GET /concept-maps/{short code}/$translate/?code=1A00&version=serving
    T-->>H: Parameters { result: true, match: [{ equivalence, concept }] }
    Note over H: Read the equivalence, not only the code.
    H->>P: Claim, carrying the target code
`} />

<Mermaid chart={`flowchart TD
    A["A payer needs another vocabulary"] --> B["GET /$translate/?code=..."]
    B --> C{"result true?"}
    C -- Yes --> D{"equivalence?"}
    D -- "equivalent / narrower" --> E["Use the target code"]
    D -- broader --> F["Use it, but log it,<br/>or show a person"]
    D -- "related / notrelated" --> G["Not a billable translation.<br/>A human decides."]
    C -- No --> H["GET /entries/?search=code<br/>why is there no match?"]
    H --> I{"A row with no_map true?"}
    I -- Yes --> J["Deliberate: this code has<br/>no target, and there is a reason.<br/>Do not chase it."]
    I -- No --> K["Nobody has mapped it yet.<br/>Raise it with the terminology owner."]
`} />

## Find the map once

Maps are configuration rather than runtime state. Resolve the map when you set the integration up
and put its short code in your configuration. Resolving it per claim adds a round trip to every
submission for an answer that changes about once a year.

Use the short code rather than the numeric id. `ICD11-TO-ICD10` says what it is in a code review and
means the same thing in every environment. `7` says nothing and is deployment specific.

<TryIt
  title="Which concept maps exist?"
  path="/concept-maps/?page_size=20"
  anonymous
  keep="the map id, and its canonical_url"
>
  Published maps are listed without a credential. Filter with `source_code_system=` and
  `target_code_system=`, which take numeric ids from the catalogue because they are filters rather
  than path lookups, or resolve one straight from the `canonical_url` a FHIR payload carries.
  More than one map can share a canonical url, so this can legitimately return several rows and the
  caller picks. Keep the `short_code` from the row you pick, not the id.
</TryIt>

## Translate a code

<TryIt
  title="Translate one code"
  method="GET"
  path="/concept-maps/ICD11-TO-ICD10/$translate/?code=1A00&version=serving"
  keep="the target code, and its equivalence"
>
  Edit the short code and the code. Every concept map route takes the map's `short_code` in place of
  its numeric id, so a request stays readable. The answer is a FHIR `Parameters` resource: `result`, then one
  `match` per target, each carrying an `equivalence` and the target `Coding`. Read both, because a
  target code without its equivalence is half the answer.
</TryIt>

<ApiRef method="GET" path="/concept-maps/{short code}/$translate/?code={code}" to="/terminology-api/concept-maps" keep="the target code">
  Full parameters, response shape and the failure cases in the reference.
</ApiRef>

Omitting `code` returns a `400` carrying a FHIR `OperationOutcome` rather than a plain error body,
which is worth knowing if your client only parses one error shape.

## When `result: false` means two things

`$translate` deliberately excludes rows declared `no_map`, and rows whose target is blank. They are
not translations, so they cannot be returned as matches.

The consequence is that `result: false` covers two completely different situations, and this
operation cannot tell you which one you are in:

<Wire
  rows={[
    { call: "Nobody has mapped this code", does: "Missing work. Raise it, because a code your clinicians use that has no crosswalk row will fail at every payer on the other vocabulary.", keep: "escalate" },
    { call: "Somebody decided it maps to nothing", does: "Completed work, with a recorded reason. There is no target and there is not going to be one, so chasing it only wastes an afternoon.", keep: "the reason" },
  ]}
/>

To tell them apart, read the rows:

<TryIt
  title="Why is there no translation?"
  path="/concept-maps/ICD11-TO-ICD10/entries/?search=1A00"
  keep="no_map, no_map_reason, equivalence"
>
  The entry listing shows what `$translate` hides. A row with `no_map: true` carries a
  `no_map_reason`, which is a decision rather than a gap. No row at all means nobody has looked yet.
  You can also filter the whole map by `equivalence=unmapped` to see how much work is outstanding
  before you depend on it.
</TryIt>

## Loading a whole crosswalk instead

Per-code translation is right for a claim. It is wrong for a bulk reconciliation of forty thousand
historical encounters, because that is forty thousand round trips.

<ApiRef method="GET" path="/concept-maps/{short code}/concept-map.json" to="/terminology-api/concept-maps" keep="the bundle">
  The whole map as a FHIR R4 ConceptMap resource, for loading into your own terminology client.
</ApiRef>

If you take this route, cache the map against the version you fetched and re-check it on a schedule.
A crosswalk copied into your database drifts in exactly the way this page opened by warning about.
The difference is that you now own the drift deliberately, with a version recorded, rather than by
accident.

  A concept map's version ordering is inferred from row creation time, because `ConceptMapVersion`
  lacks the `sequence` ordinal its code system counterpart has. Without that ordinal there is nothing
  to reconstruct past membership from, so a `?version=` read of `$translate` degrades to translating
  against the current rows rather than the rows as they stood at that release.

  It still answers, but the answer is today's rather than a frozen historical translation. If you may
  ever have to defend what a code translated to, record the target and the equivalence at the time you
  used them. This gap is specific to concept maps. Code systems and value sets do freeze, so the same
  caveat does not apply to `/concepts/` or an expansion.

## What breaks here

<CostTable
  rows={[
    {
      where: "Submission to one payer only",
      symptom: "\"Claims to this payer reject on the diagnosis, everyone else is fine\"",
      cause:
        "Either no translation is happening for the payer on the other vocabulary, or the map id in configuration points at the wrong pair of systems.",
    },
    {
      where: "A handful of codes, repeatedly",
      symptom: "\"These five diagnoses always fail and we keep re-raising them\"",
      cause:
        "They are almost certainly `no_map` rows with a recorded reason. Read /entries/ once and stop chasing them.",
    },
    {
      where: "An audit or a payer query",
      symptom: "\"The claim says one thing and the clinical record says another\"",
      cause:
        "A broader translation applied silently. Log the equivalence alongside the translated code so you can explain the difference.",
    },
    {
      where: "A bulk reconciliation",
      symptom: "\"The job takes nine hours and gets throttled\"",
      cause:
        "One $translate per row. Fetch the map once as FHIR and translate locally.",
    },
    {
      where: "Months after a vocabulary revision",
      symptom: "\"Our stored ICD-10 codes no longer match the crosswalk\"",
      cause:
        "The second code was stored at capture time. Store the primary code and derive the other one when it is needed.",
    },
  ]}
/>

**Next:** [Getting the data you need](/rail/reference/Terminology-Getting-Data) ·
[Validating a code](/rail/reference/Terminology-Validating-Codes) ·
[Pinning to a release](/rail/reference/Terminology-Pinning-Versions)
