FHIR $Lookup Example: A Practical How-To Guide for 2026

You're probably dealing with this right now. A source feed hands you a code like 44054006, a clinician asks what it means, and your pipeline can't move until you know whether that code is a diagnosis, a finding, a procedure, or something else entirely.
That's where a good FHIR $lookup example stops being tutorial material and starts being daily engineering practice. In real systems, $lookup is the fast path for understanding a known code. It isn't how you discover terms. It's how you ask a terminology service, “Tell me exactly what this code is, in this code system, with the metadata I need to trust it.”
Why FHIR $lookup Is Essential for Clinical Data

A feed lands in your pipeline with a SNOMED code, the source display looks questionable, and an OMOP mapping step is waiting on the answer. At that point, $lookup is not a nice extra. It is the fastest way to confirm what the code represents, which labels belong to it, and which properties you can trust downstream.
FHIR defines $lookup as a CodeSystem operation for retrieving details about one known concept. In practice, that means you send the code and system you already have, then get back the display, designations, and terminology-specific properties needed for validation and transformation. If you want a broader overview of how FHIR terminology APIs fit together, this guide to a FHIR terminology server API for OMOP workflows gives the larger picture.
When $lookup is the right tool
$lookup earns its place once the code is already in hand and the next question is operational, not exploratory. Common cases include:
- Display validation so source labels do not drift from the terminology service you rely on
- Property retrieval for attributes used in mapping, QA rules, and terminology-aware transforms
- Version-aware checks when reproducibility matters for audits or reruns
- OMOP vocabulary prep before you resolve standard concepts, inspect relationships, or feed a mapping workflow
Search is still useful, but it answers a different question. Search helps you find candidate concepts. $lookup helps you verify one exact concept.
Practical rule: If your payload already includes
systemandcode, call$lookupfirst.
Why engineers care in production
This matters most in the middle of a real integration. Terminology calls often sit inside ETL jobs, validation services, FHIR ingestion workers, and clinician-facing review tools. If the response is inconsistent, if version handling is vague, or if the endpoint is built for generic FHIR demos instead of vocabulary operations, the rest of the pipeline gets harder to reason about.
That is where OMOP-specific workflow design starts to matter. A generic FHIR terminology server can return concept details, but OMOP teams usually need more than a single lookup result. They need to move from code inspection into standardization work, concept mapping, and vocabulary-aware validation without stitching together multiple incompatible services. OMOPHub is useful here because the FHIR endpoint is paired with endpoints built for OMOP vocabulary operations, so $lookup can act as the first verification step instead of a dead end.
Self-hosting still makes sense in some environments. Air-gapped deployments, internal extensions, and strict network controls can justify it. For many delivery teams, though, the trade-off is straightforward. Running your own terminology stack gives you control, but it also gives you more maintenance, more upgrade work, and more chances for vocabulary drift between lookup behavior and the OMOP workflows that depend on it.
Making Your First FHIR $lookup Request

A developer gets a SNOMED code in an inbound payload, needs a display string for review, and has to confirm the terminology server resolves the code before it enters the OMOP mapping flow. That is the right moment to call $lookup.
Here is a simple request against OMOPHub's FHIR endpoint:
curl -G "https://fhir.omophub.com/fhir/r4/CodeSystem/\$lookup" \
-H "Authorization: Bearer oh_your_api_key" \
-H "Accept: application/fhir+json" \
--data-urlencode "system=http://snomed.info/sct" \
--data-urlencode "code=44054006"
This request is intentionally plain. In production, plain is good. You want something an ingestion worker, validation service, or analyst notebook can issue without extra ceremony.
What each part does
The shape of the request matters more than the tool you use.
| Part | Purpose |
|---|---|
GET via curl -G | Sends the parameters as a query string, which works well for a basic $lookup call |
/CodeSystem/$lookup | Invokes the FHIR terminology operation |
system | Tells the server which code system namespace to use |
code | Identifies the concept to resolve |
Accept: application/fhir+json | Requests a standard FHIR JSON response |
Authorization | Reuses the same Bearer token pattern you use across the service |
Generic FHIR tutorials usually stop here. For OMOP work, this request is more useful because it sits on the same service surface as the vocabulary operations you will need next. You verify the source code first, then move into mapping or standardization work without switching stacks.
What to expect back
The response is usually a FHIR Parameters resource, not a CodeSystem resource. That distinction matters when you wire this into code because your parser needs to read named parameters rather than resource fields.
A typical response shape looks like this:
{
"resourceType": "Parameters",
"parameter": [
{ "name": "name", "valueString": "..." },
{ "name": "display", "valueString": "..." },
{ "name": "version", "valueString": "..." }
]
}
Some servers also return repeated property entries. Those become more important once you need synonyms, crosswalk hints, or version-specific metadata, but the first check is simpler. Confirm that the code resolves, the display is what you expect, and the returned version is acceptable for your pipeline.
How to read the response
Start with three values:
displayis the label a reviewer or downstream app will see.namegives context about the code system or concept naming.versionshows which terminology release answered the request, when provided.
version is the field teams skip until something breaks. Then it becomes the first thing to inspect. If a code resolved one way last quarter and resolves differently today, version metadata usually tells you whether you have a terminology update issue or an application bug.
For a broader explanation of terminology endpoint patterns before you embed this in services or ETL jobs, see the guide to FHIR terminology server APIs.
Decoding $lookup Parameters and Properties
A $lookup call starts earning its keep once you stop treating it as a label fetch and start treating it as a controlled vocabulary query. In OMOP workflows, that usually means two things: ask for the right properties up front, and pin the lookup to a version or date context when the server supports it.
Key FHIR $lookup parameters
| Parameter | Type | Description | Example |
|---|---|---|---|
code | string | The concept code to inspect | 44054006 |
system | uri | The canonical code system URI | http://snomed.info/sct |
version | string | Requests a specific terminology version when supported | 20230901 |
property | string | Requests selected concept properties | synonyms |
date | dateTime | Requests evaluation in a time-aware context when supported | 2025-01-15T00:00:00Z |
code and system identify the concept. The other parameters determine whether the answer is useful in production.
Why property deserves more attention
The property parameter controls how much concept detail comes back in the Parameters response. That matters once your pipeline has to do more than render a display string. Synonyms help with search normalization. Mapping-related properties help when you reconcile source codes against standard OMOP concepts. Terminology-specific attributes often feed validation rules, cohort logic, or QA checks.
If you skip property, you often create extra work later.
A common failure pattern looks like this: a developer confirms that $lookup resolves the code, ships the integration, and only later realizes the ETL job also needs synonyms, a classification hint, or a source-specific mapping attribute. Now the team has to add another request path, update parsers, and re-test downstream assumptions. Asking for the needed properties on day one is usually cheaper.
There is a trade-off. Smaller responses are better for UI interactions and high-volume validation calls. Richer responses are better for ETL, terminology review, and audit support. Choose based on the caller, not on what a generic tutorial happens to show.
Version context is part of the contract
Version drift causes real problems in vocabulary pipelines. The code still resolves. The display may even look fine. Then a downstream OMOP mapping changes after a terminology refresh, and nobody can explain which release answered the original lookup.
That is why version and date matter. If the terminology service supports them, use them deliberately and store what came back. For clinical analytics, reproducibility is not a nice-to-have. It is how you explain a mapping decision months later.
Generic FHIR examples rarely focus on this OMOP-specific pain point. OMOPHub's vocabulary-oriented endpoints are more practical here because the lookup step is closer to the rest of the vocabulary workflow. You are not just checking whether a code exists. You are validating a concept against a vocabulary state that has to line up with mapping, standardization, and audit expectations. If you want a broader view of that SDK and vocabulary workflow design, see the OMOP vocabulary SDK guide.
What to inspect in returned properties
Once properties are present, review them with the downstream use case in mind:
- Synonyms support search, ingestion cleanup, and source term normalization.
- Crosswalk or mapping-related properties help connect source vocabularies to OMOP standard concepts.
- Version-sensitive attributes help explain why the same code may behave differently across terminology releases.
- Terminology-specific metadata helps validators and ETL rules make decisions without hard-coded lookup tables.
For OMOP-focused integrations, that last point matters more than many FHIR tutorials admit. A generic terminology server can return technically correct FHIR. A vocabulary service built for OMOP work is more useful when the next step is concept mapping, vocabulary QA, or release-aware ETL behavior.
Programmatic Lookups with the OMOPHub Python and R SDKs

Raw HTTP is fine for proving connectivity. It's a weak pattern for a recurring ETL job, a notebook workflow, or a service that has to parse FHIR Parameters cleanly every time.
Benchmark data from InterSystems shows that turning FHIR JSON into dynamic objects and then static classes is a successful ETL pattern, but inefficient implementation can add 300ms of latency. The same reference notes that efficient key/value access patterns help support the sub-50ms response targets common in high-performance APIs, as discussed in the InterSystems IRIS for Health FHIR data handling documentation.
Python example
With a Python client, the useful abstraction isn't magic. It's just less boilerplate around auth, request construction, and response parsing.
import os
import requests
API_KEY = os.environ["OMOPHUB_API_KEY"]
url = "https://fhir.omophub.com/fhir/r4/CodeSystem/$lookup"
params = {
"system": "http://snomed.info/sct",
"code": "44054006"
}
headers = {
"Authorization": f"Bearer {API_KEY}",
"Accept": "application/fhir+json"
}
resp = requests.get(url, params=params, headers=headers, timeout=30)
resp.raise_for_status()
payload = resp.json()
for p in payload.get("parameter", []):
print(p.get("name"), p)
That keeps the response raw, which is often what you want first. Parse after you've inspected real payloads.
For package-level tooling and examples, see the OMOPHub Python SDK repository.
R example
R users tend to hit this from data prep, phenotype work, or mapping QA. The mechanics are similar.
library(httr2)
library(jsonlite)
api_key <- Sys.getenv("OMOPHUB_API_KEY")
resp <- request("https://fhir.omophub.com/fhir/r4/CodeSystem/$lookup") |>
req_headers(
Authorization = paste("Bearer", api_key),
Accept = "application/fhir+json"
) |>
req_url_query(
system = "http://snomed.info/sct",
code = "44054006"
) |>
req_perform()
payload <- resp_body_json(resp)
str(payload$parameter)
For R-specific client patterns, wrappers, and related examples, the OMOPHub R SDK repository is the place to start.
Why SDK-style access wins
The main win is consistency. You don't want every engineer in the team handling Parameters slightly differently.
- Auth stays centralized instead of being repeated in every script.
- Parsing becomes reusable so your ETL jobs don't fork on trivial JSON details.
- Error handling is easier to standardize across notebook code, batch jobs, and services.
If you're deciding whether to build a tiny internal wrapper or lean on an existing client, the OMOP vocabulary SDK overview captures the trade-off well. Reduce repetitive transport code first. Optimize business logic second.
Integrating Lookup Results into OMOP Workflows

A source feed lands with system and code, and the terminology looks valid. The ETL still cannot decide where that record belongs in OMOP.
$lookup helps confirm what the source code means. In OMOP work, the next questions are operational. Which standard concept does it map to? What domain is it in? Which CDM table should receive it? Those decisions drive the load logic, the QA checks, and the audit trail.
Generic FHIR tutorials usually stop at display text and returned properties. Production OMOP pipelines need one step beyond that. They need a reliable way to turn a valid coding into an OMOP-ready mapping decision.
Why $lookup alone is often not enough
Suppose your source data gives you a FHIR coding like this:
{
"system": "http://snomed.info/sct",
"code": "44054006"
}
A standard $lookup response can confirm the display and expose terminology metadata. That is useful for validation, analyst review, and UI rendering. It does not resolve OMOP vocabulary relationships for you.
Your ETL still has to:
- Find the standard OMOP concept
- Follow OMOP mapping relationships
- Confirm the target domain
- Select the correct CDM table
- Store enough mapping context for traceability
Teams that push all of that into the client usually end up with the same vocabulary logic copied across batch jobs, notebooks, and services. That duplication is where mapping drift starts.
The one-call resolver pattern
A cleaner pattern is to keep $lookup for terminology inspection and use a resolver for OMOP placement. OMOPHub exposes a FHIR-to-OMOP resolver that accepts the FHIR system URI and code, then returns the mapped standard concept, domain, and target table in one response. The server handles the OMOP vocabulary traversal instead of forcing every downstream job to rebuild it.
That endpoint looks like this:
curl -X POST "https://api.omophub.com/v1/fhir/resolve" \
-H "Authorization: Bearer oh_your_api_key" \
-H "Content-Type: application/json" \
-d '{
"system": "http://snomed.info/sct",
"code": "44054006",
"resource_type": "Condition"
}'
This split is practical. Use $lookup when you need to inspect the source terminology. Use a resolver when the primary requirement is to load into OMOP correctly.
Where this fits in a pipeline
In a production pipeline, $lookup is usually a support step rather than the final mapping step.
- Ingress checks that codings are complete and use the expected system URI
- Lookup pulls display text or source metadata for QA, analyst review, or debugging
- Resolve maps the coding to the OMOP standard concept and target table
- Load writes the record to the correct CDM table with source and mapping metadata preserved
That separation keeps responsibilities clear. $lookup answers, "what code is this?" The resolver answers, "where does it land in OMOP?"
If you are designing that handoff between FHIR codings and OMOP concepts, the FHIR to OMOP vocabulary mapping guide goes deeper on the mapping layer and its ETL implications.
Common $lookup Errors and Pro Tips for Success
A typical failure looks small at first. An inbound Condition carries 44054006 but drops the system URI, or sends SNOMED instead of the canonical SNOMED URI. The $lookup call fails, the mapping step has nothing to work with, and the ETL job now needs triage instead of a clean retry.
In production, the expensive part is rarely the bad request itself. The expensive part is finding out whether the problem came from the sender, the terminology store, or your own parameter handling.
AWS HealthLake is strict about request shape. If code or system is missing or malformed, expect a client error. If the code system or concept is not available in the datastore, expect a not found response. It also only resolves against terminology content it hosts, so a generic assumption that the server will query external sources often leads to wasted debugging time.
What breaks most often
- Missing
system. A bare code is usually ambiguous outside a tightly controlled feed. - Wrong canonical URI.
SNOMED,snomed, andhttp://snomed.info/sctdo not mean the same thing to a FHIR terminology server. - Assuming external fallback. Many implementations only search locally loaded CodeSystems and won't reach out to another terminology service.
- Ignoring version behavior. If your source feed changes vocabulary version and your lookup layer does not preserve that context, test results and audit results can diverge.
Pro tips that save time
- Test the coding before you debug the client. Start with the exact
system,code, and optionalversionvalues you received. That isolates vocabulary issues from auth, SDK, and HTTP issues. - Cache with context. Use cache keys that include system, code, and version when present. Caching by code alone creates subtle errors that are hard to spot in OMOP loads.
- Log the raw
Parametersresponse during development. Repeated properties and display variants matter. Flattening too early throws away detail you may need for QA or terminology review. - Treat 404 as a vocabulary or feed problem first. Common causes are an unloaded code system, an inactive code, or the wrong canonical URI from the source system.
- Use SDK error handling in batch jobs. Standardized retries and clearer exception types make it easier to separate transient transport failures from vocabulary defects.
One practical point matters in OMOP workflows. $lookup helps you verify the source code and inspect terminology metadata, but it does not answer the downstream ETL question by itself. In pipelines built around OMOPHub, that means using $lookup to validate and inspect the incoming coding, then handing off to the resolver layer when you need the standard concept, domain, and target table. Generic FHIR tutorials usually stop at the terminology response. Real ETL work does not.
The security model is also simpler than many teams expect. A terminology lookup service usually handles codes, concept identifiers, and search terms rather than direct clinical narrative or patient payloads. That makes it easier to place in front of broader FHIR to OMOP pipelines, provided you still enforce per-user API keys, HTTPS, and standard OAuth2 controls where supported.
If you're building ETL, terminology QA, or FHIR-to-OMOP mapping workflows, OMOPHub is one option to evaluate. It exposes both a FHIR terminology service and a resolver API, so you can use standard $lookup where it fits and switch to direct OMOP concept resolution when your pipeline needs domain and CDM table output instead of raw terminology metadata.


