# Getting the data you need

  The same terminology comes out in four shapes. One question picks between them: is a program
  reading this, a FHIR client, or a person?

Wherever this documentation says "from a list you fetched", this is the page it means.

## Address artefacts by their code, not their id

Before any of the calls below, note that every code system, value set and concept map is addressable
by its short code as well as by its numeric id. `/code-systems/IDENTIFIER-TYPES/` and
`/code-systems/1251/` resolve to the same row, and so does every action hanging off it: expansions,
FHIR exports, `$validate-code`, `$translate`.

Use the code. An id is deployment specific, so the number that identifies a vocabulary in the
development environment is not the number in production. A published short code is the same
everywhere and readable in a diff.

<Wire
  rows={[
    { call: "Resolution is pk-first", does: "A value that looks numeric is tried as an id first and then falls through to the code column, so an all-digits short code is still reachable. After an exact miss the lookup ignores case, because a code derived from a canonical url takes that url's casing and nobody types it back the same way.", keep: "the short code" },
    { call: "One ambiguity, resolved predictably", does: "A short code identical to some other row's numeric id resolves to the id. That is the documented rule, but it is also a reason not to mint numeric-looking short codes.", keep: "avoid numeric codes" },
    { call: "Concepts are the exception", does: "A concept code is unique only within its system, so concept routes take a numeric id. Resolve it from GET /concepts/?system=<short code> and store it beside the code.", keep: "the concept id" },
    { call: "Filters still take ids", does: "?code_system=, ?source_code_system= and ?target_code_system= are filters rather than path lookups, so they take numeric ids. ?system= is the code-based alternative and accepts a short code or a canonical url.", keep: "?system=" },
  ]}
/>

## Pick the shape first

<Wire
  rows={[
    { call: "Paged JSON", does: "A running integration. Cheap, cacheable, pinned to a version, and the only one of the four you should call while a patient is waiting.", keep: "GET /concepts/" },
    { call: "FHIR R4", does: "Loading a vocabulary into a terminology client that speaks FHIR, or handing a payer something in a standard shape. One request, one whole resource, with a size bound on it.", keep: "GET /code-systems/{short code}/export/" },
    { call: "CSV or Excel", does: "A person. Reviewing a vocabulary, seeding a mapping exercise, sending a list to somebody who will open it in a spreadsheet. It is asynchronous: you ask, then poll, then download.", keep: "POST /release-jobs/" },
    { call: "Codes only", does: "A bare list, when all you need is the enumeration and nothing about it. Generating a constant, checking membership offline.", keep: "format: CODES" },
  ]}
/>

One mistake is worth naming up front. A spreadsheet is not an integration. Downloading an Excel file
on a schedule and importing it into your own tables reproduces, by hand, what the paged JSON call
already does correctly, and it loses the version pinning that makes the JSON safe.

## For a program: paged JSON

It is one call, and one parameter on it does the real work.

<TryIt
  title="Fetch a coded list"
  path="/concepts/?system=IDENTIFIER-TYPES&version=serving&page_size=50"
  keep="code, display, next"
>
  Swap `system` for any short code from the catalogue. It accepts the short code or the canonical
  url, and `code_system=` takes the numeric id instead. `version=serving` is the parameter that
  matters, because it pins the read to the release the owner is serving. Look at `count` and `next`
  in the answer before you decide your sync is finished.
</TryIt>

<Wire
  rows={[
    { call: "?system= or ?code_system=", does: "Short code or canonical url in the first; numeric id in the second. Use whichever identity you actually hold rather than translating between them.", keep: "the short code" },
    { call: "?version=serving", does: "The release the author or regulator is serving. Without it you get whatever that artefact defaults to: the working copy including drafts if it is unpinned, the pinned version if it is not. Send it and the answer stops depending on somebody else's setting.", keep: "serving" },
    { call: "?page_size= and next", does: "Paging. Follow next rather than computing page numbers; a sync that reads page one ships a fraction of a vocabulary and looks like it worked.", keep: "next" },
    { call: "?parent_code= and ?root_only=", does: "Walking a hierarchy one level at a time, rather than pulling forty thousand rows to render a dropdown.", keep: "parent_code" },
    { call: "?concept_class= and ?status=", does: "Separating selectable concepts from grouping nodes, and active codes from withdrawn ones.", keep: "concept_class" },
  ]}
/>

Where the vocabulary is a tree rather than a list, [walking it](/rail/reference/Terminology-Walking-The-Tree)
is its own technique and worth reading before you fetch anything large.

## For a FHIR client: the resource itself

<TryIt
  title="One code system as FHIR"
  path="/code-systems/IDENTIFIER-TYPES/export/?format=fhir&version=serving"
  keep="the CodeSystem resource"
>
  The FHIR R4 CodeSystem, built and returned in the response. `?format=fhir` selects the
  `application/fhir+json` media type; without it you get ordinary JSON with the same body.
</TryIt>

Three things this endpoint does that are worth knowing before you point a client at it.

<Wire
  rows={[
    { call: "It is bounded", does: "The full payload holds every concept in memory before responding, so a system above the inline limit answers 413, and the message names the release job to use instead. The 413 is a real answer rather than a sign that the service is down.", keep: "413 means go async" },
    { call: "?_summary=true", does: "The header without the codes, at the same cost whether the system holds ten concepts or forty thousand. The call to use when you only need identity and metadata.", keep: "content: not-present" },
    { call: "A closed system has no FHIR form", does: "A provider rate agreement answers 406. It is not a public vocabulary and there is no representation to serve.", keep: "406 is deliberate" },
  ]}
/>

The same representation is available through the ordinary detail read as `?view=fhir`. Both routes
call one method and share the same refusals, so they cannot drift apart.

Value sets have the same pair: `/expansion/` for the paged member list, and the FHIR form for a
client that wants the resource.

<ApiRef method="GET" path="/value-sets/{short code}/expansion/?version=serving" to="/terminology-api/value-sets" keep="codes">
  The member list of a value set. Identical response shape whether the expansion was precomputed or
  resolved on the spot, so you never branch on which happened.
</ApiRef>

## For a person: CSV, Excel, or a bare code list

This is a release job: you ask, you poll, you download. Three calls. It is asynchronous because a
full export of a large vocabulary takes too long to hold a request open for.

<Wireframe
  title="Export a code system"
  actor="Terminology reviewer"
  rows={[
    [{ label: "Artefact", from: "codesystem · SHA-INTERVENTIONS", kind: "field", span: 2 },
     { label: "Version", from: "v1.4.0 (serving)", kind: "field", span: 2, note: "A released version, not the working copy" }],
    [{ label: "Format", from: "XLSX", kind: "field", swatch: 1, span: 2, note: "FHIR · CSV · XLSX · CODES" },
     { label: "Mode", from: "SNAPSHOT", kind: "field", swatch: 2, span: 2, note: "FULL · SNAPSHOT · DELTA" }],
    [{ label: "Include withdrawn concepts", from: "include_inactive: false", kind: "text", span: 4, note: "Off for a review list; on when the file has to explain historical records" }],
    [{ label: "Preview first", kind: "action", span: 2, note: "Row counts and samples, nothing stored" },
     { label: "Generate", kind: "action", span: 2 }],
  ]}
  caption="Format decides who can open the file. Mode decides what is in it, and mode is the choice people get wrong, because FULL and SNAPSHOT differ exactly when a concept has been withdrawn since the release."
/>

<Mermaid chart={`sequenceDiagram
    autonumber
    participant Y as You
    participant T as Terminology service

    Y->>T: POST /release-jobs/preview/ (same body as create)
    T-->>Y: Row counts + sample rows, nothing stored
    Note over Y: Confirm this is the dataset you meant.

    Y->>T: POST /release-jobs/ { artifact, version_id, mode, format }
    T-->>Y: 201 { id, status: "queued" }

    loop until COMPLETED
      Y->>T: GET /release-jobs/{job id}/
      T-->>Y: { status, row_count }
      Note over Y,T: Polling needs no credential.
    end

    Y->>T: GET /release-jobs/{job id}/download/
    T-->>Y: 302 to the current presigned URL
    Note over Y: Follow the redirect.<br/>Never store the URL it points at.
`} />

<Mermaid chart={`flowchart TD
    A["Somebody needs a file"] --> B{"Who opens it?"}
    B -- "A person, in a spreadsheet" --> C["format: XLSX or CSV"]
    B -- "A FHIR client" --> D["format: FHIR"]
    B -- "Code generation" --> E["format: CODES"]
    C --> F{"First copy, or an update?"}
    D --> F
    E --> F
    F -- "First copy" --> G["mode: FULL or SNAPSHOT"]
    F -- "Update to an existing copy" --> H["mode: DELTA<br/>+ base_version_id"]
    G --> I["POST /release-jobs/preview/<br/>check before you queue"]
    H --> I
    I --> J["POST /release-jobs/"]
    J --> K["Poll GET /release-jobs/{job id}/"]
    K --> L{"status COMPLETED?"}
    L -- No --> K
    L -- Yes --> M["GET /release-jobs/{job id}/download/"]
`} />

### The two choices that shape the file

<Wire
  rows={[
    { call: "format: FHIR", does: "FHIR R4 JSON. For a machine that speaks it.", keep: "-" },
    { call: "format: CSV", does: "One row per concept, openable anywhere, diffable in git.", keep: "-" },
    { call: "format: XLSX", does: "Excel. What to send someone who will annotate it and send it back.", keep: "-" },
    { call: "format: CODES", does: "The bare enumeration, nothing else. For generating a constant.", keep: "-" },
  ]}
/>

<Wire
  rows={[
    { call: "mode: FULL", does: "Everything belonging to that version, exactly as it was frozen. This is the defensible choice, because a released version never changes and the export can be reproduced.", keep: "version_id" },
    { call: "mode: SNAPSHOT", does: "Membership as it stands now, with include_inactive deciding whether withdrawn concepts come too. Right for a picker list and wrong for an audit, because what it returns depends on when you asked.", keep: "include_inactive" },
    { call: "mode: DELTA", does: "Only what changed against base_version_id. Use it to update a local copy incrementally instead of re-downloading the whole thing. On a large vocabulary that turns an hour into a minute.", keep: "base_version_id" },
  ]}
/>

<TryIt
  title="Poll a release job"
  path="/release-jobs/1/"
  anonymous
  keep="status, row_count"
>
  Polling is public, so this one runs with no credential at all. Edit the id. Creating a job and
  downloading the file both need a token; reading the job status does not.
</TryIt>

<ApiRef method="POST" path="/release-jobs/" to="/terminology-api/getting-the-data-out" keep="id">
  Create the job. The full body, both required-field failures, and the mode and format enumerations
  are in the reference.
</ApiRef>

### Five things that will otherwise cost you an afternoon

<Wire
  rows={[
    { call: "This is the one call that needs numeric ids", does: "artifact_id and version_id are both integers here, so resolve them first: GET /code-systems/<short code>/ gives you the artefact id, and /versions/ gives you the version ids with is_serving marking the one serving resolves to. Everywhere else on this page takes a short code.", keep: "both ids" },
    { call: "version_id is required", does: "Omit it and the create answers 400 with {\"version_id\": [\"This field is required.\"]}. base_version_id is required the same way when mode is DELTA.", keep: "version_id" },
    { call: "Do not store output_file_url", does: "It is a presigned link regenerated on every poll, so the copy you saved is not the one that works. Hit /download/ instead, which always redirects to the live one.", keep: "the job id" },
    { call: "A 202 is not a file", does: "The older /bulk/export/ endpoints queue a CSV and answer 202 with no job id at all, so there is nothing to poll and no link to follow. Use a release job whenever you need the file back.", keep: "use release jobs" },
    { call: "Preview before you queue", does: "/release-jobs/preview/ takes the same body and answers with counts and sample rows, storing nothing. It is the cheapest way to find out that your DELTA base was the wrong version.", keep: "row_count" },
  ]}
/>

## Keeping a local copy honest

<BuildSequence
  steps={[
    { do: "Sync on a schedule, never during a visit", detail: "A pick-list fetched while a patient waits can also fail while a patient waits." },
    { do: "Read ?version=serving, and record what it resolved to", detail: "Log the version with every coded value you store. When a code stops being accepted, that log lets you answer why instead of guessing." },
    { do: "Update with DELTA where the vocabulary is large", detail: "Re-downloading forty thousand concepts to learn that three changed is a habit that eventually gets scheduled hourly by mistake." },
    { do: "Poll /staleness/ for value sets rather than re-expanding", detail: "stale_count: 0 means your cached expansion still reflects its sources. Re-expansion is an authoring action and it is expensive." },
    { do: "Put it behind one interface of your own", detail: "A single getValueSet(system) in your code means a list moving from a bespoke endpoint to the generic one is a one-file change rather than a migration." },
  ]}
/>

## What breaks here

<CostTable
  rows={[
    {
      where: "A sync that has always worked",
      symptom: "\"We are missing codes that are definitely published\"",
      cause:
        "Paging. The first page came back, looked right, and nobody followed next.",
    },
    {
      where: "A FHIR export of a large system",
      symptom: "\"It returns 413 and we assumed the service was down\"",
      cause:
        "It is the documented bound, and the message names the alternative. Use ?_summary=true for the header or a release job for the content.",
    },
    {
      where: "A download link shared with a colleague",
      symptom: "\"The link expired / the link 403s for them\"",
      cause:
        "A stored output_file_url. Share the job id and let them hit /download/.",
    },
    {
      where: "An export used as evidence",
      symptom: "\"Two people exported the same list and got different files\"",
      cause:
        "SNAPSHOT mode, taken at different times. FULL against a version_id is reproducible; SNAPSHOT is deliberately not.",
    },
    {
      where: "A scheduled spreadsheet import",
      symptom: "\"Our list drifts from the published one\"",
      cause:
        "A spreadsheet used as an integration. The paged JSON call exists precisely so this is not necessary.",
    },
  ]}
/>

**Next:** [Pinning to a release](/rail/reference/Terminology-Pinning-Versions) ·
[Translating between vocabularies](/rail/reference/Terminology-Concept-Maps) ·
[The whole surface](/rail/reference/Terminology-Service)
