# Docs for Agents Source: https://docs.omophub.com/ai/docs-for-agents Machine-readable llms.txt, llms-full.txt, and raw Markdown endpoints that give AI agents structured OMOPHub documentation and LLM context. ## Overview OMOPHub provides documentation in three formats specifically designed for AI consumption. Each format serves a different use case depending on your context window budget and the depth of information your agent needs. ## Documentation Formats Concise index of the OMOPHub API. Fits in small context windows. Complete documentation in a single file. Full API reference included. Structured skills reference for AI agents with action-oriented instructions. ## When to Use Each Format | Format | Size | Best For | | :-------------- | :----- | :------------------------------------------------------------------------------------------------------------------------------------------------------- | | `llms.txt` | Small | Quick orientation. Gives an agent a high-level map of available endpoints and capabilities. Use when context window space is limited. | | `llms-full.txt` | Large | Deep integration work. Contains the full API reference, parameter details, and response schemas. Use when the agent needs to write code against the API. | | `skill.md` | Medium | Action-oriented tasks. Structured as capabilities the agent can perform, with step-by-step instructions. Use for agents that follow skill/tool patterns. | ## Direct URLs You can fetch these files directly for injection into your agent's context: ``` https://docs.omophub.com/llms.txt https://docs.omophub.com/llms-full.txt https://docs.omophub.com/skill.md ``` ## Usage Example To give an AI agent access to OMOPHub documentation, include the appropriate file in the system prompt or context: ```python theme={null} import httpx # Fetch the concise index for a small context window response = httpx.get("https://docs.omophub.com/llms.txt") llms_context = response.text # Include in your agent's system prompt system_prompt = f"""You are a medical coding assistant. Use the following API documentation to look up medical concepts: {llms_context} """ ``` # AI and LLM integration with OMOP vocabularies Source: https://docs.omophub.com/ai/integration-guide Pair LLMs with OMOPHub vocabulary lookups to eliminate hallucinated medical codes, ground clinical chatbots, and verify every OMOP concept ID. ## 1. The "Stochastic Parrot" in the Exam Room An LLM passes the USMLE. It drafts a patient summary that reads better than most residents' notes. It explains a complex diagnosis in plain language that would take a doctor fifteen minutes to compose. Magic. Then you ask it for the ICD-10 code for "Type 2 diabetes mellitus with diabetic chronic kidney disease." It returns E11.22. Confident. Authoritative. And wrong: the actual code is E11.22 for Type 2 DM with diabetic CKD, but the decimal placement, edition specificity, and mapping to the correct OMOP concept are all things the LLM is guessing at based on pattern matching, not looking up in a source of truth. In a clinical setting, "mostly right" is the same as "dangerously wrong." This is the hallucination problem for clinical AI. LLMs are reasoning engines - they understand clinical language, extract entities, and follow logical chains. But they are not vocabulary databases. They predict the next likely token; they don't look up codes. When they output a SNOMED concept ID, an ICD-10 code, or an RxNorm identifier, they're generating something that *looks right* based on training data patterns, not verifying it against a live vocabulary source. **OMOPHub** solves the verification half of this problem. It's a vocabulary API: the source of truth for OMOP concept IDs, SNOMED codes, RxNorm identifiers, LOINC codes, and 100+ other vocabularies. By pairing an LLM (for reasoning and extraction) with OMOPHub (for vocabulary lookup and verification), you get a system where the LLM does what it's good at and OMOPHub does what it's good at. Neither one alone is sufficient. Together, they're a clinical AI pipeline that's both intelligent and grounded. ## 2. The Core Concept: The "OMOP-Loop" Every clinical AI tool that touches standardized codes should follow this three-step pattern: **1. Extract**: The LLM processes unstructured text (physician notes, patient questions, trial protocols) and identifies clinical entities: conditions, drugs, procedures, measurements. **2. Ground**: Each extracted entity is sent to OMOPHub. The API returns the actual, verified OMOP concept ID. No guessing, no hallucination - just a vocabulary lookup. If the entity doesn't match anything in OMOPHub, that's a signal to flag it for review rather than fabricate a code. **3. Reason**: The LLM uses the grounded, verified concept IDs to perform its final task: generating a query, drafting a coded summary, checking trial eligibility, or building a concept set. The key insight: the LLM never generates codes directly. It extracts clinical meaning from text (which it's excellent at), then hands off to OMOPHub for the standardization step (which a vocabulary API is built for). This separation of concerns is what makes the system safe. ## 3. Use Case A: Building a Grounded Clinical Research Chatbot A researcher asks: "Find me all patients with heart failure who are on ACE inhibitors." A generic chatbot understands the intent but doesn't know which OMOP concept IDs to query. A grounded chatbot does. **The Workflow:** LLM extracts "heart failure" and "ACE inhibitor" then OMOPHub resolves each to a standard concept ID and expands descendants and only then the application generates OMOP CDM SQL with verified IDs. ```bash theme={null} pip install omophub ``` ````python Python theme={null} import omophub import json from openai import OpenAI # Or any LLM client omop = omophub.OMOPHub() llm = OpenAI() # API key from environment def ground_clinical_query(user_query): """Extract clinical entities via LLM, ground them via OMOPHub.""" print(f"User query: {user_query}\n") # --- Step 1: EXTRACT (LLM) --- extraction_prompt = ( f"Extract clinical conditions and drug classes from this query: " f"'{user_query}'. Return ONLY a JSON array of strings, no explanation." ) response = llm.chat.completions.create( model="gpt-4o-mini", messages=[{"role": "user", "content": extraction_prompt}], ) raw_output = response.choices[0].message.content.strip() # Strip markdown code fences if present if raw_output.startswith("```"): raw_output = raw_output.split("\n", 1)[1].rsplit("```", 1)[0] extracted_terms = json.loads(raw_output) print(f" LLM extracted: {extracted_terms}\n") grounded = {} # --- Step 2: GROUND (OMOPHub) --- for term in extracted_terms: print(f" Grounding: '{term}'") # Determine likely domain from context # (In production, have the LLM tag each entity with its domain) is_drug = any(kw in term.lower() for kw in ["inhibitor", "blocker", "agonist", "drug", "medication"]) vocab_filter = ["RxNorm"] if is_drug else ["SNOMED"] domain_filter = ["Drug"] if is_drug else ["Condition"] try: # Search OMOPHub for the standard concept results = omop.search.basic( term, vocabulary_ids=vocab_filter, domain_ids=domain_filter, page_size=3, ) candidates = results.get("concepts", []) if results else [] # If basic search misses, try semantic if not candidates: semantic = omop.search.semantic(term, vocabulary_ids=vocab_filter, domain_ids=domain_filter, page_size=3) candidates = (semantic.get("results", semantic.get("concepts", [])) if semantic else []) if candidates: best = candidates[0] concept_id = best["concept_id"] print(f" -> {best.get('concept_name')} (ID: {concept_id})") # Expand descendants for comprehensive concept set concept_set = {concept_id} try: desc = omop.hierarchy.descendants(concept_id, max_levels=3, relationship_types=["Is a"]) desc_list = ( desc if isinstance(desc, list) else desc.get("concepts", []) ) if desc else [] for d in desc_list: concept_set.add(d["concept_id"]) except omophub.APIError: pass # Use just the parent if hierarchy fails grounded[term] = { "concept_name": best.get("concept_name"), "concept_id": concept_id, "domain": best.get("domain_id"), "expanded_count": len(concept_set), "concept_ids": list(concept_set), } print(f" -> Expanded to {len(concept_set)} concepts (including descendants)") else: print(f" -> NOT FOUND - flagging for manual review") grounded[term] = {"concept_name": None, "concept_id": None, "status": "ungrounded"} except omophub.APIError as e: print(f" -> API error: {e.message}") return grounded # --- Example --- query = "Patients with heart failure on ACE inhibitors" result = ground_clinical_query(query) # Step 3: REASON - use grounded IDs to build a query print("\n--- Grounded Concept Sets ---") for term, data in result.items(): if data.get("concept_id"): print(f" '{term}': {data['concept_name']} -> {data['expanded_count']} concept IDs") else: print(f" '{term}': UNGROUNDED - needs manual resolution") ```` **The Key Insight:** The LLM never touches a concept ID. It extracts "heart failure" and "ACE inhibitor" as text strings. OMOPHub resolves those strings to verified OMOP concept IDs. The hierarchy expansion ensures you catch patients coded with specific subtypes (e.g., "Congestive heart failure" or "Acute on chronic systolic heart failure"). This is the OMOP-Loop in action: Extract → Ground → Reason. ## 4. Use Case B: Grounded Clinical Note Coding An AI scribe generates a visit summary. For it to be useful for billing and research, the clinical entities need to be linked to verified codes - not LLM-hallucinated ones. **The Workflow:** LLM generates the summary → LLM extracts key clinical entities → OMOPHub grounds each entity to the correct vocabulary (ICD-10-CM for conditions, RxNorm for drugs). ````python Python theme={null} import omophub import json from openai import OpenAI omop = omophub.OMOPHub() llm = OpenAI() # AI-generated visit summary visit_summary = """ Patient presents with persistent cough and shortness of breath for 2 weeks. Physical exam reveals wheezing. Suspected acute bronchitis. Performed a chest X-ray to rule out pneumonia. Prescribed Albuterol inhaler. """ print("Grounding clinical note for coding...\n") # Step 1: LLM extracts entities with domain tags extraction_prompt = f"""Extract clinical entities from this visit note. Return JSON array of objects with "term" and "type" (condition/procedure/drug): {visit_summary} Return ONLY the JSON array.""" response = llm.chat.completions.create( model="gpt-4o-mini", messages=[{"role": "user", "content": extraction_prompt}], ) raw = response.choices[0].message.content.strip() if raw.startswith("```"): raw = raw.split("\n", 1)[1].rsplit("```", 1)[0] entities = json.loads(raw) print(f" LLM extracted: {json.dumps(entities, indent=2)}\n") # Step 2: Ground each entity via OMOPHub in the appropriate vocabulary DOMAIN_CONFIG = { "condition": {"vocabs": ["SNOMED"], "domains": ["Condition"]}, "procedure": {"vocabs": ["SNOMED"], "domains": ["Procedure"]}, "drug": {"vocabs": ["RxNorm"], "domains": ["Drug"]}, } suggested_codes = [] for entity in entities: term = entity.get("term", "") entity_type = entity.get("type", "condition").lower() config = DOMAIN_CONFIG.get(entity_type, DOMAIN_CONFIG["condition"]) print(f" Grounding [{entity_type}]: '{term}'") try: results = omop.search.basic( term, vocabulary_ids=config["vocabs"], domain_ids=config["domains"], page_size=3, ) candidates = results.get("concepts", []) if results else [] if candidates: best = candidates[0] print(f" -> {best.get('concept_name')} ({best.get('vocabulary_id')}: {best.get('concept_code', 'N/A')}, OMOP: {best['concept_id']})") # For billing codes, also get the ICD-10-CM mapping if entity_type == "condition": mappings = omop.mappings.get(best["concept_id"], target_vocabulary="ICD10CM") map_list = ( mappings if isinstance(mappings, list) else mappings.get("concepts", mappings.get("mappings", [])) ) if mappings else [] if map_list: icd = map_list[0] print(f" -> ICD-10-CM: {icd.get('concept_name')} ({icd.get('concept_code')})") suggested_codes.append({ "term": term, "type": entity_type, "concept_name": best.get("concept_name"), "concept_id": best["concept_id"], "vocabulary": best.get("vocabulary_id"), "code": best.get("concept_code"), }) else: print(f" -> No match found") except omophub.APIError as e: print(f" -> API error: {e.message}") print(f"\n--- Suggested Codes (All Verified via OMOPHub) ---") for code in suggested_codes: print(f" [{code['vocabulary']}] {code['concept_name']} (Code: {code['code']}, OMOP: {code['concept_id']})") ```` **The Key Insight:** The LLM extracts entities and tags them by type. OMOPHub resolves each one in the appropriate vocabulary. No code is ever generated by the LLM - every code comes from a verified vocabulary lookup. This is the difference between "AI-suggested coding" (risky) and "AI-extracted, vocabulary-verified coding" (safe). ## 5. The Hallucination Safety Net This is the most important pattern in the entire article. Every time an LLM outputs a clinical code or concept ID, verify it against OMOPHub before using it. ```python Python theme={null} import omophub omop = omophub.OMOPHub() def verify_concept(concept_id, expected_domain=None): """ Verify that an LLM-generated concept ID actually exists in OMOP. Returns the concept if valid, None if hallucinated. """ try: concept = omop.concepts.get(concept_id) if not concept: return {"valid": False, "reason": "Concept ID does not exist"} # Check domain if expected if expected_domain and concept.get("domain_id") != expected_domain: return { "valid": False, "reason": f"Domain mismatch: expected {expected_domain}, got {concept.get('domain_id')}", "concept": concept, } # Check if it's a standard concept if concept.get("standard_concept") != "S": return { "valid": False, "reason": f"Non-standard concept (standard_concept='{concept.get('standard_concept')}'). Use the 'Maps to' standard equivalent.", "concept": concept, } return { "valid": True, "concept_name": concept.get("concept_name"), "concept_id": concept["concept_id"], "domain": concept.get("domain_id"), "vocabulary": concept.get("vocabulary_id"), } except omophub.APIError: return {"valid": False, "reason": "Concept ID not found in OMOP vocabulary"} # --- Example: LLM claims these are valid concept IDs --- llm_outputs = [ {"claimed_id": 201826, "claimed_name": "Type 2 diabetes mellitus", "domain": "Condition"}, {"claimed_id": 9999999, "claimed_name": "Fake condition", "domain": "Condition"}, {"claimed_id": 4329847, "claimed_name": "Myocardial infarction", "domain": "Condition"}, ] print("Hallucination Safety Check:\n") for output in llm_outputs: result = verify_concept(output["claimed_id"], expected_domain=output["domain"]) status = "VERIFIED" if result["valid"] else "REJECTED" reason = f" - {result['reason']}" if not result["valid"] else "" print(f" {status}: ID {output['claimed_id']} ({output['claimed_name']}){reason}") ``` **The Key Insight:** This is a 10-line function that catches hallucinated codes before they enter your system. `concepts.get()` is a real OMOPHub SDK method - it takes a concept ID and returns the concept if it exists. If the LLM hallucinated the ID, the call returns nothing. If the ID exists but is in the wrong domain (the LLM said it was a Condition but it's actually a Drug), the domain check catches it. If it's a non-standard concept, you get a warning to find the standard equivalent. This single validation step is worth more than any amount of prompt engineering for clinical safety. ## 6. Conclusion: Don't Just Prompt - Ground LLMs are extraordinary reasoning engines. They understand clinical language better than any rule-based system ever could. But they are not vocabulary databases, and they should never be trusted to generate clinical codes from memory. The OMOP-Loop - Extract, Ground, Reason - is the pattern that makes clinical AI safe: * The LLM extracts clinical meaning from text (what it's built for) * OMOPHub verifies and standardizes every entity against the OMOP vocabularies (what it's built for) * The LLM reasons over verified, grounded data to produce its final output No hallucinated codes. No fabricated concept IDs. No "mostly right" in a setting where mostly right is dangerously wrong. Build the OMOP-Loop into your next clinical AI tool. Start with the hallucination safety net - `concepts.get()` on every LLM-generated concept ID. That single check is the difference between a demo and a deployable system. # MCP Server HTTP and Docker deployment guide Source: https://docs.omophub.com/ai/mcp-docker Deploy the OMOPHub MCP Server as an HTTP service or Docker container for centralized, multi-agent access to OMOP medical vocabularies. ## HTTP Transport Mode Run the MCP server as an HTTP service that clients connect to via URL. Useful for centralized deployments where multiple AI agents share one server instance. ```bash theme={null} # Start HTTP server on port 3100 npx -y @omophub/omophub-mcp --transport=http --port=3100 --api-key=oh_your_key_here ``` The server exposes two endpoints: | Endpoint | Description | | :-------- | :--------------------------------------------------- | | `/mcp` | MCP protocol endpoint - connect your AI clients here | | `/health` | Health check endpoint for monitoring | ```bash theme={null} # Verify the server is running curl http://localhost:3100/health # → {"status":"ok","version":"1.1.0","uptime_seconds":42} ``` ## Docker The Docker image defaults to HTTP mode on port 3100 with health checks built in. ### Quick Start ```bash theme={null} # HTTP mode (default in Docker) - serves MCP on port 3100 docker run -e OMOPHUB_API_KEY=oh_your_key_here -p 3100:3100 omophub/omophub-mcp ``` ### Stdio Mode For piping to local MCP clients: ```bash theme={null} docker run -i -e OMOPHUB_API_KEY=oh_your_key_here omophub/omophub-mcp --transport=stdio ``` ### Docker Compose ```yaml theme={null} services: omophub-mcp: image: omophub/omophub-mcp ports: - "3100:3100" environment: OMOPHUB_API_KEY: ${OMOPHUB_API_KEY} healthcheck: test: ["CMD", "curl", "-f", "http://localhost:3100/health"] interval: 30s timeout: 10s retries: 3 start_period: 10s ``` Multi-arch support: the Docker image is available for both `amd64` and `arm64` architectures. ## Health Checks ### HTTP Mode The health endpoint is automatically available at `/health` on the same port as the MCP endpoint: ```bash theme={null} npx @omophub/omophub-mcp --transport=http --port=3100 --api-key=oh_your_key curl http://localhost:3100/health ``` ### Stdio Mode Use `--health-port` for a standalone health endpoint when running in stdio mode: ```bash theme={null} HEALTH_PORT=8080 OMOPHUB_API_KEY=oh_your_key npx @omophub/omophub-mcp curl http://localhost:8080/health ``` ## CLI Arguments ```bash theme={null} # Stdio mode (default) npx @omophub/omophub-mcp --api-key=oh_your_key --base-url=https://custom.api.com/v1 # HTTP mode npx @omophub/omophub-mcp --transport=http --port=3100 --api-key=oh_your_key # Stdio mode with standalone health endpoint npx @omophub/omophub-mcp --api-key=oh_your_key --health-port=8080 ``` # Hosted MCP Server for OMOP vocabularies Source: https://docs.omophub.com/ai/mcp-hosted Connect AI clients to the OMOPHub MCP Server hosted at mcp.omophub.com with no installation, giving agents instant access to OMOP vocabularies. ## What is Hosted MCP? OMOPHub runs an MCP server for you at `mcp.omophub.com`. Connect your AI client directly - no npm install, no Docker, no infrastructure to manage. Just your API key and a URL. All 9 tools are available: concept search, semantic search, hierarchy navigation, cross-vocabulary mapping, and more. ## Endpoint | URL | Description | | :------------------------ | :-------------------- | | `https://mcp.omophub.com` | MCP protocol endpoint | ## Client Compatibility | Client | Hosted MCP | How | | :------------------ | :--------: | :----------------------------------------------------------------------------------------------------------------------------------- | | **Claude Code** | Yes | Custom headers supported natively | | **VS Code** | Yes | `headers` field in mcp.json | | **Cursor** | Yes | `headers` field in MCP config | | **Windsurf** | Yes | Same as Cursor | | **Claude Desktop** | Use npx | Custom Connectors UI only supports OAuth, not Bearer tokens. Use the [self-hosted npx setup](/ai/mcp-server#claude-desktop) instead. | | **Claude.ai (web)** | Use npx | Same limitation as Claude Desktop. | ## Quick Setup You need two things: the endpoint URL and your API key (get one at [dashboard.omophub.com](https://dashboard.omophub.com)). ### Claude Code ```bash theme={null} claude mcp add omophub --transport http \ -H "Authorization: Bearer oh_your_key_here" \ https://mcp.omophub.com ``` ### VS Code Add to `.vscode/mcp.json`: ```json theme={null} { "servers": { "omophub": { "type": "http", "url": "https://mcp.omophub.com", "headers": { "Authorization": "Bearer oh_your_key_here" } } } } ``` ### Cursor Add to your Cursor MCP configuration: ```json theme={null} { "mcpServers": { "omophub": { "url": "https://mcp.omophub.com", "headers": { "Authorization": "Bearer oh_your_key_here" } } } } ``` ### Windsurf Same configuration pattern as Cursor: ```json theme={null} { "mcpServers": { "omophub": { "url": "https://mcp.omophub.com", "headers": { "Authorization": "Bearer oh_your_key_here" } } } } ``` ### Claude Desktop Claude Desktop's Custom Connectors UI only supports OAuth authentication and cannot send custom Bearer tokens. Use the self-hosted npx approach instead - it works identically and requires no infrastructure: ```json theme={null} { "mcpServers": { "omophub": { "command": "npx", "args": ["-y", "@omophub/omophub-mcp"], "env": { "OMOPHUB_API_KEY": "oh_your_key_here" } } } } ``` ## Verify Connection ```bash theme={null} curl https://mcp.omophub.com/health # {"status":"ok","version":"1.3.0"} ``` Prefer to run the MCP server yourself? See the self-hosted installation guide. ## Troubleshooting | Issue | Solution | | :------------------- | :--------------------------------------------------------- | | `Connection refused` | Verify the URL is `https://mcp.omophub.com` (not just `/`) | | `401 Unauthorized` | Check your API key is valid and prefixed with `oh_` | # OMOPHub MCP Server for Claude, Cursor, and VS Code Source: https://docs.omophub.com/ai/mcp-server Install the OMOPHub MCP Server to give Claude, Cursor, VS Code, and other AI clients direct access to OMOP medical vocabularies and concept search. ## What is the MCP Server? The OMOPHub MCP Server connects your AI assistant to the entire OHDSI ATHENA vocabulary. No database setup, no CSV wrangling - just ask your AI about medical concepts and get verified answers. ``` You: "Map ICD-10 code E11.9 to SNOMED" Claude: Found it - E11.9 (Type 2 diabetes mellitus without complications) maps to SNOMED concept 201826 (Type 2 diabetes mellitus) via standard 'Maps to' relationship. ``` Skip installation entirely. Connect your AI client directly to mcp.omophub.com with just your API key. ## Installation Install via npm - no build step required: ```bash theme={null} npm install -g @omophub/omophub-mcp ``` Or run directly with `npx`: ```bash theme={null} npx -y @omophub/omophub-mcp ``` ### Claude Desktop Open Claude Desktop settings > **Developer** tab > **Edit Config**. Add to `claude_desktop_config.json`: ```json theme={null} { "mcpServers": { "omophub": { "command": "npx", "args": ["-y", "@omophub/omophub-mcp"], "env": { "OMOPHUB_API_KEY": "oh_your_key_here" } } } } ``` ### Claude Code ```bash theme={null} claude mcp add omophub -- npx -y @omophub/omophub-mcp # Then set OMOPHUB_API_KEY in your environment ``` ### Cursor Open the command palette > **Cursor Settings** > **MCP** > **Add new global MCP server**. Add to `.cursor/mcp.json`: ```json theme={null} { "mcpServers": { "omophub": { "command": "npx", "args": ["-y", "@omophub/omophub-mcp"], "env": { "OMOPHUB_API_KEY": "oh_your_key_here" } } } } ``` ### VS Code Add to `.vscode/mcp.json`: ```json theme={null} { "servers": { "omophub": { "command": "npx", "args": ["-y", "@omophub/omophub-mcp"], "env": { "OMOPHUB_API_KEY": "oh_your_key_here" } } } } ``` ### Windsurf Add to your Windsurf MCP configuration: ```json theme={null} { "mcpServers": { "omophub": { "command": "npx", "args": ["-y", "@omophub/omophub-mcp"], "env": { "OMOPHUB_API_KEY": "oh_your_key_here" } } } } ``` ## Available Tools | Tool | Description | | :---------------------- | :---------------------------------------------------------------------------------- | | `search_concepts` | Search for medical concepts by name or clinical term across all vocabularies | | `get_concept` | Get detailed info about a specific OMOP concept by `concept_id` | | `get_concept_by_code` | Look up a concept using a vocabulary-specific code (e.g., ICD-10 `E11.9`) | | `map_concept` | Map a concept to equivalent concepts in other vocabularies | | `get_hierarchy` | Navigate concept hierarchy - ancestors, descendants, or both | | `list_vocabularies` | List available medical vocabularies with statistics | | `semantic_search` | Search using natural language with neural embeddings - understands clinical meaning | | `find_similar_concepts` | Find concepts similar to a reference concept, name, or description | | `explore_concept` | Get concept details, hierarchy, and cross-vocabulary mappings in one call | See detailed parameter schemas and examples for each tool. ## Environment Variables | Variable | Required | Description | | :------------------------- | :------: | :------------------------------------------------------------- | | `OMOPHUB_API_KEY` | Yes | Your OMOPHub API key | | `OMOPHUB_BASE_URL` | | Custom API base URL (default: `https://api.omophub.com/v1`) | | `OMOPHUB_LOG_LEVEL` | | `debug` · `info` · `warn` · `error` (default: `info`) | | `OMOPHUB_ANALYTICS_OPTOUT` | | Set to `true` to disable analytics headers | | `MCP_TRANSPORT` | | `stdio` (default) or `http` | | `MCP_PORT` | | HTTP server port (default: `3100`, only with `http` transport) | | `HEALTH_PORT` | | Port for standalone health endpoint in stdio mode | ## Example Prompts Try these after installing: > "What's the OMOP concept ID for type 2 diabetes?" > "Map ICD-10 code E11.9 to SNOMED" > "Show me all descendants of Diabetes mellitus in SNOMED" > "Search for metformin in RxNorm" > "Help me build a concept set for heart failure including all descendants" > "Find concepts related to 'heart attack'" *(uses semantic search)* > "Give me everything about SNOMED concept 201826" *(uses explore)* > "What concepts are similar to 'Type 2 diabetes mellitus'?" ## Troubleshooting | Error | Solution | | :---------------------- | :---------------------------------------------------------------------------------------------------- | | `API key required` | Set `OMOPHUB_API_KEY` in your environment or MCP config | | `Authentication failed` | API key may be invalid or expired - [generate a new one](https://dashboard.omophub.com) | | `Rate limit exceeded` | Automatic retries are built in. For higher limits, [upgrade your plan](https://dashboard.omophub.com) | | Tools not appearing | Restart your AI client, verify `npx @omophub/omophub-mcp` runs without errors | # MCP tools reference for OMOPHub vocabularies Source: https://docs.omophub.com/ai/mcp-tools Complete parameter reference for all 11 OMOPHub MCP tools: search, mapping, hierarchy, FHIR resolve, and vocabulary lookup for AI clients. **Multilingual support**: AI agents using these MCP tools can accept queries in any language. The agent translates to English before calling OMOPHub, then presents results in the user's language. No special configuration needed - just ask in your language. ## search\_concepts Search for medical concepts by name or clinical term across all vocabularies. ### Parameters | Parameter | Type | Required | Description | | :----------------- | :----- | :------: | :------------------------------------------------------------------------------------------------- | | `query` | string | Yes | The medical term or concept name to search for (1–500 characters) | | `vocabulary_ids` | string | | Comma-separated vocabulary IDs to filter by. Examples: `SNOMED`, `ICD10CM`, `RxNorm`, `LOINC` | | `domain_ids` | string | | Comma-separated domain IDs to filter by. Examples: `Condition`, `Drug`, `Measurement`, `Procedure` | | `standard_concept` | string | | Filter by standard concept status: `S` for Standard, `C` for Classification | | `page` | number | | Page number, 1-based (default: `1`) | | `page_size` | number | | Results per page, 1–50 (default: `10`) | ### Example ``` "Search for metformin in RxNorm" → search_concepts(query: "metformin", vocabulary_ids: "RxNorm") ``` *** ## get\_concept Get detailed information about a specific OMOP concept by its numeric ID. ### Parameters | Parameter | Type | Required | Description | | :----------- | :----- | :------: | :---------------------------------------- | | `concept_id` | number | Yes | The OMOP concept\_id (numeric identifier) | ### Example ``` "What is concept 201826?" → get_concept(concept_id: 201826) ``` *** ## get\_concept\_by\_code Look up a concept using a vocabulary-specific code (e.g., ICD-10 `E11.9`, SNOMED `44054006`). ### Parameters | Parameter | Type | Required | Description | | :-------------- | :----- | :------: | :-------------------------------------------------------------------------------------------- | | `vocabulary_id` | string | Yes | The vocabulary system. Examples: `ICD10CM`, `SNOMED`, `RxNorm`, `LOINC`, `HCPCS`, `NDC` | | `concept_code` | string | Yes | The vocabulary-specific code. Examples: `E11.9` (ICD-10), `44054006` (SNOMED), `4850` (LOINC) | ### Example ``` "Look up ICD-10 code E11.9" → get_concept_by_code(vocabulary_id: "ICD10CM", concept_code: "E11.9") ``` *** ## map\_concept Map a concept to equivalent concepts in other vocabularies (e.g., SNOMED to ICD-10). ### Parameters | Parameter | Type | Required | Description | | :-------------------- | :----- | :------: | :-------------------------------------------------------------------------------- | | `concept_id` | number | Yes | The source OMOP concept\_id to map from | | `target_vocabularies` | string | | Comma-separated vocabulary IDs to map to. Examples: `ICD10CM`, `SNOMED`, `RxNorm` | ### Example ``` "Map SNOMED concept 201826 to ICD-10" → map_concept(concept_id: 201826, target_vocabularies: "ICD10CM") ``` *** ## get\_hierarchy Navigate concept hierarchy - ancestors, descendants, or both. ### Parameters | Parameter | Type | Required | Description | | :--------------- | :----- | :------: | :------------------------------------------------------------------------------------ | | `concept_id` | number | Yes | The OMOP concept\_id | | `direction` | string | | `up` for ancestors, `down` for descendants, `both` for full context (default: `both`) | | `max_levels` | number | | Maximum levels to traverse (default: `5` for up, `10` for down) | | `max_results` | number | | Maximum nodes to return, 1–500 (default: `500`) | | `vocabulary_ids` | string | | Comma-separated vocabulary IDs to filter results | ### Example ``` "Show me all descendants of Diabetes mellitus" → get_hierarchy(concept_id: 201820, direction: "down", max_levels: 3) ``` *** ## list\_vocabularies List available medical vocabularies with statistics. ### Parameters | Parameter | Type | Required | Description | | :-------- | :----- | :------: | :-------------------------------------------------- | | `search` | string | | Optional search term to filter vocabularies by name | ### Example ``` "What vocabularies are available?" → list_vocabularies() "Search for drug vocabularies" → list_vocabularies(search: "drug") ``` *** ## semantic\_search Search for medical concepts using natural language with neural embeddings. Unlike keyword search, semantic search understands clinical meaning - "heart attack" finds "Myocardial infarction", "high blood sugar" finds "Hyperglycemia". ### Parameters | Parameter | Type | Required | Description | | :----------------- | :----- | :------: | :------------------------------------------------------------------------------------ | | `query` | string | Yes | Natural language description of the medical concept to find (1–500 characters) | | `vocabulary_ids` | string | | Comma-separated vocabulary IDs to filter by. Examples: `SNOMED`, `ICD10CM`, `RxNorm` | | `domain_ids` | string | | Comma-separated domain IDs to filter by. Examples: `Condition`, `Drug`, `Measurement` | | `standard_concept` | string | | Filter by standard concept status: `S` for Standard, `C` for Classification | | `threshold` | number | | Minimum similarity score, 0.0–1.0 (default: `0.5`). Higher = stricter matching | | `page_size` | number | | Results to return, 1–50 (default: `10`) | ### Example ``` "Find concepts related to heart attack" → semantic_search(query: "heart attack", vocabulary_ids: "SNOMED", threshold: 0.5) ``` *** ## find\_similar\_concepts Find medical concepts similar to a reference concept, name, or natural language query. Supports three similarity algorithms. Provide exactly **one** of: `concept_id`, `concept_name`, or `query`. ### Parameters | Parameter | Type | Required | Description | | :--------------------- | :----- | :----------: | :----------------------------------------------------------------------------------------------- | | `concept_id` | number | One of three | Find concepts similar to this OMOP concept ID | | `concept_name` | string | One of three | Find concepts similar to this concept name | | `query` | string | One of three | Find concepts matching this natural language description | | `algorithm` | string | | Similarity algorithm: `semantic` (meaning), `lexical` (text), `hybrid` (both). Default: `hybrid` | | `similarity_threshold` | number | | Minimum similarity score, 0.0–1.0 (default: `0.7`) | | `page_size` | number | | Results to return, 1–100 (default: `20`) | | `vocabulary_ids` | string | | Comma-separated vocabulary IDs to filter results | | `domain_ids` | string | | Comma-separated domain IDs to filter results | ### Example ``` "What concepts are similar to Type 2 diabetes?" → find_similar_concepts(concept_name: "Type 2 diabetes mellitus", algorithm: "hybrid") ``` *** ## explore\_concept Get a comprehensive view of a medical concept in one call: detailed info, ancestors/descendants hierarchy, and cross-vocabulary mappings. Use this instead of calling `get_concept` + `get_hierarchy` + `map_concept` separately. ### Parameters | Parameter | Type | Required | Description | | :-------------------- | :------ | :------: | :------------------------------------------------------------------------------- | | `concept_id` | number | Yes | The OMOP concept\_id to explore | | `include_hierarchy` | boolean | | Include ancestors and descendants (default: `true`) | | `hierarchy_levels` | number | | How many hierarchy levels to fetch, 1–5 (default: `2`) | | `include_mappings` | boolean | | Include cross-vocabulary mappings (default: `true`) | | `target_vocabularies` | string | | Comma-separated vocabulary IDs to filter mappings. Examples: `ICD10CM`, `SNOMED` | ### Example ``` "Give me everything about SNOMED concept 201826" → explore_concept(concept_id: 201826) "Show concept 201826 with ICD-10 mappings only" → explore_concept(concept_id: 201826, target_vocabularies: "ICD10CM") ``` *** ## fhir\_resolve Resolve a FHIR coded value (system URI + code) to its OMOP standard concept, CDM target table, and optional Phoebe recommendations. Supports text-only input via semantic search fallback. ### Parameters | Parameter | Type | Required | Description | | :------------------------ | :------ | :------: | :--------------------------------------------------------------------------------------------- | | `system` | string | | FHIR code system URI (e.g. `http://snomed.info/sct`, `http://loinc.org`) | | `code` | string | | Code value from the FHIR Coding | | `display` | string | | Display text for semantic search fallback when code is unavailable | | `vocabulary_id` | string | | Direct OMOP vocabulary\_id (e.g. `SNOMED`, `ICD10CM`), bypasses URI resolution | | `resource_type` | string | | FHIR resource type (`Condition`, `Observation`, `MedicationRequest`, `Procedure`, etc.) | | `include_recommendations` | boolean | | Include Phoebe-recommended related concepts (default: `false`) | | `include_quality` | boolean | | Include mapping quality signal: `high`, `medium`, `low`, or `manual_review` (default: `false`) | At least one of (`system` + `code`), (`vocabulary_id` + `code`), or `display` is required. ### Example ``` "What OMOP concept does SNOMED 44054006 map to?" → fhir_resolve(system: "http://snomed.info/sct", code: "44054006", resource_type: "Condition") "Resolve this ICD-10 code to OMOP with quality signal" → fhir_resolve(system: "http://hl7.org/fhir/sid/icd-10-cm", code: "E11.9", include_quality: true) "Find the OMOP concept for 'heart attack' using semantic search" → fhir_resolve(display: "heart attack", resource_type: "Condition") ``` *** ## fhir\_resolve\_codeable\_concept Resolve a FHIR CodeableConcept with multiple codings. Picks the best match per OHDSI vocabulary preference (SNOMED > RxNorm > LOINC > CVX > ICD-10). Falls back to the text field via semantic search if no coding resolves. ### Parameters | Parameter | Type | Required | Description | | :------------------------ | :------ | :------: | :---------------------------------------------------------------------------------------- | | `coding` | array | Yes | Array of FHIR Coding entries (max 20). Each with `system`, `code`, and optional `display` | | `text` | string | | CodeableConcept.text for semantic search fallback if no coding resolves | | `resource_type` | string | | FHIR resource type for domain alignment | | `include_recommendations` | boolean | | Include Phoebe recommendations (default: `false`) | | `include_quality` | boolean | | Include mapping quality signal (default: `false`) | ### Example ``` "Which coding should I use from this CodeableConcept with SNOMED and ICD-10?" → fhir_resolve_codeable_concept( coding: [ {system: "http://snomed.info/sct", code: "44054006"}, {system: "http://hl7.org/fhir/sid/icd-10-cm", code: "E11.9"} ], resource_type: "Condition" ) ``` # AI agent onboarding for OMOP medical vocabularies Source: https://docs.omophub.com/ai/onboarding Give AI agents grounded access to 10M+ OMOP medical concepts with zero hallucination risk - search, map, and navigate vocabularies from any client. ## Why OMOPHub for AI? LLMs are powerful reasoning engines, but they hallucinate clinical codes. OMOPHub provides the verification layer: a vocabulary API covering SNOMED CT, ICD-10, RxNorm, LOINC, and 100+ medical terminologies. Your AI agent extracts clinical meaning from text. OMOPHub grounds it in verified OMOP concept IDs. No fabricated codes, no "mostly right" in a setting where mostly right is dangerously wrong. ## Prerequisites Sign up at [dashboard.omophub.com](https://dashboard.omophub.com). Go to **API Keys** in your dashboard and create a new key. ## Choose Your Integration Path Connect OMOPHub to Claude, Cursor, VS Code, or any MCP-compatible AI client. Install in under a minute. Feed your AI agent the right documentation format: `llms.txt`, `llms-full.txt`, or `skill.md`. Detailed reference for all 6 MCP tools: parameters, descriptions, and example usage. Build grounded clinical AI pipelines with the OMOP-Loop pattern. Eliminate hallucinated codes in production. ## What You Can Do | Capability | Description | | :--------------------------- | :----------------------------------------------------------------------- | | **Concept lookup** | Find OMOP concept IDs for any clinical term in seconds | | **Cross-vocabulary mapping** | Map between ICD-10, SNOMED, RxNorm, LOINC, and 100+ vocabularies | | **Hierarchy navigation** | Explore ancestors and descendants for phenotype definitions | | **Concept set building** | Let your AI agent assemble complete concept sets for cohort definitions | | **Code validation** | Verify medical codes and check their standard mappings | | **Hallucination detection** | Validate every LLM-generated concept ID against the OMOP source of truth | # Batch Concepts Source: https://docs.omophub.com/api-reference/concepts/batch-concepts POST /concepts/batch ## Overview Efficiently retrieve information for multiple concepts at once. This endpoint is optimized for bulk operations and reduces the number of API calls needed. ## Request Body Array of concept IDs to retrieve (max: 100) Include relationships for all concepts Include synonyms for all concepts Include cross-vocabulary mappings for all concepts Filter results to specific vocabularies Only return standard concepts ```bash cURL theme={null} curl -X POST "https://api.omophub.com/v1/concepts/batch" \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "concept_ids": [320128, 201826, 4329847], "include_relationships": true, "include_synonyms": true }' ``` ```python Python theme={null} import requests payload = { "concept_ids": [320128, 201826, 4329847], "include_relationships": True, "include_synonyms": True, "include_mappings": False } response = requests.post( "https://api.omophub.com/v1/concepts/batch", headers={"Authorization": "Bearer YOUR_API_KEY"}, json=payload ) concepts = response.json() for concept in concepts['data']['concepts']: print(f"{concept['concept_name']} ({concept['vocabulary_id']})") ``` ```javascript JavaScript theme={null} const response = await fetch('https://api.omophub.com/v1/concepts/batch', { method: 'POST', headers: { 'Authorization': 'Bearer YOUR_API_KEY', 'Content-Type': 'application/json' }, body: JSON.stringify({ concept_ids: [320128, 201826, 4329847], include_relationships: true, include_synonyms: true }) }); const data = await response.json(); console.log(`Retrieved ${data.data.concepts.length} concepts`); ``` ```bash cURL (with filtering) theme={null} curl -X POST "https://api.omophub.com/v1/concepts/batch" \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "concept_ids": [320128, 201826, 4329847, 313217], "vocabulary_filter": ["SNOMED", "ICD10CM"], "include_mappings": true, "standard_only": true }' ``` ```python Python (comprehensive) theme={null} import requests # Large batch request with comprehensive data concept_ids = [320128, 201826, 4329847, 313217, 435216] payload = { "concept_ids": concept_ids, "include_relationships": True, "include_synonyms": True, "include_mappings": True, "standard_only": True } response = requests.post( "https://api.omophub.com/v1/concepts/batch", headers={"Authorization": "Bearer YOUR_API_KEY"}, json=payload ) data = response.json() print(f"Successfully retrieved {len(data['data']['concepts'])} concepts") print(f"Failed to retrieve {len(data['data']['failed_concepts'])} concepts") # Process successful concepts for concept in data['data']['concepts']: print(f"\n{concept['concept_name']}") print(f" ID: {concept['concept_id']}") print(f" Vocabulary: {concept['vocabulary_id']}") print(f" Domain: {concept['domain_id']}") if concept.get('synonyms'): print(f" Synonyms: {len(concept['synonyms'])} found") if concept.get('relationships'): print(f" Relationships: {len(concept['relationships'])} found") ``` ```json theme={null} { "success": true, "data": { "concepts": [ { "concept_id": 320128, "concept_name": "Essential hypertension", "concept_code": "59621000", "vocabulary_id": "SNOMED", "domain_id": "Condition", "concept_class_id": "Clinical Finding", "standard_concept": "S", "valid_start_date": "1970-01-01", "valid_end_date": "2099-12-31", "invalid_reason": null, "synonyms": [ { "concept_synonym_name": "Primary hypertension", "language_concept_id": 4180186, "language_concept_name": "English" }, { "concept_synonym_name": "Idiopathic hypertension", "language_concept_id": 4180186, "language_concept_name": "English" } ], "relationships": [ { "relationship_id": "Is a", "target_concept_id": 316866, "target_concept_name": "Hypertensive disorder", "target_vocabulary_id": "SNOMED", "relationship_direction": "outbound" } ] }, { "concept_id": 201826, "concept_name": "Type 2 diabetes mellitus", "concept_code": "44054006", "vocabulary_id": "SNOMED", "domain_id": "Condition", "concept_class_id": "Clinical Finding", "standard_concept": "S", "valid_start_date": "1970-01-01", "valid_end_date": "2099-12-31", "invalid_reason": null, "synonyms": [ { "concept_synonym_name": "Type II diabetes mellitus", "language_concept_id": 4180186, "language_concept_name": "English" }, { "concept_synonym_name": "Adult-onset diabetes", "language_concept_id": 4180186, "language_concept_name": "English" } ], "relationships": [ { "relationship_id": "Is a", "target_concept_id": 73211009, "target_concept_name": "Diabetes mellitus", "target_vocabulary_id": "SNOMED", "relationship_direction": "outbound" } ] }, { "concept_id": 4329847, "concept_name": "Myocardial infarction", "concept_code": "22298006", "vocabulary_id": "SNOMED", "domain_id": "Condition", "concept_class_id": "Clinical Finding", "standard_concept": "S", "valid_start_date": "1970-01-01", "valid_end_date": "2099-12-31", "invalid_reason": null, "synonyms": [ { "concept_synonym_name": "Heart attack", "language_concept_id": 4180186, "language_concept_name": "English" }, { "concept_synonym_name": "MI - Myocardial infarction", "language_concept_id": 4180186, "language_concept_name": "English" } ], "relationships": [ { "relationship_id": "Is a", "target_concept_id": 134057, "target_concept_name": "Acute myocardial infarction", "target_vocabulary_id": "SNOMED", "relationship_direction": "inbound" } ] } ], "failed_concepts": [], "summary": { "total_requested": 3, "successful_retrievals": 3, "failed_retrievals": 0, "vocabularies_represented": ["SNOMED"], "domains_represented": ["Condition"] } }, "meta": { "request_id": "req_batch_123", "timestamp": "2024-12-22T10:00:00Z", "vocab_release": "2025.2", "query_time_ms": 45.2 } } ``` # Batch Hierarchy Queries Source: https://docs.omophub.com/api-reference/concepts/batch-hierarchy-queries POST https://api.omophub.com/v1/concepts/hierarchy/batch Run multiple OMOP concept hierarchy lookups in a single batched request to efficiently retrieve ancestors and descendants for bulk ETL workflows. ## Overview This endpoint allows you to perform multiple hierarchy queries (ancestors, descendants, or full hierarchy) for multiple concepts in a single API call. This is highly efficient for applications that need hierarchical information for many concepts simultaneously. ## Request Body Array of hierarchy query requests (1-100 queries) Unique identifier for this query (used to match results) The OMOP concept ID to query hierarchy for Type of hierarchy query. Options: `ancestors`, `descendants`, `hierarchy`, `level`, `relationships`, `related`, `traverse` Optional parameters for the query Maximum number of hierarchy levels to traverse Array of relationship types to follow (default: \["Is a"]) Limit results to specific vocabularies Limit results to specific domains Include relationships to invalid/deprecated concepts ## Query Parameters Specific vocabulary release version (e.g., "2025.1") ## Response Indicates whether the request was successful Response data containing batch results Array of query results matching the input order The query identifier from the request The concept ID that was queried The operation that was performed Whether this individual query succeeded Result data if the query succeeded (structure varies by operation) Array of ancestor concepts (for ancestors/hierarchy operations) OMOP concept ID Primary concept name Source vocabulary identifier Clinical domain classification Concept class identifier Standard concept flag (S, C, or null) Distance from the source concept in hierarchy Array of descendant concepts (for descendants/hierarchy operations) OMOP concept ID Primary concept name Source vocabulary identifier Clinical domain classification Concept class identifier Standard concept flag (S, C, or null) Distance from the source concept in hierarchy Total number of results Error message if the query failed Response metadata Unique request identifier ISO 8601 timestamp Vocabulary release version used ```bash cURL theme={null} curl -X POST "https://api.omophub.com/v1/concepts/hierarchy/batch" \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "queries": [ { "query_id": "q1", "concept_id": 201826, "operation": "ancestors", "params": { "max_levels": 5 } }, { "query_id": "q2", "concept_id": 4182210, "operation": "descendants", "params": { "max_levels": 3 } }, { "query_id": "q3", "concept_id": 313217, "operation": "hierarchy", "params": { "max_levels": 4, "relationship_types": ["Is a"] } } ] }' ``` ```python Python theme={null} import requests def batch_hierarchy_query(queries, api_key): url = "https://api.omophub.com/v1/concepts/hierarchy/batch" headers = { "Authorization": f"Bearer {api_key}", "Content-Type": "application/json" } data = {"queries": queries} response = requests.post(url, json=data, headers=headers) return response.json() # Example usage queries = [ { "query_id": "diabetes", "concept_id": 201826, "operation": "ancestors", "params": {"max_levels": 5} }, { "query_id": "hypertension", "concept_id": 4182210, "operation": "descendants", "params": {"max_levels": 3} }, { "query_id": "afib", "concept_id": 313217, "operation": "hierarchy", "params": {"max_levels": 4} } ] result = batch_hierarchy_query(queries, "YOUR_API_KEY") for query_result in result['data']['results']: if query_result['success']: data = query_result['data'] ancestors = len(data.get('ancestors', [])) descendants = len(data.get('descendants', [])) print(f"{query_result['query_id']}: {ancestors} ancestors, {descendants} descendants") else: print(f"{query_result['query_id']}: Error - {query_result['error']}") ``` ```javascript JavaScript theme={null} const batchHierarchyQuery = async (queries) => { const response = await fetch('https://api.omophub.com/v1/concepts/hierarchy/batch', { method: 'POST', headers: { 'Authorization': 'Bearer YOUR_API_KEY', 'Content-Type': 'application/json' }, body: JSON.stringify({ queries }) }); return response.json(); }; // Example usage const queries = [ { query_id: 'q1', concept_id: 201826, operation: 'ancestors', params: { max_levels: 5 } }, { query_id: 'q2', concept_id: 4182210, operation: 'descendants', params: { max_levels: 3 } }, { query_id: 'q3', concept_id: 313217, operation: 'hierarchy', params: { max_levels: 4 } } ]; const result = await batchHierarchyQuery(queries); result.data.results.forEach((queryResult) => { if (queryResult.success) { const ancestors = queryResult.data.ancestors?.length || 0; const descendants = queryResult.data.descendants?.length || 0; console.log(`${queryResult.query_id}: ${ancestors} ancestors, ${descendants} descendants`); } else { console.error(`${queryResult.query_id} failed: ${queryResult.error}`); } }); ``` ```json theme={null} { "success": true, "data": { "results": [ { "query_id": "q1", "concept_id": 201826, "operation": "ancestors", "success": true, "data": { "ancestors": [ { "concept_id": 73211009, "concept_name": "Diabetes mellitus", "vocabulary_id": "SNOMED", "domain_id": "Condition", "concept_class_id": "Clinical Finding", "standard_concept": "S", "level": 1 }, { "concept_id": 64572001, "concept_name": "Disease", "vocabulary_id": "SNOMED", "domain_id": "Condition", "concept_class_id": "Clinical Finding", "standard_concept": "S", "level": 2 } ], "total_count": 2 } }, { "query_id": "q2", "concept_id": 4182210, "operation": "descendants", "success": true, "data": { "descendants": [ { "concept_id": 320128, "concept_name": "Essential hypertension", "vocabulary_id": "SNOMED", "domain_id": "Condition", "concept_class_id": "Clinical Finding", "standard_concept": "S", "level": 1 } ], "total_count": 1 } }, { "query_id": "q3", "concept_id": 313217, "operation": "hierarchy", "success": true, "data": { "ancestors": [ { "concept_id": 49436004, "concept_name": "Atrial fibrillation and flutter", "vocabulary_id": "SNOMED", "domain_id": "Condition", "concept_class_id": "Clinical Finding", "standard_concept": "S", "level": 1 } ], "descendants": [], "total_count": 1 } } ] }, "meta": { "request_id": "req_batch_hierarchy_123", "timestamp": "2024-12-22T10:00:00Z", "vocab_release": "2025.1" } } ``` ## Usage Examples ### Simple Batch Ancestors Get ancestors for multiple concepts: ```json theme={null} { "queries": [ {"query_id": "q1", "concept_id": 201826, "operation": "ancestors"}, {"query_id": "q2", "concept_id": 4182210, "operation": "ancestors"}, {"query_id": "q3", "concept_id": 313217, "operation": "ancestors"} ] } ``` ### Mixed Operations Combine different operations in one batch: ```json theme={null} { "queries": [ {"query_id": "ancestors_query", "concept_id": 201826, "operation": "ancestors", "params": {"max_levels": 5}}, {"query_id": "descendants_query", "concept_id": 4182210, "operation": "descendants", "params": {"max_levels": 3}}, {"query_id": "full_hierarchy", "concept_id": 313217, "operation": "hierarchy", "params": {"max_levels": 4}} ] } ``` ### With Filtering Use vocabulary and relationship filters: ```json theme={null} { "queries": [ { "query_id": "filtered_query", "concept_id": 201826, "operation": "hierarchy", "params": { "max_levels": 5, "relationship_types": ["Is a"], "vocabulary_ids": ["SNOMED", "ICD10CM"] } } ] } ``` ## Important Notes * **Batch size limit**: Maximum 100 queries per batch request * **Performance optimization**: Batch processing is significantly faster than individual requests * **Error handling**: Individual query failures don't affect other queries in the batch * **Result matching**: Use `query_id` to match results to your input queries * **Rate limiting**: Batch requests count as single requests for rate limiting purposes ## Related Endpoints * [Get Concept Ancestors](/api-reference/concepts/get-concept-ancestors) - Single concept ancestors * [Get Concept Descendants](/api-reference/concepts/get-concept-descendants) - Single concept descendants * [Get Concept Hierarchy](/api-reference/concepts/get-concept-hierarchy) - Complete hierarchy tree # Batch Relationship Queries Source: https://docs.omophub.com/api-reference/concepts/batch-relationship-queries POST https://api.omophub.com/v1/concepts/relationships/batch Run multiple OMOP concept relationship queries in a single batched request for efficient bulk processing of mappings and vocabulary crosswalks. ## Overview This endpoint allows you to perform multiple relationship-related queries for multiple concepts in a single API call. It supports different operation types: `relationships`, `related`, and `traverse`. ## Request Body Array of relationship query requests (1-100 queries) Unique identifier for this query (used to match results) The OMOP concept ID to query relationships for Type of relationship query. Options: `relationships`, `related`, `traverse` Optional parameters for the query Array of relationship types to filter by (e.g., \["Maps to", "Is a"]) Array of vocabulary IDs to filter related concepts Include relationships to invalid/deprecated concepts ## Query Parameters Specific vocabulary release version (e.g., "2025.1") ## Response Indicates whether the overall batch request was successful Array of query results matching the input order The query identifier from the request The concept ID that was queried The operation that was performed Whether this individual query succeeded Result data if the query succeeded (structure varies by operation) Error message if the query failed Response metadata Unique request identifier ISO 8601 timestamp Vocabulary release version used ```bash cURL theme={null} curl -X POST "https://api.omophub.com/v1/concepts/relationships/batch" \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "queries": [ { "query_id": "diabetes_rels", "concept_id": 201826, "operation": "relationships", "params": { "relationship_types": ["Maps to", "Is a"] } }, { "query_id": "hypertension_rels", "concept_id": 4182210, "operation": "relationships", "params": { "relationship_types": ["Maps to"], "vocabulary_ids": ["ICD10CM", "ICD9CM"] } }, { "query_id": "afib_related", "concept_id": 313217, "operation": "related", "params": { "include_invalid": false } } ] }' ``` ```python Python theme={null} import requests def batch_relationship_query(queries, api_key): url = "https://api.omophub.com/v1/concepts/relationships/batch" headers = { "Authorization": f"Bearer {api_key}", "Content-Type": "application/json" } data = {"queries": queries} response = requests.post(url, json=data, headers=headers) return response.json() # Example: Batch query for cardiovascular conditions queries = [ { "query_id": "diabetes", "concept_id": 201826, "operation": "relationships", "params": { "relationship_types": ["Maps to", "Is a"] } }, { "query_id": "hypertension", "concept_id": 4182210, "operation": "relationships", "params": { "relationship_types": ["Maps to"], "vocabulary_ids": ["ICD10CM"] } }, { "query_id": "afib", "concept_id": 313217, "operation": "related", "params": {} } ] result = batch_relationship_query(queries, "YOUR_API_KEY") for query_result in result['data']: if query_result['success']: print(f"{query_result['query_id']}: Success") else: print(f"{query_result['query_id']}: Error - {query_result['error']}") ``` ```javascript JavaScript theme={null} const batchRelationshipQuery = async (queries) => { const response = await fetch('https://api.omophub.com/v1/concepts/relationships/batch', { method: 'POST', headers: { 'Authorization': 'Bearer YOUR_API_KEY', 'Content-Type': 'application/json' }, body: JSON.stringify({ queries }) }); return response.json(); }; // Example usage const queries = [ { query_id: 'q1', concept_id: 201826, operation: 'relationships', params: { relationship_types: ['Maps to', 'Is a'] } }, { query_id: 'q2', concept_id: 4182210, operation: 'relationships', params: { relationship_types: ['Maps to'], vocabulary_ids: ['ICD10CM'] } }, { query_id: 'q3', concept_id: 313217, operation: 'related', params: {} } ]; const result = await batchRelationshipQuery(queries); result.data.forEach((queryResult) => { if (queryResult.success) { console.log(`${queryResult.query_id}: Success`); } else { console.error(`${queryResult.query_id} failed: ${queryResult.error}`); } }); ``` ```json theme={null} { "success": true, "data": [ { "query_id": "diabetes_rels", "concept_id": 201826, "operation": "relationships", "success": true, "data": { "relationships": [ { "relationship_id": "Maps to", "concept_id_2": 44054006, "concept_name": "Diabetes mellitus type 2", "vocabulary_id": "SNOMED" }, { "relationship_id": "Is a", "concept_id_2": 73211009, "concept_name": "Diabetes mellitus", "vocabulary_id": "SNOMED" } ] } }, { "query_id": "hypertension_rels", "concept_id": 4182210, "operation": "relationships", "success": true, "data": { "relationships": [ { "relationship_id": "Maps to", "concept_id_2": 312327, "concept_name": "Essential hypertension", "vocabulary_id": "ICD10CM" } ] } }, { "query_id": "afib_related", "concept_id": 313217, "operation": "related", "success": true, "data": { "related_concepts": [ { "concept_id": 49436004, "concept_name": "Atrial fibrillation and flutter", "vocabulary_id": "SNOMED" } ] } } ], "meta": { "request_id": "req_batch_relationships_123", "timestamp": "2024-12-22T10:00:00Z", "vocab_release": "2025.1" } } ``` ## Usage Examples ### Basic Batch Query Get relationships for multiple concepts: ```json theme={null} { "queries": [ {"query_id": "q1", "concept_id": 201826, "operation": "relationships"}, {"query_id": "q2", "concept_id": 4182210, "operation": "relationships"}, {"query_id": "q3", "concept_id": 313217, "operation": "relationships"} ] } ``` ### Filtered by Relationship Type Get only mapping relationships: ```json theme={null} { "queries": [ { "query_id": "diabetes_maps", "concept_id": 201826, "operation": "relationships", "params": { "relationship_types": ["Maps to"] } }, { "query_id": "hypertension_maps", "concept_id": 4182210, "operation": "relationships", "params": { "relationship_types": ["Maps to"], "vocabulary_ids": ["ICD10CM"] } } ] } ``` ### Mixed Operations Combine different operations in one batch: ```json theme={null} { "queries": [ { "query_id": "rels_query", "concept_id": 201826, "operation": "relationships", "params": { "relationship_types": ["Is a", "Subsumes"] } }, { "query_id": "related_query", "concept_id": 4182210, "operation": "related", "params": {} }, { "query_id": "traverse_query", "concept_id": 313217, "operation": "traverse", "params": { "relationship_types": ["Is a"] } } ] } ``` ## Important Notes * **Batch size limit**: Maximum 100 queries per batch request * **Performance optimization**: Batch processing is significantly more efficient than individual requests * **Error isolation**: Individual query failures don't affect other queries in the batch * **Result matching**: Use `query_id` to match results to your input queries * **Rate limiting**: Batch requests count as single requests for rate limiting purposes ## Related Endpoints * [Get Concept Relationships](/api-reference/concepts/get-concept-relationships) - Single concept relationships * [Batch Hierarchy Queries](/api-reference/concepts/batch-hierarchy-queries) - Batch hierarchy queries * [Traverse Relationships](/api-reference/concepts/traverse-relationships) - Complex relationship traversals # Check Concept Relations Source: https://docs.omophub.com/api-reference/concepts/check-concept-relations GET https://api.omophub.com/v1/concepts/{conceptId}/relations/any Check whether an OMOP concept has any relationships without loading the full relationship payload - a lightweight existence check for ETL validation. ## Overview This lightweight endpoint quickly determines whether a concept has any relationships to other concepts. This is useful for UI components, validation logic, or filtering concepts that are isolated vs. connected in the terminology network. ## Path Parameters The unique OMOP concept ID to check for relationships ## Query Parameters Comma-separated list of relationship types to check (e.g., "Maps to", "Is a") Comma-separated list of vocabulary IDs to limit relationship check Include reverse relationships (concepts that relate to this concept) Include relationships to invalid/deprecated concepts Specific vocabulary release version (e.g., "2024.1") ## Response Indicates whether the request was successful Response data containing relationship check results The concept ID that was checked Whether this concept has any relationships matching the specified criteria Summary information about relationships (when has\_relations is true) Total number of relationships found Number of outbound relationships (from this concept) Number of inbound relationships (to this concept) Array of relationship types found (up to 10 most common) Array of vocabularies that have related concepts (up to 10) Whether concept has mappings to standard concepts Details about what was checked Relationship types included in the check Vocabularies included in the check Whether reverse relationships were checked Whether invalid relationships were checked Response metadata and API information Unique request identifier for debugging ISO 8601 timestamp of the response Vocabulary release version used ```bash cURL theme={null} curl -X GET "https://api.omophub.com/v1/concepts/201826/relations/any" \ -H "Authorization: Bearer YOUR_API_KEY" ``` ```python Python theme={null} import requests concept_id = 201826 # Type 2 diabetes url = f"https://api.omophub.com/v1/concepts/{concept_id}/relations/any" params = { "relationship_types": "Maps to,Is a", "include_reverse": True } headers = { "Authorization": "Bearer YOUR_API_KEY" } response = requests.get(url, params=params, headers=headers) data = response.json() if data['data']['has_relations']: print(f"✓ Concept {concept_id} has relationships") summary = data['data']['relation_summary'] print(f" - Total: {summary['total_count']}") print(f" - Standard mappings: {'Yes' if summary['has_standard_mappings'] else 'No'}") else: print(f"✗ Concept {concept_id} has no relationships") ``` ```javascript JavaScript theme={null} const conceptId = 201826; // Type 2 diabetes const response = await fetch(`https://api.omophub.com/v1/concepts/${conceptId}/relations/any`, { method: 'GET', headers: { 'Authorization': 'Bearer YOUR_API_KEY' } }); const relationCheck = await response.json(); const data = relationCheck.data; if (data.has_relations) { console.log(`Concept ${conceptId} has ${data.relation_summary.total_count} relationships`); console.log('Relationship types:', data.relation_summary.relationship_types); } else { console.log(`Concept ${conceptId} has no relationships`); } ``` ```bash cURL (filtered check) theme={null} curl -X GET "https://api.omophub.com/v1/concepts/201826/relations/any?relationship_types=Maps%20to&vocabulary_ids=ICD10CM,ICD9CM" \ -H "Authorization: Bearer YOUR_API_KEY" ``` ```python Python (batch validation) theme={null} import requests def validate_concept_connectivity(concept_ids): """ Check which concepts have standard mappings for data quality validation """ connected_concepts = [] isolated_concepts = [] for concept_id in concept_ids: url = f"https://api.omophub.com/v1/concepts/{concept_id}/relations/any" params = { "relationship_types": "Maps to", "vocabulary_ids": "SNOMED,ICD10CM,ICD9CM" } response = requests.get(url, params=params, headers={ "Authorization": "Bearer YOUR_API_KEY" }) data = response.json() concept_data = data['data'] if concept_data['has_relations'] and concept_data['relation_summary']['has_standard_mappings']: connected_concepts.append({ 'concept_id': concept_id, 'total_mappings': concept_data['relation_summary']['total_count'], 'vocabularies': concept_data['relation_summary']['connected_vocabularies'] }) else: isolated_concepts.append(concept_id) print(f"Connected concepts: {len(connected_concepts)}") print(f"Isolated concepts: {len(isolated_concepts)}") return connected_concepts, isolated_concepts # Validate a set of cardiovascular concepts cardiovascular_concepts = [201826, 4182210, 313217, 320128, 434557] connected, isolated = validate_concept_connectivity(cardiovascular_concepts) for concept in connected[:3]: # Show top 3 print(f"Concept {concept['concept_id']}: {concept['total_mappings']} mappings across {len(concept['vocabularies'])} vocabularies") ``` ```json theme={null} { "success": true, "data": { "concept_id": 201826, "has_relations": true, "relation_summary": { "total_count": 15, "outbound_count": 8, "inbound_count": 7, "relationship_types": ["Maps to", "Is a", "Has finding site", "May be a"], "connected_vocabularies": ["SNOMED", "ICD10CM", "ICD9CM"], "has_standard_mappings": true }, "checks_performed": { "included_relationship_types": ["all"], "included_vocabularies": ["all"], "included_reverse_relations": true, "included_invalid_relations": false } }, "meta": { "request_id": "req_check_relations_123", "timestamp": "2024-12-22T10:00:00Z", "vocab_release": "2025.2" } } ``` ## Usage Examples ### Simple Relationship Check Check if a concept has any relationships: ```bash theme={null} curl -X GET "https://api.omophub.com/v1/concepts/201826/relations/any" \ -H "Authorization: Bearer YOUR_API_KEY" ``` ### Check for Standard Mappings Check if concept has "Maps to" relationships: ```bash theme={null} curl -G "https://api.omophub.com/v1/concepts/201826/relations/any" \ -H "Authorization: Bearer YOUR_API_KEY" \ --data-urlencode "relationship_types=Maps to" ``` ### Check Cross-Vocabulary Relations Check relationships to specific vocabularies: ```bash theme={null} curl -X GET "https://api.omophub.com/v1/concepts/201826/relations/any?vocabulary_ids=ICD10CM,ICD9CM" \ -H "Authorization: Bearer YOUR_API_KEY" ``` ### Batch Checking in Applications Use in loops or batch processing to filter connected concepts: ```javascript theme={null} const conceptIds = [201826, 4182210, 313217]; const connectedConcepts = []; for (const id of conceptIds) { const response = await fetch(`/v1/concepts/${id}/relations/any`); const result = await response.json(); const data = result.data; if (data.has_relations && data.relation_summary.has_standard_mappings) { connectedConcepts.push({ concept_id: id, relation_count: data.relation_summary.total_count }); } } ``` ## Important Notes * **Performance optimized** - Much faster than fetching full relationship data * **Filtering support** - All the same filters as the full relations endpoint * **UI integration** - Perfect for showing relationship icons or badges * **Batch processing** - Ideal for filtering large concept lists * **Standard mappings** - The `has_standard_mappings` flag is useful for data quality checks ## Related Endpoints * [Get Concept Relationships](/api-reference/concepts/get-concept-relationships) - Get full relationship data * [Batch Hierarchy Queries](/api-reference/concepts/batch-hierarchy-queries) - Check relationships for multiple concepts # Get Concept Source: https://docs.omophub.com/api-reference/concepts/get-concept GET /concepts/{conceptId} ## Overview Retrieve comprehensive information about a medical concept using its unique concept ID. This endpoint provides full concept details including synonyms, relationships, and classification information. ## Path Parameters The unique concept identifier (e.g., 320128 for "Essential hypertension") ## Query Parameters Specific vocabulary release to use (e.g., "2025.2"). Uses latest if not specified. Include related concepts (parents, children, mappings) Include all synonyms and alternative names Include ancestor and descendant concepts in hierarchy Include concepts with invalid\_reason set (deprecated/updated concepts). Set to false to only return valid concepts. ```bash cURL theme={null} curl -X GET "https://api.omophub.com/v1/concepts/320128" \ -H "Authorization: Bearer YOUR_API_KEY" ``` ```python Python theme={null} import requests response = requests.get( "https://api.omophub.com/v1/concepts/320128", headers={"Authorization": "Bearer YOUR_API_KEY"} ) concept = response.json() ``` ```javascript JavaScript theme={null} const response = await fetch('https://api.omophub.com/v1/concepts/320128', { headers: { 'Authorization': 'Bearer YOUR_API_KEY' } }); const concept = await response.json(); ``` ```bash cURL (with details) theme={null} curl -X GET "https://api.omophub.com/v1/concepts/320128?include_relationships=true&include_synonyms=true" \ -H "Authorization: Bearer YOUR_API_KEY" ``` ```python Python (with details) theme={null} import requests params = { "include_relationships": True, "include_synonyms": True } response = requests.get( "https://api.omophub.com/v1/concepts/320128", headers={"Authorization": "Bearer YOUR_API_KEY"}, params=params ) concept = response.json() ``` ```json theme={null} { "success": true, "data": { "concept_id": 320128, "concept_name": "Essential hypertension", "concept_code": "59621000", "vocabulary_id": "SNOMED", "domain_id": "Condition", "concept_class_id": "Clinical Finding", "standard_concept": "S", "valid_start_date": "1970-01-01", "valid_end_date": "2099-12-31", "invalid_reason": null, "is_valid": true, "is_standard": true, "is_classification": false, "synonyms": [ "Primary hypertension", "Idiopathic hypertension", "High blood pressure" ], "relationships": { "parents": [ { "concept_id": 316866, "concept_name": "Hypertensive disorder", "relationship_id": "Is a" } ], "children": [ { "concept_id": 4124681, "concept_name": "Accelerated hypertension", "relationship_id": "Subsumes" } ] } }, "meta": { "request_id": "req_concept_123", "timestamp": "2024-12-22T10:00:00Z", "vocab_release": "2025.2" } } ``` ## Common Use Cases ### 1. Clinical Decision Support Get concept details for clinical coding and documentation systems. ### 2. Data Validation Verify concept information during data quality checks. ### 3. User Interface Display Show detailed concept information in healthcare applications. ### 4. Relationship Analysis Understand concept hierarchies and mappings for analysis. ## Related Endpoints * [Get Concept by Code](/api-reference/concepts/get-concept-by-code) - Look up concept using vocabulary-specific code * [Get Concept Relationships](/api-reference/concepts/get-concept-relationships) - Get all relationships for a concept * [Search Concepts](/api-reference/search/basic-search) - Search for concepts by name or description # Get Concept by Code Source: https://docs.omophub.com/api-reference/concepts/get-concept-by-code GET /concepts/by-code/{vocabulary_id}/{concept_code} ## Overview Retrieve concept information using the vocabulary-specific code instead of the universal concept ID. This is useful when working with source data that contains original codes. ## Path Parameters The vocabulary identifier (e.g., "SNOMED", "ICD10CM", "LOINC") The vocabulary-specific code (e.g., "59621000" for SNOMED, "I10" for ICD10CM) ```bash cURL theme={null} curl -X GET "https://api.omophub.com/v1/concepts/by-code/SNOMED/59621000" \ -H "Authorization: Bearer YOUR_API_KEY" ``` ```python Python theme={null} import requests response = requests.get( "https://api.omophub.com/v1/concepts/by-code/SNOMED/59621000", headers={"Authorization": "Bearer YOUR_API_KEY"} ) concept = response.json() ``` ```javascript JavaScript theme={null} const response = await fetch('https://api.omophub.com/v1/concepts/by-code/SNOMED/59621000', { headers: { 'Authorization': 'Bearer YOUR_API_KEY' } }); const concept = await response.json(); ``` ```bash cURL (ICD10CM) theme={null} curl -X GET "https://api.omophub.com/v1/concepts/by-code/ICD10CM/I10" \ -H "Authorization: Bearer YOUR_API_KEY" ``` ```python Python (ICD10CM) theme={null} import requests response = requests.get( "https://api.omophub.com/v1/concepts/by-code/ICD10CM/I10", headers={"Authorization": "Bearer YOUR_API_KEY"} ) icd_concept = response.json() ``` ```json Response theme={null} { "success": true, "data": { "concept_id": 320128, "concept_name": "Essential hypertension", "concept_code": "59621000", "vocabulary_id": "SNOMED", "domain_id": "Condition", "concept_class_id": "Clinical Finding", "standard_concept": "S", "valid_start_date": "1970-01-01T00:00:00.000Z", "valid_end_date": "2099-12-31T00:00:00.000Z", "invalid_reason": null, "is_valid": true, "is_standard": true, "is_classification": false }, "meta": { "request_id": "req_concept_code_123", "timestamp": "2024-12-22T10:00:00Z", "vocab_release": "2025.2" } } ``` ```json Response (with details) theme={null} { "success": true, "data": { "concept_id": 320128, "concept_name": "Essential hypertension", "concept_code": "59621000", "vocabulary_id": "SNOMED", "domain_id": "Condition", "concept_class_id": "Clinical Finding", "standard_concept": "S", "valid_start_date": "1970-01-01T00:00:00.000Z", "valid_end_date": "2099-12-31T00:00:00.000Z", "invalid_reason": null, "is_valid": true, "is_standard": true, "is_classification": false, "synonyms": [ "Primary hypertension", "Idiopathic hypertension", "High blood pressure" ], "relationships": { "parents": [ { "concept_id": 316866, "concept_name": "Hypertensive disorder", "relationship_id": "Is a" } ], "children": [ { "concept_id": 4124681, "concept_name": "Accelerated hypertension", "relationship_id": "Subsumes" } ] } }, "meta": { "request_id": "req_concept_code_123", "timestamp": "2024-12-22T10:00:00Z", "vocab_release": "2025.2" } } ``` ## Response Fields | Field | Type | Description | | ------------------- | ------- | ----------------------------------------------------------------------- | | `concept_id` | integer | Unique concept identifier | | `concept_name` | string | Human-readable concept name | | `concept_code` | string | Vocabulary-specific code | | `vocabulary_id` | string | Source vocabulary identifier | | `domain_id` | string | Clinical domain (Condition, Drug, etc.) | | `concept_class_id` | string | Concept classification within vocabulary | | `standard_concept` | string | "S" (Standard), "C" (Classification), or null | | `valid_start_date` | string | ISO 8601 timestamp when concept became valid | | `valid_end_date` | string | ISO 8601 timestamp when concept expires | | `invalid_reason` | string | Reason for deprecation, or null if valid | | `is_valid` | boolean | Whether concept is currently valid (not expired) | | `is_standard` | boolean | Whether concept is a standard concept | | `is_classification` | boolean | Whether concept is a classification concept | | `synonyms` | array | List of synonym names (only when `include_synonyms=true`) | | `relationships` | object | Parent and child relationships (only when `include_relationships=true`) | ## Query Parameters Specific vocabulary release to use (e.g., "2025.2"). Uses latest if not specified. Include concept synonyms in the response Include concept relationships and mappings Include concept hierarchy information Include concepts with invalid\_reason set (deprecated/updated concepts). Set to false to only return valid concepts. ## Common Use Cases ### 1. Source Data Processing Convert source system codes to standardized concepts during ETL processes. ### 2. Legacy System Integration Map existing codes from legacy healthcare systems. ### 3. Claims Processing Look up diagnosis and procedure codes from insurance claims. ### 4. Clinical Documentation Validate codes entered by healthcare providers. # Get Concept Level Source: https://docs.omophub.com/api-reference/concepts/get-concept-level GET https://api.omophub.com/v1/concepts/{conceptId}/level Get the hierarchical level and depth of an OMOP concept within its vocabulary tree for quick classification and hierarchy-aware filtering. ## Overview This endpoint returns basic hierarchical positioning information for a concept, including its depth from root concepts and counts of ancestors and descendants. ## Path Parameters The unique OMOP concept ID to get level information for ## Query Parameters Specific vocabulary release version (e.g., "2025.1") ## Response Indicates whether the request was successful Response data containing concept level information The concept ID that was queried The level of the concept in the hierarchy (0-based) Number of levels from the root concept to this concept Number of concepts at the same level (may be null if not calculated) Total number of descendant concepts below this concept Total number of ancestor concepts above this concept Response metadata and API information Unique request identifier for debugging ISO 8601 timestamp of the response Vocabulary release version used ```bash cURL theme={null} curl -X GET "https://api.omophub.com/v1/concepts/201826/level" \ -H "Authorization: Bearer YOUR_API_KEY" ``` ```python Python theme={null} import requests concept_id = 201826 # Type 2 diabetes url = f"https://api.omophub.com/v1/concepts/{concept_id}/level" headers = { "Authorization": "Bearer YOUR_API_KEY" } response = requests.get(url, headers=headers) data = response.json() level_info = data['data'] print(f"Concept ID: {level_info['concept_id']}") print(f"Depth from root: {level_info['depth_from_root']}") print(f"Total ancestors: {level_info['total_ancestors']}") print(f"Total descendants: {level_info['total_descendants']}") ``` ```javascript JavaScript theme={null} const conceptId = 201826; // Type 2 diabetes const response = await fetch(`https://api.omophub.com/v1/concepts/${conceptId}/level`, { method: 'GET', headers: { 'Authorization': 'Bearer YOUR_API_KEY' } }); const levelInfo = await response.json(); const data = levelInfo.data; console.log(`Concept ID: ${data.concept_id}`); console.log(`Depth from root: ${data.depth_from_root}`); console.log(`Total ancestors: ${data.total_ancestors}`); console.log(`Total descendants: ${data.total_descendants}`); ``` ```json theme={null} { "success": true, "data": { "concept_id": 201826, "level": 0, "depth_from_root": 4, "breadth_at_level": null, "total_descendants": 15, "total_ancestors": 3 }, "meta": { "request_id": "req_concept_level_123", "timestamp": "2024-12-22T10:00:00Z", "vocab_release": "2025.1" } } ``` ## Usage Examples ### Basic Level Information Get hierarchy level for a concept: ```bash theme={null} curl -X GET "https://api.omophub.com/v1/concepts/201826/level" \ -H "Authorization: Bearer YOUR_API_KEY" ``` ### With Specific Vocabulary Version Get level information for a specific vocabulary release: ```bash theme={null} curl -X GET "https://api.omophub.com/v1/concepts/201826/level?vocab_release=2025.1" \ -H "Authorization: Bearer YOUR_API_KEY" ``` ## Important Notes * **Depth interpretation** - `depth_from_root` indicates how many "Is a" relationship steps exist between this concept and the root * **Descendant count** - `total_descendants` includes all child concepts at any depth below * **Ancestor count** - `total_ancestors` includes all parent concepts up to the root ## Related Endpoints * [Get Concept Hierarchy](/api-reference/concepts/get-concept-hierarchy) - Full hierarchical tree * [Get Concept Ancestors](/api-reference/concepts/get-concept-ancestors) - Parent concepts * [Get Concept Descendants](/api-reference/concepts/get-concept-descendants) - Child concepts # Get Concept Relationships Source: https://docs.omophub.com/api-reference/concepts/get-concept-relationships GET https://api.omophub.com/v1/concepts/{conceptId}/relationships Retrieve all OMOP concept relationships - including "Maps to", "Is a", and vocabulary-specific links - to traverse concept networks in clinical data. ## Overview Retrieve all relationships for a concept, including hierarchical relationships (parents/children), mappings to other vocabularies, and semantic relationships. ## Path Parameters The unique concept identifier ## Query Parameters Filter by relationship type IDs (comma-separated)
**Examples**: `Is a`, `Subsumes`, `Maps to`, `Is a,Subsumes`
Filter relationships to specific target vocabularies (comma-separated)
**Examples**: `SNOMED`, `ICD10CM`, `SNOMED,ICD10CM`
Filter relationships to specific target domains (comma-separated)
**Examples**: `Condition`, `Drug`, `Condition,Procedure`
Include relationships to invalid concepts Only include relationships to standard concepts Include reverse relationships (where this concept is the target) Specific vocabulary release version (defaults to latest) ```bash cURL theme={null} curl -X GET "https://api.omophub.com/v1/concepts/320128/relationships" \ -H "Authorization: Bearer YOUR_API_KEY" ``` ```python Python theme={null} import requests response = requests.get( "https://api.omophub.com/v1/concepts/320128/relationships", headers={"Authorization": "Bearer YOUR_API_KEY"} ) relationships = response.json() ``` ```javascript JavaScript theme={null} const response = await fetch('https://api.omophub.com/v1/concepts/320128/relationships', { headers: { 'Authorization': 'Bearer YOUR_API_KEY' } }); const relationships = await response.json(); ``` ```bash cURL (filtered) theme={null} curl -X GET "https://api.omophub.com/v1/concepts/320128/relationships?relationship_ids=Is%20a&vocabulary_ids=SNOMED" \ -H "Authorization: Bearer YOUR_API_KEY" ``` ```python Python (filtered) theme={null} import requests params = { "relationship_ids": "Is a", "vocabulary_ids": "SNOMED", "standard_only": True } response = requests.get( "https://api.omophub.com/v1/concepts/320128/relationships", headers={"Authorization": "Bearer YOUR_API_KEY"}, params=params ) filtered_relationships = response.json() ``` ```json theme={null} { "success": true, "data": { "relationships": [ { "relationship_id": "Is a", "concept_id_1": 320128, "concept_id_2": 316866, "concept_name": "Hypertensive disorder", "concept_code": "38341003", "vocabulary_id": "SNOMED", "domain_id": "Condition", "standard_concept": "S", "valid_start_date": "1970-01-01", "valid_end_date": "2099-12-31" }, { "relationship_id": "Maps to", "concept_id_1": 320128, "concept_id_2": 435216, "concept_name": "Essential hypertension", "concept_code": "I10", "vocabulary_id": "ICD10CM", "domain_id": "Condition", "standard_concept": "S", "valid_start_date": "1970-01-01", "valid_end_date": "2099-12-31" }, { "relationship_id": "Has finding site", "concept_id_1": 320128, "concept_id_2": 4103720, "concept_name": "Cardiovascular system", "concept_code": "113257007", "vocabulary_id": "SNOMED", "domain_id": "Spec Anatomic Site", "standard_concept": "S", "valid_start_date": "1970-01-01", "valid_end_date": "2099-12-31" } ] }, "meta": { "request_id": "req_relationships_123", "timestamp": "2024-12-22T10:00:00Z", "vocab_release": "2025.2", "version": "2025.2", "duration": 45 } } ``` ## Response Fields Indicates if the request was successful Array of relationship objects Type of relationship (e.g., "Is a", "Maps to", "Subsumes") Source concept ID Target concept ID Target concept name Target concept code Target vocabulary ID Target domain ID Standard concept flag (S=Standard, C=Classification, null=Non-standard) Relationship validity start date Relationship validity end date Unique identifier for the request Request timestamp (ISO 8601 UTC format) Vocabulary release version used Schema version used for the query Request duration in milliseconds ## Usage Examples ### Get All Relationships Retrieve all relationships for a concept: ```bash theme={null} curl -X GET "https://api.omophub.com/v1/concepts/320128/relationships" \ -H "Authorization: Bearer YOUR_API_KEY" ``` ### Filter by Relationship Type Get only hierarchical relationships: ```bash theme={null} curl -X GET "https://api.omophub.com/v1/concepts/320128/relationships?relationship_ids=Is%20a,Subsumes" \ -H "Authorization: Bearer YOUR_API_KEY" ``` ### Filter by Target Vocabulary Get mappings to specific vocabularies: ```bash theme={null} curl -X GET "https://api.omophub.com/v1/concepts/320128/relationships?vocabulary_ids=ICD10CM,ICD9CM" \ -H "Authorization: Bearer YOUR_API_KEY" ``` ### Standard Concepts Only Get relationships to standard concepts only: ```bash theme={null} curl -X GET "https://api.omophub.com/v1/concepts/320128/relationships?standard_only=true" \ -H "Authorization: Bearer YOUR_API_KEY" ``` ### Include Reverse Relationships Include relationships where this concept is the target: ```bash theme={null} curl -X GET "https://api.omophub.com/v1/concepts/320128/relationships?include_reverse=true" \ -H "Authorization: Bearer YOUR_API_KEY" ``` ## Common Relationship Types | Relationship | Description | | --------------------------- | -------------------------------- | | `Is a` | Hierarchical parent relationship | | `Subsumes` | Hierarchical child relationship | | `Maps to` | Mapping to another vocabulary | | `Mapped from` | Reverse mapping relationship | | `Has finding site` | Anatomical location | | `Has associated morphology` | Associated morphological finding | ## Related Endpoints * [Get Concept](/api-reference/concepts/get-concept) - Get detailed concept information * [Get Relationship Options](/api-reference/concepts/get-relationship-options) - Get available relationship types for a concept * [Traverse Relationships](/api-reference/concepts/traverse-relationships) - Complex relationship traversal # Get Trending Concepts Source: https://docs.omophub.com/api-reference/concepts/get-concept-trending GET https://api.omophub.com/v1/concepts/trending Retrieve trending OMOP concepts ranked by search frequency and usage patterns to surface popular medical terminology across OMOPHub clients. ## Overview This endpoint provides insights into trending concepts by analyzing search frequency and usage patterns. It identifies concepts that are seeing increased activity over different time periods. ## Query Parameters Time period for trend analysis
**Options**: `day`, `week`, `month`
Filter trends to specific vocabularies (comma-separated)
**Examples**: `SNOMED`, `SNOMED,ICD10CM`, `RXNORM,LOINC`
Maximum number of trending concepts to return (1-1000) Specific vocabulary release version (defaults to latest) ## Response Indicates if the request was successful Time period analyzed (`day`, `week`, or `month`) Array of trending concepts Unique concept identifier Primary concept name Source vocabulary identifier Number of searches for this concept in the period Percentage growth rate compared to the previous period Calculated trend score (higher = more trending). Weighted combination of search volume (60%) and growth rate (40%). Total number of trending concepts found Maximum results returned (from page\_size parameter) Unique identifier for the request Request timestamp (ISO 8601 UTC format) Vocabulary release version used ```bash cURL theme={null} curl -X GET "https://api.omophub.com/v1/concepts/trending?period=week&vocabulary_ids=SNOMED,ICD10CM&page_size=20" \ -H "Authorization: Bearer YOUR_API_KEY" ``` ```javascript JavaScript theme={null} const response = await fetch('https://api.omophub.com/v1/concepts/trending?period=week&vocabulary_ids=SNOMED&page_size=50', { method: 'GET', headers: { 'Authorization': 'Bearer YOUR_API_KEY' } }); const data = await response.json(); console.log(`Found ${data.data.total_concepts} trending concepts`); ``` ```python Python theme={null} import requests headers = { 'Authorization': 'Bearer YOUR_API_KEY' } params = { 'period': 'month', 'vocabulary_ids': 'SNOMED,ICD10CM', 'page_size': 50 } response = requests.get( 'https://api.omophub.com/v1/concepts/trending', headers=headers, params=params ) data = response.json() for concept in data['data']['concepts']: print(f"{concept['concept_name']}: score {concept['trend_score']}") ``` ```json Response theme={null} { "success": true, "data": { "period": "week", "concepts": [ { "concept_id": 201826, "concept_name": "Type 2 diabetes mellitus", "vocabulary_id": "SNOMED", "search_count": 1547, "growth_rate": 23.5, "trend_score": 7.8 }, { "concept_id": 320128, "concept_name": "Essential hypertension", "vocabulary_id": "SNOMED", "search_count": 1234, "growth_rate": 18.2, "trend_score": 6.9 }, { "concept_id": 4329847, "concept_name": "Myocardial infarction", "vocabulary_id": "SNOMED", "search_count": 987, "growth_rate": 15.7, "trend_score": 5.4 } ], "total_concepts": 156, "limit": 20 }, "meta": { "request_id": "req_trending_abc123", "timestamp": "2024-01-15T10:30:00Z", "vocab_release": "2025.2" } } ``` ## Usage Examples ### Weekly Trending Concepts Get trending concepts from the past week: ```bash theme={null} curl -X GET "https://api.omophub.com/v1/concepts/trending?period=week&page_size=50" \ -H "Authorization: Bearer YOUR_API_KEY" ``` ### Vocabulary-Specific Trends Focus on trends within specific vocabularies: ```bash theme={null} curl -X GET "https://api.omophub.com/v1/concepts/trending?vocabulary_ids=SNOMED&period=month" \ -H "Authorization: Bearer YOUR_API_KEY" ``` ### Daily Trending Get the most recent daily trends: ```bash theme={null} curl -X GET "https://api.omophub.com/v1/concepts/trending?period=day&page_size=100" \ -H "Authorization: Bearer YOUR_API_KEY" ``` ## Trend Score Calculation The `trend_score` is a composite metric calculated from: * **Search volume (60% weight)**: Number of searches for the concept in the current period * **Growth rate (40% weight)**: Percentage change compared to the previous period of equal length Higher scores indicate concepts with both high search volume and significant growth. ## Related Endpoints * [Search Concepts](/api-reference/search/basic-search) - Search for specific concepts * [Get Concept](/api-reference/concepts/get-concept) - Detailed concept information * [Suggest Concepts](/api-reference/concepts/suggest-concepts) - Concept suggestions # Get Related Concepts Source: https://docs.omophub.com/api-reference/concepts/get-related-concepts GET https://api.omophub.com/v1/concepts/{conceptId}/related Find OMOP concepts related through relationship analysis - ingredients, routes, brand names, and hierarchical links - for concept set expansion. ## Overview This endpoint finds concepts that are related to a given concept through various relationship types. Results are scored and ordered by relationship strength. ## Path Parameters The unique OMOP concept ID to find related concepts for ## Query Parameters Maximum number of related concepts to return (1-100) Minimum relationship score (0.0-1.0) to include in results Comma-separated list of relationship types to filter by (e.g., "Is a,Maps to") Specific vocabulary release version (e.g., "2025.1") ## Response Indicates if the request was successful Array of related concepts ordered by relationship score Related concept ID Related concept name Related concept code Related concept vocabulary ID Related concept vocabulary name Related concept domain Related concept class Standard concept flag ("S" for standard, "C" for classification, null for non-standard) Relationship type ID (e.g., "Is a", "Maps to") Human-readable relationship name Relationship strength score (0.0-1.0) Number of relationship steps from source concept Response metadata Unique request identifier ISO 8601 timestamp Vocabulary release version used ```bash cURL theme={null} curl -X GET "https://api.omophub.com/v1/concepts/201826/related?min_score=0.4&page_size=20" \ -H "Authorization: Bearer YOUR_API_KEY" ``` ```python Python theme={null} import requests concept_id = 201826 # Type 2 diabetes url = f"https://api.omophub.com/v1/concepts/{concept_id}/related" params = { "min_score": 0.4, "page_size": 30, "relationship_types": "Is a,Maps to" } headers = { "Authorization": "Bearer YOUR_API_KEY" } response = requests.get(url, params=params, headers=headers) data = response.json() print(f"Found {len(data['data'])} related concepts") for related in data['data'][:5]: print(f" - {related['concept_name']} ({related['vocabulary_id']})") print(f" Relationship: {related['relationship_name']}") print(f" Score: {related['relationship_score']:.2f}") ``` ```javascript JavaScript theme={null} const conceptId = 201826; // Type 2 diabetes const response = await fetch(`https://api.omophub.com/v1/concepts/${conceptId}/related?min_score=0.3&page_size=20`, { headers: { 'Authorization': 'Bearer YOUR_API_KEY' } }); const result = await response.json(); console.log(`Found ${result.data.length} related concepts`); result.data.slice(0, 5).forEach((concept, index) => { console.log(`${index + 1}. ${concept.concept_name} (${concept.vocabulary_id})`); console.log(` Relationship: ${concept.relationship_name}`); console.log(` Score: ${concept.relationship_score.toFixed(2)}`); }); ``` ```json theme={null} { "success": true, "data": [ { "concept_id": 73211009, "concept_name": "Diabetes mellitus", "concept_code": "73211009", "vocabulary_id": "SNOMED", "vocabulary_name": "SNOMED CT", "domain_id": "Condition", "concept_class_id": "Clinical Finding", "standard_concept": "S", "relationship_id": "Is a", "relationship_name": "Is a", "relationship_score": 0.95, "relationship_distance": 1 }, { "concept_id": 443735, "concept_name": "Type 2 diabetes mellitus without complications", "concept_code": "443735", "vocabulary_id": "SNOMED", "vocabulary_name": "SNOMED CT", "domain_id": "Condition", "concept_class_id": "Clinical Finding", "standard_concept": "S", "relationship_id": "Subsumes", "relationship_name": "Subsumes", "relationship_score": 0.87, "relationship_distance": 1 }, { "concept_id": 435216, "concept_name": "Type 2 diabetes mellitus", "concept_code": "E11", "vocabulary_id": "ICD10CM", "vocabulary_name": "ICD10CM", "domain_id": "Condition", "concept_class_id": "ICD10 code", "standard_concept": null, "relationship_id": "Maps to", "relationship_name": "Maps to", "relationship_score": 0.92, "relationship_distance": 1 } ], "meta": { "request_id": "req_related_123", "timestamp": "2024-12-22T10:00:00Z", "vocab_release": "2025.1" } } ``` ## Usage Examples ### Basic Related Concepts Find general related concepts: ```bash theme={null} curl -X GET "https://api.omophub.com/v1/concepts/201826/related" \ -H "Authorization: Bearer YOUR_API_KEY" ``` ### High-Score Relationships Get only strongly related concepts: ```bash theme={null} curl -X GET "https://api.omophub.com/v1/concepts/201826/related?min_score=0.7&page_size=10" \ -H "Authorization: Bearer YOUR_API_KEY" ``` ### Filtered by Relationship Type Find specific relationship types: ```bash theme={null} curl -X GET "https://api.omophub.com/v1/concepts/201826/related?relationship_types=Is%20a,Maps%20to" \ -H "Authorization: Bearer YOUR_API_KEY" ``` ## Important Notes * **Score interpretation** - Scores above 0.7 indicate strong relationships, 0.4-0.7 moderate, below 0.4 weak * **Distance interpretation** - Distance of 1 means direct relationship, higher values indicate indirect connections * **Performance** - Results are cached for improved response times on repeated queries ## Related Endpoints * [Get Concept Relationships](/api-reference/concepts/get-concept-relationships) - Get all direct relationships * [Traverse Relationships](/api-reference/concepts/traverse-relationships) - Complex relationship traversals # Get Relationship Options Source: https://docs.omophub.com/api-reference/concepts/get-relationship-options GET https://api.omophub.com/v1/concepts/{conceptId}/relationships/options Get the list of available relationship types for a specific OMOP concept so you can filter, paginate, or traverse only the links you care about. ## Overview This endpoint returns all available relationship types that exist for a specific concept. This is useful for building dynamic UI components like dropdowns and understanding what relationships are available for a concept. ## Path Parameters The unique OMOP concept ID to get relationship options for ## Query Parameters Specific vocabulary release version (e.g., "2025.1") ## Response Indicates whether the request was successful Response data containing relationship options The concept ID that was queried Array of available relationship types for this concept Unique relationship type ID (e.g., "Is a", "Maps to") Human-readable relationship name Response metadata Unique request identifier ISO 8601 timestamp Vocabulary release version used ```bash cURL theme={null} curl -X GET "https://api.omophub.com/v1/concepts/201826/relationships/options" \ -H "Authorization: Bearer YOUR_API_KEY" ``` ```python Python theme={null} import requests concept_id = 201826 # Type 2 diabetes url = f"https://api.omophub.com/v1/concepts/{concept_id}/relationships/options" headers = { "Authorization": "Bearer YOUR_API_KEY" } response = requests.get(url, headers=headers) data = response.json() print(f"Concept ID: {data['data']['concept_id']}") print("Available relationship types:") for rel in data['data']['available_relationships']: print(f" - {rel['name']} ({rel['id']})") ``` ```javascript JavaScript theme={null} const conceptId = 201826; // Type 2 diabetes const response = await fetch(`https://api.omophub.com/v1/concepts/${conceptId}/relationships/options`, { method: 'GET', headers: { 'Authorization': 'Bearer YOUR_API_KEY' } }); const result = await response.json(); const data = result.data; console.log(`Concept ID: ${data.concept_id}`); console.log('Available relationships:'); data.available_relationships.forEach(rel => { console.log(` - ${rel.name} (${rel.id})`); }); ``` ```json theme={null} { "success": true, "data": { "concept_id": 201826, "available_relationships": [ { "id": "Is a", "name": "Is a" }, { "id": "Maps to", "name": "Maps to" }, { "id": "Mapped from", "name": "Mapped from" }, { "id": "Subsumes", "name": "Subsumes" } ] }, "meta": { "request_id": "req_relationship_options_123", "timestamp": "2024-12-22T10:00:00Z", "vocab_release": "2025.1" } } ``` ## Usage Examples ### Basic Relationship Options Get all available relationship types for a concept: ```bash theme={null} curl -X GET "https://api.omophub.com/v1/concepts/201826/relationships/options" \ -H "Authorization: Bearer YOUR_API_KEY" ``` ### With Specific Vocabulary Version Get relationship options for a specific vocabulary release: ```bash theme={null} curl -X GET "https://api.omophub.com/v1/concepts/201826/relationships/options?vocab_release=2025.1" \ -H "Authorization: Bearer YOUR_API_KEY" ``` ### UI Dropdown Integration Use in a dropdown component: ```javascript theme={null} async function loadRelationshipOptions(conceptId) { const response = await fetch(`/v1/concepts/${conceptId}/relationships/options`, { headers: { 'Authorization': 'Bearer YOUR_API_KEY' } }); const result = await response.json(); return result.data.available_relationships.map(rel => ({ value: rel.id, label: rel.name })); } // Use in UI const options = await loadRelationshipOptions(201826); const selectHtml = options.map(opt => `` ).join(''); ``` ## Important Notes * **Dynamic options** - Available relationship types vary by concept * **Both directions** - Returns relationships where the concept is either source or target * **Valid only** - Only returns relationships with valid (non-deprecated) status ## Related Endpoints * [Get Concept Relationships](/api-reference/concepts/get-concept-relationships) - Get actual relationships for a concept * [Get Relationship Types](/api-reference/relationships/get-relationship-types) - Get all relationship types in the system # Recommended Concepts Source: https://docs.omophub.com/api-reference/concepts/recommended-concepts POST /concepts/recommended ## Overview The Recommended Concepts endpoint implements the OHDSI Phoebe algorithm to provide curated concept recommendations. This feature helps researchers discover related concepts during cohort building and phenotype development. **Key Features:** * Curated recommendations from OHDSI community data * Relationship-based concept discovery * Support for multiple source concepts * Filtering by vocabulary, domain, and relationship type **Use Cases:** * Cohort building assistance * Phenotype expansion * Related concept discovery * Clinical terminology exploration ## Request Body Array of source concept IDs to get recommendations for (min: 1, max: 100) Filter recommendations by relationship types (max: 20). Examples: "Has finding", "Associated finding", "Has clinical course" Filter recommended concepts to specific vocabularies (max: 50). Examples: \["SNOMED", "ICD10CM", "LOINC"] Filter recommended concepts to specific domains (max: 50). Examples: \["Condition", "Procedure", "Drug"] Only return standard concepts (S flag in OMOP CDM) Include invalid/deprecated concepts in results Page number for pagination (min: 1) Number of recommendations to return per page (min: 1, max: 1000) ```bash cURL theme={null} curl -X POST "https://api.omophub.com/v1/concepts/recommended" \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "concept_ids": [201826, 4329847], "standard_only": true, "page": 1, "page_size": 20 }' ``` ```python Python theme={null} import requests payload = { "concept_ids": [201826, 4329847], # Type 2 diabetes, Myocardial infarction "relationship_types": ["Has finding", "Associated finding"], "vocabulary_ids": ["SNOMED"], "standard_only": True, "page": 1, "page_size": 20 } response = requests.post( "https://api.omophub.com/v1/concepts/recommended", headers={"Authorization": "Bearer YOUR_API_KEY"}, json=payload ) data = response.json() for source_id, recommendations in data['data'].items(): print(f"\nRecommendations for concept {source_id}:") for rec in recommendations[:5]: print(f" - {rec['concept_name']} ({rec['vocabulary_id']}) via {rec['relationship_id']}") ``` ```javascript JavaScript theme={null} const response = await fetch('https://api.omophub.com/v1/concepts/recommended', { method: 'POST', headers: { 'Authorization': 'Bearer YOUR_API_KEY', 'Content-Type': 'application/json' }, body: JSON.stringify({ concept_ids: [201826, 4329847], relationship_types: ['Has finding', 'Associated finding'], vocabulary_ids: ['SNOMED'], standard_only: true, page: 1, page_size: 20 }) }); const data = await response.json(); console.log(`Page ${data.meta.pagination.page} of ${data.meta.pagination.total_pages}`); console.log(`Total items: ${data.meta.pagination.total_items}`); ``` ```bash cURL (with filtering) theme={null} curl -X POST "https://api.omophub.com/v1/concepts/recommended" \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "concept_ids": [201826], "vocabulary_ids": ["SNOMED", "ICD10CM"], "domain_ids": ["Condition", "Measurement"], "relationship_types": ["Has finding"], "standard_only": true, "include_invalid": false, "page": 1, "page_size": 50 }' ``` ```python Python (comprehensive) theme={null} import requests # Get comprehensive recommendations for Type 2 diabetes payload = { "concept_ids": [201826], # Type 2 diabetes mellitus (SNOMED) "vocabulary_ids": ["SNOMED", "LOINC"], "domain_ids": ["Condition", "Measurement", "Procedure"], "standard_only": True, "page": 1, "page_size": 50 } response = requests.post( "https://api.omophub.com/v1/concepts/recommended", headers={"Authorization": "Bearer YOUR_API_KEY"}, json=payload ) data = response.json() # Analyze recommendations by domain from collections import Counter domains = Counter(rec['domain_id'] for recs in data['data'].values() for rec in recs) print(f"Recommendations by domain: {dict(domains)}") # Show top recommendations for source_id, recommendations in data['data'].items(): print(f"\nTop 10 recommendations for concept {source_id}:") for i, rec in enumerate(recommendations[:10], 1): print(f" {i}. {rec['concept_name']}") print(f" [{rec['vocabulary_id']}] via '{rec['relationship_id']}'") print(f" Domain: {rec['domain_id']}, Code: {rec['concept_code']}") ``` ```json theme={null} { "success": true, "data": { "201826": [ { "concept_id": 4193704, "concept_name": "Hyperglycemia", "vocabulary_id": "SNOMED", "concept_code": "80394007", "domain_id": "Condition", "concept_class_id": "Clinical Finding", "standard_concept": "S", "invalid_reason": null, "valid_start_date": "1970-01-01", "valid_end_date": "2099-12-31", "relationship_id": "Has finding" }, { "concept_id": 4058243, "concept_name": "Diabetic retinopathy", "vocabulary_id": "SNOMED", "concept_code": "4855003", "domain_id": "Condition", "concept_class_id": "Clinical Finding", "standard_concept": "S", "invalid_reason": null, "valid_start_date": "1970-01-01", "valid_end_date": "2099-12-31", "relationship_id": "Associated finding" }, { "concept_id": 3004410, "concept_name": "Hemoglobin A1c measurement", "vocabulary_id": "LOINC", "concept_code": "4548-4", "domain_id": "Measurement", "concept_class_id": "Lab Test", "standard_concept": "S", "invalid_reason": null, "valid_start_date": "1970-01-01", "valid_end_date": "2099-12-31", "relationship_id": "Has finding" } ], "4329847": [ { "concept_id": 437894, "concept_name": "Coronary arteriosclerosis", "vocabulary_id": "SNOMED", "concept_code": "53741008", "domain_id": "Condition", "concept_class_id": "Clinical Finding", "standard_concept": "S", "invalid_reason": null, "valid_start_date": "1970-01-01", "valid_end_date": "2099-12-31", "relationship_id": "Associated finding" }, { "concept_id": 4213540, "concept_name": "Troponin measurement", "vocabulary_id": "LOINC", "concept_code": "6598-7", "domain_id": "Measurement", "concept_class_id": "Lab Test", "standard_concept": "S", "invalid_reason": null, "valid_start_date": "1970-01-01", "valid_end_date": "2099-12-31", "relationship_id": "Has finding" } ] }, "meta": { "request_id": "rec_1234567890_abc", "vocab_release": "2025.2", "pagination": { "page": 1, "page_size": 100, "total_items": 296, "total_pages": 3, "has_next": true, "has_previous": false } } } ``` ## Implementation Notes ### Data Source * Data is curated by the OHDSI community using the Phoebe algorithm * Version-independent: recommendations persist across vocabulary releases ### Response Structure * Results are **grouped by source concept ID** * Each source concept ID maps to an array of recommended concepts * Recommendations include full concept details and relationship information ### Filtering * Apply multiple filters simultaneously (vocabulary, domain, relationship type) * `standard_only=false` (default) includes all concepts; set to `true` to filter to standard concepts only * `include_invalid=false` (default) excludes deprecated concepts ## Related Endpoints * [Search Concepts](/api-reference/search/basic-search) - Full-text concept search * [Get Concept Relationships](/api-reference/concepts/get-concept-relationships) - Direct relationships * [Get Concept Hierarchy](/api-reference/hierarchy/get-concept-hierarchy) - Hierarchical relationships * [Batch Concepts](/api-reference/concepts/batch-concepts) - Retrieve multiple concepts # Suggest Concepts Source: https://docs.omophub.com/api-reference/concepts/suggest-concepts GET /concepts/suggest ## Overview Get intelligent concept suggestions for autocomplete functionality, based on partial text input. Uses prefix matching on concept names to provide relevant suggestions. ## Query Parameters Partial text input for suggestions (minimum 2 characters, maximum 100 characters) Page number for pagination (1-based) Number of suggestions per page (max: 100) Comma-separated list of vocabulary IDs to filter suggestions (e.g., "SNOMED,ICD10CM") Comma-separated list of domain IDs to filter suggestions (e.g., "Condition,Drug") Vocabulary release version to query (e.g., "2025.1"). Defaults to latest release. ```bash cURL theme={null} curl -X GET "https://api.omophub.com/v1/concepts/suggest?query=diab&page_size=5" \ -H "Authorization: Bearer YOUR_API_KEY" ``` ```bash cURL with filters theme={null} curl -X GET "https://api.omophub.com/v1/concepts/suggest?query=diab&vocabulary_ids=SNOMED&domain_ids=Condition&page=1&page_size=10" \ -H "Authorization: Bearer YOUR_API_KEY" ``` ```python Python theme={null} import requests params = { "query": "diab", "page": 1, "page_size": 5, "vocabulary_ids": "SNOMED", "domain_ids": "Condition" } response = requests.get( "https://api.omophub.com/v1/concepts/suggest", headers={"Authorization": "Bearer YOUR_API_KEY"}, params=params ) data = response.json() ``` ```javascript JavaScript theme={null} const params = new URLSearchParams({ query: 'diab', page: '1', page_size: '5', vocabulary_ids: 'SNOMED', domain_ids: 'Condition' }); const response = await fetch(`https://api.omophub.com/v1/concepts/suggest?${params}`, { headers: { 'Authorization': 'Bearer YOUR_API_KEY' } }); const data = await response.json(); ``` ```json theme={null} { "success": true, "data": [ { "concept_id": 201826, "concept_name": "Type 2 diabetes mellitus", "concept_code": "44054006", "vocabulary_id": "SNOMED", "domain_id": "Condition", "concept_class_id": "Clinical Finding", "standard_concept": "S", "valid_start_date": "2002-01-31", "valid_end_date": "2099-12-31", "invalid_reason": null }, { "concept_id": 4000678, "concept_name": "Diabetes mellitus", "concept_code": "73211009", "vocabulary_id": "SNOMED", "domain_id": "Condition", "concept_class_id": "Clinical Finding", "standard_concept": "S", "valid_start_date": "2002-01-31", "valid_end_date": "2099-12-31", "invalid_reason": null }, { "concept_id": 435216, "concept_name": "Type 1 diabetes mellitus", "concept_code": "46635009", "vocabulary_id": "SNOMED", "domain_id": "Condition", "concept_class_id": "Clinical Finding", "standard_concept": "S", "valid_start_date": "2002-01-31", "valid_end_date": "2099-12-31", "invalid_reason": null }, { "concept_id": 4174977, "concept_name": "Diabetic retinopathy", "concept_code": "4855003", "vocabulary_id": "SNOMED", "domain_id": "Condition", "concept_class_id": "Clinical Finding", "standard_concept": "S", "valid_start_date": "2002-01-31", "valid_end_date": "2099-12-31", "invalid_reason": null }, { "concept_id": 443767, "concept_name": "Diabetic nephropathy", "concept_code": "127013003", "vocabulary_id": "SNOMED", "domain_id": "Condition", "concept_class_id": "Clinical Finding", "standard_concept": "S", "valid_start_date": "2002-01-31", "valid_end_date": "2099-12-31", "invalid_reason": null } ], "meta": { "request_id": "req_suggest_123", "timestamp": "2024-12-22T10:00:00Z", "vocab_release": "2025.2", "pagination": { "page": 1, "page_size": 5, "total_items": 47, "total_pages": 10, "has_next": true, "has_previous": false } } } ``` # Traverse Relationships Source: https://docs.omophub.com/api-reference/concepts/traverse-relationships POST https://api.omophub.com/v1/concepts/relationships/traverse Traverse OMOP relationship networks across vocabularies to discover connected concepts - RxNorm ingredients, SNOMED parents, ICD mappings, and more. ## Overview This endpoint allows you to traverse relationship networks starting from one or more concepts, following chains of relationships to discover connected concepts. Uses a recursive graph traversal algorithm with cycle detection. ## Request Body Array of starting concept IDs for traversal (1-50 concepts) Optional parameters for controlling the traversal Maximum traversal depth (1-5) Array of relationship types to follow (default: \["Is a", "Subsumes", "Mapped from"]) Limit traversal to specific vocabularies Include invalid/deprecated concepts in traversal Include detailed path information in response Use breadth-first traversal (default is depth-first) Minimum relevance score filter (0.0-1.0) Maximum number of results to return ## Query Parameters Specific vocabulary release version (e.g., "2025.1") ## Response Indicates whether the request was successful Response data containing traversal results Array of concept IDs that were used as starting points Array of concepts discovered during traversal Discovered concept ID Discovered concept name Concept vocabulary Distance from starting concept Array of concept names in the path Array of relationship types used Detailed path information (when include\_paths=true) Source concept ID Target concept ID Path details Path distance Summary statistics for the traversal Total concepts discovered Maximum distance reached Relationship types traversed Response metadata Unique request identifier ISO 8601 timestamp Vocabulary release version used ```bash cURL theme={null} curl -X POST "https://api.omophub.com/v1/concepts/relationships/traverse" \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "concept_ids": [201826], "traversal_params": { "relationship_types": ["Is a", "Maps to"], "max_depth": 3 } }' ``` ```python Python theme={null} import requests def traverse_relationships(concept_ids, traversal_params=None): url = "https://api.omophub.com/v1/concepts/relationships/traverse" headers = { "Authorization": "Bearer YOUR_API_KEY", "Content-Type": "application/json" } data = {"concept_ids": concept_ids} if traversal_params: data["traversal_params"] = traversal_params response = requests.post(url, json=data, headers=headers) return response.json() # Example: Traverse from Type 2 diabetes result = traverse_relationships( concept_ids=[201826], traversal_params={ "relationship_types": ["Is a", "Subsumes", "Maps to"], "max_depth": 3, "include_paths": True } ) data = result['data'] print(f"Starting concepts: {data['starting_concepts']}") print(f"Discovered {data['traversal_summary']['total_discovered']} concepts") print(f"Max distance: {data['traversal_summary']['max_distance_reached']}") for concept in data['discovered_concepts'][:5]: print(f" - {concept['concept_name']} (distance: {concept['distance']})") ``` ```javascript JavaScript theme={null} const traverseRelationships = async (conceptIds, traversalParams = {}) => { const response = await fetch('https://api.omophub.com/v1/concepts/relationships/traverse', { method: 'POST', headers: { 'Authorization': 'Bearer YOUR_API_KEY', 'Content-Type': 'application/json' }, body: JSON.stringify({ concept_ids: conceptIds, traversal_params: traversalParams }) }); return response.json(); }; // Example usage const result = await traverseRelationships([201826], { relationship_types: ['Is a', 'Maps to'], max_depth: 3 }); const data = result.data; console.log(`Discovered ${data.traversal_summary.total_discovered} concepts`); data.discovered_concepts.slice(0, 5).forEach(concept => { console.log(`- ${concept.concept_name} (distance: ${concept.distance})`); }); ``` ```json theme={null} { "success": true, "data": { "starting_concepts": [201826], "discovered_concepts": [ { "concept_id": 73211009, "concept_name": "Diabetes mellitus", "vocabulary_id": "SNOMED", "distance": 1, "path": ["Type 2 diabetes mellitus", "Diabetes mellitus"], "relationship_path": ["Is a"] }, { "concept_id": 443767, "concept_name": "Type 2 diabetes mellitus with complications", "vocabulary_id": "SNOMED", "distance": 1, "path": ["Type 2 diabetes mellitus", "Type 2 diabetes mellitus with complications"], "relationship_path": ["Subsumes"] }, { "concept_id": 435216, "concept_name": "Type 2 diabetes mellitus", "vocabulary_id": "ICD10CM", "distance": 1, "path": ["Type 2 diabetes mellitus", "Type 2 diabetes mellitus"], "relationship_path": ["Maps to"] } ], "relationship_paths": [], "traversal_summary": { "total_discovered": 25, "max_distance_reached": 3, "relationship_types_used": ["Is a", "Subsumes", "Maps to"] } }, "meta": { "request_id": "req_traverse_123", "timestamp": "2025-01-15T10:00:00Z", "vocab_release": "2025.1" } } ``` ## Usage Examples ### Basic Traversal Simple traversal with default settings: ```json theme={null} { "concept_ids": [201826], "traversal_params": { "max_depth": 2 } } ``` ### With Path Details Include detailed path information: ```json theme={null} { "concept_ids": [201826], "traversal_params": { "relationship_types": ["Is a", "Maps to"], "max_depth": 3, "include_paths": true } } ``` ### Multiple Starting Concepts Traverse from multiple concepts: ```json theme={null} { "concept_ids": [201826, 4182210, 313217], "traversal_params": { "max_depth": 2, "page_size": 50 } } ``` ### Vocabulary-Filtered Traversal Limit to specific vocabularies: ```json theme={null} { "concept_ids": [201826], "traversal_params": { "vocabulary_ids": ["SNOMED", "ICD10CM"], "max_depth": 3 } } ``` ## Important Notes * **Concept limit**: Maximum 50 starting concepts per request * **Depth limit**: Maximum traversal depth is 5 to prevent performance issues * **Cycle detection**: The algorithm prevents infinite loops by tracking visited concepts * **Performance**: Deep traversals with many starting concepts may take longer * **Default relationships**: If not specified, uses \["Is a", "Subsumes", "Mapped from"] ## Related Endpoints * [Batch Hierarchy Queries](/api-reference/concepts/batch-hierarchy-queries) - Batch hierarchy queries * [Get Concept Relationships](/api-reference/concepts/get-concept-relationships) - Single concept relationships * [Get Concept Hierarchy](/api-reference/concepts/get-concept-hierarchy) - Hierarchical relationships only # Get Concept Classes Source: https://docs.omophub.com/api-reference/domains/get-concept-classes GET /v1/concept-classes Retrieve all OMOP concept classes that categorize medical concepts - Clinical Finding, Ingredient, Procedure, and the complete OHDSI classification set. This endpoint returns concept classes, which provide categorization of medical concepts within their domains. Concept classes distinguish between different types of concepts like "Clinical Finding" vs "Disorder" within conditions, or "Ingredient" vs "Clinical Drug" within drugs. ## Query Parameters Filter concept classes to those used by specific vocabularies. Comma-separated list.
**Example:** `SNOMED,RxNorm,ICD10CM`
Include concept counts and vocabulary coverage for each concept class. Specific vocabulary release version to query
**Example:** `2025.1`
## Response Indicates if the request was successful. Response data container. Array of concept class objects. Unique identifier for the concept class. Human-readable name of the concept class. OMOP concept ID representing this concept class. Number of concepts in this concept class (when `include_stats=true`). List of vocabulary IDs that have concepts in this class (when `include_stats=true`). Response metadata. Unique identifier for this request. Vocabulary release version used. Response timestamp (ISO format). ```bash cURL theme={null} curl -X GET "https://api.omophub.com/v1/concept-classes?include_stats=true" \ -H "Authorization: Bearer YOUR_API_KEY" ``` ```javascript JavaScript theme={null} const response = await fetch( 'https://api.omophub.com/v1/concept-classes?include_stats=true', { headers: { 'Authorization': 'Bearer YOUR_API_KEY' } } ); const data = await response.json(); console.log(`Found ${data.data.concept_classes.length} concept classes`); // Display concept classes with counts data.data.concept_classes.forEach(cc => { console.log(`${cc.concept_class_name}: ${cc.concept_count || 'N/A'} concepts`); }); ``` ```python Python theme={null} import requests response = requests.get( "https://api.omophub.com/v1/concept-classes", params={"include_stats": True, "vocabulary_ids": "SNOMED,RxNorm"}, headers={"Authorization": "Bearer YOUR_API_KEY"} ) data = response.json() for cc in data["data"]["concept_classes"]: print(f"{cc['concept_class_name']}: {cc.get('concept_count', 'N/A')} concepts") ``` ```json theme={null} { "success": true, "data": { "concept_classes": [ { "concept_class_id": "Clinical Finding", "concept_class_name": "Clinical Finding", "concept_class_concept_id": 441840, "concept_count": 425891, "vocabulary_coverage": ["SNOMED", "Read"] }, { "concept_class_id": "Ingredient", "concept_class_name": "Ingredient", "concept_class_concept_id": 441828, "concept_count": 45678, "vocabulary_coverage": ["RxNorm", "SNOMED"] }, { "concept_class_id": "Procedure", "concept_class_name": "Procedure", "concept_class_concept_id": 441838, "concept_count": 198765, "vocabulary_coverage": ["SNOMED", "HCPCS"] } ] }, "meta": { "request_id": "req_abc123", "vocab_release": "2025.1", "timestamp": "2025-01-05T10:00:00Z" } } ``` ## Usage Examples ### All Concept Classes Get all concept classes without statistics: ```bash theme={null} GET /v1/concept-classes ``` ### With Statistics Get concept classes with concept counts and vocabulary coverage: ```bash theme={null} GET /v1/concept-classes?include_stats=true ``` ### Filter by Vocabulary Get concept classes used by specific vocabularies: ```bash theme={null} GET /v1/concept-classes?vocabulary_ids=SNOMED,RxNorm&include_stats=true ``` ## Related Endpoints * [Get Domains](/api-reference/domains/get-domains) - Available domain information * [Get Domain Concepts](/api-reference/domains/get-domain-concepts) - Concepts within a domain ## Notes * Concept classes provide finer categorization than domains * Many concept classes are vocabulary-specific (e.g., "3-char billing code" for ICD codes) * When filtering by vocabulary, only concept classes that contain concepts from those vocabularies are returned * The `vocabulary_coverage` field shows which vocabularies contribute concepts to each class # Get Domain Concepts Source: https://docs.omophub.com/api-reference/domains/get-domain-concepts GET /v1/domains/{domain_id}/concepts Retrieve all OMOP concepts within a specific domain - Condition, Drug, Measurement, Procedure - with pagination for building domain-scoped concept sets. This endpoint provides access to all medical concepts within a particular domain, such as all conditions, drugs, or procedures. ## Path Parameters The domain identifier to retrieve concepts from.
**Example:** `Condition`, `Drug`, `Procedure`
## Query Parameters Filter concepts to specific vocabularies within the domain (comma-separated).
**Example:** `SNOMED,ICD10CM`
Return only standard concepts (standard\_concept = 'S'). Include invalid/deprecated concepts in results. Page number for pagination (1-based). Number of concepts to return per page (max 1000). Specific vocabulary release version to query
**Example:** `2025.1`
## Response Indicates if the request was successful. Response data container. Array of concept objects within the domain. Unique concept identifier. Standard name of the concept. Original code from the vocabulary. Vocabulary containing this concept. Human-readable vocabulary name. Domain identifier. Concept class identifier. Standard concept designation ('S', 'C', or null). Date when concept became valid (ISO format). Date when concept becomes invalid (ISO format). Reason for concept invalidation if applicable. Response metadata and pagination information. Unique identifier for this request. Vocabulary release version used. Response timestamp (ISO format). Current page number. Items per page. Total concepts matching filters. Total number of pages. Whether next page exists. Whether previous page exists. ```bash cURL theme={null} curl -X GET "https://api.omophub.com/v1/domains/Condition/concepts?vocabulary_ids=SNOMED&standard_only=true&page_size=50" \ -H "Authorization: Bearer YOUR_API_KEY" ``` ```javascript JavaScript theme={null} const response = await fetch( 'https://api.omophub.com/v1/domains/Condition/concepts?vocabulary_ids=SNOMED&standard_only=true&page_size=50', { headers: { 'Authorization': 'Bearer YOUR_API_KEY' } } ); const data = await response.json(); console.log(`Found ${data.meta.pagination.total_items} concepts`); data.data.concepts.forEach(concept => { console.log(`${concept.concept_name} (${concept.concept_id})`); }); ``` ```python Python theme={null} import requests response = requests.get( "https://api.omophub.com/v1/domains/Drug/concepts", params={ "vocabulary_ids": "RxNorm", "standard_only": True, "page_size": 100 }, headers={"Authorization": "Bearer YOUR_API_KEY"} ) data = response.json() for concept in data["data"]["concepts"]: print(f"{concept['concept_name']} ({concept['concept_id']})") ``` ```json theme={null} { "success": true, "data": { "concepts": [ { "concept_id": 201826, "concept_name": "Type 2 diabetes mellitus", "concept_code": "44054006", "vocabulary_id": "SNOMED", "vocabulary_name": "Systematized Nomenclature of Medicine Clinical Terms", "domain_id": "Condition", "concept_class_id": "Clinical Finding", "standard_concept": "S", "valid_start_date": "1970-01-01", "valid_end_date": "2099-12-31", "invalid_reason": null }, { "concept_id": 46635009, "concept_name": "Type 1 diabetes mellitus", "concept_code": "46635009", "vocabulary_id": "SNOMED", "vocabulary_name": "Systematized Nomenclature of Medicine Clinical Terms", "domain_id": "Condition", "concept_class_id": "Clinical Finding", "standard_concept": "S", "valid_start_date": "1970-01-01", "valid_end_date": "2099-12-31", "invalid_reason": null } ] }, "meta": { "request_id": "req_abc123", "vocab_release": "2025.1", "timestamp": "2025-01-05T10:00:00Z", "pagination": { "page": 1, "page_size": 50, "total_items": 847, "total_pages": 17, "has_next": true, "has_previous": false } } } ``` ## Usage Examples ### All Concepts in a Domain Get all concepts within the Condition domain: ```bash theme={null} GET /v1/domains/Condition/concepts?page_size=100 ``` ### Standard Concepts Only Get only standard concepts from a specific vocabulary: ```bash theme={null} GET /v1/domains/Drug/concepts?standard_only=true&vocabulary_ids=RxNorm ``` ### Filter by Multiple Vocabularies Get concepts from multiple vocabularies: ```bash theme={null} GET /v1/domains/Procedure/concepts?vocabulary_ids=SNOMED,HCPCS ``` ### Include Invalid Concepts Include deprecated concepts in results: ```bash theme={null} GET /v1/domains/Condition/concepts?include_invalid=true ``` ## Related Endpoints * [Get Domains](/api-reference/domains/get-domains) - Available domain list * [Get Concept Details](/api-reference/concepts/get-concept-details) - Individual concept information * [Search Concepts](/api-reference/search/basic-search) - Cross-domain concept search ## Notes * Domain queries can return large result sets - use pagination appropriately * Standard concepts (S) are preferred for most clinical applications * Default page size is 20, maximum is 1000 * Results are not sorted by default # Get Domain Statistics Source: https://docs.omophub.com/api-reference/domains/get-domain-statistics GET /v1/domains/{domain_id}/statistics Get statistical overview for an OMOP domain including concept counts, standard concept coverage, and vocabulary distribution for capacity planning. This endpoint provides statistical information about a domain, including concept counts, vocabulary distribution, and concept class breakdown. ## Path Parameters The domain identifier to get statistics for.
**Example:** `Condition`, `Drug`, `Procedure`
## Query Parameters Specific vocabulary release version to query
**Example:** `2025.1`
## Response Domain identifier. Statistical overview of the domain. Domain identifier. Human-readable domain name. Total number of concepts in the domain. Number of standard concepts (standard\_concept = 'S'). Number of non-standard concepts. Number of invalid/deprecated concepts. Distribution of concepts by vocabulary. Vocabulary identifier. Human-readable vocabulary name. Number of concepts from this vocabulary. Distribution of concepts by concept class. Concept class identifier. Human-readable concept class name. Number of concepts in this class. Date range of concept validity dates. Earliest valid\_start\_date (ISO format). Latest valid\_start\_date (ISO format). Response metadata. Unique identifier for this request. Vocabulary release version used. Response timestamp (ISO format). ```bash cURL theme={null} curl -X GET "https://api.omophub.com/v1/domains/Condition/statistics" \ -H "Authorization: Bearer YOUR_API_KEY" ``` ```javascript JavaScript theme={null} const response = await fetch( 'https://api.omophub.com/v1/domains/Condition/statistics', { headers: { 'Authorization': 'Bearer YOUR_API_KEY' } } ); const data = await response.json(); console.log(`Total concepts: ${data.data.statistics.total_concepts}`); console.log(`Standard: ${data.data.statistics.standard_concepts}`); // Show vocabulary distribution data.data.statistics.vocabulary_breakdown.forEach(v => { console.log(`${v.vocabulary_id}: ${v.concept_count} concepts`); }); ``` ```python Python theme={null} import requests response = requests.get( "https://api.omophub.com/v1/domains/Drug/statistics", headers={"Authorization": "Bearer YOUR_API_KEY"} ) data = response.json() stats = data["data"]["statistics"] print(f"Total: {stats['total_concepts']}") print(f"Standard: {stats['standard_concepts']}") print(f"Non-standard: {stats['non_standard_concepts']}") # Vocabulary breakdown for v in stats["vocabulary_breakdown"]: print(f" {v['vocabulary_id']}: {v['concept_count']}") ``` ```json theme={null} { "success": true, "data": { "domain_id": "Condition", "statistics": { "domain_id": "Condition", "domain_name": "Condition", "total_concepts": 845672, "standard_concepts": 423891, "non_standard_concepts": 421781, "invalid_concepts": 12453, "vocabulary_breakdown": [ { "vocabulary_id": "SNOMED", "vocabulary_name": "Systematized Nomenclature of Medicine Clinical Terms", "concept_count": 387421 }, { "vocabulary_id": "ICD10CM", "vocabulary_name": "International Classification of Diseases, Tenth Revision, Clinical Modification", "concept_count": 98234 }, { "vocabulary_id": "ICD10", "vocabulary_name": "International Classification of Diseases, Tenth Revision", "concept_count": 87456 } ], "concept_class_breakdown": [ { "concept_class_id": "Clinical Finding", "concept_class_name": "Clinical Finding", "concept_count": 312456 }, { "concept_class_id": "Disorder", "concept_class_name": "Disorder", "concept_count": 156789 } ], "creation_date_range": { "earliest": "1970-01-01", "latest": "2024-12-01" } } }, "meta": { "request_id": "req_abc123", "vocab_release": "2025.1", "timestamp": "2025-01-05T10:00:00Z" } } ``` ## Usage Examples ### Get Condition Domain Statistics ```bash theme={null} GET /v1/domains/Condition/statistics ``` ### Get Drug Domain Statistics ```bash theme={null} GET /v1/domains/Drug/statistics ``` ### Get Procedure Domain Statistics ```bash theme={null} GET /v1/domains/Procedure/statistics ``` ## Related Endpoints * [Get Domains](/api-reference/domains/get-domains) - List all available domains * [Get Domain Concepts](/api-reference/domains/get-domain-concepts) - Get concepts within a domain ## Notes * Statistics are calculated from the current vocabulary release * The vocabulary\_breakdown shows which source vocabularies contribute concepts to the domain * The concept\_class\_breakdown shows the distribution of concept types within the domain * Standard concepts are preferred for clinical applications # Get Domains Source: https://docs.omophub.com/api-reference/domains/get-domains GET /v1/domains Retrieve all OMOP domains - Condition, Drug, Measurement, Observation, Procedure - that group medical concepts into high-level semantic categories. This endpoint provides information about all domains available in the OMOP vocabulary system. Domains represent high-level categorizations that group medical concepts by their semantic meaning, such as Condition, Drug, Procedure, and others. ## Query Parameters Include concept counts and vocabulary coverage for each domain Specific vocabulary release version to query
**Example:** `2025.1`
## Response Array of domain objects with their properties Unique identifier for the domain (e.g., "Condition", "Drug", "Procedure") Human-readable name of the domain OMOP concept ID representing this domain Total number of concepts in this domain (when `include_stats=true`) Number of standard concepts in this domain (when `include_stats=true`) List of vocabularies that contribute concepts to this domain (when `include_stats=true`) Response metadata Unique identifier for this request Vocabulary release version used for this response ```bash cURL theme={null} curl -X GET "https://api.omophub.com/v1/domains?include_stats=true" \ -H "Authorization: Bearer YOUR_API_KEY" ``` ```javascript JavaScript theme={null} const response = await fetch('https://api.omophub.com/v1/domains?include_stats=true', { headers: { 'Authorization': 'Bearer YOUR_API_KEY' } }); const data = await response.json(); console.log(`Found ${data.data.domains.length} domains`); // Display domains with concept counts data.data.domains.forEach(domain => { const count = domain.concept_count || 'N/A'; console.log(`${domain.domain_name}: ${count} concepts`); }); ``` ```python Python theme={null} import requests url = "https://api.omophub.com/v1/domains" params = {"include_stats": "true"} headers = {"Authorization": "Bearer YOUR_API_KEY"} response = requests.get(url, params=params, headers=headers) data = response.json() print(f"Found {len(data['data']['domains'])} domains") # Display domains sorted by concept count domains = data['data']['domains'] for domain in sorted(domains, key=lambda d: d.get('concept_count', 0), reverse=True): count = domain.get('concept_count', 0) standard = domain.get('standard_concept_count', 0) print(f"{domain['domain_name']}: {count:,} total, {standard:,} standard") ``` ```r R theme={null} library(omophub) client <- omophub_client() domains <- client$domains$list(include_stats = TRUE) # Display domains for (domain in domains$domains) { cat(sprintf("%s: %d concepts\n", domain$domain_name, domain$concept_count %||% 0)) } ``` ```json Basic Response theme={null} { "success": true, "data": { "domains": [ { "domain_id": "Condition", "domain_name": "Condition", "domain_concept_id": 19 }, { "domain_id": "Drug", "domain_name": "Drug", "domain_concept_id": 13 }, { "domain_id": "Procedure", "domain_name": "Procedure", "domain_concept_id": 10 }, { "domain_id": "Measurement", "domain_name": "Measurement", "domain_concept_id": 21 }, { "domain_id": "Observation", "domain_name": "Observation", "domain_concept_id": 27 } ] }, "meta": { "request_id": "req_abc123", "vocab_release": "2025.1" } } ``` ```json With Statistics (include_stats=true) theme={null} { "success": true, "data": { "domains": [ { "domain_id": "Condition", "domain_name": "Condition", "domain_concept_id": 19, "concept_count": 845672, "standard_concept_count": 423891, "vocabulary_coverage": ["SNOMED", "ICD10CM", "ICD10", "ICD9CM", "Read"] }, { "domain_id": "Drug", "domain_name": "Drug", "domain_concept_id": 13, "concept_count": 652341, "standard_concept_count": 198765, "vocabulary_coverage": ["RxNorm", "RxNorm Extension", "NDC", "SNOMED"] }, { "domain_id": "Procedure", "domain_name": "Procedure", "domain_concept_id": 10, "concept_count": 456789, "standard_concept_count": 234561, "vocabulary_coverage": ["SNOMED", "ICD10PCS", "HCPCS"] }, { "domain_id": "Measurement", "domain_name": "Measurement", "domain_concept_id": 21, "concept_count": 234567, "standard_concept_count": 156789, "vocabulary_coverage": ["LOINC", "SNOMED"] }, { "domain_id": "Observation", "domain_name": "Observation", "domain_concept_id": 27, "concept_count": 123456, "standard_concept_count": 87654, "vocabulary_coverage": ["SNOMED", "LOINC"] } ] }, "meta": { "request_id": "req_xyz789", "vocab_release": "2025.1" } } ``` ## Usage Examples ### Basic Domain List Get all available domains: ```javascript theme={null} const response = await fetch('/v1/domains', { headers: { 'Authorization': 'Bearer YOUR_API_KEY' } }); const { data } = await response.json(); console.log(data.domains); ``` ### Domains with Statistics Get domains with concept counts and vocabulary coverage: ```javascript theme={null} const response = await fetch('/v1/domains?include_stats=true', { headers: { 'Authorization': 'Bearer YOUR_API_KEY' } }); const { data } = await response.json(); // Find the domain with the most concepts const largestDomain = data.domains.reduce((max, domain) => (domain.concept_count > (max.concept_count || 0)) ? domain : max , {}); console.log(`Largest domain: ${largestDomain.domain_name} with ${largestDomain.concept_count} concepts`); ``` ### Filter by Vocabulary Coverage Find domains that include concepts from a specific vocabulary: ```javascript theme={null} const response = await fetch('/v1/domains?include_stats=true', { headers: { 'Authorization': 'Bearer YOUR_API_KEY' } }); const { data } = await response.json(); // Find domains with SNOMED concepts const snomedDomains = data.domains.filter(domain => domain.vocabulary_coverage?.includes('SNOMED') ); console.log('Domains with SNOMED concepts:', snomedDomains.map(d => d.domain_name)); ``` ## Related Endpoints * [Get Domain Concepts](/api-reference/domains/get-domain-concepts) - Concepts within a specific domain * [Get Concept Classes](/api-reference/domains/get-concept-classes) - Concept class classifications * [Search Concepts](/api-reference/search/basic-search) - Search within specific domains * [Get Vocabularies](/api-reference/vocabulary/get-vocabularies) - Available vocabularies ## Notes * Domains provide the highest level of semantic categorization in OMOP * The most commonly used clinical domains are Condition, Drug, Procedure, and Measurement * Use `include_stats=true` to get concept counts for capacity planning and analysis * The `vocabulary_coverage` field shows which vocabularies contribute concepts to each domain # API error codes and troubleshooting reference Source: https://docs.omophub.com/api-reference/errors Full reference of OMOPHub API error codes and HTTP statuses with guidance on how to troubleshoot 4xx and 5xx responses in your integration. # Error Handling OMOPHub API uses conventional HTTP response codes and provides detailed error information. ## HTTP Status Codes ### Success Codes * `200 OK` - Request successful * `201 Created` - Resource created successfully * `204 No Content` - Request successful, no content to return ### Client Error Codes (4xx) * `400 Bad Request` - Invalid request parameters * `401 Unauthorized` - Authentication required * `403 Forbidden` - Insufficient permissions * `404 Not Found` - Resource not found * `413 Payload Too Large` - Request body exceeds size limits * `422 Unprocessable Entity` - Valid request format but semantic errors * `429 Too Many Requests` - Rate limit exceeded ### Server Error Codes (5xx) * `500 Internal Server Error` - Unexpected server error * `503 Service Unavailable` - Service temporarily unavailable ## Common Error Codes ### Authentication & Authorization * `missing_api_key` (401) - No API key provided in Authorization header * `invalid_api_key` (403) - API key is invalid, expired, or revoked * `unauthorized` (401) - Authentication token is invalid * `forbidden` (403) - Insufficient permissions for this resource ### Request Validation * `validation_error` (422) - Well-formed request but semantically invalid parameters * `missing_required_field` (400) - Required fields missing or malformed in the request * `bad_request` (400) - General malformed request (bad JSON/format) ### Resource Management * `not_found` (404) - Requested resource does not exist * `concept_not_found` (404) - Specific concept ID not found * `vocabulary_not_found` (404) - Vocabulary ID not found ### Rate Limits & Quotas * `rate_limit_exceeded` (429) - Too many requests per second * `daily_quota_exceeded` (429) - Daily request limit reached ### Healthcare & Security * `healthcare_data_error` (422) - Medical data validation failure * `invalid_mapping` (400) - Concept mapping validation error ### System & Service Errors * `internal_server_error` (500) - Unexpected server error * `service_unavailable` (503) - API temporarily unavailable ### Version & Compatibility * `version_not_supported` (400) - Requested API version not supported * `search_error` (400) - Search operation failed # FHIR Batch Bundle terminology operations Source: https://docs.omophub.com/api-reference/fhir-terminology/batch Execute multiple FHIR terminology operations - $lookup, $validate-code, $translate, $expand - in a single FHIR Bundle request for efficient processing. ## Overview Send multiple FHIR terminology operations in a single HTTP request using a FHIR Batch Bundle. Each entry is processed independently, and results are returned in a batch-response Bundle preserving entry order. This is how ETL pipelines perform bulk terminology lookups and translations efficiently via FHIR. ## Request ```bash theme={null} curl -X POST "https://fhir.omophub.com/fhir/r4/" \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/fhir+json" \ -d '{ "resourceType": "Bundle", "type": "batch", "entry": [ { "request": { "method": "GET", "url": "CodeSystem/$lookup?system=http://snomed.info/sct&code=44054006" } }, { "request": { "method": "GET", "url": "ConceptMap/$translate?sourceCode=E11.9&system=http://hl7.org/fhir/sid/icd-10-cm&targetSystem=https://fhir-terminology.ohdsi.org" } }, { "request": { "method": "GET", "url": "CodeSystem/$validate-code?url=http://snomed.info/sct&code=44054006" } } ] }' ``` ## Response ```json theme={null} { "resourceType": "Bundle", "type": "batch-response", "entry": [ { "resource": { "resourceType": "Parameters", "parameter": [ { "name": "display", "valueString": "Type 2 diabetes mellitus" } ] }, "response": { "status": "200 OK" } }, { "resource": { "resourceType": "Parameters", "parameter": [ { "name": "result", "valueBoolean": true } ] }, "response": { "status": "200 OK" } }, { "resource": { "resourceType": "Parameters", "parameter": [ { "name": "result", "valueBoolean": true } ] }, "response": { "status": "200 OK" } } ] } ``` ## Supported Operations The following GET operations can be included in batch entries: | Operation | Example URL | | ------------------------- | --------------------------------------------------------------------------------------------------------------------------------- | | `$lookup` | `CodeSystem/$lookup?system=http://snomed.info/sct&code=44054006` | | `$validate-code` | `CodeSystem/$validate-code?url=http://snomed.info/sct&code=44054006` | | `$translate` | `ConceptMap/$translate?sourceCode=E11.9&system=http://hl7.org/fhir/sid/icd-10-cm&targetSystem=https://fhir-terminology.ohdsi.org` | | `$expand` | `ValueSet/$expand?url=http://snomed.info/sct?fhir_vs=isa/73211009&count=5` | | `$subsumes` | `CodeSystem/$subsumes?codeA=73211009&codeB=44054006&system=http://snomed.info/sct` | | ValueSet `$validate-code` | `ValueSet/$validate-code?url=http://snomed.info/sct?fhir_vs&system=http://snomed.info/sct&code=44054006` | **Not supported in batch.** [CodeSystem search](/api-reference/fhir-terminology/codesystem-search), [CodeSystem instance read](/api-reference/fhir-terminology/codesystem-read), [ValueSet search](/api-reference/fhir-terminology/valueset-search), [$find-matches](/api-reference/fhir-terminology/find-matches), [$closure](/api-reference/fhir-terminology/closure), and \[$diff](/api-reference/fhir-terminology/diff) cannot be included in a batch Bundle - call them directly. `$find-matches`and`\$closure\` are POST-only operations and batch only supports GET entries; the search-type and instance-read endpoints are omitted by design because they're single-shot discovery calls that don't benefit from batching. ## Limits * **Maximum 100 entries** per batch. Exceeding this returns a 400 error. * **GET only** for MVP. POST-with-body entries are not supported. * **No transaction bundles** - only `type: "batch"` is accepted. * **Metering**: Each entry counts as 1 API call toward your quota. ## Partial Failure Individual entry errors do not fail the entire batch. Each entry receives its own HTTP status in the response: ```json theme={null} { "resourceType": "Bundle", "type": "batch-response", "entry": [ { "resource": { "resourceType": "Parameters", "parameter": [] }, "response": { "status": "200 OK" } }, { "resource": { "resourceType": "OperationOutcome", "issue": [{ "severity": "error", "code": "not-found", "diagnostics": "Code '999999' not found in SNOMED" }] }, "response": { "status": "404 Not Found" } } ] } ``` ## Errors | HTTP | Cause | | ---- | ----------------------------------------------------------------------------------------------------- | | 400 | Request body is not a FHIR Bundle, type is not "batch", missing entry array, or more than 100 entries | # FHIR ConceptMap/$closure for subsumption tables Source: https://docs.omophub.com/api-reference/fhir-terminology/closure Compute and maintain FHIR subsumption closure tables across OMOP concepts - track ancestor/descendant links between vocabulary version updates. ## Overview Given a set of concept codes, computes all subsumption (is-a) relationships among them and returns a FHIR ConceptMap. Used by advanced FHIR clients for offline hierarchy reasoning. This is a stateless operation -- the closure is computed fresh each request. ## Request ```bash theme={null} curl -X POST "https://fhir.omophub.com/fhir/r4/ConceptMap/\$closure" \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/fhir+json" \ -d '{ "resourceType": "Parameters", "parameter": [ { "name": "name", "valueString": "my-closure" }, { "name": "concept", "valueCoding": { "system": "http://snomed.info/sct", "code": "73211009" } }, { "name": "concept", "valueCoding": { "system": "http://snomed.info/sct", "code": "44054006" } }, { "name": "concept", "valueCoding": { "system": "http://snomed.info/sct", "code": "46635009" } } ] }' ``` ## Parameters | Parameter | Type | Required | Description | | --------- | ------ | ---------------- | -------------------------------------------------------------- | | `name` | string | No | Client-defined closure table name (accepted but not persisted) | | `concept` | Coding | Yes (repeatable) | Concept codes to include in the closure (max 50) | ## Response ```json theme={null} { "resourceType": "ConceptMap", "name": "my-closure", "status": "active", "group": [ { "source": "http://snomed.info/sct", "target": "http://snomed.info/sct", "element": [ { "code": "73211009", "target": [ { "code": "44054006", "equivalence": "subsumes" }, { "code": "46635009", "equivalence": "subsumes" } ] } ] } ] } ``` In this example, "Diabetes mellitus" (73211009) subsumes both "Type 2 diabetes mellitus" (44054006) and "Type 1 diabetes mellitus" (46635009). ## Limits * Maximum **50 concepts** per request * POST only (per FHIR specification) * Stateless -- no server-side closure table is maintained ## Errors | HTTP | Issue Code | Cause | | ---- | ---------- | ------------------------------------------------------ | | 400 | `invalid` | No concept parameters provided, or exceeds 50 concepts | # FHIR CodeSystem read for OMOP vocabularies Source: https://docs.omophub.com/api-reference/fhir-terminology/codesystem-read Fetch a specific FHIR CodeSystem resource by ID - either a per-vocabulary stub (SNOMED, LOINC, RxNorm) or the unified OMOP omnibus CodeSystem. ## Overview Returns a single `CodeSystem` resource by its FHIR `id`. OMOPHub recognizes two ID shapes: 1. **Per-vocabulary stub IDs** - short lowercase slugs (`loinc`, `snomed`, `rxnorm`, `icd-10-cm`, …) that map 1:1 to canonical FHIR system URIs. These return `content: "not-present"` stubs with metadata only. 2. **Unified OMOP release IDs** - the release-specific FHIR id (e.g. `omop-v20250827`) for the omnibus OMOP CodeSystem that covers every supported vocabulary under one URL. These return the full CodeSystem resource with all OMOP property definitions. ## Request ### Per-vocabulary stub ```bash theme={null} curl "https://fhir.omophub.com/fhir/r4/CodeSystem/loinc" \ -H "Authorization: Bearer YOUR_API_KEY" ``` ### Unified OMOP release ```bash theme={null} curl "https://fhir.omophub.com/fhir/r4/CodeSystem/omop-v20250827" \ -H "Authorization: Bearer YOUR_API_KEY" ``` Use the [metadata endpoint](/api-reference/fhir-terminology/metadata) to discover the current release ID, or call [CodeSystem search](/api-reference/fhir-terminology/codesystem-search) with `url=https://fhir-terminology.ohdsi.org` to fetch the unified resource via the canonical URL. ## Response (per-vocabulary stub) ```json theme={null} { "resourceType": "CodeSystem", "id": "loinc", "url": "http://loinc.org", "name": "LOINC", "title": "Logical Observation Identifiers Names and Codes", "status": "active", "experimental": false, "publisher": "Regenstrief Institute", "description": "Logical Observation Identifiers Names and Codes - served by OMOPHub. Use $lookup and $validate-code operations to query concepts; the full concept list is not served inline (content: not-present).", "content": "not-present" } ``` Instance reads return a bare `CodeSystem` resource, not a Bundle. Only search-type queries wrap results in a Bundle. ## Response (unified OMOP release) ```json theme={null} { "resourceType": "CodeSystem", "id": "omop-v20250827", "url": "https://fhir-terminology.ohdsi.org", "version": "2025.2", "name": "OMOPConcepts", "title": "OMOP Standardized Vocabularies", "status": "active", "date": "2025-08-27", "publisher": "OHDSI / OMOPHub", "description": "OMOP Standardized Vocabularies (release 2025.2). Served by OMOPHub at https://fhir.omophub.com. Content is identical to the OHDSI ATHENA vocabulary release.", "content": "not-present", "property": [ { "code": "concept-id", "description": "The OMOP concept_id value for the concept.", "type": "integer" }, { "code": "source-concept-code", "description": "The OMOP concept_code value as a FHIR Coding (includes system URI + code).", "type": "Coding" } ] } ``` The full response lists every OMOP property and relationship type the server supports - see [\$lookup properties](/api-reference/fhir-terminology/overview#property-naming) in the overview for the complete set. ## Why two shapes? OMOPHub serves all 130+ vocabularies from a single unified backend. The per-vocab stubs are thin FHIR-conformant views over that backend, added so clients like HAPI and EHRbase can discover `http://loinc.org` as a first-class CodeSystem instead of having to know the OMOP omnibus URL. Both shapes resolve to the same concept data; pick whichever matches your client's expectations. | Use the stub when | Use the unified CodeSystem when | | ----------------------------------------------------------------- | -------------------------------------------------------------------------------- | | A FHIR client is probing support by canonical URI (HAPI, EHRbase) | You need release-specific metadata (version, release date, property definitions) | | You want concise metadata for UI pickers | You're building a client that walks OMOP properties directly | | Round-tripping `url: http://loinc.org` from a search result | Round-tripping `url: https://fhir-terminology.ohdsi.org` | ## Errors | HTTP | Issue Code | Cause | | ---- | ----------- | ----------------------------------------------------------------- | | 401 | `login` | Missing or invalid API key | | 404 | `not-found` | `id` does not match any known stub or released vocabulary version | ## See Also * [CodeSystem search](/api-reference/fhir-terminology/codesystem-search) - discovery endpoint that returns these stubs in a Bundle * [\$lookup](/api-reference/fhir-terminology/lookup) - query concept details using the `url` from the resource * [FHIR Terminology overview](/api-reference/fhir-terminology/overview) - full list of supported system URIs and stub IDs # FHIR CodeSystem search and URI resolution Source: https://docs.omophub.com/api-reference/fhir-terminology/codesystem-search Discover which FHIR CodeSystems OMOPHub serves and resolve canonical system URIs to instance IDs for SNOMED, LOINC, RxNorm, and ICD-10. ## Overview Search-type query on `CodeSystem`. This is the discovery call every FHIR client makes before invoking operations against a specific code system. HAPI FHIR's `RemoteTerminologyServiceValidationSupport.fetchCodeSystem()` runs `GET /CodeSystem?url=http://loinc.org` to decide whether to route code validation through OMOPHub - if the response Bundle is non-empty, HAPI proceeds; if it's empty, HAPI skips OMOPHub for that system. OMOPHub exposes per-vocabulary CodeSystem stubs with `content: "not-present"`. The stubs are metadata-only; concept data is served via the `$lookup`, `$validate-code`, `$expand`, and `$translate` operations. ## Modes ### 1. Search by canonical system URI ```bash theme={null} curl "https://fhir.omophub.com/fhir/r4/CodeSystem?url=http://loinc.org" \ -H "Authorization: Bearer YOUR_API_KEY" ``` Returns a `searchset` Bundle with one entry - the LOINC stub. ### 2. Search by the unified OMOP URL ```bash theme={null} curl "https://fhir.omophub.com/fhir/r4/CodeSystem?\ url=https://fhir-terminology.ohdsi.org" \ -H "Authorization: Bearer YOUR_API_KEY" ``` Returns a `searchset` Bundle with one entry - the full OMOP omnibus CodeSystem (with all property definitions and the current vocabulary release metadata). ### 3. List all supported CodeSystems ```bash theme={null} curl "https://fhir.omophub.com/fhir/r4/CodeSystem" \ -H "Authorization: Bearer YOUR_API_KEY" ``` Returns a `searchset` Bundle with 21 entries - 20 per-vocabulary stubs plus the unified OMOP CodeSystem. Useful for building UI pickers and for one-shot capability discovery. ### 4. Search by unknown URI ```bash theme={null} curl "https://fhir.omophub.com/fhir/r4/CodeSystem?url=http://example.com/unknown" \ -H "Authorization: Bearer YOUR_API_KEY" ``` Returns an empty Bundle (`total: 0`), not a 404. This is the FHIR search convention - "no matches found", not "endpoint not found". ## Parameters | Parameter | Type | Required | Description | | --------- | ---- | -------- | ------------------------------------------------------------------------ | | `url` | uri | No | Canonical system URI to resolve. Omit to list all supported CodeSystems. | ## Response (per-vocabulary stub) ```json theme={null} { "resourceType": "Bundle", "type": "searchset", "total": 1, "entry": [ { "fullUrl": "https://fhir.omophub.com/fhir/r4/CodeSystem/loinc", "resource": { "resourceType": "CodeSystem", "id": "loinc", "url": "http://loinc.org", "name": "LOINC", "title": "Logical Observation Identifiers Names and Codes", "status": "active", "experimental": false, "publisher": "Regenstrief Institute", "description": "Logical Observation Identifiers Names and Codes - served by OMOPHub. Use $lookup and $validate-code operations to query concepts; the full concept list is not served inline (content: not-present).", "content": "not-present" } } ] } ``` The stub carries metadata only. `content: "not-present"` is the standard FHIR value for terminology servers that expose concepts via operations rather than inline enumeration. Clients should follow up with [$lookup](/api-reference/fhir-terminology/lookup), [$validate-code](/api-reference/fhir-terminology/validate-code), or [\$subsumes](/api-reference/fhir-terminology/subsumes) using the same canonical URI (`http://loinc.org`) - the operation handlers dispatch on the FHIR system URI and route to the underlying OMOP vocabulary. ## Response (list-all mode) Same Bundle shape as above, with 21 entries. Each stub has a distinct `id` (the short slug - `loinc`, `snomed`, `rxnorm`, etc.) and `url` (the canonical FHIR system URI). The final entry is the unified OMOP CodeSystem (`https://fhir-terminology.ohdsi.org`) with the release-specific id (e.g. `omop-v20250827`) and full property definitions. ## Stub-to-URI map See the [Supported Vocabularies](/api-reference/fhir-terminology/overview#supported-vocabularies) table in the overview for the authoritative list of `id` ↔ `url` pairs. ## Errors | HTTP | Issue Code | Cause | | ---- | ---------- | -------------------------- | | 401 | `login` | Missing or invalid API key | Unknown URIs return an empty Bundle, not an error. ## See Also * [CodeSystem instance read](/api-reference/fhir-terminology/codesystem-read) - `GET /CodeSystem/{id}` for follow-up reads after discovery * [\$lookup](/api-reference/fhir-terminology/lookup) - fetch concept details for a code in a discovered system * [HAPI FHIR Integration](/guides/integration/hapi-fhir) - how HAPI's validator uses this endpoint # FHIR CodeSystem/$diff for vocabulary version changes Source: https://docs.omophub.com/api-reference/fhir-terminology/diff Compare OMOP concepts between vocabulary releases with FHIR $diff - detect added, removed, and changed codes across Athena version upgrades. ## Overview Compares concepts between two OMOP vocabulary releases. Returns counts and details of added and deprecated concepts. This is an OMOPHub-exclusive operation with no FHIR standard equivalent. ## Request ```bash theme={null} curl "https://fhir.omophub.com/fhir/r4/CodeSystem/\$diff?\ from=2025.2&to=2026.1&count=5" \ -H "Authorization: Bearer YOUR_API_KEY" ``` ### With vocabulary filter ```bash theme={null} curl "https://fhir.omophub.com/fhir/r4/CodeSystem/\$diff?\ from=2025.2&to=2026.1&system=http://snomed.info/sct&count=10" \ -H "Authorization: Bearer YOUR_API_KEY" ``` ## Parameters | Parameter | Type | Required | Description | | --------- | ------- | -------- | ---------------------------------------------------------------------------------------- | | `from` | string | Yes | Old vocabulary version. Accepts OMOPHub version (`2025.2`) or FHIR ID (`omop-v20250827`) | | `to` | string | Yes | New vocabulary version. Same formats as `from` | | `system` | uri | No | Filter to a specific vocabulary | | `count` | integer | No | Max detail entries per category (default: 100, max: 1000) | ## Response ```json theme={null} { "resourceType": "Parameters", "parameter": [ { "name": "from", "valueString": "2025.2" }, { "name": "to", "valueString": "2026.1" }, { "name": "total-added", "valueInteger": 1247 }, { "name": "total-deprecated", "valueInteger": 83 }, { "name": "added", "part": [ { "name": "code", "valueCode": "1234567" }, { "name": "display", "valueString": "New concept name" }, { "name": "property", "part": [ { "name": "code", "valueCode": "vocabulary-id" }, { "name": "value", "valueCode": "SNOMED" } ] } ] }, { "name": "deprecated", "part": [ { "name": "code", "valueCode": "7654321" }, { "name": "display", "valueString": "Deprecated concept" }, { "name": "property", "part": [ { "name": "code", "valueCode": "invalid-reason" }, { "name": "value", "valueCode": "U" } ] } ] } ] } ``` ## Errors | HTTP | Issue Code | Cause | | ---- | ----------- | ------------------------------------------------------- | | 400 | `invalid` | Missing `from` or `to` parameter, or unknown system URI | | 404 | `not-found` | Version not found in vocabulary releases | # FHIR ValueSet/$expand for OMOP concept sets Source: https://docs.omophub.com/api-reference/fhir-terminology/expand Expand a FHIR ValueSet into its complete list of matching codes - hierarchy-aware, version-pinned expansions across SNOMED, LOINC, and RxNorm. ## Overview Expands an implicit ValueSet to return a paginated list of matching codes. This powers dropdown menus, autocomplete, and template validation in EHRbase, HAPI FHIR, and other FHIR clients. OMOPHub supports implicit ValueSets defined by code system URIs - no stored ValueSet definitions required. ## Supported Patterns ### All codes from a code system ```bash theme={null} curl "https://fhir.omophub.com/fhir/r4/ValueSet/\$expand?\ url=http://snomed.info/sct?fhir_vs&count=5" \ -H "Authorization: Bearer YOUR_API_KEY" ``` ### Descendants of a concept (is-a filter) ```bash theme={null} curl "https://fhir.omophub.com/fhir/r4/ValueSet/\$expand?\ url=http://snomed.info/sct?fhir_vs=isa/73211009&count=20" \ -H "Authorization: Bearer YOUR_API_KEY" ``` This expands to all descendants of "Diabetes mellitus" (73211009). This is the pattern EHRbase uses in template `referenceSetUri`: ```xml theme={null} terminology://fhir.omophub.com/fhir/r4/ValueSet/$expand?url=http://snomed.info/sct?fhir_vs=isa/73211009 ``` ### With text filter (typeahead/autocomplete) ```bash theme={null} curl "https://fhir.omophub.com/fhir/r4/ValueSet/\$expand?\ url=http://snomed.info/sct?fhir_vs&filter=diabet&count=10" \ -H "Authorization: Bearer YOUR_API_KEY" ``` ## Parameters | Parameter | Type | Required | Description | | --------- | ------- | -------- | --------------------------------------------------------------------------------------------------------------- | | `url` | uri | Yes | Implicit ValueSet URL (e.g., `http://snomed.info/sct?fhir_vs` or `http://snomed.info/sct?fhir_vs=isa/73211009`) | | `filter` | string | No | Text filter to match against concept display names | | `offset` | integer | No | Paging start position (default: 0) | | `count` | integer | No | Page size (default: 100, max: 1000) | ## Response ```json theme={null} { "resourceType": "ValueSet", "status": "active", "expansion": { "identifier": "urn:uuid:a1b2c3d4-...", "timestamp": "2026-04-12T10:00:00Z", "total": 247, "offset": 0, "parameter": [ { "name": "offset", "valueInteger": 0 }, { "name": "count", "valueInteger": 20 } ], "contains": [ { "system": "http://snomed.info/sct", "code": "44054006", "display": "Type 2 diabetes mellitus" }, { "system": "http://snomed.info/sct", "code": "46635009", "display": "Type 1 diabetes mellitus" } ] } } ``` ## Pagination Use `offset` and `count` to page through large expansions. The response includes `total` for the total number of matching concepts. ```bash theme={null} # Page 2 (items 20-39) curl "https://fhir.omophub.com/fhir/r4/ValueSet/\$expand?\ url=http://snomed.info/sct?fhir_vs=isa/73211009&offset=20&count=20" \ -H "Authorization: Bearer YOUR_API_KEY" ``` **Deterministic sort order.** Concepts are sorted by OMOP `concept_id` so successive calls return the same first-page results and paginate consistently. FHIR clients that cache one expansion per ValueSet (EHRbase's composition validator, HAPI FHIR's `RemoteTerminologyServiceValidationSupport`) can rely on this - a code observed in page 0 of one call will still be in page 0 of the next call. **Double percent-encoding tolerance.** Spring-based clients (HAPI FHIR, EHRbase via `UriComponentsBuilder`) sometimes percent-encode the `url` parameter twice - once for the template `referenceSetUri` and again when building the outbound HTTP request. `$expand` detects the second encoding layer automatically and decodes it before parsing the implicit ValueSet URL, so both single- and double-encoded forms work. Malformed percent-encoding falls back to a clean 400 `OperationOutcome` instead of a 500. ## Errors | HTTP | Issue Code | Cause | | ---- | ---------- | -------------------------------------------------------------------------------- | | 400 | `invalid` | Missing `url` parameter, invalid ValueSet URL format, or unknown code system URI | # FHIR CodeSystem/$find-matches for property search Source: https://docs.omophub.com/api-reference/fhir-terminology/find-matches Find OMOP concepts by property criteria using FHIR $find-matches - search by ingredient, dose form, lab method, or other coded property attributes. ## Overview Returns concepts matching a set of property criteria. This is a reverse lookup -- "find me concepts with these characteristics" rather than "look up this specific code." Maps to OMOPHub's search capabilities with domain, vocabulary, and concept class filters. ## Request ```bash theme={null} curl -X POST "https://fhir.omophub.com/fhir/r4/CodeSystem/\$find-matches" \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/fhir+json" \ -d '{ "resourceType": "Parameters", "parameter": [ { "name": "system", "valueUri": "http://snomed.info/sct" }, { "name": "property", "part": [ { "name": "code", "valueCode": "domain-id" }, { "name": "value", "valueCode": "Condition" } ] } ] }' ``` `$find-matches` requires structured `property` parameters with `code` and `value` parts. Use POST with a FHIR Parameters body - GET query strings cannot express this structure. ## Parameters | Parameter | Type | Required | Description | | ---------- | --------- | --------------- | -------------------------------------------------------------------------- | | `system` | uri | No | Code system to search within | | `property` | composite | No (repeatable) | Property criteria (code + value pair). If omitted, returns a broad search. | ### Supported Property Codes | Property Code | Description | Example Value | | ------------------ | --------------- | ----------------------------------------------- | | `domain-id` | OMOP domain | `Condition`, `Drug`, `Measurement`, `Procedure` | | `vocabulary-id` | OMOP vocabulary | `SNOMED`, `ICD10CM`, `LOINC`, `RxNorm` | | `concept-class-id` | Concept class | `Clinical Finding`, `Ingredient`, `Lab Test` | | `standard-concept` | Standard status | `S` (Standard), `C` (Classification) | | `concept-name` | Text search | `diabetes`, `myocardial` | ## Response ```json theme={null} { "resourceType": "Parameters", "parameter": [ { "name": "match", "part": [ { "name": "concept", "valueCoding": { "system": "http://snomed.info/sct", "code": "44054006", "display": "Type 2 diabetes mellitus" } } ] }, { "name": "match", "part": [ { "name": "concept", "valueCoding": { "system": "http://snomed.info/sct", "code": "73211009", "display": "Diabetes mellitus" } } ] } ] } ``` # FHIR CodeSystem/$lookup for OMOP concept details Source: https://docs.omophub.com/api-reference/fhir-terminology/lookup Look up OMOP concept details by code and system with FHIR $lookup - return display, designations, properties, and version info for any concept. ## Overview Returns detailed information about a concept from a code system. This is the most commonly used FHIR terminology operation. Supports both **GET** (query parameters) and **POST** (FHIR Parameters body), and both **type-level** (by system URI) and **instance-level** (by CodeSystem resource ID). ## Request ### Type-level (by system URI) ```bash theme={null} # By vocabulary-specific code (ETL developers) curl "https://fhir.omophub.com/fhir/r4/CodeSystem/\$lookup?\ system=http://snomed.info/sct&code=44054006" \ -H "Authorization: Bearer YOUR_API_KEY" # By OMOP concept_id (FHIR-compatible) curl "https://fhir.omophub.com/fhir/r4/CodeSystem/\$lookup?\ system=https://fhir-terminology.ohdsi.org&code=201826" \ -H "Authorization: Bearer YOUR_API_KEY" ``` ### Instance-level (by CodeSystem ID) ```bash theme={null} curl "https://fhir.omophub.com/fhir/r4/CodeSystem/omop-v20260227/\$lookup?\ code=201826" \ -H "Authorization: Bearer YOUR_API_KEY" ``` ### POST with FHIR Parameters body ```bash theme={null} curl -X POST "https://fhir.omophub.com/fhir/r4/CodeSystem/\$lookup" \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/fhir+json" \ -d '{ "resourceType": "Parameters", "parameter": [ { "name": "system", "valueUri": "http://snomed.info/sct" }, { "name": "code", "valueCode": "44054006" } ] }' ``` ### With property filter Request only specific properties to reduce response size: ```bash theme={null} curl "https://fhir.omophub.com/fhir/r4/CodeSystem/\$lookup?\ system=http://snomed.info/sct&code=44054006&\ property=concept-id&property=domain-id&property=standard-concept" \ -H "Authorization: Bearer YOUR_API_KEY" ``` ## Parameters | Parameter | Type | Required | Description | | ---------- | ------ | ---------------- | ----------------------------------------- | | `system` | uri | Yes (type-level) | FHIR code system URI | | `code` | code | Yes | The code to look up | | `version` | string | No | Vocabulary version | | `property` | code | No (repeatable) | Which properties to return (default: all) | ## Response The response echoes the requested `system` and `code`, includes `display` and any known `designation` entries (synonyms), then lists the OMOP properties. ```json theme={null} { "resourceType": "Parameters", "parameter": [ { "name": "system", "valueUri": "http://snomed.info/sct" }, { "name": "name", "valueString": "OMOP Concepts" }, { "name": "version", "valueString": "latest" }, { "name": "code", "valueCode": "44054006" }, { "name": "display", "valueString": "Type 2 diabetes mellitus" }, { "name": "designation", "part": [ { "name": "language", "valueCode": "en" }, { "name": "value", "valueString": "Diabetes mellitus type 2" } ] }, { "name": "designation", "part": [ { "name": "language", "valueCode": "en" }, { "name": "value", "valueString": "Type II diabetes mellitus" } ] }, { "name": "property", "part": [ { "name": "code", "valueCode": "concept-id" }, { "name": "value", "valueInteger": 201826 } ] }, { "name": "property", "part": [ { "name": "code", "valueCode": "source-concept-code" }, { "name": "value", "valueCoding": { "system": "http://snomed.info/sct", "code": "44054006" } } ] }, { "name": "property", "part": [ { "name": "code", "valueCode": "domain-id" }, { "name": "value", "valueCode": "Condition" } ] }, { "name": "property", "part": [ { "name": "code", "valueCode": "vocabulary-id" }, { "name": "value", "valueCode": "SNOMED" } ] }, { "name": "property", "part": [ { "name": "code", "valueCode": "concept-class-id" }, { "name": "value", "valueCode": "Disorder" } ] }, { "name": "property", "part": [ { "name": "code", "valueCode": "standard-concept" }, { "name": "value", "valueCode": "S" } ] }, { "name": "property", "part": [ { "name": "code", "valueCode": "inactive" }, { "name": "value", "valueBoolean": false } ] }, { "name": "property", "part": [ { "name": "code", "valueCode": "valid-start-date" }, { "name": "value", "valueDate": "2002-01-31" } ] }, { "name": "property", "part": [ { "name": "code", "valueCode": "valid-end-date" }, { "name": "value", "valueDate": "2099-12-31" } ] } ] } ``` ### Response fields | Field | Description | | ------------- | -------------------------------------------------------------------------------------------- | | `system` | Echoes the requested code system URI | | `name` | CodeSystem name (`OMOP Concepts`) | | `version` | Vocabulary version used | | `code` | The looked-up code | | `display` | Primary concept name | | `designation` | Synonyms and alternate names (one entry per synonym) | | `property` | OMOP properties (see [overview](/api-reference/fhir-terminology/overview) for the full list) | ## Phoebe Concept Recommendations (OMOPHub-Exclusive) Request `property=recommended` to include Phoebe-recommended related concepts. These are clinically relevant concepts associated with the looked-up code - a feature unique to OMOPHub. ```bash theme={null} curl "https://fhir.omophub.com/fhir/r4/CodeSystem/\$lookup?\ system=http://snomed.info/sct&code=44054006&property=recommended" \ -H "Authorization: Bearer YOUR_API_KEY" ``` Each recommendation appears as a `property` entry with a `valueCoding` and a `subproperty` describing the relationship: ```json theme={null} { "name": "property", "part": [ { "name": "code", "valueCode": "recommended" }, { "name": "value", "valueCoding": { "system": "http://snomed.info/sct", "code": "80394007", "display": "Hyperglycemia" } }, { "name": "subproperty", "part": [ { "name": "code", "valueCode": "relationship" }, { "name": "value", "valueString": "Has finding" } ] } ] } ``` Recommendations are only returned when explicitly requested via `property=recommended`. Standard lookups without this property have no performance impact. ## Errors | HTTP | Issue Code | Cause | | ---- | ----------- | ---------------------------------------------------------------------- | | 400 | `invalid` | Missing `code` parameter, or missing `system` for type-level `$lookup` | | 404 | `not-found` | Code not found in the specified code system | # FHIR CapabilityStatement and terminology metadata Source: https://docs.omophub.com/api-reference/fhir-terminology/metadata Discover supported FHIR terminology operations, served code systems, and the active OMOP vocabulary release via the CapabilityStatement endpoint. ## Overview Returns a FHIR `CapabilityStatement` describing what operations and resources OMOPHub exposes at a given FHIR version endpoint. This is the first endpoint every conformant FHIR client hits - HAPI FHIR, Firely, Postman collections, and the inspectors built into most FHIR tooling all probe `GET /metadata` before making any other call. ## Request ```bash theme={null} curl "https://fhir.omophub.com/fhir/r4/metadata" \ -H "Authorization: Bearer YOUR_API_KEY" ``` ### Version variants | Path | FHIR version | | ------------------------ | -------------- | | `GET /fhir/metadata` | defaults to R4 | | `GET /fhir/r4/metadata` | R4 (4.0.1) | | `GET /fhir/r4b/metadata` | R4B (4.3.0) | | `GET /fhir/r5/metadata` | R5 (5.0.0) | | `GET /fhir/r6/metadata` | R6 (6.0.0) | The response shape is version-adapted - field names follow the conventions of the requested FHIR version. ## Response ```json theme={null} { "resourceType": "CapabilityStatement", "status": "active", "kind": "instance", "fhirVersion": "4.0.1", "format": ["application/fhir+json", "application/fhir+xml"], "date": "2026-04-13", "publisher": "OMOPHub", "software": { "name": "OMOPHub FHIR Terminology Service", "version": "1.0.0" }, "implementation": { "description": "OMOPHub FHIR Terminology Service - OMOP Standardized Vocabularies", "url": "https://fhir.omophub.com/fhir/r4" }, "rest": [ { "mode": "server", "resource": [ { "type": "CodeSystem", "interaction": [ { "code": "read" }, { "code": "search-type" } ], "operation": [ { "name": "lookup", "definition": "http://hl7.org/fhir/OperationDefinition/CodeSystem-lookup" }, { "name": "validate-code", "definition": "http://hl7.org/fhir/OperationDefinition/CodeSystem-validate-code" }, { "name": "subsumes", "definition": "http://hl7.org/fhir/OperationDefinition/CodeSystem-subsumes" }, { "name": "find-matches", "definition": "http://hl7.org/fhir/OperationDefinition/CodeSystem-find-matches" } ] }, { "type": "ValueSet", "interaction": [ { "code": "search-type" } ], "operation": [ { "name": "expand", "definition": "http://hl7.org/fhir/OperationDefinition/ValueSet-expand" }, { "name": "validate-code", "definition": "http://hl7.org/fhir/OperationDefinition/ValueSet-validate-code" } ] }, { "type": "ConceptMap", "operation": [ { "name": "translate", "definition": "http://hl7.org/fhir/OperationDefinition/ConceptMap-translate" }, { "name": "closure", "definition": "http://hl7.org/fhir/OperationDefinition/ConceptMap-closure" } ] } ] } ] } ``` The advertised `format` array includes `application/fhir+xml` because the server accepts XML requests via content negotiation. Most operations return clean XML; the three Bundle-shaped endpoints (CodeSystem search, ValueSet search, batch) are best-effort in XML and should be called with JSON in practice. See the [XML support matrix](/api-reference/fhir-terminology/overview#xml-support-matrix) in the overview for the per-endpoint breakdown. ## Response headers Every response from the FHIR Terminology Service - `/metadata` included - carries two OMOPHub-specific headers that tell you which OMOP vocabulary release served the request: | Header | Example | Meaning | | ------------------------ | -------- | -------------------------------------------------------------- | | `X-Vocab-Release` | `2025.2` | Human-friendly release identifier | | `X-Vocab-Release-Status` | `active` | Lifecycle status (`ready`, `active`, `deprecated`, `archived`) | Use these headers to confirm which release your client is actually hitting, especially when pinning a specific version via the `X-Vocab-Release` request header or `vocab_release` query parameter. See [Vocabulary Versions](/vocabulary-versions) for the full release history. ## Content negotiation `GET /metadata` respects the same `Accept` header rules as every other FHIR endpoint - JSON is returned by default; XML is served only when the client strictly prefers it. See the [Content Type](/api-reference/fhir-terminology/overview#content-type) section of the overview for the q-value semantics. ## Errors | HTTP | Issue Code | Cause | | ---- | ---------- | -------------------------- | | 401 | `login` | Missing or invalid API key | ## See Also * [Overview](/api-reference/fhir-terminology/overview) - full list of operations, supported vocabularies, and extension properties * [CodeSystem search](/api-reference/fhir-terminology/codesystem-search) - discovery for the supported system URIs advertised in this CapabilityStatement # FHIR R4, R5, and R6 terminology service overview Source: https://docs.omophub.com/api-reference/fhir-terminology/overview Standards-compliant FHIR R4/R5/R6 terminology operations over OMOP vocabularies - $lookup, $validate-code, $translate, $expand, and $subsumes. ## Overview OMOPHub exposes its vocabulary data through a standards-compliant **FHIR Terminology Service** at `/fhir/{r4,r5,r6}/`. Any FHIR-compatible client (HAPI FHIR, Firely, etc.) can query OMOP concepts using standard FHIR operations. This is NOT a full FHIR server - it serves terminology resources only (CodeSystem, ConceptMap, ValueSet). For clinical resources, continue using your FHIR server and point it at OMOPHub for terminology resolution. ## Endpoints | Operation | Path | Description | | -------------------------------------------------------------------------------------- | ----------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------- | | **[Metadata](/api-reference/fhir-terminology/metadata)** | `GET /fhir/{version}/metadata` | CapabilityStatement describing server capabilities | | **[CodeSystem search](/api-reference/fhir-terminology/codesystem-search)** | `GET /fhir/{version}/CodeSystem?url={uri}` | Discover support for a canonical system URI (e.g. `http://loinc.org`). Omit `url` to list all supported CodeSystems | | **[CodeSystem instance read](/api-reference/fhir-terminology/codesystem-read)** | `GET /fhir/{version}/CodeSystem/{id}` | Fetch a single CodeSystem by FHIR id (stub id like `loinc` or release id like `omop-v20250827`) | | **[\$lookup](/api-reference/fhir-terminology/lookup)** | `GET\|POST /fhir/{version}/CodeSystem/$lookup` | Concept details by code | | **[\$validate-code](/api-reference/fhir-terminology/validate-code)** | `GET\|POST /fhir/{version}/CodeSystem/$validate-code` | Check if a code is valid in a CodeSystem | | **[\$translate](/api-reference/fhir-terminology/translate)** | `GET\|POST /fhir/{version}/ConceptMap/$translate` | Translate between vocabularies | | **[\$expand](/api-reference/fhir-terminology/expand)** | `GET\|POST /fhir/{version}/ValueSet/$expand` | Expand a ValueSet to list matching codes | | **[ValueSet search](/api-reference/fhir-terminology/valueset-search)** | `GET /fhir/{version}/ValueSet?url={uri}` | Preflight check for ValueSet support before calling `$expand` / `$validate-code` | | **[ValueSet/\$validate-code](/api-reference/fhir-terminology/valueset-validate-code)** | `GET\|POST /fhir/{version}/ValueSet/$validate-code` | Check if a code is a member of a ValueSet | | **[\$subsumes](/api-reference/fhir-terminology/subsumes)** | `GET\|POST /fhir/{version}/CodeSystem/$subsumes` | Test subsumption between concepts | | **[\$find-matches](/api-reference/fhir-terminology/find-matches)** | `GET\|POST /fhir/{version}/CodeSystem/$find-matches` | Find concepts by property criteria | | **[\$closure](/api-reference/fhir-terminology/closure)** | `POST /fhir/{version}/ConceptMap/$closure` | Compute subsumption closure for a concept set | | **[Batch](/api-reference/fhir-terminology/batch)** | `POST /fhir/{version}/` | Execute multiple GET operations in one request | ### OMOPHub Extensions These operations and properties are OMOPHub-specific and not part of the FHIR Terminology Service specification. | Extension | Path / Parameter | Description | | -------------------------------------------------- | -------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **[\$diff](/api-reference/fhir-terminology/diff)** | `GET\|POST /fhir/{version}/CodeSystem/$diff` | Compare concepts between vocabulary versions | | **Phoebe recommendations** | `$lookup?property=recommended` | Clinically-related concept recommendations from OMOPHub's Phoebe concept-set recommendation engine. See [\$lookup](/api-reference/fhir-terminology/lookup#phoebe-concept-recommendations-omophub-exclusive) | | **`target-table` product property** | `$translate` response `product[]` | Each `$translate` match includes the OMOP CDM destination table (`condition_occurrence`, `drug_exposure`, `measurement`, ...) - saves a separate domain lookup for ETL pipelines. See [\$translate](/api-reference/fhir-terminology/translate) | | **Per-vocabulary CodeSystem stubs** | `GET /CodeSystem/{stub-id}` | Thin FHIR-conformant CodeSystem stubs (one per canonical system URI) for clients that expect per-vocab discovery rather than OMOPHub's unified omnibus CodeSystem. See [CodeSystem search](/api-reference/fhir-terminology/codesystem-search) | ## FHIR Versions All four FHIR versions are supported from the same endpoint: * `/fhir/r4/` - FHIR R4 (4.0.1) - US Cures Act, EHDS mandate * `/fhir/r4b/` - FHIR R4B (4.3.0) * `/fhir/r5/` - FHIR R5 (5.0.0) * `/fhir/r6/` - FHIR R6 (6.0.0) * `/fhir/` - Defaults to R4 ## Authentication OMOPHub supports two authentication methods for FHIR operations. Both use the same API key (`oh_xxx`) from your [dashboard](https://dashboard.omophub.com/api-keys) and share the same rate-limit and metering pool. ### Bearer token (default) Pass the API key directly in the `Authorization` header: ```bash theme={null} curl "https://fhir.omophub.com/fhir/r4/metadata" \ -H "Authorization: Bearer oh_your_api_key" ``` This is what curl, Postman, the OMOPHub SDKs, and any FHIR client that lets you configure a static bearer token should use. ### OAuth2 `client_credentials` (for Spring-based clients) FHIR clients built on Spring Security OAuth2 - HAPI FHIR JPA Starter, EHRbase, most Java/Kotlin Spring Boot stacks - expect to obtain a Bearer token from an OAuth2 token endpoint. OMOPHub exposes a minimal RFC 6749 `client_credentials` shim at `https://fhir.omophub.com/oauth2/token`: ```bash theme={null} curl -X POST "https://fhir.omophub.com/oauth2/token" \ -H "Content-Type: application/x-www-form-urlencoded" \ -d "grant_type=client_credentials&client_id=oh_your_api_key" ``` The response echoes the API key as the `access_token` (`{"access_token": "oh_...", "token_type": "Bearer", "expires_in": 31536000}`). Both `client_secret_basic` (HTTP Basic auth header, Spring's default) and `client_secret_post` (form body) authentication methods are accepted. `client_secret` is **not validated** - the `client_id` is the sole credential. Supply any non-empty placeholder for `client_secret` to satisfy Spring's config schema. For full walkthroughs, see the [HAPI FHIR](/guides/integration/hapi-fhir) and [EHRbase / openEHR](/guides/integration/ehrbase-openehr) integration guides. ## Content Type OMOPHub responds with `Content-Type: application/fhir+json` by default and serves FHIR XML on request. Content negotiation follows RFC 7231 §5.3.2 with two OMOPHub-specific rules: * **JSON wins on ties.** When a client advertises both JSON and XML at the same q-value (HAPI FHIR's default client sends `Accept: application/fhir+xml;q=1.0, application/fhir+json;q=1.0, ...`), OMOPHub returns JSON. XML is served only when XML is **strictly preferred** - i.e. the client's highest-scoring XML media type has a higher q-value than its highest-scoring JSON media type. * **Wildcards are honored.** `*/*` and `application/*` count as matches at their advertised q-value, so a client sending `Accept: */*` receives JSON (the default), and a client sending `Accept: application/*, application/fhir+xml;q=0.5` also receives JSON (the wildcard matches JSON at q=1.0, which beats the explicit XML at q=0.5). ### Requesting XML explicitly Send `Accept: application/fhir+xml` (with no JSON at equal or higher q-value): ```bash theme={null} curl "https://fhir.omophub.com/fhir/r4/CodeSystem/\$lookup?\ system=http://snomed.info/sct&code=44054006" \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Accept: application/fhir+xml" ``` Response (abbreviated): ```xml theme={null} ``` ### XML support matrix OMOPHub's XML serializer is **clean for single-resource responses** and **best-effort for Bundle-shaped responses**. Request JSON for the Bundle-shaped endpoints unless you have a specific reason to need XML. | Endpoint | Response shape | XML status | | -------------------------------------------------------- | --------------------- | ------------------------ | | `GET /metadata` | `CapabilityStatement` | ✅ Clean XML | | `GET /CodeSystem/$lookup` | `Parameters` | ✅ Clean XML | | `GET /CodeSystem/$validate-code` | `Parameters` | ✅ Clean XML | | `GET /CodeSystem/$subsumes` | `Parameters` | ✅ Clean XML | | `GET /CodeSystem/$find-matches` | `Parameters` | ✅ Clean XML | | `GET /CodeSystem/$diff` | `Parameters` | ✅ Clean XML | | `GET /ConceptMap/$translate` | `Parameters` | ✅ Clean XML | | `POST /ConceptMap/$closure` | `ConceptMap` | ✅ Clean XML | | `GET /ValueSet/$expand` | `ValueSet` | ✅ Clean XML | | `GET /ValueSet/$validate-code` | `Parameters` | ✅ Clean XML | | `GET /CodeSystem/{id}` (instance read) | `CodeSystem` | ✅ Clean XML | | Handler-emitted 4xx / 5xx errors | `OperationOutcome` | ✅ Clean XML | | Pre-auth errors (401 missing API key) | `OperationOutcome` | ⚠️ JSON-only (see below) | | `GET /CodeSystem` (search-type, with or without `?url=`) | `Bundle` | ⚠️ Request JSON | | `GET /ValueSet?url=...` (search-type) | `Bundle` | ⚠️ Request JSON | | `POST /` (batch Bundle) | `Bundle` | ⚠️ Request JSON | **Bundle-shaped responses should be requested as JSON.** The XML serializer produces malformed output for nested resources inside a `Bundle.entry[].resource` (the inner resource's type is not used as the wrapping element name). The three Bundle-shaped endpoints above are all discovery / batch operations - FHIR clients calling them from code already prefer JSON, so this is not a common path in practice. Request JSON for those endpoints explicitly, or let content negotiation pick JSON on a tie (the default HAPI / Firely / EHRbase client behavior). **Pre-auth error responses come back as JSON even when XML was requested.** When a request fails authentication (401, missing or invalid API key) or exhausts the monthly quota (429), the FHIR-scoped error middleware rewrites the response into an `OperationOutcome` resource but emits it as JSON regardless of `Accept`. The response body is still valid FHIR OperationOutcome - HAPI, Firely, and most FHIR clients parse it correctly - it's only the content-type header and wire format that don't honor the XML preference. Handler-emitted errors (404 unknown code, 400 bad parameter, 403 restricted vocab, 500 internal) *do* respect the Accept header and return XML. Errors on any `/fhir/*` path - including auth failures, missing routes, and unsupported operations - are returned as FHIR `OperationOutcome` resources in whichever format the client negotiated (JSON by default, XML when requested). The generic REST JSON envelope (`{"success": false, "error": {...}}`) is never used on FHIR routes. ## CodeSystem URLs OMOPHub serves CodeSystem resources at two equivalent URL shapes: 1. **Canonical per-vocabulary URIs** - e.g. `http://snomed.info/sct`, `http://loinc.org`, `http://www.nlm.nih.gov/research/umls/rxnorm`. These resolve to per-vocabulary stubs (`content: "not-present"`) and are what FHIR clients like HAPI and EHRbase use for discovery. Every URI in the [Supported Vocabularies](#supported-vocabularies) table is a valid CodeSystem URL. 2. **Unified OMOP omnibus URL** - `https://fhir-terminology.ohdsi.org`. A single CodeSystem covering all 130+ OMOP vocabularies by `concept_id`. This is the OHDSI community canonical URL; use it when you want to address concepts by their OMOP internal ID rather than by vocabulary-specific code. Both shapes resolve to the same underlying concept data. Operations like `$lookup` and `$validate-code` dispatch on the `system` / `url` parameter, so you can use whichever matches your client's expectations. Codes resolved via OMOPHub are interchangeable with codes from any other FHIR server serving OMOP vocabularies. Discover the full list of supported URLs with [CodeSystem search](/api-reference/fhir-terminology/codesystem-search) or the [metadata endpoint](/api-reference/fhir-terminology/metadata). ## Supported Vocabularies Query [CodeSystem search](/api-reference/fhir-terminology/codesystem-search) without a `url` parameter for the live list, or see the current registry below. Each row is a valid **CodeSystem URL** (usable in `$lookup`, `$validate-code`, `$translate`, etc.) with a corresponding **stub id** (usable in `GET /CodeSystem/{id}`). | FHIR System URI | Stub `id` | OMOP vocabulary\_id | Description | | --------------------------------------------- | ----------------- | ------------------- | ----------------------------------- | | `https://fhir-terminology.ohdsi.org` | `omop-v{release}` | (unified) | All OMOP concepts by concept\_id | | `http://snomed.info/sct` | `snomed` | SNOMED | SNOMED CT (International Edition) | | `http://loinc.org` | `loinc` | LOINC | Lab tests, measurements | | `http://www.nlm.nih.gov/research/umls/rxnorm` | `rxnorm` | RxNorm | Drugs (US) | | `http://hl7.org/fhir/sid/icd-10` | `icd-10` | ICD10 | ICD-10 (WHO) | | `http://hl7.org/fhir/sid/icd-10-cm` | `icd-10-cm` | ICD10CM | ICD-10-CM (US) | | `http://hl7.org/fhir/sid/icd-10-cn` | `icd-10-cn` | ICD10CN | ICD-10 Chinese Edition | | `http://hl7.org/fhir/sid/icd-10-gm` | `icd-10-gm` | ICD10GM | ICD-10 German Modification | | `http://hl7.org/fhir/sid/icd-10-pcs` | `icd-10-pcs` | ICD10PCS | ICD-10 Procedure Coding System | | `http://hl7.org/fhir/sid/icd-9-cm` | `icd-9-cm` | ICD9CM | ICD-9-CM (legacy) | | `http://hl7.org/fhir/sid/icd-9` | `icd-9-proc` | ICD9Proc | ICD-9 Procedure Codes | | `http://hl7.org/fhir/sid/icd-9-cn` | `icd-9-proc-cn` | ICD9ProcCN | ICD-9 Chinese Edition Procedures | | `http://hl7.org/fhir/sid/icd-o-3` | `icd-o-3` | ICDO3 | Oncology morphology | | `http://hl7.org/fhir/sid/cvx` | `cvx` | CVX | Vaccines | | `http://hl7.org/fhir/sid/ndc` | `ndc` | NDC | Drug products (US) | | `http://unitsofmeasure.org` | `ucum` | UCUM | Units of measure | | `urn:iso:std:iso:11073:10101` | `mdc` | MDC | ISO/IEEE 11073 Medical Device Codes | | `http://www.whocc.no/atc` | `atc` | ATC | Drug classification (WHO) | | `http://fdasis.nlm.nih.gov` | `unii` | UNII | Substance identifiers (FDA) | | `http://va.gov/terminology/medrt` | `med-rt` | MED-RT | Medication reference (VA) | | `http://www.nlm.nih.gov/research/umls/hcpcs` | `hcpcs` | HCPCS | Procedures (US outpatient) | ## Three Access Patterns **1. By vocabulary-specific code (type-level, most common):** ``` GET /fhir/r4/CodeSystem/$lookup?system=http://snomed.info/sct&code=44054006 ``` Pattern 1 is what ETL developers and FHIR clients need most of the time - they have FHIR-native codes from source systems and want to resolve them against the canonical vocabulary URI. **2. By OMOP concept\_id (via the unified omnibus URL):** ``` GET /fhir/r4/CodeSystem/$lookup?system=https://fhir-terminology.ohdsi.org&code=201826 ``` Pattern 2 is useful when you already have an OMOP concept\_id (from a previous `$lookup`, from an ETL pipeline, or from a `$translate` result) and want to look it up without maintaining a vocabulary-ID round-trip. **3. By CodeSystem instance ID (instance-level):** ``` GET /fhir/r4/CodeSystem/loinc/$lookup?code=2951-2 GET /fhir/r4/CodeSystem/omop-v20250827/$lookup?code=201826 ``` Pattern 3 uses the CodeSystem's FHIR `id` from [CodeSystem search](/api-reference/fhir-terminology/codesystem-search) - either a per-vocab stub id (`loinc`, `snomed`, ...) or a release-specific unified id (`omop-v20250827`). This is what HAPI FHIR emits after its discovery phase, and what you get back from [CodeSystem instance read](/api-reference/fhir-terminology/codesystem-read). ## Designations (Synonyms) `$lookup` responses include `designation` entries for concept synonyms. Each designation has a `language` code and a `value` string. These are the alternate names from the OMOP `concept_synonym` table. ## Property Naming Standard OMOP properties use kebab-case names for interoperability. Relationship properties use their OMOP relationship names (e.g., `Maps to`, `Is a`). ### Standard OMOP properties | Property | Type | Description | | --------------------- | ------- | ---------------------------------------------------------- | | `concept-id` | integer | OMOP concept\_id | | `source-concept-code` | Coding | Original source code as FHIR Coding (system URI + code) | | `concept-name` | string | OMOP concept\_name | | `domain-id` | code | OMOP domain (Condition, Drug, Measurement, etc.) | | `vocabulary-id` | code | OMOP vocabulary\_id (SNOMED, ICD10CM, LOINC, etc.) | | `concept-class-id` | code | OMOP concept\_class\_id (Clinical Finding, Disorder, etc.) | | `standard-concept` | code | S=Standard, C=Classification, NS=Non-standard | | `inactive` | boolean | Whether concept is inactive | | `valid-start-date` | date | Date concept became valid | | `valid-end-date` | date | Date concept becomes invalid | | `invalid-reason` | code | D=Deleted, U=Updated (omitted if valid) | ### Relationship properties | Property | Type | Description | | ------------- | ------ | ------------------------ | | `Maps to` | Coding | Standard concept mapping | | `Mapped from` | Coding | Inverse of Maps to | | `Is a` | Coding | Parent/supertype | | `Subsumes` | Coding | Child/subtype | Use the `property` query parameter on `$lookup` to request specific properties (e.g. `property=concept-id&property=domain-id`). When omitted, all standard properties are returned. ## Metering FHIR operations count against the same API-call quota as REST API calls - there is no separate FHIR pool. Two exceptions to the "1 operation = 1 call" rule: * **[Batch Bundles](/api-reference/fhir-terminology/batch)** are metered per entry. A 50-entry batch counts as 50 calls. * **[Resolver batch](/api-reference/fhir/resolve-batch)** (`POST /v1/fhir/resolve/batch`) is also metered per coding, matching FHIR batch behavior. Rate-limit headers (`X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset`) are included on every response. ## See Also * [FHIR Integration guide](/guides/integration/fhir-integration) - comprehensive walkthrough of the FHIR Resolver and Terminology Service for new integrations * [FHIR Concept Resolver](/api-reference/fhir/resolve) - OMOPHub-native JSON for ETL pipelines, with `target_table` + mapping-quality signals * [EHRbase / openEHR Integration](/guides/integration/ehrbase-openehr) - use OMOPHub for openEHR template terminology validation * [HAPI FHIR Integration](/guides/integration/hapi-fhir) - config for HAPI JPA Starter (via reverse proxy) and custom Spring Boot HAPI builds * [EHR Integration](/guides/integration/ehr-integration) - SMART on FHIR, CDS Hooks, and point-of-care patterns # FHIR CodeSystem/$subsumes for concept hierarchy Source: https://docs.omophub.com/api-reference/fhir-terminology/subsumes Test subsumption relationships between two OMOP concepts with FHIR $subsumes - determine parent, child, equivalent, or not-subsumed status. ## Overview Tests whether concept A subsumes (is an ancestor of) concept B, or vice versa. Used for hierarchy-aware code validation and clinical decision support. Supports both type-level and instance-level invocation. ## Request ```bash theme={null} # Does "Diabetes mellitus" subsume "Type 2 diabetes mellitus"? curl "https://fhir.omophub.com/fhir/r4/CodeSystem/\$subsumes?\ codeA=73211009&codeB=44054006&system=http://snomed.info/sct" \ -H "Authorization: Bearer YOUR_API_KEY" ``` ### By OMOP concept ID ```bash theme={null} curl "https://fhir.omophub.com/fhir/r4/CodeSystem/\$subsumes?\ codeA=201820&codeB=201826&system=https://fhir-terminology.ohdsi.org" \ -H "Authorization: Bearer YOUR_API_KEY" ``` ### Instance-level ```bash theme={null} curl "https://fhir.omophub.com/fhir/r4/CodeSystem/omop-v20260227/\$subsumes?\ codeA=201820&codeB=201826" \ -H "Authorization: Bearer YOUR_API_KEY" ``` ## Parameters | Parameter | Type | Required | Description | | --------- | ---- | ---------------- | ------------------- | | `codeA` | code | Yes | First concept code | | `codeB` | code | Yes | Second concept code | | `system` | uri | Yes (type-level) | Code system URI | ## Response ```json theme={null} { "resourceType": "Parameters", "parameter": [ { "name": "outcome", "valueCode": "subsumes" } ] } ``` ## Possible Outcomes | Outcome | Meaning | | -------------- | -------------------------------------------- | | `equivalent` | A and B are the same concept | | `subsumes` | A is an ancestor of B (A subsumes B) | | `subsumed-by` | A is a descendant of B (B subsumes A) | | `not-subsumed` | No hierarchical relationship between A and B | ## Examples ```bash theme={null} # A subsumes B (Diabetes mellitus → Type 2 DM) # → outcome: subsumes # Reverse (Type 2 DM → Diabetes mellitus) # → outcome: subsumed-by # Same concept # → outcome: equivalent # Unrelated concepts (Type 2 DM vs Myocardial infarction) # → outcome: not-subsumed ``` ## Errors | HTTP | Issue Code | Cause | | ---- | ----------- | ----------------------------------------------- | | 400 | `invalid` | Missing `codeA`, `codeB`, or `system` parameter | | 404 | `not-found` | Code not found in the specified code system | # FHIR ConceptMap/$translate across OMOP vocabularies Source: https://docs.omophub.com/api-reference/fhir-terminology/translate Translate codes between vocabularies with FHIR $translate - SNOMED to ICD-10, ICD-9 to SNOMED, RxNorm to ATC, and other OMOP standard mappings. ## Overview Translates codes between vocabularies using OMOP `Maps to` relationships. Supports both **forward** (source code to OMOP standard) and **reverse** (OMOP concept to source code) translation. Key R4/R5 difference: R4 responses use `equivalence`, R5/R6 use `relationship`. ## Forward Translation Translate a source code to its OMOP standard equivalent: ```bash theme={null} curl "https://fhir.omophub.com/fhir/r4/ConceptMap/\$translate?\ sourceCode=E11.9&\ system=http://hl7.org/fhir/sid/icd-10-cm&\ targetSystem=https://fhir-terminology.ohdsi.org" \ -H "Authorization: Bearer YOUR_API_KEY" ``` ### POST with sourceCoding ```bash theme={null} curl -X POST "https://fhir.omophub.com/fhir/r4/ConceptMap/\$translate" \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/fhir+json" \ -d '{ "resourceType": "Parameters", "parameter": [ { "name": "sourceCoding", "valueCoding": { "system": "http://hl7.org/fhir/sid/icd-10-cm", "code": "E11.9" } }, { "name": "targetSystem", "valueUri": "https://fhir-terminology.ohdsi.org" } ] }' ``` ## Reverse Translation Look up the source code for an OMOP concept: ```bash theme={null} curl "https://fhir.omophub.com/fhir/r4/ConceptMap/\$translate?\ targetCode=201826&\ system=https://fhir-terminology.ohdsi.org" \ -H "Authorization: Bearer YOUR_API_KEY" ``` ## Parameters ### Forward | Parameter | Type | Required | Description | | -------------- | ------ | -------- | ------------------------------------------------------------------------------- | | `sourceCode` | code | Yes\* | Source code to translate | | `sourceCoding` | Coding | Yes\* | Alternative: Coding with system + code | | `system` | uri | Yes\* | Source code system URI (required with `sourceCode`, included in `sourceCoding`) | | `targetSystem` | uri | No | Target code system (defaults to OMOP unified) | \*Provide either `sourceCode` + `system` or `sourceCoding`. ### Reverse | Parameter | Type | Required | Description | | -------------- | ------ | -------- | ----------------------------------------------------------------------------------------------------------------------------------------- | | `targetCode` | code | Yes\* | OMOP concept\_id to reverse-lookup | | `targetCoding` | Coding | Yes\* | Alternative: Coding with system + code | | `system` | uri | No | The code system of the target concept (use `https://fhir-terminology.ohdsi.org` for OMOP unified, or a vocabulary-specific URI to filter) | \*Provide either `targetCode` or `targetCoding`. ## Response (R4) ```json theme={null} { "resourceType": "Parameters", "parameter": [ { "name": "result", "valueBoolean": true }, { "name": "match", "part": [ { "name": "equivalence", "valueCode": "equivalent" }, { "name": "concept", "valueCoding": { "system": "https://fhir-terminology.ohdsi.org", "code": "201826", "display": "Type 2 diabetes mellitus" } }, { "name": "product", "part": [ { "name": "property", "valueUri": "http://omophub.com/fhir/property/domain-id" }, { "name": "value", "valueString": "Condition" } ] }, { "name": "product", "part": [ { "name": "property", "valueUri": "http://omophub.com/fhir/property/target-table" }, { "name": "value", "valueString": "condition_occurrence" } ] } ] } ] } ``` The `target-table` product property is OMOPHub-exclusive. It tells ETL developers exactly which OMOP CDM table the translated concept belongs to. ## R5/R6 Difference On the `/fhir/r5/` and `/fhir/r6/` endpoints, `equivalence` is renamed to `relationship`: ```json theme={null} { "name": "relationship", "valueCode": "equivalent" } ``` ## Equivalence Mapping | OMOP Relationship | FHIR Equivalence | | ----------------- | ---------------- | | Maps to | `equivalent` | | Maps to value | `equivalent` | | Is a | `wider` | | Mapped from | `narrower` | | (no mapping) | `unmatched` | # FHIR CodeSystem/$validate-code for OMOP concepts Source: https://docs.omophub.com/api-reference/fhir-terminology/validate-code Check whether a code exists in a FHIR CodeSystem backed by OMOP vocabularies - validates SNOMED, LOINC, RxNorm, ICD-10, and HCPCS codes on the wire. ## Overview Validates whether a code is valid in a given code system. Returns `true`/`false` with the display name. **Important:** Per the FHIR spec, `$validate-code` uses the `url` parameter (not `system`). This differs from `$lookup` which uses `system`. ## Request ```bash theme={null} curl "https://fhir.omophub.com/fhir/r4/CodeSystem/\$validate-code?\ url=http://snomed.info/sct&code=44054006" \ -H "Authorization: Bearer YOUR_API_KEY" ``` With display verification: ```bash theme={null} curl "https://fhir.omophub.com/fhir/r4/CodeSystem/\$validate-code?\ url=http://snomed.info/sct&code=44054006&\ display=Type%202%20diabetes%20mellitus" \ -H "Authorization: Bearer YOUR_API_KEY" ``` ## Parameters | Parameter | Type | Required | Description | | --------- | ------ | ---------------- | ------------------------------------------------ | | `url` | uri | Yes (type-level) | FHIR code system URI (NOTE: `url`, not `system`) | | `code` | code | Yes | The code to validate | | `display` | string | No | Expected display text to verify | ## Response (valid code) ```json theme={null} { "resourceType": "Parameters", "parameter": [ { "name": "result", "valueBoolean": true }, { "name": "display", "valueString": "Type 2 diabetes mellitus" } ] } ``` ## Response (display mismatch) ```json theme={null} { "resourceType": "Parameters", "parameter": [ { "name": "result", "valueBoolean": true }, { "name": "display", "valueString": "Type 2 diabetes mellitus" }, { "name": "message", "valueString": "Display mismatch: provided 'Diabetes type 2', actual 'Type 2 diabetes mellitus'." } ] } ``` ## Response (invalid code) ```json theme={null} { "resourceType": "Parameters", "parameter": [ { "name": "result", "valueBoolean": false }, { "name": "message", "valueString": "Code '999999' not found in SNOMED" } ] } ``` # FHIR ValueSet search for OMOP concept sets Source: https://docs.omophub.com/api-reference/fhir-terminology/valueset-search Discover whether a FHIR ValueSet is supported by OMOPHub before calling $expand or $validate-code - catalog lookup for common clinical value sets. ## Overview Search-type query on `ValueSet`. FHIR clients - notably HAPI FHIR's `RemoteTerminologyServiceValidationSupport` and EHRbase's `FhirTerminologyValidation` - call this endpoint as a preflight check: "does this server support the ValueSet I'm about to query?". If the returned `searchset` Bundle contains a matching entry, the client proceeds to [$expand](/api-reference/fhir-terminology/expand) or [ValueSet/$validate-code](/api-reference/fhir-terminology/valueset-validate-code). If the Bundle is empty, the client skips OMOPHub entirely for that ValueSet. OMOPHub recognizes the same implicit ValueSet URL patterns that `$expand` handles - no stored ValueSet definitions required. ## Request ```bash theme={null} curl "https://fhir.omophub.com/fhir/r4/ValueSet?\ url=http://snomed.info/sct?fhir_vs=isa/73211009" \ -H "Authorization: Bearer YOUR_API_KEY" ``` ## Parameters | Parameter | Type | Required | Description | | --------- | ---- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------ | | `url` | uri | No | Implicit ValueSet URL to check support for. Omit to request an empty Bundle (OMOPHub does not enumerate every possible implicit ValueSet). | ### Supported URL patterns | Pattern | Meaning | | ----------------------------- | ----------------------------------------------- | | `{system}?fhir_vs` | All concepts from a code system | | `{system}?fhir_vs=isa/{code}` | Descendants of a specific concept (is-a filter) | Where `{system}` is any of the [supported FHIR system URIs](/api-reference/fhir-terminology/overview#supported-vocabularies), and `{code}` is a code within that system. ## Response (supported) ```json theme={null} { "resourceType": "Bundle", "type": "searchset", "total": 1, "entry": [ { "fullUrl": "https://fhir.omophub.com/fhir/r4/ValueSet/http---snomed-info-sct-fhir-vs-isa-73211009", "resource": { "resourceType": "ValueSet", "id": "http---snomed-info-sct-fhir-vs-isa-73211009", "url": "http://snomed.info/sct?fhir_vs=isa/73211009", "status": "active", "experimental": false, "name": "OMOPHub implicit ValueSet: http://snomed.info/sct?fhir_vs=isa/73211009", "description": "Implicit ValueSet backed by the OMOPHub FHIR Terminology Service. Call $expand on this ValueSet to enumerate its members." } } ] } ``` The returned resource is a metadata stub. It does **not** include the expansion - callers must invoke [\$expand](/api-reference/fhir-terminology/expand) to enumerate members. ## Response (unknown URL) ```json theme={null} { "resourceType": "Bundle", "type": "searchset", "total": 0 } ``` An empty Bundle (not a 404) is the FHIR convention for "no matches". ## Double percent-encoding tolerance Spring-based clients (HAPI, EHRbase) sometimes encode the `url` parameter twice - once for the template or code (`terminology://fhir.hl7.org/ValueSet?url=http%3A%2F%2F...`) and again when building the outbound HTTP request. OMOPHub detects the second encoding layer automatically and decodes it before parsing the implicit URL. Single-encoded URLs work too. ## Errors Only malformed requests return errors - unknown URLs are handled via the empty-Bundle response. | HTTP | Issue Code | Cause | | ---- | ---------- | -------------------------- | | 401 | `login` | Missing or invalid API key | ## See Also * [ValueSet/\$expand](/api-reference/fhir-terminology/expand) - enumerate the members of a supported ValueSet * [ValueSet/\$validate-code](/api-reference/fhir-terminology/valueset-validate-code) - check membership for a specific code * [EHRbase / openEHR Integration](/guides/integration/ehrbase-openehr) - walkthrough of the full composition-validation flow that drives this endpoint # FHIR ValueSet/$validate-code membership check Source: https://docs.omophub.com/api-reference/fhir-terminology/valueset-validate-code Check whether an OMOP code is a member of a specified FHIR ValueSet - validate Observations, Conditions, and Medications against expected sets. ## Overview Validates whether a code is a member of an implicit ValueSet. EHRbase uses this during composition validation to check if a submitted code falls within the allowed set for a template field. Supports the same ValueSet URL patterns as `$expand`. FHIR clients typically call [ValueSet search](/api-reference/fhir-terminology/valueset-search) first as a preflight check to confirm the server supports the target ValueSet, then call `$validate-code` (or `$expand`) for the actual membership test. The two endpoints are independent - you can call `$validate-code` directly without the preflight if you already know the URL is supported. ## Supported Patterns ### All codes in a code system ```bash theme={null} curl "https://fhir.omophub.com/fhir/r4/ValueSet/\$validate-code?\ url=http://snomed.info/sct?fhir_vs&\ system=http://snomed.info/sct&code=44054006" \ -H "Authorization: Bearer YOUR_API_KEY" ``` Is SNOMED code 44054006 valid? (Yes, if it exists in the SNOMED vocabulary.) ### Descendants of a concept (is-a filter) ```bash theme={null} curl "https://fhir.omophub.com/fhir/r4/ValueSet/\$validate-code?\ url=http://snomed.info/sct?fhir_vs=isa/73211009&\ system=http://snomed.info/sct&code=44054006" \ -H "Authorization: Bearer YOUR_API_KEY" ``` Is "Type 2 diabetes mellitus" (44054006) a descendant of "Diabetes mellitus" (73211009)? (Yes.) ## Parameters | Parameter | Type | Required | Description | | --------- | ------ | -------- | --------------------------------------------------------------------------------------------------------------- | | `url` | uri | Yes | Implicit ValueSet URL (e.g., `http://snomed.info/sct?fhir_vs` or `http://snomed.info/sct?fhir_vs=isa/73211009`) | | `system` | uri | No | Code system URI of the code being validated (defaults to the ValueSet system) | | `code` | code | Yes | The code to validate | | `display` | string | No | Expected display text to verify | ## Response (valid code) ```json theme={null} { "resourceType": "Parameters", "parameter": [ { "name": "result", "valueBoolean": true }, { "name": "display", "valueString": "Type 2 diabetes mellitus" } ] } ``` ## Response (not a member) ```json theme={null} { "resourceType": "Parameters", "parameter": [ { "name": "result", "valueBoolean": false }, { "name": "message", "valueString": "Code '22298006' is not a member of the ValueSet (not a descendant of '73211009')." } ] } ``` ## Response (display mismatch) ```json theme={null} { "resourceType": "Parameters", "parameter": [ { "name": "result", "valueBoolean": true }, { "name": "display", "valueString": "Type 2 diabetes mellitus" }, { "name": "message", "valueString": "Display mismatch: provided 'Diabetes type 2', actual 'Type 2 diabetes mellitus'." } ] } ``` ## Errors | HTTP | Issue Code | Cause | | ---- | ----------- | ------------------------------------------------------------------------------- | | 400 | `invalid` | Missing `url` or `code` parameter, invalid ValueSet URL, or unknown code system | | 404 | `not-found` | ValueSet root concept not found | # Resolve FHIR Coding Source: https://docs.omophub.com/api-reference/fhir/resolve POST /fhir/resolve ## Overview Translate a single FHIR `Coding` (system URI + code) into a fully resolved OMOP concept chain: source concept, standard concept, target CDM table, optional mapping quality signal, and optional Phoebe recommendations. The resolver chains existing OMOPHub primitives - `get_concept_by_code`, `mappings`, `semantic_search`, `validate`, and `recommended` - behind a single composite call. If the structured code lookup fails (or only `display` text is provided), the resolver falls back to semantic search. Use this endpoint when you are building a FHIR-to-OMOP ETL pipeline and need to determine which OMOP CDM table a FHIR resource should land in based on its coded fields. ## Request Body FHIR code system URI, e.g. `http://snomed.info/sct`, `http://loinc.org`, or `http://hl7.org/fhir/sid/icd-10-cm`. Required unless `vocabulary_id` or `display` is provided. The code value from the FHIR `Coding`. Required when `system` or `vocabulary_id` is provided. Optional human-readable display text from the FHIR `Coding`. Used as the semantic search fallback when structured lookup fails, or as the primary input for text-only `CodeableConcept` resolution. Direct OMOP `vocabulary_id` (e.g. `SNOMED`, `ICD10CM`). Alternative to `system` for callers who already know the OMOP vocabulary and want to skip URI resolution. FHIR resource type that carried this coding (e.g. `Condition`, `Observation`, `MedicationStatement`). Used to validate domain alignment and to filter the semantic search fallback. Supported values: `Condition`, `Observation`, `MedicationStatement`, `MedicationRequest`, `MedicationAdministration`, `Immunization`, `Procedure`, `AllergyIntolerance`, `DiagnosticReport`. Include Phoebe-recommended related concepts in the response. Maximum Phoebe recommendations to return (1–20). Include a `mapping_quality` signal (`high`, `medium`, `low`, or `manual_review`) on the resolution. At least one of (`system` + `code`), (`vocabulary_id` + `code`), or `display` must be provided. ```bash cURL (SNOMED direct) theme={null} curl -X POST "https://api.omophub.com/v1/fhir/resolve" \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "system": "http://snomed.info/sct", "code": "44054006", "resource_type": "Condition" }' ``` ```bash cURL (ICD-10-CM mapped) theme={null} curl -X POST "https://api.omophub.com/v1/fhir/resolve" \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "system": "http://hl7.org/fhir/sid/icd-10-cm", "code": "E11.9", "include_quality": true }' ``` ```bash cURL (text-only semantic fallback) theme={null} curl -X POST "https://api.omophub.com/v1/fhir/resolve" \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "display": "Blood Sugar", "resource_type": "Observation" }' ``` ```bash cURL (with Phoebe recommendations) theme={null} curl -X POST "https://api.omophub.com/v1/fhir/resolve" \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "system": "http://snomed.info/sct", "code": "44054006", "include_recommendations": true, "recommendations_limit": 5 }' ``` ```python Python theme={null} import requests resp = requests.post( "https://api.omophub.com/v1/fhir/resolve", headers={"Authorization": "Bearer YOUR_API_KEY"}, json={ "system": "http://snomed.info/sct", "code": "44054006", "resource_type": "Condition", }, ) data = resp.json()["data"] print(data["resolution"]["standard_concept"]["concept_id"]) # 201826 print(data["resolution"]["target_table"]) # condition_occurrence ``` ```json Direct SNOMED resolution theme={null} { "success": true, "data": { "input": { "system": "http://snomed.info/sct", "code": "44054006", "resource_type": "Condition" }, "resolution": { "vocabulary_id": "SNOMED", "source_concept": { "concept_id": 201826, "concept_name": "Type 2 diabetes mellitus", "concept_code": "44054006", "vocabulary_id": "SNOMED", "domain_id": "Condition", "concept_class_id": "Clinical Finding", "standard_concept": "S" }, "standard_concept": { "concept_id": 201826, "concept_name": "Type 2 diabetes mellitus", "concept_code": "44054006", "vocabulary_id": "SNOMED", "domain_id": "Condition", "concept_class_id": "Clinical Finding", "standard_concept": "S" }, "mapping_type": "direct", "target_table": "condition_occurrence", "domain_resource_alignment": "aligned" } }, "meta": { "request_id": "req_fhir_resolve_123", "timestamp": "2026-04-09T10:00:00Z", "vocab_release": "2025.2" } } ``` ```json ICD-10-CM mapped to SNOMED theme={null} { "success": true, "data": { "input": { "system": "http://hl7.org/fhir/sid/icd-10-cm", "code": "E11.9" }, "resolution": { "vocabulary_id": "ICD10CM", "source_concept": { "concept_id": 45576876, "concept_name": "Type 2 diabetes mellitus without complications", "concept_code": "E11.9", "vocabulary_id": "ICD10CM", "domain_id": "Condition", "concept_class_id": "5-char billing code", "standard_concept": null }, "standard_concept": { "concept_id": 201826, "concept_name": "Type 2 diabetes mellitus", "concept_code": "44054006", "vocabulary_id": "SNOMED", "domain_id": "Condition", "concept_class_id": "Clinical Finding", "standard_concept": "S" }, "mapping_type": "mapped", "relationship_id": "Maps to", "target_table": "condition_occurrence", "domain_resource_alignment": "not_checked", "mapping_quality": "high" } } } ``` ```json Semantic search fallback theme={null} { "success": true, "data": { "input": { "display": "Blood Sugar", "resource_type": "Observation" }, "resolution": { "vocabulary_id": null, "source_concept": { "concept_id": 3004501, "concept_name": "Glucose [Mass/volume] in Blood", "concept_code": "2339-0", "vocabulary_id": "LOINC", "domain_id": "Measurement", "concept_class_id": "Lab Test", "standard_concept": "S" }, "standard_concept": { "concept_id": 3004501, "concept_name": "Glucose [Mass/volume] in Blood", "concept_code": "2339-0", "vocabulary_id": "LOINC", "domain_id": "Measurement", "concept_class_id": "Lab Test", "standard_concept": "S" }, "mapping_type": "semantic_match", "similarity_score": 0.91, "target_table": "measurement", "domain_resource_alignment": "aligned", "quality_note": "Resolved via semantic search - verify concept match before production use" } } } ``` ```json With Phoebe recommendations theme={null} { "success": true, "data": { "input": { "system": "http://snomed.info/sct", "code": "44054006" }, "resolution": { "vocabulary_id": "SNOMED", "source_concept": { "concept_id": 201826, "concept_name": "Type 2 diabetes mellitus", "concept_code": "44054006", "vocabulary_id": "SNOMED", "domain_id": "Condition", "concept_class_id": "Clinical Finding", "standard_concept": "S" }, "standard_concept": { "concept_id": 201826, "concept_name": "Type 2 diabetes mellitus", "concept_code": "44054006", "vocabulary_id": "SNOMED", "domain_id": "Condition", "concept_class_id": "Clinical Finding", "standard_concept": "S" }, "mapping_type": "direct", "target_table": "condition_occurrence", "domain_resource_alignment": "not_checked", "recommendations": [ { "concept_id": 4193704, "concept_name": "Hyperglycemia", "concept_code": "80394007", "vocabulary_id": "SNOMED", "domain_id": "Condition", "concept_class_id": "Clinical Finding", "standard_concept": "S", "relationship_id": "Has finding" }, { "concept_id": 4058243, "concept_name": "Diabetic retinopathy", "concept_code": "4855003", "vocabulary_id": "SNOMED", "domain_id": "Condition", "concept_class_id": "Clinical Finding", "standard_concept": "S", "relationship_id": "Associated finding" }, { "concept_id": 3004410, "concept_name": "Hemoglobin A1c/Hemoglobin.total in Blood", "concept_code": "4548-4", "vocabulary_id": "LOINC", "domain_id": "Measurement", "concept_class_id": "Lab Test", "standard_concept": "S", "relationship_id": "Has finding" } ] } }, "meta": { "request_id": "req_fhir_resolve_recs_123", "timestamp": "2026-04-09T10:00:00Z", "vocab_release": "2025.2" } } ``` ## Response Fields ### `resolution` | Field | Type | Description | | ------------------------------- | -------------- | ------------------------------------------------------------------------------ | | `vocabulary_id` | string \| null | OMOP vocabulary the source concept came from | | `source_concept` | object | The concept resolved from the input code | | `standard_concept` | object | The standard concept (after any `Maps to` traversal) | | `mapping_type` | string | `direct`, `mapped`, `semantic_match`, or `unmapped` | | `relationship_id` | string | Set to `Maps to` when `mapping_type = mapped` | | `similarity_score` | number | Only for `semantic_match` - cosine similarity (0–1) | | `target_table` | string \| null | OMOP CDM target table (e.g. `condition_occurrence`) | | `domain_resource_alignment` | string | `aligned`, `misaligned`, or `not_checked` | | `alignment_note` | string | Human-readable explanation when misaligned | | `mapping_quality` | string | Only when `include_quality=true`: `high`, `medium`, `low`, or `manual_review` | | `quality_note` | string | Advisory note, e.g. for semantic matches | | `alternative_standard_concepts` | array | Additional `Maps to` targets when the source maps to several standard concepts | | `recommendations` | array | Phoebe recommendations when `include_recommendations=true` | ### Mapping types * **`direct`** - The source code was found by exact lookup and is itself a standard concept. * **`mapped`** - The source code was found but is non-standard; the response contains the standard target reached via `Maps to`. * **`semantic_match`** - The resolver fell back to semantic search (code miss or text-only input). Similarity score ≥ 0.70 is required for a match; ≥ 0.85 is flagged as `high` quality, 0.70–0.84 as `medium`. * **`unmapped`** - The source concept was found but no standard target exists via `Maps to`. The source is returned as-is; review recommended. ## Errors | HTTP | `error.code` | Cause | | ---- | ----------------------- | ------------------------------------------------------------------------------------------------- | | 400 | `unknown_system` | Unknown FHIR code system URI. Response includes a `suggestion` when a close match exists. | | 400 | `missing_input` | None of `(system + code)`, `(vocabulary_id + code)`, or `display` was provided. | | 400 | `invalid_resource_type` | `resource_type` is not a supported FHIR resource type. | | 403 | `vocabulary_restricted` | The code system maps to a restricted vocabulary. | | 404 | `concept_not_found` | No concept was found via direct lookup and no semantic fallback candidate met the 0.70 threshold. | ## See also * [Batch resolve](/api-reference/fhir/resolve-batch) - up to 100 codings per request * [CodeableConcept resolve](/api-reference/fhir/resolve-codeable-concept) - pick the best match across multiple codings * [FHIR Integration Guide](/guides/integration/fhir-integration) - end-to-end ETL patterns # Resolve FHIR Codings - Batch Source: https://docs.omophub.com/api-reference/fhir/resolve-batch POST /fhir/resolve/batch ## Overview Batch-resolve up to **100** FHIR `Coding` inputs in a single request. The batch never fails because one coding could not be resolved - failing items are reported inline in the `results` array with an `error` field, and the surrounding successful resolutions are still returned. Use this endpoint in ETL pipelines where you are iterating over a FHIR Bundle or exporting a FHIR dataset and need high-throughput code resolution. ## Request Body Array of FHIR coding inputs. Each item accepts the same fields as the single resolve endpoint: `system`, `code`, `display`, `vocabulary_id`. Maximum 100 items. FHIR resource type applied to every item in the batch. Used for domain alignment checks and as the domain filter on semantic search fallbacks. Supported values match the single resolve endpoint. Include Phoebe recommendations on every successful resolution. Maximum Phoebe recommendations per resolved concept (1–20). Include a `mapping_quality` signal on every successful resolution. ```bash cURL theme={null} curl -X POST "https://api.omophub.com/v1/fhir/resolve/batch" \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "codings": [ { "system": "http://snomed.info/sct", "code": "44054006" }, { "system": "http://loinc.org", "code": "2951-2" }, { "system": "http://www.nlm.nih.gov/research/umls/rxnorm", "code": "197696" }, { "system": "http://hl7.org/fhir/sid/icd-10-cm", "code": "E11.9" } ] }' ``` ```python Python theme={null} import requests resp = requests.post( "https://api.omophub.com/v1/fhir/resolve/batch", headers={"Authorization": "Bearer YOUR_API_KEY"}, json={ "codings": [ {"system": "http://snomed.info/sct", "code": "44054006"}, {"system": "http://loinc.org", "code": "2951-2"}, {"system": "http://www.nlm.nih.gov/research/umls/rxnorm", "code": "197696"}, ], "include_quality": True, }, ) for item in resp.json()["data"]["results"]: if "resolution" in item: print(item["resolution"]["standard_concept"]["concept_name"], "→", item["resolution"]["target_table"]) else: print("failed:", item["error"]["code"], item["input"]) ``` ```json Mixed batch with one failure theme={null} { "success": true, "data": { "results": [ { "input": { "system": "http://snomed.info/sct", "code": "44054006" }, "resolution": { "vocabulary_id": "SNOMED", "source_concept": { "concept_id": 201826, "concept_name": "Type 2 diabetes mellitus", "concept_code": "44054006", "vocabulary_id": "SNOMED", "domain_id": "Condition", "concept_class_id": "Clinical Finding", "standard_concept": "S" }, "standard_concept": { "concept_id": 201826, "concept_name": "Type 2 diabetes mellitus", "concept_code": "44054006", "vocabulary_id": "SNOMED", "domain_id": "Condition", "concept_class_id": "Clinical Finding", "standard_concept": "S" }, "mapping_type": "direct", "target_table": "condition_occurrence", "domain_resource_alignment": "not_checked" } }, { "input": { "system": "http://snomed.info/sct", "code": "00000000" }, "error": { "code": "concept_not_found", "message": "No matching OMOP concept found for the provided input", "details": { "vocabulary_id": "SNOMED", "code": "00000000" } } } ], "summary": { "total": 2, "resolved": 1, "failed": 1 } }, "meta": { "request_id": "req_fhir_batch_123", "timestamp": "2026-04-09T10:00:00Z", "vocab_release": "2025.2" } } ``` ## Response Shape Each entry in `results` is either a successful resolution (same shape as [`POST /fhir/resolve`](/api-reference/fhir/resolve)) or an error entry: ```json theme={null} { "input": { /* echoed coding plus resource_type */ }, "error": { "code": "concept_not_found", "message": "…", "details": { /* optional */ } } } ``` The `summary` block reports totals to make monitoring easier: | Field | Type | Description | | ---------- | ------- | ------------------------------------ | | `total` | integer | Number of codings submitted | | `resolved` | integer | Number that resolved successfully | | `failed` | integer | Number that reported an inline error | ## Errors | HTTP | `error.code` | Cause | | ---- | ----------------------- | ----------------------------------------------------- | | 400 | `validation_error` | `codings` missing, empty, or exceeds 100 items | | 400 | `invalid_resource_type` | `resource_type` is not a supported FHIR resource type | Per-item errors (e.g. `concept_not_found`, `unknown_system`, `vocabulary_restricted`) are returned inline in the `results` array and do not fail the batch. ## See also * [Single resolve](/api-reference/fhir/resolve) * [CodeableConcept resolve](/api-reference/fhir/resolve-codeable-concept) # Resolve FHIR CodeableConcept Source: https://docs.omophub.com/api-reference/fhir/resolve-codeable-concept POST /fhir/resolve/codeable-concept ## Overview A FHIR `CodeableConcept` often contains multiple `Coding` entries for the same clinical idea - for example, both a SNOMED code and an ICD-10-CM code for the same condition. This endpoint resolves every coding in parallel and picks the **best match** according to OHDSI vocabulary preference: 1. SNOMED CT - conditions, procedures, observations 2. RxNorm - drugs 3. LOINC - measurements 4. CVX - vaccines 5. ICD-10 / ICD-10-CM - classification 6. Other vocabularies The remaining resolutions are returned in `alternatives`, and any codings that failed to resolve are reported in `unresolved`. If no coding resolves and a top-level `text` is provided, the resolver falls back to semantic search on that text. ## Request Body Array of FHIR `Coding` objects, each with required `system` and `code` fields and an optional `display`. Maximum 20 codings per request. Optional top-level `CodeableConcept.text`. Used as the semantic search fallback when none of the structured codings can be resolved. FHIR resource type carrying the CodeableConcept. Applied to every resolution for domain alignment and semantic search filtering. Include Phoebe recommendations on resolved concepts. Maximum Phoebe recommendations per resolved concept (1–20). Include a `mapping_quality` signal on resolved concepts. At least one of `coding` or `text` must be provided. ```bash cURL theme={null} curl -X POST "https://api.omophub.com/v1/fhir/resolve/codeable-concept" \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "coding": [ { "system": "http://snomed.info/sct", "code": "44054006", "display": "Type 2 diabetes mellitus" }, { "system": "http://hl7.org/fhir/sid/icd-10-cm", "code": "E11.9", "display": "Type 2 diabetes mellitus without complications" } ], "resource_type": "Condition" }' ``` ```python Python theme={null} import requests resp = requests.post( "https://api.omophub.com/v1/fhir/resolve/codeable-concept", headers={"Authorization": "Bearer YOUR_API_KEY"}, json={ "coding": [ {"system": "http://snomed.info/sct", "code": "44054006"}, {"system": "http://hl7.org/fhir/sid/icd-10-cm", "code": "E11.9"}, ], "resource_type": "Condition", }, ) data = resp.json()["data"] print(data["best_match"]["resolution"]["standard_concept"]["concept_name"]) print("alternatives:", len(data["alternatives"])) ``` ```json SNOMED wins over ICD-10-CM theme={null} { "success": true, "data": { "input": { "coding": [ { "system": "http://snomed.info/sct", "code": "44054006" }, { "system": "http://hl7.org/fhir/sid/icd-10-cm", "code": "E11.9" } ], "resource_type": "Condition" }, "best_match": { "input": { "system": "http://snomed.info/sct", "code": "44054006", "resource_type": "Condition" }, "resolution": { "vocabulary_id": "SNOMED", "source_concept": { "concept_id": 201826, "concept_name": "Type 2 diabetes mellitus", "concept_code": "44054006", "vocabulary_id": "SNOMED", "domain_id": "Condition", "concept_class_id": "Clinical Finding", "standard_concept": "S" }, "standard_concept": { "concept_id": 201826, "concept_name": "Type 2 diabetes mellitus", "concept_code": "44054006", "vocabulary_id": "SNOMED", "domain_id": "Condition", "concept_class_id": "Clinical Finding", "standard_concept": "S" }, "mapping_type": "direct", "target_table": "condition_occurrence", "domain_resource_alignment": "aligned" } }, "alternatives": [ { "input": { "system": "http://hl7.org/fhir/sid/icd-10-cm", "code": "E11.9", "resource_type": "Condition" }, "resolution": { "vocabulary_id": "ICD10CM", "source_concept": { "concept_id": 45576876, "concept_name": "Type 2 diabetes mellitus without complications", "concept_code": "E11.9", "vocabulary_id": "ICD10CM", "domain_id": "Condition", "concept_class_id": "5-char billing code", "standard_concept": null }, "standard_concept": { "concept_id": 201826, "concept_name": "Type 2 diabetes mellitus", "concept_code": "44054006", "vocabulary_id": "SNOMED", "domain_id": "Condition", "concept_class_id": "Clinical Finding", "standard_concept": "S" }, "mapping_type": "mapped", "relationship_id": "Maps to", "target_table": "condition_occurrence", "domain_resource_alignment": "aligned" } } ], "unresolved": [] }, "meta": { "request_id": "req_fhir_cc_123", "timestamp": "2026-04-09T10:00:00Z", "vocab_release": "2026.1" } } ``` ## Response Shape | Field | Type | Description | | -------------- | -------------- | ------------------------------------------------------------------ | | `input` | object | Echo of the submitted `CodeableConcept` | | `best_match` | object \| null | Highest-priority successful resolution, or `null` if none resolved | | `alternatives` | array | Other successful resolutions, ordered by vocabulary preference | | `unresolved` | array | Codings that failed, each with `input` and `error` | ## Text fallback When every structured `coding` entry fails to resolve and the request includes a non-empty `text`, the resolver performs a semantic search against `text` and returns the top match as `best_match`. The failed structured codings are still reported in `unresolved` so you can audit why they didn't resolve. ## Errors | HTTP | `error.code` | Cause | | ---- | ----------------------- | ------------------------------------------------------------------ | | 400 | `validation_error` | Neither `coding` nor `text` provided, or `coding` exceeds 20 items | | 400 | `invalid_resource_type` | `resource_type` is not a supported FHIR resource type | Per-coding errors are reported inline in `unresolved`. ## See also * [Single resolve](/api-reference/fhir/resolve) * [Batch resolve](/api-reference/fhir/resolve-batch) * [FHIR Integration Guide](/guides/integration/fhir-integration) # Get Concept Ancestors Source: https://docs.omophub.com/api-reference/hierarchy/get-concept-ancestors GET /v1/concepts/{concept_id}/ancestors Retrieve all ancestor OMOP concepts for a given concept - hierarchical context, classification paths, and parent terms for concept set expansion. This endpoint returns the complete ancestor hierarchy for a specific concept, including parent concepts, grandparents, and all higher-level classifications up to the root of the vocabulary hierarchy. ## Path Parameters The unique identifier of the concept to retrieve ancestors for
**Example:** `201826` (Type 2 diabetes mellitus)
## Query Parameters Filter ancestors to specific vocabularies (comma-separated)
**Example:** `SNOMED,ICD10CM`
Filter ancestors to specific domains (comma-separated)
**Example:** `Condition,Observation`
Maximum number of hierarchy levels to traverse
**Range:** `1-20`
Relationship types to follow for hierarchy traversal. Spaces must be percent-encoded when used in query strings (e.g., "Is%20a,Part%20of").
**Default:** `Is a`
**Allowed values:** `Is a`, `Part of`, `Has part`, `Subsumes`, `Has ingredient`, `RxNorm has dose form`
**Example:** `Is%20a,Part%20of` (URL-encoded)
Include `hierarchy_level` field for each ancestor (distance from source concept) Include `path_length` field for each ancestor Include deprecated/invalid concepts in ancestry (default excludes them) Page number for pagination (1-based) Number of ancestor concepts to return per page (max 1000) Specific vocabulary release version to query
**Example:** `2025.1`
## Response The concept ID for which ancestors were retrieved Standard name of the source concept Vocabulary containing the source concept Array of ancestor concepts in hierarchical order Unique identifier for the ancestor concept Standard name of the ancestor concept Original code from the vocabulary Vocabulary containing this ancestor concept Human-readable vocabulary name Domain classification of the ancestor Concept class identifier Standard concept designation ('S', 'C', or null) Distance from source concept (always present) Minimum levels of separation from source concept Maximum levels of separation from source concept Relationship type ID (e.g., "Is a") Relationship type name (e.g., "Is a") Distance from source concept (only when include\_distance=true, same as level) Path length from source concept (only when include\_paths=true, same as level) Date when concept became valid (ISO format) Date when concept became invalid (ISO format) Reason for concept invalidation if applicable Summary statistics about the ancestor hierarchy Total number of ancestor concepts found Maximum depth of the hierarchy tree List of vocabulary IDs represented in ancestors List of relationship type IDs found in hierarchy Response metadata and pagination information Current page number Items per page Total ancestor concepts Total number of pages Whether next page exists Whether previous page exists ```bash cURL theme={null} curl -X GET "https://api.omophub.com/v1/concepts/201826/ancestors?include_paths=true&include_distance=true&max_levels=5" \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Accept: application/json" ``` ```javascript JavaScript theme={null} const response = await fetch('https://api.omophub.com/v1/concepts/201826/ancestors?include_paths=true&include_distance=true&max_levels=5', { method: 'GET', headers: { 'Authorization': 'Bearer YOUR_API_KEY', 'Content-Type': 'application/json' } }); const ancestorData = await response.json(); console.log(`Found ${ancestorData.data.hierarchy_summary.total_ancestors} ancestors`); console.log('Classification paths:', ancestorData.data.ancestors[0].classification_paths); ``` ```python Python theme={null} import requests concept_id = 201826 # Type 2 diabetes mellitus url = f"https://api.omophub.com/v1/concepts/{concept_id}/ancestors" params = { "include_paths": True, "include_distance": True, "max_levels": 5 } headers = { "Authorization": "Bearer YOUR_API_KEY", "Content-Type": "application/json" } response = requests.get(url, params=params, headers=headers) ancestor_data = response.json() print(f"Concept: {ancestor_data['data']['concept_name']}") print(f"Total ancestors: {ancestor_data['data']['hierarchy_summary']['total_ancestors']}") for ancestor in ancestor_data['data']['ancestors'][:5]: level = ancestor.get('level', 'Unknown') print(f"Level {level}: {ancestor['concept_name']} ({ancestor['concept_id']})") ``` ```json theme={null} { "success": true, "data": { "concept_id": 201826, "concept_name": "Type 2 diabetes mellitus", "vocabulary_id": "SNOMED", "ancestors": [ { "concept_id": 73211009, "concept_name": "Diabetes mellitus", "concept_code": "73211009", "vocabulary_id": "SNOMED", "vocabulary_name": "SNOMED Clinical Terms", "domain_id": "Condition", "concept_class_id": "Clinical Finding", "standard_concept": "S", "level": 1, "min_levels_of_separation": 1, "max_levels_of_separation": 1, "relationship_id": "Is a", "relationship_name": "Is a", "hierarchy_level": 1, "valid_start_date": "1970-01-01", "valid_end_date": "2099-12-31", "invalid_reason": null }, { "concept_id": 362969004, "concept_name": "Disorder of endocrine system", "concept_code": "362969004", "vocabulary_id": "SNOMED", "vocabulary_name": "SNOMED Clinical Terms", "domain_id": "Condition", "concept_class_id": "Clinical Finding", "standard_concept": "S", "level": 2, "min_levels_of_separation": 2, "max_levels_of_separation": 2, "relationship_id": "Is a", "relationship_name": "Is a", "hierarchy_level": 2, "valid_start_date": "2002-01-31", "valid_end_date": "2099-12-31", "invalid_reason": null }, { "concept_id": 64572001, "concept_name": "Disease", "concept_code": "64572001", "vocabulary_id": "SNOMED", "vocabulary_name": "SNOMED Clinical Terms", "domain_id": "Condition", "concept_class_id": "Clinical Finding", "standard_concept": "S", "level": 3, "min_levels_of_separation": 3, "max_levels_of_separation": 3, "relationship_id": "Is a", "relationship_name": "Is a", "hierarchy_level": 3, "valid_start_date": "1970-01-01", "valid_end_date": "2099-12-31", "invalid_reason": null } ], "hierarchy_summary": { "total_ancestors": 3, "max_hierarchy_depth": 3, "unique_vocabularies": ["SNOMED"], "relationship_types_used": ["Is a"] } }, "meta": { "pagination": { "page": 1, "page_size": 100, "total_items": 3, "total_pages": 1, "has_next": false, "has_previous": false }, "request_id": "req_ancestors_201826_20250104_103100", "timestamp": "2025-01-04T10:31:00Z", "vocab_release": "2025.1" } } ``` ## Usage Examples ### Basic Ancestor Retrieval Get all ancestors for a specific concept: ```javascript theme={null} const ancestors = await fetch('/v1/concepts/201826/ancestors'); ``` ### Limited Hierarchy Depth Retrieve ancestors up to a specific number of levels: ```javascript theme={null} const nearAncestors = await fetch('/v1/concepts/201826/ancestors?max_levels=3'); ``` ### Classification Path Analysis Get complete classification paths from concept to root: ```javascript theme={null} const pathData = await fetch('/v1/concepts/201826/ancestors?include_paths=true&include_distance=true'); ``` ### Cross-Vocabulary Hierarchy Analyze ancestors within specific vocabulary: ```javascript theme={null} const snomedAncestors = await fetch('/v1/concepts/201826/ancestors?vocabulary_id=SNOMED'); ``` ### Multiple Relationship Types Follow different types of hierarchical relationships: ```javascript theme={null} const extendedHierarchy = await fetch('/v1/concepts/201826/ancestors?relationship_types=Is%20a,Part%20of'); ``` ## Related Endpoints * [Get Concept Descendants](/api-reference/hierarchy/get-concept-descendants) - Retrieve child concepts * [Get Concept Hierarchy](/api-reference/hierarchy/get-concept-hierarchy) - Complete hierarchy view * [Get Concept Relationships](/api-reference/relationships/get-concept-relationships) - All concept relationships * [Get Concept Details](/api-reference/concepts/get-concept-details) - Complete concept information ## Notes * Hierarchy traversal follows "Is a" relationships by default, but can be customized * Some concepts may have multiple classification paths to different root concepts * Cross-vocabulary concepts may have ancestors in different vocabularies * Standard concepts are prioritized in hierarchy traversal unless explicitly disabled * Deprecated concepts are excluded from ancestry unless specifically requested * Maximum hierarchy depth is typically 6-8 levels for most medical vocabularies # Get Concept Descendants Source: https://docs.omophub.com/api-reference/hierarchy/get-concept-descendants GET /v1/concepts/{concept_id}/descendants Retrieve all descendant OMOP concepts for a given concept - hierarchical children and specialized terms for phenotype and cohort definitions. This endpoint returns all descendant concepts (children, grandchildren, etc.) for a specific concept, allowing exploration of more specific terms and sub-classifications within the medical vocabulary hierarchy. ## Path Parameters The unique identifier of the concept to retrieve descendants for
**Example:** `73211009` (Diabetes mellitus)
## Query Parameters Filter descendants to specific vocabularies (comma-separated)
**Example:** `SNOMED,ICD10CM`
Filter descendants to specific domains (comma-separated)
**Example:** `Condition,Drug`
Maximum number of hierarchy levels to traverse downward
**Range:** `1-20`
Relationship types to follow for hierarchy traversal. Spaces must be URL-encoded (%20) in URLs.
**Default:** `Is a`
**Allowed values:** `Is a`, `Has part`, `Subsumes`, `Part of`, `Has ingredient`, `RxNorm has dose form`
**Example:** `Is%20a,Has%20part` (URL-encoded)
Include `hierarchy_level` field for each descendant (distance from source concept) Include `path_length` field for each descendant Include deprecated/invalid concepts in descendants (default excludes them) Page number for pagination (1-based) Number of descendant concepts to return per page (max 1000) Specific vocabulary release version to query
**Example:** `2025.1`
## Response The concept ID for which descendants were retrieved Standard name of the source concept Vocabulary containing the source concept Array of descendant concepts in hierarchical order Unique identifier for the descendant concept Standard name of the descendant concept Original code from the vocabulary Vocabulary containing this descendant concept Human-readable vocabulary name Domain classification of the descendant Concept class identifier Standard concept designation ('S', 'C', or null) Distance from source concept (always present) Minimum levels of separation from source concept Maximum levels of separation from source concept Relationship type ID (e.g., "Subsumes") Relationship type name (e.g., "Subsumes") Distance from source concept (only when include\_distance=true, same as level) Path length from source concept (only when include\_paths=true, same as level) Date when concept became valid (ISO format) Date when concept became invalid (ISO format) Reason for concept invalidation if applicable Summary statistics about the descendant hierarchy Total number of descendant concepts found Maximum depth of the descendant tree List of vocabulary IDs represented in descendants List of relationship type IDs found in hierarchy Response metadata and pagination information Current page number Items per page Total descendant concepts Total number of pages Whether next page exists Whether previous page exists ```bash cURL theme={null} curl -X GET "https://api.omophub.com/v1/concepts/73211009/descendants?include_distance=true&max_levels=3" \ -H "Authorization: Bearer YOUR_API_KEY" ``` ```python Python theme={null} import requests concept_id = 73211009 # Diabetes mellitus url = f"https://api.omophub.com/v1/concepts/{concept_id}/descendants" params = { "include_distance": True, "max_levels": 3, "page_size": 50 } headers = { "Authorization": "Bearer YOUR_API_KEY" } response = requests.get(url, params=params, headers=headers) descendant_data = response.json() print(f"Concept: {descendant_data['concept_name']}") print(f"Total descendants: {descendant_data['hierarchy_summary']['total_descendants']}") ``` ```javascript JavaScript theme={null} const response = await fetch('https://api.omophub.com/v1/concepts/73211009/descendants?include_distance=true&max_levels=3', { headers: { 'Authorization': 'Bearer YOUR_API_KEY' } }); const descendantData = await response.json(); console.log(`Found ${descendantData.hierarchy_summary.total_descendants} descendants`); console.log(`Max depth: ${descendantData.hierarchy_summary.max_hierarchy_depth}`); ``` ```bash cURL (filtered by domain) theme={null} curl -X GET "https://api.omophub.com/v1/concepts/73211009/descendants?domain_ids=Condition&max_levels=2&page_size=25" \ -H "Authorization: Bearer YOUR_API_KEY" ``` ```python Python (with hierarchy grouping) theme={null} import requests from collections import defaultdict concept_id = 73211009 response = requests.get( f"https://api.omophub.com/v1/concepts/{concept_id}/descendants", headers={"Authorization": "Bearer YOUR_API_KEY"}, params={"include_distance": True, "max_levels": 3} ) data = response.json() by_level = defaultdict(list) for desc in data['descendants']: level = desc.get('hierarchy_level', 0) by_level[level].append(desc['concept_name']) for level in sorted(by_level.keys()): print(f"Level {level}: {len(by_level[level])} concepts") ``` ```json theme={null} { "success": true, "data": { "concept_id": 73211009, "concept_name": "Diabetes mellitus", "vocabulary_id": "SNOMED", "descendants": [ { "concept_id": 44054006, "concept_name": "Type 2 diabetes mellitus", "concept_code": "44054006", "vocabulary_id": "SNOMED", "vocabulary_name": "SNOMED Clinical Terms", "domain_id": "Condition", "concept_class_id": "Clinical Finding", "standard_concept": "S", "level": 1, "min_levels_of_separation": 1, "max_levels_of_separation": 1, "relationship_id": "Subsumes", "relationship_name": "Subsumes", "hierarchy_level": 1, "valid_start_date": "1970-01-01", "valid_end_date": "2099-12-31", "invalid_reason": null }, { "concept_id": 46635009, "concept_name": "Type 1 diabetes mellitus", "concept_code": "46635009", "vocabulary_id": "SNOMED", "vocabulary_name": "SNOMED Clinical Terms", "domain_id": "Condition", "concept_class_id": "Clinical Finding", "standard_concept": "S", "level": 1, "min_levels_of_separation": 1, "max_levels_of_separation": 1, "relationship_id": "Subsumes", "relationship_name": "Subsumes", "hierarchy_level": 1, "valid_start_date": "1970-01-01", "valid_end_date": "2099-12-31", "invalid_reason": null }, { "concept_id": 237599002, "concept_name": "Insulin dependent diabetes mellitus", "concept_code": "237599002", "vocabulary_id": "SNOMED", "vocabulary_name": "SNOMED Clinical Terms", "domain_id": "Condition", "concept_class_id": "Clinical Finding", "standard_concept": "S", "level": 2, "min_levels_of_separation": 2, "max_levels_of_separation": 2, "relationship_id": "Subsumes", "relationship_name": "Subsumes", "hierarchy_level": 2, "valid_start_date": "2002-01-31", "valid_end_date": "2099-12-31", "invalid_reason": null } ], "hierarchy_summary": { "total_descendants": 847, "max_hierarchy_depth": 6, "unique_vocabularies": ["SNOMED"], "relationship_types_used": ["Subsumes"] } }, "meta": { "pagination": { "page": 1, "page_size": 100, "total_items": 847, "total_pages": 9, "has_next": true, "has_previous": false }, "request_id": "req_descendants_123", "timestamp": "2024-12-22T10:00:00Z", "vocab_release": "2025.2" } } ``` ## Usage Examples **Note:** Examples assume a preconfigured client with base URL `https://api.omophub.com` and authentication headers. For direct usage: ```javascript theme={null} const response = await fetch('https://api.omophub.com/v1/concepts/73211009/descendants', { headers: { 'Authorization': 'Bearer YOUR_API_KEY', 'Accept': 'application/json' } }); ``` ### Basic Descendant Retrieval Get all descendants for a specific concept: ```javascript theme={null} const descendants = await fetch('/v1/concepts/73211009/descendants'); ``` ### Limited Hierarchy Depth Retrieve descendants up to a specific number of levels: ```javascript theme={null} const nearDescendants = await fetch('/v1/concepts/73211009/descendants?max_levels=2'); ``` ### Domain-Specific Descendants Filter descendants to specific medical domains: ```javascript theme={null} const conditionDescendants = await fetch('/v1/concepts/73211009/descendants?domain_ids=Condition'); ``` ### Detailed Descendant Information Get descendants with hierarchy levels and path information: ```javascript theme={null} const detailedData = await fetch('/v1/concepts/73211009/descendants?include_distance=true&include_paths=true'); ``` ## Related Endpoints * [Get Concept Ancestors](/api-reference/hierarchy/get-concept-ancestors) - Retrieve parent concepts * [Get Concept Hierarchy](/api-reference/hierarchy/get-concept-hierarchy) - Complete hierarchy view * [Get Concept Relationships](/api-reference/relationships/get-concept-relationships) - All concept relationships * [Search Concepts](/api-reference/search/basic-search) - Search within specific hierarchies ## Notes * Descendant traversal follows "Is a" relationships by default, but can include other relationship types * Large hierarchies may contain thousands of descendants - use pagination and filtering appropriately * Standard concepts are prioritized unless explicitly disabled * Some medical concepts may have very deep hierarchies (6+ levels) * Cross-vocabulary concepts may have descendants from multiple vocabularies * Deprecated concepts are excluded from descendants unless specifically requested with `include_invalid=true` # Get Concept Hierarchy Source: https://docs.omophub.com/api-reference/hierarchy/get-concept-hierarchy GET /v1/concepts/{concept_id}/hierarchy Retrieve the complete OMOP hierarchy for a concept - ancestors and descendants in one unified view for phenotype development and concept sets. This endpoint provides a comprehensive hierarchical view of a concept, showing its position within the medical vocabulary structure by including both ancestor (parent) and descendant (child) relationships in a single response. ## Path Parameters The unique identifier of the concept to retrieve hierarchy for
**Example:** `73211009` (Diabetes mellitus)
## Query Parameters Response format for hierarchy data
**Options:** `flat`, `graph`
* `flat`: Returns ancestors and descendants as separate arrays (default) * `graph`: Returns nodes and edges for visualization (similar to OHDSI Athena)
Filter hierarchy to specific vocabularies (comma-separated)
**Example:** `SNOMED,ICD10CM`
Filter hierarchy to specific domains (comma-separated)
**Example:** `Condition,Drug`
Maximum number of hierarchy levels to traverse in both directions
**Range:** `1-20`
Maximum number of results to return per direction (ancestors/descendants) for performance optimization
**Range:** `1-5000`
**Recommended:** Use 100-500 for interactive queries, up to 1000 for bulk analysis
Comma-separated list of relationship types to follow for hierarchy traversal
**Example:** `Is a,Part of`
Include deprecated/invalid concepts in hierarchy Specific vocabulary release version to query
**Example:** `2025.1`
## Response ### Flat Format (default) The concept ID for which hierarchy was retrieved Array of ancestor concepts in hierarchical order Unique identifier for the ancestor concept Standard name of the ancestor concept Original code from the vocabulary Vocabulary containing this ancestor Human-readable vocabulary name Domain classification Concept class identifier Standard concept designation ('S', 'C', or null) Distance from source concept Minimum levels of separation from source concept Maximum levels of separation from source concept Relationship type ID (e.g., "Is a") Relationship type name Array of descendant concepts in hierarchical order Unique identifier for the descendant concept Standard name of the descendant concept Original code from the vocabulary Vocabulary containing this descendant Human-readable vocabulary name Domain classification Concept class identifier Standard concept designation ('S', 'C', or null) Distance from source concept Minimum levels of separation from source concept Maximum levels of separation from source concept Relationship type ID (e.g., "Subsumes") Relationship type name Current concept's level in the hierarchy Maximum hierarchy depth Total number of ancestor concepts Total number of descendant concepts ### Graph Format (format=graph) The concept ID for which hierarchy was retrieved Array of concept nodes for visualization Unique identifier for the concept Concept name Hierarchical level (0 for central concept, negative for ancestors, positive for descendants) Vocabulary containing this concept Domain classification Concept class identifier Standard concept designation ('S', 'C', or null) Array of relationship edges between concepts Source concept ID Target concept ID Relationship type (e.g., "Is a", "Subsumes") ```bash cURL (flat format) theme={null} curl -X GET "https://api.omophub.com/v1/concepts/73211009/hierarchy?max_levels=3" \ -H "Authorization: Bearer YOUR_API_KEY" ``` ```bash cURL (graph format) theme={null} curl -X GET "https://api.omophub.com/v1/concepts/73211009/hierarchy?format=graph&max_levels=3" \ -H "Authorization: Bearer YOUR_API_KEY" ``` ```python Python (flat format) theme={null} import requests concept_id = 73211009 # Diabetes mellitus url = f"https://api.omophub.com/v1/concepts/{concept_id}/hierarchy" params = { "max_levels": 3, "vocabulary_ids": "SNOMED" } headers = { "Authorization": "Bearer YOUR_API_KEY" } response = requests.get(url, params=params, headers=headers) hierarchy_data = response.json() print(f"Concept ID: {hierarchy_data['data']['concept_id']}") print(f"Total ancestors: {hierarchy_data['data']['total_ancestors']}") print(f"Total descendants: {hierarchy_data['data']['total_descendants']}") ``` ```python Python (graph format for visualization) theme={null} import requests concept_id = 73211009 url = f"https://api.omophub.com/v1/concepts/{concept_id}/hierarchy" params = { "format": "graph", "max_levels": 3 } headers = {"Authorization": "Bearer YOUR_API_KEY"} response = requests.get(url, params=params, headers=headers) graph_data = response.json() # Use for D3.js or other visualization libraries nodes = graph_data['data']['nodes'] edges = graph_data['data']['edges'] print(f"Nodes: {len(nodes)}, Edges: {len(edges)}") ``` ```javascript JavaScript (flat format) theme={null} const response = await fetch('https://api.omophub.com/v1/concepts/73211009/hierarchy?max_levels=3', { headers: { 'Authorization': 'Bearer YOUR_API_KEY' } }); const hierarchyData = await response.json(); console.log(`Ancestors: ${hierarchyData.data.total_ancestors}`); console.log(`Descendants: ${hierarchyData.data.total_descendants}`); ``` ```javascript JavaScript (graph format) theme={null} const response = await fetch('https://api.omophub.com/v1/concepts/73211009/hierarchy?format=graph&max_levels=3', { headers: { 'Authorization': 'Bearer YOUR_API_KEY' } }); const graphData = await response.json(); // Use with D3.js force-directed graph const { nodes, edges } = graphData.data; ``` ```json Flat Format Response theme={null} { "success": true, "data": { "concept_id": 73211009, "ancestors": [ { "concept_id": 362969004, "concept_name": "Disorder of endocrine system", "concept_code": "362969004", "vocabulary_id": "SNOMED", "vocabulary_name": "SNOMED Clinical Terms", "domain_id": "Condition", "concept_class_id": "Clinical Finding", "standard_concept": "S", "level": 1, "min_levels_of_separation": 1, "max_levels_of_separation": 1, "relationship_id": "Is a", "relationship_name": "Is a" }, { "concept_id": 64572001, "concept_name": "Disease", "concept_code": "64572001", "vocabulary_id": "SNOMED", "vocabulary_name": "SNOMED Clinical Terms", "domain_id": "Condition", "concept_class_id": "Clinical Finding", "standard_concept": "S", "level": 2, "min_levels_of_separation": 2, "max_levels_of_separation": 2, "relationship_id": "Is a", "relationship_name": "Is a" } ], "descendants": [ { "concept_id": 44054006, "concept_name": "Type 2 diabetes mellitus", "concept_code": "44054006", "vocabulary_id": "SNOMED", "vocabulary_name": "SNOMED Clinical Terms", "domain_id": "Condition", "concept_class_id": "Clinical Finding", "standard_concept": "S", "level": 1, "min_levels_of_separation": 1, "max_levels_of_separation": 1, "relationship_id": "Subsumes", "relationship_name": "Subsumes" }, { "concept_id": 46635009, "concept_name": "Type 1 diabetes mellitus", "concept_code": "46635009", "vocabulary_id": "SNOMED", "vocabulary_name": "SNOMED Clinical Terms", "domain_id": "Condition", "concept_class_id": "Clinical Finding", "standard_concept": "S", "level": 1, "min_levels_of_separation": 1, "max_levels_of_separation": 1, "relationship_id": "Subsumes", "relationship_name": "Subsumes" } ], "level": 3, "max_level": 5, "total_ancestors": 2, "total_descendants": 2 }, "meta": { "request_id": "req_hierarchy_123", "timestamp": "2024-12-22T10:30:00Z", "vocab_release": "2025.2" } } ``` ```json Graph Format Response theme={null} { "success": true, "data": { "concept_id": 73211009, "nodes": [ { "id": 73211009, "name": "Diabetes mellitus", "level": 0, "vocabulary_id": "SNOMED", "domain_id": "Condition", "concept_class_id": "Clinical Finding", "standard_concept": "S" }, { "id": 362969004, "name": "Disorder of endocrine system", "level": -1, "vocabulary_id": "SNOMED", "domain_id": "Condition", "concept_class_id": "Clinical Finding", "standard_concept": "S" }, { "id": 44054006, "name": "Type 2 diabetes mellitus", "level": 1, "vocabulary_id": "SNOMED", "domain_id": "Condition", "concept_class_id": "Clinical Finding", "standard_concept": "S" }, { "id": 46635009, "name": "Type 1 diabetes mellitus", "level": 1, "vocabulary_id": "SNOMED", "domain_id": "Condition", "concept_class_id": "Clinical Finding", "standard_concept": "S" } ], "edges": [ { "from": 73211009, "to": 362969004, "relationship_id": "Is a" }, { "from": 73211009, "to": 44054006, "relationship_id": "Subsumes" }, { "from": 73211009, "to": 46635009, "relationship_id": "Subsumes" } ] }, "meta": { "request_id": "req_hierarchy_456", "timestamp": "2024-12-22T10:30:00Z", "vocab_release": "2025.2" } } ``` ## Usage Examples ### Basic Hierarchy View Get complete hierarchy context for a concept: ```javascript theme={null} const hierarchy = await fetch('/v1/concepts/73211009/hierarchy'); ``` ### Limited Depth Hierarchy Control the depth of ancestor and descendant traversal: ```javascript theme={null} const limitedHierarchy = await fetch('/v1/concepts/73211009/hierarchy?max_levels=2&max_results=100'); ``` ### Graph Format for Visualization Get hierarchy in graph structure for D3.js or similar visualization libraries: ```javascript theme={null} const graphData = await fetch('/v1/concepts/73211009/hierarchy?format=graph&max_levels=3'); ``` ### Filtered Hierarchy Filter to specific vocabularies: ```javascript theme={null} const filteredHierarchy = await fetch('/v1/concepts/73211009/hierarchy?vocabulary_ids=SNOMED&domain_ids=Condition'); ``` ## Related Endpoints * [Get Concept Ancestors](/api-reference/hierarchy/get-concept-ancestors) - Detailed ancestor information with pagination * [Get Concept Descendants](/api-reference/hierarchy/get-concept-descendants) - Detailed descendant information with pagination * [Get Concept Relationships](/api-reference/relationships/get-concept-relationships) - All concept relationships * [Search Concepts](/api-reference/search/basic-search) - Search within hierarchies ## Notes * The hierarchy endpoint combines ancestors and descendants in a single request for convenience * Use the `graph` format when building visualizations (compatible with D3.js, Cytoscape, etc.) * The `flat` format is better for data processing and analysis * In graph format, level 0 is the central concept, negative levels are ancestors, positive are descendants * Large hierarchies may be limited by `max_results` to ensure performance * Cross-vocabulary concepts may show relationships spanning multiple vocabularies # OMOPHub REST API and FHIR terminology introduction Source: https://docs.omophub.com/api-reference/introduction Base URLs, authentication, core capabilities, and quick-start examples for the OMOPHub REST API and FHIR R4/R5/R6 terminology service. Understand the base URLs, authentication, response formats, and the full scope of the OMOPHub API. ## Base URLs OMOPHub exposes two surfaces, both served over HTTPS: **REST API** ``` https://api.omophub.com/v1 ``` **FHIR Terminology Service** (R4 by default; `r4b`, `r5`, and `r6` available via path prefix) ``` https://fhir.omophub.com/fhir/r4 ``` Both endpoints enforce HTTPS - plain HTTP requests are rejected. ## Authentication All requests require a Bearer token in the `Authorization` header: ``` Authorization: Bearer oh_xxxxxxxxx ``` Get your API key from the [OMOPHub Dashboard](https://dashboard.omophub.com/api-keys). The same API key works for both the REST API and the FHIR Terminology Service. Rate limits and metering are shared across both endpoints. The FHIR Terminology Service additionally accepts RFC 6749 `client_credentials` via `POST /oauth2/token` for Spring Security OAuth2 clients - see the [FHIR Terminology Service overview](/api-reference/fhir-terminology/overview#authentication) for details. ## Core Capabilities **Search & Discovery** * Full-text search across 100+ medical vocabularies (SNOMED CT, ICD-10, LOINC, RxNorm, NDC, HCPCS, ATC, and more) * Semantic search powered by neural embeddings - match by clinical meaning, not just keywords * Autocomplete, fuzzy matching, phonetic search, and faceted filtering **FHIR Terminology Service** * Standard FHIR operations: `$lookup`, `$validate-code`, `$translate`, `$expand`, `$subsumes`, `$find-matches`, `$closure` * OMOP-specific operation: `$diff` for comparing concepts between vocabulary releases * CodeSystem and ValueSet search-type queries for client discovery (HAPI, EHRbase) * FHIR batch Bundles, per-vocabulary CodeSystem stubs, and R4 / R4B / R5 / R6 wire formats on the same endpoint **FHIR Concept Resolver** * One-call resolution: FHIR `Coding` or `CodeableConcept` → OMOP standard concept + domain + CDM target table * `Maps to` traversal for non-standard codes, semantic fallback for display-text-only inputs * Single, batch (up to 100 codings), and `CodeableConcept` variants with OHDSI vocabulary preference ranking **Vocabulary Mapping** * Cross-vocabulary mapping between any OMOP vocabulary pair * Mapping quality scores and coverage analysis * Batch mapping: up to 100 concepts per request **Concept Navigation** * Traverse ancestor / descendant hierarchies with configurable depth and relationship-type filters * Explore non-hierarchical relationships (`Maps to`, `Has ingredient`, `RxNorm has dose form`, etc.) * Phoebe-powered concept recommendations for phenotype development **Batch & Bulk Operations** * Batch concept retrieval, relationship queries, hierarchy queries, and mappings (up to 100 per request) * Bulk search and bulk semantic search for large ETL workloads * Each batch / bulk request counts as one API call against your quota **OHDSI Compliance** * Synced with OHDSI ATHENA releases * Version-specific API routing via `X-Vocab-Release` header or `vocab_release` query parameter ## API Structure **RESTful design** * Resource-based endpoints (`/concepts`, `/vocabularies`, `/search`, `/mappings`, `/fhir/resolve`) * Standard HTTP methods (GET, POST) * Consistent JSON response envelope **Field naming** * `snake_case` for all fields (`concept_id`, `vocabulary_id`, `domain_id`, `target_concept_code`) * Consistent naming across REST API, SDK responses, and FHIR property codes **Pagination** * Page-based: `page` (1-indexed) and `page_size` (default 20, max 1000) query parameters * Response `meta.pagination` includes `total_items`, `total_pages`, `page`, `page_size`, `has_next`, `has_previous` ## API Categories Text search, semantic search, autocomplete, facets, bulk search, and similarity. Look up by ID or code, get recommendations, traverse relationships, batch retrieval. Ancestors, descendants, and full hierarchy trees with relationship-type filters. Relationship types and per-concept relationship queries. Cross-vocabulary mapping, batch mapping, coverage analysis, and validation. List, search, and inspect vocabulary metadata, statistics, and releases. OMOP domains, concept classes, and per-domain statistics. `Coding` and `CodeableConcept` → OMOP standard concept, CDM target table, in one call. `$lookup`, `$translate`, `$validate-code`, `$expand`, `$subsumes`, and the full spec-conformant surface. ## Quick Start Examples ### Search for Concepts ```bash theme={null} curl "https://api.omophub.com/v1/search/concepts?query=diabetes&vocabulary_ids=SNOMED&page_size=5" \ -H "Authorization: Bearer YOUR_API_KEY" ``` ### Semantic Search Match by clinical meaning, not just keyword overlap. Uses neural embeddings. ```bash theme={null} curl "https://api.omophub.com/v1/concepts/semantic-search?query=chest+pain+that+gets+worse+when+breathing&page_size=5" \ -H "Authorization: Bearer YOUR_API_KEY" ``` ### Get Concept Details ```bash theme={null} curl "https://api.omophub.com/v1/concepts/201826" \ -H "Authorization: Bearer YOUR_API_KEY" ``` ### Map Between Vocabularies ```bash theme={null} curl "https://api.omophub.com/v1/concepts/201826/mappings?target_vocabularies=ICD10CM" \ -H "Authorization: Bearer YOUR_API_KEY" ``` ### Resolve a FHIR Code to OMOP One call: FHIR system URI + code in, OMOP standard concept + CDM target table out. ```bash theme={null} curl -X POST "https://api.omophub.com/v1/fhir/resolve" \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "system": "http://snomed.info/sct", "code": "44054006", "resource_type": "Condition" }' ``` ### FHIR `$lookup` Spec-conformant FHIR operation for clients that speak FHIR natively (HAPI, EHRbase, Firely). ```bash theme={null} curl "https://fhir.omophub.com/fhir/r4/CodeSystem/\$lookup?system=http://snomed.info/sct&code=44054006" \ -H "Authorization: Bearer YOUR_API_KEY" ``` ## Response Format All REST API responses follow a consistent envelope: ```json theme={null} { "success": true, "data": { // Response payload }, "meta": { "request_id": "req_abc123", "timestamp": "2026-04-14T10:30:00Z", "vocab_release": "2025.2" } } ``` FHIR Terminology Service responses use standard FHIR resource formats (`Parameters`, `Bundle`, `OperationOutcome`, `CodeSystem`, `ValueSet`, `ConceptMap`) with `Content-Type: application/fhir+json`. XML is served via content negotiation - see the [FHIR Terminology Service content type](/api-reference/fhir-terminology/overview#content-type) for the negotiation rules and XML support matrix. ## Response Codes OMOPHub uses standard HTTP status codes to signal success or failure. `2xx` codes indicate success, `4xx` are client errors, `5xx` are server errors. | Status | Description | | ------ | --------------------------------------------------------------- | | `200` | Request succeeded. | | `400` | Invalid parameters - check request body or query string. | | `401` | Missing or invalid API key. | | `403` | Forbidden - insufficient permissions, or restricted vocabulary. | | `404` | Resource not found. | | `429` | Rate limit exceeded - check the `Retry-After` header. | | `5xx` | Server error - retry with exponential backoff. | REST API errors return a `{"success": false, "error": {...}}` envelope. FHIR Terminology Service errors on `/fhir/*` paths return a FHIR `OperationOutcome` resource instead. See [Errors](/api-reference/errors) for the complete error code reference and troubleshooting guide. ## Rate Limits Default free-tier limit: **2 requests per second**, monthly call quota per plan. Batch and bulk endpoints process up to 100 items per request and count as one API call against your quota. Every response includes `X-RateLimit-Limit`, `X-RateLimit-Remaining`, and `X-RateLimit-Reset` headers. See [Rate Limits](/api-reference/rate-limit) for plan-specific limits and header semantics. ## Next Steps `$lookup`, `$translate`, `$validate-code`, `$expand`, `$subsumes`, and OMOP-specific operations. End-to-end workflow from FHIR-coded data to OMOP CDM placement. Text search, semantic search, autocomplete, and facets. Connect Claude, Cursor, or VS Code to medical vocabularies via the MCP Server. Typed responses, async support, batch helpers. R6 client for vocabulary access in R workflows. Complete error code reference and troubleshooting. Per-plan limits, batch metering, and the rate-limit response headers. # Batch Map Concepts Source: https://docs.omophub.com/api-reference/mappings/batch-map-concepts POST https://api.omophub.com/v1/concepts/map/batch Perform multiple OMOP concept mapping operations in a single batched request - high-throughput cross-vocabulary translation for ETL pipelines. ## Overview This endpoint allows you to perform multiple concept mapping operations in a single API call. Each mapping request maps a set of source concepts to a target vocabulary, enabling efficient bulk processing. ## Request Body Array of mapping requests (1-50 items) Unique identifier for this mapping request (for tracking results) Array of OMOP concept IDs to map (minimum 1 integer) Target vocabulary ID to map to (e.g., "ICD10CM", "SNOMED", "ICD9CM") ## Query Parameters Specific vocabulary release version (e.g., "2025.1") ## Response Indicates if the batch request was processed successfully Array of mapping results matching the input order The request ID provided in the input Array of source concept IDs that were mapped The target vocabulary for this mapping request Array of concept mappings found The source OMOP concept ID that was mapped The target OMOP concept ID in the target vocabulary Human-readable name of the target concept The code of the target concept in its vocabulary The relationship type (e.g., "Maps to", "Maps to value") Whether this individual mapping request succeeded Error message if the mapping request failed (null if successful) Index of this request in the original input array Response metadata and API information Unique request identifier for debugging ISO 8601 timestamp of the response Vocabulary release version used ```bash cURL theme={null} curl -X POST "https://api.omophub.com/v1/concepts/map/batch" \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "mapping_requests": [ { "request_id": "diabetes_icd10", "source_concepts": [201826, 443735], "target_vocabulary": "ICD10CM" }, { "request_id": "hypertension_snomed", "source_concepts": [4182210], "target_vocabulary": "SNOMED" } ] }' ``` ```javascript JavaScript theme={null} const response = await fetch('https://api.omophub.com/v1/concepts/map/batch', { method: 'POST', headers: { 'Authorization': 'Bearer YOUR_API_KEY', 'Content-Type': 'application/json' }, body: JSON.stringify({ mapping_requests: [ { request_id: "diabetes_icd10", source_concepts: [201826, 443735], target_vocabulary: "ICD10CM" }, { request_id: "hypertension_snomed", source_concepts: [4182210], target_vocabulary: "SNOMED" } ] }) }); const result = await response.json(); for (const item of result.data) { console.log(`Request ${item.request_id}: ${item.success ? 'Success' : 'Failed'}`); if (item.success) { console.log(` Found ${item.mappings.length} mappings to ${item.target_vocabulary}`); } else { console.log(` Error: ${item.error}`); } } ``` ```python Python theme={null} import requests response = requests.post( "https://api.omophub.com/v1/concepts/map/batch", headers={ "Authorization": "Bearer YOUR_API_KEY", "Content-Type": "application/json" }, json={ "mapping_requests": [ { "request_id": "diabetes_icd10", "source_concepts": [201826, 443735], "target_vocabulary": "ICD10CM" }, { "request_id": "hypertension_snomed", "source_concepts": [4182210], "target_vocabulary": "SNOMED" } ] } ) result = response.json() for item in result["data"]: status = "Success" if item["success"] else "Failed" print(f"Request {item['request_id']}: {status}") if item["success"]: print(f" Found {len(item['mappings'])} mappings to {item['target_vocabulary']}") else: print(f" Error: {item['error']}") ``` ```json theme={null} { "success": true, "data": [ { "request_id": "diabetes_icd10", "source_concepts": [201826, 443735], "target_vocabulary": "ICD10CM", "mappings": [ { "source_concept_id": 201826, "target_concept_id": 443735, "target_concept_name": "Type 2 diabetes mellitus without complications", "target_concept_code": "E11.9", "relationship_id": "Maps to" } ], "success": true, "error": null, "request_index": 0 }, { "request_id": "hypertension_snomed", "source_concepts": [4182210], "target_vocabulary": "SNOMED", "mappings": [ { "source_concept_id": 4182210, "target_concept_id": 316866, "target_concept_name": "Hypertensive disorder", "target_concept_code": "38341003", "relationship_id": "Maps to" } ], "success": true, "error": null, "request_index": 1 } ], "meta": { "request_id": "req_batch_abc123", "timestamp": "2024-12-22T10:00:00Z", "vocab_release": "2025.2" } } ``` ## Important Notes * **Batch size limit**: Maximum 50 mapping requests per batch * **Error isolation**: Individual mapping failures don't affect other mappings in the batch * **Result ordering**: Results are returned in the same order as input requests * **Request IDs**: Use unique `request_id` values to correlate results with your input ## Related Endpoints * [Get Concept Mappings](/api-reference/mappings/get-concept-mappings) - Get mappings for a single concept * [Validate Mappings](/api-reference/mappings/validate-mappings) - Validate existing mappings # Get Concept Mappings Source: https://docs.omophub.com/api-reference/mappings/get-concept-mappings GET /concepts/{concept_id}/mappings ## Overview Retrieve all mappings from a concept to equivalent or related concepts in other vocabulary systems. ## Path Parameters The OMOP concept ID to get mappings for ## Query Parameters Filter mappings to a specific target vocabulary (e.g., "ICD10CM", "SNOMED") Include invalid/deprecated mappings in results Specific vocabulary release version to use (e.g., "2025.1", "2024.2"). When not specified, uses the default/latest vocabulary version. ## Response Indicates if the request was successful Contains the mapping results Array of concept mappings OMOP concept ID of the source concept Name of the source concept OMOP concept ID of the mapped target concept Name of the target concept Type of relationship (e.g., "Maps to") Confidence score for the mapping (0.0 to 1.0) Response metadata Unique request identifier for tracing and support ISO 8601 timestamp of when the request was processed Vocabulary release version used for this request ## Examples ```bash cURL theme={null} curl -X GET "https://api.omophub.com/v1/concepts/320128/mappings?target_vocabulary=ICD10CM" \ -H "Authorization: Bearer YOUR_API_KEY" ``` ```javascript JavaScript theme={null} const response = await fetch('https://api.omophub.com/v1/concepts/320128/mappings?target_vocabulary=ICD10CM', { method: 'GET', headers: { 'Authorization': 'Bearer YOUR_API_KEY', 'Content-Type': 'application/json' } }); const result = await response.json(); console.log(`Found ${result.data.mappings.length} mappings`); ``` ```python Python theme={null} import requests params = {"target_vocabulary": "ICD10CM"} response = requests.get( "https://api.omophub.com/v1/concepts/320128/mappings", headers={"Authorization": "Bearer YOUR_API_KEY"}, params=params ) result = response.json() print(f"Found {len(result['data']['mappings'])} mappings") ``` ```json Response theme={null} { "success": true, "data": { "mappings": [ { "source_concept_id": 320128, "source_concept_name": "Essential hypertension", "target_concept_id": 319826, "target_concept_name": "Essential hypertension", "relationship_id": "Maps to", "confidence": 1.0 } ] }, "meta": { "request_id": "req_abc123", "timestamp": "2025-01-05T12:00:00.000Z", "vocab_release": "2025.1" } } ``` ## Related Endpoints * [Map Concepts](/api-reference/mappings/map-concepts) - Map multiple concepts to a target vocabulary * [Batch Map Concepts](/api-reference/mappings/batch-map-concepts) - Batch mapping operations # Get Mapping Coverage Source: https://docs.omophub.com/api-reference/mappings/get-mapping-coverage GET /v1/mappings/coverage Analyze mapping coverage between a source and target OMOP vocabulary - quantify how many concepts translate across SNOMED, ICD-10, LOINC, and RxNorm. ## Overview This endpoint analyzes mapping coverage between two vocabularies, providing domain-level breakdown and identifying unmapped concepts. Useful for understanding interoperability between vocabulary pairs. ## Query Parameters Source vocabulary ID to analyze mappings from
**Example:** `SNOMED`
Target vocabulary ID to analyze mappings to
**Example:** `ICD10CM`
Comma-separated domain IDs to filter the analysis
**Example:** `Condition,Drug`
Specific vocabulary release version (e.g., "2025.1") ## Response Indicates if the request was successful Coverage analysis results Source vocabulary ID analyzed Target vocabulary ID analyzed Coverage breakdown by domain Domain identifier Human-readable domain name Number of source concepts in this domain Number of concepts with mappings to target vocabulary Percentage of concepts with mappings (0-100) Overall coverage percentage across all domains (0-100) Sample of concepts without mappings (up to 100) Concept identifier Concept name Domain of the concept Reason for no mapping Response metadata and API information Unique request identifier for debugging ISO 8601 timestamp of the response Vocabulary release version used ```bash cURL theme={null} curl -X GET "https://api.omophub.com/v1/mappings/coverage?source_vocabulary=SNOMED&target_vocabulary=ICD10CM" \ -H "Authorization: Bearer YOUR_API_KEY" ``` ```javascript JavaScript theme={null} const response = await fetch('https://api.omophub.com/v1/mappings/coverage?source_vocabulary=SNOMED&target_vocabulary=ICD10CM', { headers: { 'Authorization': 'Bearer YOUR_API_KEY' } }); const result = await response.json(); console.log(`Overall coverage: ${result.data.overall_coverage}%`); result.data.domain_coverage.forEach(domain => { console.log(`${domain.domain_name}: ${domain.coverage_percentage}% (${domain.mapped_concepts}/${domain.source_concepts})`); }); ``` ```python Python theme={null} import requests response = requests.get( "https://api.omophub.com/v1/mappings/coverage", params={ "source_vocabulary": "SNOMED", "target_vocabulary": "ICD10CM" }, headers={"Authorization": "Bearer YOUR_API_KEY"} ) result = response.json() print(f"Overall coverage: {result['data']['overall_coverage']}%") for domain in result['data']['domain_coverage']: print(f"{domain['domain_name']}: {domain['coverage_percentage']}%") ``` ```json theme={null} { "success": true, "data": { "source_vocabulary": "SNOMED", "target_vocabulary": "ICD10CM", "domain_coverage": [ { "domain_id": "Condition", "domain_name": "Condition", "source_concepts": 234567, "mapped_concepts": 178234, "coverage_percentage": 75.9 }, { "domain_id": "Procedure", "domain_name": "Procedure", "source_concepts": 89234, "mapped_concepts": 45678, "coverage_percentage": 51.2 }, { "domain_id": "Observation", "domain_name": "Observation", "source_concepts": 45678, "mapped_concepts": 12345, "coverage_percentage": 27.0 } ], "overall_coverage": 65.5, "unmapped_concepts": [ { "concept_id": 4087682, "concept_name": "Finding related to ability to move", "domain_id": "Condition", "reason": "No direct mapping" }, { "concept_id": 4273391, "concept_name": "Coronary artery bypass graft", "domain_id": "Procedure", "reason": "No direct mapping" } ] }, "meta": { "request_id": "req_coverage_abc123", "timestamp": "2024-12-22T10:00:00Z", "vocab_release": "2025.2" } } ``` ## Usage Examples ### Basic Coverage Analysis Analyze coverage from SNOMED to ICD-10-CM: ```javascript theme={null} const coverage = await fetch('/v1/mappings/coverage?source_vocabulary=SNOMED&target_vocabulary=ICD10CM'); ``` ### Filter by Domain Analyze coverage for specific domains only: ```javascript theme={null} const conditionCoverage = await fetch('/v1/mappings/coverage?source_vocabulary=SNOMED&target_vocabulary=ICD10CM&domain_ids=Condition,Procedure'); ``` ### Use Specific Vocabulary Version Analyze coverage for a specific vocabulary release: ```javascript theme={null} const coverage = await fetch('/v1/mappings/coverage?source_vocabulary=SNOMED&target_vocabulary=ICD10CM&vocab_release=2025.1'); ``` ## Related Endpoints * [Get Concept Mappings](/api-reference/mappings/get-concept-mappings) - Mappings for specific concepts * [Get Mapping Quality](/api-reference/mappings/get-mapping-quality) - Quality metrics between vocabularies * [Map Concepts](/api-reference/mappings/map-concepts) - Map concepts to a target vocabulary ## Notes * Both `source_vocabulary` and `target_vocabulary` are required parameters * The `unmapped_concepts` array contains a sample of up to 100 unmapped concepts * Coverage percentages are calculated based on valid, non-deprecated concepts * Use `domain_ids` to focus analysis on specific medical domains # Get Mapping Quality Source: https://docs.omophub.com/api-reference/mappings/get-mapping-quality GET /v1/mappings/quality/{source_vocabulary}/{target_vocabulary} Analyze OMOP mapping quality between two vocabularies to assess reliability and trustworthiness of cross-vocabulary translations for production use. This endpoint provides analysis of mapping quality between a specific vocabulary pair, essential for understanding the reliability of cross-vocabulary translations, identifying high-confidence mappings, and assessing the trustworthiness of mapping relationships for clinical and operational use. ## Path Parameters The source vocabulary identifier
**Example:** `SNOMED`, `ICD10CM`, `RxNorm`
The target vocabulary identifier
**Example:** `ICD10CM`, `HCPCS`, `NDC`
## Query Parameters Specific vocabulary release version to query
**Example:** `2025.1`
## Response High-level summary of mapping quality Total number of mappings included in analysis Mappings that have quality metadata Overall average confidence score across all mappings Overall quality assessment
**Values:** `excellent`, `good`, `moderate`, `poor`
Percentage of mappings with confidence > 0.8 (0-100%) Percentage of officially validated mappings (0-100%) Percentage of disputed or problematic mappings
Quality metrics for each vocabulary pair Quality metrics for each source vocabulary Human-readable vocabulary name Quality metrics by target vocabulary Quality metrics for this vocabulary pair Target vocabulary name Number of mappings between these vocabularies Average confidence score for this pair Median confidence score Standard deviation of confidence scores Distribution of equivalence types Rate of validated mappings (0.0-1.0 fraction) Quality rating for this vocabulary pair Main use cases for these mappings Average quality of outgoing mappings Quality analysis by medical domain Quality metrics for each domain Human-readable domain name Number of mappings in this domain Average confidence score for domain Distribution of quality levels Mappings with confidence score of 0.9 or higher Mappings with confidence score between 0.7 and 0.9 Mappings with confidence score between 0.5 and 0.7 Mappings with confidence score below 0.5 Vocabulary pairs with highest quality in this domain Vocabulary pairs with lowest quality in this domain Common quality issues in this domain Detailed quality metrics and distributions Distribution of confidence scores Confidence score ranges and counts Key percentile values Skewness of confidence distribution Kurtosis of confidence distribution Analysis of semantic similarity scores Average semantic similarity score Correlation between similarity and confidence Mappings with low similarity but high confidence Mappings with high similarity but low confidence Quality breakdown by mapping source Quality metrics for official mappings Quality metrics for community mappings Quality metrics for algorithmic mappings Quality metrics for manually created mappings Impact of validation on quality perception Quality comparison between validated and unvalidated Accuracy of validation process Common characteristics of disputed mappings Analysis of quality outliers and anomalies (when include\_outliers=true) Mappings with unusually low quality scores Source concept identifier Target concept identifier Confidence score Why this is considered an outlier Potential quality issues identified Mappings with exceptionally high quality Mappings with inconsistent quality indicators Common patterns in outlier mappings Historical quality trends (when include\_trends=true) Time series of quality improvements Date of measurement Average confidence at this time Validation rate at this time Quality of newly added mappings Annual rate of quality improvement (0.0-1.0 fraction) Areas with most rapid quality improvement Areas with declining quality Recommendations for quality improvement (when include\_recommendations=true) Type of quality improvement recommendation Brief title of the recommendation Detailed description Implementation priority Expected quality improvement Vocabulary pairs that would benefit Estimated implementation effort Analysis metadata and processing information Date when quality analysis was performed Timestamp of underlying mapping data Scope of the quality analysis Time taken to perform analysis Method used to calculate confidence scores ```bash cURL theme={null} curl -X GET "https://api.omophub.com/v1/mappings/quality/SNOMED/ICD10CM" \ -H "Authorization: Bearer YOUR_API_KEY" ``` ```bash cURL (with vocab_release) theme={null} curl -X GET "https://api.omophub.com/v1/mappings/quality/SNOMED/ICD10CM?vocab_release=2025.1" \ -H "Authorization: Bearer YOUR_API_KEY" ``` ```javascript JavaScript theme={null} const response = await fetch('https://api.omophub.com/v1/mappings/quality/SNOMED/ICD10CM', { method: 'GET', headers: { 'Authorization': 'Bearer YOUR_API_KEY' } }); const qualityData = await response.json(); const { quality_overview } = qualityData.data; console.log('=== Mapping Quality Analysis ==='); console.log(`Total mappings analyzed: ${quality_overview.total_mappings_analyzed.toLocaleString()}`); console.log(`Overall quality rating: ${quality_overview.overall_quality_rating}`); console.log(`Average confidence: ${(quality_overview.overall_average_confidence * 100).toFixed(1)}%`); console.log(`High-quality mappings: ${quality_overview.high_quality_percentage.toFixed(1)}%`); console.log(`Validated mappings: ${quality_overview.validated_percentage.toFixed(1)}%`); // Display domain quality console.log('\n=== Quality by Domain ==='); Object.entries(quality_by_domain).forEach(([domainId, domainData]) => { console.log(`${domainData.domain_name}:`); console.log(` Average confidence: ${(domainData.average_confidence * 100).toFixed(1)}%`); console.log(` Total mappings: ${domainData.total_mappings.toLocaleString()}`); const dist = domainData.quality_distribution; console.log(` Quality distribution:`); console.log(` Excellent: ${dist.excellent.toLocaleString()}`); console.log(` Good: ${dist.good.toLocaleString()}`); console.log(` Moderate: ${dist.moderate.toLocaleString()}`); console.log(` Poor: ${dist.poor.toLocaleString()}`); if (domainData.most_reliable_vocabulary_pairs.length > 0) { console.log(` Most reliable pairs: ${domainData.most_reliable_vocabulary_pairs.slice(0, 3).join(', ')}`); } if (domainData.quality_challenges.length > 0) { console.log(` Quality challenges: ${domainData.quality_challenges.slice(0, 2).join(', ')}`); } }); // Display vocabulary pair quality console.log('\n=== Top Quality Vocabulary Pairs ==='); const vocabPairs = []; Object.entries(qualityData.data.quality_by_vocabulary_pair).forEach(([sourceVocab, sourceData]) => { Object.entries(sourceData.target_vocabularies).forEach(([targetVocab, pairData]) => { vocabPairs.push({ pair: `${sourceVocab} → ${targetVocab}`, confidence: pairData.average_confidence, mappings: pairData.total_mappings, rating: pairData.quality_rating }); }); }); vocabPairs.sort((a, b) => b.confidence - a.confidence) .slice(0, 10) .forEach(pair => { console.log(`${pair.pair}: ${(pair.confidence * 100).toFixed(1)}% avg confidence (${pair.mappings.toLocaleString()} mappings, ${pair.rating})`); }); // Display quality metrics if (quality_metrics) { console.log('\n=== Quality Metrics ==='); if (quality_metrics.confidence_score_distribution) { const confDist = quality_metrics.confidence_score_distribution; console.log('Confidence Score Statistics:'); console.log(` Mean: ${(qualityData.data.quality_overview.overall_average_confidence * 100).toFixed(1)}%`); if (confDist.percentiles) { console.log(` 25th percentile: ${(confDist.percentiles['25'] * 100).toFixed(1)}%`); console.log(` 50th percentile: ${(confDist.percentiles['50'] * 100).toFixed(1)}%`); console.log(` 75th percentile: ${(confDist.percentiles['75'] * 100).toFixed(1)}%`); } } if (quality_metrics.mapping_source_analysis) { console.log('\nQuality by Mapping Source:'); Object.entries(quality_metrics.mapping_source_analysis).forEach(([source, data]) => { if (data.average_confidence) { console.log(` ${source}: ${(data.average_confidence * 100).toFixed(1)}% avg confidence`); } }); } } // Display recommendations if (qualityData.data.recommendations) { console.log('\n=== Quality Improvement Recommendations ==='); qualityData.data.recommendations.slice(0, 5).forEach((rec, i) => { console.log(`${i + 1}. ${rec.title} (${rec.priority} priority)`); console.log(` ${rec.description}`); console.log(` Expected improvement: ${rec.expected_improvement}`); if (rec.affected_vocabulary_pairs.length > 0) { console.log(` Affects: ${rec.affected_vocabulary_pairs.slice(0, 3).join(', ')}`); } }); } ``` ```python Python theme={null} import requests source_vocab = "SNOMED" target_vocab = "ICD10CM" url = f"https://api.omophub.com/v1/mappings/quality/{source_vocab}/{target_vocab}" headers = { "Authorization": "Bearer YOUR_API_KEY" } response = requests.get(url, headers=headers) data = response.json() print("=== MAPPING QUALITY ANALYSIS ===") overview = data['data']['quality_overview'] print(f"Total mappings analyzed: {overview['total_mappings_analyzed']:,}") print(f"Overall quality rating: {overview['overall_quality_rating']}") print(f"Average confidence: {overview['overall_average_confidence']:.3f}") print(f"High-quality mappings: {overview['high_quality_percentage']:.1f}%") print(f"Validated mappings: {overview['validated_percentage']:.1f}%") print(f"Disputed mappings: {overview['disputed_percentage']:.1f}%") # Domain quality analysis print(f"\n=== QUALITY BY DOMAIN ===") domain_quality = data['data']['quality_by_domain'] domain_df = pd.DataFrame([ { 'domain': info['domain_name'], 'total_mappings': info['total_mappings'], 'avg_confidence': info['average_confidence'], 'excellent': info['quality_distribution']['excellent'], 'good': info['quality_distribution']['good'], 'moderate': info['quality_distribution']['moderate'], 'poor': info['quality_distribution']['poor'] } for domain_id, info in domain_quality.items() ]) domain_df = domain_df.sort_values('avg_confidence', ascending=False) print(domain_df.to_string(index=False, float_format='%.3f')) # Vocabulary pair quality analysis print(f"\n=== VOCABULARY PAIR QUALITY ===") vocab_pairs = [] for source_vocab, source_data in data['data']['quality_by_vocabulary_pair'].items(): for target_vocab, pair_data in source_data['target_vocabularies'].items(): vocab_pairs.append({ 'source': source_vocab, 'target': target_vocab, 'pair': f"{source_vocab} → {target_vocab}", 'avg_confidence': pair_data['average_confidence'], 'total_mappings': pair_data['total_mappings'], 'validation_rate': pair_data['validation_rate'], 'quality_rating': pair_data['quality_rating'] }) vocab_pair_df = pd.DataFrame(vocab_pairs) vocab_pair_df = vocab_pair_df.sort_values('avg_confidence', ascending=False) print("Top 10 Quality Vocabulary Pairs:") top_pairs = vocab_pair_df.head(10) for _, row in top_pairs.iterrows(): print(f"{row['pair']}: {row['avg_confidence']:.3f} confidence, {row['total_mappings']:,} mappings ({row['quality_rating']})") print("\nBottom 5 Quality Vocabulary Pairs:") bottom_pairs = vocab_pair_df.tail(5) for _, row in bottom_pairs.iterrows(): print(f"{row['pair']}: {row['avg_confidence']:.3f} confidence, {row['total_mappings']:,} mappings ({row['quality_rating']})") # Quality metrics analysis if 'quality_metrics' in data['data']: metrics = data['data']['quality_metrics'] print(f"\n=== DETAILED QUALITY METRICS ===") # Confidence distribution if 'confidence_score_distribution' in metrics: conf_dist = metrics['confidence_score_distribution'] print(f"Confidence Score Statistics:") if 'percentiles' in conf_dist: for percentile, value in conf_dist['percentiles'].items(): print(f" {percentile}th percentile: {value:.3f}") if 'skewness' in conf_dist: print(f" Skewness: {conf_dist['skewness']:.3f}") if 'kurtosis' in conf_dist: print(f" Kurtosis: {conf_dist['kurtosis']:.3f}") # Semantic similarity analysis if 'semantic_similarity_analysis' in metrics: sem_analysis = metrics['semantic_similarity_analysis'] print(f"\nSemantic Similarity Analysis:") print(f" Average similarity: {sem_analysis['average_similarity']:.3f}") print(f" Similarity-confidence correlation: {sem_analysis['similarity_confidence_correlation']:.3f}") print(f" Low similarity, high confidence: {sem_analysis['low_similarity_high_confidence']:,}") print(f" High similarity, low confidence: {sem_analysis['high_similarity_low_confidence']:,}") # Mapping source analysis if 'mapping_source_analysis' in metrics: source_analysis = metrics['mapping_source_analysis'] print(f"\nQuality by Mapping Source:") for source, source_data in source_analysis.items(): if 'average_confidence' in source_data: print(f" {source}: {source_data['average_confidence']:.3f} avg confidence") # Outlier analysis if 'quality_outliers' in data['data']: outliers = data['data']['quality_outliers'] print(f"\n=== QUALITY OUTLIERS ===") if outliers.get('low_quality_outliers'): print(f"Low Quality Outliers ({len(outliers['low_quality_outliers'])}):") for outlier in outliers['low_quality_outliers'][:5]: print(f" Concepts {outlier['source_concept_id']} → {outlier['target_concept_id']}") print(f" Confidence: {outlier['confidence_score']:.3f}") print(f" Reason: {outlier['outlier_reason']}") if outliers.get('outlier_patterns'): print(f"\nCommon Outlier Patterns:") for pattern in outliers['outlier_patterns'][:3]: print(f" - {pattern}") # Trends analysis if 'quality_trends' in data['data']: trends = data['data']['quality_trends'] print(f"\n=== QUALITY TRENDS ===") print(f"Quality improvement rate: {trends['quality_improvement_rate']:.2f}% annually") if trends.get('fastest_improving_areas'): print(f"Fastest improving areas:") for area in trends['fastest_improving_areas'][:3]: print(f" - {area}") # Plot quality trend if trends.get('quality_over_time'): quality_history = trends['quality_over_time'] dates = [point['date'] for point in quality_history] confidences = [point['average_confidence'] for point in quality_history] validation_rates = [point['validation_rate'] for point in quality_history] fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(12, 10)) # Confidence trend ax1.plot(dates, confidences, marker='o', linewidth=2) ax1.set_title('Average Confidence Score Over Time') ax1.set_ylabel('Confidence Score') ax1.tick_params(axis='x', rotation=45) ax1.grid(True, alpha=0.3) # Validation rate trend ax2.plot(dates, validation_rates, marker='s', linewidth=2, color='green') ax2.set_title('Validation Rate Over Time') ax2.set_xlabel('Date') ax2.set_ylabel('Validation Rate') ax2.tick_params(axis='x', rotation=45) ax2.grid(True, alpha=0.3) plt.tight_layout() plt.show() # Recommendations if 'recommendations' in data['data']: print(f"\n=== QUALITY IMPROVEMENT RECOMMENDATIONS ===") recommendations = data['data']['recommendations'] for i, rec in enumerate(recommendations[:7], 1): print(f"{i}. {rec['title']} ({rec['priority']} Priority)") print(f" {rec['description']}") print(f" Expected improvement: {rec['expected_improvement']}") print(f" Implementation effort: {rec['implementation_effort']}") if rec.get('affected_vocabulary_pairs'): print(f" Affects: {', '.join(rec['affected_vocabulary_pairs'][:3])}") print() # Create comprehensive quality dashboard fig, ((ax1, ax2), (ax3, ax4)) = plt.subplots(2, 2, figsize=(16, 12)) # 1. Domain quality comparison domain_names = domain_df['domain'] domain_confidences = domain_df['avg_confidence'] ax1.barh(domain_names, domain_confidences, color='skyblue') ax1.set_xlabel('Average Confidence Score') ax1.set_title('Quality by Medical Domain') ax1.set_xlim(0, 1) # 2. Quality distribution pie chart quality_counts = [ domain_df['excellent'].sum(), domain_df['good'].sum(), domain_df['moderate'].sum(), domain_df['poor'].sum() ] quality_labels = ['Excellent', 'Good', 'Moderate', 'Poor'] colors = ['#2ecc71', '#3498db', '#f39c12', '#e74c3c'] ax2.pie(quality_counts, labels=quality_labels, colors=colors, autopct='%1.1f%%') ax2.set_title('Overall Quality Distribution') # 3. Vocabulary pair quality heatmap if len(vocab_pair_df) > 0: # Create pivot table for heatmap pivot_data = vocab_pair_df.pivot_table( values='avg_confidence', index='source', columns='target', fill_value=0 ) sns.heatmap(pivot_data, annot=True, fmt='.2f', cmap='RdYlGn', ax=ax3) ax3.set_title('Vocabulary Pair Quality Matrix') ax3.set_xlabel('Target Vocabulary') ax3.set_ylabel('Source Vocabulary') # 4. Confidence score histogram if 'quality_metrics' in data['data'] and 'confidence_score_distribution' in data['data']['quality_metrics']: conf_dist = data['data']['quality_metrics']['confidence_score_distribution'] if 'histogram_bins' in conf_dist: bins = conf_dist['histogram_bins'] bin_centers = [(bin['min'] + bin['max']) / 2 for bin in bins] bin_counts = [bin['count'] for bin in bins] ax4.bar(bin_centers, bin_counts, width=0.05, alpha=0.7, color='lightcoral') ax4.set_xlabel('Confidence Score') ax4.set_ylabel('Number of Mappings') ax4.set_title('Confidence Score Distribution') ax4.set_xlim(0, 1) plt.tight_layout() plt.show() # Summary report print(f"\n=== SUMMARY REPORT ===") print(f"• Analyzed {overview['total_mappings_analyzed']:,} mappings across {len(domain_quality)} domains") print(f"• Overall system quality rating: {overview['overall_quality_rating'].upper()}") print(f"• {overview['high_quality_percentage']:.1f}% of mappings are high-quality (confidence > 0.8)") print(f"• {overview['validated_percentage']:.1f}% of mappings are officially validated") print(f"• Top quality domain: {domain_df.iloc[0]['domain']} ({domain_df.iloc[0]['avg_confidence']:.3f} avg confidence)") print(f"• Best vocabulary pair: {vocab_pair_df.iloc[0]['pair']} ({vocab_pair_df.iloc[0]['avg_confidence']:.3f} confidence)") if 'recommendations' in data['data']: high_priority_recs = [r for r in data['data']['recommendations'] if r['priority'] == 'high'] print(f"• {len(high_priority_recs)} high-priority improvement recommendations identified") ``` ```json theme={null} { "success": true, "data": { "quality_overview": { "total_mappings_analyzed": 4578923, "mappings_with_quality_data": 3456789, "overall_average_confidence": 0.847, "overall_quality_rating": "good", "high_quality_percentage": 72.3, "validated_percentage": 68.9, "disputed_percentage": 2.7 }, "quality_by_vocabulary_pair": { "SNOMED": { "vocabulary_name": "Systematized Nomenclature of Medicine Clinical Terms", "target_vocabularies": { "ICD10CM": { "target_vocabulary_name": "International Classification of Diseases, Tenth Revision, Clinical Modification", "total_mappings": 156789, "average_confidence": 0.923, "median_confidence": 0.950, "confidence_std_dev": 0.087, "equivalence_distribution": { "exact": 89234, "broader": 45678, "narrower": 12345, "related": 9532 }, "validation_rate": 0.847, "quality_rating": "excellent", "primary_use_cases": ["billing", "quality_reporting", "clinical_documentation"] }, "HCPCS": { "target_vocabulary_name": "Healthcare Common Procedure Coding System", "total_mappings": 67890, "average_confidence": 0.756, "median_confidence": 0.780, "confidence_std_dev": 0.145, "equivalence_distribution": { "exact": 23456, "broader": 34567, "narrower": 6789, "related": 3078 }, "validation_rate": 0.623, "quality_rating": "good", "primary_use_cases": ["billing", "procedure_coding"] } }, "overall_outgoing_quality": 0.875 } }, "quality_by_domain": { "Condition": { "domain_name": "Condition", "total_mappings": 2847562, "average_confidence": 0.891, "quality_distribution": { "excellent": 1456789, "good": 987654, "moderate": 345678, "poor": 57441 }, "most_reliable_vocabulary_pairs": [ "SNOMED → ICD10CM", "ICD10CM → SNOMED", "SNOMED → ICD10" ], "least_reliable_vocabulary_pairs": [ "Read → ICPC2", "Local → Standard" ], "quality_challenges": [ "Complex conditions often map to broader categories", "Regional terminology variations affect consistency", "Rare diseases have limited mapping coverage" ] }, "Drug": { "domain_name": "Drug", "total_mappings": 987654, "average_confidence": 0.823, "quality_distribution": { "excellent": 456789, "good": 345678, "moderate": 123456, "poor": 61731 }, "most_reliable_vocabulary_pairs": [ "RxNorm → NDC", "NDC → RxNorm" ], "least_reliable_vocabulary_pairs": [ "SNOMED → RxNorm", "Local drug codes → RxNorm" ], "quality_challenges": [ "Generic vs brand name mapping complexity", "Dosage form variations affect precision", "Discontinued medications create gaps" ] } }, "quality_metrics": { "confidence_score_distribution": { "histogram_bins": [ {"min": 0.0, "max": 0.1, "count": 12456}, {"min": 0.1, "max": 0.2, "count": 23456}, {"min": 0.8, "max": 0.9, "count": 456789}, {"min": 0.9, "max": 1.0, "count": 678901} ], "percentiles": { "25": 0.734, "50": 0.847, "75": 0.923, "95": 0.978 }, "skewness": -0.342, "kurtosis": 2.156 }, "semantic_similarity_analysis": { "average_similarity": 0.823, "similarity_confidence_correlation": 0.756, "low_similarity_high_confidence": 23456, "high_similarity_low_confidence": 12345 }, "mapping_source_analysis": { "official_mappings": { "average_confidence": 0.912, "total_count": 2345678, "validation_rate": 0.923 }, "community_mappings": { "average_confidence": 0.734, "total_count": 456789, "validation_rate": 0.456 }, "algorithmic_mappings": { "average_confidence": 0.678, "total_count": 567890, "validation_rate": 0.234 } } }, "quality_outliers": { "low_quality_outliers": [ { "source_concept_id": 12345, "target_concept_id": 67890, "confidence_score": 0.123, "outlier_reason": "Very low confidence despite official mapping", "potential_issues": [ "Semantic mismatch", "Outdated mapping relationship" ] } ], "outlier_patterns": [ "Algorithmic mappings between distant concept classes", "Legacy mappings not updated with vocabulary revisions", "Cross-domain mappings with semantic drift" ] }, "recommendations": [ { "recommendation_type": "validation_review", "title": "Review disputed SNOMED to HCPCS mappings", "description": "Systematic review of 2,847 disputed mappings between SNOMED procedures and HCPCS codes to improve validation rate", "priority": "high", "expected_improvement": "Increase HCPCS mapping quality by 15-20%", "affected_vocabulary_pairs": ["SNOMED → HCPCS"], "implementation_effort": "moderate" }, { "recommendation_type": "algorithmic_improvement", "title": "Enhance semantic similarity algorithms for drug mappings", "description": "Improve algorithmic mapping quality between drug vocabularies using enhanced semantic similarity models", "priority": "medium", "expected_improvement": "Increase drug domain confidence by 10%", "affected_vocabulary_pairs": ["RxNorm → NDC", "SNOMED → RxNorm"], "implementation_effort": "high" } ] }, "meta": { "request_id": "req_quality_analysis_234567", "timestamp": "2024-12-22T10:00:00Z", "vocab_release": "2025.2", "analysis_scope": { "vocabularies_included": 23, "domains_analyzed": 15, "mapping_relationships_analyzed": 4578923 }, "confidence_calculation_method": "weighted_semantic_similarity_with_validation" } } ``` ## Usage Examples ### SNOMED to ICD10CM Quality Analyze mapping quality between SNOMED and ICD10CM: ```javascript theme={null} const quality = await fetch('/v1/mappings/quality/SNOMED/ICD10CM'); ``` ### RxNorm to NDC Quality Analyze drug vocabulary mapping quality: ```javascript theme={null} const drugQuality = await fetch('/v1/mappings/quality/RxNorm/NDC'); ``` ### With Specific Vocabulary Release Query quality metrics for a specific vocabulary version: ```javascript theme={null} const versionedQuality = await fetch('/v1/mappings/quality/SNOMED/HCPCS?vocab_release=2025.1'); ``` ## Related Endpoints * [Get Concept Mappings](/api-reference/mappings/get-concept-mappings) - Individual concept mapping quality * [Get Vocabulary Mappings](/api-reference/mappings/get-vocabulary-mappings) - Quality within vocabulary pairs * [Get Mapping Coverage](/api-reference/mappings/get-mapping-coverage) - Coverage vs quality analysis ## Notes * Quality analysis requires substantial computational resources for large datasets * Confidence scores are calculated using multiple factors including semantic similarity and validation status * Official mappings generally have higher quality than algorithmic or community mappings * Quality can vary significantly between vocabulary pairs and domains * Disputed mappings may indicate areas needing expert review * Quality trends help identify improvement or degradation over time * Outlier analysis reveals mappings that may need special attention * Quality thresholds help focus on the most reliable mappings for production use # Map Concepts Source: https://docs.omophub.com/api-reference/mappings/map-concepts POST https://api.omophub.com/v1/concepts/map Map medical concepts between OMOP vocabularies to find equivalent or related concepts across SNOMED, ICD-10, LOINC, RxNorm, and other terminologies. ## Overview This endpoint maps medical concepts from source vocabularies to target vocabularies, finding equivalent or related concepts across different medical terminology systems. It's essential for healthcare data integration, cross-system interoperability, and clinical data standardization. ## Request Body Array of source OMOP concept IDs to map (1-100 concepts). Use this OR `source_codes`, not both.
**Important**: These must be OMOP concept IDs (internal database IDs), NOT vocabulary-specific codes. Use `/v1/concepts/by-code/{vocabulary}/{code}` to look up concept IDs from codes, or use `source_codes` parameter instead.
**Example**: `[201826, 192671]` (OMOP concept IDs for Type 2 diabetes and Myocardial infarction)
Array of vocabulary/code pairs to map (1-100 concepts). Use this OR `source_concepts`, not both.
This allows mapping directly from source system codes (e.g., SNOMED codes) without first looking up the OMOP concept ID.
**Example**: ```json theme={null} [ {"vocabulary_id": "SNOMED", "concept_code": "44054006"}, {"vocabulary_id": "SNOMED", "concept_code": "22298006"} ] ```
Target vocabulary identifier to map concepts to
**Examples**: `"ICD10CM"`, `"SNOMED"`, `"ICD9CM"`, `"LOINC"`, `"RXNORM"`
Type of concept mapping to filter results by. When not specified, all mapping types are returned.
**Options**: * `"direct"` - Direct mappings only (Maps to, Mapped from) * `"equivalent"` - Equivalent concepts (Maps to, Source - contains, Has precise ingredient) * `"broader"` - Broader concepts (Is a, Subsumes, Has ingredient) * `"narrower"` - Narrower concepts (Subsumes, Consists of)
Include invalid/deprecated concepts in mapping results ## Query Parameters Specific vocabulary release version to use for mapping (e.g., "2025.1", "2024.2"). When not specified, uses the default/latest vocabulary version. ## Response Indicates if the request was successful Error information (present only on error responses) Error code identifier (e.g., "INVALID\_VOCABULARY", "RATE\_LIMIT\_EXCEEDED") Human-readable error message Additional error details including supported vocabularies and validation failures List of supported vocabulary names when vocabulary validation fails Contains the concept mapping results Array of concept mappings found OMOP concept ID of the source concept Name of the source concept Vocabulary of the source concept OMOP concept ID of the mapped target concept Name of the target concept Vocabulary of the target concept Original source code in the target vocabulary Type of relationship between concepts Human-readable relationship description Confidence score for the mapping (0.0 to 1.0) Start date when the mapping became valid End date when the mapping expires Original source code in the source vocabulary Reason if the mapping is invalid, null otherwise Classification of the mapping: direct, equivalent, broader, narrower, or related Response metadata Unique request identifier for tracing and support ISO 8601 timestamp of when the request was processed Vocabulary release version used for this request ```bash cURL (ICD-10 to SNOMED) theme={null} curl -X POST "https://api.omophub.com/v1/concepts/map" \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "source_codes": [ {"vocabulary_id": "ICD10CM", "concept_code": "E11.9"}, {"vocabulary_id": "ICD10CM", "concept_code": "I21.9"} ], "target_vocabulary": "SNOMED", "mapping_type": "direct" }' ``` ```bash cURL (with OMOP concept IDs) theme={null} curl -X POST "https://api.omophub.com/v1/concepts/map" \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "source_concepts": [45576876, 45591524], "target_vocabulary": "SNOMED", "mapping_type": "direct", "include_invalid": false }' ``` ```javascript JavaScript theme={null} // Map ICD-10 codes to SNOMED const response = await fetch('https://api.omophub.com/v1/concepts/map', { method: 'POST', headers: { 'Authorization': 'Bearer YOUR_API_KEY', 'Content-Type': 'application/json' }, body: JSON.stringify({ source_codes: [ { vocabulary_id: "ICD10CM", concept_code: "E11.9" }, { vocabulary_id: "ICD10CM", concept_code: "I21.9" } ], target_vocabulary: "SNOMED", mapping_type: "direct" }) }); const data = await response.json(); ``` ```python Python theme={null} import requests url = "https://api.omophub.com/v1/concepts/map" # Map ICD-10 codes to SNOMED standard concepts payload = { "source_codes": [ {"vocabulary_id": "ICD10CM", "concept_code": "E11.9"}, {"vocabulary_id": "ICD10CM", "concept_code": "I21.9"} ], "target_vocabulary": "SNOMED", "mapping_type": "direct" } response = requests.post( url, json=payload, headers={"Authorization": "Bearer YOUR_API_KEY"} ) data = response.json() ``` ```json Response theme={null} { "success": true, "data": { "mappings": [ { "source_concept_id": 45576876, "source_concept_name": "Type 2 diabetes mellitus without complications", "source_concept_code": "E11.9", "source_vocabulary_id": "ICD10CM", "target_concept_id": 201826, "target_concept_name": "Type 2 diabetes mellitus", "target_concept_code": "44054006", "target_vocabulary_id": "SNOMED", "relationship_id": "Maps to", "relationship_name": "Maps to", "valid_start_date": "2023-01-01", "valid_end_date": "2099-12-31", "invalid_reason": null, "mapping_confidence": 1.0, "mapping_type": "direct" }, { "source_concept_id": 45591524, "source_concept_name": "Acute myocardial infarction, unspecified", "source_concept_code": "I21.9", "source_vocabulary_id": "ICD10CM", "target_concept_id": 4329847, "target_concept_name": "Myocardial infarction", "target_concept_code": "22298006", "target_vocabulary_id": "SNOMED", "relationship_id": "Maps to", "relationship_name": "Maps to", "valid_start_date": "2023-01-01", "valid_end_date": "2099-12-31", "invalid_reason": null, "mapping_confidence": 1.0, "mapping_type": "direct" } ] }, "meta": { "request_id": "req_abc123", "timestamp": "2025-01-05T12:00:00.000Z", "vocab_release": "2025.1" } } ``` ## Understanding OMOP Mapping Direction In OMOP, mappings go from **non-standard** concepts TO **standard** concepts. This means: * **ICD-10-CM → SNOMED** works (ICD-10 is non-standard, SNOMED is standard) * **SNOMED → ICD-10-CM** often returns empty (SNOMED concepts are already standard - they are mapping *targets*, not *sources*) If you get empty results, check whether your source concept is already a standard concept. Use `GET /v1/concepts/{id}` to check the `standard_concept` field - if it's `"S"`, try mapping in the opposite direction. ## Usage Examples ### ICD-10 to SNOMED (most common use case) Map ICD-10-CM diagnosis codes to SNOMED standard concepts: ```json theme={null} { "source_codes": [ {"vocabulary_id": "ICD10CM", "concept_code": "E11.9"}, {"vocabulary_id": "ICD10CM", "concept_code": "I21.9"} ], "target_vocabulary": "SNOMED", "mapping_type": "direct" } ``` ### Cross-System Integration Map multiple ICD-10 codes for EHR standardization: ```json theme={null} { "source_codes": [ {"vocabulary_id": "ICD10CM", "concept_code": "E11.9"}, {"vocabulary_id": "ICD10CM", "concept_code": "I21.9"}, {"vocabulary_id": "ICD10CM", "concept_code": "J44.1"} ], "target_vocabulary": "SNOMED", "mapping_type": "direct", "include_invalid": false } ``` ### Drug Mapping Map drug codes from NDC to RxNorm: ```json theme={null} { "source_codes": [ {"vocabulary_id": "NDC", "concept_code": "00002-4462-30"}, {"vocabulary_id": "NDC", "concept_code": "00093-3147-01"} ], "target_vocabulary": "RxNorm", "mapping_type": "equivalent" } ``` ### Using OMOP Concept IDs If you already have OMOP concept IDs (e.g., from a search result), use `source_concepts` instead of `source_codes`: ```json theme={null} { "source_concepts": [45576876, 45591524], "target_vocabulary": "SNOMED", "mapping_type": "direct" } ``` The `source_concepts` parameter expects **OMOP concept IDs** (internal database IDs), NOT vocabulary-specific codes. If you have vocabulary codes (e.g., ICD-10 code "E11.9"), use the `source_codes` parameter instead, or first look up the OMOP concept ID using `/v1/concepts/by-code/{vocabulary}/{code}`. ## Related Endpoints * [Batch Map Concepts](/api-reference/mappings/batch-map-concepts) - Batch mapping operations * [Get Concept Mappings](/api-reference/mappings/get-concept-mappings) - Get mappings for a specific concept * [Validate Mappings](/api-reference/mappings/validate-mappings) - Validate mapping accuracy # Validate Mappings Source: https://docs.omophub.com/api-reference/mappings/validate-mappings POST https://api.omophub.com/v1/mappings/validate Validate the accuracy and quality of OMOP concept mappings between vocabularies - verify translations before committing them to source_to_concept_map. ## Overview This endpoint validates concept mappings between vocabularies, checking for concept validity, domain compatibility, and relationship correctness. It returns a validation score and identifies potential issues with each mapping. ## Request Body Array of concept mappings to validate (1-100 items) The source OMOP concept ID The target OMOP concept ID to validate mapping to The relationship type of the mapping (e.g., "Maps to", "Maps to value", "direct") ## Query Parameters Specific vocabulary release version (e.g., "2025.1") ## Response Indicates if the request was successful Array of validation results matching the input order The source concept ID that was validated The target concept ID that was validated The mapping relationship type that was validated Whether the mapping is considered valid (validation\_score >= 0.50) Overall validation score (0.0-1.0). Score starts at 1.0 and is reduced based on issues found. Array of validation issue descriptions (strings) Array of recommendations to improve the mapping (strings) Response metadata and API information Unique request identifier for debugging ISO 8601 timestamp of the response Vocabulary release version used ```bash cURL theme={null} curl -X POST "https://api.omophub.com/v1/mappings/validate" \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "mappings": [ { "source_concept_id": 201826, "target_concept_id": 443735, "mapping_type": "Maps to" }, { "source_concept_id": 4182210, "target_concept_id": 320128, "mapping_type": "Maps to" } ] }' ``` ```javascript JavaScript theme={null} const response = await fetch('https://api.omophub.com/v1/mappings/validate', { method: 'POST', headers: { 'Authorization': 'Bearer YOUR_API_KEY', 'Content-Type': 'application/json' }, body: JSON.stringify({ mappings: [ { source_concept_id: 201826, target_concept_id: 443735, mapping_type: "Maps to" } ] }) }); const result = await response.json(); for (const validation of result.data) { console.log(`${validation.source_concept_id} → ${validation.target_concept_id}`); console.log(` Valid: ${validation.is_valid}, Score: ${validation.validation_score}`); if (validation.issues.length > 0) { console.log(` Issues: ${validation.issues.join(', ')}`); } } ``` ```python Python theme={null} import requests response = requests.post( "https://api.omophub.com/v1/mappings/validate", headers={ "Authorization": "Bearer YOUR_API_KEY", "Content-Type": "application/json" }, json={ "mappings": [ { "source_concept_id": 201826, "target_concept_id": 443735, "mapping_type": "Maps to" } ] } ) result = response.json() for validation in result["data"]: print(f"{validation['source_concept_id']} → {validation['target_concept_id']}") print(f" Valid: {validation['is_valid']}, Score: {validation['validation_score']}") if validation["issues"]: print(f" Issues: {', '.join(validation['issues'])}") ``` ```json theme={null} { "success": true, "data": [ { "source_concept_id": 201826, "target_concept_id": 443735, "mapping_type": "Maps to", "is_valid": true, "validation_score": 0.85, "issues": [], "recommendations": [] }, { "source_concept_id": 4182210, "target_concept_id": 320128, "mapping_type": "Maps to", "is_valid": false, "validation_score": 0.40, "issues": [ "No direct mapping relationship exists", "Domain mismatch between concepts" ], "recommendations": [ "Verify mapping relationship type is correct", "Consider using a target concept in the same domain" ] } ], "meta": { "request_id": "req_validate_abc123", "timestamp": "2024-12-22T10:00:00Z", "vocab_release": "2025.2" } } ``` ## Validation Scoring The validation score starts at 1.0 (100%) and is reduced based on issues found: | Issue | Score Reduction | | --------------------------------------- | --------------- | | Invalid source concept | -0.40 | | Invalid target concept | -0.40 | | Domain mismatch | -0.20 | | Standard to non-standard mapping | -0.15 | | No relationship exists between concepts | -0.25 | A mapping is considered **valid** when `validation_score >= 0.50`. ## Related Endpoints * [Get Concept Mappings](/api-reference/mappings/get-concept-mappings) - Get mappings for a concept * [Batch Map Concepts](/api-reference/mappings/batch-map-concepts) - Map multiple concepts at once # API rate limits, quotas, and throttling headers Source: https://docs.omophub.com/api-reference/rate-limit Understand OMOPHub API rate limits per plan, how to read throttling response headers, and how to request higher quotas for production workloads. The response headers describe your current rate limit following every request in conformance with the [IETF standard](https://datatracker.ietf.org/doc/html/draft-ietf-httpapi-ratelimit-headers): | Header name | Description | | --------------------- | ------------------------------------------------------------------- | | `ratelimit-limit` | Maximum number of requests allowed within a window. | | `ratelimit-remaining` | How many requests you have left within the current window. | | `ratelimit-reset` | How many seconds until the limits are reset. | | `ratelimit-window` | The window size in seconds for the rate limit. | | `retry-after` | How many seconds you should wait before making a follow-up request. | ## Rate Limit Responses The default maximum rate limit is 2 requests per second. This number can be increased upon request. After that, you’ll hit the rate limit and receive a 429 response error code. You can find all 429 responses by filtering for 429 at the OMOPHub Logs page. To prevent this, we recommend reducing the rate at which you request the API. This can be done by introducing a queue mechanism or reducing the number of concurrent requests per second. If you have specific requirements, [contact support](https://omophub.com/contact) to request a rate increase. # List Relationships for a Concept Source: https://docs.omophub.com/api-reference/relationships/get-concept-relationships GET /v1/concepts/{concept_id}/relationships Retrieve all relationships for a specific OMOP concept - Maps to, Is a, ingredient of, and vocabulary-specific links for crosswalk analysis. This endpoint provides relationship information for a concept, showing how it connects to other concepts through various relationship types such as "Is a", "Part of", "Has ingredient", and others in medical vocabularies. ## Path Parameters The unique identifier of the concept to retrieve relationships for
**Example:** `201826` (Type 2 diabetes mellitus)
## Query Parameters Comma-separated list of relationship IDs to filter by
**Example:** `Is a,Maps to,Has finding site`
Filter relationships to specific vocabularies
**Example:** `SNOMED,ICD10CM,RxNorm`
Filter related concepts to specific domains
**Example:** `Condition,Drug,Procedure`
Only return relationships to standard concepts Include relationships to deprecated/invalid concepts Include reverse relationships (where this concept is concept\_id\_2) Page number for pagination (1-based) Number of relationships to return per page (max 1000) Specific vocabulary release version
**Example:** `2025.1`
## Response Array of relationship objects Source concept ID (the concept you queried) Target concept ID (the related concept) Relationship type identifier (e.g., "Is a", "Maps to") Display name of the relationship Reverse relationship identifier (e.g., "Subsumes" for "Is a") Full details of the source concept Concept identifier Concept name Original vocabulary code Vocabulary identifier Vocabulary display name Domain classification Concept class Standard concept flag (S, C, or null) Validity start date Validity end date Reason if invalid (null if valid) Full details of the target concept (same structure as concept\_1) Date when relationship became valid Date when relationship becomes invalid Reason if relationship is invalid (null if valid) Response metadata including pagination Current page number Items per page Total relationships Total number of pages Whether next page exists Whether previous page exists Unique request identifier Request timestamp Vocabulary release version used ```bash cURL theme={null} curl -G "https://api.omophub.com/v1/concepts/201826/relationships" \ -H "Authorization: Bearer YOUR_API_KEY" \ --data-urlencode "relationship_ids=Is a,Maps to" \ --data-urlencode "page_size=50" ``` ```python Python theme={null} import requests concept_id = 201826 url = f"https://api.omophub.com/v1/concepts/{concept_id}/relationships" params = { "relationship_ids": "Is a,Maps to", "vocabulary_ids": "SNOMED", "page_size": 50 } headers = {"Authorization": "Bearer YOUR_API_KEY"} response = requests.get(url, params=params, headers=headers) data = response.json() print(f"Total relationships: {data['meta']['pagination']['total_items']}") for rel in data['data']['relationships']: print(f" {rel['relationship_id']}: {rel['concept_2']['concept_name']}") ``` ```javascript JavaScript theme={null} const params = new URLSearchParams({ 'relationship_ids': 'Is a,Maps to', 'page_size': '50' }); const response = await fetch( `https://api.omophub.com/v1/concepts/201826/relationships?${params}`, { headers: { 'Authorization': 'Bearer YOUR_API_KEY' } } ); const data = await response.json(); console.log(`Found ${data.meta.pagination.total_items} relationships`); ``` ```json theme={null} { "success": true, "data": { "relationships": [ { "concept_id_1": 201826, "concept_id_2": 73211009, "relationship_id": "Is a", "relationship_name": "Is a", "reverse_relationship_id": "Subsumes", "concept_1": { "concept_id": 201826, "concept_name": "Type 2 diabetes mellitus", "concept_code": "44054006", "vocabulary_id": "SNOMED", "vocabulary_name": "SNOMED", "domain_id": "Condition", "concept_class_id": "Clinical Finding", "standard_concept": "S", "valid_start_date": "2002-01-31", "valid_end_date": "2099-12-31", "invalid_reason": null }, "concept_2": { "concept_id": 73211009, "concept_name": "Diabetes mellitus", "concept_code": "73211009", "vocabulary_id": "SNOMED", "vocabulary_name": "SNOMED", "domain_id": "Condition", "concept_class_id": "Clinical Finding", "standard_concept": "S", "valid_start_date": "2002-01-31", "valid_end_date": "2099-12-31", "invalid_reason": null }, "valid_start_date": "2002-01-31", "valid_end_date": "2099-12-31", "invalid_reason": null } ] }, "meta": { "request_id": "req_abc123", "timestamp": "2025-01-04T10:30:00Z", "vocab_release": "2025.1", "version": "2025.1", "duration": 45, "pagination": { "page": 1, "page_size": 100, "total_items": 47, "total_pages": 1, "has_next": false, "has_previous": false } } } ``` ## Usage Examples ### All Relationships Get all relationships for a concept: ```bash theme={null} curl "https://api.omophub.com/v1/concepts/201826/relationships" \ -H "Authorization: Bearer YOUR_API_KEY" ``` ### Hierarchical Relationships Only Retrieve only "Is a" relationships: ```bash theme={null} curl "https://api.omophub.com/v1/concepts/201826/relationships?relationship_ids=Is%20a" \ -H "Authorization: Bearer YOUR_API_KEY" ``` ### Cross-Vocabulary Mappings Find mappings to ICD-10: ```bash theme={null} curl "https://api.omophub.com/v1/concepts/201826/relationships?relationship_ids=Maps%20to&vocabulary_ids=ICD10CM" \ -H "Authorization: Bearer YOUR_API_KEY" ``` ### Including Reverse Relationships Include relationships where this concept is the target: ```bash theme={null} curl "https://api.omophub.com/v1/concepts/201826/relationships?include_reverse=true" \ -H "Authorization: Bearer YOUR_API_KEY" ``` ## Related Endpoints * [Get Relationship Types](/api-reference/relationships/get-relationship-types) - Available relationship types * [Get Concept Ancestors](/api-reference/hierarchy/get-concept-ancestors) - Hierarchical parent relationships * [Get Concept Descendants](/api-reference/hierarchy/get-concept-descendants) - Hierarchical child relationships * [Get Concept Mappings](/api-reference/mappings/get-concept-mappings) - Cross-vocabulary mappings ## Notes * Relationships are bidirectional in the database; use `include_reverse=true` to see both directions * Standard concepts are not filtered by default; use `standard_only=true` to filter * Large concepts may have hundreds of relationships - use pagination appropriately * The response includes full concept details for both source and target concepts # Get Relationship Types Source: https://docs.omophub.com/api-reference/relationships/get-relationship-types GET /v1/relationships/types Retrieve all available OMOP relationship types - Maps to, Is a, Subsumes, and the complete set of OHDSI-defined links between medical concepts. This endpoint returns all relationship types from the OMOP CDM RELATIONSHIP table, which defines the types of connections that can exist between concepts. ## Query Parameters Page number for pagination (1-based) Number of relationship types to return per page (max 500) Specific vocabulary release version
**Example:** `2025.1`
## Response Array of relationship type objects from the OMOP CDM RELATIONSHIP table Unique identifier for the relationship type (e.g., "Is a", "Maps to") Display name of the relationship type Whether this relationship creates hierarchical structure ("0" or "1") Whether this relationship is used in CONCEPT\_ANCESTOR computation ("0" or "1") The relationship\_id of the reverse relationship (e.g., "Subsumes" for "Is a") Concept ID representing this relationship type in the CONCEPT table Response metadata including pagination Current page number Items per page Total relationship types Total number of pages Whether next page exists Whether previous page exists Unique request identifier Vocabulary release version used ```bash cURL theme={null} curl "https://api.omophub.com/v1/relationships/types?page_size=50" \ -H "Authorization: Bearer YOUR_API_KEY" ``` ```python Python theme={null} import requests url = "https://api.omophub.com/v1/relationships/types" params = {"page_size": 50} headers = {"Authorization": "Bearer YOUR_API_KEY"} response = requests.get(url, params=params, headers=headers) data = response.json() print(f"Total relationship types: {data['meta']['pagination']['total_items']}") for rel_type in data['data']['relationship_types']: hierarchical = "Yes" if rel_type['is_hierarchical'] == '1' else "No" print(f" {rel_type['relationship_id']} -> {rel_type['reverse_relationship_id']} (Hierarchical: {hierarchical})") ``` ```javascript JavaScript theme={null} const response = await fetch('https://api.omophub.com/v1/relationships/types?page_size=50', { headers: { 'Authorization': 'Bearer YOUR_API_KEY' } }); const data = await response.json(); console.log(`Found ${data.meta.pagination.total_items} relationship types`); data.data.relationship_types.forEach(type => { console.log(`${type.relationship_id} <-> ${type.reverse_relationship_id}`); }); ``` ```json theme={null} { "success": true, "data": { "relationship_types": [ { "relationship_id": "Is a", "relationship_name": "Is a", "is_hierarchical": "1", "defines_ancestry": "1", "reverse_relationship_id": "Subsumes", "relationship_concept_id": 44818821 }, { "relationship_id": "Maps to", "relationship_name": "Maps to", "is_hierarchical": "0", "defines_ancestry": "0", "reverse_relationship_id": "Mapped from", "relationship_concept_id": 44818981 }, { "relationship_id": "Has finding site", "relationship_name": "Has finding site", "is_hierarchical": "0", "defines_ancestry": "0", "reverse_relationship_id": "Finding site of", "relationship_concept_id": 44818788 } ] }, "meta": { "request_id": "req_abc123", "vocab_release": "2025.1", "pagination": { "page": 1, "page_size": 100, "total_items": 642, "total_pages": 7, "has_next": true, "has_previous": false } } } ``` ## Usage Examples ### All Relationship Types Get all available relationship types: ```bash theme={null} curl "https://api.omophub.com/v1/relationships/types" \ -H "Authorization: Bearer YOUR_API_KEY" ``` ### Paginated Results Navigate through pages of relationship types: ```bash theme={null} curl "https://api.omophub.com/v1/relationships/types?page=2&page_size=50" \ -H "Authorization: Bearer YOUR_API_KEY" ``` ## Related Endpoints * [Get Concept Relationships](/api-reference/relationships/get-concept-relationships) - Relationships for specific concepts * [Get Concept Mappings](/api-reference/mappings/get-concept-mappings) - Cross-vocabulary mappings * [Get Vocabularies](/api-reference/vocabulary/get-vocabularies) - Available vocabularies ## Notes * Relationship types come from the OMOP CDM RELATIONSHIP table * All relationships exist symmetrically (both directions) * Hierarchical relationships (like "Is a") are used to build the CONCEPT\_ANCESTOR table * The `defines_ancestry` field indicates which relationships contribute to ancestor computation * Each relationship type has a corresponding concept in the CONCEPT table # Advanced Search Source: https://docs.omophub.com/api-reference/search/advanced-search POST /v1/search/advanced Perform multi-criteria search across OMOP medical vocabularies with advanced filtering by domain, vocabulary, concept class, and standard status. ## Overview The advanced search endpoint provides powerful multi-criteria search capabilities across healthcare vocabularies. Use complex queries, filters, and ranking to find the most relevant medical concepts. **Best for:** Complex search scenarios requiring multiple filters, specific vocabularies, or advanced ranking criteria. **Query Requirements:** Queries must be at least 3 characters long OR include at least one filter (`vocabulary_ids`, `domain_ids`, or `concept_class_ids`). This prevents overly broad searches that could timeout. ## Endpoint /v1/search/advanced ## Authentication Bearer token with your API key ## Request Body The search query term **Example:** `"diabetes mellitus type 2"` **Validation:** Query must be at least 3 characters long, OR include at least one filter (`vocabulary_ids`, `domain_ids`, or `concept_class_ids`). Short queries without filters are rejected to prevent performance issues. Array of vocabulary IDs to search within **Options:** `SNOMED`, `ICD10CM`, `ICD9CM`, `RxNorm`, `LOINC`, `HCPCS`, etc. **Default:** All vocabularies Array of domain IDs to filter by **Options:** `Condition`, `Drug`, `Procedure`, `Measurement`, `Observation`, etc. Array of concept class IDs to filter by **Examples:** `Clinical Finding`, `Pharmaceutical Substance`, `Procedure` Whether to include only standard concepts Whether to include invalid/deprecated concepts Array of relationship filter objects ```json theme={null} [{ "relationship_id": "Maps to", "target_vocabularies": ["SNOMED"], "required": true }] ``` Filter by validity date range ```json theme={null} { "start_date": "2020-01-01", "end_date": "2024-12-31" } ``` Page number (1-based indexing) Number of results per page (max: 1000) ## Response Whether the request was successful Array of matching concepts Unique concept identifier Human-readable concept name Concept code within its vocabulary Source vocabulary identifier Human-readable vocabulary name Concept domain classification Concept class within vocabulary Standard concept designation (S = Standard, C = Classification) Date when concept became valid Date when concept becomes invalid Reason for invalidity (if applicable) Search relevance score (0.0 to 1.0) Available facets for further filtering Vocabulary facets with counts Domain facets with counts Concept class facets with counts Search execution metadata Query execution time in milliseconds Total number of matching concepts Highest relevance score in results Search algorithm used Response metadata including pagination Unique identifier for this request ISO 8601 timestamp of the response Vocabulary release version used Search metadata The search query used Total number of matching concepts Filters that were applied to the search Pagination information Current page number Number of results per page Total number of results Total number of pages Whether a next page exists Whether a previous page exists ## Examples ### Basic Search ```bash cURL theme={null} curl -X POST "https://api.omophub.com/v1/search/advanced" \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "query": "diabetes mellitus type 2", "page_size": 10 }' ``` ```python Python theme={null} results = client.search.advanced_search( query="diabetes mellitus type 2", page_size=10 ) for concept in results.data: print(f"{concept.concept_id}: {concept.concept_name}") print(f" Vocabulary: {concept.vocabulary_id}") print(f" Score: {concept.relevance_score:.3f}") ``` ```javascript JavaScript theme={null} const results = await client.search.advancedSearch({ query: 'diabetes mellitus type 2', page_size: 10 }); results.data.forEach(concept => { console.log(`${concept.concept_id}: ${concept.concept_name}`); console.log(` Vocabulary: ${concept.vocabulary_id}`); console.log(` Score: ${concept.relevance_score.toFixed(3)}`); }); ``` ```r R theme={null} results <- advanced_search_concepts(client, query = "diabetes mellitus type 2", page_size = 10 ) for (i in 1:nrow(results$data$concepts)) { concept <- results$data$concepts[i, ] cat(paste(concept$concept_id, concept$concept_name, sep = ": "), "\n") cat(paste(" Vocabulary:", concept$vocabulary_id), "\n") cat(paste(" Score:", round(concept$relevance_score, 3)), "\n") } ``` ```json Example Response theme={null} { "success": true, "data": { "concepts": [ { "concept_id": 201826, "concept_name": "Type 2 diabetes mellitus", "concept_code": "44054006", "vocabulary_id": "SNOMED", "vocabulary_name": "Systematized Nomenclature of Medicine Clinical Terms", "domain_id": "Condition", "concept_class_id": "Clinical Finding", "standard_concept": "S", "valid_start_date": "1970-01-01", "valid_end_date": "2099-12-31", "invalid_reason": null, "relevance_score": 0.953 }, { "concept_id": 435216, "concept_name": "Diabetes mellitus type 2 in obese", "concept_code": "E11.0", "vocabulary_id": "ICD10CM", "vocabulary_name": "International Classification of Diseases, Tenth Revision, Clinical Modification", "domain_id": "Condition", "concept_class_id": "4-char billing code", "standard_concept": null, "valid_start_date": "2015-10-01", "valid_end_date": "2099-12-31", "invalid_reason": null, "relevance_score": 0.847 } ], "facets": { "vocabularies": [ {"vocabulary_id": "SNOMED", "vocabulary_name": "SNOMED CT", "count": 45}, {"vocabulary_id": "ICD10CM", "vocabulary_name": "ICD-10-CM", "count": 23}, {"vocabulary_id": "ICD9CM", "vocabulary_name": "ICD-9-CM", "count": 12} ], "domains": [ {"domain_id": "Condition", "domain_name": "Condition", "count": 67}, {"domain_id": "Observation", "domain_name": "Observation", "count": 13} ], "concept_classes": [ {"concept_class_id": "Clinical Finding", "count": 45}, {"concept_class_id": "4-char billing code", "count": 23} ] }, "search_metadata": { "query_time_ms": 127, "total_results": 80, "max_relevance_score": 0.953, "search_algorithm": "full_text_with_ranking" } }, "meta": { "request_id": "req_adv_search_abc123", "timestamp": "2024-01-15T10:30:00Z", "vocab_release": "2025.1", "search": { "query": "diabetes mellitus type 2", "total_results": 80, "filters_applied": { "vocabulary_ids": [], "domain_ids": [], "standard_concepts_only": true } }, "pagination": { "page": 1, "page_size": 10, "total_items": 80, "total_pages": 8, "has_next": true, "has_previous": false } } } ``` ### Filtered Search ```bash cURL theme={null} curl -X POST "https://api.omophub.com/v1/search/advanced" \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "query": "hypertension", "vocabulary_ids": ["SNOMED", "ICD10CM"], "domain_ids": ["Condition"], "standard_concepts_only": true, "date_range": { "start_date": "2020-01-01", "end_date": "2024-12-31" }, "page_size": 20 }' ``` ```python Python theme={null} results = client.search.advanced_search( query="hypertension", vocabulary_ids=["SNOMED", "ICD10CM"], domain_ids=["Condition"], standard_concepts_only=True, date_range={ "start_date": "2020-01-01", "end_date": "2024-12-31" }, page_size=20 ) print(f"Found {results.meta.pagination.total_items} concepts") print(f"Query executed in {results.data.search_metadata.query_time_ms}ms") ``` ```javascript JavaScript theme={null} const results = await client.search.advancedSearch({ query: 'hypertension', vocabulary_ids: ['SNOMED', 'ICD10CM'], domain_ids: ['Condition'], standard_concepts_only: true, date_range: { start_date: '2020-01-01', end_date: '2024-12-31' }, page_size: 20 }); console.log(`Found ${results.meta.pagination.total_items} concepts`); console.log(`Query executed in ${results.data.search_metadata.query_time_ms}ms`); ``` ```r R theme={null} results <- advanced_search_concepts(client, query = "hypertension", vocabulary_ids = c("SNOMED", "ICD10CM"), domain_ids = c("Condition"), standard_concepts_only = TRUE, date_range = list( start_date = "2020-01-01", end_date = "2024-12-31" ), page_size = 20 ) cat(paste("Found", results$meta$pagination$total_items, "concepts\n")) cat(paste("Query executed in", results$data$search_metadata$query_time_ms, "ms\n")) ``` ### Complex Relationship Filtering ```bash cURL theme={null} curl -X POST "https://api.omophub.com/v1/search/advanced" \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "query": "insulin", "vocabulary_ids": ["RxNorm"], "domain_ids": ["Drug"], "relationship_filters": [ { "relationship_id": "RxNorm has dose form", "target_vocabularies": ["RxNorm"], "required": true } ], "concept_class_ids": ["Clinical Drug"], "page_size": 15 }' ``` ```python Python theme={null} results = client.search.advanced_search( query="insulin", vocabulary_ids=["RxNorm"], domain_ids=["Drug"], relationship_filters=[ { "relationship_id": "RxNorm has dose form", "target_vocabularies": ["RxNorm"], "required": True } ], concept_class_ids=["Clinical Drug"], page_size=15 ) for concept in results.data: print(f"Drug: {concept.concept_name}") print(f" Code: {concept.concept_code}") print(f" Class: {concept.concept_class_id}") ``` ```javascript JavaScript theme={null} const results = await client.search.advancedSearch({ query: 'insulin', vocabulary_ids: ['RxNorm'], domain_ids: ['Drug'], relationship_filters: [ { relationship_id: 'RxNorm has dose form', target_vocabularies: ['RxNorm'], required: true } ], concept_class_ids: ['Clinical Drug'], page_size: 15 }); results.data.forEach(concept => { console.log(`Drug: ${concept.concept_name}`); console.log(` Code: ${concept.concept_code}`); console.log(` Class: ${concept.concept_class_id}`); }); ``` ```r R theme={null} results <- advanced_search_concepts(client, query = "insulin", vocabulary_ids = c("RxNorm"), domain_ids = c("Drug"), relationship_filters = list( list( relationship_id = "RxNorm has dose form", target_vocabularies = c("RxNorm"), required = TRUE ) ), concept_class_ids = c("Clinical Drug"), page_size = 15 ) for (i in 1:nrow(results$data$concepts)) { concept <- results$data$concepts[i, ] cat(paste("Drug:", concept$concept_name), "\n") cat(paste(" Code:", concept$concept_code), "\n") cat(paste(" Class:", concept$concept_class_id), "\n") } ``` ## Search Features ### Full-Text Search * **Multi-field search**: Searches concept names, synonyms, and descriptions * **Phrase matching**: Use quotes for exact phrases: `"myocardial infarction"` * **Wildcard support**: Use `*` for partial matching: `diabet*` * **Boolean operators**: Use AND, OR, NOT: `diabetes AND type 2` ### Relevance Scoring Results are ranked by relevance using: 1. **Exact matches**: Exact concept name matches score highest 2. **Phrase matches**: Complete phrase matches in names or synonyms 3. **Term frequency**: Frequency of query terms in concept text 4. **Vocabulary priority**: Standard concepts ranked higher 5. **Clinical relevance**: Healthcare-specific ranking adjustments ### Faceted Search Use facets to understand result distribution: ```python theme={null} # Access facets for filtering insights facets = results.data.facets print("Available vocabularies:") for vocab in facets.vocabularies: print(f" {vocab.vocabulary_name}: {vocab.count} concepts") print("\nAvailable domains:") for domain in facets.domains: print(f" {domain.domain_name}: {domain.count} concepts") ``` ## Performance Tips 1. **Use specific vocabulary\_ids**: Limit search to relevant vocabularies only 2. **Filter by domain\_ids**: Reduce result set with domain filters 3. **Reasonable page sizes**: Use page\_size of 20-100 for best performance 4. **Cache results**: Cache frequently accessed searches 5. **Use standard concepts**: Set `standard_concepts_only: true` for faster queries 6. **Avoid broad queries**: Short queries (\< 3 characters) without filters are rejected. Use more specific terms or add filters for better results. ## Related Endpoints Simple concept search with minimal parameters Real-time search suggestions Find semantically similar concepts Get available search facets # Basic Concept Search Source: https://docs.omophub.com/api-reference/search/basic-search GET /search/concepts ## Overview Search for medical concepts using text queries. This endpoint provides fast, relevant results across all supported vocabularies using advanced search algorithms. **For clinical concept lookups**, add `vocabulary_ids=SNOMED` and/or `domain_ids=Condition` filters. Without filters, results include all vocabularies (LOINC questionnaires, administrative codes, etc.) which may not be the clinical concepts you expect. This endpoint performs **keyword matching** - best for exact medical terms like "myocardial infarction" or concept codes like "E11.9". For **natural language queries** like "heart attack" or "high blood sugar", use [Semantic Search](/api-reference/search/semantic-search) which uses neural embeddings to understand clinical meaning and returns results ranked by similarity. ## Query Parameters Search query text (1-100 characters) Filter search to specific vocabularies (comma-separated)
**Examples**: `SNOMED`, `SNOMED,ICD10CM`, `RXNORM,NDC`
Filter by medical domains (comma-separated)
**Examples**: `Condition`, `Condition,Procedure`, `Drug,Device`
Page number (1-based indexing) Number of results per page (max: 1000) Specific vocabulary release version to query
**Example:** `2025.1`
```bash cURL theme={null} curl -X GET "https://api.omophub.com/v1/search/concepts?query=hypertension&page_size=10" \ -H "Authorization: Bearer YOUR_API_KEY" ``` ```python Python theme={null} import requests params = {"query": "hypertension", "page_size": 10} response = requests.get( "https://api.omophub.com/v1/search/concepts", headers={"Authorization": "Bearer YOUR_API_KEY"}, params=params ) concepts = response.json() ``` ```javascript JavaScript (Node.js) theme={null} const params = new URLSearchParams({ query: 'hypertension', page_size: '10' }); const response = await fetch(`https://api.omophub.com/v1/search/concepts?${params}`, { headers: { 'Authorization': `Bearer ${process.env.OMOPHUB_API_KEY}` } }); const concepts = await response.json(); ``` ```bash cURL (filtered) theme={null} curl -X GET "https://api.omophub.com/v1/search/concepts?query=diabetes&vocabulary_ids=SNOMED&domain_ids=Condition" \ -H "Authorization: Bearer YOUR_API_KEY" ``` ```python Python (filtered) theme={null} import requests params = { "query": "diabetes", "vocabulary_ids": "SNOMED", "domain_ids": "Condition" } response = requests.get( "https://api.omophub.com/v1/search/concepts", headers={"Authorization": "Bearer YOUR_API_KEY"}, params=params ) filtered_concepts = response.json() ``` ```json theme={null} { "success": true, "data": [ { "concept_id": 320128, "concept_name": "Essential hypertension", "concept_code": "59621000", "vocabulary_id": "SNOMED", "domain_id": "Condition", "concept_class_id": "Clinical Finding", "standard_concept": "S", "match_score": 0.95, "match_type": "exact", "valid_start_date": "1970-01-01", "valid_end_date": "2099-12-31" }, { "concept_id": 316866, "concept_name": "Hypertensive disorder", "concept_code": "38341003", "vocabulary_id": "SNOMED", "domain_id": "Condition", "concept_class_id": "Clinical Finding", "standard_concept": "S", "match_score": 0.89, "match_type": "partial", "valid_start_date": "1970-01-01", "valid_end_date": "2099-12-31" }, { "concept_id": 4124681, "concept_name": "Accelerated hypertension", "concept_code": "48146000", "vocabulary_id": "SNOMED", "domain_id": "Condition", "concept_class_id": "Clinical Finding", "standard_concept": "S", "match_score": 0.85, "match_type": "fuzzy", "valid_start_date": "1970-01-01", "valid_end_date": "2099-12-31" } ], "meta": { "pagination": { "page": 1, "page_size": 10, "total_items": 247, "total_pages": 25, "has_next": true, "has_previous": false }, "search": { "query": "hypertension", "total_results": 247, "filters_applied": { "vocabulary_ids": ["SNOMED", "ICD10CM"] } }, "request_id": "req_search_123", "timestamp": "2024-12-22T10:00:00Z", "vocab_release": "2025.2" } } ``` ## Search Features ### 1. **Fuzzy Matching** Handles typos and misspellings automatically. ### 2. **Synonym & Alias Matching** Matches concept synonyms and alternate names when `include_synonyms=true`. ### 3. **Relevance Scoring** Results ranked by relevance with match scores. ### 4. **Multi-vocabulary** Searches across all vocabularies simultaneously. ### 5. **Fast Performance** Optimized for sub-second response times. ## Common Use Cases ### 1. Clinical Coding Find appropriate codes for clinical documentation. ### 2. Data Standardization Map free-text clinical notes to standard concepts. ### 3. Quality Assurance Validate and standardize existing coded data. ### 4. Research Applications Find concepts for cohort definitions and outcome measures. # Bulk Search Source: https://docs.omophub.com/api-reference/search/bulk-search POST https://api.omophub.com/v1/search/bulk Perform OMOP concept search on multiple queries simultaneously with optimized batch processing - ideal for ETL pipelines and terminology mapping jobs. ## Overview This endpoint allows you to submit multiple search queries in a single request, enabling efficient batch processing of search operations. It's ideal for applications that need to search for many terms simultaneously, such as data processing pipelines, bulk concept mapping, or batch validation workflows. Each query uses full-text search with a default limit of 20 results per query (configurable via `page_size`, max 100). Up to 50 queries can be submitted per request. For paginated results or semantic search, use the [Basic Search](/api-reference/search/basic-search) or [Semantic Search](/api-reference/search/semantic-search) endpoints instead. ## Request Body Array of search query objects (1-50 items) Unique identifier for this search within the batch Search term or phrase Enable bulk-optimized search mode for better performance Filter results to specific vocabularies (e.g., `["SNOMED", "ICD10CM"]`). Overrides `defaults.vocabulary_ids` for this search. Filter results to specific domains (e.g., `["Condition", "Drug"]`). Overrides `defaults.domain_ids` for this search. Filter results to specific concept classes (e.g., `["Clinical Finding"]`). Overrides `defaults.concept_class_ids` for this search. Filter by standard concept status: `"S"` (Standard) or `"C"` (Classification). Overrides `defaults.standard_concept` for this search. Include invalid/deprecated concepts. Overrides `defaults.include_invalid` for this search. Number of results per search (1-100). Overrides `defaults.page_size` for this search. Default filter parameters applied to all searches. Individual searches can override any default. Default vocabulary filter for all searches (e.g., `["SNOMED"]`) Default domain filter for all searches (e.g., `["Condition"]`) Default concept class filter for all searches Default standard concept filter: `"S"` or `"C"` Default invalid concept inclusion Default results per search (1-100) ## Query Parameters Specific vocabulary release version (defaults to latest) ## Response Indicates if the request was successful Array of search results, one per query Identifier matching the request's search\_id Original search query Query execution status: `completed` or `failed` Array of matching concepts Unique concept identifier Primary concept name Concept code Source vocabulary Domain classification Concept class Standard concept indicator Search relevance score Error message (if query failed) Processing time for this query in milliseconds Unique identifier for the request Request timestamp Vocabulary release version used ```bash cURL theme={null} curl -X POST "https://api.omophub.com/v1/search/bulk" \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "defaults": { "vocabulary_ids": ["SNOMED"], "standard_concept": "S" }, "searches": [ { "search_id": "s1", "query": "diabetes" }, { "search_id": "s2", "query": "hypertension" }, { "search_id": "s3", "query": "aspirin", "vocabulary_ids": ["RxNorm"], "domain_ids": ["Drug"] } ] }' ``` ```javascript JavaScript theme={null} const response = await fetch('https://api.omophub.com/v1/search/bulk', { method: 'POST', headers: { 'Authorization': 'Bearer YOUR_API_KEY', 'Content-Type': 'application/json' }, body: JSON.stringify({ defaults: { vocabulary_ids: ['SNOMED'], standard_concept: 'S' }, searches: [ { search_id: 's1', query: 'diabetes' }, { search_id: 's2', query: 'hypertension' }, { search_id: 's3', query: 'aspirin', vocabulary_ids: ['RxNorm'], domain_ids: ['Drug'] } ] }) }); const data = await response.json(); ``` ```python Python theme={null} import requests headers = { 'Authorization': 'Bearer YOUR_API_KEY', 'Content-Type': 'application/json' } payload = { 'defaults': { 'vocabulary_ids': ['SNOMED'], 'standard_concept': 'S' }, 'searches': [ { 'search_id': 's1', 'query': 'diabetes' }, { 'search_id': 's2', 'query': 'hypertension' }, { 'search_id': 's3', 'query': 'aspirin', 'vocabulary_ids': ['RxNorm'], 'domain_ids': ['Drug'] } ] } response = requests.post( 'https://api.omophub.com/v1/search/bulk', headers=headers, json=payload ) data = response.json() ``` ```json Response theme={null} { "success": true, "data": [ { "search_id": "s1", "query": "diabetes", "status": "completed", "results": [ { "concept_id": 201826, "concept_name": "Type 2 diabetes mellitus", "concept_code": "44054006", "vocabulary_id": "SNOMED", "domain_id": "Condition", "concept_class_id": "Clinical Finding", "standard_concept": "S", "search_score": 100 }, { "concept_id": 4000678, "concept_name": "Diabetes mellitus", "concept_code": "73211009", "vocabulary_id": "SNOMED", "domain_id": "Condition", "concept_class_id": "Clinical Finding", "standard_concept": "S", "search_score": 90 } ], "duration": 856 }, { "search_id": "s2", "query": "hypertension", "status": "completed", "results": [ { "concept_id": 320128, "concept_name": "Essential hypertension", "concept_code": "59621000", "vocabulary_id": "SNOMED", "domain_id": "Condition", "concept_class_id": "Clinical Finding", "standard_concept": "S", "search_score": 100 }, { "concept_id": 316866, "concept_name": "Hypertensive disorder", "concept_code": "38341003", "vocabulary_id": "SNOMED", "domain_id": "Condition", "concept_class_id": "Clinical Finding", "standard_concept": "S", "search_score": 90 } ], "duration": 1034 }, { "search_id": "s3", "query": "aspirin", "status": "completed", "results": [ { "concept_id": 1112807, "concept_name": "Aspirin", "concept_code": "1191", "vocabulary_id": "RxNorm", "domain_id": "Drug", "concept_class_id": "Ingredient", "standard_concept": "S", "search_score": 100 } ], "duration": 957 } ], "meta": { "request_id": "req_bulk_search_123", "timestamp": "2024-01-15T10:30:00Z", "vocab_release": "2025.2" } } ``` ## Usage Examples ### Basic Bulk Search Search multiple terms simultaneously without filters: ```bash theme={null} curl -X POST "https://api.omophub.com/v1/search/bulk" \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "searches": [ {"search_id": "s1", "query": "diabetes"}, {"search_id": "s2", "query": "hypertension"}, {"search_id": "s3", "query": "asthma"} ] }' ``` ### Filtered Bulk Search with Defaults Apply common filters to all searches using `defaults`, with per-search overrides: ```bash theme={null} curl -X POST "https://api.omophub.com/v1/search/bulk" \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "defaults": { "vocabulary_ids": ["SNOMED"], "domain_ids": ["Condition"], "standard_concept": "S" }, "searches": [ {"search_id": "s1", "query": "diabetes"}, {"search_id": "s2", "query": "heart failure"}, {"search_id": "s3", "query": "metformin", "vocabulary_ids": ["RxNorm"], "domain_ids": ["Drug"]} ] }' ``` In this example, searches s1 and s2 use the defaults (SNOMED, Condition domain, standard concepts only). Search s3 overrides with RxNorm vocabulary and Drug domain. ### Per-Search Filters Apply different filters to each search individually: ```bash theme={null} curl -X POST "https://api.omophub.com/v1/search/bulk" \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "searches": [ {"search_id": "s1", "query": "diabetes", "vocabulary_ids": ["SNOMED"], "domain_ids": ["Condition"]}, {"search_id": "s2", "query": "metformin", "vocabulary_ids": ["RxNorm"], "domain_ids": ["Drug"]}, {"search_id": "s3", "query": "HbA1c", "vocabulary_ids": ["LOINC"], "domain_ids": ["Measurement"]} ] }' ``` ### High-Volume Processing Process larger batches from a file: ```bash theme={null} curl -X POST "https://api.omophub.com/v1/search/bulk" \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d @bulk_queries.json ``` Example `bulk_queries.json`: ```json theme={null} { "defaults": { "vocabulary_ids": ["SNOMED"], "standard_concept": "S" }, "searches": [ {"search_id": "q1", "query": "diabetes mellitus"}, {"search_id": "q2", "query": "essential hypertension"}, {"search_id": "q3", "query": "chronic kidney disease"}, {"search_id": "q4", "query": "atrial fibrillation"}, {"search_id": "q5", "query": "congestive heart failure"} ] } ``` ## Optimization Strategies ### Query Grouping * **Batch Size**: Optimal batch size is 10-50 queries per request * **Bulk Mode**: The `bulk_mode` parameter (default `true`) enables optimized query execution for better throughput * **Result Limit**: Each query returns up to 20 results by default (configurable via `page_size`, max 100) ### Performance Tips * Use concise, specific search terms for faster results * Split very large query sets into multiple requests of 50 queries each * Use the `search_id` field to correlate results with your source data * Use `defaults` to apply common filters instead of repeating them per search * Applying filters (vocabulary, domain) generally improves performance by narrowing the search space ### Error Handling * Each query in the batch is processed independently * Failed queries return `status: "failed"` with an `error` message * Successful queries are not affected by failures in other queries ## Related Endpoints * [Basic Search](/api-reference/search/basic-search) - Single query search with pagination * [Advanced Search](/api-reference/search/advanced-search) - Complex search with advanced filters * [Semantic Search](/api-reference/search/semantic-search) - AI-powered contextual search # Get Similar Concepts by ID Source: https://docs.omophub.com/api-reference/search/get-search-similar-by-id GET https://api.omophub.com/v1/search/similar/{conceptId} Find OMOP concepts similar to a specific concept identified by its concept ID using semantic similarity - discover related clinical terminology fast. ## Overview This endpoint finds medical concepts that are semantically similar to a specific concept identified by its OMOP concept ID. It uses the same advanced machine learning algorithms as the POST version but optimized for concept-to-concept similarity matching, making it ideal for discovering related concepts when you have a specific starting point. ## Path Parameters The OMOP concept ID to find similar concepts for ## Query Parameters Target vocabularies to search within (comma-separated)
**Examples**: `SNOMED`, `SNOMED,ICD10CM`, `RXNORM,NDC`
Clinical domains to focus the similarity search (comma-separated)
**Examples**: `Condition,Procedure`, `Drug,Device`
Concept classes to include in similarity search (comma-separated)
**Examples**: `Clinical Finding,Procedure`, `Ingredient,Brand Name`
Minimum similarity score threshold (0.0 to 1.0)
**Higher values** = More strict similarity matching
Number of similar concepts to return per page Page number (1-based indexing) Include similarity scores in the response Include explanations for why concepts are considered similar Filter to standard concepts only
**Options**: `S` (standard), `C` (classification), `NULL` (non-standard)
Include invalid/deprecated concepts in similarity search Similarity algorithm to use
**Options**: `semantic` (embedding-based), `lexical` (text-based), `hybrid` (combined)
Exclude the source concept from results ## Response Indicates if the request was successful Error information (present only on error responses) Error code identifier Human-readable error message Additional error details or context Contains the similar concepts search results Information about the source concept used for similarity search OMOP concept ID of the source concept Primary name of the source concept Vocabulary-specific code Source vocabulary identifier Clinical domain classification Concept class identifier Standard concept designation ('S', 'C', or null for non-standard) Array of concepts similar to the source concept OMOP concept ID Primary concept name Vocabulary-specific concept code Source vocabulary identifier Clinical domain classification Concept class identifier Similarity score between 0.0 and 1.0 Reason for similarity match (if requested) Standard concept designation ('S', 'C', or null for non-standard) Alternative names for the concept Information about the similarity search The concept ID used as the similarity source Similarity algorithm applied Minimum similarity threshold applied Total concepts evaluated for similarity Number of similar concepts returned Search processing time in milliseconds ```bash cURL theme={null} curl -X GET "https://api.omophub.com/v1/search/similar/44054006?vocabulary_ids=SNOMED,ICD10CM&similarity_threshold=0.8&page_size=10&include_scores=true&include_explanations=true" \ -H "Authorization: Bearer YOUR_API_KEY" ``` ```javascript JavaScript theme={null} const response = await fetch( 'https://api.omophub.com/v1/search/similar/44054006?vocabulary_ids=SNOMED,ICD10CM&similarity_threshold=0.8&page_size=10&include_scores=true&include_explanations=true', { method: 'GET', headers: { 'Authorization': 'Bearer YOUR_API_KEY' } } ); const data = await response.json(); ``` ```python Python theme={null} import requests url = "https://api.omophub.com/v1/search/similar/44054006" params = { "vocabulary_ids": "SNOMED,ICD10CM", "similarity_threshold": 0.8, "page_size": 10, "include_scores": "true", "include_explanations": "true" } response = requests.get( url, params=params, headers={"Authorization": "Bearer YOUR_API_KEY"} ) data = response.json() ``` ```json Response theme={null} { "success": true, "data": { "source_concept": { "concept_id": 44054006, "concept_name": "Type 2 diabetes mellitus", "concept_code": "44054006", "vocabulary_id": "SNOMED", "domain_id": "Condition", "concept_class_id": "Clinical Finding", "standard_concept": "S" }, "similar_concepts": [ { "concept_id": 201826, "concept_name": "Type 2 diabetes mellitus without complications", "concept_code": "E11.9", "vocabulary_id": "ICD10CM", "domain_id": "Condition", "concept_class_id": "4-char nonbill code", "similarity_score": 0.92, "explanation": "High similarity - more specific variant of the same condition", "standard_concept": null, "synonyms": ["Type II diabetes mellitus uncomplicated"] }, { "concept_id": 443729, "concept_name": "Diabetes mellitus type 2 with hyperglycemia", "concept_code": "443729", "vocabulary_id": "SNOMED", "domain_id": "Condition", "concept_class_id": "Clinical Finding", "similarity_score": 0.89, "explanation": "High similarity - related complication of the same condition", "standard_concept": "S", "synonyms": ["Type 2 DM with elevated glucose"] }, { "concept_id": 432367, "concept_name": "Diabetes mellitus type 2 with diabetic nephropathy", "concept_code": "E11.21", "vocabulary_id": "ICD10CM", "domain_id": "Condition", "concept_class_id": "5-char billing code", "similarity_score": 0.86, "explanation": "High similarity - complication of the same condition", "standard_concept": null, "synonyms": ["Type 2 DM with kidney disease"] } ], "search_metadata": { "source_concept_id": 44054006, "algorithm_used": "hybrid", "similarity_threshold": 0.8, "total_candidates": 125000, "results_returned": 3, "processing_time_ms": 187 } }, "meta": { "request_id": "req_sim_xyz789", "timestamp": "2024-01-15T10:30:00Z", "vocab_release": "2025.1", "pagination": { "page": 1, "page_size": 20, "total_items": 3, "total_pages": 1, "has_next": false, "has_previous": false }, "search": { "query": "", "total_results": 3, "filters_applied": { "similarity_type": "concept" } } } } ``` ```json Error Response theme={null} { "success": false, "error": { "code": "CONCEPT_NOT_FOUND", "message": "The specified concept ID does not exist", "details": { "concept_id": 99999999, "suggestion": "Verify the concept ID exists in the OMOP vocabularies" } } } ``` ## Usage Examples ### Basic Similarity by ID Find concepts similar to Type 2 Diabetes (concept ID 44054006): ```bash theme={null} GET /v1/search/similar/44054006?page_size=5&similarity_threshold=0.7 ``` ### Cross-Vocabulary Similarity Find similar concepts across multiple vocabularies: ```bash theme={null} GET /v1/search/similar/1308216?vocabulary_ids=SNOMED,ICD10CM,LOINC&domain_ids=Condition,Measurement&include_explanations=true ``` ### Drug Similarity Search Find similar pharmaceutical concepts: ```bash theme={null} GET /v1/search/similar/1503297?vocabulary_ids=RXNORM&domain_ids=Drug&concept_class_ids=Ingredient,Clinical Drug&similarity_threshold=0.8 ``` ### High-Precision Similarity Get only highly similar concepts with detailed scoring: ```bash theme={null} GET /v1/search/similar/443390?similarity_threshold=0.9&page_size=10&include_scores=true&algorithm=semantic ``` ## Performance Notes * **Concept-to-concept similarity** is typically faster than query-based similarity * **Hybrid algorithm** provides best balance of accuracy and performance * **Semantic algorithm** is most accurate but computationally intensive * **Caching** is applied for frequently requested concept similarities ## Related Endpoints * [POST Search Similar](/api-reference/search/search-similar) - Find similar concepts using query text * [Get Concept](/api-reference/concepts/get-concept) - Get detailed information about a specific concept # Search Autocomplete Source: https://docs.omophub.com/api-reference/search/search-autocomplete GET /v1/search/autocomplete Get real-time autocomplete suggestions for OMOP medical terminology searches with optional phonetic matching for typo-tolerant clinical lookups. ## Overview This endpoint provides autocomplete functionality for medical terminology searches. It offers real-time suggestions as users type, helping them find the correct medical terms quickly. The endpoint supports both standard prefix matching and phonetic (Soundex) matching for handling common misspellings. ## Query Parameters Search query for autocompletion (minimum 2 characters, maximum 100) Filter suggestions to specific vocabularies (comma-separated)
**Examples**: `SNOMED`, `ICD10CM,LOINC`, `RXNORM`
Filter suggestions to specific domains (comma-separated)
**Examples**: `Condition`, `Drug,Procedure`
Page number (1-based indexing) Number of suggestions to return per page (1-20) Matching algorithm to use
**Options**: `default`, `soundex`
Use `soundex` for phonetic matching to handle misspellings
Specific vocabulary release version (defaults to latest) ## Response Array of autocomplete suggestions with concept details ```bash cURL theme={null} curl -X GET "https://api.omophub.com/v1/search/autocomplete?query=diab&page_size=5" \ -H "Authorization: Bearer YOUR_API_KEY" ``` ```python Python theme={null} import requests response = requests.get( "https://api.omophub.com/v1/search/autocomplete", headers={"Authorization": "Bearer YOUR_API_KEY"}, params={ "query": "diab", "vocabulary_ids": "SNOMED,ICD10CM", "page_size": 10 } ) data = response.json() suggestions = data["suggestions"] ``` ```javascript JavaScript theme={null} const response = await fetch( 'https://api.omophub.com/v1/search/autocomplete?query=diab&page_size=5', { headers: { 'Authorization': 'Bearer YOUR_API_KEY' } } ); const suggestions = await response.json(); ``` ```r R theme={null} library(omophub) client <- OMOPHub$new() suggestions <- client$search$autocomplete("diab", page_size = 5) ``` ```json Response theme={null} { "suggestions": [ { "concept_id": 201826, "concept_name": "Type 2 diabetes mellitus", "concept_code": "44054006", "vocabulary_id": "SNOMED", "domain_id": "Condition", "concept_class_id": "Clinical Finding", "standard_concept": "S", "score": 0.95 }, { "concept_id": 201254, "concept_name": "Type 1 diabetes mellitus", "concept_code": "46635009", "vocabulary_id": "SNOMED", "domain_id": "Condition", "concept_class_id": "Clinical Finding", "standard_concept": "S", "score": 0.92 }, { "concept_id": 443238, "concept_name": "Diabetic retinopathy", "concept_code": "4855003", "vocabulary_id": "SNOMED", "domain_id": "Condition", "concept_class_id": "Clinical Finding", "standard_concept": "S", "score": 0.88 } ], "meta": { "query": "diab", "algorithm": "default", "total_suggestions": 3, "vocab_release": "2025.2" } } ``` ## Usage Examples ### Basic Autocomplete Get suggestions for a partial query: ```bash theme={null} curl -X GET "https://api.omophub.com/v1/search/autocomplete?query=asth&page_size=10" \ -H "Authorization: Bearer YOUR_API_KEY" ``` ### Vocabulary-Filtered Autocomplete Focus suggestions on specific vocabularies: ```bash theme={null} curl -X GET "https://api.omophub.com/v1/search/autocomplete?query=hyper&vocabulary_ids=SNOMED,ICD10CM&page_size=10" \ -H "Authorization: Bearer YOUR_API_KEY" ``` ### Domain-Filtered Autocomplete Get suggestions only for specific domains: ```bash theme={null} curl -X GET "https://api.omophub.com/v1/search/autocomplete?query=aspir&domain_ids=Drug&page_size=10" \ -H "Authorization: Bearer YOUR_API_KEY" ``` ### Phonetic Matching (Soundex) Use phonetic matching to handle misspellings: ```bash theme={null} curl -X GET "https://api.omophub.com/v1/search/autocomplete?query=diabetis&algorithm=soundex&page_size=10" \ -H "Authorization: Bearer YOUR_API_KEY" ``` This will find "diabetes" even when spelled incorrectly as "diabetis". ## Algorithms ### Default Algorithm * Standard prefix matching * Fast and accurate for correctly spelled queries * Best for real-time autocomplete UX ### Soundex Algorithm * Phonetic matching based on pronunciation * Handles common misspellings and typos * Useful for voice-to-text input or uncertain spellings * Example: "noomonia" matches "pneumonia" ## Performance Notes * **Minimum query length**: 2 characters required * **Response time**: Optimized for real-time use (\<100ms typical) * **Caching**: Results are cached for 24 hours per vocabulary release * **Rate limiting**: Standard rate limits apply ## Related Endpoints * [Basic Search](/api-reference/search/basic-search) - Full-text search with filters * [Similarity Search](/api-reference/search/semantic-search) - Find semantically similar concepts # Search Facets Source: https://docs.omophub.com/api-reference/search/search-facets GET https://api.omophub.com/v1/search/facets Retrieve search facets for filtering medical terminology search results by vocabulary, domain, concept class, and standard concept status. ## Overview This endpoint provides faceted search capabilities for medical terminology, returning aggregated counts that can be used to filter and refine search results. Facets help users understand the distribution of concepts across different dimensions. ## Query Parameters Search query to generate facets for. When provided, facets reflect the distribution of matching concepts. Filter facets to specific vocabularies (comma-separated)
**Examples**: `SNOMED`, `ICD10CM,LOINC`, `RXNORM`
Specific vocabulary release version (defaults to latest)
**Examples**: `2025.1`, `2024.2`
## Response Indicates if the request was successful Contains the faceted search results The search query provided (empty string if none) Object containing facet categories and their values Vocabulary facets with concept counts Vocabulary identifier (e.g., "SNOMED", "ICD10CM") Number of concepts in this vocabulary matching the query Domain facets with concept counts Domain identifier (e.g., "Condition", "Drug", "Procedure") Number of concepts in this domain matching the query Concept class facets with counts Concept class identifier (e.g., "Clinical Finding", "Procedure") Number of concepts in this class matching the query Standard concept status facets Standard concept indicator: `S` (Standard), `C` (Classification), `N` (Non-standard) Number of concepts with this status matching the query Response metadata Unique identifier for the request Vocabulary release version used ```bash cURL theme={null} curl -X GET "https://api.omophub.com/v1/search/facets?query=diabetes&vocabulary_ids=SNOMED,ICD10CM" \ -H "Authorization: Bearer YOUR_API_KEY" ``` ```javascript JavaScript theme={null} const response = await fetch( 'https://api.omophub.com/v1/search/facets?query=diabetes&vocabulary_ids=SNOMED,ICD10CM', { method: 'GET', headers: { 'Authorization': 'Bearer YOUR_API_KEY' } } ); const data = await response.json(); ``` ```python Python theme={null} import requests response = requests.get( "https://api.omophub.com/v1/search/facets", params={ "query": "diabetes", "vocabulary_ids": "SNOMED,ICD10CM" }, headers={"Authorization": "Bearer YOUR_API_KEY"} ) data = response.json() ``` ```json Response theme={null} { "success": true, "data": { "query": "diabetes", "facets": { "vocabularies": [ { "vocabulary_id": "SNOMED", "count": 245 }, { "vocabulary_id": "ICD10CM", "count": 67 } ], "domains": [ { "domain_id": "Condition", "count": 198 }, { "domain_id": "Observation", "count": 87 }, { "domain_id": "Drug", "count": 27 } ], "concept_classes": [ { "concept_class_id": "Clinical Finding", "count": 156 }, { "concept_class_id": "Disorder", "count": 87 }, { "concept_class_id": "Procedure", "count": 42 } ], "standard_concepts": [ { "standard_concept": "S", "count": 245 }, { "standard_concept": "C", "count": 34 }, { "standard_concept": "N", "count": 33 } ] } }, "meta": { "request_id": "req_abc123", "vocab_release": "2025.1" } } ``` ## Usage Examples ### Get Facets for a Search Query ```bash theme={null} GET /v1/search/facets?query=hypertension ``` ### Filter Facets by Vocabulary ```bash theme={null} GET /v1/search/facets?query=medication&vocabulary_ids=RXNORM,NDC ``` ### General Facets Without Query Get overall distribution of concepts: ```bash theme={null} GET /v1/search/facets ``` ## Facet Types ### Vocabularies Shows distribution across medical vocabularies (SNOMED, ICD10CM, LOINC, RxNorm, etc.). Use to filter searches to specific terminology systems. ### Domains Shows distribution across clinical domains (Condition, Drug, Procedure, Measurement, etc.). Use to focus on specific types of medical concepts. ### Concept Classes Shows distribution across concept classifications (Clinical Finding, Disorder, Ingredient, etc.). Provides more granular filtering than domains. ### Standard Concepts Shows distribution by OMOP standard concept status: * **S (Standard)**: Preferred concepts for OMOP CDM analytics * **C (Classification)**: Classification concepts for grouping * **N (Non-standard)**: Source concepts that map to standard concepts ## Related Endpoints * [Basic Search](/api-reference/search/basic-search) - Apply facet filters to search * [Advanced Search](/api-reference/search/advanced-search) - Complex search with multiple filters # Search Similar Concepts Source: https://docs.omophub.com/api-reference/search/search-similar POST https://api.omophub.com/v1/search/similar Find semantically similar medical concepts using advanced machine learning algorithms with flexible search criteria and body parameters. ## Overview This endpoint identifies medical concepts that are semantically similar to a provided query or set of criteria. It leverages advanced machine learning models trained on medical terminology to discover related concepts that may not share exact keywords but are clinically relevant and contextually similar. ## Request Body Primary search query or concept description to find similar concepts for Target vocabularies to search within (array of strings)
**Examples**: `["SNOMED", "ICD10CM"]`, `["RXNORM", "NDC"]`
Clinical domains to focus the similarity search (array of strings)
**Examples**: `["Condition", "Procedure"]`, `["Drug", "Device"]`
Concept classes to include in similarity search (array of strings)
**Examples**: `["Clinical Finding", "Procedure"]`, `["Ingredient", "Brand Name"]`
Minimum similarity score threshold (0 to 1.0)
**Higher values** = More strict similarity matching
Number of similar concepts to return per page Page number (1-based indexing) Include similarity scores in the response Include explanations for why concepts are considered similar Filter to standard concepts only
**Options**: `S` (standard), `C` (classification), `N` (non-standard)
Include invalid/deprecated concepts in similarity search Similarity algorithm to use
**Options**: * `semantic` - Neural embedding-based similarity. Best for finding conceptually similar terms (e.g., "heart attack" → "Myocardial infarction"). * `lexical` - Text-based Jaccard word similarity. Good for fuzzy text matching and typo tolerance. * `hybrid` (default) - Combines word and character similarity for balanced matching.
## Response Indicates if the request was successful Contains the similar concepts search results Array of concepts similar to the query OMOP concept ID Primary concept name Vocabulary-specific concept code Source vocabulary identifier Clinical domain classification Concept class identifier Similarity score between 0.0 and 1.0 Reason for similarity match (if requested) Standard concept flag (S, C, or N) Alternative names for the concept Information about the similarity search The original search query provided Similarity algorithm applied (`semantic`, `lexical`, or `hybrid`) Minimum similarity threshold applied Approximate count of concepts evaluated for similarity. For `semantic` algorithm, this is a lower bound based on sampled results. Number of similar concepts returned Search processing time in milliseconds Time spent on embedding generation (only present when `algorithm_used` is `semantic`) ```bash cURL theme={null} curl -X POST "https://api.omophub.com/v1/search/similar" \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "query": "type 2 diabetes mellitus", "vocabulary_ids": ["SNOMED", "ICD10CM"], "domain_ids": ["Condition"], "similarity_threshold": 0.8, "page_size": 10, "include_scores": true, "include_explanations": true, "standard_concept": "S" }' ``` ```javascript JavaScript theme={null} const response = await fetch('https://api.omophub.com/v1/search/similar', { method: 'POST', headers: { 'Authorization': 'Bearer YOUR_API_KEY', 'Content-Type': 'application/json' }, body: JSON.stringify({ query: "type 2 diabetes mellitus", vocabulary_ids: ["SNOMED", "ICD10CM"], domain_ids: ["Condition"], similarity_threshold: 0.8, page_size: 10, include_scores: true, include_explanations: true, standard_concept: "S" }) }); const data = await response.json(); ``` ```python Python theme={null} import requests url = "https://api.omophub.com/v1/search/similar" payload = { "query": "type 2 diabetes mellitus", "vocabulary_ids": ["SNOMED", "ICD10CM"], "domain_ids": ["Condition"], "similarity_threshold": 0.8, "page_size": 10, "include_scores": True, "include_explanations": True, "standard_concept": "S" } response = requests.post( url, json=payload, headers={"Authorization": "Bearer YOUR_API_KEY"} ) data = response.json() ``` ```json Response theme={null} { "success": true, "data": { "similar_concepts": [ { "concept_id": 44054006, "concept_name": "Type 2 diabetes mellitus", "concept_code": "44054006", "vocabulary_id": "SNOMED", "domain_id": "Condition", "concept_class_id": "Clinical Finding", "similarity_score": 1.0, "explanation": "Exact match - same clinical concept", "standard_concept": "S", "synonyms": ["Non-insulin dependent diabetes mellitus", "Adult-onset diabetes"] }, { "concept_id": 201826, "concept_name": "Type 2 diabetes mellitus without complications", "concept_code": "E11.9", "vocabulary_id": "ICD10CM", "domain_id": "Condition", "concept_class_id": "4-char nonbill code", "similarity_score": 0.92, "explanation": "High similarity - more specific variant of the same condition", "standard_concept": "S", "synonyms": ["Type II diabetes mellitus uncomplicated"] }, { "concept_id": 443729, "concept_name": "Diabetes mellitus type 2 with hyperglycemia", "concept_code": "443729", "vocabulary_id": "SNOMED", "domain_id": "Condition", "concept_class_id": "Clinical Finding", "similarity_score": 0.89, "explanation": "High similarity - related complication of the same condition", "standard_concept": "S", "synonyms": ["Type 2 DM with elevated glucose"] } ], "search_metadata": { "original_query": "type 2 diabetes mellitus", "algorithm_used": "hybrid", "similarity_threshold": 0.8, "total_candidates": 125000, "results_returned": 3, "processing_time_ms": 245 } }, "meta": { "request_id": "req_sim_abc123", "timestamp": "2024-01-15T10:30:00Z", "vocab_release": "2025.1", "pagination": { "page": 1, "page_size": 20, "total_items": 3, "total_pages": 1, "has_next": false, "has_previous": false }, "search": { "query": "type 2 diabetes mellitus", "total_results": 3, "filters_applied": { "similarity_type": "concept" } } } } ``` ```json Error Response theme={null} { "success": false, "error": { "code": "INVALID_THRESHOLD", "message": "Similarity threshold must be between 0 and 1.0", "details": { "provided_threshold": 1.5, "valid_range": "0-1.0" } } } ``` ## Usage Examples ### Basic Similarity Search Find concepts similar to a medical condition: ```json theme={null} { "query": "hypertension", "page_size": 5, "similarity_threshold": 0.7 } ``` ### Cross-Vocabulary Similarity Find similar concepts across multiple vocabularies: ```json theme={null} { "query": "myocardial infarction", "vocabulary_ids": ["SNOMED", "ICD10CM", "ICD9CM"], "domain_ids": ["Condition"], "similarity_threshold": 0.8, "include_explanations": true } ``` ### Pharmacological Similarity Find similar drug concepts with detailed scoring: ```json theme={null} { "query": "metformin hydrochloride", "vocabulary_ids": ["RXNORM"], "domain_ids": ["Drug"], "concept_class_ids": ["Ingredient", "Clinical Drug"], "similarity_threshold": 0.75, "include_scores": true, "algorithm": "semantic" } ``` ### Semantic Search for Clinical Terms Use neural embeddings to find conceptually similar terms (even when words don't match): ```json theme={null} { "query": "heart attack", "vocabulary_ids": ["SNOMED"], "domain_ids": ["Condition"], "similarity_threshold": 0.5, "algorithm": "semantic", "page_size": 10 } ``` This will find "Myocardial infarction" and related concepts even though the words "heart" and "attack" don't appear in the medical term. ## Algorithm Comparison | Feature | `semantic` | `lexical` | `hybrid` (default) | | --------------------- | --------------------- | ----------------------- | --------------------------- | | Model | Neural embeddings | Jaccard word similarity | Word + character similarity | | Best for | Conceptual similarity | Fuzzy text matching | Balanced matching | | "heart attack" → "MI" | Excellent | Poor (word mismatch) | Poor | | Typo tolerance | Moderate | Good | Good | | Total counts | Approximate | Exact | Exact | | Speed | Fast (15-50ms) | Fast (50-200ms) | Fast (50-200ms) | | Requirements | Embedding service | None | None | **Semantic Algorithm Pagination:** When using `algorithm: "semantic"`, the `total_candidates` and pagination counts are approximate values optimized for performance. Use `has_next` in the response to reliably determine if more results exist. ## Related Endpoints * [GET Search Similar by ID](/api-reference/search/get-search-similar-by-id) - Get similar concepts for a specific concept ID * [Semantic Search](/api-reference/search/semantic-search) - Dedicated semantic search endpoint using neural embeddings * [Advanced Search](/api-reference/search/advanced-search) - Advanced search with multiple criteria # Search Suggest Source: https://docs.omophub.com/api-reference/search/search-suggest GET https://api.omophub.com/v1/search/suggest Get intelligent search suggestions and alternative query recommendations based on user intent, medical context, and search patterns. ## Overview This endpoint provides intelligent search suggestions and query recommendations to help users refine their medical terminology searches. It analyzes user queries, medical context, and search patterns to suggest alternative search terms, related concepts, and improved query formulations. ## Query Parameters Original search query for which suggestions are needed Target vocabularies for suggestions (comma-separated)
**Examples**: `SNOMED`, `ICD10CM,LOINC`, `RXNORM,NDC`
Filter suggestions to specific domains (comma-separated)
**Examples**: `Condition,Procedure`, `Drug,Device`
Filter to specific concept classes (comma-separated) Types of suggestions to include (comma-separated)
**Options**: `spelling`, `synonym`, `related`, `broader`, `narrower`, `alternative`, `trending`, `popular`, `all`
Medical specialty context for relevant suggestions
**Examples**: `cardiology`, `oncology`, `pediatrics`, `emergency_medicine`
User type for suggestion personalization
**Options**: `physician`, `nurse`, `pharmacist`, `researcher`, `patient`, `student`
Detected or specified search intent
**Options**: `diagnostic`, `therapeutic`, `procedural`, `research`, `educational`, `administrative`
Include spelling corrections and typo fixes Include alternative phrasings and synonyms Include conceptually related terms Include broader and narrower concepts Include trending medical terms Include popular search terms Include explanations for why suggestions are relevant Enable personalized suggestions based on user history Cultural or regional context for suggestions
**Examples**: `US`, `UK`, `international`, `multicultural`
Language for suggestions (ISO 639-1 code) Preferred complexity level for suggestions
**Options**: `simple`, `intermediate`, `advanced`, `technical`, `balanced`
Whether suggestions should be patient-appropriate Filter by standard concept status: `S`, `N`, `C` Include suggestions for invalid/deprecated concepts Page number (1-based indexing) Number of suggestions to return per page (max 100) Minimum relevance score for suggestions (0.0-1.0) Specific vocabulary release version (defaults to latest) ## Response Indicates if the request was successful Original search query Automatically detected search intent Confidence in intent detection (0.0-1.0) Assessment of query quality (Excellent, Good, Fair, Poor) Medical concepts identified in the query Potential issues with the query Ways the query could be improved Spelling and typo corrections Original potentially misspelled term Suggested correction Confidence in the correction (0.0-1.0) Type of correction (typo, spelling, grammar) Explanation of the correction (if include\_explanations=true) Synonym suggestions Original term from query Suggested synonym Relevance score (0.0-1.0) Associated concept ID (if applicable) Source vocabulary Typical usage context Level of formality (Technical, Clinical, Lay) Why this synonym is relevant (if include\_explanations=true) Conceptually related suggestions Suggested related term Type of relationship (associated, causes, treats, etc.) Relevance score (0.0-1.0) Concept ID Source vocabulary Domain classification Clinical context or use case Relationship explanation (if include\_explanations=true) More general concept suggestions Suggested broader term Levels up in hierarchy Relevance score (0.0-1.0) Concept ID Source vocabulary Type of generalization (category, class, system) When to use this broader term More specific concept suggestions Suggested narrower term Levels down in hierarchy Relevance score (0.0-1.0) Concept ID Source vocabulary Type of specialization (subtype, specific case, variant) Level of clinical specificity Alternative ways to phrase the query Alternative query phrasing Type of alternative (formal, informal, technical, lay) Appropriateness score (0.0-1.0) Intended audience for this phrasing Expected number of search results Advantages of this phrasing Trending medical terms related to the query Trending term Trend strength score (0.0-1.0) Context for the trend (research, clinical, news) Relevance to original query (0.0-1.0) How long it's been trending Medical specialties driving the trend Popular search terms related to the query Popular term Popularity score (0.0-1.0) Relative search volume (High, Medium, Low) Primary user base for this term Seasonal usage pattern (if applicable) Stability of popularity (Stable, Growing, Declining) Recommended search strategy Recommended vocabularies for this query Recommended domains to focus on Suggested filters to apply General search tips for this type of query Personalized recommendations (if personalization=true) Detected user search patterns User's preferred vocabularies User's commonly searched domains Suggested medical specialties based on history Suggested learning opportunities Unique identifier for the request Request timestamp Suggestion engine version Total number of suggestions returned Time taken to generate suggestions Whether personalization was used Contextual factors influencing suggestions Vocabulary release version used ```bash cURL theme={null} curl -X GET "https://api.omophub.com/v1/search/suggest?query=hart%20atack&vocabulary_ids=SNOMED,ICD10CM&suggestion_types=spelling,synonym,related&medical_context=cardiology&include_explanations=true" \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" ``` ```javascript JavaScript theme={null} const response = await fetch('https://api.omophub.com/v1/search/suggest?query=diabetis&user_context=physician&include_corrections=true&include_alternatives=true&include_related=true&include_hierarchical=true', { method: 'GET', headers: { 'Authorization': 'Bearer YOUR_API_KEY', 'Content-Type': 'application/json' } }); const data = await response.json(); ``` ```python Python theme={null} import requests headers = { 'Authorization': 'Bearer YOUR_API_KEY', 'Content-Type': 'application/json' } params = { 'query': 'chest pain breathing problems', 'vocabulary_ids': 'SNOMED,ICD10CM', 'domain_ids': 'Condition,Procedure', 'suggestion_types': 'spelling,synonym,related,broader,narrower', 'medical_context': 'emergency_medicine', 'user_context': 'physician', 'search_intent': 'diagnostic', 'include_corrections': 'true', 'include_alternatives': 'true', 'include_related': 'true', 'include_hierarchical': 'true', 'include_trending': 'true', 'include_explanations': 'true', 'complexity_level': 'advanced', 'page_size': 30, 'relevance_threshold': 0.7 } response = requests.get( 'https://api.omophub.com/v1/search/suggest', headers=headers, params=params ) data = response.json() ``` ```json Response theme={null} { "success": true, "data": { "query": "hart atack", "query_analysis": { "detected_intent": "diagnostic", "confidence": 0.89, "query_quality": "Poor", "medical_concepts": [ { "concept": "heart attack", "confidence": 0.85, "issues": ["spelling error in 'hart'", "spelling error in 'atack'"] } ], "potential_issues": [ "Multiple spelling errors detected", "Informal terminology used", "Could be more specific" ], "improvement_opportunities": [ "Use correct spelling: 'heart attack'", "Consider medical terminology: 'myocardial infarction'", "Specify type: 'acute myocardial infarction'" ] }, "suggestions": { "spelling_corrections": [ { "original": "hart", "correction": "heart", "confidence": 0.95, "correction_type": "spelling", "explanation": "Common misspelling of 'heart' in medical queries" }, { "original": "atack", "correction": "attack", "confidence": 0.92, "correction_type": "spelling", "explanation": "Missing 't' in 'attack' - common typo" } ], "synonyms": [ { "original_term": "heart attack", "synonym": "myocardial infarction", "relevance_score": 0.98, "concept_id": 22298006, "vocabulary_id": "SNOMED", "usage_context": "Clinical documentation", "formality_level": "Technical", "explanation": "Standard medical terminology for heart attack, preferred in clinical settings" }, { "original_term": "heart attack", "synonym": "MI", "relevance_score": 0.92, "concept_id": 22298006, "vocabulary_id": "SNOMED", "usage_context": "Clinical abbreviation", "formality_level": "Clinical", "explanation": "Common clinical abbreviation for myocardial infarction" }, { "original_term": "heart attack", "synonym": "acute coronary syndrome", "relevance_score": 0.85, "concept_id": 394659003, "vocabulary_id": "SNOMED", "usage_context": "Emergency medicine", "formality_level": "Clinical", "explanation": "Broader clinical term encompassing heart attacks and related conditions" } ], "related_concepts": [ { "suggested_term": "angina pectoris", "relationship_type": "related_condition", "relevance_score": 0.87, "concept_id": 194828000, "vocabulary_id": "SNOMED", "domain_id": "Condition", "clinical_context": "Precursor or related chest pain condition", "explanation": "Related cardiac condition that can precede or mimic heart attack" }, { "suggested_term": "coronary artery disease", "relationship_type": "underlying_cause", "relevance_score": 0.82, "concept_id": 53741008, "vocabulary_id": "SNOMED", "domain_id": "Condition", "clinical_context": "Underlying pathology leading to heart attack", "explanation": "Primary underlying condition that causes most heart attacks" }, { "suggested_term": "cardiac arrest", "relationship_type": "potential_complication", "relevance_score": 0.79, "concept_id": 410429000, "vocabulary_id": "SNOMED", "domain_id": "Condition", "clinical_context": "Serious complication of heart attack", "explanation": "Potential life-threatening complication of myocardial infarction" } ], "broader_concepts": [ { "suggested_term": "cardiovascular disease", "hierarchy_level": 2, "relevance_score": 0.76, "concept_id": 49601007, "vocabulary_id": "SNOMED", "generalization_type": "system", "use_case": "For broader cardiovascular condition searches" }, { "suggested_term": "heart disease", "hierarchy_level": 1, "relevance_score": 0.81, "concept_id": 56265001, "vocabulary_id": "SNOMED", "generalization_type": "category", "use_case": "For general cardiac condition searches" } ], "narrower_concepts": [ { "suggested_term": "ST elevation myocardial infarction", "hierarchy_level": 1, "relevance_score": 0.89, "concept_id": 401314000, "vocabulary_id": "SNOMED", "specialization_type": "subtype", "clinical_specificity": "High - specific type based on ECG findings" }, { "suggested_term": "non-ST elevation myocardial infarction", "hierarchy_level": 1, "relevance_score": 0.87, "concept_id": 401303003, "vocabulary_id": "SNOMED", "specialization_type": "subtype", "clinical_specificity": "High - specific type based on ECG findings" }, { "suggested_term": "acute anterior myocardial infarction", "hierarchy_level": 1, "relevance_score": 0.84, "concept_id": 54329005, "vocabulary_id": "SNOMED", "specialization_type": "anatomical_location", "clinical_specificity": "Very High - location-specific type" } ], "alternative_phrasings": [ { "alternative_query": "heart attack", "phrasing_type": "corrected_lay", "appropriateness_score": 0.95, "target_audience": "General public, patients", "expected_results": 234, "advantages": [ "Correct spelling", "Widely understood", "Patient-friendly" ] }, { "alternative_query": "myocardial infarction", "phrasing_type": "medical_technical", "appropriateness_score": 0.92, "target_audience": "Healthcare professionals", "expected_results": 187, "advantages": [ "Precise medical terminology", "Preferred in clinical documentation", "Standardized term" ] }, { "alternative_query": "acute MI", "phrasing_type": "clinical_abbreviation", "appropriateness_score": 0.87, "target_audience": "Clinical staff", "expected_results": 156, "advantages": [ "Efficient clinical communication", "Commonly used abbreviation", "Time-saving" ] } ], "trending_suggestions": [ { "suggested_term": "troponin elevation", "trend_score": 0.78, "trend_context": "diagnostic_biomarker", "relevance_to_query": 0.82, "trend_duration": "6 months", "specialty_focus": [ "emergency_medicine", "cardiology" ] } ], "popular_suggestions": [ { "suggested_term": "chest pain", "popularity_score": 0.94, "search_volume": "High", "user_base": "Healthcare professionals and patients", "seasonal_pattern": "Stable year-round", "stability": "Stable" }, { "suggested_term": "cardiac symptoms", "popularity_score": 0.86, "search_volume": "Medium", "user_base": "Healthcare professionals", "seasonal_pattern": "None", "stability": "Stable" } ] }, "contextual_recommendations": { "search_strategy": "Start with corrected spelling, then explore specific types if needed", "vocabulary_recommendations": [ { "vocabulary": "SNOMED", "reason": "Comprehensive cardiac terminology", "priority": "High" }, { "vocabulary": "ICD10CM", "reason": "Administrative coding and billing", "priority": "Medium" } ], "domain_focus": [ "Condition", "Observation" ], "filter_suggestions": [ { "filter": "domain=Condition", "reason": "Focus on diagnostic conditions" }, { "filter": "specialty=cardiology", "reason": "Cardiac-specific context" } ], "search_tips": [ "Use correct spelling for better results", "Try both lay terms and medical terminology", "Consider specific types (STEMI, NSTEMI) for detailed information", "Include related symptoms (chest pain, shortness of breath) for broader context" ] }, "personalization_insights": null }, "meta": { "request_id": "req_search_suggest_123", "timestamp": "2024-01-15T10:30:00Z", "suggestion_engine_version": "v3.1.0", "total_suggestions": 45, "processing_time_ms": 178, "personalization_applied": false, "context_factors": [ "medical_context: cardiology", "spelling_errors_detected", "intent: diagnostic", "formality: mixed" ], "vocab_release": "2024.2" } } ``` ## Usage Examples ### Spelling Correction Suggestions Get suggestions for queries with spelling errors: ```bash theme={null} curl -X GET "https://api.omophub.com/v1/search/suggest?query=pnemonia&suggestion_types=spelling,synonym&include_corrections=true" \ -H "Authorization: Bearer YOUR_API_KEY" ``` ### Comprehensive Query Suggestions Get comprehensive suggestions for query improvement: ```bash theme={null} curl -X GET "https://api.omophub.com/v1/search/suggest?query=diabetes&suggestion_types=all&medical_context=endocrinology&user_context=physician&include_explanations=true" \ -H "Authorization: Bearer YOUR_API_KEY" ``` ### Alternative Phrasing Suggestions Get alternative ways to phrase a medical query: ```bash theme={null} curl -X GET "https://api.omophub.com/v1/search/suggest?query=high%20blood%20pressure&suggestion_types=alternative,synonym&complexity_level=technical&patient_facing=false" \ -H "Authorization: Bearer YOUR_API_KEY" ``` ### Hierarchical Concept Suggestions Get broader and narrower concept suggestions: ```bash theme={null} curl -X GET "https://api.omophub.com/v1/search/suggest?query=pneumonia&suggestion_types=broader,narrower&include_hierarchical=true&vocabulary_ids=SNOMED" \ -H "Authorization: Bearer YOUR_API_KEY" ``` ### Patient-Friendly Suggestions Get patient-appropriate suggestions: ```bash theme={null} curl -X GET "https://api.omophub.com/v1/search/suggest?query=myocardial%20infarction&user_context=patient&patient_facing=true&complexity_level=simple&include_alternatives=true" \ -H "Authorization: Bearer YOUR_API_KEY" ``` ### Trending and Popular Suggestions Get trending and popular related terms: ```bash theme={null} curl -X GET "https://api.omophub.com/v1/search/suggest?query=covid&suggestion_types=trending,popular,related&include_trending=true&include_popular=true" \ -H "Authorization: Bearer YOUR_API_KEY" ``` ## Suggestion Types ### Spelling Corrections * **Purpose**: Fix typos and spelling errors * **Confidence**: High for obvious corrections * **Use Case**: Improve query accuracy * **Examples**: "hart" → "heart", "diabetis" → "diabetes" ### Synonyms * **Purpose**: Alternative terms with same meaning * **Formality Levels**: Technical, Clinical, Lay * **Use Case**: Find standard terminology * **Examples**: "heart attack" → "myocardial infarction" ### Related Concepts * **Relationships**: Associated, causes, treats, complicates * **Clinical Context**: Related conditions and procedures * **Use Case**: Explore connected medical concepts * **Examples**: "diabetes" → "insulin", "hyperglycemia" ### Broader Concepts * **Hierarchy**: Move up concept hierarchies * **Generalization**: Category, system, class level * **Use Case**: Widen search scope * **Examples**: "pneumonia" → "lung disease" → "respiratory condition" ### Narrower Concepts * **Specialization**: Subtypes, variants, specific cases * **Clinical Specificity**: Increase diagnostic precision * **Use Case**: Get more specific results * **Examples**: "diabetes" → "type 2 diabetes" → "insulin-resistant diabetes" ### Alternative Phrasings * **Formality**: Formal, informal, technical, lay * **Target Audience**: Professionals, patients, researchers * **Use Case**: Reach different audiences * **Examples**: "MI" vs "heart attack" vs "myocardial infarction" ### Trending Suggestions * **Temporal**: Currently popular or emerging * **Context**: Research, clinical practice, news * **Use Case**: Discover emerging topics * **Examples**: New treatments, recent outbreaks, research terms ### Popular Suggestions * **Usage**: Commonly searched terms * **Stability**: Stable, growing, or declining popularity * **Use Case**: Find common related searches * **Examples**: High-volume search terms ## Search Intent Detection ### Diagnostic Intent * **Characteristics**: Symptom descriptions, condition names * **Suggestions**: Differential diagnoses, related symptoms * **Context**: Clinical assessment, patient evaluation * **Examples**: "chest pain" → diagnostic conditions ### Therapeutic Intent * **Characteristics**: Treatment queries, medication searches * **Suggestions**: Treatment options, drug alternatives * **Context**: Treatment planning, medication selection * **Examples**: "diabetes treatment" → therapeutic options ### Procedural Intent * **Characteristics**: Procedure names, intervention queries * **Suggestions**: Related procedures, alternatives * **Context**: Procedure selection, surgical planning * **Examples**: "cardiac catheterization" → related procedures ### Research Intent * **Characteristics**: Technical terms, study-related queries * **Suggestions**: Research terminology, methodological terms * **Context**: Academic research, literature review * **Examples**: "biomarkers" → research concepts ### Educational Intent * **Characteristics**: Learning-focused queries * **Suggestions**: Educational resources, explanatory terms * **Context**: Medical education, patient education * **Examples**: "anatomy" → educational concepts ### Administrative Intent * **Characteristics**: Coding, billing, documentation queries * **Suggestions**: Administrative codes, documentation terms * **Context**: Medical coding, billing processes * **Examples**: "ICD codes" → administrative terminology ## Personalization Features ### User Pattern Analysis * **Search History**: Commonly searched terms and domains * **Vocabulary Preferences**: Preferred medical vocabularies * **Specialty Focus**: Medical specialties of interest * **Complexity Level**: Preferred terminology complexity ### Personalized Suggestions * **Relevant Terms**: Terms matching user's interests * **Vocabulary Bias**: Suggestions from preferred vocabularies * **Specialty Context**: Specialty-specific recommendations * **Learning Path**: Educational progression suggestions ### Privacy Considerations * **Opt-in**: Personalization requires explicit consent * **Data Security**: Secure storage of user patterns * **Anonymization**: Personal data anonymization * **User Control**: Users can disable personalization ## Cultural and Regional Context ### Regional Terminology * **US Medical Terms**: American medical terminology preferences * **UK Medical Terms**: British medical terminology variations * **International**: WHO and international standard terms * **Multicultural**: Inclusive terminology for diverse populations ### Cultural Sensitivity * **Inclusive Language**: Culturally appropriate medical terms * **Accessibility**: Terms accessible to diverse populations * **Local Practices**: Regional medical practice variations * **Language Variants**: Regional language preferences ## Related Endpoints * [Search Autocomplete](/api-reference/search/search-autocomplete) - Real-time query completion * [Basic Search](/api-reference/search/basic-search) - Execute suggested searches * [Semantic Search](/api-reference/search/semantic-search) - Meaning-based search * [Fuzzy Search](/api-reference/search/fuzzy-search) - Typo-tolerant search # Search Trending Source: https://docs.omophub.com/api-reference/search/search-trending GET /v1/search/trending Retrieve trending and popular OMOP concept search queries based on platform-wide usage patterns and analytics data to surface relevant terms. This endpoint provides insights into trending medical concepts and popular search queries, helping users discover relevant terminology based on community usage patterns and temporal trends. ## Query Parameters Comma-separated list of vocabulary IDs to filter trending searches
**Example:** `SNOMED,ICD10CM,LOINC`
Comma-separated list of domain IDs to focus trending analysis
**Example:** `Condition,Drug,Procedure`
Time period for trending analysis
**Options:** `1d`, `7d`, `30d`, `90d`
Type of trending metric to analyze
**Options:** `search_volume`, `new_concepts`, `rising_queries`, `seasonal_patterns`
Include detailed trending statistics and growth metrics Include related trending concepts and co-occurrence patterns Number of trending items to return (max 100) Page number for pagination (1-based) ## Response Array of trending search items with analytics data The trending search query or concept term Associated concept ID if trending item is a specific concept (returned as string to prevent precision loss) Standard concept name if applicable Vocabulary containing the trending concept Domain classification of the trending concept Normalized trending score (0-100) Number of searches in the specified time period Percentage growth rate compared to previous period Direction of the trend
**Values:** `rising`, `declining`, `stable`, `new`
Detailed trending statistics (when include\_statistics=true) Highest search volume in the period Average daily search volume Trend consistency score (0-1) Seasonal adjustment factor ISO timestamp when trend was first detected Rate of change in search volume Related trending concepts (when include\_related=true) Related concept identifier (returned as string to prevent precision loss) Related concept name Correlation strength with main trending item (0-1) Rate of co-occurrence in searches Geographic distribution of trend (when available) Top geographic regions for this trend Regional trending scores
Response metadata and pagination information Current page number Items per page Total trending items available Total number of pages Whether next page exists Whether previous page exists Time period used for trend analysis Analysis start date (ISO format) Analysis end date (ISO format) Type of period analyzed Timestamp of last trending data update ```bash theme={null} curl -X GET "https://api.omophub.com/v1/search/trending?time_period=7d&trend_type=search_volume&include_statistics=true&page_size=10" \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" ``` ```javascript theme={null} const response = await fetch('https://api.omophub.com/v1/search/trending?time_period=7d&trend_type=search_volume&include_statistics=true&page_size=10', { method: 'GET', headers: { 'Authorization': 'Bearer YOUR_API_KEY', 'Content-Type': 'application/json' } }); const trendingData = await response.json(); console.log('Trending searches:', trendingData.data.data); ``` ```python theme={null} import requests url = "https://api.omophub.com/v1/search/trending" params = { "time_period": "7d", "trend_type": "search_volume", "include_statistics": True, "page_size": 10 } headers = { "Authorization": "Bearer YOUR_API_KEY", "Content-Type": "application/json" } response = requests.get(url, params=params, headers=headers) trending_data = response.json() print(f"Found {len(trending_data['data']['data'])} trending items") for item in trending_data['data']['data']: print(f"- {item['query']}: {item['trend_score']} score, {item['growth_rate']}% growth") ``` ```json theme={null} { "success": true, "data": { "data": [ { "query": "covid-19 symptoms", "concept_id": "840539006", "concept_name": "Disease caused by severe acute respiratory syndrome coronavirus 2", "vocabulary_id": "SNOMED", "domain_id": "Condition", "trend_score": 95.8, "search_volume": 15420, "growth_rate": 234.7, "trend_direction": "rising", "statistics": { "peak_volume": 2180, "average_volume": 1850.0, "consistency_score": 0.82, "seasonal_factor": 1.15, "first_seen": "2024-12-15T08:00:00Z", "velocity": 12.3 }, "related_concepts": [ { "concept_id": "49727002", "concept_name": "Cough", "correlation_score": 0.78, "co_occurrence_rate": 0.45 }, { "concept_id": "386661006", "concept_name": "Fever", "correlation_score": 0.72, "co_occurrence_rate": 0.41 } ] }, { "query": "diabetes management", "concept_id": "73211009", "concept_name": "Diabetes mellitus", "vocabulary_id": "SNOMED", "domain_id": "Condition", "trend_score": 87.3, "search_volume": 8920, "growth_rate": 45.2, "trend_direction": "rising", "statistics": { "peak_volume": 1420, "average_volume": 1274.3, "consistency_score": 0.91, "seasonal_factor": 1.03, "first_seen": "2024-12-10T14:30:00Z", "velocity": 8.7 } }, { "query": "hypertension treatment", "concept_id": "38341003", "concept_name": "Hypertensive disorder, systemic arterial", "vocabulary_id": "SNOMED", "domain_id": "Condition", "trend_score": 79.1, "search_volume": 6750, "growth_rate": 28.9, "trend_direction": "stable" } ], "meta": { "pagination": { "page": 1, "page_size": 10, "total_items": 156, "total_pages": 16, "has_next": true, "has_previous": false }, "analysis_period": { "start_date": "2024-12-15T00:00:00Z", "end_date": "2024-12-22T00:00:00Z", "period_type": "7d" }, "data_freshness": "2024-12-22T12:00:00Z", "request_id": "req_search_trending_7d_analytics_001", "timestamp": "2024-12-22T10:30:00Z", "vocab_release": "2025.2" } } } ``` ## Usage Examples ### Basic Trending Analysis Get the most popular trending searches in the past week: ```javascript theme={null} const trending = await fetch('/v1/search/trending?time_period=7d&page_size=20'); ``` ### Domain-Specific Trends Find trending searches within specific medical domains: ```javascript theme={null} const conditionTrends = await fetch('/v1/search/trending?domain_ids=Condition,Drug&time_period=30d'); ``` ### Rising Query Detection Identify rapidly growing search queries: ```javascript theme={null} const risingQueries = await fetch('/v1/search/trending?trend_type=rising_queries&include_statistics=true'); ``` ### Vocabulary-Specific Analysis Analyze trends within specific vocabularies: ```javascript theme={null} const snomedTrends = await fetch('/v1/search/trending?vocabulary_ids=SNOMED&time_period=90d&include_related=true'); ``` ### Seasonal Pattern Analysis Detect seasonal trending patterns: ```javascript theme={null} const seasonalTrends = await fetch('/v1/search/trending?trend_type=seasonal_patterns&time_period=90d'); ``` ## Related Endpoints * [Search Concepts](/api-reference/search/basic-search) - Primary concept search functionality * [Search Autocomplete](/api-reference/search/search-autocomplete) - Real-time search suggestions * [Search Facets](/api-reference/search/search-facets) - Search result faceting and filtering * [Search Suggest](/api-reference/search/search-suggest) - Intelligent search suggestions * [Get Vocabulary Statistics](/api-reference/vocabulary/get-vocabulary-statistics) - Overall vocabulary usage stats ## Notes * Trending data is updated every 4 hours with the latest search analytics * Trend scores are normalized across all vocabularies and time periods * Geographic data may not be available for all trending items due to privacy considerations * Rising queries are identified using proprietary algorithms that account for baseline search volume * Seasonal patterns require at least 90 days of historical data for accurate detection * Some trending data may be filtered to exclude potentially sensitive health information # Bulk Semantic Search Source: https://docs.omophub.com/api-reference/search/semantic-bulk-search POST https://api.omophub.com/v1/search/semantic-bulk Perform semantic OMOP concept search on multiple natural-language queries simultaneously with optimized batch processing for LLM and AI workflows. ## Overview This endpoint allows you to submit multiple semantic search queries in a single request, combining the power of vector similarity matching with efficient batch processing. We host a vector similarity service and do not use third party services for this. It's ideal for processing clinical notes, batch NLP pipelines, or any workflow requiring high-confidence concept matching across many terms. Each query uses vector similarity search with a default limit of 10 results per query (configurable via `page_size`, max 50). Up to 25 queries can be submitted per request. ## Request Body Array of semantic search query objects (1-25 items) Unique identifier for this search within the batch Natural language search query (1-500 characters) Number of results per search (1-50). Overrides `defaults.page_size` for this search. Minimum similarity score (0.0-1.0). Higher values = stricter matching. Overrides `defaults.threshold` for this search. Filter results to specific vocabularies (e.g., `["SNOMED", "ICD10CM"]`). Overrides `defaults.vocabulary_ids` for this search. Filter results to specific domains (e.g., `["Condition", "Drug"]`). Overrides `defaults.domain_ids` for this search. Filter by standard concept status: `"S"` (Standard) or `"C"` (Classification). Overrides `defaults.standard_concept` for this search. Filter by concept class (e.g., `"Clinical Finding"`). Overrides `defaults.concept_class_id` for this search. Default parameters applied to all searches. Individual searches can override any default. Default results per search (1-50) Default minimum similarity score (0.0-1.0) Default vocabulary filter for all searches (e.g., `["SNOMED"]`) Default domain filter for all searches (e.g., `["Condition"]`) Default standard concept filter: `"S"` or `"C"` Default concept class filter ## Query Parameters Specific vocabulary release version (defaults to latest) ## Response Indicates if the request was successful Response data object containing results and summary counts Array of search results, one per query Identifier matching the request's search\_id Original search query Query execution status: `completed` or `failed` Array of matching concepts with similarity scores Unique concept identifier Primary concept name Concept code from source vocabulary Source vocabulary Domain classification Concept class Standard concept indicator: `S`, `C`, or `null` Semantic similarity score (0.0-1.0). Higher = more similar. The text that matched (concept name or synonym) Error message (only present if query failed) The similarity threshold used for this query Number of results returned for this query Processing time for this query in milliseconds Query enhancement details (only present if query was modified) Original query before enhancement Enhanced query used for search List of abbreviations that were expanded List of misspellings that were corrected Total number of searches in the request Number of successfully completed searches Number of failed searches Total processing time in milliseconds Unique identifier for the request Vocabulary release version used Request timestamp ```bash cURL theme={null} curl -X POST "https://api.omophub.com/v1/search/semantic-bulk" \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "defaults": { "vocabulary_ids": ["SNOMED"], "standard_concept": "S", "threshold": 0.5, "page_size": 5 }, "searches": [ { "search_id": "s1", "query": "heart attack" }, { "search_id": "s2", "query": "sugar diabetes", "threshold": 0.7 }, { "search_id": "s3", "query": "aspirin tablets", "vocabulary_ids": ["RxNorm"], "domain_ids": ["Drug"] } ] }' ``` ```javascript JavaScript theme={null} const response = await fetch('https://api.omophub.com/v1/search/semantic-bulk', { method: 'POST', headers: { 'Authorization': 'Bearer YOUR_API_KEY', 'Content-Type': 'application/json' }, body: JSON.stringify({ defaults: { vocabulary_ids: ['SNOMED'], standard_concept: 'S', threshold: 0.5, page_size: 5 }, searches: [ { search_id: 's1', query: 'heart attack' }, { search_id: 's2', query: 'sugar diabetes', threshold: 0.7 }, { search_id: 's3', query: 'aspirin tablets', vocabulary_ids: ['RxNorm'], domain_ids: ['Drug'] } ] }) }); const data = await response.json(); ``` ```python Python theme={null} import requests headers = { 'Authorization': 'Bearer YOUR_API_KEY', 'Content-Type': 'application/json' } payload = { 'defaults': { 'vocabulary_ids': ['SNOMED'], 'standard_concept': 'S', 'threshold': 0.5, 'page_size': 5 }, 'searches': [ { 'search_id': 's1', 'query': 'heart attack' }, { 'search_id': 's2', 'query': 'sugar diabetes', 'threshold': 0.7 }, { 'search_id': 's3', 'query': 'aspirin tablets', 'vocabulary_ids': ['RxNorm'], 'domain_ids': ['Drug'] } ] } response = requests.post( 'https://api.omophub.com/v1/search/semantic-bulk', headers=headers, json=payload ) data = response.json() ``` ```json Response theme={null} { "success": true, "data": { "results": [ { "search_id": "s1", "query": "heart attack", "status": "completed", "results": [ { "concept_id": 4329847, "concept_name": "Myocardial infarction", "concept_code": "22298006", "vocabulary_id": "SNOMED", "domain_id": "Condition", "concept_class_id": "Clinical Finding", "standard_concept": "S", "similarity_score": 0.92, "matched_text": "Myocardial infarction" }, { "concept_id": 434376, "concept_name": "Acute myocardial infarction", "concept_code": "57054005", "vocabulary_id": "SNOMED", "domain_id": "Condition", "concept_class_id": "Clinical Finding", "standard_concept": "S", "similarity_score": 0.89, "matched_text": "Acute myocardial infarction" } ], "similarity_threshold": 0.5, "result_count": 2, "duration": 45, "query_enhancement": null }, { "search_id": "s2", "query": "sugar diabetes", "status": "completed", "results": [ { "concept_id": 201826, "concept_name": "Type 2 diabetes mellitus", "concept_code": "44054006", "vocabulary_id": "SNOMED", "domain_id": "Condition", "concept_class_id": "Clinical Finding", "standard_concept": "S", "similarity_score": 0.88, "matched_text": "Type 2 diabetes mellitus" }, { "concept_id": 4000678, "concept_name": "Diabetes mellitus", "concept_code": "73211009", "vocabulary_id": "SNOMED", "domain_id": "Condition", "concept_class_id": "Clinical Finding", "standard_concept": "S", "similarity_score": 0.85, "matched_text": "Diabetes mellitus" } ], "similarity_threshold": 0.7, "result_count": 2, "duration": 38, "query_enhancement": null }, { "search_id": "s3", "query": "aspirin tablets", "status": "completed", "results": [ { "concept_id": 1112807, "concept_name": "Aspirin", "concept_code": "1191", "vocabulary_id": "RxNorm", "domain_id": "Drug", "concept_class_id": "Ingredient", "standard_concept": "S", "similarity_score": 0.94, "matched_text": "Aspirin" } ], "similarity_threshold": 0.5, "result_count": 1, "duration": 32, "query_enhancement": null } ], "total_searches": 3, "completed_count": 3, "failed_count": 0, "total_duration": 156 }, "meta": { "request_id": "req_sem_bulk_abc123", "vocab_release": "2025.2", "timestamp": "2025-01-15T10:30:00Z" } } ``` ## Key Differences from Bulk Search | Feature | Bulk Semantic Search | Bulk Search | | ----------------------- | ---------------------------------------------- | ------------------------------- | | **Search method** | Vector similarity (embeddings) | Full-text keyword matching | | **Score field** | `similarity_score` (0.0-1.0) | `search_score` (relevance rank) | | **Max queries** | 25 per request | 50 per request | | **Max page\_size** | 50 | 100 | | **Threshold parameter** | Yes (filters by similarity) | No | | **Query enhancement** | Yes (abbreviation expansion, typo correction) | No | | **Response shape** | `data` (object with results array and summary) | `data` (direct array) | ## Use Cases ### Clinical Notes Processing Process extracted terms from clinical notes in batch: ```python theme={null} # Terms extracted from clinical notes via NLP clinical_terms = [ "chest pain radiating to left arm", "shortness of breath on exertion", "elevated troponin levels", "irregular heartbeat" ] payload = { "defaults": { "vocabulary_ids": ["SNOMED"], "domain_ids": ["Condition", "Observation"], "standard_concept": "S", "threshold": 0.6 }, "searches": [ {"search_id": f"note_{i}", "query": term} for i, term in enumerate(clinical_terms) ] } response = requests.post( "https://api.omophub.com/v1/search/semantic-bulk", headers=headers, json=payload ) ``` ### High-Confidence Batch Matching Use a high threshold for automated mapping pipelines where accuracy is critical: ```python theme={null} payload = { "defaults": { "threshold": 0.8, "standard_concept": "S", "page_size": 3 }, "searches": [ {"search_id": "dx1", "query": "heart attack"}, {"search_id": "dx2", "query": "high blood pressure"}, {"search_id": "dx3", "query": "sugar diabetes"} ] } ``` ### Multi-Domain Batch Search Search across different domains in a single request using per-search overrides: ```python theme={null} payload = { "defaults": {"standard_concept": "S", "threshold": 0.5}, "searches": [ { "search_id": "cond1", "query": "chest pain", "domain_ids": ["Condition"], "vocabulary_ids": ["SNOMED"] }, { "search_id": "drug1", "query": "blood thinner medication", "domain_ids": ["Drug"], "vocabulary_ids": ["RxNorm"] }, { "search_id": "lab1", "query": "blood sugar test", "domain_ids": ["Measurement"], "vocabulary_ids": ["LOINC"] } ] } ``` ## Error Handling ### Per-Query Failure Isolation Each query in the batch is processed independently. Failed queries do not affect other queries: ```json theme={null} { "success": true, "data": { "results": [ { "search_id": "s1", "query": "heart attack", "status": "completed", "results": [...], "similarity_threshold": 0.5, "result_count": 5, "duration": 42, "query_enhancement": null }, { "search_id": "s2", "query": "", "status": "failed", "results": [], "error": "Search query is required", "similarity_threshold": 0.5, "result_count": 0, "duration": 1, "query_enhancement": null } ], "total_searches": 2, "completed_count": 1, "failed_count": 1, "total_duration": 48 } } ``` ## Related Endpoints * [Semantic Search](/api-reference/search/semantic-search) - Single query semantic search with pagination * [Bulk Search](/api-reference/search/bulk-search) - Keyword-based bulk search (up to 50 queries) * [Basic Search](/api-reference/search/basic-search) - Single query keyword search with pagination * [Similar Concepts](/api-reference/search/search-similar) - Find concepts similar to a given concept # Semantic Search Source: https://docs.omophub.com/api-reference/search/semantic-search GET /concepts/semantic-search ## Overview Semantic search uses LLM generated embeddings to find OMOP concepts that are semantically similar to your query, even when exact keyword matches don't exist. This is ideal for natural language queries and clinical text processing. **Example queries:** * "heart attack" → finds "Myocardial infarction" * "sugar diabetes" → finds "Type 2 diabetes mellitus" * "high blood pressure" → finds "Essential hypertension" * "belly pain" → finds "Abdominal pain" ## When to Use Semantic Search | Scenario | Use Semantic Search | Use Basic Search | | ------------------------- | ------------------- | ---------------- | | Natural language queries | Yes | No | | Patient-reported symptoms | Yes | No | | Clinical shorthand/slang | Yes | No | | Exact code lookup | No | Yes | | Browsing vocabularies | No | Yes | ## Query Parameters Natural language search query (1-500 characters) Page number (1-based) Results per page (1-100) Minimum similarity score (0.0-1.0). Higher values = stricter matching.
**Recommended thresholds:** - `0.7` - Very high confidence matches only - `0.5` - Balanced, high precision (default) - `0.3` - More exploratory results
Filter to specific vocabularies (comma-separated)
**Examples:** `SNOMED`, `SNOMED,ICD10CM`, `SNOMED,ICD10CM,RXNORM`
Filter to specific domains (comma-separated)
**Examples:** `Condition`, `Drug`, `Condition,Drug,Procedure`
Filter by standard concept status
**Values:** `S` (Standard), `C` (Classification)
Vocabulary version (e.g., `2025v2`). Uses default if not specified. ```bash cURL theme={null} curl -X GET "https://api.omophub.com/v1/concepts/semantic-search?query=heart%20attack&page_size=5" \ -H "Authorization: Bearer YOUR_API_KEY" ``` ```python Python theme={null} import requests response = requests.get( "https://api.omophub.com/v1/concepts/semantic-search", params={"query": "heart attack", "page_size": 5}, headers={"Authorization": "Bearer YOUR_API_KEY"} ) results = response.json() for concept in results["data"]["results"]: print(f"{concept['similarity_score']:.2f} - {concept['concept_name']}") ``` ```javascript JavaScript (Node.js) theme={null} const params = new URLSearchParams({ query: 'heart attack', page_size: '5', }); const response = await fetch( `https://api.omophub.com/v1/concepts/semantic-search?${params}`, { headers: { Authorization: `Bearer ${process.env.OMOPHUB_API_KEY}`, }, } ); const results = await response.json(); ``` ```bash cURL (with filters) theme={null} curl -X GET "https://api.omophub.com/v1/concepts/semantic-search?query=chest%20pain&vocabulary_ids=SNOMED,ICD10CM&domain_ids=Condition&threshold=0.5" \ -H "Authorization: Bearer YOUR_API_KEY" ``` ```python Python (with filters) theme={null} import requests params = { "query": "chest pain", "vocabulary_ids": "SNOMED,ICD10CM", "domain_ids": "Condition", "threshold": 0.5, "standard_concept": "S" } response = requests.get( "https://api.omophub.com/v1/concepts/semantic-search", headers={"Authorization": "Bearer YOUR_API_KEY"}, params=params ) filtered_results = response.json() ``` ```python Python (pagination) theme={null} import requests # Get page 2 of results params = { "query": "diabetes", "page": 2, "page_size": 20 } response = requests.get( "https://api.omophub.com/v1/concepts/semantic-search", headers={"Authorization": "Bearer YOUR_API_KEY"}, params=params ) page_2_results = response.json() pagination = page_2_results["meta"]["pagination"] print(f"Page {pagination['page']} of {pagination['total_pages']}") ``` ```json theme={null} { "success": true, "data": { "query": "heart attack", "results": [ { "concept_id": 4329847, "concept_name": "Myocardial infarction", "domain_id": "Condition", "vocabulary_id": "SNOMED", "concept_class_id": "Clinical Finding", "standard_concept": "S", "concept_code": "22298006", "similarity_score": 0.92, "matched_text": "Myocardial infarction" }, { "concept_id": 434376, "concept_name": "Acute myocardial infarction", "domain_id": "Condition", "vocabulary_id": "SNOMED", "concept_class_id": "Clinical Finding", "standard_concept": "S", "concept_code": "57054005", "similarity_score": 0.89, "matched_text": "Acute myocardial infarction" }, { "concept_id": 4108217, "concept_name": "Old myocardial infarction", "domain_id": "Condition", "vocabulary_id": "SNOMED", "concept_class_id": "Clinical Finding", "standard_concept": "S", "concept_code": "1755008", "similarity_score": 0.85, "matched_text": "Old myocardial infarction" } ], "total_results": 45, "latency_ms": 28 }, "meta": { "pagination": { "page": 1, "page_size": 20, "total_items": 45, "total_pages": 3, "has_next": true, "has_previous": false }, "request_id": "req_sem_abc123", "vocab_release": "2025v1", "timestamp": "2025-01-15T10:30:00Z", "search": { "query": "heart attack", "total_results": 45, "filters_applied": {} } } } ``` ## Response Fields ### Data Object Original search query Array of matching concepts with similarity scores Approximate number of matching concepts. This is a lower bound based on sampled results, not an exact count. Use `has_next` for reliable pagination. Processing time in milliseconds ### Result Object OMOP concept\_id Standard concept name Semantic similarity score (0.0-1.0). Higher = more similar. The text that matched (concept name or synonym) OMOP domain (e.g., Condition, Drug, Procedure) Source vocabulary (e.g., SNOMED, ICD10CM, RxNorm) Concept classification within the vocabulary Standard concept flag: S (Standard), C (Classification), or null Original code from the source vocabulary ### Pagination Object (in meta) Current page number Number of results per page Approximate total number of matching items (lower bound). Use `has_next` for reliable pagination. Approximate total number of pages. Use `has_next` to determine if more pages exist. **Reliable indicator** of whether more results are available. Use this for pagination loops. **Reliable indicator** of whether previous pages exist. **Pagination Note:** For performance reasons, `total_items` and `total_pages` are approximate values based on sampled results. Always use `has_next` to determine if more pages exist rather than comparing `page` to `total_pages`. ## How It Works 1. **Query Embedding**: Your query is converted to a 768-dimensional vector using neural embeddings 2. **Vector Search**: The query vector is compared against pre-computed concept embeddings 3. **Ranking**: Results are ranked by cosine similarity score 4. **Filtering**: Optional filters (vocabulary, domain) are applied 5. **Deduplication**: Results are deduplicated by concept\_id (keeping highest score) ## Similarity Score Interpretation | Score Range | Interpretation | | ----------- | --------------------------------------- | | 0.9 - 1.0 | Excellent match, high confidence | | 0.7 - 0.9 | Good match, likely relevant | | 0.5 - 0.7 | Moderate match, review recommended | | 0.3 - 0.5 | Weak match, may be tangentially related | | \< 0.3 | Poor match, likely not relevant | ## Performance * **Latency**: \~15-50ms typical * **Throughput**: \~100 requests/second ## Use Cases ### 1. Natural Language Processing Process patient-reported symptoms and clinical notes: ```python theme={null} # Patient says: "I've been having trouble breathing" results = client.semantic_search(query="trouble breathing") # Returns: Dyspnea, Shortness of breath, Respiratory distress ``` ### 2. Clinical Decision Support Map clinical observations to standard codes: ```python theme={null} # Nurse notes: "pt appears confused and agitated" results = client.semantic_search( query="confused and agitated", domain_ids="Condition" ) # Returns: Delirium, Acute confusional state, Agitation ``` ### 3. Code Mapping Assistance Find mappings for non-standard terminology: ```python theme={null} # Legacy code description: "DM2 uncontrolled" results = client.semantic_search( query="DM2 uncontrolled", vocabulary_ids="SNOMED", standard_concept="S" ) # Returns: Type 2 diabetes mellitus without complications ``` ### 4. Paginating Through Results Iterate through large result sets: ```python theme={null} page = 1 while True: results = client.semantic_search( query="diabetes", page=page, page_size=50 ) # Process results for concept in results["data"]["results"]: print(concept["concept_name"]) # Check if there are more pages if not results["meta"]["pagination"]["has_next"]: break page += 1 ``` ## Related Endpoints * [Basic Concept Search](/api-reference/search/basic-search) - Keyword-based search * [Similar Concepts](/api-reference/search/search-similar) - Find concepts similar to a given concept (supports `algorithm: "semantic"` to use the same embedding model) * [Autocomplete](/api-reference/search/search-autocomplete) - Type-ahead suggestions **Tip:** The `/search/similar` endpoint also supports semantic search via the `algorithm: "semantic"` parameter. Use that endpoint when you need additional features like starting from a `concept_id` or getting detailed similarity explanations. # Get Vocabulary Details Source: https://docs.omophub.com/api-reference/vocabularies/get-vocabulary GET /vocabularies/{vocabularyId} ## Overview Retrieve details about a specific vocabulary including its name, reference, version, and concept identifier. ## Path Parameters The vocabulary identifier (e.g., "SNOMED", "ICD10CM", "LOINC", "RxNorm") ## Query Parameters Specific vocabulary release version (e.g., "2025.1") ## Response Indicates whether the request was successful Vocabulary details Vocabulary identifier (e.g., "SNOMED", "ICD10CM") Human-readable vocabulary name Reference or source organization for the vocabulary Version identifier for the vocabulary Concept ID representing this vocabulary in the concept table Response metadata Unique identifier for request tracing ISO 8601 timestamp Vocabulary release version used ```bash cURL theme={null} curl -X GET "https://api.omophub.com/v1/vocabularies/SNOMED" \ -H "Authorization: Bearer YOUR_API_KEY" ``` ```python Python theme={null} import requests response = requests.get( "https://api.omophub.com/v1/vocabularies/SNOMED", headers={"Authorization": "Bearer YOUR_API_KEY"} ) vocabulary = response.json() print(f"Name: {vocabulary['data']['vocabulary_name']}") print(f"Version: {vocabulary['data']['vocabulary_version']}") ``` ```javascript JavaScript theme={null} const response = await fetch('https://api.omophub.com/v1/vocabularies/SNOMED', { headers: { 'Authorization': 'Bearer YOUR_API_KEY' } }); const vocabulary = await response.json(); ``` ```json theme={null} { "success": true, "data": { "vocabulary_id": "SNOMED", "vocabulary_name": "Systematized Nomenclature of Medicine Clinical Terms", "vocabulary_reference": "SNOMED International", "vocabulary_version": "2024-07-01", "vocabulary_concept_id": 45756746 }, "meta": { "request_id": "req_vocab_123", "timestamp": "2024-12-22T10:00:00Z", "vocab_release": "2025.1" } } ``` ## Notes * Use the vocabulary ID returned from the [List Vocabularies](/api-reference/vocabularies/list-vocabularies) endpoint * For detailed statistics, use the [Get Vocabulary Stats](/api-reference/vocabularies/get-vocabulary-stats) endpoint * For domain information, use the [Get Vocabulary Domains](/api-reference/vocabularies/get-vocabulary-domains) endpoint # Get Vocabulary Concept Classes Source: https://docs.omophub.com/api-reference/vocabularies/get-vocabulary-concept-classes GET https://api.omophub.com/v1/vocabularies/concept-classes Retrieve all concept classes within a specific OMOP vocabulary - Clinical Finding for SNOMED, Ingredient for RxNorm, and each vocabulary's class set. ## Overview This endpoint returns all OMOP concept classes, providing insights into the classification structure of medical concepts. ## Query Parameters Specific vocabulary release version (e.g., "2025.1") ## Response Indicates whether the request was successful Array of concept classes Unique identifier for the concept class Human-readable name of the concept class The concept ID representing this concept class Response metadata Unique identifier for request tracing ISO 8601 timestamp Vocabulary release version used ```bash cURL theme={null} curl -X GET "https://api.omophub.com/v1/vocabularies/concept-classes" \ -H "Authorization: Bearer YOUR_API_KEY" ``` ```python Python theme={null} import requests response = requests.get( "https://api.omophub.com/v1/vocabularies/concept-classes", headers={"Authorization": "Bearer YOUR_API_KEY"} ) data = response.json() ``` ```json theme={null} { "success": true, "data": [ { "concept_class_id": "Clinical Finding", "concept_class_name": "Clinical Finding", "concept_class_concept_id": 404684003 }, { "concept_class_id": "Procedure", "concept_class_name": "Procedure", "concept_class_concept_id": 71388002 }, { "concept_class_id": "Substance", "concept_class_name": "Substance", "concept_class_concept_id": 105590001 } ], "meta": { "request_id": "req_abc123", "timestamp": "2024-12-22T10:30:00Z", "vocab_release": "2025.1" } } ``` ## Notes * Returns all concept classes from the vocabulary database * Results are ordered alphabetically by concept class name * The `concept_class_concept_id` field contains the concept ID that represents this concept class # Get Vocabulary Concepts Source: https://docs.omophub.com/api-reference/vocabularies/get-vocabulary-concepts GET /v1/vocabularies/{vocabularyId}/concepts Get concepts within a specific OMOP vocabulary with pagination - browse SNOMED, ICD-10, LOINC, RxNorm, or any of 100+ vocabulary code lists. Retrieve concepts that belong to a specific vocabulary with optional filtering and pagination. ## Path Parameters The vocabulary identifier (e.g., "SNOMED", "ICD10CM", "LOINC", "RxNorm"). ## Query Parameters Search term to filter concepts by name or code. Page number for pagination (1-based, max 1000). For large vocabularies, use the `search` parameter to filter results instead of deep pagination. Number of concepts per page (max 1000). Filter by standard concept status. Options: `S` (standard), `C` (classification), `all`. Include invalid or deprecated concepts. Include concept relationships in response. Include concept synonyms in response. Sort field. Options: `name`, `concept_id`, `concept_code`. Sort order. Options: `asc`, `desc`. Specific vocabulary release version to query
**Example:** `2025.1`
## Response Indicates if the request was successful. Response data container. Array of concept objects. Unique concept identifier. Standard name of the concept. Original code from the vocabulary. Vocabulary containing this concept. Domain of the concept. Concept class identifier. Standard concept designation ('S', 'C', or null). Reason for invalidation if applicable. Date when concept became valid. Date when concept becomes invalid. Response metadata. Unique identifier for this request. Vocabulary release version used. Response timestamp (ISO format). Current page number. Items per page. Total concepts available. Total number of pages. Whether next page exists. Whether previous page exists. ```bash cURL theme={null} curl -X GET "https://api.omophub.com/v1/vocabularies/SNOMED/concepts?page_size=10" \ -H "Authorization: Bearer YOUR_API_KEY" ``` ```python Python theme={null} import requests response = requests.get( "https://api.omophub.com/v1/vocabularies/SNOMED/concepts", params={"page_size": 10, "standard_concept": "S"}, headers={"Authorization": "Bearer YOUR_API_KEY"} ) data = response.json() for concept in data["data"]["concepts"]: print(f"{concept['concept_name']}: {concept['concept_id']}") ``` ```javascript JavaScript theme={null} const response = await fetch( 'https://api.omophub.com/v1/vocabularies/SNOMED/concepts?page_size=10&standard_concept=S', { headers: { 'Authorization': 'Bearer YOUR_API_KEY' } } ); const data = await response.json(); console.log(`Found ${data.meta.pagination.total_items} concepts`); ``` ```bash cURL (with search) theme={null} curl -X GET "https://api.omophub.com/v1/vocabularies/SNOMED/concepts?search=diabetes&standard_concept=S&page_size=20" \ -H "Authorization: Bearer YOUR_API_KEY" ``` ```json theme={null} { "success": true, "data": { "concepts": [ { "concept_id": 201826, "concept_name": "Type 2 diabetes mellitus", "concept_code": "44054006", "vocabulary_id": "SNOMED", "domain_id": "Condition", "concept_class_id": "Clinical Finding", "standard_concept": "S", "invalid_reason": null, "valid_start_date": "2002-01-31", "valid_end_date": "2099-12-31" }, { "concept_id": 201254, "concept_name": "Hypertensive disorder", "concept_code": "38341003", "vocabulary_id": "SNOMED", "domain_id": "Condition", "concept_class_id": "Clinical Finding", "standard_concept": "S", "invalid_reason": null, "valid_start_date": "2002-01-31", "valid_end_date": "2099-12-31" } ] }, "meta": { "request_id": "req_abc123", "vocab_release": "2025.1", "timestamp": "2025-01-05T10:00:00Z", "pagination": { "page": 1, "page_size": 10, "total_items": 125847, "total_pages": 12585, "has_next": true, "has_previous": false } } } ``` ## Usage Examples ### Basic List Get concepts from a vocabulary: ```bash theme={null} GET /v1/vocabularies/SNOMED/concepts?page_size=50 ``` ### Search Within Vocabulary Search for concepts by name: ```bash theme={null} GET /v1/vocabularies/SNOMED/concepts?search=diabetes&standard_concept=S ``` ### With Sorting Sort by concept name descending: ```bash theme={null} GET /v1/vocabularies/RxNorm/concepts?sort_by=name&sort_order=desc ``` ### Include Additional Data Include relationships and synonyms: ```bash theme={null} GET /v1/vocabularies/SNOMED/concepts?include_relationships=true&include_synonyms=true ``` ## Related Endpoints * [List Vocabularies](/api-reference/vocabularies/list-vocabularies) - Get all vocabularies * [Get Vocabulary](/api-reference/vocabularies/get-vocabulary) - Get vocabulary details * [Get Concept](/api-reference/concepts/get-concept) - Get individual concept details # Get Vocabulary Domain Statistics Source: https://docs.omophub.com/api-reference/vocabularies/get-vocabulary-domain-stats GET https://api.omophub.com/v1/vocabularies/{vocabularyId}/stats/domains/{domainId} Get statistics for a specific domain within an OMOP vocabulary - concept counts and standard coverage per Condition, Drug, Measurement, and more. ## Overview This endpoint provides statistics about concepts within a specific domain of a particular vocabulary, including concept counts and class distribution. ## Path Parameters The vocabulary identifier (e.g., "SNOMED", "ICD10CM", "LOINC") The domain identifier (e.g., "Condition", "Drug", "Procedure", "Measurement") ## Query Parameters Specific vocabulary release version (e.g., "2025.1") ## Response Indicates whether the request was successful Domain statistics data Vocabulary identifier Human-readable vocabulary name Domain identifier Human-readable domain name Total number of concepts in this vocabulary-domain combination Number of standard concepts (standard\_concept = 'S') Number of classification concepts (standard\_concept = 'C') Number of invalid/deprecated concepts Number of currently active concepts Top concept classes in this domain (up to 10) Concept class identifier Concept class name Number of concepts in this class ISO 8601 timestamp of when statistics were calculated Response metadata Unique identifier for request tracing ISO 8601 timestamp Vocabulary release version used ```bash cURL theme={null} curl -X GET "https://api.omophub.com/v1/vocabularies/SNOMED/stats/domains/Condition" \ -H "Authorization: Bearer YOUR_API_KEY" ``` ```python Python theme={null} import requests response = requests.get( "https://api.omophub.com/v1/vocabularies/SNOMED/stats/domains/Condition", headers={"Authorization": "Bearer YOUR_API_KEY"} ) data = response.json() print(f"Total concepts: {data['data']['total_concepts']}") print(f"Standard concepts: {data['data']['standard_concepts']}") ``` ```json theme={null} { "success": true, "data": { "vocabulary_id": "SNOMED", "vocabulary_name": "Systematized Nomenclature of Medicine Clinical Terms", "domain_id": "Condition", "domain_name": "Condition", "total_concepts": 125847, "standard_concepts": 118943, "classification_concepts": 4521, "invalid_concepts": 2391, "active_concepts": 123456, "concept_classes": [ { "concept_class_id": "Clinical Finding", "concept_class_name": "Clinical Finding", "concept_count": 87234 }, { "concept_class_id": "Disorder", "concept_class_name": "Disorder", "concept_count": 23456 }, { "concept_class_id": "Finding", "concept_class_name": "Finding", "concept_count": 15157 } ], "last_updated": "2024-12-22T10:32:00Z" }, "meta": { "request_id": "req_abc123", "timestamp": "2024-12-22T10:32:00Z", "vocab_release": "2025.1" } } ``` ## Notes * Returns statistics for a specific vocabulary-domain combination * The `concept_classes` array shows the top 10 concept classes by count * Use this endpoint to understand concept distribution within a domain # Get Vocabulary Domains Source: https://docs.omophub.com/api-reference/vocabularies/get-vocabulary-domains GET /v1/vocabularies/domains Retrieve all OHDSI domains available within a specific OMOP vocabulary - filter by the domains SNOMED, ICD-10, or RxNorm actually cover. Retrieve the list of standard OHDSI domains. Domains categorize concepts by their clinical or administrative function (e.g., Condition, Drug, Procedure). ## Query Parameters Specific vocabulary release version to use. If not provided, uses the latest available release.
**Format**: `YYYY.Q` (e.g., `2025.1`)
## Response Indicates if the request was successful. Array of domain objects. Unique domain identifier (e.g., "Condition", "Drug", "Procedure"). Human-readable domain name. Brief description of the domain. Unique identifier for the request. Request timestamp. Vocabulary release version used. ```bash cURL theme={null} curl -X GET "https://api.omophub.com/v1/vocabularies/domains" \ -H "Authorization: Bearer YOUR_API_KEY" ``` ```python Python theme={null} import requests response = requests.get( "https://api.omophub.com/v1/vocabularies/domains", headers={"Authorization": "Bearer YOUR_API_KEY"} ) data = response.json() for domain in data["data"]["domains"]: print(f"{domain['domain_id']}: {domain['domain_name']}") ``` ```javascript JavaScript theme={null} const response = await fetch('https://api.omophub.com/v1/vocabularies/domains', { headers: { 'Authorization': 'Bearer YOUR_API_KEY' } }); const data = await response.json(); console.log(data.data.domains); ``` ```json Response theme={null} { "success": true, "data": { "domains": [ { "domain_id": "Condition", "domain_name": "Condition", "description": "OHDSI standard domain: Condition" }, { "domain_id": "Drug", "domain_name": "Drug", "description": "OHDSI standard domain: Drug" }, { "domain_id": "Procedure", "domain_name": "Procedure", "description": "OHDSI standard domain: Procedure" }, { "domain_id": "Measurement", "domain_name": "Measurement", "description": "OHDSI standard domain: Measurement" }, { "domain_id": "Observation", "domain_name": "Observation", "description": "OHDSI standard domain: Observation" }, { "domain_id": "Device", "domain_name": "Device", "description": "OHDSI standard domain: Device" }, { "domain_id": "Specimen", "domain_name": "Specimen", "description": "OHDSI standard domain: Specimen" }, { "domain_id": "Visit", "domain_name": "Visit", "description": "OHDSI standard domain: Visit" } ] }, "meta": { "request_id": "req_abc123def456", "timestamp": "2025-01-05T10:30:00Z", "vocab_release": "2025.1" } } ``` ## Available Domains The endpoint returns all standard OHDSI domains: ### Clinical Domains * **Condition** - Diseases, disorders, and clinical findings * **Procedure** - Medical procedures and interventions * **Observation** - Clinical observations and assessments * **Measurement** - Quantitative test results and vital signs * **Drug** - Medications and pharmaceutical products * **Device** - Medical devices and equipment * **Specimen** - Biological specimens and samples ### Administrative Domains * **Visit** - Healthcare encounters and visit types * **Provider** - Healthcare provider roles and specialties * **Location** - Geographic locations * **Care Site** - Healthcare facilities * **Payer** - Insurance and payment-related concepts * **Plan** - Insurance plans * **Sponsor** - Study sponsors * **Cost** - Healthcare cost information * **Revenue** - Revenue codes ### Other Domains * **Episode** - Clinical episodes * **Regimen** - Treatment regimens * **Metadata** - Vocabulary metadata * **Type Concept** - Type concepts * **Relationship** - Relationship types * **Gender** - Gender concepts * **Race** - Race concepts * **Ethnicity** - Ethnicity concepts ## Related Endpoints * [Get Domains](/api-reference/domains/get-domains) - Detailed domain information with statistics * [Get Domain Concepts](/api-reference/domains/get-domain-concepts) - Concepts within a specific domain * [List Vocabularies](/api-reference/vocabularies/list-vocabularies) - Available vocabularies # Get Vocabulary Releases Source: https://docs.omophub.com/api-reference/vocabularies/get-vocabulary-releases GET /v1/vocabularies/releases Retrieve the list of all available OHDSI OMOP vocabulary dataset releases, including release dates, version identifiers, and validity metadata. Retrieve information about all OHDSI vocabulary dataset releases available in the system. Each release represents a complete vocabulary dataset version (e.g., "2025.1") containing all OMOP vocabularies at a specific point in time. ## Query Parameters Include inactive releases in the response. By default, only active releases are returned. ## Response Indicates if the request was successful. Array of vocabulary release objects. Unique identifier for the release. Release identifier (e.g., "2025.1", "2024.2"). Version number. ATHENA vocabulary version this release is based on. ISO 8601 formatted release date. Release status (e.g., "ready", "pending", "deprecated"). Whether this is the default release used when no version is specified. Whether this release is currently active and available for use. Total number of concepts across all vocabularies in this release. Number of vocabularies included in this release. Total number of concept relationships in this release. Unique identifier for the request. Request timestamp. Current default vocabulary release. Pagination information. Current page number. Number of items returned. Total number of releases. Total number of pages. Whether there are more pages. Whether there are previous pages. ```bash cURL theme={null} curl -X GET "https://api.omophub.com/v1/vocabularies/releases" \ -H "Authorization: Bearer YOUR_API_KEY" ``` ```bash cURL (with options) theme={null} curl -X GET "https://api.omophub.com/v1/vocabularies/releases?include_inactive=true" \ -H "Authorization: Bearer YOUR_API_KEY" ``` ```python Python theme={null} import requests response = requests.get( "https://api.omophub.com/v1/vocabularies/releases", headers={"Authorization": "Bearer YOUR_API_KEY"} ) data = response.json() for release in data["data"]: print(f"{release['vocab_release']}: {release['total_concepts']} concepts") ``` ```javascript JavaScript theme={null} const response = await fetch('https://api.omophub.com/v1/vocabularies/releases', { headers: { 'Authorization': 'Bearer YOUR_API_KEY' } }); const data = await response.json(); console.log(data.data); ``` ```json Response theme={null} { "success": true, "data": [ { "id": 1, "vocab_release": "2025.1", "version_number": "1", "athena_version": "v5.3.1", "release_date": "2025-01-01T00:00:00Z", "status": "ready", "is_default": true, "is_active": true, "total_concepts": 8547321, "total_vocabularies": 87, "total_relationships": 45678234 }, { "id": 2, "vocab_release": "2024.2", "version_number": "4", "athena_version": "v5.3.0", "release_date": "2024-10-01T00:00:00Z", "status": "ready", "is_default": false, "is_active": true, "total_concepts": 8234567, "total_vocabularies": 85, "total_relationships": 44567123 } ], "meta": { "request_id": "req_abc123def456", "timestamp": "2025-01-05T10:30:00Z", "vocab_release": "2025.1", "pagination": { "page": 1, "page_size": 2, "total_items": 2, "total_pages": 1, "has_next": false, "has_previous": false } } } ``` ## Usage Examples ### Get Active Releases ```bash theme={null} curl -X GET "https://api.omophub.com/v1/vocabularies/releases" \ -H "Authorization: Bearer YOUR_API_KEY" ``` ### Include Inactive Releases ```bash theme={null} curl -X GET "https://api.omophub.com/v1/vocabularies/releases?include_inactive=true" \ -H "Authorization: Bearer YOUR_API_KEY" ``` ## Notes * Each release represents a complete OHDSI vocabulary dataset at a point in time * The `is_default` release is used when no specific version is requested * Use the `vocab_release` value (e.g., "2025.1") in the `vocab_release` query parameter of other endpoints to query specific versions ## Related Endpoints * [List Vocabularies](/api-reference/vocabularies/list-vocabularies) - Get all vocabularies in a release # Get Vocabulary Statistics Source: https://docs.omophub.com/api-reference/vocabularies/get-vocabulary-stats GET /v1/vocabularies/{vocabularyId}/stats Retrieve comprehensive statistics for a specific OMOP vocabulary including concept counts, standard concept coverage, and domain distribution breakdown. Retrieve detailed statistical information about a vocabulary, including the total number of concepts, breakdown by standard/classification status, and domain distribution. ## Path Parameters The unique identifier for the vocabulary (e.g., "SNOMED", "ICD10CM", "LOINC"). ## Query Parameters Specific vocabulary release version to query
**Example:** `2025.1`
## Response Indicates if the request was successful. The vocabulary identifier. Full vocabulary name. Total number of concepts in the vocabulary. Number of standard concepts (standard\_concept = 'S'). Number of classification concepts (standard\_concept = 'C'). Number of invalid/deprecated concepts. Number of currently active concepts. Earliest valid start date among concepts. Latest valid end date among concepts. Timestamp of when statistics were last updated. Array of domain statistics. Domain identifier. Domain name. Number of concepts in this domain. Unique identifier for the request. Request timestamp. Vocabulary release version used. ```bash cURL theme={null} curl -X GET "https://api.omophub.com/v1/vocabularies/SNOMED/stats" \ -H "Authorization: Bearer YOUR_API_KEY" ``` ```python Python theme={null} import requests response = requests.get( "https://api.omophub.com/v1/vocabularies/SNOMED/stats", headers={"Authorization": "Bearer YOUR_API_KEY"} ) data = response.json() print(f"Total concepts: {data['data']['total_concepts']}") ``` ```javascript JavaScript theme={null} const response = await fetch('https://api.omophub.com/v1/vocabularies/SNOMED/stats', { headers: { 'Authorization': 'Bearer YOUR_API_KEY' } }); const data = await response.json(); console.log(`Total concepts: ${data.data.total_concepts}`); ``` ```json Response theme={null} { "success": true, "data": { "vocabulary_id": "SNOMED", "vocabulary_name": "Systematized Nomenclature of Medicine Clinical Terms", "total_concepts": 354652, "standard_concepts": 298456, "classification_concepts": 56196, "invalid_concepts": 12543, "active_concepts": 342109, "valid_start_date": "2002-01-31", "valid_end_date": "2099-12-31", "last_updated": "2025-01-05T10:30:00Z", "domain_distribution": [ { "domain_id": "Condition", "domain_name": "Condition", "concept_count": 112543 }, { "domain_id": "Procedure", "domain_name": "Procedure", "concept_count": 87321 }, { "domain_id": "Observation", "domain_name": "Observation", "concept_count": 65432 } ] }, "meta": { "request_id": "req_abc123def456", "timestamp": "2025-01-05T10:30:00Z", "vocab_release": "2025.1" } } ``` ```json Error Response theme={null} { "success": false, "error": { "code": "VOCABULARY_NOT_FOUND", "message": "Vocabulary 'INVALID_VOCAB' not found" }, "meta": { "request_id": "req_error123", "timestamp": "2025-01-05T10:30:00Z" } } ``` ## Usage Examples ### Get Statistics for SNOMED ```bash theme={null} GET /v1/vocabularies/SNOMED/stats ``` ### Get Statistics for ICD-10-CM ```bash theme={null} GET /v1/vocabularies/ICD10CM/stats ``` ### Get Statistics for a Specific Release ```bash theme={null} GET /v1/vocabularies/SNOMED/stats?vocab_release=2025.1 ``` ## Related Endpoints * [List Vocabularies](/api-reference/vocabularies/list-vocabularies) - Get all available vocabularies * [Get Vocabulary Details](/api-reference/vocabularies/get-vocabulary) - Get detailed vocabulary information * [Get Vocabulary Concepts](/api-reference/vocabularies/get-vocabulary-concepts) - Get concepts within a vocabulary # List Vocabularies Source: https://docs.omophub.com/api-reference/vocabularies/list-vocabularies GET /v1/vocabularies Get a paginated list of all available OMOP medical vocabularies - SNOMED CT, ICD-10, LOINC, RxNorm, ATC, HCPCS, and 100+ OHDSI-supported terminologies. This endpoint returns a paginated list of all available medical vocabularies in the system, including SNOMED CT, ICD-10-CM, LOINC, RxNorm, and others. ## Query Parameters Page number for pagination (1-based). Number of vocabularies to return per page (max 1000). Include concept count statistics for each vocabulary. Include inactive or deprecated vocabularies in the results. Sort field. Options: `name`, `priority`, `updated` Sort order. Options: `asc`, `desc` Specific vocabulary release version (e.g., "2025.1"). Defaults to the latest available release. ## Response Indicates if the request was successful. Response data container. Array of vocabulary objects. Unique identifier for the vocabulary (e.g., "SNOMED", "ICD10CM"). Full name of the vocabulary. Reference or source organization for the vocabulary. Version identifier of the vocabulary. OMOP concept ID representing this vocabulary. Response metadata and pagination information. Unique identifier for this request. Vocabulary release version used. Response timestamp (ISO format). Current page number. Items per page. Total vocabularies available. Total number of pages. Whether next page exists. Whether previous page exists. ```bash cURL theme={null} curl -X GET "https://api.omophub.com/v1/vocabularies" \ -H "Authorization: Bearer YOUR_API_KEY" ``` ```python Python theme={null} import requests response = requests.get( "https://api.omophub.com/v1/vocabularies", headers={"Authorization": "Bearer YOUR_API_KEY"} ) data = response.json() for vocab in data["data"]["vocabularies"]: print(f"{vocab['vocabulary_id']}: {vocab['vocabulary_name']}") ``` ```javascript JavaScript theme={null} const response = await fetch('https://api.omophub.com/v1/vocabularies', { headers: { 'Authorization': 'Bearer YOUR_API_KEY' } }); const data = await response.json(); data.data.vocabularies.forEach(vocab => { console.log(`${vocab.vocabulary_id}: ${vocab.vocabulary_name}`); }); ``` ```bash cURL (with options) theme={null} curl -X GET "https://api.omophub.com/v1/vocabularies?include_stats=true&sort_by=name&sort_order=asc&page_size=50" \ -H "Authorization: Bearer YOUR_API_KEY" ``` ```python Python (with options) theme={null} import requests response = requests.get( "https://api.omophub.com/v1/vocabularies", params={ "include_stats": True, "sort_by": "name", "sort_order": "asc", "page_size": 50 }, headers={"Authorization": "Bearer YOUR_API_KEY"} ) data = response.json() ``` ```json theme={null} { "success": true, "data": { "vocabularies": [ { "vocabulary_id": "SNOMED", "vocabulary_name": "Systematized Nomenclature of Medicine Clinical Terms", "vocabulary_reference": "SNOMED CT", "vocabulary_version": "US Edition 20240301", "vocabulary_concept_id": 45756746 }, { "vocabulary_id": "ICD10CM", "vocabulary_name": "International Classification of Diseases, Tenth Revision, Clinical Modification", "vocabulary_reference": "CMS", "vocabulary_version": "2024", "vocabulary_concept_id": 45756681 }, { "vocabulary_id": "LOINC", "vocabulary_name": "Logical Observation Identifiers Names and Codes", "vocabulary_reference": "Regenstrief Institute", "vocabulary_version": "2.76", "vocabulary_concept_id": 45756747 }, { "vocabulary_id": "RxNorm", "vocabulary_name": "RxNorm", "vocabulary_reference": "National Library of Medicine", "vocabulary_version": "2024-03-04", "vocabulary_concept_id": 45756748 } ] }, "meta": { "request_id": "req_abc123", "vocab_release": "2025.1", "timestamp": "2025-01-05T10:00:00Z", "pagination": { "page": 1, "page_size": 20, "total_items": 25, "total_pages": 2, "has_next": true, "has_previous": false } } } ``` ## Usage Examples ### Basic List Get all vocabularies with default pagination: ```bash theme={null} GET /v1/vocabularies ``` ### With Statistics Include concept counts for each vocabulary: ```bash theme={null} GET /v1/vocabularies?include_stats=true ``` ### Sorted by Name Get vocabularies sorted alphabetically: ```bash theme={null} GET /v1/vocabularies?sort_by=name&sort_order=asc ``` ### Include Inactive Include deprecated vocabularies: ```bash theme={null} GET /v1/vocabularies?include_inactive=true ``` ## Related Endpoints * [Get Vocabulary](/api-reference/vocabularies/get-vocabulary) - Get details for a specific vocabulary * [Search Vocabularies](/api-reference/vocabularies/search-vocabularies) - Search vocabularies by name * [Get Vocabulary Concepts](/api-reference/vocabularies/get-vocabulary-concepts) - Get concepts within a vocabulary # Search Vocabularies Source: https://docs.omophub.com/api-reference/vocabularies/search-vocabularies GET /v1/vocabularies/search Search across OMOP vocabulary names and descriptions to find the right terminology - SNOMED, LOINC, ICD-10, and the full OHDSI-supported catalog. Search for vocabularies by name or description. Returns matching vocabularies with relevance scoring. ## Query Parameters Search query string. Searches across vocabulary names and descriptions. Page number for pagination (1-based). Number of results per page (max 100). Include inactive vocabularies in search results. Specific vocabulary release version to query
**Example:** `2025.1`
## Response Indicates if the request was successful. Array of matching vocabulary objects. Unique identifier for the vocabulary. Full name of the vocabulary. Reference or source for the vocabulary. Version of the vocabulary. OMOP concept ID for the vocabulary. Response metadata. Unique identifier for this request. Vocabulary release version used. Response timestamp (ISO format). Search query used. Total matching vocabularies. Filters that were applied. Current page number. Items per page. Total items available. Total number of pages. Whether next page exists. Whether previous page exists. ```bash cURL theme={null} curl -X GET "https://api.omophub.com/v1/vocabularies/search?query=SNOMED" \ -H "Authorization: Bearer YOUR_API_KEY" ``` ```python Python theme={null} import requests response = requests.get( "https://api.omophub.com/v1/vocabularies/search", params={"query": "SNOMED"}, headers={"Authorization": "Bearer YOUR_API_KEY"} ) data = response.json() for vocab in data["data"]: print(f"{vocab['vocabulary_id']}: {vocab['vocabulary_name']}") ``` ```javascript JavaScript theme={null} const response = await fetch( 'https://api.omophub.com/v1/vocabularies/search?query=SNOMED', { headers: { 'Authorization': 'Bearer YOUR_API_KEY' } } ); const data = await response.json(); data.data.forEach(vocab => { console.log(`${vocab.vocabulary_id}: ${vocab.vocabulary_name}`); }); ``` ```bash cURL (with options) theme={null} curl -X GET "https://api.omophub.com/v1/vocabularies/search?query=drug&include_inactive=true&page_size=50" \ -H "Authorization: Bearer YOUR_API_KEY" ``` ```json theme={null} { "success": true, "data": [ { "vocabulary_id": "SNOMED", "vocabulary_name": "Systematized Nomenclature of Medicine Clinical Terms", "vocabulary_reference": "SNOMED International", "vocabulary_version": "US Edition 20240301", "vocabulary_concept_id": 45756746 }, { "vocabulary_id": "Nebraska Lexicon", "vocabulary_name": "Nebraska Lexicon", "vocabulary_reference": "University of Nebraska", "vocabulary_version": "2024-01", "vocabulary_concept_id": 32675 } ], "meta": { "request_id": "req_abc123", "vocab_release": "2025.1", "timestamp": "2025-01-05T10:00:00Z", "search": { "query": "SNOMED", "total_results": 2, "filters_applied": { "search_type": "full_text_search", "include_inactive": false } }, "pagination": { "page": 1, "page_size": 20, "total_items": 2, "total_pages": 1, "has_next": false, "has_previous": false } } } ``` ## Usage Examples ### Basic Search Search for vocabularies by name: ```bash theme={null} GET /v1/vocabularies/search?query=SNOMED ``` ### With Pagination Search with custom page size: ```bash theme={null} GET /v1/vocabularies/search?query=ICD&page=1&page_size=50 ``` ### Include Inactive Include deprecated vocabularies: ```bash theme={null} GET /v1/vocabularies/search?query=classification&include_inactive=true ``` ## Related Endpoints * [List Vocabularies](/api-reference/vocabularies/list-vocabularies) - List all vocabularies * [Get Vocabulary](/api-reference/vocabularies/get-vocabulary) - Get specific vocabulary details # Authentication and API key overview for OMOPHub Source: https://docs.omophub.com/authentication/overview Understand OMOPHub authentication options including API keys and bearer tokens, scopes, header usage, and security best practices for API access. ## API Key Authentication API keys are used for all programmatic access to the OMOPHub API. They provide: * **Long-lived access**: No expiration for continuous integration * **Granular permissions**: Control access to specific resources * **Usage tracking**: Monitor API consumption per key * **Multiple keys**: Create keys for different environments ### API Key Types Individual developer keys * Tied to your user account * Inherit your account permissions * Usage counts toward personal quota * Can be revoked anytime Shared keys for team collaboration (Coming Soon) * Associated with team/organization * Shared usage limits * Role-based access control * Audit logging per team member ## Authentication Flow ### API Authentication Flow ```mermaid theme={null} sequenceDiagram participant Client participant API participant Auth Service Client->>API: Request with API key API->>Auth Service: Validate API key Auth Service->>API: Return permissions API->>Client: Process request ``` ### API Key Security 1. **Environment Variables**: Store keys in environment variables ```bash theme={null} export OMOPHUB_API_KEY="oh_xxxxxxxxx" ``` 2. **Key Rotation**: Regularly rotate keys (every 90 days recommended) 3. **Separate Environments**: Use different keys for dev/staging/production 4. **Monitor Usage**: Check API key usage regularly for anomalies ## Authentication Headers ### API Key Authentication Include your API key in the Authorization header: ```bash cURL theme={null} curl -H "Authorization: Bearer oh_xxxxxxxxx" \ https://api.omophub.com/v1/vocabularies ``` ```python Python theme={null} client = OMOPHubClient(api_key="oh_xxxxxxxxx") ``` ```r R theme={null} client <- omophub_client(api_key = "oh_xxxxxxxxx") ``` ## Troubleshooting * Check the key hasn't been revoked * Verify you're using the correct environment * Confirm proper Authorization header format * Check X-RateLimit headers in responses * Implement exponential backoff * Consider upgrading your plan * Use caching to reduce requests ## Next Steps Create keys for API access # EHR Integration Source: https://docs.omophub.com/guides/integration/ehr-integration Resolve EHR-native codes to standardized OMOP concepts for clinical decision support, lab normalization, and bidirectional EHR vocabulary integration. ## 1. The "Tab Fatigue" Problem A doctor needs to check if a new prescription will interact with the patient's current medications. The drug interaction database wants RxNorm codes. The EHR has the medications stored with local formulary codes. The patient's research record uses OMOP concept IDs. Three systems, three vocabularies, zero interoperability. The doctor opens another tab. This is "Tab Fatigue" or, as clinicians call it, "Death by a Thousand Clicks." The insights exist: drug interaction alerts, care gap notifications, clinical trial eligibility signals. But they're trapped in separate systems that don't speak the same vocabulary language. Getting data *out* of an EHR for research is hard. Getting standardized insights *back into* the EHR at the point of care is the real "Last Mile" problem. The vocabulary translation layer is where **OMOPHub** fits. When an EHR-integrated application (like a SMART on FHIR app) needs to convert local or FHIR-native codes into standardized OMOP concepts, OMOPHub provides the lookup: search for a code, get back the OMOP concept ID, access its hierarchy and relationships. It's one component in a larger integration pipeline, not the middleware itself, but the vocabulary resolution step that makes the rest possible. The full FHIR-to-OMOP pipeline looks like this: 1. **FHIR client** reads patient data from the EHR (medications, conditions, labs) 2. **Your application code** parses the FHIR resources and extracts coded elements 3. **OMOPHub** resolves those codes to standardized OMOP concept IDs 4. **Your CDS / analytics engine** runs logic against the standardized concepts OMOPHub handles step 3. That's a narrow but critical role: without it, every integration project maintains its own vocabulary mapping tables, which drift out of date and diverge across installations. **Steps 2 and 3 collapse into one call.** OMOPHub's [FHIR Resolver](/guides/integration/fhir-integration) (`POST /v1/fhir/resolve`) takes a FHIR `Coding` or `CodeableConcept` and returns a standard OMOP concept plus the CDM target table in a single request - no hand-rolled parser, no URI mapping table, no `Maps to` traversal. The examples below use the Resolver as the default path. You can still drop to the raw `/v1/concepts/*` primitives when you need custom hierarchy walks or relationship queries the Resolver doesn't cover. ## 2. The Core Concept: Code Resolution at the Point of Care EHR data arrives in many vocabulary flavors. A medication might be coded in RxNorm, NDC, a local formulary code, or just free text. A diagnosis might be ICD-10-CM in billing, SNOMED in the problem list, or a local shorthand in a quick note. A lab result could use LOINC, a vendor-specific code, or an institution-specific abbreviation. For any clinical decision support to work across EHR installations, these codes need to resolve to a common vocabulary. OMOP provides that common layer - and OMOPHub provides programmatic access to it. The resolution patterns: * **Known standard code** (e.g., RxNorm "1049502") → `POST /v1/fhir/resolve` with `system` + `code` → standard OMOP concept, CDM target table, mapping type * **Local display name** (e.g., "Creatinine, Serum") → `POST /v1/fhir/resolve` with `display` only → Resolver falls back to semantic search scoped by `resource_type` * **Free text** (e.g., "Chest pain") → same as above, no `code` / `system` * **Proprietary code** (e.g., "MED\_LOCAL\_4827") → OMOPHub can't help directly; requires a pre-built local mapping table The first three are the Resolver's three native modes. The fourth remains the irreducible problem: local codes that aren't coded against any published vocabulary need a mapping table maintained by the site's data team. Once you have OMOP concept IDs, you can reuse the same concept sets, phenotypes, and clinical logic across every EHR installation: no per-site mapping tables needed. ## 3. Use Case A: Real-Time Clinical Decision Support A SMART on FHIR app needs to check a newly prescribed medication against the patient's current conditions and medications: all standardized to OMOP concepts. **The Scenario:** A doctor prescribes Amoxicillin. The app extracts the medication from the FHIR `MedicationRequest` resource, resolves it to an OMOP concept via OMOPHub, and feeds it to a CDS engine that checks for interactions against the patient's OMOP-standardized history. ```python Python theme={null} import requests, omophub # Sample FHIR MedicationRequest (simplified) # In production, this comes from the EHR via FHIR API fhir_medication_request = { "resourceType": "MedicationRequest", "id": "medrx001", "medicationCodeableConcept": { "coding": [ { "system": "http://www.nlm.nih.gov/research/umls/rxnorm", "code": "1049502", "display": "Amoxicillin 500 MG Oral Capsule", } ], "text": "Amoxicillin 500mg capsule", }, "subject": {"reference": "Patient/example"}, } # --- Resolve the whole CodeableConcept in one call --- # The CodeableConcept endpoint accepts the full `coding` array and applies # OHDSI vocabulary preference when multiple codings are present. If no # standard code is found it falls back to semantic search on `text`. resp = requests.post( "https://api.omophub.com/v1/fhir/resolve/codeable-concept", headers={"Authorization": "Bearer oh_your_api_key"}, json={ "coding": fhir_medication_request["medicationCodeableConcept"]["coding"], "text": fhir_medication_request["medicationCodeableConcept"].get("text"), "resource_type": "MedicationRequest", }, ) best = resp.json()["data"]["best_match"]["resolution"] omop_drug_id = best["standard_concept"]["concept_id"] omop_drug_name = best["standard_concept"]["concept_name"] target_table = best["target_table"] # "drug_exposure" mapping_type = best["mapping_type"] # "direct" - RxNorm is already standard print(f"Resolved: {omop_drug_name} (OMOP {omop_drug_id})") print(f"CDM destination: {target_table}") print(f"Ready for CDS: pass {omop_drug_id} to your interaction checker") # --- Optional: ingredient lookup for class-level DDI rules --- # "Has ingredient" is a non-hierarchical relationship, not an ancestor, # so this leg stays on the raw primitives rather than the Resolver. client = omophub.OMOPHub() rels = client.concepts.relationships(omop_drug_id) ingredients = [ r for r in rels.get("relationships", []) if "ingredient" in r.get("relationship_id", "").lower() or r.get("concept_class_id") == "Ingredient" ] if ingredients: ing = ingredients[0] print(f"Ingredient: {ing['concept_name']} ({ing['concept_id']})") ``` **The Key Insight:** The Resolver collapses the three-step "parse FHIR URI → find concept → traverse Maps to" chain into a single call and tells you (via `target_table`) which OMOP CDM table to write to. The ingredient-level step stays separate because `Has ingredient` is a non-hierarchical relationship - the right tool is `concepts.relationships()`, not a hierarchy walk. The Resolver for the ETL leg, the raw primitives for the domain-specific enrichment: two tools for two different shapes of problem. **See also:** [FHIR Integration guide](/guides/integration/fhir-integration) for the full Resolver response shape (source vs standard concept, `mapping_type`, `alternative_standard_concepts`, `mapping_quality`). ## 4. Use Case B: Normalizing Local Lab Codes for a Sidecar App EHR-embedded "sidecar" apps (SMART on FHIR apps for specialty workflows) often need to display standardized trends from lab data that arrives in 50 different local code variants. **The Scenario:** A CKD management app needs to show creatinine trends. The EHR uses "Cr\_Serum," "Creat\_Blood," and "Kidney\_Fx\_Creat" - all meaning the same thing. The app needs a single LOINC concept to unify them. ```python Python theme={null} import requests # Lab results from EHR (via FHIR Observation resources, parsed by your code) local_labs = [ {"local_code": "Cr_Serum", "display": "Creatinine, Serum", "value": 1.2, "unit": "mg/dL"}, {"local_code": "Creat_Blood", "display": "Blood Creatinine", "value": 106, "unit": "umol/L"}, {"local_code": "Kidney_Fx_Creat", "display": "Creatinine Level", "value": 0.9, "unit": "mg/dL"}, ] # Batch-resolve all three local variants in a single call. # The Resolver's semantic-fallback path uses the `display` text scoped # to the `resource_type` domain - no manual basic-then-semantic chain. resp = requests.post( "https://api.omophub.com/v1/fhir/resolve/batch", headers={"Authorization": "Bearer oh_your_api_key"}, json={ "codings": [ { "display": lab["display"], "resource_type": "Observation", # no system/code - tells the Resolver to go straight to # semantic search over the display text } for lab in local_labs ] }, ) items = resp.json()["data"]["items"] standardized = [] for lab, item in zip(local_labs, items): res = item.get("resolution") or {} std = res.get("standard_concept") if not std: print(f" {lab['local_code']}: no match (may need local mapping)") continue standardized.append({ "original_code": lab["local_code"], "value": lab["value"], "unit": lab["unit"], "loinc_concept_id": std["concept_id"], "loinc_name": std["concept_name"], "mapping_type": res["mapping_type"], # "semantic_match" "similarity_score": res.get("similarity_score"), }) # All three local codes should collapse to the same LOINC creatinine concept unique_loinc = {s["loinc_concept_id"] for s in standardized} print(f"Standardized {len(standardized)}/{len(local_labs)} local codes") print(f"Unique LOINC concepts: {len(unique_loinc)}") for s in standardized: print(f" {s['loinc_name']}: {s['value']} {s['unit']} (from '{s['original_code']}')") ``` **The Key Insight:** `POST /v1/fhir/resolve/batch` is the natural fit for "N local variants, need the canonical LOINC". Each `display`-only input tells the Resolver "I don't have a coded system - use semantic search scoped to the Observation domain." The response's `similarity_score` and `mapping_type: "semantic_match"` let you decide which matches to trust (high-similarity: auto-accept; low: surface to the site's data team for review). Batch counts as N API calls, same as N single resolves, so there's no pricing penalty for bundling. **Caveat:** This only works when the local display name is descriptive enough for semantic search to match. Highly abbreviated codes (like "Cr\_S" or "KFC\_001") won't match anything useful. Those need a pre-built local mapping maintained by the site's data team - the `target_table` and `mapping_quality: "manual_review"` fields in the Resolver response help you route them to the right workflow. ## 5. The "Feedback Loop": Writing Back to the EHR True integration is bidirectional. When your OMOPHub-powered app identifies something - a more precise diagnosis code, a care gap, a trial match - that information should flow back to the EHR. **Example:** A clinician types "chest pain" in a free-text field. Your app searches OMOPHub and suggests "Angina pectoris" (a specific SNOMED condition concept) with its concept ID and code. The clinician confirms, and the app writes the structured SNOMED code back to the EHR via a FHIR Condition resource. The mechanics of the write-back are handled by the EHR's FHIR write API - OMOPHub doesn't write to EHRs. But OMOPHub provides the vocabulary backbone: the standardized concept ID, name, and code that get written back. This closes the loop: messy data comes out of the EHR → OMOPHub standardizes it → your app adds clinical intelligence → clean, structured data goes back in. Over time, this feedback loop improves the EHR data itself - more precise codes, fewer local abbreviations, better downstream analytics. The vocabulary standardization that started as a research need becomes a data quality improvement engine. ## 6. Conclusion: Making Data Invisible (and Useful) The best clinical technology is invisible. The clinician doesn't see "FHIR-to-OMOP transformation" or "vocabulary resolution" - they see a drug interaction alert that fires at the right moment, a lab trend that makes sense across visits, a trial match that arrives without a separate login. OMOPHub's role in this is specific: it resolves codes from EHR-native vocabularies to standardized OMOP concepts via API. It doesn't parse FHIR resources, run CDS logic, or write back to EHRs. But it provides the vocabulary resolution layer that every other component depends on. And it does so without requiring per-site mapping tables or local vocabulary databases. If you're building SMART on FHIR apps or EHR-integrated clinical tools, start with the vocabulary problem. Take a FHIR MedicationRequest from your test environment, extract the coded medication, and resolve it through OMOPHub. That single API call - billing code in, standard concept out - is the foundation everything else builds on. The data complexity becomes invisible. The clinical insight becomes useful. ## 7. Next Steps Full reference for the FHIR Resolver (single, batch, CodeableConcept) and the OMOPHub FHIR Terminology Service - `$lookup`, `$validate-code`, `$translate`, `$expand`, `$subsumes`, batch Bundles. Terminology-validated openEHR capture: wire EHRbase's composition validator at OMOPHub via the FHIR Terminology Service so coded fields in openEHR templates are checked at commit time. Point a HAPI FHIR server at OMOPHub as a remote terminology backend. Covers both the HAPI JPA Starter (reverse-proxy auth pattern) and custom Spring Boot HAPI builds. # EHRbase and openEHR integration with OMOPHub Source: https://docs.omophub.com/guides/integration/ehrbase-openehr Configure EHRbase to use OMOPHub as a remote FHIR R4 terminology server for validating coded openEHR template elements and ValueSet expansion. ## 1. What This Guide Covers EHRbase is the leading open-source openEHR Clinical Data Repository. When openEHR templates constrain coded elements to external terminologies (SNOMED CT, LOINC, ICD-10, RxNorm, and the rest of the OMOP vocabulary set), EHRbase can delegate validation to a remote FHIR terminology server. OMOPHub's FHIR Terminology Service is one such server. This guide shows the exact configuration that makes that work and walks through one round-trip - template upload, composition commit, code validation - against a real EHRbase instance pointed at OMOPHub. It is **not** a general openEHR interoperability guide: the promise is narrow and concrete - *template terminology validation* and *ValueSet expansion*, nothing else. **Tested, not "drop-in."** The snippets below are verified against `ehrbase/ehrbase:next` and OMOPHub's production FHIR Terminology Service. Your EHRbase deployment may have additional security, network, or validation configuration that changes the details - treat this as a reference pattern to adapt, not a turnkey install script. ## 2. Prerequisites * An EHRbase instance - `ehrbase/ehrbase:next` or v2.x. The integration uses Spring Security OAuth2 and Spring's `RemoteTerminologyServiceValidationSupport`, which have been stable since EHRbase 2.0. * An OMOPHub API key from your [dashboard](https://dashboard.omophub.com/api-keys). Free tier works for testing. * Network access from EHRbase to `fhir.omophub.com:443` (production) or to your own local OMOPHub dev instance. * Familiarity with openEHR templates (OPT, ADL 1.4) and basic YAML configuration. EHRbase only speaks **FHIR R4** for external terminology validation - point it at `https://fhir.omophub.com/fhir/r4/`. OMOPHub also serves R5 and R6, but EHRbase will not use them. ## 3. Configuration EHRbase consumes two config surfaces: one Spring Security OAuth2 client registration (for outbound authentication) and one external terminology provider block (for the validator). **Use an `application.yml` overlay, not environment variables.** EHRbase's external terminology config is a `Map` - Spring's relaxed-binding for maps via env vars is fragile and silently drops entries. Mount a YAML file via `SPRING_CONFIG_ADDITIONAL_LOCATION` to avoid the problem entirely. ### 3.1 `ehrbase.yml` overlay Mount this file into your EHRbase container at `/config/ehrbase.yml`: ```yaml ehrbase.yml theme={null} spring: security: oauth2: client: registration: omophub: client-id: ${OMOPHUB_CLIENT_ID} client-secret: ${OMOPHUB_CLIENT_SECRET} authorization-grant-type: client_credentials provider: omophub provider: omophub: token-uri: https://fhir.omophub.com/oauth2/token validation: external-terminology: enabled: true fail-on-error: true provider: omophub: type: FHIR url: https://fhir.omophub.com/fhir/r4/ oauth2-client: omophub ``` Key points: * `client-id` is your OMOPHub API key (starts with `oh_`). `client-secret` is required by Spring's config shape but not validated - use any non-empty value. * OMOPHub's token endpoint accepts both RFC 6749 client authentication methods (`client_secret_basic` / `client_secret_post`), so you do **not** need to set `client-authentication-method` explicitly - Spring's default works. * `fail-on-error: true` makes EHRbase reject composition commits when terminology validation fails. Flip to `false` in development while you're iterating on templates. * The trailing slash on `url:` matters - EHRbase's validator concatenates path segments without normalizing. ### 3.2 Docker Compose A minimal stack for local testing: ```yaml docker-compose.yml theme={null} services: ehrdb: image: ehrbase/ehrbase-v2-postgres:16.2 environment: POSTGRES_USER: postgres POSTGRES_PASSWORD: postgres EHRBASE_USER_ADMIN: ehrbase EHRBASE_PASSWORD_ADMIN: ehrbase EHRBASE_USER: ehrbase_restricted EHRBASE_PASSWORD: ehrbase_restricted healthcheck: test: ['CMD-SHELL', 'pg_isready -U postgres'] interval: 5s timeout: 5s retries: 12 ehrbase: image: ehrbase/ehrbase:next depends_on: ehrdb: condition: service_healthy environment: DB_URL: jdbc:postgresql://ehrdb:5432/ehrbase DB_USER_ADMIN: ehrbase DB_PASS_ADMIN: ehrbase DB_USER: ehrbase_restricted DB_PASS: ehrbase_restricted SECURITY_AUTHTYPE: BASIC SECURITY_AUTHUSER: ehrbase-user SECURITY_AUTHPASSWORD: ChangeMe SECURITY_AUTHADMINUSER: ehrbase-admin SECURITY_AUTHADMINPASSWORD: ChangeMeToo EHRBASE_TEMPLATE_ALLOWOVERWRITE: 'true' SPRING_CONFIG_ADDITIONAL_LOCATION: /config/ehrbase.yml OMOPHUB_CLIENT_ID: ${OMOPHUB_CLIENT_ID} OMOPHUB_CLIENT_SECRET: unused volumes: - ./ehrbase.yml:/config/ehrbase.yml:ro ports: - '8080:8080' ``` Start it: ```bash theme={null} export OMOPHUB_CLIENT_ID=oh_your_api_key_here docker compose up -d ``` ### 3.3 Confirm the config loaded EHRbase prints a single, unambiguous log line at startup if the provider registered correctly: ``` [main] o.e.c.c.v.ValidationConfiguration : Initializing 'omophub' external terminology provider (type: FHIR) at https://fhir.omophub.com/fhir/r4/ secured by oauth2 client 'omophub' ``` No line → no provider. Check for YAML parse errors, missing env vars, or a typo in the `oauth2-client:` reference. ## 4. Template Terminology Binding openEHR templates (ADL 1.4 OPT files) bind coded elements to FHIR value sets via a `` element on the terminology constraint. EHRbase recognizes three authority prefixes as "route this through the configured FHIR provider": * `//fhir.hl7.org/...` * `terminology://fhir.hl7.org/...` * `//hl7.org/fhir/...` The body of the URI must contain a FHIR implicit ValueSet URL in a `url=` query parameter. For OMOP-supported systems the two useful forms are: ```xml theme={null} terminology://fhir.hl7.org/ValueSet?url=http%3A%2F%2Fsnomed.info%2Fsct%3Ffhir_vs terminology://fhir.hl7.org/ValueSet?url=http%3A%2F%2Fsnomed.info%2Fsct%3Ffhir_vs%3Disa%2F73211009 ``` The inner `url=` value is URL-encoded because `` is parsed as a URI string. The decoded form of the second example is `http://snomed.info/sct?fhir_vs=isa/73211009` - the implicit SNOMED ValueSet of all descendants of *Diabetes mellitus* (73211009). Other FHIR system URIs OMOPHub recognizes: | Vocabulary | FHIR system URI | | ---------- | --------------------------------------------- | | SNOMED CT | `http://snomed.info/sct` | | LOINC | `http://loinc.org` | | RxNorm | `http://www.nlm.nih.gov/research/umls/rxnorm` | | ICD-10-CM | `http://hl7.org/fhir/sid/icd-10-cm` | | NDC | `http://hl7.org/fhir/sid/ndc` | These five cover most clinical-terms bindings. OMOPHub registers a total of **20 vocabulary-specific CodeSystems** plus the unified OMOP omnibus - see [Supported Vocabularies](/api-reference/fhir-terminology/overview#supported-vocabularies) for the complete list. Two variants worth naming for EHRbase deployments specifically: * **ICD-10-GM** (`http://hl7.org/fhir/sid/icd-10-gm`) - the German Modification used for hospital billing in Germany. Typically the right choice for any EHRbase deployment billing against G-DRG. * **ICD-10-CN** (`http://hl7.org/fhir/sid/icd-10-cn`) - the Chinese Edition maintained by CAMS, used in Chinese hospital systems. Both are served identically to ICD-10-CM - register them in a template `referenceSetUri` the same way, and `$expand` / `$validate-code` will dispatch to the correct OMOP vocabulary. ## 5. How Validation Actually Works When EHRbase commits a composition that contains a `DV_CODED_TEXT` field constrained by one of these FHIR-routed `referenceSetUri` values, it walks a three-step flow: EHRbase's Spring Security OAuth2 client obtains a Bearer token from `https://fhir.omophub.com/oauth2/token` using `client_credentials` grant type. The `client_id` you configured (your API key) gets echoed back as the `access_token`. EHRbase calls `GET /fhir/r4/ValueSet?url=` to check whether the server knows about the referenced ValueSet. OMOPHub responds with a `Bundle` (type `searchset`) containing a stub ValueSet resource if it recognizes the system URI. An empty bundle means "not supported" and EHRbase skips validation for that field. If support check passes, EHRbase calls `GET /fhir/r4/ValueSet/$expand?url=`. OMOPHub returns a `ValueSet` resource with an `expansion.contains[]` list. EHRbase's JsonPath validator looks for the submitted code in `expansion.contains[?(@.code=='')]`. If it's there, the commit proceeds; if not, the commit is rejected. All three requests carry the Bearer token in the `Authorization` header. EHRbase caches token + expansion responses in its `externalFhirTerminologyCache`, so repeated commits against the same value set don't re-hit OMOPHub. ## 6. Example Round-Trip ### 6.1 Upload a template Any ADL 1.4 OPT that includes at least one `` binding will do. EHRbase's own test fixtures provide a good starting point - see the `IDCR Allergies List.v0.opt` template in the EHRbase repository. ```bash theme={null} curl -u ehrbase-admin:ChangeMeToo \ -H "Content-Type: application/xml" \ -H "Prefer: return=minimal" \ --data-binary @allergies-fhir.opt \ http://localhost:8080/ehrbase/rest/openehr/v1/definition/template/adl1.4 ``` Expected: `HTTP 201`. The template is now registered with whatever FHIR bindings it declared. ### 6.2 Create an EHR ```bash theme={null} EHR_ID=$(curl -s -u ehrbase-user:ChangeMe \ -X POST -H "Prefer: return=representation" -H "Accept: application/json" \ http://localhost:8080/ehrbase/rest/openehr/v1/ehr \ | python3 -c 'import json,sys; print(json.load(sys.stdin)["ehr_id"]["value"])') echo "EHR_ID=$EHR_ID" ``` ### 6.3 Submit a composition with an invalid code Build a minimal composition body that includes a `DV_CODED_TEXT` field bound to one of the template's FHIR-routed constraints, with a code that is definitely **not** in the target value set: ```json composition-invalid.json theme={null} { "_type": "COMPOSITION", "archetype_details": { "template_id": { "value": "IDCR Allergies List.v0" }, "rm_version": "1.0.4" }, "...": "... required RM fields omitted for brevity ...", "content": [ { "items": [ { "value": { "_type": "DV_CODED_TEXT", "value": "Not a real substance", "defining_code": { "_type": "CODE_PHRASE", "terminology_id": { "_type": "TERMINOLOGY_ID", "value": "http://snomed.info/sct" }, "code_string": "99999999" } } } ] } ] } ``` ```bash theme={null} curl -u ehrbase-user:ChangeMe \ -X POST -H "Content-Type: application/json" \ --data-binary @composition-invalid.json \ "http://localhost:8080/ehrbase/rest/openehr/v1/ehr/$EHR_ID/composition" ``` Expected response - `HTTP 422`: ```json theme={null} { "error": "Unprocessable Entity", "message": "... Failed to validate DvCodedText{defining_code=http://snomed.info/sct::99999999, value=Not a real substance}, : The value 99999999 does not match any option from value set http://snomed.info/sct?fhir_vs=isa/105590001" } ``` This is the canonical "terminology rejected a code" path. EHRbase called OMOPHub's `$expand`, scanned the returned `expansion.contains[]`, found no `code:'99999999'` entry, and refused the commit. ### 6.4 Submit a composition with a valid code Replace `code_string` with a concept that is actually in the value set - any descendant of the constraint's parent concept. For the SNOMED `Substance` (`105590001`) value set, that means any concept with `concept_class_id = 'Substance'` whose `Is a` ancestry includes 105590001. ```bash theme={null} # Query the expansion to pick a valid code: VS_URL='http%3A%2F%2Fsnomed.info%2Fsct%3Ffhir_vs%3Disa%2F105590001' curl -sS "https://fhir.omophub.com/fhir/r4/ValueSet/\$expand?url=$VS_URL&count=20" \ -H "Authorization: Bearer $OMOPHUB_CLIENT_ID" \ | python3 -c 'import json,sys; [print(c["code"], c["display"]) for c in json.load(sys.stdin)["expansion"]["contains"]]' ``` Set `defining_code.code_string` to one of the codes returned, leave `terminology_id.value` as `http://snomed.info/sct` (it must match the `system` field in the expansion), and re-submit - expect `HTTP 201`. **Terminology ID must match the expansion's `system`.** EHRbase compares `DV_CODED_TEXT.defining_code.terminology_id.value` against the `system` URI returned by `$expand`. If the template sets the `terminology_id` to the value-set sentinel URL (as some auto-generated example compositions do), validation will fail with `"The terminology {code} must be {system}"`. Always use the canonical system URI (`http://snomed.info/sct`, `http://loinc.org`, etc.) in submitted compositions. ## 7. Supported Operations EHRbase uses a subset of OMOPHub's FHIR Terminology Service during validation: | EHRbase use | OMOPHub endpoint | When it fires | | ---------------------------- | ---------------------------------------- | -------------------------------------------------------------------------- | | Fetch OAuth2 token | `POST /oauth2/token` | First validation call, then cached | | ValueSet support check | `GET /fhir/r4/ValueSet?url=...` | Before each `$expand` for a new VS | | ValueSet expansion | `GET /fhir/r4/ValueSet/$expand?url=...` | On composition commit, per FHIR-bound field | | CodeSystem validation (rare) | `GET /fhir/r4/CodeSystem/$validate-code` | Only if templates use `terminology://fhir.hl7.org/CodeSystem?...` bindings | The rest of the Terminology Service (`$lookup`, `$translate`, `$subsumes`, `$closure`, `$find-matches`) is not reached through EHRbase's validator - if you need those, call OMOPHub directly from your application code. See the [FHIR Integration guide](/guides/integration/fhir-integration) for the full operation surface. ## 8. What OMOPHub Adds vs. Generic Terminology Servers | Capability | OMOPHub | Ontoserver | Local ATHENA + HAPI | | --------------------------------- | ----------------- | ---------- | ------------------------ | | OMOP vocabulary coverage | 130+ vocabularies | Limited | Full (manual download) | | Semantic search (`$find-matches`) | Yes | No | No | | Phoebe recommendations | Yes (opt-in) | No | No | | FHIR → OMOP concept resolver | Yes | No | No | | R4 support | Yes | Yes | R4 only | | Zero infrastructure | Yes (cloud API) | Self-host | PostgreSQL + HAPI server | | SDKs (Python, R, MCP) | Yes | No | No | For hospitals already running EHRbase and planning to publish to OMOP for research, using OMOPHub for both ends (template validation *and* ETL mapping) keeps vocabulary consistency across the capture and analytics sides of the pipeline. ## 9. openEHR → OMOP Pipeline The full loop for a site that uses EHRbase as its clinical repository and OMOP for observational research: Clinician enters data in an openEHR form. Coded fields are validated against OMOPHub's `$expand` at commit time. EHRbase persists the composition with verified SNOMED / LOINC / RxNorm codes. Your ETL pipeline reads compositions from EHRbase and lifts the `DV_CODED_TEXT` fields. Each Coding goes through OMOPHub's [FHIR Resolver](/guides/integration/fhir-integration) (`POST /v1/fhir/resolve`) to get the standard OMOP concept + CDM target table. Write the resolved concepts into OMOP CDM tables - `measurement`, `condition_occurrence`, `drug_exposure`, whichever the resolver's `target_table` field says. The same OMOPHub API key serves both ends. Same vocabulary release, same mapping decisions, same concept IDs - no capture/analytics drift. ## 10. Limitations and Caveats This integration pattern has been tested against `ehrbase/ehrbase:next` and OMOPHub's production FHIR Terminology Service as of 2026-04. EHRbase configurations vary by deployment (security profiles, template sets, validation rules), and production rollouts should run the [verification checklist](#11-verification-checklist) against their own environment before going live. Specific limitations to be aware of: * **OMOPHub does not replace openEHR's built-in terminology.** It handles external clinical terminologies only - codes from SNOMED, LOINC, ICD-10, RxNorm, etc. openEHR's own `openehr-terminology` (used for things like `language`, `territory`, and AOM-level constraint codes) is served by EHRbase internally and does not route through the FHIR provider. * **OMOP vocabulary content is optimized for observational research.** Some source codes are collapsed to a single standard concept in OMOP where a strict clinical coding system would distinguish them. If your templates require sub-concept granularity (e.g. distinguishing two variants of the same SNOMED code), OMOP's `Maps to` behavior may treat them as equivalent. * **Implicit ValueSets only.** OMOPHub supports the FHIR implicit ValueSet URL patterns (`{system}?fhir_vs`, `{system}?fhir_vs=isa/{code}`) but not user-defined named ValueSets uploaded via `PUT /ValueSet/:id`. Templates that bind to custom ValueSet IDs must be rewritten to use implicit-VS bindings. * **OAuth2 is a compatibility shim.** OMOPHub's `/oauth2/token` endpoint implements just enough of RFC 6749 §2.3.1 to satisfy Spring Security: `client_credentials` grant, either `client_secret_basic` or `client_secret_post` client authentication, no scopes enforced, no refresh tokens, `expires_in` is nominal. Your API key is echoed back as the access token. Don't build your own clients against it - use the REST API directly. * **Template terminology binding only.** This guide covers coded-field validation at composition commit time. It does **not** cover AQL terminology-aware querying, archetype-level binding, or bidirectional FHIR ↔ openEHR data transformation. Those are separate problems outside the scope of a terminology server. * **EHRbase caches aggressively.** The `externalFhirTerminologyCache` holds expansion results in memory - if you change OMOPHub content, restart EHRbase (or wait for the TTL) before expecting the cache to refresh. ## 11. Verification Checklist Run these in order against your own EHRbase + OMOPHub setup: | # | Test | Expected result | | - | --------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------- | | 1 | Start EHRbase with OMOPHub configured | Startup log shows `Initializing 'omophub' external terminology provider (type: FHIR) at https://fhir.omophub.com/fhir/r4/` | | 2 | `curl https://fhir.omophub.com/fhir/r4/metadata -H "Authorization: Bearer $KEY"` | HTTP 200, FHIR `CapabilityStatement` | | 3 | `curl -X POST https://fhir.omophub.com/oauth2/token -d "grant_type=client_credentials&client_id=$KEY&client_secret=unused"` | HTTP 200, `{"access_token":"oh_...","token_type":"Bearer",...}` | | 4 | Upload an OPT template with a `terminology://fhir.hl7.org/ValueSet?...` binding | HTTP 201 from EHRbase's template endpoint | | 5 | Submit a composition with a SNOMED code *in* the bound ValueSet | HTTP 201 | | 6 | Submit a composition with a SNOMED code *not* in the bound ValueSet | HTTP 422, error text `"does not match any option from value set"` | | 7 | Check EHRbase logs for `OAuth2AuthorizationException` | Should not appear - if it does, verify `client-id` matches your OMOPHub API key | ## 12. Troubleshooting **`OAuth2AuthorizationException: [invalid_request] client_id is required`** Your OMOPHub API key is missing or the Spring placeholder didn't resolve. Confirm `OMOPHUB_CLIENT_ID` is set in the container's environment (`docker exec ehrbase printenv | grep OMOPHUB`) and that `ehrbase.yml` is mounted at the path `SPRING_CONFIG_ADDITIONAL_LOCATION` points to. **`502 Bad Gateway: 404 Not Found from GET .../fhir/r4/ValueSet`** EHRbase's ValueSet support check is hitting the wrong base URL. Most common cause: the `url:` under `validation.external-terminology.provider.omophub` is missing the `/fhir/r4/` path prefix. The correct value is `https://fhir.omophub.com/fhir/r4/` (with trailing slash). **`The terminology {code} must be {system}`** The `terminology_id.value` in your submitted `DV_CODED_TEXT` does not match the `system` URI returned by `$expand`. Use the canonical system URI (`http://snomed.info/sct`, not the ValueSet sentinel URL) in composition bodies. **`Failed to validate DvCodedText ... No example for terminology ... available`** You're submitting an auto-generated example composition with placeholder values. EHRbase's template example generator produces `code_string: "42"` for fields bound to unknown FHIR ValueSets - always a cache-miss. Replace with real codes before testing validation. **EHRbase log shows terminology-provider init, but commits never hit OMOPHub** Your template is not actually using the FHIR binding prefixes. Check that `` starts with `terminology://fhir.hl7.org/` or `//fhir.hl7.org/` - other URI schemes (e.g. `terminology:SNOMED-CT?subset=...`) are handled by EHRbase's internal terminology and never route through the FHIR provider. ## 13. Next Steps The full surface of OMOPHub's FHIR Terminology Service - `$lookup`, `$translate`, `$subsumes`, `$find-matches`, batch Bundles, plus the OMOP FHIR Resolver for ETL pipelines. Point a HAPI FHIR server at OMOPHub as a remote terminology backend - useful if your openEHR stack also fronts a HAPI FHIR facade for external clients. Patterns for CDS Hooks, SMART on FHIR apps, and sidecar architectures that resolve vocabularies at the point of care. # FHIR Integration Source: https://docs.omophub.com/guides/integration/fhir-integration Use OMOPHub with FHIR - the FHIR Resolver for CodeableConcept mapping, the FHIR Terminology Service, and recipes for Observations and Medications. ## 1. Exchanging Data vs. Exchanging Meaning FHIR solved the data exchange problem. A hospital can send a FHIR `Observation` to a research platform, a FHIR `MedicationStatement` to a safety database, or a FHIR `Condition` to a registry. The structure is standardized. The JSON is valid. The API works. But open the payload and look at the `CodeableConcept` - the field that carries the clinical meaning. You'll find LOINC codes from one system, proprietary local codes from another, and sometimes just a `text` field with "Blood Sugar" and no structured code at all. The data exchanged successfully. The meaning got lost in translation. This is the semantic interoperability gap. FHIR standardizes *structure*; OMOP standardizes *vocabulary*. To go from exchanged FHIR data to analyzable OMOP records, you need to resolve every `CodeableConcept` to a standard OMOP concept ID. **OMOPHub** gives you two purpose-built surfaces for this job: * **The FHIR Resolver** (`POST /v1/fhir/resolve*`) - a composite, OMOP-aware endpoint that takes a FHIR `Coding` or `CodeableConcept` and returns a standard OMOP concept, a mapping type, and a CDM target table. This is what you want for ETL pipelines. * **The FHIR Terminology Service** (`https://fhir.omophub.com/fhir/{r4,r4b,r5,r6}/*`) - a spec-conformant FHIR terminology server implementing `$lookup`, `$validate-code`, `$translate`, `$expand`, `$subsumes`, `$closure`, `$find-matches`, and the OMOPHub-custom `$diff`. This is what you want when a FHIR client, HAPI FHIR server, or SMART app needs to talk to a real terminology service. This guide covers both - when to use each, what they return, and how they combine in real pipelines. ## 2. Three Integration Paths OMOPHub exposes three ways to do vocabulary resolution over FHIR data. Pick the one that matches your job: | Your job | Use | Endpoint | | ----------------------------------------------------------------------------------------------------- | ---------------------------- | ---------------------------------------------------------- | | ETL: map a FHIR `Coding` to an OMOP standard concept + CDM target table | **FHIR Resolver** | `POST https://api.omophub.com/v1/fhir/resolve` | | A FHIR client needs a conformant terminology server ($lookup, $validate-code, $translate, $expand...) | **FHIR Terminology Service** | `https://fhir.omophub.com/fhir/r4/*` | | HAPI FHIR server needs a remote terminology backend | **FHIR Terminology Service** | See [HAPI FHIR Integration](/guides/integration/hapi-fhir) | | Building SMART on FHIR / CDS Hooks apps at point of care | **FHIR Resolver** | See [EHR Integration](/guides/integration/ehr-integration) | | Custom traversal, hierarchy walks, ingredient resolution, semantic search | **REST primitives** | `/v1/concepts/*`, `/v1/search/*`, `/v1/mappings/*` | **When in doubt, reach for the Resolver first.** If you're parsing FHIR resources and writing OMOP rows, the Resolver collapses URI mapping, code lookup, `Maps to` traversal, CDM target-table assignment, and semantic fallback into a single call. The Terminology Service is conformance-first - it returns `Parameters` and `OperationOutcome` resources for tools that speak FHIR natively. ## 3. Authentication & Endpoints OMOPHub serves the Resolver and the Terminology Service from two hostnames, but both are backed by the same service and the same API key. | Surface | Base URL | Format | | ------------------------ | -------------------------- | -------------------------------------- | | REST API + FHIR Resolver | `https://api.omophub.com` | OMOPHub JSON envelope | | FHIR Terminology Service | `https://fhir.omophub.com` | FHIR `Parameters` / `OperationOutcome` | Every endpoint requires a Bearer token. Get one from your [dashboard](https://dashboard.omophub.com/api-keys) and pass it in the `Authorization` header: ```bash theme={null} KEY="oh_your_api_key" # 30-second smoke test - should return a FHIR CapabilityStatement curl https://fhir.omophub.com/fhir/r4/metadata \ -H "Authorization: Bearer $KEY" # Same for the resolver curl https://api.omophub.com/v1/vocabularies \ -H "Authorization: Bearer $KEY" ``` Both hosts accept traffic on `/v1/*` and `/fhir/*`, but the convention is: **`api.omophub.com` for REST and the Resolver, `fhir.omophub.com` for the Terminology Service**. Use the matching host in your clients so DNS, CORS, and logs stay clean. FHIR Terminology Service versions are path-segment prefixes: `/fhir/r4`, `/fhir/r4b`, `/fhir/r5`, `/fhir/r6`. Omitting the segment (`/fhir/metadata`) defaults to **r4**. Response headers `X-Vocab-Release` and `X-Vocab-Release-Status` tell you which OMOP vocabulary release served the request. ## 4. Path A: The FHIR Resolver (Recommended for ETL) The Resolver is a single composite endpoint that chains every step of FHIR → OMOP resolution: 1. Resolve the FHIR `system` URI to an OMOP `vocabulary_id`. 2. Look up the source concept by code + vocabulary. 3. If the source concept is non-standard, follow `Maps to` to the standard equivalent. 4. If the code isn't found, fall back to semantic search on the `display` text, filtered by the expected domain. 5. Determine the CDM `target_table` (e.g. `measurement`, `drug_exposure`, `condition_occurrence`) from the resolved concept's domain. 6. Optionally return Phoebe recommendations and mapping-quality signals. It comes in three flavors: * `POST /v1/fhir/resolve` - single `Coding` * `POST /v1/fhir/resolve/batch` - up to 100 codings per request, metered as N calls * `POST /v1/fhir/resolve/codeable-concept` - full `CodeableConcept` with OHDSI vocabulary preference ranking when multiple codings are present ### 4.1 Single Coding ```python Python theme={null} import requests resp = requests.post( "https://api.omophub.com/v1/fhir/resolve", headers={"Authorization": "Bearer oh_your_api_key"}, json={ "system": "http://loinc.org", "code": "2339-0", "display": "Blood Glucose", "resource_type": "Observation", "include_recommendations": False, "include_quality": True, }, ) result = resp.json()["data"]["resolution"] result["standard_concept"]["concept_id"] # 3004501 result["standard_concept"]["concept_name"] # "Glucose [Mass/volume] in Serum or Plasma" result["target_table"] # "measurement" result["mapping_type"] # "direct" result["domain_resource_alignment"] # "aligned" result["mapping_quality"] # "high" ``` The response envelope follows the standard OMOPHub `{success, data, meta}` shape. The `resolution` object always contains a `source_concept` (what was found for the input code) and a `standard_concept` (what you should write to CDM tables after `Maps to` traversal) - they're identical when the source is already standard. ### 4.2 Batch Resolve Bulk ETL workloads should use the batch endpoint. Each item is resolved independently and errors are reported per-item: ```bash theme={null} curl -X POST https://api.omophub.com/v1/fhir/resolve/batch \ -H "Authorization: Bearer $KEY" \ -H "Content-Type: application/json" \ -d '{ "codings": [ { "system": "http://loinc.org", "code": "2339-0", "resource_type": "Observation" }, { "system": "http://hl7.org/fhir/sid/icd-10-cm", "code": "E11.9", "resource_type": "Condition" }, { "system": "http://hl7.org/fhir/sid/ndc", "code": "0071-0155-01", "resource_type": "MedicationStatement" } ] }' ``` Batch requests count against your quota **per coding**, not per HTTP call. A 50-item batch meters as 50 API calls. This matches how the Resolver actually does the work and keeps pricing predictable. ### 4.3 CodeableConcept with Vocabulary Preference When a single `CodeableConcept` carries multiple codings for the same clinical meaning (common in EHR exports - SNOMED *and* ICD-10-CM for the same condition), the CodeableConcept endpoint picks the best one per OHDSI's vocabulary preference: ```python Python theme={null} resp = requests.post( "https://api.omophub.com/v1/fhir/resolve/codeable-concept", headers={"Authorization": "Bearer oh_your_api_key"}, json={ "coding": [ {"system": "http://snomed.info/sct", "code": "44054006"}, {"system": "http://hl7.org/fhir/sid/icd-10-cm", "code": "E11.9"}, ], "text": "Type 2 diabetes mellitus", "resource_type": "Condition", }, ) best = resp.json()["data"]["best_match"]["resolution"] best["source_concept"]["vocabulary_id"] # "SNOMED" (preferred over ICD10CM for Condition) best["standard_concept"]["concept_id"] # 201826 best["target_table"] # "condition_occurrence" ``` The response also includes all the alternative resolutions under `data.all_matches`, so you can audit the preference decision. ### 4.4 Response Shape: the Fields That Matter Every resolver response - single, batch, and CodeableConcept - returns the same `resolution` object. The fields worth knowing: | Field | What it tells you | | ------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `source_concept` | The OMOP concept matching the input code in its source vocabulary. May be non-standard. | | `standard_concept` | The OMOP concept after `Maps to` traversal. **This is what you write to the CDM.** | | `mapping_type` | `direct` (source was already standard), `mapped` (followed `Maps to`), `semantic_match` (fell back to display-text search), or `unmapped`. | | `similarity_score` | Only present when `mapping_type = semantic_match` - the text similarity of the fallback match. | | `target_table` | The OMOP CDM destination table (`measurement`, `condition_occurrence`, `drug_exposure`, `procedure_occurrence`, `observation`, ...). Computed from the standard concept's domain. | | `domain_resource_alignment` | `aligned` / `misaligned` - whether the resolved concept's domain matches the FHIR resource type you declared in `resource_type`. A `misaligned` Observation → `drug` domain is a strong signal that the source data is wrong. | | `alternative_standard_concepts` | Other plausible `Maps to` targets (for ambiguous source codes). | | `recommendations` | Phoebe recommendations, if `include_recommendations: true`. | | `mapping_quality` | `high` / `medium` / `low` / `manual_review`, if `include_quality: true`. | ## 5. Path B: The FHIR Terminology Service When you need a real FHIR terminology server - because your client speaks FHIR, because you're conformance-testing, or because HAPI FHIR wants a remote backend - point it at `https://fhir.omophub.com`. Every operation is spec-conformant (for FHIR R4/R4B/R5/R6) and returns FHIR `Parameters` or `OperationOutcome` resources with `application/fhir+json`. Every FHIR route accepts an optional version prefix: ``` GET https://fhir.omophub.com/fhir/metadata # defaults to r4 GET https://fhir.omophub.com/fhir/r4/metadata # explicit r4 GET https://fhir.omophub.com/fhir/r5/metadata # r5 GET https://fhir.omophub.com/fhir/r6/metadata # r6 (draft) ``` All examples below use `/fhir/r4`. Replace with `/fhir/r5` or `/fhir/r6` as needed. ### 5.1 CapabilityStatement - `GET /metadata` Every FHIR server starts here. OMOPHub returns a `CapabilityStatement` listing the supported resources and operations: ```bash theme={null} curl https://fhir.omophub.com/fhir/r4/metadata \ -H "Authorization: Bearer $KEY" ``` The response declares support for `CodeSystem` (read, search-type, `$lookup`, `$validate-code`, `$subsumes`, `$find-matches`), `ValueSet` (`$expand`, `$validate-code`), and `ConceptMap` (`$translate`, `$closure`). ### 5.2 `$lookup` - concept details for a code Returns the display, designations, and properties for a given code. Supports the full FHIR `$lookup` parameter set, plus the OMOPHub-specific `property=recommended` for Phoebe recommendations. ```bash GET theme={null} curl "https://fhir.omophub.com/fhir/r4/CodeSystem/\$lookup?\ system=http://snomed.info/sct&code=44054006" \ -H "Authorization: Bearer $KEY" ``` ```bash POST theme={null} curl -X POST https://fhir.omophub.com/fhir/r4/CodeSystem/\$lookup \ -H "Authorization: Bearer $KEY" \ -H "Content-Type: application/fhir+json" \ -d '{ "resourceType": "Parameters", "parameter": [ { "name": "system", "valueUri": "http://snomed.info/sct" }, { "name": "code", "valueCode": "44054006" }, { "name": "property", "valueString": "recommended" } ] }' ``` Response is a `Parameters` resource with `name`, `display`, `designation[]`, and `property[]` entries. ### 5.3 `$validate-code` - check that a code belongs to a system or value set Two variants: one against a `CodeSystem`, one against a `ValueSet`. ```bash theme={null} # CodeSystem - does SNOMED 44054006 exist, and is the display correct? curl "https://fhir.omophub.com/fhir/r4/CodeSystem/\$validate-code?\ url=http://snomed.info/sct&code=44054006&display=Type+2+diabetes+mellitus" \ -H "Authorization: Bearer $KEY" # ValueSet - is this code in the US Core Condition value set? curl "https://fhir.omophub.com/fhir/r4/ValueSet/\$validate-code?\ url=http://hl7.org/fhir/ValueSet/condition-code&\ system=http://snomed.info/sct&code=44054006" \ -H "Authorization: Bearer $KEY" ``` The response is a `Parameters` resource with `result: boolean`, `display: string` (the canonical display), and `message: string` (on mismatch). ### 5.4 `$translate` - map a code between vocabularies Uses the OMOP `Maps to` relationship graph to translate a source code to a target CodeSystem. ```bash GET theme={null} curl "https://fhir.omophub.com/fhir/r4/ConceptMap/\$translate?\ system=http://hl7.org/fhir/sid/icd-10-cm&code=E11.9&\ target=http://snomed.info/sct" \ -H "Authorization: Bearer $KEY" ``` ```bash POST theme={null} curl -X POST https://fhir.omophub.com/fhir/r4/ConceptMap/\$translate \ -H "Authorization: Bearer $KEY" \ -H "Content-Type: application/fhir+json" \ -d '{ "resourceType": "Parameters", "parameter": [ { "name": "system", "valueUri": "http://hl7.org/fhir/sid/icd-10-cm" }, { "name": "code", "valueCode": "E11.9" }, { "name": "target", "valueUri": "http://snomed.info/sct" } ] }' ``` The response contains one or more `match` parameters, each with an `equivalence` (`equivalent`, `wider`, `narrower`, ...) and a `concept` Coding. ### 5.5 `$expand` - enumerate a value set Expands a `ValueSet` URL into its member concepts. Supports `filter`, `count`, `offset`, and `includeDesignations`. Works for implicit SNOMED value sets (`http://snomed.info/sct?fhir_vs`) and for OMOPHub's curated value sets. ```bash theme={null} curl "https://fhir.omophub.com/fhir/r4/ValueSet/\$expand?\ url=http://snomed.info/sct?fhir_vs&filter=diabetes&count=20" \ -H "Authorization: Bearer $KEY" ``` The response is a `ValueSet` with an `expansion` containing `total` (across the whole match, not just this page), `offset`, `parameter[]`, and `contains[]` (the member concepts). ### 5.6 `$subsumes` - hierarchy relationship between two codes Answers: is code A an ancestor, descendant, or equivalent of code B in the same CodeSystem? Useful for phenotype inclusion/exclusion rules. ```bash theme={null} # Is "Diabetes mellitus" (73211009) an ancestor of "Type 2 diabetes" (44054006)? curl "https://fhir.omophub.com/fhir/r4/CodeSystem/\$subsumes?\ system=http://snomed.info/sct&codeA=73211009&codeB=44054006" \ -H "Authorization: Bearer $KEY" ``` Returns `outcome: subsumes | subsumed-by | equivalent | not-subsumed`. ### 5.7 `$find-matches` - semantic search in a FHIR envelope OMOPHub's semantic search exposed through the standard FHIR operation. Send `property` parameters describing what you're looking for; get `Coding` results back. ```bash theme={null} curl -X POST "https://fhir.omophub.com/fhir/r4/CodeSystem/\$find-matches" \ -H "Authorization: Bearer $KEY" \ -H "Content-Type: application/fhir+json" \ -d '{ "resourceType": "Parameters", "parameter": [ { "name": "system", "valueUri": "http://snomed.info/sct" }, { "name": "property", "part": [ { "name": "code", "valueCode": "display" }, { "name": "value", "valueString": "hemoglobin a1c" } ]} ] }' ``` Use this when your client is FHIR-native and you'd rather not maintain a second integration for OMOPHub's `/v1/search/semantic` endpoint. ### 5.8 `$closure` - maintain a transitive closure table The standard FHIR operation for incrementally building a transitive closure as new codes are seen. Useful when you're materializing a phenotype or cohort definition over streaming data. ```bash theme={null} curl -X POST https://fhir.omophub.com/fhir/r4/ConceptMap/\$closure \ -H "Authorization: Bearer $KEY" \ -H "Content-Type: application/fhir+json" \ -d '{ "resourceType": "Parameters", "parameter": [ { "name": "name", "valueString": "t2dm-cohort" }, { "name": "concept", "valueCoding": { "system": "http://snomed.info/sct", "code": "44054006" } }, { "name": "concept", "valueCoding": { "system": "http://snomed.info/sct", "code": "46635009" } } ] }' ``` Returns a `ConceptMap` with the new subsumption relationships discovered for the codes in this batch, relative to everything already in the closure. ### 5.9 `$diff` - OMOPHub-custom vocabulary version comparison Not in the FHIR spec. OMOPHub adds this because vocabulary releases change over time and CDM operators need to know what moved. `$diff` compares two OMOP vocabulary releases and returns the added, removed, and changed concepts for a given CodeSystem. ```bash theme={null} curl "https://fhir.omophub.com/fhir/r4/CodeSystem/\$diff?\ url=http://snomed.info/sct&from=2024v2&to=2026v1" \ -H "Authorization: Bearer $KEY" ``` Use cases: impact analysis before cutting a new release into production, audit trails for regulated pipelines, release notes for cohort definitions. ### 5.10 Batch Bundle - multiple operations in one request OMOPHub supports the FHIR `batch`/`transaction` Bundle pattern. Send a `Bundle` with multiple `entry.request` items and get back a `Bundle` with one `response` per entry. ```bash theme={null} curl -X POST https://fhir.omophub.com/fhir/r4/ \ -H "Authorization: Bearer $KEY" \ -H "Content-Type: application/fhir+json" \ -d '{ "resourceType": "Bundle", "type": "batch", "entry": [ { "request": { "method": "GET", "url": "CodeSystem/$lookup?system=http://snomed.info/sct&code=44054006" } }, { "request": { "method": "GET", "url": "CodeSystem/$validate-code?url=http://loinc.org&code=2339-0" } }, { "request": { "method": "GET", "url": "ConceptMap/$translate?system=http://hl7.org/fhir/sid/icd-10-cm&code=E11.9&target=http://snomed.info/sct" } } ] }' ``` Each entry is metered individually - a 10-operation bundle counts as 10 API calls. ## 6. Path C: Raw Vocabulary Primitives The Resolver and the Terminology Service are opinionated. When you need full control - walking `Has ingredient` relationships, custom semantic-search filters, hierarchy traversal with specific relationship types - drop to the REST primitives: ```python Python theme={null} import omophub client = omophub.OMOPHub() # 1. Find a concept by code concept = client.search.basic("44054006", vocabulary_ids=["SNOMED"])["concepts"][0] # 2. Walk its Maps-to relationships mappings = client.mappings.get(concept["concept_id"], target_vocabulary="SNOMED") # 3. Explore hierarchy (ancestors / descendants) ancestors = client.hierarchy.ancestors(concept["concept_id"], max_levels=5) # 4. Non-hierarchy relationships (e.g. "Has ingredient" for drugs) rels = client.concepts.relationships(concept["concept_id"]) ``` Use this path when: * You need `Has ingredient` / `RxNorm has ing` resolution for drugs (see the MedicationStatement use case in §8 below). * You want to control the exact relationship types walked during a hierarchy traversal. * You want to combine semantic search with domain filters that the Resolver doesn't expose. * You're building tooling that needs raw concept metadata (vocabulary ID, concept class, domain, validity). ## 7. Use Case: FHIR Observation → OMOP Measurement The most common FHIR-to-OMOP mapping. Lab results and vital signs arrive as `Observation` resources and need to become OMOP `measurement` records. **Scenario:** A blood glucose `Observation` arrives with LOINC code `2339-0`. Resolve it to a standard OMOP concept and build a measurement row. ```python Python theme={null} import requests, json fhir_obs = { "resourceType": "Observation", "status": "final", "code": { "coding": [{ "system": "http://loinc.org", "code": "2339-0", "display": "Glucose [Mass/volume] in Blood", }], "text": "Blood Glucose", }, "valueQuantity": {"value": 120, "unit": "mg/dL"}, "effectiveDateTime": "2023-10-26T09:30:00Z", } coding = fhir_obs["code"]["coding"][0] resp = requests.post( "https://api.omophub.com/v1/fhir/resolve", headers={"Authorization": "Bearer oh_your_api_key"}, json={ "system": coding["system"], "code": coding["code"], "display": fhir_obs["code"].get("text"), "resource_type": "Observation", }, ) result = resp.json()["data"]["resolution"] omop_measurement = { "measurement_concept_id": result["standard_concept"]["concept_id"], "measurement_source_value": coding.get("display", ""), "value_as_number": fhir_obs["valueQuantity"]["value"], "unit_source_value": fhir_obs["valueQuantity"].get("unit"), "measurement_date": fhir_obs["effectiveDateTime"][:10], } print(json.dumps(omop_measurement, indent=2)) ``` **Key insight:** LOINC codes are usually already standard in OMOP, so `mapping_type = direct`. The Resolver confirms `target_table = "measurement"` - use that as a sanity check that your downstream CDM writer is pointed at the right table. If the Observation carried a local code instead of LOINC, the Resolver would fall back to semantic search over the `display` text automatically. **See also:** [Lab normalization in EHR Integration](/guides/integration/ehr-integration) for the sidecar-app architecture that wraps this resolution pattern. ## 8. Use Case: FHIR MedicationStatement → OMOP Ingredient Medication reconciliation requires rolling specific product codes up to their active ingredient for class-level analysis. **Scenario:** A FHIR `MedicationStatement` arrives with NDC code `0071-0155-01` (Lipitor 10mg). For a drug safety study, you need the active ingredient (Atorvastatin), not the branded product. The Resolver handles the NDC → standard RxNorm leg. The ingredient traversal is a separate `concepts.relationships` call because "Has ingredient" is a non-hierarchical relationship, not an ancestor. ```python Python theme={null} import requests, omophub client = omophub.OMOPHub() # Step 1: NDC → standard RxNorm via the Resolver resp = requests.post( "https://api.omophub.com/v1/fhir/resolve", headers={"Authorization": "Bearer oh_your_api_key"}, json={ "system": "http://hl7.org/fhir/sid/ndc", "code": "0071-0155-01", "display": "Lipitor 10 MG Oral Tablet", "resource_type": "MedicationStatement", }, ) rxnorm = resp.json()["data"]["resolution"]["standard_concept"] print(rxnorm["concept_id"], rxnorm["concept_name"]) # 40165252 "atorvastatin 10 MG Oral Tablet [Lipitor]" # Step 2: Walk "Has ingredient" to find the active ingredient concept rels = client.concepts.relationships(rxnorm["concept_id"]) ingredients = [ r for r in rels.get("relationships", []) if "ingredient" in r.get("relationship_id", "").lower() or r.get("concept_class_id") == "Ingredient" ] if ingredients: ing = ingredients[0] print(ing["concept_id"], ing["concept_name"]) # 1545958 "atorvastatin" ``` **Key insight:** The Resolver gives you the CDM-correct `drug_exposure_concept_id` (the branded product) in one call. Ingredient-level analysis is a *separate* question - OMOP stores drug composition as relationships, not hierarchy - and requires a follow-up call on the raw primitives. Don't try to squeeze this into the Resolver; use the right tool per leg. **See also:** [Real-time medication CDS in EHR Integration](/guides/integration/ehr-integration) for the CDS-Hooks pattern that combines ingredient lookup with drug-drug interaction alerting. ## 9. FHIR Extensions & CodeableConcept Nuances Real-world FHIR uses extensions - extra data fields not in the base spec. US Core adds detailed race/ethnicity extensions to `Patient`; mCODE adds staging and grade extensions to `Condition`; QI-Core adds adherence extensions to `MedicationStatement`. When extensions contain `CodeableConcept` values, the resolution pattern is identical to the base resources. ```python Python theme={null} # US Core detailed race extension on a Patient resource patient_extension = { "url": "http://hl7.org/fhir/us/core/StructureDefinition/us-core-race", "extension": [{ "url": "ombCategory", "valueCoding": { "system": "urn:oid:2.16.840.1.113883.6.238", "code": "2106-3", "display": "White", } }] } # Resolve the Coding inside the extension just like any other Coding coding = patient_extension["extension"][0]["valueCoding"] resp = requests.post( "https://api.omophub.com/v1/fhir/resolve", headers={"Authorization": "Bearer oh_your_api_key"}, json={ "system": coding["system"], "code": coding["code"], "display": coding["display"], "resource_type": "Patient", }, ) # Write the result to person.race_concept_id ``` The **parsing** of extensions - finding them by URL, extracting their values - is application code. The **vocabulary resolution** step is identical regardless of whether the `CodeableConcept` lives at the top of a resource, inside a component, or inside an extension. When a `CodeableConcept` in an extension has a `text` field but no `coding`, send it to the Resolver with `display` set and no `system`/`code`. The Resolver will fall back to semantic search over the text, scoped to whatever `resource_type` hint you provide. ## 10. Versioning, Rate Limits & Errors ### Vocabulary versioning Every response from the Terminology Service carries two headers telling you which OMOP vocabulary release served the request: ``` X-Vocab-Release: 2026v1 X-Vocab-Release-Status: active ``` You can pin a specific release in three ways (first match wins): 1. Header: `X-Vocab-Release: 2026v1` 2. Query parameter: `?vocab_release=2026v1` 3. Path segment in OMOPHub's legacy path-versioned routes (not used by the FHIR service - use the header for FHIR requests) Available releases are published in the `CapabilityStatement` at `/metadata` and listed in detail in [vocabulary-versions](/vocabulary-versions). ### Rate limits FHIR and REST share the same quota pool per API key. Default free-tier limit is 60 requests/minute. Every response includes: ``` X-RateLimit-Limit: 60 X-RateLimit-Remaining: 42 X-RateLimit-Reset: 1729951200 ``` Batch operations meter **per item**, not per HTTP call: a 50-coding `POST /v1/fhir/resolve/batch` or a 10-entry FHIR Bundle counts as 50 or 10 calls respectively. Plan your quota accordingly. ### Error envelopes The two surfaces return different shapes: ```json FHIR Terminology Service theme={null} { "resourceType": "OperationOutcome", "issue": [{ "severity": "error", "code": "not-found", "diagnostics": "Code 'abc123' not found in system 'http://snomed.info/sct'" }] } ``` ```json FHIR Resolver (REST) theme={null} { "success": false, "error": { "code": "CONCEPT_NOT_FOUND", "message": "No OMOP concept found for code 'abc123' in SNOMED" }, "meta": { "request_id": "6638747b-4b12-482b-968d-bc5080336555", "timestamp": "2026-04-13T16:48:09.578Z" } } ``` HTTP status codes are the same across both surfaces: `400` for bad parameters, `401` for missing/invalid API key, `404` for unknown code or value set, `410` for archived vocabulary releases, `429` for rate-limit exhaustion, `5xx` for server errors. ## 11. Next Steps The complete end-to-end workflow from FHIR-coded data to OMOP CDM tables. Supported FHIR operations, tested clients, and auth patterns at a glance. Point your HAPI FHIR server at OMOPHub as a remote terminology service - one config change, no local ATHENA download. CDS Hooks, SMART on FHIR apps, sidecar architectures, and real-time vocabulary resolution at the point of care. The Extract → Ground → Reason pattern for using OMOPHub as a vocabulary backbone for clinical LLMs. FHIR-specific caveats: implicit ValueSets only, Bundle-shaped XML is best-effort, and what's on the roadmap. # HAPI FHIR Integration Source: https://docs.omophub.com/guides/integration/hapi-fhir Configure HAPI FHIR to use OMOPHub as a remote terminology backend - $validate-code, $lookup, and $translate over the FHIR R4/R5 wire protocol. ## 1. Overview HAPI FHIR can delegate terminology operations to a remote FHIR terminology server for code validation, concept lookup, and cross-vocabulary translation. Point it at `https://fhir.omophub.com/fhir/r4` and you get all 130+ OMOP vocabularies (SNOMED CT, LOINC, RxNorm, ICD-10, NDC, UCUM, …) without downloading ATHENA and managing a local PostgreSQL vocabulary database. This guide covers two deployment shapes: * **HAPI FHIR JPA Starter** (`hapiproject/hapi:latest` Docker image) - the most common "just run HAPI" setup. Needs a small reverse-proxy workaround because the starter's config schema does not expose a Bearer-token field. * **Custom Spring Boot HAPI build** - when you embed `HapiFhirContext` + `RemoteTerminologyServiceValidationSupport` in your own Spring Boot app, you can attach a `BearerTokenAuthInterceptor` directly and skip the proxy. Both are covered below, with working snippets verified against HAPI FHIR 8.8.0. OMOPHub serves **R4, R5, and R6**. This guide uses R4 throughout because it's what HAPI FHIR JPA Starter and most production HAPI deployments run. To target R5 or R6, replace `/fhir/r4` with `/fhir/r5` or `/fhir/r6` in your config - both the terminology endpoints and the `CapabilityStatement` at `/metadata` work identically. ## 2. How HAPI Consumes OMOPHub HAPI FHIR's `RemoteTerminologyServiceValidationSupport` validates a code in three steps: `GET /fhir/r4/CodeSystem?url=http://loinc.org` - "does this server know LOINC?". HAPI's client expects a FHIR `Bundle` (type `searchset`) with at least one `CodeSystem` entry. OMOPHub returns a lightweight stub with `content: "not-present"`, which tells HAPI the server supports LOINC via operations but doesn't host the full concept list as a resource. If the Bundle is empty, HAPI skips OMOPHub entirely for that vocabulary. `GET /fhir/r4/CodeSystem/$validate-code?url=http://loinc.org&code=2951-2` - HAPI asks OMOPHub whether `2951-2` is a valid LOINC code and (optionally) whether the display text matches. OMOPHub's handler dispatches on the FHIR system URI, looks up the concept in the underlying OMOP schema, and returns a FHIR `Parameters` resource with `result: boolean` and the canonical `display` string. If OMOPHub returns `result: false` or the HTTP request fails, HAPI's validation chain falls through to whatever other support classes it has configured (in-memory default value sets, local CodeSystems loaded into the JPA database, etc.). This makes OMOPHub safely additive - it never *blocks* validation that HAPI could answer locally. HAPI's code resolution flow also covers `$lookup` (single-code lookup for display/designations/properties) and `$translate` (cross-vocabulary mapping via `Maps to`). Those are wired through the same config; no extra setup. ## 3. Path A: HAPI FHIR JPA Starter (Docker) This is the path if you're running the stock `hapiproject/hapi:latest` image. The challenge: **the JPA Starter's YAML schema has no `auth` field on its remote-terminology config**. It accepts `system` and `url` for each provider, but it never calls `addClientInterceptor()` on the resulting validation-support bean, so Bearer tokens can't be attached through configuration alone. We work around this with a tiny reverse proxy that injects the `Authorization` header on outbound requests. **The YAML layout used by the JPA Starter is `hapi.fhir.remote_terminology_service..{system, url}` - not `hapi.fhir.validation.remote_terminology_service_urls`.** Older HAPI FHIR documentation and blog posts reference the `validation.remote_terminology_service_urls` key - that property path is not bound by the stock JPA Starter and silently has no effect. Always verify your config against `src/main/resources/application.yaml` in the `hapifhir/hapi-fhir-jpaserver-starter` repo. ### 3.1 `application.yaml` overlay Mount this file into the container at `/app/config/application.yaml`: ```yaml application.yaml theme={null} hapi: fhir: fhir_version: R4 validation: requests_enabled: true responses_enabled: true remote_terminology_service: omophub: system: '*' url: http://omophub-proxy:8080/fhir/r4/ ``` Notes: * `system: '*'` makes OMOPHub the catch-all terminology authority for every code system. If you want to scope it (e.g. SNOMED via OMOPHub, ICD-9 via a different server), register multiple entries and set each `system` to a specific canonical URI. * `url:` must point at your reverse proxy, not at OMOPHub directly - the proxy is what adds the `Authorization` header. Trailing slash required. * `validation.requests_enabled` / `responses_enabled` are off by default in the JPA Starter - flip them on so validation actually runs against incoming and outgoing resources. ### 3.2 Reverse proxy (nginx) This proxy listens on port 8080, forwards everything to `https://fhir.omophub.com`, and injects `Authorization: Bearer $OMOPHUB_CLIENT_ID` on every request. ```nginx nginx.conf theme={null} events { worker_connections 64; } http { server { listen 8080; location / { proxy_pass https://fhir.omophub.com; proxy_set_header Host fhir.omophub.com; proxy_set_header Authorization "Bearer ${OMOPHUB_CLIENT_ID}"; proxy_http_version 1.1; proxy_read_timeout 30s; } } } ``` ### 3.3 Docker Compose A minimal stack - HAPI + Postgres + the auth-injecting proxy: ```yaml docker-compose.yml theme={null} services: hapi-db: image: postgres:16 environment: POSTGRES_USER: admin POSTGRES_PASSWORD: admin POSTGRES_DB: hapi healthcheck: test: ['CMD-SHELL', 'pg_isready -U admin'] interval: 5s omophub-proxy: image: nginx:alpine environment: OMOPHUB_CLIENT_ID: ${OMOPHUB_CLIENT_ID} volumes: - ./nginx.conf:/etc/nginx/templates/default.conf.template:ro command: > sh -c 'envsubst "\$$OMOPHUB_CLIENT_ID" < /etc/nginx/templates/default.conf.template > /etc/nginx/nginx.conf && nginx -g "daemon off;"' hapi: image: hapiproject/hapi:latest depends_on: hapi-db: { condition: service_healthy } omophub-proxy: { condition: service_started } environment: SPRING_DATASOURCE_URL: jdbc:postgresql://hapi-db:5432/hapi SPRING_DATASOURCE_USERNAME: admin SPRING_DATASOURCE_PASSWORD: admin SPRING_DATASOURCE_DRIVER_CLASS_NAME: org.postgresql.Driver SPRING_JPA_PROPERTIES_HIBERNATE_DIALECT: org.hibernate.dialect.PostgreSQLDialect SPRING_CONFIG_ADDITIONAL_LOCATION: file:/app/config/application.yaml volumes: - ./application.yaml:/app/config/application.yaml:ro ports: ['8080:8080'] ``` Start it: ```bash theme={null} export OMOPHUB_CLIENT_ID=oh_your_api_key_here docker compose up -d ``` ## 4. Path B: Custom Spring Boot HAPI Build If you're embedding HAPI FHIR into your own Spring Boot application (as opposed to running the JPA Starter image), you can wire the remote terminology validator programmatically - no proxy needed. The `BearerTokenAuthInterceptor` attaches to the HTTP client before every outbound call. ### 4.1 Static Bearer token ```java TerminologyConfig.java theme={null} @Configuration public class TerminologyConfig { @Value("${omophub.api-key}") private String omophubApiKey; @Bean public IValidationSupport omophubTerminologyService(FhirContext fhirContext) { RemoteTerminologyServiceValidationSupport remote = new RemoteTerminologyServiceValidationSupport(fhirContext); remote.setBaseUrl("https://fhir.omophub.com/fhir/r4"); remote.addClientInterceptor(new BearerTokenAuthInterceptor(omophubApiKey)); return remote; } } ``` Register the bean in your `ValidationSupportChain`: ```java theme={null} @Bean public ValidationSupportChain validationSupportChain( FhirContext fhirContext, IValidationSupport omophubTerminologyService ) { return new ValidationSupportChain( new DefaultProfileValidationSupport(fhirContext), omophubTerminologyService, new InMemoryTerminologyServerValidationSupport(fhirContext), new CommonCodeSystemsTerminologyService(fhirContext) ); } ``` Put OMOPHub **before** `InMemoryTerminologyServerValidationSupport` so your code system queries reach OMOPHub first; the in-memory support handles a few HL7-standard value sets that OMOPHub doesn't. ### 4.2 OAuth2 client\_credentials (alternative) OMOPHub's `/oauth2/token` endpoint accepts RFC 6749 `client_credentials` with either `client_secret_basic` (the Spring Security default) or `client_secret_post`. If your deployment prefers rotating OAuth2 tokens to static bearer keys, wire it via Spring Security: ```yaml application.yml theme={null} spring: security: oauth2: client: registration: omophub: client-id: ${OMOPHUB_CLIENT_ID} client-secret: unused # ignored by OMOPHub - see note below authorization-grant-type: client_credentials provider: omophub provider: omophub: token-uri: https://fhir.omophub.com/oauth2/token ``` OMOPHub's `/oauth2/token` endpoint is a minimal RFC 6749 `client_credentials` shim: it accepts either `client_secret_basic` (the Spring Security default) or `client_secret_post` client authentication, and it **does not validate `client_secret`** - your OMOPHub API key is the sole credential, passed as `client_id`. Spring's config schema requires `client-secret` to be non-empty, so supply any non-empty placeholder (`unused`, `not-required`, etc.); OMOPHub ignores the value either way. Then install an interceptor that fetches a token from the authorized-client manager and passes it as a Bearer header: ```java theme={null} @Bean public IClientInterceptor omophubOAuth2Interceptor( OAuth2AuthorizedClientManager clientManager ) { return (theRequest) -> { var principal = new AnonymousAuthenticationToken( "omophub", "omophub", List.of(new SimpleGrantedAuthority("ROLE_SERVICE")) ); var authorizedClient = clientManager.authorize( OAuth2AuthorizeRequest.withClientRegistrationId("omophub") .principal(principal) .build() ); String token = authorizedClient.getAccessToken().getTokenValue(); theRequest.addHeader("Authorization", "Bearer " + token); }; } ``` The token OMOPHub returns is just your API key wrapped in an RFC 6749 envelope (`{access_token, token_type, expires_in}`) - but the round trip does exercise token refresh behavior, which is useful for enterprise deployments that rotate credentials. ## 5. Verify It Works Walk through these curls against your HAPI server after startup. Every command should produce the expected output - if any fails, jump to [Troubleshooting](#7-troubleshooting). ### 5.1 HAPI itself is up ```bash theme={null} curl -sS http://localhost:8080/fhir/metadata | jq '.software' # Expected: { "name": "HAPI FHIR Server", "version": "..." } ``` ### 5.2 CodeSystem discovery reaches OMOPHub HAPI's validator calls the FHIR CodeSystem search-type endpoint to check which systems OMOPHub serves. You can trigger the same call HAPI would make: ```bash theme={null} curl -sS -X POST http://localhost:8080/fhir/CodeSystem/\$validate-code \ -H "Content-Type: application/fhir+json" \ -d '{ "resourceType": "Parameters", "parameter": [ { "name": "url", "valueUri": "http://loinc.org" }, { "name": "code", "valueCode": "2951-2" } ] }' | jq '.parameter' ``` Expected: ```json theme={null} [ { "name": "result", "valueBoolean": true }, { "name": "display", "valueString": "Sodium [Moles/volume] in Serum or Plasma" } ] ``` If you see `result: false` and a message like `"CodeSystem is unknown and can't be validated: http://loinc.org"`, the remote terminology service wasn't consulted - check the config path first, then the proxy's auth header (see troubleshooting). ### 5.3 Bogus code is rejected ```bash theme={null} curl -sS -X POST http://localhost:8080/fhir/CodeSystem/\$validate-code \ -H "Content-Type: application/fhir+json" \ -d '{ "resourceType": "Parameters", "parameter": [ { "name": "url", "valueUri": "http://loinc.org" }, { "name": "code", "valueCode": "99999-9" } ] }' | jq '.parameter' ``` Expected: ```json theme={null} [ { "name": "result", "valueBoolean": false }, { "name": "message", "valueString": "... Code '99999-9' not found in LOINC" } ] ``` HAPI forwards the exact error message OMOPHub returned, so you'll see "not found in LOINC" rather than a generic HAPI message - a quick visual confirmation that the remote server is in the loop. ### 5.4 SNOMED works the same way ```bash theme={null} curl -sS -X POST http://localhost:8080/fhir/CodeSystem/\$validate-code \ -H "Content-Type: application/fhir+json" \ -d '{ "resourceType": "Parameters", "parameter": [ { "name": "url", "valueUri": "http://snomed.info/sct" }, { "name": "code", "valueCode": "44054006" } ] }' | jq '.parameter' # Expected: result: true, display: "Type 2 diabetes mellitus" ``` ### 5.5 Direct check against OMOPHub As a sanity check independent of HAPI, curl OMOPHub directly (or through the proxy): ```bash theme={null} curl -sS https://fhir.omophub.com/fhir/r4/CodeSystem?url=http://loinc.org \ -H "Authorization: Bearer $OMOPHUB_CLIENT_ID" | jq '.entry[0].resource | {id, url, content}' # Expected: { "id": "loinc", "url": "http://loinc.org", "content": "not-present" } ``` ## 6. Supported Operations HAPI's `RemoteTerminologyServiceValidationSupport` consumes this subset of OMOPHub's FHIR Terminology Service: | Operation | OMOPHub endpoint | HAPI trigger | | ---------------------------- | --------------------------------------------- | ------------------------------------------------------------------ | | CodeSystem discovery | `GET /fhir/r4/CodeSystem?url=` | Every time HAPI first needs to resolve a system | | CodeSystem instance read | `GET /fhir/r4/CodeSystem/{id}` | HAPI follow-up after discovery | | Code validation | `GET/POST /fhir/r4/CodeSystem/$validate-code` | `$validate-code` operation + resource validation with coded fields | | Concept lookup | `GET/POST /fhir/r4/CodeSystem/$lookup` | `$lookup` operation + display-text resolution | | Cross-vocabulary translation | `GET/POST /fhir/r4/ConceptMap/$translate` | `$translate` operation | | Subsumption testing | `GET/POST /fhir/r4/CodeSystem/$subsumes` | `$subsumes` operation | | ValueSet expansion | `GET/POST /fhir/r4/ValueSet/$expand` | `$expand` operation, CQL engine, some validators | | ValueSet support check | `GET /fhir/r4/ValueSet?url=` | Pre-flight for `$expand` / `$validate-code` | Operations HAPI does **not** proxy automatically (but you can call them directly from application code): `$find-matches`, `$closure`, `$diff`, and FHIR Batch Bundles. See the [FHIR Integration guide](/guides/integration/fhir-integration) for the full operation surface including those. ### Version routing All the routes above accept an optional version prefix: * `/fhir/r4/...` - FHIR R4 (what HAPI JPA Starter uses by default) * `/fhir/r5/...` - FHIR R5 (point HAPI 7.x + `HAPI_FHIR_FHIR_VERSION: R5` here) * `/fhir/r6/...` - FHIR R6 (draft) Every response includes the vocabulary release that served it: ``` X-Vocab-Release: 2026.1 X-Vocab-Release-Status: active ``` Pin a specific release with the `X-Vocab-Release` request header or the `vocab_release` query parameter. See [Vocabulary Versions](/vocabulary-versions) for the full release history. ### Error envelope All 4xx/5xx responses from `/fhir/*` paths return a FHIR `OperationOutcome` resource (not the generic REST JSON envelope), so HAPI's validator parses them cleanly and surfaces the `diagnostics` field in its own logs. For example, a missing API key returns: ```json theme={null} { "resourceType": "OperationOutcome", "issue": [{ "severity": "error", "code": "login", "details": { "text": "Missing API key in the authorization header" }, "diagnostics": "missing_api_key" }] } ``` ## 7. Troubleshooting **`CodeSystem is unknown and can't be validated: http://loinc.org`** on every `$validate-code` HAPI's remote terminology chain never reached OMOPHub. Three likely causes: 1. **Config not loaded.** Check `docker exec env | grep SPRING_CONFIG_ADDITIONAL_LOCATION` and confirm the overlay file is mounted. HAPI silently ignores unknown config keys, so a typo (e.g. `remote_terminology` vs `remote_terminology_service`) yields no startup error - only the "unknown" validator error at query time. 2. **Auth failing at the proxy.** Run `docker logs ` and look for `401` responses from the upstream. Your `OMOPHUB_CLIENT_ID` env var may not be populated inside the nginx container - `envsubst` only substitutes variables that are actually exported. 3. **HAPI connecting but hitting a parse error.** Check HAPI's logs for `HAPI-1359: Failed to parse response`. If the stack trace mentions Woodstox / StAX / XML, HAPI's client fell into XML-parse mode because the `Accept` header negotiated XML first. OMOPHub prefers JSON on q-value ties, so this shouldn't happen with the stock HAPI client - but a custom `IGenericClient` with `setEncoding(EncodingEnum.XML)` will fail. Remove the XML encoding preference and it works. **`HAPI-1359: Failed to parse response ... HAPI-1684: Unknown resource name "resourceType"`** Classic XML-parser-reading-JSON error (the row/col coords refer to Woodstox output). OMOPHub's content negotiation returns JSON whenever the client advertises JSON support at equal-or-higher q-value than XML, which matches HAPI's default. If you're seeing this error, your HAPI client is either: * Configured to request XML only (`Accept: application/fhir+xml`), or * Forcing XML via `setEncoding(EncodingEnum.XML)` - switch to `JSON`. **`No live upstreams` from nginx after network restart** If the Docker network reshuffles IPs and nginx caches a stale upstream, restart the proxy container: `docker compose restart omophub-proxy`. Optionally add `resolver 127.0.0.11 valid=5s;` and `set` the upstream via a variable to force DNS re-resolution on every request. **`Unknown code system URI: http://example.com/unknown`** OMOPHub returned an `OperationOutcome` saying the URI isn't recognized. Check that the system URI in your HAPI request matches one of the [supported FHIR system URIs](/guides/integration/fhir-integration#3-authentication--endpoints). OMOPHub suggests the closest match in its error message - use that. ## 8. Next Steps The full OMOPHub FHIR Terminology Service surface - `$find-matches`, `$closure`, `$diff`, Batch Bundles, plus the OMOP FHIR Resolver for ETL pipelines. Point EHRbase at OMOPHub for template terminology validation. Uses the same FHIR endpoints as HAPI, with OAuth2 client\_credentials as the native auth path. CDS Hooks, SMART on FHIR apps, and sidecar architectures that resolve vocabularies at the point of care. # Batch & Performance Best Practices Source: https://docs.omophub.com/guides/production/batch-performance Optimize OMOPHub API usage for ETL pipelines, bulk lookups, and high-throughput workflows - deduplicate before mapping, batch, and cache stable data. If you're processing thousands or millions of records through OMOPHub, how you structure your API calls matters. This guide covers patterns that reduce your API usage, improve throughput, and keep your pipelines fast. ## 1. Rule 1: Deduplicate Before You Map This is the single most impactful optimization. A dataset with 10 million patient records might contain only 3,000 unique diagnosis codes. Map the 3,000, not the 10 million. ```python Python theme={null} import omophub import pandas as pd client = omophub.OMOPHub() # Load your source data df = pd.read_csv("patient_diagnoses.csv") print(f"Total rows: {len(df):,}") # 10,000,000 # Extract unique codes unique_codes = df["icd10_code"].dropna().unique().tolist() print(f"Unique codes: {len(unique_codes):,}") # 2,847 # Batch-resolve the unique codes - not the 10 million rows result = client.mappings.map( target_vocabulary="SNOMED", source_codes=[ {"vocabulary_id": "ICD10CM", "concept_code": code} for code in unique_codes ], ) # Build a {source_code: target_concept_id} cache mapping_cache = {} for m in result["data"]["mappings"]: mapping_cache[m["source_concept_code"]] = m["target_concept_id"] # Apply the cache to all 10M rows - zero additional API calls df["snomed_concept_id"] = df["icd10_code"].map(mapping_cache) ``` This turns 10 million API calls into \~30 calls (2,847 codes / 100 items per batch = 29 batches, each counting as one call). Same result, \~350,000x fewer calls. ## 2. Rule 2: Use Batch Endpoints OMOPHub provides batch and bulk endpoints that process multiple items in a single HTTP request. Each batch request counts as **one API call**, regardless of how many items are in the batch. Available batch and bulk endpoints: | Endpoint | Purpose | | --------------------------------------- | ----------------------------------------------------------------------- | | `POST /v1/concepts/batch` | Retrieve up to 100 concepts by ID | | `POST /v1/concepts/map/batch` | Map up to 100 source codes or source concept IDs to a target vocabulary | | `POST /v1/concepts/hierarchy/batch` | Batch ancestor / descendant lookups | | `POST /v1/concepts/relationships/batch` | Batch relationship queries | | `POST /v1/search/bulk` | Run multiple search queries in one request | | `POST /v1/search/semantic-bulk` | Batch semantic search with embeddings | | `POST /v1/fhir/resolve/batch` | FHIR Resolver batch: up to 100 codings per request | ```python Python theme={null} # Instead of this (N API calls): for concept_id in concept_ids: result = client.concepts.get(concept_id) # Do this (1 API call per 100 concepts): result = client.concepts.batch(concept_ids[:100]) for concept in result["data"]["concepts"]: print(f"{concept['concept_id']}: {concept['concept_name']}") ``` Batch endpoints accept up to 100 items per request. For larger inputs, chunk into groups of 100 and send the chunks sequentially or with controlled concurrency. Each chunk counts as one API call against your quota. ## 3. Rule 3: Cache What's Stable Vocabulary data changes only when OHDSI publishes a new ATHENA release (typically every 2 to 3 months). Concept metadata you look up today will return the same result tomorrow, next week, and next month until the next release. Design your caching accordingly: ```python Python theme={null} import json from pathlib import Path CACHE_FILE = Path("vocabulary_cache.json") def get_with_cache(client, concept_id): cache = json.loads(CACHE_FILE.read_text()) if CACHE_FILE.exists() else {} key = str(concept_id) if key in cache: return cache[key] # Cache miss, fetch from API result = client.concepts.get(concept_id) cache[key] = result CACHE_FILE.write_text(json.dumps(cache)) return result ``` For production ETL pipelines, consider: * **File cache** (JSON, SQLite) for single-machine pipelines * **Redis cache** with a TTL aligned to your vocabulary update cycle (60-90 days) * **Database table** (`source_to_concept_map`) for team-wide shared caches. See the [Collaborative Mapping](/guides/use-cases/collaborative-mapping) guide. The [Lean ETL Mapping Cache](/guides/use-cases/lean-etl-mapping-cache) guide walks through the full pattern: build a validated mapping cache with OMOPHub during development, apply it with local lookups in production. ## 4. Rule 4: Use the Right Search Endpoint Different search endpoints have different characteristics. Use the most specific one for your use case. | Endpoint | Best for | | ---------------------------------- | ---------------------------------------------------------------------------- | | `GET /v1/concepts/{concept_id}` | Direct lookup when you already have the OMOP concept ID | | `GET /v1/concepts/by-code` | Lookup by vocabulary code (e.g. ICD-10 `E11.9`) when you know the vocabulary | | `GET /v1/search/concepts` | Keyword / full-text search with filters | | `GET /v1/search/autocomplete` | Prefix matching for search-as-you-type UIs | | `GET /v1/concepts/semantic-search` | Natural-language or fuzzy matching via embeddings | If you have the concept ID, don't search by name. If you have the exact vocabulary code, use `by-code` instead of text search. Save semantic search for when the user query is ambiguous or phrased in natural language - it handles synonyms, abbreviations, and clinical descriptions, but is meaningfully slower than keyword search. ## 5. Rule 5: Build Autocomplete Responsibly If you're powering a search-as-you-type UI with OMOPHub: **Debounce aggressively.** Don't fire an API call on every keystroke. Wait 300-500ms after the user stops typing before sending the request. **Use the autocomplete endpoint.** `GET /v1/search/autocomplete` is optimized for prefix matching and returns faster than full search. **Set a minimum query length.** Don't search for single characters. Require at least 3 characters before triggering a search. **Cache recent results client-side.** If the user types "diab", gets results, then types "diabe", the "diab" result set is a superset of "diabe" matches and can be filtered locally without another request. ```javascript JavaScript theme={null} // Pseudocode for responsible autocomplete const DEBOUNCE_MS = 400; const MIN_CHARS = 3; const cache = new Map(); let debounceTimer; function onSearchInput(query) { clearTimeout(debounceTimer); if (query.length < MIN_CHARS) return; debounceTimer = setTimeout(async () => { if (cache.has(query)) return showResults(cache.get(query)); const res = await fetch( `https://api.omophub.com/v1/search/autocomplete?query=${encodeURIComponent(query)}`, { headers: { Authorization: `Bearer ${API_KEY}` } }, ); const data = await res.json(); cache.set(query, data); showResults(data); }, DEBOUNCE_MS); } ``` ## 6. Rule 6: Limit Hierarchy Depth Concept hierarchies can be deep. SNOMED `Is a` trees sometimes traverse 20+ levels for highly specialized terms. For most phenotype definitions, 3 to 5 levels is plenty. ```python Python theme={null} # Inefficient: unlimited depth ancestors = client.hierarchy.ancestors(201826) # Efficient: clinically relevant depth ancestors = client.hierarchy.ancestors( 201826, max_levels=5, vocabulary_ids=["SNOMED"], # narrow by vocabulary relationship_types=["Is a"], # narrow by relationship type ) ``` `max_levels` matters for both latency and for the number of concepts you pay to pull back. Cap it at the clinical depth you actually need. ## 7. Rule 7: Handle Errors and Retries OMOPHub returns standard HTTP status codes. Build retry logic for transient failures only: | Status | Meaning | Action | | ------ | ------------ | ------------------------------------------------- | | `200` | Success | Process response | | `400` | Bad request | Fix your request, do not retry | | `401` | Unauthorized | Check your API key | | `404` | Not found | Concept or code does not exist, do not retry | | `429` | Rate limited | Back off and retry after the `retry-after` header | | `5xx` | Server error | Retry with exponential backoff | ```python Python theme={null} import time def api_call_with_retry(fn, max_retries=3): for attempt in range(max_retries): try: return fn() except omophub.RateLimitError as e: wait = getattr(e, "retry_after", None) or (2 ** attempt) time.sleep(wait) except omophub.ServerError: time.sleep(2 ** attempt) raise RuntimeError("Max retries exceeded") ``` Never retry 400-level errors (except 429). A 400 means your request is malformed; retrying will produce the same result. A 404 means the concept or code does not exist in the vocabulary, and your code should flag it for manual review, not loop. ## 8. Putting It All Together: ETL Pipeline Pattern Here's the recommended shape for a production ETL pipeline: Pull a distinct list of codes from your source data. This is your mapping input, not the full patient dataset. Before hitting the API, check if you've already mapped each code in a previous run. Load your `source_to_concept_map` cache. For codes not in the cache, use the batch mapping endpoint (`POST /v1/concepts/map/batch`) or the FHIR Resolver batch (`POST /v1/fhir/resolve/batch`). Chunk into groups of 100. Write the new mappings back to your cache file or database table. Join the mapping cache against your source data via local lookup (pandas merge, SQL JOIN, dict lookup). No API calls needed for this step. Any source code with no mapping gets flagged for manual review. Do not silently drop records. This pattern means your first ETL run makes the most API calls, and every subsequent run makes fewer because the cache grows. By the third or fourth run, you're hitting the API only for genuinely new codes. For more on the full end-to-end pattern, see [Lean ETL Mapping Cache](/guides/use-cases/lean-etl-mapping-cache) and [Collaborative Mapping](/guides/use-cases/collaborative-mapping). # Interoperability Test Matrix Source: https://docs.omophub.com/guides/production/interop-test-matrix Supported FHIR operations, tested clients, auth patterns, and known integration scenarios for OMOPHub - check compatibility before writing code. This page documents what OMOPHub supports, what has been tested against real clients, and where the boundaries are. Use it to evaluate compatibility before writing integration code. ## 1. FHIR R4 / R4B / R5 / R6 Terminology Operations The FHIR Terminology Service is served at `https://fhir.omophub.com/fhir/{r4,r4b,r5,r6}/`. R4 is the default when no version prefix is provided. | Operation | Resource | Status | Notes | | -------------------------------- | -------------------------- | :--------: | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `$lookup` | CodeSystem | Production | GET and POST. Returns OMOP properties (`concept-id`, `domain-id`, `concept-class-id`, `standard-concept`, etc.) and Phoebe recommendations via `property=recommended` | | `$validate-code` | CodeSystem | Production | GET and POST. Validates code existence and optional `display` match | | `$subsumes` | CodeSystem | Production | Tests hierarchical subsumption between two concepts in the same system | | `$find-matches` | CodeSystem | Production | Semantic property-based matching - OMOPHub's semantic search exposed in a FHIR envelope | | `$translate` | ConceptMap | Production | GET and POST. Maps between any two vocabularies via OMOP `Maps to` relationships | | `$closure` | ConceptMap | Production | Incremental transitive-closure maintenance for streaming concept sets | | `$expand` | ValueSet | Production | Expands implicit ValueSets (`{system}?fhir_vs` and `{system}?fhir_vs=isa/{code}`). Deterministic sort by `concept_id` for stable pagination | | `$validate-code` | ValueSet | Production | Validates code membership in an implicit ValueSet | | `$diff` | CodeSystem | Production | **OMOPHub custom** - compares concepts between two OMOP vocabulary releases. No FHIR spec equivalent | | `GET /metadata` | CapabilityStatement | Production | Returns declared resources, operations, and both JSON + XML formats | | `GET /metadata?mode=terminology` | TerminologyCapabilities | Production | Returns the full list of supported `codeSystem` URIs dynamically from the vocab config | | `GET /CodeSystem?url=` | CodeSystem (search-type) | Production | HAPI FHIR's discovery call - returns a `searchset` `Bundle` with a per-vocabulary stub or empty Bundle for unknown URIs | | `GET /CodeSystem` (list-all) | CodeSystem (search-type) | Production | Returns all 20 per-vocab stubs + the unified OMOP omnibus CodeSystem | | `GET /CodeSystem/{id}` | CodeSystem (instance read) | Production | Per-vocab stub IDs (`loinc`, `snomed`, `rxnorm`, …) + release IDs (`omop-v20250827`) | | `GET /ValueSet?url=` | ValueSet (search-type) | Production | Preflight support check used by HAPI and EHRbase before `$expand` / `$validate-code` | | `POST /` (Batch Bundle) | Bundle | Production | FHIR batch Bundle with multiple `entry.request` items; metered per entry, not per call | ### Not supported | Capability | Reason | | ------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | FHIR clinical resources (`Patient`, `Condition`, `Observation`, …) | OMOPHub is a terminology service, not a clinical-data server | | FHIR REST interactions (`create`, `update`, `delete`, `history`, `patch`) | Read-only vocabulary data - content comes from OHDSI ATHENA releases | | Extensional ValueSet storage | Only implicit ValueSets (`{system}?fhir_vs`). User-defined extensional ValueSets are on the roadmap | | Full-fidelity XML Bundle serialization | Parameters, OperationOutcome, and single-resource responses serialize to clean FHIR XML. Bundle-shaped responses (search-type, batch) are best-effort - see the [XML support matrix](/api-reference/fhir-terminology/overview#xml-support-matrix) | ## 2. OMOPHub Concept Resolver Purpose-built REST endpoints for FHIR-to-OMOP ETL pipelines. Returns OMOPHub's native JSON envelope rather than FHIR `Parameters`. | Endpoint | Description | | ---------------------------------------- | ------------------------------------------------------------------------------------------------------------------ | | `POST /v1/fhir/resolve` | Single `Coding` → OMOP standard concept + domain + CDM target table + mapping type | | `POST /v1/fhir/resolve/batch` | Batch resolution, up to 100 codings per request. Metered per coding | | `POST /v1/fhir/resolve/codeable-concept` | Full `CodeableConcept` resolution with OHDSI vocabulary preference ranking and `text` fallback via semantic search | See the [FHIR Integration guide](/guides/integration/fhir-integration) for request / response shapes and the [FHIR → OMOP Standardization](/guides/workflows/fhir-to-omop) guide for the end-to-end pipeline pattern. ## 3. Supported FHIR Code System URIs OMOPHub maps 19 canonical FHIR system URIs + 1 OMOP omnibus URL (total: **20 CodeSystems** served via the Terminology Service). Source of truth is `apps/api/src/config/fhir-vocabularies.ts` - use `GET /fhir/r4/metadata?mode=terminology` to get the current list from the live service. | FHIR System URI | OMOP `vocabulary_id` | | --------------------------------------------- | -------------------- | | `http://snomed.info/sct` | `SNOMED` | | `http://loinc.org` | `LOINC` | | `http://www.nlm.nih.gov/research/umls/rxnorm` | `RxNorm` | | `http://hl7.org/fhir/sid/icd-10` | `ICD10` | | `http://hl7.org/fhir/sid/icd-10-cm` | `ICD10CM` | | `http://hl7.org/fhir/sid/icd-10-pcs` | `ICD10PCS` | | `http://hl7.org/fhir/sid/icd-10-cn` | `ICD10CN` | | `http://hl7.org/fhir/sid/icd-10-gm` | `ICD10GM` | | `http://hl7.org/fhir/sid/icd-9-cm` | `ICD9CM` | | `http://hl7.org/fhir/sid/icd-9` | `ICD9Proc` | | `http://hl7.org/fhir/sid/icd-9-cn` | `ICD9ProcCN` | | `http://hl7.org/fhir/sid/icd-o-3` | `ICDO3` | | `http://hl7.org/fhir/sid/cvx` | `CVX` | | `http://hl7.org/fhir/sid/ndc` | `NDC` | | `http://www.nlm.nih.gov/research/umls/hcpcs` | `HCPCS` | | `http://www.whocc.no/atc` | `ATC` | | `http://unitsofmeasure.org` | `UCUM` | | `urn:iso:std:iso:11073:10101` | `MDC` | | `http://fdasis.nlm.nih.gov` | `UNII` | | `http://va.gov/terminology/medrt` | `MED-RT` | | `https://fhir-terminology.ohdsi.org` | OMOP unified omnibus | If your FHIR data uses a system URI not in this list, the Resolver returns a `400` error with an `unknown_system` code and a suggested match from the levenshtein-closest URI. You can bypass URI resolution by passing `vocabulary_id` directly instead of `system`. ## 4. Tested Client Scenarios The following integration patterns have been validated against real clients. ### HAPI FHIR Server HAPI FHIR's `RemoteTerminologyServiceValidationSupport` can delegate validation to OMOPHub. **Two deployment shapes** work, depending on whether you're using the stock JPA Starter or building a custom Spring Boot HAPI app: **HAPI JPA Starter (`hapiproject/hapi:*` Docker image)** - the stock image's YAML schema does not expose a Bearer-token field for remote terminology services (`AppProperties.RemoteSystem` in the starter only binds `system` and `url`; `TerminologyConfig` never calls `addClientInterceptor`). Use a reverse-proxy pattern: * Stand up nginx / Caddy / cloud gateway in front of OMOPHub * Configure the proxy to inject `Authorization: Bearer $OMOPHUB_CLIENT_ID` on every upstream request * Point HAPI's remote terminology URL at the proxy * Config key: `hapi.fhir.remote_terminology_service..{system,url}` - note that older blog posts reference `hapi.fhir.validation.remote_terminology_service_urls`, which is a different property path that the JPA Starter silently ignores **Custom Spring Boot HAPI app** - use the HAPI FHIR library directly and attach a `BearerTokenAuthInterceptor` programmatically: ```java theme={null} @Bean public IValidationSupport omophubTerminologyService(FhirContext ctx) { RemoteTerminologyServiceValidationSupport remote = new RemoteTerminologyServiceValidationSupport(ctx); remote.setBaseUrl("https://fhir.omophub.com/fhir/r4"); remote.addClientInterceptor( new BearerTokenAuthInterceptor("oh_your_api_key")); return remote; } ``` * **Verified operations:** `$lookup`, `$validate-code`, CodeSystem search-type (discovery call), `GET /metadata` * **Verified against:** HAPI FHIR 8.8.0 * **See:** [HAPI FHIR Integration](/guides/integration/hapi-fhir) for full config, verification checklist, and the `CodeSystem is unknown` troubleshooting path ### EHRbase (openEHR) EHRbase delegates template-terminology validation to an external FHIR R4 server via Spring's `RemoteTerminologyServiceValidationSupport`. Unlike HAPI JPA Starter, EHRbase supports OAuth2 `client_credentials` natively via `spring.security.oauth2.client.*` config blocks - and OMOPHub exposes a compatible token endpoint at `POST /oauth2/token`. * **Connection:** Configure `validation.external-terminology.provider.omophub.url` to `https://fhir.omophub.com/fhir/r4/` * **Auth:** OAuth2 `client_credentials` - `spring.security.oauth2.client.registration.omophub.client-id` set to your OMOPHub API key, `client-secret` to any non-empty placeholder (OMOPHub's shim doesn't validate it) * **Verified operations:** ValueSet search-type (discovery), `$expand`, `$validate-code`, OAuth2 token exchange * **Verified against:** `ehrbase/ehrbase:next` with `application.yml` overlay * **End-to-end test result:** invalid SNOMED code submitted via composition → EHRbase returns HTTP 422 with `"does not match any option from value set ..."` - the full `template upload → composition commit → $validate-code → reject` loop is live * **See:** [EHRbase / openEHR Integration](/guides/integration/ehrbase-openehr) for the complete docker-compose and template-binding walkthrough ### Direct REST / curl All FHIR operations support both GET (query parameters) and POST (FHIR `Parameters` body): ```bash theme={null} # GET style curl "https://fhir.omophub.com/fhir/r4/CodeSystem/\$lookup?\ system=http://snomed.info/sct&code=44054006" \ -H "Authorization: Bearer oh_your_api_key" # POST style curl -X POST "https://fhir.omophub.com/fhir/r4/CodeSystem/\$lookup" \ -H "Authorization: Bearer oh_your_api_key" \ -H "Content-Type: application/fhir+json" \ -d '{ "resourceType": "Parameters", "parameter": [ { "name": "system", "valueUri": "http://snomed.info/sct" }, { "name": "code", "valueCode": "44054006" } ] }' ``` ### Python SDK ```python theme={null} import omophub client = omophub.OMOPHub() # Concept Resolver result = client.fhir.resolve( system="http://snomed.info/sct", code="44054006", resource_type="Condition", ) # Batch resolve up to 100 codings batch = client.fhir.resolve_batch([ {"system": "http://snomed.info/sct", "code": "44054006"}, {"system": "http://loinc.org", "code": "2339-0"}, ]) # CodeableConcept with OHDSI vocabulary preference ranking cc = client.fhir.resolve_codeable_concept( coding=[ {"system": "http://snomed.info/sct", "code": "44054006"}, {"system": "http://hl7.org/fhir/sid/icd-10-cm", "code": "E11.9"}, ], resource_type="Condition", ) ``` ### R SDK R6 object-oriented style via `$`-dereferencing: ```r theme={null} library(omophub) client <- OMOPHubClient$new(api_key = "oh_your_api_key") result <- client$fhir$resolve( system = "http://snomed.info/sct", code = "44054006", resource_type = "Condition" ) ``` ### MCP Server (AI agents) The OMOPHub MCP Server exposes 11 tools to Claude, Cursor, VS Code, and any MCP-compatible AI client. Install with `npx -y @omophub/omophub-mcp`. See [AI Onboarding](/ai/onboarding) and the [MCP Tools Reference](/ai/mcp-tools) for the full tool list. ## 5. Authentication | Pattern | Supported | Notes | | --------------------------------------------- | :--------: | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Bearer token (`Authorization: Bearer oh_xxx`) | Production | Primary auth for REST and FHIR endpoints. Transmitted over TLS 1.2+ | | OAuth2 `client_credentials` (RFC 6749) | Production | `POST https://fhir.omophub.com/oauth2/token`. Accepts both `client_secret_basic` (Spring Security default) and `client_secret_post`. `client_secret` is not validated - the `client_id` (your API key) is the sole credential | | API key in query parameter | No | Header-only for security | | No auth / open access | No | All endpoints require authentication | The OAuth2 shim is specifically designed to satisfy Spring Security OAuth2 clients (HAPI FHIR custom builds, EHRbase, any Java/Kotlin Spring Boot stack). The returned `access_token` is your API key wrapped in an RFC 6749 envelope - the token exchange is nominal but lets Spring-based clients treat OMOPHub like any other OAuth2-protected resource. ## 6. Content Negotiation | Content Type | Supported | Notes | | ----------------------- | :--------: | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `application/fhir+json` | Production | Default | | `application/json` | Production | Treated as FHIR JSON | | `application/fhir+xml` | Production | Served when strictly preferred via `Accept` header q-values. Clean for `Parameters`, `OperationOutcome`, `CapabilityStatement`, `ValueSet`, `ConceptMap`, bare `CodeSystem` reads. **Bundle-shaped responses** (CodeSystem/ValueSet search-type, batch Bundles) are best-effort - request JSON for those endpoints | | `*/*` | Production | Resolves to JSON via wildcard content negotiation | JSON wins on ties. Content negotiation follows RFC 7231 §5.3.2 q-value semantics; HAPI FHIR's default client sends both JSON and XML at q=1.0, so the tie-break selects JSON. See the [Content Type section](/api-reference/fhir-terminology/overview#content-type) of the FHIR Terminology Service overview for the full XML support matrix. ## 7. Rate Limits FHIR endpoints share the same rate-limit and metering pool as the REST API. Each FHIR operation counts as one API call. Batch and bulk endpoints (`POST /v1/fhir/resolve/batch`, `POST /fhir/r4/` batch Bundles, `/v1/concepts/batch`, `/v1/search/bulk`, and so on) meter per item inside the batch - a 100-item batch counts as 100 calls against your monthly quota. See [Rate Limits](/api-reference/rate-limit) for the headers (`ratelimit-limit`, `ratelimit-remaining`, `ratelimit-reset`, `ratelimit-window`, `retry-after`) and the contact path for requesting higher limits. ## 8. Vocabulary Coverage OMOPHub serves the full OHDSI ATHENA vocabulary set except licensed vocabularies. See [Known Limitations](/guides/production/known-limitations) for the full boundary discussion, and [Vocabulary Releases](/vocabulary-versions) for the current release cadence. # Known Limitations Source: https://docs.omophub.com/guides/production/known-limitations What OMOPHub does not do - explicit product boundaries around write operations, custom vocabularies, and PHI so you can plan your integration safely. OMOPHub covers a lot of ground, but knowing what it *doesn't* do is just as important for planning your integration. This page documents explicit boundaries so there are no surprises in production. ## 1. Vocabulary Coverage **CPT4 is excluded.** The AMA requires a commercial license to distribute CPT4 codes. OMOPHub does not include CPT4 content. Queries against `http://www.ama-assn.org/go/cpt` return a 403 `OperationOutcome` on the FHIR Terminology Service and a `forbidden` error on the REST API. If your workflow requires CPT4, license it separately from the AMA and manage it in local vocabulary tables. **MedDRA is excluded.** MedDRA licensing restrictions apply similarly. MedDRA concepts are not available through the API. **UMLS-restricted vocabularies.** Some vocabularies in ATHENA require a UMLS license for download. OMOPHub serves the subset of ATHENA vocabularies that can be distributed without per-user UMLS licensing. If your workflow needs vocabularies that require UMLS access, check the OHDSI ATHENA download page for the specific license requirements. SNOMED CT is available through OMOPHub. SNOMED International member countries - including the US, UK, EU member states, Australia, and many others - have national licenses that cover the use of SNOMED CT content. If you're in a non-member country, check your local SNOMED licensing requirements before deploying. ## 2. FHIR Terminology Service **Not a full FHIR server.** OMOPHub implements FHIR terminology operations (`$lookup`, `$translate`, `$validate-code`, `$expand`, `$subsumes`, `$find-matches`, `$closure`) plus the FHIR Concept Resolver and the OMOP-custom `$diff` operation. It does **not** implement FHIR clinical resources (`Patient`, `Observation`, `Condition`, etc.), FHIR REST interactions (create, update, delete, history, patch), or the full FHIR search specification. It is a terminology service, not an EHR backend. **Implicit ValueSets only.** OMOPHub supports implicit ValueSets derived from OMOP vocabulary structure - e.g. `http://snomed.info/sct?fhir_vs` for all SNOMED concepts or `http://snomed.info/sct?fhir_vs=isa/73211009` for descendants of a specific concept. User-defined extensional ValueSets (upload and store a custom value set by `id`) are **not currently supported**. This is on the roadmap. **XML is best-effort for Bundle-shaped responses.** Content negotiation serves `application/fhir+xml` when the client strictly prefers it, and the serializer produces clean FHIR XML for `Parameters`, `OperationOutcome`, `ConceptMap`, `ValueSet`, and bare `CodeSystem` resources. **Bundle-shaped responses** - `GET /CodeSystem?url=...`, `GET /ValueSet?url=...`, and batch Bundles - return malformed XML for nested resources; request JSON for those endpoints. See the [XML support matrix](/api-reference/fhir-terminology/overview#xml-support-matrix) for the per-endpoint breakdown. **Pre-auth errors come back as JSON.** When a FHIR request fails authentication (401) or exhausts the monthly quota (429) before reaching the content-type middleware, the error response is an `OperationOutcome` but serialized as JSON regardless of the `Accept` header. Handler-emitted errors (404, 400, 403, 5xx from inside an operation) do honor the Accept header. See the [Content Type](/api-reference/fhir-terminology/overview#content-type) section of the FHIR overview for the full rules. ## 3. API Behavior **Read-only.** OMOPHub is a vocabulary lookup service. You cannot write, update, or delete concepts, relationships, or mappings through the API. All vocabulary content comes from OHDSI ATHENA releases. **No custom mapping storage.** You cannot upload your own source-to-concept mappings to OMOPHub. The [Collaborative Mapping guide](/guides/use-cases/collaborative-mapping) shows how to build and export `source_to_concept_map` files locally using OMOPHub lookups, but the mappings themselves are stored on your side. **Rate limits apply.** The default rate limit is **2 requests per second** per API key. Monthly quotas vary by plan. See [Rate Limits](/api-reference/rate-limit) for the headers (`ratelimit-limit`, `ratelimit-remaining`, `ratelimit-reset`) and the contact path for requesting higher limits. **Batch endpoints count as one call.** Batch and bulk endpoints (`/v1/concepts/batch`, `/v1/search/bulk`, `/v1/search/semantic-bulk`, `/v1/concepts/map/batch`, `/v1/concepts/hierarchy/batch`, `/v1/concepts/relationships/batch`, `/v1/fhir/resolve/batch`) count as a single API call regardless of the number of items in the request. Use them - see [Batch & Performance](/guides/production/batch-performance). **No real-time vocabulary updates.** Vocabulary content is updated when OHDSI publishes a new ATHENA release (typically every 2–3 months). There is no live feed of vocabulary changes. See [Vocabulary Releases](/vocabulary-versions) for the current release and update cadence, and [Vocabulary Lifecycle Management](/guides/use-cases/vocabulary-lifecycle-management) for the detection pattern. ## 4. Infrastructure **SaaS only.** OMOPHub is a hosted service. There is no on-premise or self-hosted deployment option. All API calls go to `https://api.omophub.com` and `https://fhir.omophub.com`. If your security requirements prohibit sending vocabulary queries to an external service, OMOPHub may not be a fit - though note that vocabulary queries contain medical terminology codes, not patient data. See [Security & Data Handling](/guides/production/security-data-handling) for what does and doesn't flow through the API. **No offline mode.** The API requires an internet connection. For air-gapped production environments, the [Lean ETL Mapping Cache](/guides/use-cases/lean-etl-mapping-cache) guide shows a hybrid approach: build and validate mappings with OMOPHub during development, apply them offline during production. ## 5. What's on the Roadmap Some of the limitations above are temporary. Here's what's planned: * **User-defined ValueSets** - upload and expand custom value sets by canonical URL * **JavaScript SDK** - npm-distributed SDK for browser and Node.js (MCP Server ships on npm today, but that's a different surface) * **Expanded FHIR XML support** - proper nested-resource serialization for Bundle responses Have a limitation that's blocking your adoption? Reach out via [omophub.com/contact](https://omophub.com/contact) - real-world use cases drive the roadmap. # Security & Data Handling Source: https://docs.omophub.com/guides/production/security-data-handling What data OMOPHub receives and stores, plus how the API handles authentication, logging, and privacy - written for security reviewers and developers. This page explains what data flows through OMOPHub, what gets stored, and how the API handles authentication and privacy. It's written for security reviewers, compliance officers, and developers evaluating OMOPHub for production use. ## 1. The Core Principle: No PHI OMOPHub is a vocabulary lookup service. It receives medical terminology codes and concept identifiers. It does **not** receive, process, or store Protected Health Information (PHI). A typical API call looks like this: ```bash theme={null} curl https://api.omophub.com/v1/concepts/201826 \ -H "Authorization: Bearer oh_xxxxxxxxx" ``` The request contains an OMOP concept ID. The response contains vocabulary metadata - concept names, codes, domains, relationships. No patient identifiers, no clinical records, no dates of service, no free-text notes. Do not include PHI in API requests. OMOPHub endpoints accept concept IDs, vocabulary codes, and search terms. If your workflow involves patient data, resolve the vocabulary codes *before* or *after* the patient-data processing step - never send patient-linked data to the API. ## 2. Authentication All API access requires a Bearer token in the `Authorization` header: ``` Authorization: Bearer oh_xxxxxxxxxxxxxxxxxxxx ``` API keys are: * **Per-user** - each key is tied to a specific account * **Revocable** - deactivate a key at any time from the dashboard * **Scopeable** - create separate keys for different environments (dev, staging, production) API keys are transmitted over HTTPS (TLS 1.2+). All API endpoints enforce HTTPS - plain HTTP requests are rejected. The FHIR Terminology Service additionally accepts OAuth2 `client_credentials` via `POST https://fhir.omophub.com/oauth2/token` for Spring Security OAuth2 clients (HAPI FHIR JPA Starter, EHRbase). The token endpoint accepts both `client_secret_basic` and `client_secret_post` methods. See the [FHIR Terminology Service authentication](/api-reference/fhir-terminology/overview#authentication) section for details. ## 3. What OMOPHub Stores **About you.** Email address, account metadata, API key hashes (not plaintext keys), and usage metrics (call counts, endpoint distribution). This data is used for authentication, billing, and service improvement. **About your requests.** OMOPHub logs API request metadata for operational purposes: timestamp, endpoint path, response status, latency, and API key identifier. Request parameters (search terms, concept IDs, vocabulary filters) may be logged for debugging and service quality. These logs are retained for operational purposes and are not shared with third parties. **About vocabulary content.** OMOPHub hosts OHDSI ATHENA vocabulary data - concepts, relationships, and mappings. This is public reference data published by OHDSI, not customer data. ## 4. What OMOPHub Does NOT Store * Patient data or PHI * Clinical records or EHR data * IP addresses of end-users of your application * Your application's source code or configuration * Custom mappings or transformation logic (those stay on your side - see [Lean ETL Mapping Cache](/guides/use-cases/lean-etl-mapping-cache)) ## 5. Encryption * **In transit:** All API traffic is encrypted via TLS 1.2 or higher * **At rest:** Data is encrypted using the hosting platform's default encryption (AES-256) ## 6. Infrastructure OMOPHub runs on Google Cloud Platform. The runtime surface is a managed container service with zero-downtime. ## 7. GDPR OMOPHub processes limited personal data (email, usage metrics) under GDPR. The Privacy Policy and Data Use Agreement cover data-processing details. Users can request data export or deletion via [omophub.com/contact](https://omophub.com/contact). ## 8. Compliance Considerations OMOPHub is a vocabulary reference service, not a clinical data processor. For most healthcare organizations: * **HIPAA:** Because OMOPHub does not receive PHI, it typically does not require a Business Associate Agreement (BAA). If your workflow architecture routes PHI through API calls (which it should not), contact us to discuss your specific setup. * **SOC 2:** Not currently certified. OMOPHub follows security best practices but has not undergone a formal SOC 2 audit. * **GDPR:** Compliant for the limited personal data processed (see §7 above). If your organization requires a formal security review or vendor questionnaire, reach out via [omophub.com/contact](https://omophub.com/contact) with your requirements. # Why OMOPHub vs Self-Hosting Source: https://docs.omophub.com/guides/production/why-omophub An honest comparison of using OMOPHub versus downloading ATHENA and running your own OMOP vocabulary database, with guidance on when each choice fits. Every OMOP team faces this question: should we download ATHENA and run our own vocabulary database, or use an API? This page gives you an honest comparison so you can decide what fits your situation. ## 1. The Self-Hosting Path The traditional approach: 1. Go to [athena.ohdsi.org](https://athena.ohdsi.org) and request a vocabulary download 2. Wait for the download link (can take hours to days) 3. Download 3–5 GB of CSV files 4. Set up a PostgreSQL database with the OMOP vocabulary schema 5. Load the CSVs (typically 30–60 minutes depending on hardware) 6. Build indexes for acceptable query performance (another 30–60 minutes) 7. Write SQL queries or build a service layer on top 8. Repeat steps 1–6 every time OHDSI publishes a new release This works. Thousands of OHDSI sites run this way. But it has real costs. ## 2. Where Self-Hosting Gets Expensive **Setup time is not zero.** A senior data engineer typically spends 1–2 days on initial setup, including schema creation, CSV loading, index tuning, and basic query testing. For teams new to OMOP, it can take a week. **Maintenance is ongoing.** ATHENA publishes vocabulary updates every 6 months. Each update means re-downloading, re-loading, re-indexing, and regression testing. Teams that skip updates end up with stale vocabularies - deprecated concepts, missing new codes, broken mappings. See [Vocabulary Lifecycle Management](/guides/use-cases/vocabulary-lifecycle-management) for the pattern to stay current. **No search out of the box.** ATHENA CSVs give you tables, not a search engine. Building fuzzy search, autocomplete, or semantic similarity requires additional tooling - Elasticsearch, custom indexing, neural embedding models. Most teams never build this, so they're stuck with exact-match SQL queries. **No API without building one.** If your ETL scripts, FHIR server, LLM pipeline, or frontend application need vocabulary access, you have to build and maintain a REST API on top of your database. That's a web framework, auth, rate limiting, caching, monitoring, and deployment - for every team, from scratch. **Scales with your team, not your problem.** Every new developer, every new project, every new environment needs access to the vocabulary database. That means either shared database access (operationally risky) or multiple copies (expensive and prone to version drift). ## 3. What OMOPHub Gives You Instead | Capability | Self-hosted ATHENA | OMOPHub | | ------------------------------------------------- | ------------------------------------------ | ------------------------------------------------------------------------------------------------------------------ | | Setup time | 1–2 days | 5 minutes (get an API key) | | Vocabulary updates | Manual re-download and re-load | Automatic, synced with ATHENA releases | | Full-text search | Build your own | Built-in | | Semantic search | Build your own (need an embedding model) | Built-in (neural embeddings) | | Autocomplete | Build your own | Built-in | | REST API | Build your own | Built-in | | Python SDK | Build your own | `pip install omophub` | | R SDK | Build your own | `install.packages("omophub")` | | MCP Server for AI agents | Build your own | `npx -y @omophub/omophub-mcp` | | FHIR Terminology Service | Build your own or deploy Echidna/Snowstorm | Built-in (`$lookup`, `$translate`, `$validate-code`, `$expand`, `$subsumes`, `$find-matches`, `$closure`, `$diff`) | | FHIR Concept Resolver (Coding → OMOP + CDM table) | Not a standard OHDSI tool; build your own | Built-in (`POST /v1/fhir/resolve`) | | Batch operations | SQL | Built-in batch endpoints - see [Batch & Performance](/guides/production/batch-performance) | | Phoebe recommendations | Requires separate setup | Built-in via `property=recommended` on `$lookup` | | Infrastructure cost | \$150–400/month (database + compute) | Free tier available; paid tiers for higher volume | | Maintenance burden | Ongoing | Zero | ## 4. When Self-Hosting Still Makes Sense OMOPHub is not the right choice for every situation: * **Air-gapped environments** where no external API calls are permitted. Though the [Lean ETL Mapping Cache](/guides/use-cases/lean-etl-mapping-cache) guide shows a hybrid approach - use OMOPHub during development, cache the results, deploy locally. * **Custom vocabulary extensions** where you've added proprietary concepts to your local OMOP vocabulary tables. OMOPHub serves standard ATHENA content only. * **Extremely high volume** workloads that exceed API rate limits and where latency requirements demand sub-millisecond local lookups. For most ETL workloads, the batch endpoints and caching strategies in [Batch & Performance](/guides/production/batch-performance) handle this comfortably. * **Regulatory requirements** that explicitly prohibit sending vocabulary queries to an external service, even when no PHI is involved. See [Security & Data Handling](/guides/production/security-data-handling) for what actually flows through the API - spoiler: vocabulary codes, not patient data. ## 5. The Hybrid Approach Many teams use both. OMOPHub for development, exploration, and ETL building - with a local vocabulary cache for production execution. The [Lean ETL Mapping Cache](/guides/use-cases/lean-etl-mapping-cache) guide walks through this pattern in detail. This gives you the best of both worlds: fast iteration with OMOPHub's search and mapping capabilities during development, and zero external dependencies in production. # Clinical Coding Automation Source: https://docs.omophub.com/guides/use-cases/clinical-coding Automate clinical coding workflows with OMOPHub - go from raw clinical text to standardized OMOP concepts for billing, documentation, and analytics. ## 1. The "Manual Mapping" Tax If you've ever spent an entire afternoon staring at a spreadsheet, trying to figure out which SNOMED code matches your hospital's local lab entry for "GLUC\_FAST\_SER" - you know the pain. It's a hidden tax on your time. And it compounds. Traditional ETL processes for converting raw healthcare data into the OMOP Common Data Model (CDM) are slow, expensive, and riddled with human error. Every local code, every proprietary label, every physician shorthand needs to be manually mapped to a standardized vocabulary. Multiply that across thousands of concepts, and you've got weeks of work that doesn't even feel like real work. But here's the thing: **most of that mapping is repetitive and predictable.** And predictable work is exactly the kind of work that should be automated. That's where **OMOPHub** comes in. It's a vocabulary API that gives you instant, programmatic access to the OHDSI ATHENA vocabularies: SNOMED, ICD-10, LOINC, RxNorm, and 100+ others - without needing to set up and maintain a local PostgreSQL database. Combine it with NLP tools or LLMs for the text extraction step, and you've got a powerful end-to-end clinical coding pipeline. The goal? Shift from being a data cleaner to a clinical researcher. Let the tools handle the 80%, so you can focus your expertise on the 20% that actually matters. ## 2. The Core Concept: From Raw Text to Standardized Concepts At its heart, clinical coding automation is about bridging the gap between diverse, messy source data and the structured world of the OMOP CDM. Think of it as a two-stage translation process: 1. **Extract:** Use NLP tools (like MedCAT, cTAKES, Amazon Comprehend Medical, or an LLM) to identify clinical entities from unstructured or semi-structured text - conditions, medications, procedures, lab tests. 2. **Map & Validate:** Use OMOPHub's vocabulary API to translate those extracted entities into standardized OMOP concept IDs (SNOMED, ICD-10, LOINC, etc.), and review the results with confidence metrics for human-in-the-loop quality assurance. **Why OMOPHub for the mapping step?** Traditionally, querying OMOP vocabularies meant downloading multi-gigabyte ATHENA CSV files and loading them into your own database. OMOPHub eliminates that overhead. Install the Python SDK, add your API key, and start searching concepts, traversing hierarchies, and building mappings in minutes. It covers all major OHDSI vocabularies with bi-annual updates, batch operations, and healthcare-grade security. It's not an NLP engine. It's the vocabulary backbone that makes your NLP pipeline clinically accurate. ## 3. Use Case A: Mapping Entities from Physician Notes (Unstructured Data) Imagine you're a clinical researcher tasked with analyzing thousands of physician notes to identify patients with specific conditions. Manually reading those notes is impractical. But even after you run NLP extraction, you still need to link extracted terms to standardized OMOP concepts. **The Scenario:** A researcher uses an NLP tool to extract clinical entities from free-text physician notes, then uses OMOPHub to map those entities to standard SNOMED or ICD-10 concepts. **The Two-Step Workflow:** * **Step 1:** An NLP tool (e.g., MedCAT, an LLM, or Amazon Comprehend Medical) processes the note and extracts entities like "acute myocardial infarction," "Type 2 Diabetes Mellitus," and "hypertension." * **Step 2:** OMOPHub's search API maps each extracted entity to the correct OMOP concept ID. **Code Snippet: Looking Up Extracted Entities via OMOPHub** First, install the OMOPHub Python SDK: ```bash theme={null} pip install omophub ``` Now, let's see how to use it: ```python Python theme={null} import omophub # Initialize the OMOPHub client with your API key # Set OMOPHUB_API_KEY as an environment variable client = omophub.OMOPHub() physician_note = """ Patient presented with severe chest pain radiating to the left arm, shortness of breath, and diaphoresis. Initial ECG showed ST elevation in leads V2-V4. Suspected acute myocardial infarction. Past medical history includes Type 2 Diabetes Mellitus and hypertension. """ # Step 1: Extract entities using your NLP tool of choice. # This is a simplified placeholder - in production you'd use # MedCAT, cTAKES, Amazon Comprehend Medical, SciSpacy, or an LLM. extracted_conditions = [ "acute myocardial infarction", "Type 2 Diabetes Mellitus", "hypertension", "chest pain", "shortness of breath", "diaphoresis", ] # Step 2: Map each extracted entity to OMOP concepts via OMOPHub print("\nMapping extracted conditions to OMOP concepts:") for condition in extracted_conditions: try: results = client.search.basic( query=condition, vocabulary_ids=["SNOMED", "ICD10CM"], standard_concept="S", # Only standard concepts page_size=3, ) if results and results.get("data"): top_match = results["data"][0] concept_name = top_match.get("concept_name", "N/A") concept_id = top_match.get("concept_id", "N/A") vocabulary_id = top_match.get("vocabulary_id", "N/A") print( f" '{condition}' -> {concept_name} " f"(ID: {concept_id}, Vocab: {vocabulary_id})" ) else: print(f" '{condition}' -> No standard concept found") except omophub.APIError as e: print(f" API Error for '{condition}': {e.status_code}") except Exception as e: print(f" Unexpected error for '{condition}': {e}") ``` **The Key Insight:** For large-scale research, "perfect" manual extraction is an illusion. The sheer volume of data makes it impractical and introduces its own inconsistencies. An automated NLP + vocabulary lookup pipeline provides a **consistent, scalable baseline**. You get 80% of the way there with a fraction of the effort, and then apply human expertise to the edge cases. That's the leverage. ## 4. Use Case B: Scaling Lab Code Mapping (Semi-Structured Data) Beyond unstructured notes, healthcare systems are full of local, proprietary codes for lab tests, medications, and procedures. Mapping these to standard vocabularies like LOINC or RxNorm is foundational for data interoperability. **The Scenario:** A data engineer needs to map local lab test names to their corresponding LOINC concept IDs. **The Logic:** For semi-structured data where you already have specific local names (not free text), OMOPHub's search and mapping APIs are ideal. Search for each local code by name, find the best standard concept match, then retrieve its cross-vocabulary mappings if needed. **Code Snippet: Mapping Local Lab Codes to LOINC** ```python Python theme={null} import omophub client = omophub.OMOPHub() local_lab_codes = [ "Fasting glucose", "Hemoglobin A1c", "Serum creatinine", "TSH level", "Urine sodium", "NON_EXISTENT_LAB_TEST", # Example of something that might not map ] print("\nMapping local lab codes to LOINC:") for lab_code in local_lab_codes: try: # Search for the lab concept in LOINC vocabulary results = client.search.basic( query=lab_code, vocabulary_ids=["LOINC"], standard_concept="S", page_size=1, ) if results and results.get("data") and len(results["data"]) > 0: match = results["data"][0] concept_name = match.get("concept_name", "N/A") concept_id = match.get("concept_id", "N/A") concept_code = match.get("concept_code", "N/A") print( f" Local: '{lab_code}' -> LOINC: {concept_name} " f"(Concept ID: {concept_id}, LOINC Code: {concept_code})" ) # Optionally: get mappings to other vocabularies # mappings = client.mappings.get(concept_id, target_vocabulary="SNOMED") else: print(f" Local: '{lab_code}' -> No LOINC match found (needs manual review)") except omophub.APIError as e: print(f" API Error for '{lab_code}': {e.status_code}") except Exception as e: print(f" Unexpected error for '{lab_code}': {e}") ``` **The Key Insight:** This is the "set it and automate it" mindset. By scripting the lookup of common local codes, data engineers can build ETL pipelines that handle the vast majority of transformations automatically. The unmapped codes get flagged for manual review. Over time, as you resolve edge cases, you build a growing lookup table that makes every subsequent ETL run faster. Less technical debt, faster time-to-analysis. ## 5. The Modern Shortcut: Semantic Search and the FHIR Concept Resolver The loops above are useful for understanding what a clinical coding pipeline needs to do. In production, you probably want two higher-level tools that collapse several steps into one API call each: * **`client.search.semantic`** - neural (BioLORD-2023-C) similarity instead of keyword matching. Handles synonyms, abbreviations, and physician shorthand that keyword search misses. * **`client.fhir.resolve` / `resolve_batch`** - combines URI lookup, standard-concept mapping, CDM target-table assignment, and a semantic-search fallback in a single call. Originally designed for FHIR-coded data, but the `display` field makes it equally useful for plain-text input. **When to use which:** | Input shape | Recommended call | | -------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------ | | Free text from NLP (`"acute MI"`) | `client.search.semantic(query=...)` for a ranked list, or `client.fhir.resolve(display=...)` when you also want the CDM target table | | Local lab name (`"Hb A1c"`) | `client.fhir.resolve_batch([{"display": "Hb A1c"}, ...])` - batched, with CDM table assignment | | Structured FHIR `Coding` / `CodeableConcept` | `client.fhir.resolve(...)` / `resolve_codeable_concept(...)` - see the [FHIR-to-OMOP workflow](/guides/workflows/fhir-to-omop) | ### Semantic search for messy extracted text ```python Python theme={null} import omophub client = omophub.OMOPHub() # Same entities extracted from a physician note extracted = [ "acute MI", # abbreviation - keyword search would miss "T2DM", # acronym "Hgb A1c elevated", # shorthand + finding ] for phrase in extracted: results = client.search.semantic( query=phrase, vocabulary_ids=["SNOMED", "LOINC"], standard_concept="S", page_size=1, min_score=0.60, ) if results.get("data"): top = results["data"][0] print( f" '{phrase}' -> {top['concept_name']} " f"(score {top['similarity_score']:.2f}, {top['domain_id']})" ) ``` Semantic search accepts a `min_score` threshold so you can auto-flag low-confidence matches for human review. ### One-call resolution with CDM target table `fhir.resolve` gives you the standard concept **and** the OMOP CDM table in the same response. For entity-level ETL this removes a whole step - you no longer need to look up the domain and pick the right table downstream. ```python Python theme={null} # Free-text input: semantic fallback picks up the right concept result = client.fhir.resolve( display="acute myocardial infarction", resource_type="Condition", ) res = result["resolution"] print(res["standard_concept"]["concept_id"]) # 312327 print(res["standard_concept"]["concept_name"]) # "Acute myocardial infarction" print(res["target_table"]) # "condition_occurrence" print(res["mapping_type"]) # "semantic_match" ``` ### Batch the lab-code pass Replacing the per-code loop in Use Case B with a single batch call cuts latency and API usage. Each batch counts as **one** call against your quota regardless of item count (see [Batch & Performance](/guides/production/batch-performance)): ```python Python theme={null} local_lab_codes = [ "Fasting glucose", "Hemoglobin A1c", "Serum creatinine", "TSH level", "Urine sodium", ] # One API call for all five codes result = client.fhir.resolve_batch( [{"display": name} for name in local_lab_codes], resource_type="Observation", ) print(f"Resolved {result['summary']['resolved']}/{result['summary']['total']}") for name, item in zip(local_lab_codes, result["results"]): if "resolution" in item: std = item["resolution"]["standard_concept"] print( f" '{name}' -> {std['concept_name']} " f"(LOINC {std['concept_code']}, {item['resolution']['target_table']})" ) else: print(f" '{name}' -> unresolved, needs manual review") ``` For 100+ items, chunk into groups of 100 (the batch endpoint's max) and send the chunks sequentially. The [Lean ETL Mapping Cache](/guides/use-cases/lean-etl-mapping-cache) pattern keeps subsequent ETL runs fast by caching resolved mappings locally. ## 6. Validation & Human-in-the-Loop Automation is powerful, but it's not infallible. Clinical data is complex, and no automated system achieves 100% accuracy without validation. This is where the "human-in-the-loop" step earns its keep. Here's how to build a practical validation workflow: * **Flag uncertain mappings:** If a search returns multiple plausible matches or the top result doesn't look right, flag it for manual review by a clinical expert. You can use heuristics like comparing the returned `concept_name` against the original term, or checking whether the concept's `domain_id` matches your expectation. * **Prioritize review:** Instead of reviewing every mapping, clinical experts focus on the ambiguous or high-impact cases - the codes that affect cohort definitions or safety endpoints. * **Iterate and improve:** Feedback from reviewers feeds back into your mapping logic. Resolved edge cases become lookup rules. The system gets smarter with each ETL cycle. **The Practical Workflow:** Export a CSV containing the original local code, the proposed OMOP concept, its concept ID, vocabulary, and a column for "Reviewed (Y/N)" and "Corrected Concept ID." This gives clinical experts a clean, sortable review surface. Simple, effective, scalable. ## 7. Conclusion: Reclaiming Your Time Clinical coding automation isn't about replacing clinical expertise - it's about deploying it where it matters most. The combination of NLP tools for entity extraction and OMOPHub for vocabulary lookup and mapping gives you a pipeline that handles the repetitive 80% while surfacing the 20% that needs your judgment. By integrating OMOPHub into your ETL workflow, you go from maintaining local vocabulary databases and doing manual lookups to making simple API calls. That's less infrastructure, faster iterations, and more time spent on actual research. Try the Python snippets with your own data. Start with a small batch of local codes. See how many map cleanly on the first pass. I think you'll be surprised at how much time you get back. The "manual mapping tax"? Consider it optional. ## 8. Related Guides The end-to-end workflow from FHIR-coded data to populated OMOP CDM tables, with the Concept Resolver as the central primitive. Dedicated deep-dive on local lab codes to LOINC, including unit handling, reference ranges, and UCUM alignment. Dedupe-then-batch rules, caching patterns, and when to reach for semantic search vs keyword search. Full reference for `client.fhir.resolve` / `resolve_batch` / `resolve_codeable_concept`, including type interop. # Clinical Trial Eligibility Screening Source: https://docs.omophub.com/guides/use-cases/clinical-trial-eligibility Resolve clinical trial criteria to OMOP concept sets and screen patients using hierarchy expansion and set-based matching for eligibility at scale. ## 1. The "Needle in a Haystack" Problem A Phase III diabetes trial with 200 empty slots and six months to fill them. The patients exist - their data is sitting right there in the OMOP CDM. The problem? The eligibility criteria say things like "Adults with HbA1c > 7.0% despite Metformin therapy" - and translating that into queries across `condition_occurrence`, `measurement`, and `drug_exposure` tables, with the right concept IDs, the right value thresholds, and the right temporal logic, takes weeks of manual work per trial. This is the bottleneck in clinical trial recruitment. Not a lack of patients - a lack of infrastructure to match patients to trials at scale. Solving this requires two things working together: (1) an NLP or LLM system to parse the eligibility criteria text into structured components (conditions, drugs, measurements, thresholds, temporal constraints), and (2) a vocabulary API to resolve those components into standardized OMOP concept IDs that you can actually query against. **OMOPHub** handles the second part. It gives you instant API access to the full OHDSI ATHENA vocabulary - SNOMED for conditions, RxNorm for drugs, LOINC for measurements - so you can resolve "Type 2 Diabetes Mellitus" to concept ID `201826`, "Metformin" to its RxNorm ingredient, and "HbA1c" to its LOINC code, all without maintaining a local vocabulary database. But OMOPHub's real superpower for trial screening is **concept set expansion**. A trial criterion says "Type 2 Diabetes." But your patients might be coded as "Type 2 DM with renal complications," "Type 2 DM with peripheral angiopathy," or a dozen other specific variants. Simple ID matching would miss them. OMOPHub's hierarchy API lets you expand a single concept into all its descendants - catching every patient who should qualify, regardless of how specifically they were coded. ## 2. The Core Concept: From Criteria Text to OMOP Queries Automating trial eligibility screening is a multi-step process. Here's how the pieces fit together: **Step 1 - Parse the criteria (NLP/LLM).** Take the eligibility text from the protocol and extract structured components. A criterion like "patients with Type 2 Diabetes Mellitus and HbA1c > 7.0%, currently on Metformin" decomposes into: * Condition: "Type 2 Diabetes Mellitus" (inclusion) * Measurement: "HbA1c" > 7.0% (inclusion) * Drug: "Metformin" (inclusion, current exposure) This step requires an NLP tool or LLM (tools like Criteria2Query, or prompting Claude/GPT with the protocol text). OMOPHub does not do this step - it's a vocabulary API, not a text parser. **Step 2 - Resolve to OMOP concepts (OMOPHub).** Take each extracted entity and look up its standardized OMOP concept ID via OMOPHub's search API: * "Type 2 Diabetes Mellitus" → SNOMED concept ID `201826` * "Metformin" → RxNorm ingredient concept ID * "HbA1c" → LOINC concept ID **Step 3 - Expand concept sets (OMOPHub).** Use `hierarchy.descendants()` to build complete concept sets. "Type 2 Diabetes Mellitus" has dozens of child concepts in SNOMED. You want to match patients coded with any of them. **Step 4 - Query the OMOP database.** Apply the resolved, expanded concept IDs against the patient database (`condition_occurrence`, `drug_exposure`, `measurement` tables) with the appropriate value/temporal filters. OMOPHub owns Steps 2 and 3. That's where it adds the most value - turning clinical terms into queryable concept sets without the overhead of a local vocabulary database. ## 3. Use Case A: Resolving Parsed Criteria to OMOP Concept Sets Suppose your NLP step has already extracted the key entities from the trial protocol. Now you need OMOP concept IDs - and not just the top-level concept, but the full expanded set of descendants. **The Scenario:** A trial requires patients with "Type 2 Diabetes Mellitus" and current "Metformin" use. You need concept sets for both. **Code Snippet: Resolving Criteria Entities and Expanding Concept Sets** ```bash theme={null} pip install omophub ``` ```python Python theme={null} import omophub client = omophub.OMOPHub() # These entities were extracted from the trial protocol by an NLP/LLM step # (OMOPHub does NOT do the extraction - it resolves terms to concept IDs) parsed_criteria = { "inclusion_conditions": ["Type 2 Diabetes Mellitus"], "inclusion_drugs": ["Metformin"], "inclusion_measurements": ["Hemoglobin A1c"], } resolved_criteria = { "condition_concept_ids": set(), "drug_concept_ids": set(), "measurement_concept_ids": set(), } print("Resolving parsed criteria to OMOP concept sets:\n") # --- Resolve Conditions --- for condition_name in parsed_criteria["inclusion_conditions"]: try: results = client.search.basic( condition_name, vocabulary_ids=["SNOMED"], domain_ids=["Condition"], page_size=3, ) candidates = results.get("concepts", []) if results else [] if candidates: top_match = candidates[0] top_id = top_match["concept_id"] print(f" Condition: '{condition_name}'") print(f" -> {top_match['concept_name']} (ID: {top_id})") # Expand concept set: get all descendants descendants = client.hierarchy.descendants(top_id, max_levels=5) desc_list = descendants if isinstance(descendants, list) else descendants.get("concepts", []) resolved_criteria["condition_concept_ids"].add(top_id) for desc in desc_list: resolved_criteria["condition_concept_ids"].add(desc["concept_id"]) print(f" -> Expanded to {len(resolved_criteria['condition_concept_ids'])} concepts (including descendants)") else: print(f" Condition: '{condition_name}' -> No SNOMED match found") except omophub.APIError as e: print(f" API error for '{condition_name}': {e.message}") # --- Resolve Drugs --- for drug_name in parsed_criteria["inclusion_drugs"]: try: results = client.search.basic( drug_name, vocabulary_ids=["RxNorm"], domain_ids=["Drug"], page_size=3, ) candidates = results.get("concepts", []) if results else [] if candidates: # Find the ingredient-level concept ingredient = None for c in candidates: if c.get("concept_class_id") == "Ingredient": ingredient = c break if not ingredient: ingredient = candidates[0] drug_id = ingredient["concept_id"] print(f" Drug: '{drug_name}'") print(f" -> {ingredient['concept_name']} (ID: {drug_id}, Class: {ingredient.get('concept_class_id', 'N/A')})") # Expand: get all drug products containing this ingredient descendants = client.hierarchy.descendants(drug_id, max_levels=3) desc_list = descendants if isinstance(descendants, list) else descendants.get("concepts", []) resolved_criteria["drug_concept_ids"].add(drug_id) for desc in desc_list: resolved_criteria["drug_concept_ids"].add(desc["concept_id"]) print(f" -> Expanded to {len(resolved_criteria['drug_concept_ids'])} concepts (ingredient + products)") else: print(f" Drug: '{drug_name}' -> No RxNorm match found") except omophub.APIError as e: print(f" API error for '{drug_name}': {e.message}") # --- Resolve Measurements --- for meas_name in parsed_criteria["inclusion_measurements"]: try: results = client.search.basic( meas_name, vocabulary_ids=["LOINC"], domain_ids=["Measurement"], page_size=3, ) candidates = results.get("concepts", []) if results else [] if candidates: top_match = candidates[0] meas_id = top_match["concept_id"] print(f" Measurement: '{meas_name}'") print(f" -> {top_match['concept_name']} (ID: {meas_id}, LOINC: {top_match.get('concept_code', 'N/A')})") resolved_criteria["measurement_concept_ids"].add(meas_id) else: print(f" Measurement: '{meas_name}' -> No LOINC match found") except omophub.APIError as e: print(f" API error for '{meas_name}': {e.message}") # --- Summary --- print(f"\nResolved Criteria Summary:") print(f" Condition concept set: {len(resolved_criteria['condition_concept_ids'])} concepts") print(f" Drug concept set: {len(resolved_criteria['drug_concept_ids'])} concepts") print(f" Measurement concept set: {len(resolved_criteria['measurement_concept_ids'])} concepts") print(f"\nNote: Numerical thresholds (e.g., HbA1c > 7.0%) and temporal logic") print(f"(e.g., 'for at least 1 year') must be handled in the database query layer.") ``` **The Key Insight:** The concept set expansion is what makes this practical. Without it, you'd miss every patient coded with a specific subtype of Type 2 Diabetes. With it, a single concept ID becomes a comprehensive set that catches all clinically equivalent patients. This is the difference between finding 50 candidates and finding 500 - and OMOPHub makes it an API call instead of a local database query. ## 4. Use Case B: Patient Pre-screening Against Trial Criteria Once you have expanded concept sets from Use Case A, screening a patient becomes straightforward set logic: does the patient's OMOP profile overlap with the trial's required concepts? **The Scenario:** A patient is in the clinic. Their OMOP record has condition, drug, and measurement concept IDs. You need to check if they match the trial criteria. **Code Snippet: Pre-screening a Patient** ```python Python theme={null} import omophub client = omophub.OMOPHub() # Patient's OMOP profile (from their EHR/OMOP database) patient_profile = { "condition_ids": {201826, 4329847, 443727, 40484648}, # 201826 = Type 2 DM, 4329847 = MI, 443727 = Essential HTN, 40484648 = CKD "drug_ids": {1503297}, # 1503297 = Metformin (example concept ID) } # Trial criteria (resolved + expanded from Use Case A) # In production, these would be the full expanded concept sets trial_criteria = { "required_condition_ids": {201826}, # Type 2 DM (would be expanded set in production) "required_drug_ids": {1503297}, # Metformin "excluded_condition_ids": {432571}, # e.g., Liver disease } print("Patient Pre-screening:\n") # Check inclusion conditions conditions_met = trial_criteria["required_condition_ids"].issubset(patient_profile["condition_ids"]) drugs_met = trial_criteria["required_drug_ids"].issubset(patient_profile.get("drug_ids", set())) # Check exclusion conditions has_exclusion = bool(trial_criteria["excluded_condition_ids"] & patient_profile["condition_ids"]) print(f" Required conditions met: {conditions_met}") print(f" Required drugs met: {drugs_met}") print(f" Has exclusion condition: {has_exclusion}") if conditions_met and drugs_met and not has_exclusion: print("\n RESULT: Patient is a POTENTIAL CANDIDATE for this trial.") print(" (Pending measurement checks - HbA1c threshold, etc.)") else: # Look up names of unmet criteria for clear reporting unmet = [] if not conditions_met: missing = trial_criteria["required_condition_ids"] - patient_profile["condition_ids"] unmet.extend(list(missing)) if not drugs_met: missing = trial_criteria["required_drug_ids"] - patient_profile.get("drug_ids", set()) unmet.extend(list(missing)) if unmet: try: details = client.concepts.batch(list(unmet)) detail_list = details if isinstance(details, list) else details.get("concepts", []) print(f"\n RESULT: Patient does NOT meet criteria.") print(f" Missing:") for concept in detail_list: print(f" - {concept.get('concept_name', 'Unknown')} (ID: {concept['concept_id']})") except omophub.APIError as e: print(f"\n RESULT: Patient does NOT meet criteria. Missing IDs: {unmet}") if has_exclusion: excluded = trial_criteria["excluded_condition_ids"] & patient_profile["condition_ids"] try: details = client.concepts.batch(list(excluded)) detail_list = details if isinstance(details, list) else details.get("concepts", []) print(f" Exclusion criteria triggered:") for concept in detail_list: print(f" - {concept.get('concept_name', 'Unknown')} (ID: {concept['concept_id']})") except omophub.APIError: print(f" Exclusion criteria triggered: IDs {excluded}") ``` **The Key Insight:** The core matching is just set operations. OMOPHub's value is in two places: (a) building the concept sets in the first place (Use Case A), and (b) providing human-readable concept names for the screening report - so the clinician sees "Missing: Metformin" instead of "Missing: concept ID 1503297." That translation from IDs to names is small but critical for clinical adoption. ## 5. The "Explainable Matching" Layer A simple "eligible / not eligible" isn't enough for clinicians. They need to understand *why* - especially for exclusions. Was the patient excluded because of a permanent contraindication, or something that could change (like a medication that could be washed out)? This is where pairing OMOPHub's structured vocabulary data with an external LLM creates real clinical value. **Example Workflow:** 1. OMOPHub identifies that the patient has concept ID `4329847` (Myocardial Infarction), which is an exclusion criterion 2. OMOPHub provides the structured metadata: concept name, domain, vocabulary 3. You feed this to an LLM with the trial context to generate a clinician-facing explanation ```python Python theme={null} # After OMOPHub resolves the exclusion reason: exclusion_data = { "concept_name": "Myocardial infarction", "concept_id": 4329847, "vocabulary": "SNOMED", "domain": "Condition", "criterion_type": "exclusion", "trial_criterion_text": "Patients with history of myocardial infarction within the past 6 months", } # Feed to an external LLM for a clinician-facing explanation: prompt = f""" A patient was flagged during clinical trial screening. Exclusion reason: The patient has a recorded {exclusion_data['concept_name']} (SNOMED concept {exclusion_data['concept_id']}), which triggers the following trial exclusion criterion: "{exclusion_data['trial_criterion_text']}" Provide a 2-sentence clinical explanation for the treating physician, including whether this exclusion could become eligible with time. """ # llm_response = your_llm_client.complete(prompt) # Expected: "This patient is currently excluded due to a history of # myocardial infarction, which falls under the trial's cardiovascular # safety exclusion. If the MI occurred more than 6 months ago, the # patient may become eligible - verify the event date in the clinical record." ``` **The Key Insight:** OMOPHub provides the structured vocabulary backbone (concept names, IDs, relationships). The LLM provides the clinical reasoning and natural language generation. Together, they transform an opaque "excluded" flag into an actionable explanation that helps the clinician make a decision. This is how you build trust in automated screening systems. ## 6. Conclusion: Accelerating the Path to "First Patient In" Clinical trial recruitment is a pipeline problem. The patients are there. The data is there. What's missing is the infrastructure to connect them efficiently. OMOPHub addresses two critical bottlenecks in that pipeline: resolving clinical terms from trial protocols into standardized OMOP concept IDs, and expanding those concepts into comprehensive concept sets that catch patients regardless of coding specificity. The first turns "Metformin" into a queryable ID. The second turns "Type 2 Diabetes" into a set of 50+ concept IDs that catches every relevant patient. Combined with NLP for criteria parsing and an LLM for explainable matching, you get a screening pipeline that takes days instead of weeks - and produces results that clinicians can understand and act on. Start with Use Case A: take one inclusion criterion from a trial on ClinicalTrials.gov, extract the clinical terms, and run them through OMOPHub's search and hierarchy APIs. See how many descendant concepts you get. That expanded concept set is the difference between a trial that struggles to recruit and one that finds its patients. # Collaborative Mapping Source: https://docs.omophub.com/guides/use-cases/collaborative-mapping Use OMOPHub to build shareable source_to_concept_map files that accelerate multi-site OMOP mapping and break the per-site mapping silo tax. ## 1. The "Silo Tax" Every hospital that joins an OMOP network faces the same mapping wall. Their lab system uses "Cr\_Serum" for creatinine. The one across town uses "CREAT\_BLD." The academic medical center uses "Creatinine\_S\_mg." All three mean the same thing: LOINC 2160-0, Creatinine \[Mass/volume] in Serum or Plasma. But each hospital maps it independently. Three data engineers, three weeks of work, three times the cost - for the same result. Scale this across 500 hospitals and 800 unique lab codes each, and you're looking at hundreds of thousands of hours of duplicated mapping labor per year. This is the **Silo Tax**: the cost of every institution solving the same vocabulary problem in isolation. The OHDSI community has partial solutions. **USAGI** is an open-source mapping tool that helps data engineers find standard concept matches for local codes. **Athena** distributes the vocabularies themselves. The OMOP CDM has a **`source_to_concept_map`** table designed specifically to store local-to-standard mappings. But none of these provide a way to *share* completed mappings between institutions. **OMOPHub** doesn't solve the sharing problem either - it doesn't have a collaborative mapping repository. But it accelerates the *creation* of mapping files by providing fast vocabulary search without requiring a local Athena installation. And because mapping files follow a standard format (`source_to_concept_map`), they can be shared between institutions manually - via GitHub, shared drives, or project-specific data exchanges. This article shows the practical workflow: use OMOPHub to build mapping files fast, format them as `source_to_concept_map` records, and share them between sites participating in the same research network. > **Honest scoping:** OMOPHub does not currently have a shared mapping repository, user-contributed mappings, or collaborative mapping API. The workflow described here uses OMOPHub for vocabulary search + standard file formats for sharing. A future collaborative mapping feature would be valuable - but this article works with what exists today. ## 2. The Core Concept: The `source_to_concept_map` as a Sharing Format The OMOP CDM includes a table purpose-built for local-to-standard mappings: ``` source_to_concept_map: - source_code (VARCHAR) - the local code, e.g., "Cr_Serum" - source_concept_id (INTEGER) - 0 if no OMOP concept for the local code - source_vocabulary_id (VARCHAR) - identifier for the local vocabulary, e.g., "HospitalA_Labs" - target_concept_id (INTEGER) - the standard OMOP concept ID, e.g., 3016723 - target_vocabulary_id (VARCHAR) - e.g., "LOINC" - valid_start_date (DATE) - valid_end_date (DATE) - invalid_reason (VARCHAR) ``` This table is the natural format for shareable mapping files. If Hospital A builds a `source_to_concept_map` for their lab codes and Hospital B has similar local codes, Hospital B can import Hospital A's mappings as a starting point - reviewing and adjusting as needed. **OMOPHub's role:** Populating the `target_concept_id` and `target_vocabulary_id` columns. For each local code, search OMOPHub to find the standard concept match, then write the result into the mapping table format. ## 3. Use Case A: Building a Shareable Mapping File with OMOPHub A multi-site sepsis research project needs all participating hospitals to map their local lab codes for inflammatory markers to standard LOINC concepts. Instead of each hospital starting from scratch, the coordinating center builds an initial mapping file using OMOPHub, then distributes it. ```bash theme={null} pip install omophub ``` ```python Python theme={null} import omophub import json from datetime import date client = omophub.OMOPHub() # Local codes from Hospital A's lab system (representative set for sepsis) local_codes = [ {"source_code": "CRP_HS", "display": "C-Reactive Protein, High Sensitivity", "vocab": "HospitalA_Labs"}, {"source_code": "PCT_Level", "display": "Procalcitonin Level", "vocab": "HospitalA_Labs"}, {"source_code": "Lactate_Art", "display": "Arterial Lactate", "vocab": "HospitalA_Labs"}, {"source_code": "WBC_Auto", "display": "White Blood Cell Count, Automated", "vocab": "HospitalA_Labs"}, {"source_code": "BldCx_Ana", "display": "Blood Culture Anaerobic", "vocab": "HospitalA_Labs"}, {"source_code": "SepsisAlert", "display": "Sepsis Alert Triggered", "vocab": "HospitalA_Events"}, ] print("Building source_to_concept_map for sepsis project...\n") mapping_records = [] for entry in local_codes: code = entry["source_code"] display = entry["display"] source_vocab = entry["vocab"] print(f" {code}: '{display}'") target_id = 0 # Default: unmapped target_vocab = "" target_name = "" match_method = "unmapped" try: # Step 1: Search OMOPHub for the best LOINC/SNOMED match results = client.search.basic( display, vocabulary_ids=["LOINC", "SNOMED"], domain_ids=["Measurement", "Condition", "Observation"], page_size=3, ) candidates = results.get("concepts", []) if results else [] # Step 2: Semantic fallback if basic search misses if not candidates: semantic = client.search.semantic(display, vocabulary_ids=["LOINC", "SNOMED"], domain_ids=["Measurement", "Condition", "Observation"], page_size=3) candidates = (semantic.get("results", semantic.get("concepts", [])) if semantic else []) if candidates: match_method = "semantic" if candidates: best = candidates[0] target_id = best["concept_id"] target_vocab = best.get("vocabulary_id", "") target_name = best.get("concept_name", "") match_method = match_method if match_method == "fuzzy" else "basic" print(f" -> {target_name} ({target_vocab}, OMOP: {target_id}) [{match_method}]") else: print(f" -> NO MATCH - needs manual review") except omophub.APIError as e: print(f" -> API error: {e.message}") match_method = "error" # Step 3: Build source_to_concept_map record mapping_records.append({ "source_code": code, "source_concept_id": 0, "source_vocabulary_id": source_vocab, "source_code_description": display, "target_concept_id": target_id, "target_vocabulary_id": target_vocab, "valid_start_date": str(date.today()), "valid_end_date": "2099-12-31", "invalid_reason": None, # Extra metadata for review (not standard OMOP columns, but useful for sharing) "_match_method": match_method, "_target_concept_name": target_name, "_reviewed": False, "_reviewer": None, }) # Summary mapped = sum(1 for r in mapping_records if r["target_concept_id"] != 0) unmapped = len(mapping_records) - mapped print(f"\n--- Mapping File Summary ---") print(f" Total: {len(mapping_records)} | Mapped: {mapped} | Needs review: {unmapped}") # In production: save as CSV for sharing # pd.DataFrame(mapping_records).to_csv("sepsis_source_to_concept_map_hospitalA.csv", index=False) print(f"\n--- source_to_concept_map Records ---") for r in mapping_records: status = f"-> {r['_target_concept_name']} ({r['target_vocabulary_id']}: {r['target_concept_id']})" if r["target_concept_id"] != 0 else "-> UNMAPPED" print(f" {r['source_code']:15s} {status} [{r['_match_method']}]") ``` **The Key Insight:** This script produces a `source_to_concept_map`-formatted file in minutes - work that would take days of manual USAGI review. The output is a CSV that can be shared with other hospitals in the network. Hospital B imports it, reviews the mappings (adjusting for their local code variants), and has a head start on their own mapping work. ## 4. Use Case B: Importing and Adapting a Shared Mapping File Hospital B receives Hospital A's mapping file. Their local codes are different ("CRP\_Serum" instead of "CRP\_HS"), but the target OMOP concepts are the same. They adapt the shared file and fill in the gaps. ```python Python theme={null} import omophub client = omophub.OMOPHub() # Hospital A's shared mapping file (loaded from CSV in production) shared_mappings = { "C-Reactive Protein": {"target_concept_id": 3010156, "target_vocab": "LOINC"}, "Procalcitonin": {"target_concept_id": 3046279, "target_vocab": "LOINC"}, "Lactate": {"target_concept_id": 3047181, "target_vocab": "LOINC"}, "White Blood Cell Count": {"target_concept_id": 3000905, "target_vocab": "LOINC"}, "Blood Culture": {"target_concept_id": 3016407, "target_vocab": "LOINC"}, } # Hospital B's local codes hospital_b_codes = [ {"source_code": "CRP_Serum", "display": "CRP Serum Level"}, {"source_code": "PCT_Quant", "display": "Quantitative Procalcitonin"}, {"source_code": "Lact_Venous", "display": "Venous Lactate"}, {"source_code": "IL6_Level", "display": "Interleukin-6 Level"}, # Not in shared file ] print("Adapting shared mapping file for Hospital B...\n") for entry in hospital_b_codes: display = entry["display"] code = entry["source_code"] # Step 1: Check if any shared mapping matches by keyword overlap matched_shared = None for shared_key, shared_val in shared_mappings.items(): if shared_key.lower() in display.lower() or any( word in display.lower() for word in shared_key.lower().split() ): matched_shared = (shared_key, shared_val) break if matched_shared: key, val = matched_shared print(f" {code}: Reused shared mapping '{key}' -> OMOP {val['target_concept_id']} ({val['target_vocab']})") else: # Step 2: Not in shared file - look up via OMOPHub print(f" {code}: Not in shared file. Searching OMOPHub...") try: results = client.search.basic( display, vocabulary_ids=["LOINC"], domain_ids=["Measurement"], page_size=1, ) candidates = results.get("concepts", []) if results else [] if candidates: best = candidates[0] print(f" -> NEW: {best.get('concept_name')} (OMOP: {best['concept_id']})") print(f" -> Add to shared mapping file for other hospitals") else: print(f" -> No match - needs manual review") except omophub.APIError as e: print(f" -> API error: {e.message}") ``` **The Key Insight:** Hospital B's mapping took minutes instead of days because Hospital A already did the vocabulary search work. The shared mapping file isn't an API feature - it's a CSV. But OMOPHub made it fast to build, and the `source_to_concept_map` format makes it portable. Hospital B's new mappings (like IL-6) get added to the shared file for Hospital C. ## 5. The Sharing Workflow The practical workflow for a multi-site research network: **Step 1: Coordinating center builds initial mapping file** (Use Case A) * Extract all unique local codes from the first participating site * Look up each via OMOPHub * Human reviews and approves each mapping * Save as `source_to_concept_map` CSV **Step 2: Distribute to network participants** * Share via GitHub repo, shared drive, or project data package * Include review metadata (\_match\_method, \_reviewed, \_reviewer) **Step 3: Each site adapts the shared file** (Use Case B) * Match their local codes against the shared mappings by display name similarity * Use OMOPHub to fill gaps (codes not in the shared file) * Contribute new mappings back to the shared file **Step 4: Iterate** * The mapping file grows with each new site * Later sites have fewer gaps to fill * The coordinating center merges contributions and resolves conflicts This isn't a fancy platform feature - it's a disciplined workflow using standard formats and a fast vocabulary API. But it's how the Silo Tax actually gets reduced in practice. **What a future collaborative mapping feature could add:** * Centralized, API-accessible mapping repository (search other institutions' mappings) * Confidence scoring based on number of institutions that agree on a mapping * Version-controlled mapping provenance * Automated conflict detection when institutions map the same local code differently These would be genuinely valuable - but they require infrastructure beyond what OMOPHub currently provides. The workflow above works today. ## 6. Conclusion: Share Mappings, Not Just Vocabularies The Silo Tax persists because sharing local-to-standard mappings is harder than it should be. Standard vocabularies are shared (via Athena). Phenotype definitions are shared (via OHDSI PhenotypeLibrary). But the translation layer - the `source_to_concept_map` that each site builds laboriously - stays locked in institutional silos. OMOPHub makes building that mapping file fast: search for a local code display name, get back the standard OMOP concept, write it to the mapping table. The `source_to_concept_map` format makes the file portable: CSV in, CSV out, same schema everywhere. The missing piece isn't technology - it's workflow discipline. If your research network adopts the pattern of building, sharing, and iterating on mapping files, the Silo Tax drops with every site that joins. The first hospital does 100% of the mapping work. The second does 30%. By the fifth, it's mostly review and edge cases. Start with one mapping file. Build it with OMOPHub. Share it with your network. Let the next site extend it. That's how collaborative mapping works in practice - not with a magic platform, but with standard formats and a fast vocabulary API. # Drug Interaction Checking Source: https://docs.omophub.com/guides/use-cases/drug-interaction-checking Move beyond alert fatigue with OMOPHub - resolve drug names to standardized RxNorm ingredients via the hierarchy for smarter drug-drug interaction checking. ## 1. The "Alert Fatigue" Problem A doctor clicks "override" on a drug interaction alert for the 47th time today. Not because the alert is wrong, but because it's not useful. It says "Interaction detected" with no context, no severity, no clinical reasoning. After the 47th time, even the dangerous alerts start blending into the noise. That's alert fatigue. And it's one of the most well-documented patient safety problems in clinical informatics. Studies consistently show that clinicians override 50–96% of DDI alerts, depending on the system. The problem isn't that interactions don't matter. It's that most DDI checkers treat every interaction the same way: if Drug A and Drug B are present, flag it. No nuance, no dosage context, no ingredient-level reasoning. Building a smarter DDI checker requires solving three distinct problems: (1) resolving drug names and codes to their active ingredients, (2) looking up actual interaction data from a reliable knowledge base, and (3) presenting the results with enough clinical context to be actionable. **OMOPHub** solves problem #1, and it solves it well. It's a vocabulary API that gives you programmatic access to the full RxNorm hierarchy, so you can instantly resolve any drug product to its standardized active ingredient without maintaining a local vocabulary database. Pair it with a DDI knowledge base (like DrugBank or FDB) for the interaction lookup, and optionally an LLM for contextual reasoning, and you've got the foundation for a DDI system that clinicians might actually trust. ## 2. The Core Concept: Navigating the RxNorm Hierarchy Drug interactions happen at the **ingredient level**. It doesn't matter whether the patient is taking Tylenol, Paracetamol 500mg tablets, or Generic Acetaminophen - the interaction risk comes from the Acetaminophen. So before you can check for interactions, you need to resolve every drug on the patient's medication list down to its active ingredient(s). RxNorm - the standardized drug nomenclature used in the OMOP CDM - is organized hierarchically: * **Branded Drug** (e.g., "Coumadin 5 MG Oral Tablet") * **Clinical Drug** (e.g., "Warfarin 5 MG Oral Tablet") * **Clinical Drug Component** (e.g., "Warfarin 5 MG") * **Ingredient** (e.g., "Warfarin") Navigating this hierarchy manually across thousands of drugs would mean writing complex SQL against a multi-gigabyte ATHENA database. OMOPHub eliminates that. Its Python SDK gives you intuitive methods to search for drugs, traverse ancestors/descendants, and retrieve concept relationships, all via simple API calls. **What OMOPHub handles:** Resolving drug names/codes to standardized RxNorm concept IDs, traversing the hierarchy to find active ingredients, and cross-vocabulary mappings (e.g., RxNorm to ATC class). **What OMOPHub does NOT handle:** Actual drug-drug interaction data. You'll need an external DDI knowledge base (DrugBank, FDB/First Databank, Medi-Span, or similar) to determine whether Ingredient A interacts with Ingredient B. OMOPHub gives you the ingredients; the DDI database tells you which combinations are dangerous. ## 3. Use Case A: Resolving Drug Names to Ingredients for Interaction Checking Here's a practical scenario: a patient's medication list mentions "Coumadin" and "Aspirin." Before you can check for interactions, you need to resolve both to their RxNorm ingredients (Warfarin and Acetylsalicylic acid). **The Workflow:** 1. Search OMOPHub for each drug name to get its standard RxNorm concept ID 2. Use hierarchy traversal to find the ingredient-level concept 3. Pass the resolved ingredients to your DDI knowledge base **Code Snippet: Resolving Drug Names to Ingredients** ```bash theme={null} pip install omophub ``` ```python Python theme={null} import omophub client = omophub.OMOPHub() # Drug names extracted from a medication list or clinical note # (In production, you'd use NLP/NER to extract these from free text first) drug_names = ["Coumadin", "Aspirin"] print("Resolving drug names to active ingredients via OMOPHub:\n") resolved_ingredients = {} for drug_name in drug_names: try: # Step 1: Search for the drug in RxNorm search_results = client.search.basic( drug_name, vocabulary_ids=["RxNorm"], domain_ids=["Drug"], page_size=3, ) if not search_results or not search_results.get("concepts"): print(f" '{drug_name}' -> No RxNorm match found") continue drug_concept = search_results["concepts"][0] drug_id = drug_concept["concept_id"] print(f" '{drug_name}' -> {drug_concept['concept_name']} (ID: {drug_id})") # Step 2: Traverse ancestors to find the Ingredient ancestors = client.hierarchy.ancestors(drug_id, max_levels=5) # Filter ancestors client-side for concept_class_id == "Ingredient" ingredient = None if ancestors: for ancestor in ancestors if isinstance(ancestors, list) else ancestors.get("concepts", []): if ancestor.get("concept_class_id") == "Ingredient": ingredient = ancestor break if ingredient: ingredient_name = ingredient["concept_name"] ingredient_id = ingredient["concept_id"] print(f" -> Ingredient: {ingredient_name} (ID: {ingredient_id})") resolved_ingredients[drug_name] = { "ingredient_name": ingredient_name, "ingredient_concept_id": ingredient_id, } else: # Fallback: check concept relationships for "Has ingredient" relationships = client.concepts.relationships(drug_id) if relationships: for rel in relationships if isinstance(relationships, list) else relationships.get("concepts", []): if rel.get("relationship_id") == "RxNorm has ing": print(f" -> Ingredient (via relationship): {rel['concept_name']}") resolved_ingredients[drug_name] = { "ingredient_name": rel["concept_name"], "ingredient_concept_id": rel["concept_id"], } break except omophub.NotFoundError: print(f" '{drug_name}' -> Concept not found") except omophub.APIError as e: print(f" API error for '{drug_name}': {e.status_code} - {e.message}") print(f"\nResolved Ingredients: {resolved_ingredients}") # Step 3: Pass resolved ingredients to your DDI knowledge base # This is where you'd call DrugBank, FDB, or your internal DDI database # Example (pseudocode): # interactions = ddi_database.check( # list(resolved_ingredients.values()) # ) ``` **The Key Insight:** OMOPHub handles the vocabulary resolution that makes DDI checking *possible* by converting messy drug names into clean, standardized ingredients. This is the step that traditionally required a local ATHENA database. With OMOPHub, it's an API call. The actual interaction lookup happens downstream, in a dedicated DDI knowledge base. ## 4. Use Case B: Batch Screening for Population Health For population-level safety surveillance, you need to screen thousands of patients' medication lists for high-risk combinations. This means batch-resolving drug concept IDs to ingredients, then checking each patient's ingredient set against known interaction rules. **The Scenario:** A researcher wants to identify all patients concurrently prescribed an SSRI and an NSAID - a combination known to increase GI bleeding risk. **Code Snippet: Batch Ingredient Resolution and Class Detection** ```python Python theme={null} import omophub client = omophub.OMOPHub() # Example: patient medication list as RxNorm concept IDs # (These would come from your OMOP drug_exposure table) patient_medication_ids = [ 19013970, # Sertraline 50 MG Oral Tablet 1118084, # Ibuprofen 200 MG Oral Tablet 19080001, # Metformin 500 MG Oral Tablet 19077987, # Amlodipine 5 MG Oral Tablet ] print(f"Screening patient medications: {patient_medication_ids}\n") patient_ingredients = [] try: # Batch-retrieve concept details batch_results = client.concepts.batch(patient_medication_ids) for concept in batch_results if isinstance(batch_results, list) else batch_results.get("concepts", []): concept_name = concept.get("concept_name", "Unknown") concept_id = concept["concept_id"] print(f" Drug: {concept_name} (ID: {concept_id})") # Resolve to ingredient via hierarchy ancestors = client.hierarchy.ancestors(concept_id, max_levels=5) if ancestors: for anc in ancestors if isinstance(ancestors, list) else ancestors.get("concepts", []): if anc.get("concept_class_id") == "Ingredient": print(f" -> Ingredient: {anc['concept_name']}") patient_ingredients.append({ "drug_name": concept_name, "ingredient_name": anc["concept_name"], "ingredient_id": anc["concept_id"], }) break except omophub.APIError as e: print(f" API error: {e.status_code} - {e.message}") # Now classify ingredients into therapeutic classes # In production, you'd use ATC hierarchy via OMOPHub mappings # For this example, we use a reference lookup SSRI_INGREDIENTS = {"Sertraline", "Fluoxetine", "Paroxetine", "Citalopram", "Escitalopram"} NSAID_INGREDIENTS = {"Ibuprofen", "Naproxen", "Diclofenac", "Celecoxib", "Indomethacin"} found_ssri = [i for i in patient_ingredients if i["ingredient_name"] in SSRI_INGREDIENTS] found_nsaid = [i for i in patient_ingredients if i["ingredient_name"] in NSAID_INGREDIENTS] print(f"\nSSRIs found: {[i['ingredient_name'] for i in found_ssri]}") print(f"NSAIDs found: {[i['ingredient_name'] for i in found_nsaid]}") if found_ssri and found_nsaid: print("\n*** ALERT: SSRI + NSAID concurrent use detected ***") print("Risk: Increased gastrointestinal bleeding.") print(f" SSRI: {found_ssri[0]['drug_name']} ({found_ssri[0]['ingredient_name']})") print(f" NSAID: {found_nsaid[0]['drug_name']} ({found_nsaid[0]['ingredient_name']})") print("Recommendation: Review clinical necessity. Consider gastroprotective agent.") else: print("\nNo SSRI + NSAID combination detected.") ``` **The Key Insight:** For population health, the power is in the batch workflow. OMOPHub's `concepts.batch()` lets you resolve a whole medication list in one call, and `hierarchy.ancestors()` gets you from product-level to ingredient-level. The therapeutic class check (SSRI vs NSAID) can be done via reference lists or, more robustly, by traversing the ATC classification hierarchy through OMOPHub's cross-vocabulary mappings. The actual interaction rules come from your clinical knowledge base or published guidelines. OMOPHub provides the vocabulary infrastructure that makes those rules applicable at scale. ## 5. Adding Clinical Context with an LLM Identifying an interaction is step one. Making it *actionable* is step two. This is where pairing OMOPHub's structured vocabulary data with an LLM creates real value. Instead of a generic "Interaction detected" alert, you can feed the resolved ingredient information into an LLM along with patient context, and generate a clinician-friendly explanation. **Simplified Example: Generating a Contextualized DDI Alert** ```python Python theme={null} # After resolving ingredients via OMOPHub (as shown above): # warfarin_info = {"ingredient": "Warfarin", "concept_id": 1310149} # aspirin_info = {"ingredient": "Aspirin", "concept_id": 1112807} # Feed structured data to an LLM for clinical reasoning # (Using any LLM API - OpenAI, Anthropic, etc.) prompt = f""" You are a clinical pharmacist. Based on the following drug information resolved from OMOP/RxNorm standardized vocabularies, provide a concise clinical assessment of the interaction risk. Patient medications (resolved to ingredients): - Warfarin (Anticoagulant, RxNorm Ingredient concept ID: 1310149) - Aspirin (Antiplatelet/NSAID, RxNorm Ingredient concept ID: 1112807) Provide: 1. Interaction severity (Minor / Moderate / Major / Contraindicated) 2. Mechanism of interaction 3. Clinical recommendation (2-3 sentences) """ # llm_response = your_llm_client.complete(prompt) # Expected output would be something like: # "Major interaction. Both Warfarin and Aspirin affect hemostasis through # complementary mechanisms - Warfarin inhibits clotting factor synthesis # while Aspirin inhibits platelet aggregation. Concurrent use significantly # increases bleeding risk. Monitor INR closely and assess whether dual # therapy is clinically necessary." ``` **The Key Insight:** The LLM doesn't replace the DDI knowledge base, it enhances how the results are communicated. By grounding the LLM prompt with OMOPHub-resolved structured data (standardized ingredient names, concept IDs, vocabulary context), you avoid hallucination and get clinically relevant explanations. This is the shift from "Computer says no" to "Here's why this matters for your patient." ## 6. Conclusion: A Three-Layer Architecture Building a DDI checker that clinicians will actually trust requires three layers, each doing what it does best: 1. **Vocabulary Resolution (OMOPHub)**: Resolve drug names, codes, and products to standardized RxNorm ingredients via API. No local database maintenance required. 2. **Interaction Lookup (DDI Knowledge Base)**: Check resolved ingredient pairs against a curated DDI database (DrugBank, FDB, Medi-Span, or clinical rules). 3. **Clinical Context (LLM, optional)**: Generate human-readable explanations that include mechanism, severity, and actionable recommendations. OMOPHub handles the first layer, the vocabulary plumbing that makes everything else possible. It turns "Coumadin 5mg" into "Warfarin" with a single API call, and it does it at scale. That's less infrastructure, faster development, and more time building the clinical logic that actually reduces alert fatigue. Start with the ingredient resolution snippets above. Plug in your DDI data source. See how many of your current "generic" alerts you can turn into actionable, context-rich clinical guidance. Alert fatigue is a systems problem. Fix the system. # Gap Analysis and OHDSI Contributions Source: https://docs.omophub.com/guides/use-cases/gap-analysis-contributions Spotting the missing links in OMOP standard vocabularies - use OMOPHub to automate gap detection and build focused shortlists for OHDSI contributions. ## 1. The "Contribution Friction" Every institution converting local EHR data to OMOP hits unmappable codes. Local lab abbreviations with no LOINC equivalent. Institutional procedure codes that don't exist in SNOMED. Novel biomarkers that haven't been added to any standard vocabulary yet. These unmapped codes are gaps - and they're valuable. Each one is a potential contribution to the OHDSI vocabulary ecosystem. If your hospital has a local code for a clinical concept that SNOMED doesn't cover, and you identify it, the OHDSI Vocabulary Team can add it in the next Athena release. Every institution that converts to OMOP after that benefits from your discovery. But finding these gaps is tedious. You have 3,000 local codes. Most will map fine. Maybe 50 are genuinely missing from the standard vocabularies. The other 2,950 are just lookup work. How do you find the 50 that matter? **OMOPHub** makes the gap detection step fast. Search for each local code programmatically. The ones that return no match (or only weak matches) are your gap candidates. That's your shortlist for human review and potential OHDSI contribution. What OMOPHub does *not* do: submit contributions to OHDSI, add concepts to Athena, or determine whether a gap is "truly missing" vs. "just badly named." Gap detection is automated; gap interpretation requires human clinical expertise. The OHDSI contribution itself goes through the community forums and vocabulary team - not through an API. ## 2. The Core Concept: Search Failures as Signals The insight is simple: if OMOPHub's search can't find a match for a clinical term, that's information. **Tiered search strategy:** 1. **Basic search** with vocabulary and domain filters - catches exact and close matches 2. **Semantic search** - catches misspellings, abbreviations, word-order variations 3. **If both fail** - flag as a gap candidate The tier matters because the *type* of failure tells you something: * **Basic fails, semantic succeeds** → probably a naming/abbreviation issue, not a real gap * **Both fail** → likely a genuine gap or very institution-specific term * **Both return results, but wrong domain** → mapping ambiguity, needs human review This tiered approach reduces false positives (codes flagged as gaps that actually have mappings) and produces a cleaner shortlist for expert review. ## 3. Use Case A: Automated Gap Detection for Local Codes A hospital joining an OMOP research network has 200 unique local diagnosis codes related to sepsis. The data engineer needs to identify which ones have no standard OMOP equivalent - those are the gaps to review manually and potentially contribute to OHDSI. ```bash theme={null} pip install omophub ``` ```python Python theme={null} import omophub client = omophub.OMOPHub() # Local codes to analyze (in production: extracted from your source system) local_codes = [ {"code": "Sepsis_OD", "display": "Sepsis with Organ Dysfunction"}, {"code": "Bacteremia_NOS", "display": "Bacteremia, unspecified"}, {"code": "Hypotension_Sep", "display": "Hypotension due to Sepsis"}, {"code": "BiomarkerXYZ", "display": "Novel Sepsis Biomarker XYZ"}, {"code": "ICD_A41.9", "display": "Sepsis, unspecified organism"}, {"code": "AtypSepsis", "display": "Atypical Sepsis Presentation"}, {"code": "qSOFA_Pos", "display": "Positive qSOFA Score"}, ] print("Automated Gap Analysis\n") results = [] for entry in local_codes: code = entry["code"] display = entry["display"] match_found = False match_tier = None match_concept = None # --- Tier 1: Basic search --- try: basic = client.search.basic( display, vocabulary_ids=["SNOMED", "ICD10CM"], domain_ids=["Condition"], page_size=3, ) candidates = basic.get("concepts", []) if basic else [] # Step 1: Filter to standard concepts only standard = [c for c in candidates if c.get("standard_concept") == "S"] if standard: match_found = True match_tier = "basic" match_concept = standard[0] except omophub.APIError: pass # --- Tier 2: Semantic search (if basic failed) --- if not match_found: try: semantic = client.search.semantic(display, domain_ids=["Condition"], standard_concept="S", page_size=3) candidates = (semantic.get("results", semantic.get("concepts", [])) if semantic else []) if candidates: match_found = True match_tier = "semantic" match_concept = candidates[0] except omophub.APIError: pass # --- Record result --- if match_found: name = match_concept.get("concept_name", "Unknown") cid = match_concept["concept_id"] vocab = match_concept.get("vocabulary_id", "N/A") print(f" MAPPED [{match_tier}]: '{display}' -> {name} ({vocab}: {cid})") results.append({ "local_code": code, "local_display": display, "status": "mapped", "match_tier": match_tier, "omop_concept_id": cid, "omop_concept_name": name, "omop_vocabulary": vocab, }) else: print(f" GAP CANDIDATE: '{display}' - no standard match found") results.append({ "local_code": code, "local_display": display, "status": "gap_candidate", "match_tier": None, "omop_concept_id": None, "omop_concept_name": None, "omop_vocabulary": None, }) # --- Summary --- mapped = [r for r in results if r["status"] == "mapped"] gaps = [r for r in results if r["status"] == "gap_candidate"] print(f"\n--- Gap Analysis Summary ---") print(f" Total codes: {len(results)}") print(f" Mapped: {len(mapped)} ({len([m for m in mapped if m['match_tier'] == 'basic'])} basic, {len([m for m in mapped if m['match_tier'] == 'semantic'])} semantic)") print(f" Gap candidates: {len(gaps)}") if gaps: print(f"\n--- Gap Candidates for Human Review ---") for g in gaps: print(f" Code: {g['local_code']:20s} Display: {g['local_display']}") ``` **The Key Insight:** "Novel Sepsis Biomarker XYZ" and "Positive qSOFA Score" will likely fail both search tiers - the first because it's genuinely novel, the second because clinical scoring concepts are sparsely represented in some vocabularies. "Sepsis, unspecified organism" will map (it's essentially ICD-10 A41.9). "Bacteremia, unspecified" will likely map to a SNOMED concept. The gap candidates are the shortlist that needs human expert review. ## 4. Use Case B: Categorizing Gaps for Targeted Action Not all gaps are the same. A gap candidate could be: * **A genuinely missing concept** - the clinical idea doesn't exist in any standard vocabulary (e.g., a brand-new biomarker) * **A missing mapping** - the concept exists in SNOMED but not in the vocabulary you searched (e.g., exists as a Procedure, not a Condition) * **A local abbreviation** - the term is too institution-specific for search to match, but the concept exists under a different name * **A composite concept** - the local code combines multiple clinical ideas that are separate concepts in OMOP Categorizing gaps helps prioritize action: missing concepts should be proposed to OHDSI; missing mappings might just need a broader search; abbreviations need local mapping work; composites need decomposition. ```python Python theme={null} import omophub client = omophub.OMOPHub() def categorize_gap(display_term): """ Attempt to categorize a gap candidate by searching more broadly. Returns a category and any near-miss candidates found. """ near_misses = [] # Step 1: Broader search - drop domain filter, search all vocabularies try: broad = client.search.basic(display_term, page_size=5) candidates = broad.get("concepts", []) if broad else [] if candidates: # Found something, but not in our original domain/vocab filter near_misses = [ { "concept_name": c.get("concept_name"), "concept_id": c["concept_id"], "domain": c.get("domain_id"), "vocabulary": c.get("vocabulary_id"), "standard": c.get("standard_concept"), } for c in candidates[:3] ] # Check if matches are in a different domain domains = set(c.get("domain_id") for c in candidates) if domains and "Condition" not in domains: return "wrong_domain", near_misses # Check if matches are non-standard (mapping exists but not standard) standards = [c.get("standard_concept") for c in candidates] if all(s != "S" for s in standards): return "non_standard_only", near_misses return "partial_match", near_misses except omophub.APIError: pass # Step 2: Try semantic search as last resort try: semantic = client.search.semantic(display_term) sem_list = ( semantic if isinstance(semantic, list) else semantic.get("concepts", []) ) if semantic else [] if sem_list: near_misses = [ { "concept_name": c.get("concept_name"), "concept_id": c["concept_id"], "domain": c.get("domain_id"), "vocabulary": c.get("vocabulary_id"), } for c in sem_list[:3] ] return "semantic_near_miss", near_misses except omophub.APIError: pass # Nothing found anywhere return "genuinely_missing", [] # Categorize the gap candidates from Use Case A gap_candidates = [ "Novel Sepsis Biomarker XYZ", "Positive qSOFA Score", "Atypical Sepsis Presentation", ] print("Categorizing gap candidates...\n") for term in gap_candidates: category, near_misses = categorize_gap(term) print(f" '{term}'") print(f" Category: {category}") if near_misses: print(f" Near misses:") for nm in near_misses: print(f" - {nm['concept_name']} ({nm['domain']}/{nm['vocabulary']})") # Recommended action based on category actions = { "genuinely_missing": "-> Propose new concept to OHDSI Vocabulary Team via forums.ohdsi.org", "wrong_domain": "-> Concept exists but in different domain. Review if domain mapping is correct.", "non_standard_only": "-> Non-standard concept exists. Check if standard equivalent was missed.", "partial_match": "-> Partial matches found. Human review needed to select best match.", "semantic_near_miss": "-> Semantic match only. May be a naming/abbreviation issue.", } print(f" Action: {actions.get(category, 'Unknown')}") print() ``` **The Key Insight:** "Novel Sepsis Biomarker XYZ" will likely categorize as `genuinely_missing` - no matches anywhere. That's your OHDSI contribution candidate. "Positive qSOFA Score" might find `semantic_near_miss` results (qSOFA-related SNOMED concepts exist but may not match the exact phrasing). "Atypical Sepsis Presentation" might get `partial_match` - "Sepsis" matches, but "Atypical" is too vague for a specific concept. Each category drives a different action. ## 5. The OHDSI Contribution Pathway Once you've identified genuinely missing concepts, the contribution process is community-driven, not API-driven: 1. **Prepare a gap report** from your analysis (the output of Use Cases A + B) 2. **Post to OHDSI Forums** (forums.ohdsi.org → Vocabulary category) describing: * The missing concept and its clinical definition * The source vocabulary where it originates (if applicable) * How many records in your data use this code (impact/frequency) * Any near-miss concepts from your OMOPHub search (shows you did due diligence) 3. **OHDSI Vocabulary Team reviews** the proposal and decides whether to add it 4. **If accepted**, it appears in the next Athena vocabulary release 5. **Update your local vocabularies** from Athena to get the new concept OMOPHub helps with step 1 (automated gap detection and categorization). Steps 2-5 are community process. This is by design - vocabulary changes affect the entire OHDSI network and need human review. **Tools that complement this workflow:** * **USAGI** - OHDSI's mapping tool for the manual review step (reviewing near-misses, approving mappings) * **Athena** - Where accepted contributions land (vocabulary downloads) * **OMOPHub** - Fast vocabulary search for gap detection (no local vocab DB needed) ## 6. Conclusion: Finding the Gaps That Matter Gap analysis isn't about finding every unmapped code - most unmapped codes are just local abbreviations that need manual mapping work. It's about finding the codes that represent genuinely missing clinical concepts in the standard vocabularies. Those are the high-value contributions to OHDSI. The tiered search approach (basic → semantic → flag) combined with gap categorization (genuinely missing vs. wrong domain vs. naming issue) produces a focused shortlist that expert reviewers can act on efficiently. Instead of reviewing 3,000 local codes, they review 50 gap candidates, 10 of which might be genuine OHDSI contributions. OMOPHub makes the detection fast. The categorization helps prioritize. The OHDSI community process handles the rest. Start with your most problematic source system - the one with the most unmapped codes. Run the gap analysis. Categorize the results. Post the genuinely missing concepts to the OHDSI forums. Your contribution makes the next institution's mapping work a little easier. # Insurance Claims Processing Source: https://docs.omophub.com/guides/use-cases/insurance-claims-processing Standardize CPT, ICD-10, and HCPCS billing codes to OMOP clinical concepts for medical necessity review, fraud detection, and risk adjustment workflows. ## 1. The "Paperwork Tax" Every insurance claim tells two stories. The billing story: a list of CPT, ICD-10, and HCPCS codes, optimized for reimbursement. The clinical story: what actually happened to the patient and why. The gap between these two stories is where billions of dollars in administrative waste, payment delays, and fraud live. A CPT code for an "Office Visit" doesn't explain why the patient was there. An ICD-10 code for "M05.79" means "Rheumatoid arthritis with rheumatoid factor, multiple sites" - but only if you know ICD-10. And a HCPCS J-code for a biologic drug tells you the molecule, not whether it was clinically appropriate for the patient's condition. Bridging this divide starts with a basic but powerful step: **standardizing the codes.** OMOP's vocabulary structure maps billing codes (ICD-10-CM, HCPCS) to clinically rich standard concepts (SNOMED for conditions/procedures, RxNorm for drugs) via "Maps to" relationships. Once standardized, you can start asking clinical questions about administrative data. **OMOPHub** makes this standardization step fast and programmatic. It's a REST API for the OHDSI ATHENA vocabularies - so instead of maintaining a local vocabulary database, you look up a billing code, get its standard OMOP concept, and immediately have access to the clinical hierarchy, relationships, and cross-vocabulary mappings. The pattern: billing code → OMOPHub search → standard concept ID → clinical context. What OMOPHub does *not* do: it doesn't determine medical necessity, detect fraud, or adjudicate claims. Those require clinical rules engines, indication databases (like FDB or DrugBank), and CMS edit tables (like CCI/NCCI). OMOPHub handles the vocabulary layer - translating billing codes into a standardized clinical language that those downstream systems can work with. ## 2. The Core Concept: Reconstructing the Clinical Event An insurance claim is a bundle of billing codes representing a clinical encounter. OMOPHub helps you unpack that bundle into standardized clinical components: * **Procedure performed** → CPT-4 code → maps to SNOMED Procedure concept * **Diagnosis justifying the procedure** → ICD-10-CM code → maps to SNOMED Condition concept * **Drug or device administered** → HCPCS J-code or NDC → maps to RxNorm Drug concept The "Maps to" relationships in the OMOP vocabulary are the bridge. Multiple billing codes can map to the same standard concept (many-to-one), and a single billing code can occasionally map to multiple standard concepts (one-to-many). OMOPHub's search and mappings APIs let you traverse these relationships programmatically. Once you have standard concept IDs, you can: * Look up concept hierarchies (is this drug an anti-TNF agent?) * Check concept relationships (what ingredient does this drug contain?) * Compare across claims (different billing codes, same clinical event) This standardization is the foundation. Everything else - necessity review, fraud detection, risk adjustment - builds on top of it. ## 3. Use Case A: Standardizing a Claim for Medical Necessity Review Medical necessity review asks: is this billed drug clinically appropriate for this diagnosed condition? The first step is standardizing both the drug and the diagnosis. **The Scenario:** A claim arrives with HCPCS code J0135 (Adalimumab/Humira) and ICD-10-CM code M05.79 (Rheumatoid arthritis with rheumatoid factor, multiple sites). You need to resolve both to standard OMOP concepts. **Code Snippet: Standardizing Billing Codes via OMOPHub** ```bash theme={null} pip install omophub ``` ```python Python theme={null} import omophub client = omophub.OMOPHub() # Billing codes from the claim claim_drug_code = "J0135" # HCPCS: Adalimumab injection claim_diag_code = "M05.79" # ICD-10-CM: RA with rheumatoid factor, multiple sites print("Standardizing claim billing codes...\n") # --- Step 1: Resolve the drug (HCPCS → RxNorm) --- print(f"Drug: HCPCS {claim_drug_code}") try: # Search for the HCPCS code in OMOPHub drug_search = client.search.basic( claim_drug_code, vocabulary_ids=["HCPCS"], page_size=1, ) drug_candidates = drug_search.get("concepts", []) if drug_search else [] drug_standard_id = None if drug_candidates: hcpcs_concept = drug_candidates[0] hcpcs_id = hcpcs_concept["concept_id"] print(f" Found: {hcpcs_concept.get('concept_name')} (OMOP ID: {hcpcs_id})") # Get the "Maps to" standard concept drug_mappings = client.mappings.get(hcpcs_id, target_vocabulary="RxNorm") drug_map_list = ( drug_mappings if isinstance(drug_mappings, list) else drug_mappings.get("concepts", drug_mappings.get("mappings", [])) ) if drug_mappings else [] if drug_map_list: standard_drug = drug_map_list[0] drug_standard_id = standard_drug["concept_id"] print(f" -> Standard RxNorm: {standard_drug.get('concept_name')} (ID: {drug_standard_id})") else: print(f" -> No RxNorm mapping found. Checking if already standard...") # HCPCS drug codes sometimes are their own standard concept if hcpcs_concept.get("standard_concept") == "S": drug_standard_id = hcpcs_id print(f" -> Using HCPCS concept as standard (ID: {drug_standard_id})") else: print(f" Not found in OMOPHub") except omophub.APIError as e: print(f" API error: {e.message}") # --- Step 2: Resolve the diagnosis (ICD-10-CM → SNOMED) --- print(f"\nDiagnosis: ICD-10-CM {claim_diag_code}") try: diag_search = client.search.basic( claim_diag_code, vocabulary_ids=["ICD10CM"], page_size=1, ) diag_candidates = diag_search.get("concepts", []) if diag_search else [] diag_standard_id = None if diag_candidates: icd_concept = diag_candidates[0] icd_id = icd_concept["concept_id"] print(f" Found: {icd_concept.get('concept_name')} (OMOP ID: {icd_id})") # Get the "Maps to" standard SNOMED concept diag_mappings = client.mappings.get(icd_id, target_vocabulary="SNOMED") diag_map_list = ( diag_mappings if isinstance(diag_mappings, list) else diag_mappings.get("concepts", diag_mappings.get("mappings", [])) ) if diag_mappings else [] if diag_map_list: standard_diag = diag_map_list[0] diag_standard_id = standard_diag["concept_id"] print(f" -> Standard SNOMED: {standard_diag.get('concept_name')} (ID: {diag_standard_id})") else: print(f" -> No SNOMED mapping found") else: print(f" Not found in OMOPHub") except omophub.APIError as e: print(f" API error: {e.message}") # --- Step 3: Explore relationships (for context, NOT for necessity determination) --- if drug_standard_id: print(f"\nDrug concept relationships:") try: drug_rels = client.concepts.relationships(drug_standard_id) rel_list = ( drug_rels if isinstance(drug_rels, list) else drug_rels.get("relationships", []) ) if drug_rels else [] for rel in rel_list[:5]: print(f" {rel.get('relationship_id', 'N/A')}: {rel.get('concept_name', 'N/A')}") if len(rel_list) > 5: print(f" ... and {len(rel_list) - 5} more relationships") except omophub.APIError: pass # --- Important limitation --- print("\n--- Medical Necessity Determination ---") print("OMOPHub standardizes billing codes to clinical concepts.") print("To determine medical necessity (is Adalimumab indicated for RA?),") print("you need an external drug indications database:") print(" - FDB First Databank") print(" - DrugBank") print(" - DailyMed / FDA label data") print(" - Medi-Span") print("OMOP vocabularies do NOT contain 'treats' or 'indicated for' relationships.") ``` **The Key Insight:** OMOPHub gets you from billing codes to standard clinical concepts in two API calls per code. That's the essential first step - without it, you're comparing HCPCS strings to ICD-10 strings, which tells you nothing clinically. With standardized concepts, you can feed them into an indications database to check medical necessity, or into a risk adjustment model for accurate scoring. OMOPHub handles the translation layer; the clinical decision logic lives elsewhere. ## 4. Use Case B: Code Standardization for Fraud Detection Fraud, Waste, and Abuse (FWA) detection - particularly "unbundling" (billing component procedures separately instead of using a comprehensive code) - requires comparing what was billed against what should have been billed. **The Scenario:** A claim includes CPT 10060 (I\&D abscess, simple) and CPT 10061 (I\&D abscess, complicated) for the same patient visit. Are these legitimately separate procedures, or is this unbundling? **The Reality:** This question can't be answered by vocabulary hierarchies alone. The authoritative source for billing bundling rules is the **CMS Correct Coding Initiative (CCI/NCCI) edit tables**, which specify exactly which code pairs can and cannot be billed together. OMOP vocabularies don't contain these rules. **Where OMOPHub helps:** Standardizing the CPT codes so they can be looked up against CCI edit tables and clinical context. ```python Python theme={null} import omophub client = omophub.OMOPHub() # CPT codes from a suspicious claim claim_cpt_codes = ["10060", "10061"] print("Standardizing CPT codes for FWA analysis...\n") standardized = [] for cpt_code in claim_cpt_codes: try: results = client.search.basic( cpt_code, vocabulary_ids=["CPT4"], page_size=1, ) candidates = results.get("concepts", []) if results else [] if candidates: concept = candidates[0] omop_id = concept["concept_id"] name = concept.get("concept_name", "Unknown") print(f" CPT {cpt_code}: {name} (OMOP ID: {omop_id})") standardized.append({ "cpt_code": cpt_code, "omop_id": omop_id, "concept_name": name, }) # Get the standard concept mapping (CPT4 → SNOMED Procedure) mappings = client.mappings.get(omop_id, target_vocabulary="SNOMED") map_list = ( mappings if isinstance(mappings, list) else mappings.get("concepts", mappings.get("mappings", [])) ) if mappings else [] if map_list: std = map_list[0] print(f" -> SNOMED: {std.get('concept_name')} (ID: {std['concept_id']})") standardized[-1]["snomed_id"] = std["concept_id"] standardized[-1]["snomed_name"] = std.get("concept_name") else: print(f" CPT {cpt_code}: Not found") except omophub.APIError as e: print(f" CPT {cpt_code}: API error - {e.message}") # --- FWA analysis requires external data --- print("\n--- Unbundling Analysis ---") print("To determine if these CPT codes represent unbundling:") print(" 1. Look up the code pair in CMS CCI/NCCI edit tables") print(" 2. Check modifier indicators (some pairs are allowed with modifiers)") print(" 3. Review anatomical site documentation") print() print("OMOPHub's role: standardize codes so they can be matched against CCI edits.") print("CCI edit tables must be maintained separately (available from CMS.gov).") if len(standardized) >= 2: print(f"\nCode pair for CCI lookup:") print(f" Column 1: CPT {standardized[0]['cpt_code']} ({standardized[0]['concept_name']})") print(f" Column 2: CPT {standardized[1]['cpt_code']} ({standardized[1]['concept_name']})") print(f" -> Check NCCI PTP edits for this pair") ``` **The Key Insight:** OMOPHub standardizes billing codes - that's its job. Unbundling detection requires CCI edit tables, which are billing policy rules, not vocabulary relationships. The workflow is: OMOPHub resolves codes to standard concepts → CCI edits determine if the code pair is allowed → clinical context (from SNOMED hierarchy) provides supporting evidence. Each tool does what it's built for. ## 5. The "Clinical Context" Enrichment Layer Once billing codes are standardized, OMOPHub's vocabulary features can add clinical context that pure billing data lacks: **Hierarchy for severity inference:** A diagnosis of "Type 2 diabetes mellitus with diabetic chronic kidney disease" (a specific SNOMED descendant) tells you more about severity than just "Type 2 diabetes." OMOPHub's hierarchy API reveals where a concept sits in the clinical tree - deeper = more specific = often more severe. **Relationships for care expectations:** Using `concepts.relationships()`, you can discover what measurements, procedures, or findings are associated with a condition in the OMOP vocabulary. A diabetes diagnosis has relationships to HbA1c measurements, foot exam procedures, and eye exam referrals - all quality metrics that payers track. **Cross-vocabulary mapping for completeness:** A claim might use ICD-10-CM codes, but quality measures are often defined in SNOMED. OMOPHub's mappings ensure you can bridge between what was billed and what's being measured. These enrichments don't replace clinical judgment or rules engines, but they provide the standardized vocabulary substrate that those systems need to function. The more context you attach to a claim at the vocabulary level, the smarter your downstream analytics become. ## 6. Conclusion: From Billing Codes to Clinical Understanding The billing-clinical divide isn't going away - ICD-10, CPT, and HCPCS serve a different purpose than SNOMED and RxNorm. But the translation between them doesn't have to be manual. OMOPHub handles the vocabulary translation layer: billing code → search → standard concept ID → hierarchy, relationships, and mappings. This standardization is the prerequisite for every downstream application - medical necessity review (with external indications data), fraud detection (with CCI edit tables), risk adjustment (with clinical severity hierarchies), and quality measurement (with standard measure definitions). The pattern is the same across all these applications: standardize first, analyze second. OMOPHub makes the "standardize first" part fast and programmatic. The "analyze second" part requires domain-specific tools and rules - but they all work better when the vocabulary layer is clean. Start with a real claim. Map its CPT, ICD-10, and HCPCS codes through OMOPHub. See what standard concepts come back, what hierarchies they belong to, and what relationships they have. That clinical context, invisible in the raw billing data, is where the value lives. # Laboratory Result Mapping Source: https://docs.omophub.com/guides/use-cases/laboratory-result-mapping Map messy local lab names to standardized LOINC codes using OMOPHub vocabulary search - bridge lab-speak to OMOP for analytics and interoperability. ## 1. The "Tower of Babel" in the Lab You open the spreadsheet and your heart sinks. Two thousand rows of local lab test names: "S-Gluc," "Glucose, Serum," "Blood Sugar Fasting," "GLC\_RANDOM." All referring to essentially the same thing, yet each a unique string that defies simple categorization. This is the Tower of Babel problem in healthcare data. Every lab system speaks its own dialect. And it doesn't stop at naming - a Glucose result of `90` is meaningless without its unit. Is it `90 mg/dL` (normal fasting) or `90 mmol/L` (you'd be dead)? Mismatched units aren't just a data quality issue; they're a patient safety hazard hiding in your ETL pipeline. Traditional string matching falls apart here. "CRP\_QUANT" doesn't look like "C reactive protein \[Mass/volume] in Serum or Plasma" - but they're the same test. What you need is a way to map messy local lab names to **LOINC** (Logical Observation Identifiers Names and Codes), the international standard for lab tests, quickly and at scale. **OMOPHub** makes the vocabulary lookup part of this problem dramatically easier. It's a REST API that gives you instant access to the full LOINC vocabulary (along with SNOMED, RxNorm, and 100+ others) via the OHDSI ATHENA standardized vocabularies. Instead of downloading multi-gigabyte vocabulary files and maintaining a local database, you search for LOINC concepts with a single API call - including fuzzy and semantic search that handles abbreviations and misspellings. The mapping process then becomes: clean up your local names, search OMOPHub for LOINC candidates, triage by match quality, and have a human review the uncertain ones. That's it. Let's build it. ## 2. The Core Concept: The 6-Axis LOINC Model Every lab test has six dimensions that define exactly what it measures. Get even one wrong, and you're comparing apples to oranges. LOINC captures all six: 1. **Component** - What's being measured? (Glucose, Sodium, Hemoglobin A1c) 2. **Property** - What characteristic? (Mass concentration, Substance concentration) 3. **Time** - When? (Point in time, 24-hour collection, 1-hour post-glucose challenge) 4. **System** - What specimen? (Serum, Plasma, Urine, Whole Blood) 5. **Scale** - What type of result? (Quantitative, Ordinal, Nominal) 6. **Method** - How was it measured? (Colorimetric, Immunoassay, Automated) So "Glucose \[Mass/volume] in Serum or Plasma" is a fundamentally different LOINC concept from "Glucose \[Moles/volume] in Urine" - even though a local system might call both of them "GLUCOSE." The challenge: local lab systems rarely provide all six axes explicitly. They give you a cryptic abbreviation like "GLUC\_FAST" and expect you to figure out the rest. **How OMOPHub helps:** It gives you programmatic search across the full LOINC vocabulary. You pass in the messy local string, and OMOPHub returns ranked candidate LOINC concepts. Its fuzzy search handles typos and abbreviations. Its basic search supports filtering by vocabulary (`LOINC`) and domain (`Measurement`). You don't need a local vocabulary database - just an API key. What OMOPHub does *not* do: it doesn't infer the six axes from context, perform NLP on clinical notes, or run an LLM. It's a vocabulary lookup engine. The intelligence in choosing the right LOINC code from the candidates still comes from your mapping logic and, for edge cases, your human reviewers. ## 3. Use Case A: Automated Mapping of Local Lab Catalogs The most common headache: a new data source arrives with 2,000 local lab test names, and you need LOINC mappings for all of them. **The Workflow:** 1. For each local lab string, search OMOPHub's LOINC vocabulary 2. Use `search.basic()` for clean names, `search.semantic()` for abbreviated/misspelled ones 3. Collect the top candidates for each local string 4. Auto-accept high-confidence matches, flag the rest for review **Code Snippet: Mapping Local Lab Strings to LOINC Candidates** ```bash theme={null} pip install omophub ``` ```python Python theme={null} import omophub client = omophub.OMOPHub() local_lab_strings = [ "GLUCOSE_FASTING", "CRP_QUANT", "URINE_PROT_24HR", "TSH", "HEMOGLOBIN A1C", "LIVER_PANEL", # Broader term - may match multiple LOINC codes "NON_EXISTENT_LAB_XYZ", # Unmappable example ] print("Automated Mapping of Local Lab Strings to LOINC:\n") mapping_results = [] for local_string in local_lab_strings: # Clean up underscores for better search results search_term = local_string.replace("_", " ") print(f" Searching for: '{local_string}' (query: '{search_term}')") try: # Try basic search first (best for clean, descriptive names) results = client.search.basic( search_term, vocabulary_ids=["LOINC"], domain_ids=["Measurement"], page_size=3, ) candidates = results.get("concepts", []) if results else [] # If basic search returns nothing, try semantic search for abbreviations if not candidates: semantic = client.search.semantic(search_term, vocabulary_ids=["LOINC"], domain_ids=["Measurement"], page_size=3) candidates = (semantic.get("results", semantic.get("concepts", [])) if semantic else []) if candidates: for i, concept in enumerate(candidates): c_name = concept.get("concept_name", "N/A") c_id = concept.get("concept_id", "N/A") c_code = concept.get("concept_code", "N/A") rank_label = "** BEST MATCH" if i == 0 else f" candidate {i + 1}" print(f" {rank_label}: {c_name} (ID: {c_id}, LOINC: {c_code})") mapping_results.append({ "local_string": local_string, "top_match": candidates[0].get("concept_name"), "top_match_id": candidates[0].get("concept_id"), "num_candidates": len(candidates), "status": "auto_mapped" if len(candidates) == 1 else "needs_review", }) else: print(" No LOINC matches found - flagged for manual mapping") mapping_results.append({ "local_string": local_string, "top_match": None, "status": "manual_mapping_required", }) except omophub.APIError as e: print(f" API error: {e.status_code} - {e.message}") print() # Summary auto_mapped = sum(1 for r in mapping_results if r["status"] == "auto_mapped") needs_review = sum(1 for r in mapping_results if r["status"] == "needs_review") manual = sum(1 for r in mapping_results if r["status"] == "manual_mapping_required") print(f"Summary: {auto_mapped} auto-mapped, {needs_review} need review, {manual} need manual mapping") ``` **The Key Insight:** This approach converts weeks of manual lookup into hours of automated search plus targeted review. The `search.basic()` call handles clean descriptive names. The `search.semantic()` fallback catches abbreviations and misspellings that basic search would miss. The result: a prioritized list of LOINC candidates for each local string, ready for human triage. ## 4. Use Case B: Unit Normalization and Value Range Validation Once your lab tests are mapped to LOINC, the next critical question is: are the actual numeric results interpretable? A Glucose result of `90` means very different things depending on the unit. **The Scenario:** Your data contains Glucose results in both `mg/dL` and `mmol/L`. You need to normalize everything to a single unit and flag physiologically implausible values. **The Logic:** OMOPHub helps you confirm the LOINC concept and retrieve its metadata. The unit conversion itself is custom logic - OMOPHub is a vocabulary API, not a calculator - but knowing the exact LOINC concept tells you what units to expect. **Code Snippet: LOINC Concept Lookup + Unit Normalization** ```python Python theme={null} import omophub client = omophub.OMOPHub() # OMOP Concept ID for "Glucose [Mass/volume] in Serum or Plasma" (LOINC: 2345-7) loinc_glucose_id = 3016723 # Lab results to validate lab_results = [ {"value": 90, "unit": "mg/dL"}, {"value": 5.0, "unit": "mmol/L"}, {"value": 90, "unit": "mmol/L"}, # Physiologically impossible {"value": 0.1, "unit": "mg/dL"}, # Physiologically impossible ] # Known conversion factors (maintained as reference data, NOT from OMOPHub) GLUCOSE_MMOL_TO_MGDL = 18.0182 # 1 mmol/L glucose = 18.0182 mg/dL try: # Step 1: Confirm the LOINC concept via OMOPHub glucose_concept = client.concepts.get(loinc_glucose_id) concept_name = glucose_concept.get("concept_name", "Unknown") concept_code = glucose_concept.get("concept_code", "N/A") print(f"LOINC Concept: {concept_name} (Code: {concept_code})") # Step 2: Optionally check relationships for unit info relationships = client.concepts.relationships(loinc_glucose_id) print(f" Relationships found: {len(relationships) if isinstance(relationships, list) else 'see details'}") except omophub.APIError as e: print(f"Could not retrieve concept: {e.message}") # Step 3: Normalize units and validate ranges (custom logic) standard_unit = "mg/dL" print(f"\nNormalizing results to: {standard_unit}\n") for result in lab_results: value = result["value"] unit = result["unit"] # Convert to standard unit if needed if unit == "mmol/L": normalized = value * GLUCOSE_MMOL_TO_MGDL print(f" {value} {unit} -> {normalized:.1f} {standard_unit} (converted)") else: normalized = value print(f" {value} {unit} (already in standard unit)") # Plausibility check if normalized < 20 or normalized > 1000: print(f" ALERT: Physiologically implausible ({normalized:.1f} {standard_unit})") elif normalized < 70 or normalized > 200: print(f" Warning: Outside typical normal range ({normalized:.1f} {standard_unit})") else: print(f" Within plausible range ({normalized:.1f} {standard_unit})") ``` **The Key Insight:** OMOPHub's role here is confirming the LOINC concept identity - making sure you're looking at the right test before you apply unit logic. The conversion factors and plausibility ranges are domain knowledge that you maintain separately (or source from clinical guidelines). The power is in the combination: OMOPHub gives you vocabulary certainty, your custom logic handles the math, and together they catch data quality issues that could otherwise corrupt downstream analyses. ## 5. The "Human-in-the-Loop" Review Workflow Even with good search, lab mapping is never 100% automated. "CBC with Diff" might map to different LOINC codes depending on whether it's a manual or automated differential. "Liver Panel" is a composite that maps to multiple individual LOINC tests. These require human judgment. Here's a practical tiered review workflow based on search result quality: **Tier 1 - Auto-Accept:** Search returns exactly one strong match (the local string is essentially the LOINC name). Accept automatically. These are your easy wins. **Tier 2 - Flag for Review:** Search returns multiple plausible candidates, or the top match is a slightly different test variant. Queue these for review by a clinical data expert. Present them with the local string, the top 3 LOINC candidates, and any context from the source system. **Tier 3 - Manual Mapping:** Search returns no results or only irrelevant matches. These need hands-on expert mapping, often requiring institutional knowledge about what the local code actually means. **The Review Interface:** Build a simple web app or even a spreadsheet with columns for: * Original local lab string * Top N LOINC candidates from OMOPHub (with concept names and codes) * Tier assignment (auto / review / manual) * Reviewer's selected LOINC code * Comments/rationale Over time, reviewed mappings become lookup rules that feed back into your ETL, reducing the manual workload with each new data source. The system gets smarter. ## 6. Conclusion: From Raw Signals to Research Insights Lab results are the bedrock of clinical research, but they're locked behind a wall of local naming chaos and unit inconsistency. The mapping problem has always been solvable - it's just been slow. OMOPHub makes the vocabulary lookup part fast. Instead of maintaining a local ATHENA database, you search LOINC concepts via API - with fuzzy and semantic search that handles the abbreviations and misspellings that make lab data so painful. Pair that with systematic unit normalization and a tiered human review workflow, and you've got a pipeline that turns 2,000 messy local lab codes into standardized, analysis-ready LOINC mappings. For data engineers, that's faster ETL builds and fewer manual hours. For researchers, it's lab data you can actually trust for cohort identification, predictive modeling, and cross-institutional comparisons. Run your messiest local lab string through the search snippet above. See what comes back. I think you'll be surprised at how far a good vocabulary API gets you. # Lean ETL Mapping Cache Source: https://docs.omophub.com/guides/use-cases/lean-etl-mapping-cache Build validated OMOP mapping caches with OMOPHub during development and apply them locally at production speed - no full vocabulary load required. ## 1. The Mapping Bottleneck Every OMOP ETL has a mapping step: take the source system's local codes and translate them to standard OMOP concept IDs. This step has two costs that people conflate but shouldn't: **Cost 1: The one-time setup.** Figuring out what each local code maps to. "Cr\_Serum" → LOINC 2160-0. "WBC\_Count" → LOINC 6690-2. "Local\_Sepsis\_Dx" → SNOMED 91302008. This is the hard intellectual work - searching vocabularies, validating matches, handling ambiguity. **Cost 2: The runtime application.** Applying those mappings to millions of records every night. This is a SQL JOIN or pandas merge - fast, mechanical, and local. The mistake is conflating these two costs and loading a full Athena vocabulary database to handle both. You don't need 4GB of vocabulary tables to map 500 unique local codes. But you also can't make HTTP API calls for every row in a million-record batch. **OMOPHub** is the right tool for Cost 1: the mapping discovery and validation phase. Search for your local codes, verify the matches, build a mapping cache. Then use that cache - locally, with no API calls - for Cost 2: the nightly production run. This is "Lean ETL": use OMOPHub to build smart, validated mapping files during development. Apply those mappings via local lookups during production. No full vocabulary load needed for the ETL. No API latency during batch processing. Best of both worlds. ## 2. The Core Concept: The Mapping Cache Pattern The workflow has three phases: **Phase 1: Extract unique source codes.** Your source data has millions of records but a much smaller set of unique codes. A hospital lab system might produce 500,000 lab results per month, but only use 800 unique local lab codes. Extract those 800 codes - that's your mapping workload. **Phase 2: Look up each unique code via OMOPHub.** Send each of the 800 codes to OMOPHub (search by display name, code, or fuzzy match). Get back the standard OMOP concept ID, name, vocabulary, and domain. This takes a few minutes for 800 codes. Save the results as a mapping file (CSV, JSON, or database table). **Phase 3: Apply the mapping cache in production.** Your nightly ETL reads the mapping file, joins it to the source data via local lookup (pandas merge, SQL JOIN, dictionary lookup), and writes the enriched records to OMOP CDM tables. Zero API calls. Full local speed. **When to re-run Phase 2:** When new source codes appear that aren't in your mapping cache. Your ETL detects unmapped codes, queues them for OMOPHub lookup, and a human (or automated process) reviews and approves the new mappings before they enter the cache. ## 3. Use Case A: Building a Mapping Cache for a Sepsis Study A multi-site sepsis research project receives data from four hospitals. Each hospital uses different local codes for lactate, WBC, blood cultures, and sepsis diagnoses. The ETL needs a consistent mapping from all local codes to standard OMOP concepts. ```bash theme={null} pip install omophub ``` ```python Python theme={null} import omophub import json client = omophub.OMOPHub() # --- Phase 1: Unique source codes from all hospitals --- # In production, extract these from: SELECT DISTINCT source_code, source_display FROM source_data unique_source_codes = [ {"source_code": "Lactate_Serum", "display": "Serum Lactate", "domain_hint": "Measurement"}, {"source_code": "WBC_Count", "display": "White Blood Cell Count", "domain_hint": "Measurement"}, {"source_code": "BldCx_Aero", "display": "Blood Culture Aerobic", "domain_hint": "Measurement"}, {"source_code": "Local_Sepsis_Dx", "display": "Sepsis Diagnosis", "domain_hint": "Condition"}, {"source_code": "PROC_CVC", "display": "Central Venous Catheter Insertion", "domain_hint": "Procedure"}, {"source_code": "INVALID_999", "display": "Unknown Local Test", "domain_hint": "Measurement"}, ] print(f"Phase 1: {len(unique_source_codes)} unique source codes to map\n") # Domain → vocabulary mapping for targeted search DOMAIN_VOCABS = { "Measurement": ["LOINC"], "Condition": ["SNOMED"], "Procedure": ["SNOMED"], "Drug": ["RxNorm"], } # --- Phase 2: Look up each code via OMOPHub --- mapping_cache = [] for entry in unique_source_codes: code = entry["source_code"] display = entry["display"] domain = entry["domain_hint"] vocabs = DOMAIN_VOCABS.get(domain, []) print(f" Looking up: '{display}' ({code})") omop_id = None omop_name = None omop_vocab = None omop_code = None match_method = None try: # Step 1: Try basic search with vocabulary filter results = client.search.basic( display, vocabulary_ids=vocabs, domain_ids=[domain] if domain else [], page_size=3, ) candidates = results.get("concepts", []) if results else [] # Step 2: If basic search misses, try semantic if not candidates: semantic = client.search.semantic(display, vocabulary_ids=vocabs, domain_ids=[domain] if domain else [], page_size=3) candidates = (semantic.get("results", semantic.get("concepts", [])) if semantic else []) if candidates: match_method = "semantic" if candidates: best = candidates[0] omop_id = best["concept_id"] omop_name = best.get("concept_name") omop_vocab = best.get("vocabulary_id") omop_code = best.get("concept_code") match_method = match_method or "basic" # Step 3: If non-standard, follow "Maps to" to get standard concept if best.get("standard_concept") != "S": std_map = client.mappings.get(omop_id, target_vocabulary=vocabs[0] if vocabs else None) map_list = ( std_map if isinstance(std_map, list) else std_map.get("concepts", std_map.get("mappings", [])) ) if std_map else [] if map_list: std = map_list[0] omop_id = std["concept_id"] omop_name = std.get("concept_name") omop_vocab = std.get("vocabulary_id") omop_code = std.get("concept_code") match_method = "mapped_to_standard" print(f" -> {omop_name} ({omop_vocab}: {omop_code}, OMOP: {omop_id}) [{match_method}]") else: print(f" -> NO MATCH - needs manual mapping") match_method = "unmapped" except omophub.APIError as e: print(f" -> API error: {e.message}") match_method = "error" mapping_cache.append({ "source_code": code, "source_display": display, "domain_hint": domain, "omop_concept_id": omop_id, "omop_concept_name": omop_name, "omop_vocabulary_id": omop_vocab, "omop_concept_code": omop_code, "match_method": match_method, "reviewed": False, # Flag for human review }) # --- Save the mapping cache --- print(f"\n--- Mapping Cache ({len(mapping_cache)} entries) ---") mapped = sum(1 for m in mapping_cache if m["omop_concept_id"]) unmapped = len(mapping_cache) - mapped print(f" Mapped: {mapped} | Unmapped: {unmapped}") # In production, save to CSV or database: # pd.DataFrame(mapping_cache).to_csv("sepsis_mapping_cache.csv", index=False) # Print for review for m in mapping_cache: status = f"OMOP {m['omop_concept_id']}" if m["omop_concept_id"] else "UNMAPPED" print(f" {m['source_code']:20s} -> {status:>12s} [{m['match_method']}]") ``` **The Key Insight:** This script runs once during ETL development - not every night. It produces a mapping file that the production ETL uses as a local lookup. If tomorrow's data batch contains a new source code not in the cache, the ETL logs it as unmapped and queues it for a new OMOPHub lookup + human review. The cache grows over time until it covers all source codes. OMOPHub calls drop to near-zero in steady state. ## 4. Use Case B: Applying the Mapping Cache in Production Once you have the mapping cache, the nightly ETL is pure local processing - no API calls. ```python Python theme={null} import pandas as pd # --- Production ETL: Apply the mapping cache --- # Step 1: Load the mapping cache (built via OMOPHub in Phase 2) mapping_cache = pd.DataFrame([ {"source_code": "Lactate_Serum", "omop_concept_id": 3047181, "omop_concept_name": "Lactate [Moles/volume] in Blood"}, {"source_code": "WBC_Count", "omop_concept_id": 3000905, "omop_concept_name": "Leukocytes [#/volume] in Blood"}, {"source_code": "BldCx_Aero", "omop_concept_id": 3016407, "omop_concept_name": "Blood culture"}, {"source_code": "Local_Sepsis_Dx", "omop_concept_id": 132797, "omop_concept_name": "Sepsis"}, {"source_code": "PROC_CVC", "omop_concept_id": 4180032, "omop_concept_name": "Insertion of central venous catheter"}, # "INVALID_999" intentionally absent - unmapped ]) # Step 2: Load today's source data batch (in production: millions of rows from EHR extract) source_batch = pd.DataFrame([ {"patient_id": "P001", "source_code": "Lactate_Serum", "value": 2.5, "datetime": "2025-02-01 09:00"}, {"patient_id": "P001", "source_code": "WBC_Count", "value": 15.2, "datetime": "2025-02-01 09:00"}, {"patient_id": "P002", "source_code": "Lactate_Serum", "value": 4.1, "datetime": "2025-02-01 10:30"}, {"patient_id": "P002", "source_code": "Local_Sepsis_Dx", "value": None, "datetime": "2025-02-01 10:30"}, {"patient_id": "P003", "source_code": "INVALID_999", "value": 99, "datetime": "2025-02-01 11:00"}, ]) print(f"Production ETL: {len(source_batch)} records to process\n") # Step 3: Merge source data with mapping cache - pure local operation, zero API calls enriched = source_batch.merge( mapping_cache[["source_code", "omop_concept_id", "omop_concept_name"]], on="source_code", how="left", ) # Step 4: Split into mapped and unmapped mapped_records = enriched[enriched["omop_concept_id"].notna()] unmapped_records = enriched[enriched["omop_concept_id"].isna()] print(f" Mapped: {len(mapped_records)} records -> ready for OMOP CDM load") print(f" Unmapped: {len(unmapped_records)} records -> queued for OMOPHub lookup\n") if not unmapped_records.empty: new_codes = unmapped_records["source_code"].unique() print(f" New unmapped codes to look up via OMOPHub: {list(new_codes)}") # The mapped records are ready for OMOP CDM insertion # (measurement table for labs, condition_occurrence for diagnoses, etc.) print(f"\n--- Sample Output ---") print(mapped_records[["patient_id", "source_code", "omop_concept_id", "value"]].to_string(index=False)) ``` **The Key Insight:** The production ETL is a pandas merge - milliseconds for millions of rows. No HTTP calls, no API latency, no rate limits, no network dependency. OMOPHub was used once (during development) to build the mapping cache. The cache is the artifact that persists. This is the actual "Lean ETL" - lean on runtime resources because the vocabulary work was done upfront. ## 5. When New Codes Appear The mapping cache isn't static. As hospitals add new tests or change local codes, unmapped codes appear. The workflow: 1. **ETL detects unmapped codes** (the `unmapped_records` in Use Case B) 2. **Queue the new codes** for OMOPHub lookup 3. **Run the Phase 2 script** (Use Case A) on just the new codes 4. **Human reviews** the suggested mappings (critical for data quality) 5. **Append approved mappings** to the cache 6. **Re-run the ETL** for the previously unmapped records This keeps the cache growing and OMOPHub calls shrinking. In a mature ETL, you might go months without needing a new OMOPHub lookup. ```python Python theme={null} import omophub client = omophub.OMOPHub() def lookup_new_codes(new_codes, mapping_cache_path="sepsis_mapping_cache.csv"): """Look up unmapped codes via OMOPHub and suggest mappings for review.""" print(f"\nLooking up {len(new_codes)} new unmapped codes...\n") suggestions = [] for code_info in new_codes: code = code_info["source_code"] display = code_info.get("display", code) try: # Step 1: Search OMOPHub for candidates results = client.search.basic(display, page_size=3) candidates = results.get("concepts", []) if results else [] if candidates: for i, c in enumerate(candidates[:3]): suggestions.append({ "source_code": code, "source_display": display, "suggestion_rank": i + 1, "omop_concept_id": c["concept_id"], "omop_concept_name": c.get("concept_name"), "omop_vocabulary_id": c.get("vocabulary_id"), "approved": False, # Human must approve }) print(f" {code}: Suggestion {i+1}: {c.get('concept_name')} ({c.get('vocabulary_id')})") else: print(f" {code}: No suggestions found - needs manual mapping") suggestions.append({ "source_code": code, "source_display": display, "suggestion_rank": None, "omop_concept_id": None, "omop_concept_name": "MANUAL MAPPING REQUIRED", "approved": False, }) except omophub.APIError as e: print(f" {code}: API error - {e.message}") return suggestions # Example: codes that failed mapping in today's ETL run new_unmapped = [ {"source_code": "INVALID_999", "display": "Unknown Local Test"}, {"source_code": "BNP_Plasma", "display": "Brain Natriuretic Peptide"}, ] suggestions = lookup_new_codes(new_unmapped) ``` ## 6. Conclusion: OMOPHub for Discovery, Local Cache for Production The "Compute Tax" isn't loading vocabulary tables - it's doing vocabulary lookups at runtime instead of build time. The lean approach: * **Build time:** Use OMOPHub to discover and validate mappings for your unique source codes. Takes minutes. Run it once per new site or when new codes appear. * **Runtime:** Apply the mapping cache via local pandas merge or SQL JOIN. Takes milliseconds per million records. No API calls. No network dependency. OMOPHub's value isn't replacing your local vocabulary - it's making the mapping discovery phase fast and accessible without requiring a full Athena installation. Once the mappings are built, they live locally. The ETL stays lean because the vocabulary intelligence was front-loaded. Start with your messiest source system. Extract the unique codes. Run them through OMOPHub. Build the mapping cache. Plug it into your ETL. Measure the difference. # Phenotype Development & Concept Set Expansion Source: https://docs.omophub.com/guides/use-cases/phenotype-development Build comprehensive OMOP concept sets for phenotype development using OMOPHub hierarchy, mapping, and semantic search APIs for clinical research. ## 1. The "Hidden Code" Problem You'd think defining "Type 2 Diabetes" in a database would be simple. Look for ICD-10 code E11. Done, right? Not even close. In OMOP CDM data, Type 2 Diabetes is a sprawling web of concepts: the top-level SNOMED code, dozens of specific subtypes ("Type 2 DM with renal complications," "Type 2 DM with peripheral angiopathy"), equivalent ICD-10 codes, related lab measurements (HbA1c in LOINC), medications (Metformin, insulin glargine in RxNorm), and procedures. Miss even one obscure code and your research cohort is undercounted. One major study found their diabetes phenotype captured 23% fewer patients when they used a narrow concept set versus a properly expanded one. This is the omission bias problem - and it's endemic in observational research. The codes are there in the vocabulary. You just can't find them all manually. **Concept set expansion** - the process of systematically identifying every relevant concept ID for your phenotype - is the solution. It works in two dimensions: * **Vertical (hierarchical):** Follow the parent-child tree. "Type 2 Diabetes Mellitus" has dozens of SNOMED descendants. You want all of them. * **Horizontal (cross-vocabulary):** Map your SNOMED concepts to their ICD10CM, LOINC, and RxNorm equivalents, so your phenotype catches patients regardless of which coding system their data uses. **OMOPHub** is a vocabulary API that makes both of these fast. Its hierarchy API traverses ancestor/descendant relationships in a single call. Its mappings API resolves concepts across vocabularies. And its search API (including fuzzy and semantic search) helps you discover concepts you didn't know to look for. No local vocabulary database required - just API calls against the full OHDSI ATHENA vocabulary. ## 2. The Core Concept: Hierarchical vs. Semantic Expansion When building a concept set, you're doing two kinds of work: **Hierarchical expansion** follows the vocabulary's built-in structure. In SNOMED, "Atrial Fibrillation" has child concepts like "Paroxysmal atrial fibrillation," "Persistent atrial fibrillation," and "Chronic atrial fibrillation." OMOPHub's `hierarchy.descendants()` walks this tree for you - specify a concept ID, set the depth, and get back every descendant. This is the "vertical" dimension: going deeper into specificity within a single vocabulary. **Cross-vocabulary mapping** is the "horizontal" dimension. Your phenotype might be defined in SNOMED, but your billing data uses ICD-10-CM. OMOPHub's `mappings.get()` finds the "Maps to" relationships between vocabularies, ensuring your phenotype works regardless of how the data was originally coded. **Semantic search** adds a discovery layer. If you search OMOPHub for "atrial fibrillation," the semantic and fuzzy search methods can surface related concepts you might not have thought to include - terms worded differently, abbreviations, or clinically adjacent concepts that share a vocabulary neighborhood. This isn't AI "understanding" the clinical meaning; it's smart text matching against a comprehensive vocabulary index. But it's remarkably effective at catching concepts that pure hierarchy traversal would miss. OMOPHub handles the vocabulary lookup. The clinical judgment - deciding which concepts belong in your phenotype and which don't - still belongs to you. ## 3. Use Case A: Rapid Concept Set Bootstrapping (From a Single Seed) A researcher starts with a single concept and needs a comprehensive concept set. Here's how to bootstrap it. **The Scenario:** You're building an Atrial Fibrillation phenotype. You know the SNOMED concept name but need to find (a) all its SNOMED descendants and (b) related concepts you might be missing. **Code Snippet: Expanding a Concept Set from a Seed** ```bash theme={null} pip install omophub ``` ```python Python theme={null} import omophub client = omophub.OMOPHub() # Step 1: Find the seed concept's OMOP concept ID # IMPORTANT: OMOPHub uses OMOP concept IDs, not SNOMED codes directly. # SNOMED code 49436004 = "Atrial fibrillation", but the OMOP concept ID is different. # Always look it up first. seed_search = client.search.basic( "Atrial fibrillation", vocabulary_ids=["SNOMED"], domain_ids=["Condition"], page_size=3, ) seed_candidates = seed_search.get("concepts", []) if seed_search else [] if not seed_candidates: print("Could not find seed concept. Check spelling or vocabulary.") else: seed = seed_candidates[0] seed_id = seed["concept_id"] seed_name = seed.get("concept_name", "Unknown") seed_code = seed.get("concept_code", "N/A") print(f"Seed: {seed_name} (OMOP ID: {seed_id}, SNOMED: {seed_code})\n") # Collect all expanded concepts expanded = {seed_id: seed_name} # Step 2: Hierarchical expansion - get all descendants print("--- Hierarchical Expansion (Descendants) ---") try: descendants = client.hierarchy.descendants( seed_id, max_levels=5, relationship_types=["Is a"], ) desc_list = ( descendants if isinstance(descendants, list) else descendants.get("concepts", []) ) for desc in desc_list: d_id = desc["concept_id"] d_name = desc.get("concept_name", "Unknown") expanded[d_id] = d_name print(f" - {d_name} (ID: {d_id})") if not desc_list: print(" No descendants found.") except omophub.APIError as e: print(f" Hierarchy API error: {e.message}") # Step 3: Semantic search - discover related concepts # This catches concepts that aren't hierarchical children but are # clinically adjacent (e.g., "Atrial flutter" near "Atrial fibrillation") print("\n--- Semantic Search (Discovery) ---") try: semantic_results = client.search.semantic("atrial fibrillation related arrhythmia") sem_list = ( semantic_results if isinstance(semantic_results, list) else semantic_results.get("concepts", []) ) if semantic_results else [] # Filter to Condition domain, exclude concepts already found new_finds = 0 for concept in sem_list: c_id = concept.get("concept_id") if c_id and c_id not in expanded and concept.get("domain_id") == "Condition": c_name = concept.get("concept_name", "Unknown") c_vocab = concept.get("vocabulary_id", "N/A") expanded[c_id] = c_name new_finds += 1 print(f" - {c_name} (ID: {c_id}, Vocab: {c_vocab})") if new_finds == 0: print(" No additional concepts found via semantic search.") except omophub.APIError as e: print(f" Semantic search error: {e.message}") # Summary print(f"\n--- Expanded Concept Set ---") print(f"Total concepts: {len(expanded)} (1 seed + {len(expanded) - 1} expanded)") print(f"First 10: {list(expanded.values())[:10]}") ``` **The Key Insight:** The hierarchy traversal is the workhorse - it systematically captures every subtype. The semantic search is the scout - it surfaces concepts in adjacent branches or different vocabularies that hierarchy alone would miss. Together, they bootstrap a concept set in seconds that would take hours of manual ATHENA browsing. ## 4. Use Case B: Cross-Vocabulary Mapping for Phenotype Validation Your SNOMED-based phenotype needs to work against ICD-10-CM billing data. If there's no mapping, you're leaking patients. **The Scenario:** You have four SNOMED concepts defining a cardiovascular phenotype. You need their ICD-10-CM equivalents to validate against billing claims. **Code Snippet: Cross-Vocabulary Mapping** ```python Python theme={null} import omophub client = omophub.OMOPHub() # Core phenotype concepts (OMOP concept IDs - verify these via search) # In production, look these up rather than hardcoding. phenotype_concepts = [ {"name": "Atrial fibrillation", "search_term": "Atrial fibrillation"}, {"name": "Myocardial infarction", "search_term": "Myocardial infarction"}, {"name": "Hypertensive disorder", "search_term": "Hypertensive disorder"}, {"name": "Chronic kidney disease", "search_term": "Chronic kidney disease"}, ] print("Cross-Vocabulary Mapping: SNOMED -> ICD-10-CM\n") all_icd10_codes = set() for entry in phenotype_concepts: # Step 1: Resolve to OMOP concept ID via search try: results = client.search.basic( entry["search_term"], vocabulary_ids=["SNOMED"], domain_ids=["Condition"], page_size=1, ) candidates = results.get("concepts", []) if results else [] if not candidates: print(f" {entry['name']}: No SNOMED match found. Skipping.") continue concept = candidates[0] omop_id = concept["concept_id"] snomed_code = concept.get("concept_code", "N/A") print(f" {concept.get('concept_name', entry['name'])} (OMOP: {omop_id}, SNOMED: {snomed_code})") # Step 2: Get ICD-10-CM mappings mappings = client.mappings.get(omop_id, target_vocabulary="ICD10CM") mapping_list = ( mappings if isinstance(mappings, list) else mappings.get("concepts", mappings.get("mappings", [])) ) if mappings else [] if mapping_list: for m in mapping_list: icd_name = m.get("concept_name", "N/A") icd_code = m.get("concept_code", "N/A") all_icd10_codes.add(icd_code) print(f" -> ICD-10-CM: {icd_name} ({icd_code})") else: print(f" -> No direct ICD-10-CM mapping found") except omophub.APIError as e: print(f" {entry['name']}: API error - {e.message}") print() print(f"Total unique ICD-10-CM codes for phenotype: {len(all_icd10_codes)}") print(f"Codes: {sorted(all_icd10_codes)}") ``` **The Key Insight:** This is the "leak check" for your phenotype. If your study database uses ICD-10-CM for billing data and your phenotype is defined only in SNOMED, you'll miss every patient who was coded only in ICD-10. By mapping each SNOMED concept to its ICD-10-CM equivalents, you ensure the phenotype captures patients regardless of the coding system - and you can spot concepts with no cross-vocabulary mapping that might need manual attention. ## 5. Exploring Concept Relationships for Phenotype Refinement Beyond hierarchy and cross-vocabulary mapping, OMOP concepts have rich relationships to each other: "Has associated finding," "Has causative agent," "Occurs after," and more. Traversing these relationships helps you discover concepts that are clinically relevant but outside the hierarchical tree. **The Idea:** For a seed concept, explore its relationships to find clinically related concepts in other domains. A condition might have associated measurements (LOINC), treatments (RxNorm), or complications (SNOMED) linked through OMOP relationships. ```python Python theme={null} import omophub client = omophub.OMOPHub() # Find the seed concept (Type 2 Diabetes Mellitus) seed_results = client.search.basic( "Type 2 diabetes mellitus", vocabulary_ids=["SNOMED"], domain_ids=["Condition"], page_size=1, ) if seed_results and seed_results.get("concepts"): seed = seed_results["concepts"][0] seed_id = seed["concept_id"] print(f"Seed: {seed.get('concept_name')} (ID: {seed_id})\n") # Get all relationships for this concept try: relationships = client.concepts.relationships(seed_id) rel_list = ( relationships if isinstance(relationships, list) else relationships.get("relationships", []) ) if relationships else [] print(f"Found {len(rel_list)} relationships:\n") # Group by relationship type for clarity by_type = {} for rel in rel_list: rel_type = rel.get("relationship_id", "Unknown") if rel_type not in by_type: by_type[rel_type] = [] by_type[rel_type].append(rel) for rel_type, rels in sorted(by_type.items()): print(f" {rel_type} ({len(rels)} concepts):") for rel in rels[:5]: # Show up to 5 per type r_name = rel.get("concept_name", "Unknown") r_id = rel.get("concept_id", "N/A") r_vocab = rel.get("vocabulary_id", "N/A") print(f" - {r_name} (ID: {r_id}, Vocab: {r_vocab})") if len(rels) > 5: print(f" ... and {len(rels) - 5} more") print() except omophub.APIError as e: print(f"Relationship lookup error: {e.message}") else: print("Could not find seed concept.") ``` **The Key Insight:** OMOP's relationship graph is an underused goldmine for phenotype development. A single concept can have dozens of relationships to associated findings, complications, and treatments. Traversing these relationships - rather than relying solely on hierarchy - helps build multi-domain phenotypes that capture the full clinical picture. For example, a diabetes phenotype should include not just condition codes, but also HbA1c measurements (LOINC) and antidiabetic medications (RxNorm). OMOPHub's concept relationship API makes this exploration programmatic. **A note on Phoebe:** The OHDSI community has developed the Phoebe algorithm, which recommends concepts based on co-occurrence patterns in real-world OMOP data. If OMOPHub exposes Phoebe functionality (check their latest documentation), it would complement the relationship-based exploration shown above with data-driven recommendations. Phoebe is particularly valuable for identifying concepts that are empirically associated with your phenotype but not linked through formal vocabulary relationships. ## 6. Conclusion: Building Better Cohorts, Faster Phenotype quality determines research quality. An incomplete concept set means an incomplete cohort, which means biased results. The traditional approach - manually browsing ATHENA, relying on expert intuition, hoping you haven't missed a code - doesn't scale. OMOPHub makes the vocabulary mechanics fast: hierarchy traversal to capture all subtypes, cross-vocabulary mapping to ensure portability, semantic search to discover concepts you didn't know to look for, and relationship exploration to build multi-domain phenotypes. What used to take days of manual vocabulary work becomes a set of API calls. The clinical judgment - deciding which concepts belong in your phenotype and which don't - still requires human expertise. But OMOPHub gives that expertise a comprehensive starting set to work with, rather than a handful of codes pulled from memory. Start with your favorite condition. Search for it, expand its descendants, map it to ICD-10-CM, and explore its relationships. You'll almost certainly discover concepts you would have missed manually. That's the difference between a phenotype that works and one that leaks. # Population Health Analytics Source: https://docs.omophub.com/guides/use-cases/population-health-analytics Analyze population health trends using OMOPHub hierarchy expansion, cross-vocabulary mapping, and OMOP CDM SQL queries for public-health action. ## 1. From Data Points to Public Health 12,000 patients with a diabetes diagnosis in your health system. How many of them had an HbA1c test in the last 12 months? If you can't answer that question in under a minute, you have a population health problem. The challenge isn't data - it's aggregation. A "Diabetes" diagnosis in one clinic is coded as a specific ICD-10 subtype in billing, a SNOMED concept in the EHR, and possibly a local code in a legacy system. Counting "all diabetics" means resolving hundreds of concept codes across vocabularies into a single, comprehensive set. Do the same for HbA1c measurements. Then query across millions of records. Manually, this takes weeks. By the time you have your answer, the care gap has widened. **OMOPHub** makes the vocabulary resolution fast. It's a REST API for the OHDSI ATHENA vocabularies - SNOMED, ICD-10, LOINC, RxNorm, and 100+ others. Its hierarchy API lets you take a broad concept like "Type 2 Diabetes Mellitus" and retrieve every descendant code in a single call - all the specific subtypes, complications, and variants. Its search API handles fuzzy and semantic matching for disparate local names. Its mappings API resolves across vocabularies. The pattern is simple: define your clinical concept at a high level, expand it into a comprehensive concept set via OMOPHub, and plug that set into your OMOP CDM SQL queries. That turns population health questions into answerable queries - and care gaps into actionable lists. ## 2. The Core Concept: Rolling Up the Hierarchy Population health analytics lives and dies on aggregation. You don't want to count 500 individual respiratory infection codes separately - you want to count "Respiratory Infections" as a category, knowing it captures everything underneath. The OMOP vocabulary hierarchy is built for this. Every specific concept has parent concepts linked by "Is a" relationships, forming a tree. "Streptococcal pneumonia" **Is a** "Bacterial pneumonia" **Is a** "Pneumonia" **Is a** "Respiratory tract infection." If you query for the parent, and include all its descendants, you catch everything. OMOPHub's `hierarchy.descendants()` does this traversal via API. Give it a concept ID and a depth, and it returns every child, grandchild, and great-grandchild in the hierarchy. This is the "roll-up" - and it's the foundation of every use case in this article. It works at any level of granularity: * **Broad:** "Cardiovascular disease" → hundreds of descendant conditions * **Medium:** "Type 2 Diabetes Mellitus" → dozens of specific subtypes * **Narrow:** "Atrial fibrillation" → a handful of specific variants This multi-level capability means you can build dashboards that start at the disease class level, then drill down to specific conditions, then to individual patients - all driven by the same hierarchy API. ## 3. Use Case A: Identifying Gaps in Care (The "Diabetes Screening" Gap) The most actionable application of population health: finding patients who should be getting care but aren't. **The Scenario:** Your health system wants to identify all patients with a Diabetes diagnosis who haven't had an HbA1c test in the last 12 months. These patients are your outreach targets. **The Workflow:** Use OMOPHub to build two concept sets (all Diabetes condition codes, all HbA1c measurement codes), then plug them into an OMOP CDM SQL query. ```bash theme={null} pip install omophub ``` ```python Python theme={null} import omophub client = omophub.OMOPHub() # OMOP concept IDs for our parent concepts # 201826 = Type 2 diabetes mellitus (SNOMED, standard concept) # 3004410 = Hemoglobin A1c (LOINC measurement - verify via OMOPHub search) dia_parent_id = 201826 hba1c_parent_id = 3004410 print("Building concept sets for care gap analysis...\n") # --- Build Diabetes concept set --- diabetes_ids = {dia_parent_id} try: dia_desc = client.hierarchy.descendants(dia_parent_id, max_levels=5, relationship_types=["Is a"]) desc_list = dia_desc if isinstance(dia_desc, list) else dia_desc.get("concepts", []) for d in desc_list: diabetes_ids.add(d["concept_id"]) print(f" Diabetes concept set: {len(diabetes_ids)} concepts (parent + descendants)") except omophub.APIError as e: print(f" Error expanding diabetes concepts: {e.message}") # --- Build HbA1c concept set --- hba1c_ids = {hba1c_parent_id} try: hba1c_desc = client.hierarchy.descendants(hba1c_parent_id, max_levels=3, relationship_types=["Is a"]) desc_list = hba1c_desc if isinstance(hba1c_desc, list) else hba1c_desc.get("concepts", []) for d in desc_list: hba1c_ids.add(d["concept_id"]) print(f" HbA1c concept set: {len(hba1c_ids)} concepts (parent + descendants)") except omophub.APIError as e: print(f" Error expanding HbA1c concepts: {e.message}") # --- Generate the OMOP CDM SQL --- dia_ids_str = ", ".join(str(i) for i in diabetes_ids) hba1c_ids_str = ", ".join(str(i) for i in hba1c_ids) print("\n--- OMOP CDM SQL: Patients with Diabetes but No Recent HbA1c ---\n") print(f"""SELECT p.person_id, p.gender_concept_id, p.year_of_birth FROM person p JOIN condition_occurrence co ON p.person_id = co.person_id WHERE co.condition_concept_id IN ({dia_ids_str}) AND NOT EXISTS ( SELECT 1 FROM measurement m WHERE m.person_id = p.person_id AND m.measurement_concept_id IN ({hba1c_ids_str}) AND m.measurement_date >= DATEADD(month, -12, GETDATE()) );""") print("\n-- Note: Date syntax varies by database engine.") print("-- SQL Server: DATEADD(month, -12, GETDATE())") print("-- PostgreSQL: CURRENT_DATE - INTERVAL '12 months'") print("-- MySQL: DATE_SUB(CURRENT_DATE(), INTERVAL 12 MONTH)") ``` **The Key Insight:** This is OMOPHub doing exactly what it's built for. The hierarchy expansion ensures your Diabetes concept set catches patients coded as "Type 2 DM with renal complications," "Type 2 DM with peripheral angiopathy," or any other specific subtype - not just the top-level code. Without this expansion, you'd systematically undercount your care gap population. The concept set feeds directly into standard OMOP CDM SQL, making the entire workflow reproducible and auditable. ## 4. Use Case B: Disease Surveillance Across Heterogeneous Sources Public health surveillance requires aggregating data from dozens of hospitals, each with different coding practices. The goal: detect trends in broad disease categories regardless of how the source data was coded. **The Scenario:** You're monitoring "Flu-like Illness" across 50 hospitals. Some report in ICD-10-CM, some in SNOMED, and some use local codes. You need a single surveillance count. **The Workflow:** 1. Build a surveillance concept set for "Respiratory tract infection" by expanding the hierarchy 2. For incoming standard codes (ICD-10, SNOMED), look them up in OMOPHub and check if they're in the set 3. For local/proprietary codes, flag for manual mapping (OMOPHub can't resolve these) ```python Python theme={null} import omophub client = omophub.OMOPHub() # Incoming diagnosis events from various hospitals incoming_events = [ {"code": "J11.1", "vocab": "ICD10CM", "hospital": "Hospital A"}, {"code": "J06.9", "vocab": "ICD10CM", "hospital": "Hospital B"}, {"code": "FLU_SYMPT", "vocab": "LOCAL", "hospital": "Hospital C"}, {"code": "233604007", "vocab": "SNOMED", "hospital": "Hospital D"}, ] # Step 1: Build surveillance concept set via hierarchy # First, find the OMOP concept ID for "Respiratory tract infection" print("Building surveillance concept set...\n") search_result = client.search.basic( "Infection of respiratory tract", vocabulary_ids=["SNOMED"], domain_ids=["Condition"], page_size=1, ) candidates = search_result.get("concepts", []) if search_result else [] if not candidates: print("Could not find parent concept for respiratory tract infection.") else: parent = candidates[0] parent_id = parent["concept_id"] print(f" Parent: {parent.get('concept_name')} (OMOP ID: {parent_id})") # Expand to all descendants surveillance_ids = {parent_id} try: descendants = client.hierarchy.descendants(parent_id, max_levels=5, relationship_types=["Is a"]) desc_list = descendants if isinstance(descendants, list) else descendants.get("concepts", []) for d in desc_list: surveillance_ids.add(d["concept_id"]) print(f" Surveillance set: {len(surveillance_ids)} concepts\n") except omophub.APIError as e: print(f" Error expanding hierarchy: {e.message}\n") # Step 2: Classify incoming events print("Classifying incoming diagnosis events:\n") flu_count = 0 unresolved = 0 for event in incoming_events: code = event["code"] vocab = event["vocab"] hospital = event["hospital"] if vocab == "LOCAL": # OMOPHub cannot resolve proprietary local codes print(f" {hospital}: '{code}' ({vocab}) -> UNRESOLVED (local code, needs manual mapping)") unresolved += 1 continue # Look up the standard code in OMOPHub try: results = client.search.basic( code, vocabulary_ids=[vocab], domain_ids=["Condition"], page_size=1, ) matches = results.get("concepts", []) if results else [] if matches: matched = matches[0] omop_id = matched["concept_id"] c_name = matched.get("concept_name", "Unknown") if omop_id in surveillance_ids: print(f" {hospital}: '{code}' ({vocab}) -> {c_name} -> FLAGGED as flu-like illness") flu_count += 1 else: print(f" {hospital}: '{code}' ({vocab}) -> {c_name} -> Not in surveillance set") else: print(f" {hospital}: '{code}' ({vocab}) -> No match found") unresolved += 1 except omophub.APIError as e: print(f" {hospital}: '{code}' ({vocab}) -> API error: {e.message}") unresolved += 1 print(f"\nSurveillance Summary:") print(f" Flu-like illness events: {flu_count}") print(f" Unresolved events: {unresolved} (need manual review)") ``` **The Key Insight:** The hierarchy-based surveillance set is the foundation - it defines exactly which concepts count as "flu-like illness." Incoming standard codes get resolved to OMOP concept IDs and checked against this set. Local codes can't be resolved automatically - that's an honest limitation. In production, you'd maintain a local-to-standard mapping table built over time with human review. The result: a single surveillance count across heterogeneous sources, updated as fast as data arrives. ## 5. Risk Stratification: Beyond Simple Counts Two patients both have "Type 2 Diabetes." One is well-controlled with no complications. The other has diabetic retinopathy, chronic kidney disease, and a prior MI. They need very different levels of care - but a simple count treats them identically. Risk stratification means identifying which patients carry comorbidities and complications that increase their risk. OMOPHub helps by building complication concept sets programmatically. ```python Python theme={null} import omophub client = omophub.OMOPHub() # A patient's condition concept IDs (from their OMOP record) patient_conditions = {201826, 40484648, 4329847, 443727, 40480853} # Note: verify all IDs via OMOPHub search before production use # Define complication categories to check # Instead of hardcoding IDs, search for the parent concept and expand complication_categories = [ "Diabetic retinopathy", "Diabetic nephropathy", "Chronic kidney disease", "Myocardial infarction", ] print("Risk Stratification: Checking for diabetes complications\n") risk_score = 0 identified_complications = [] for comp_name in complication_categories: try: results = client.search.basic( comp_name, vocabulary_ids=["SNOMED"], domain_ids=["Condition"], page_size=1, ) candidates = results.get("concepts", []) if results else [] if not candidates: print(f" '{comp_name}': Not found in SNOMED") continue comp_concept = candidates[0] comp_id = comp_concept["concept_id"] # Expand to include subtypes comp_set = {comp_id} try: desc = client.hierarchy.descendants(comp_id, max_levels=3, relationship_types=["Is a"]) desc_list = desc if isinstance(desc, list) else desc.get("concepts", []) for d in desc_list: comp_set.add(d["concept_id"]) except omophub.APIError: pass # Use just the parent if hierarchy fails # Check if patient has any concept in this complication set overlap = patient_conditions & comp_set if overlap: risk_score += 1 identified_complications.append(comp_concept.get("concept_name", comp_name)) print(f" PRESENT: {comp_concept.get('concept_name', comp_name)} ({len(comp_set)} concepts in set, {len(overlap)} matched)") else: print(f" Absent: {comp_concept.get('concept_name', comp_name)} ({len(comp_set)} concepts checked)") except omophub.APIError as e: print(f" '{comp_name}': API error - {e.message}") print(f"\nRisk Summary:") print(f" Complications found: {len(identified_complications)} of {len(complication_categories)}") print(f" Complications: {', '.join(identified_complications) if identified_complications else 'None'}") print(f" Risk tier: {'HIGH' if risk_score >= 2 else 'MODERATE' if risk_score == 1 else 'LOW'}") ``` **The Key Insight:** The hierarchy expansion makes risk stratification robust. Instead of checking for a single concept ID (which would miss patients coded with a specific subtype), you check against an expanded set. A patient coded as "Stage 3 chronic kidney disease" gets caught by the CKD complication check because it's a descendant of the parent CKD concept. This is the same hierarchy roll-up from Use Case A, applied to a different question - and it's the pattern that makes OMOPHub genuinely useful for population health. ## 6. Conclusion: From Reactive to Proactive Population health analytics comes down to answering simple questions at scale: Who has diabetes but isn't being monitored? Is flu season hitting harder this year than last? Which patients are at highest risk for complications? The vocabulary complexity is what makes these questions hard. OMOPHub handles the vocabulary layer - expanding concept hierarchies, resolving across vocabularies, looking up codes - so you can focus on the clinical logic and the actionable insights. The pattern across all three use cases is the same: define a clinical concept, expand it into a comprehensive concept set via OMOPHub's hierarchy API, and use that set to query your OMOP CDM database. Whether you're finding care gaps, running surveillance, or stratifying risk, the vocabulary machinery is identical. That's the power of standardization. Start with Use Case A. Pick a quality measure that matters to your organization - HbA1c for diabetics, colonoscopy for age-eligible patients, statin therapy for cardiovascular risk. Build the concept sets, write the SQL, and see what falls out. The care gaps are there. OMOPHub helps you find them. # Vocabulary Lifecycle Management Source: https://docs.omophub.com/guides/use-cases/vocabulary-lifecycle-management Automate detection of stale OMOP vocabularies and deprecated concepts with OMOPHub so you only update from Athena when changes actually affect you. ## 1. The "Maintenance Tax" Every OMOP implementation has a dirty secret: the vocabulary tables. They're the backbone of everything - every concept ID, every mapping, every hierarchy traversal depends on them. And they go stale. SNOMED releases twice a year. RxNorm updates monthly. ICD-10-CM gets annual revisions. LOINC adds new lab codes quarterly. Each release can add concepts, deprecate others, change mappings, or restructure hierarchies. If your local vocabulary tables are from six months ago, your ETL is mapping against an outdated reality. A new drug approved in March doesn't exist in your January vocabulary. A SNOMED concept that was merged in the April release is still two separate concepts in your system. This is **Version Drift** - and the manual process of fixing it is the **Maintenance Tax**: download multi-gigabyte Athena files, load them into your database (hours to days), re-run ETL validation, hope nothing broke. It's a lost weekend every quarter, and most teams put it off because the operational cost is so high. **OMOPHub** doesn't eliminate the need for local vocabulary tables - your OMOP CDM requires them for performant SQL joins. But it provides a **fast, always-current API** for vocabulary lookups that complements your local installation in specific, high-value ways: * **Version checking:** Is your local SNOMED current, or has a new release dropped? * **Concept validation:** Is this concept ID still active, or was it deprecated? * **Ad-hoc resolution:** During development, look up a concept without querying your local database * **Gap detection:** Find concepts in the latest vocabulary that don't exist in your local installation Think of OMOPHub as the "weather check" for your vocabulary - you still need a roof (local tables), but you want to know when a storm is coming (vocabulary updates that could affect your data). ## 2. The Core Concept: When to Use OMOPHub vs. Local Vocabularies This distinction matters. Getting it wrong leads to either operational fragility (over-reliance on API) or stale data (ignoring updates). **Use local vocabulary tables when:** * Running production ETL on millions of records (local SQL joins are orders of magnitude faster than API calls) * Executing OMOP CDM queries that join against `concept`, `concept_relationship`, or `concept_ancestor` tables * Reproducing research results (you need a fixed vocabulary version, not a live API that might change) * Any workflow where performance and reliability are critical **Use OMOPHub API when:** * Checking if your local vocabularies are current * Validating individual concepts during development or debugging * Resolving codes in low-volume, real-time applications (CDS alerts, FHIR integration) * Detecting deprecated or changed concepts in your existing OMOP data * Prototyping before you have a full local Athena installation **The combination:** OMOPHub tells you *when* to update. Athena provides the *files* to update with. Your local database is *where* you update to. This three-part workflow is vocabulary lifecycle management done right. ## 3. Use Case A: Detecting When Your Local Vocabularies Are Stale Before updating anything, you need to know *if* an update is needed. OMOPHub can help by letting you check the current state of vocabulary concepts against what you have locally. **The Scenario:** Your ETL runs nightly. Once a week, before the ETL starts, a pre-check script queries OMOPHub to determine if any key vocabularies have changed since your last Athena download. **The Approach:** Since the OMOPHub SDK doesn't expose a dedicated vocabulary version endpoint, we use a practical proxy: check a set of well-known "sentinel" concepts from each vocabulary and compare their metadata (validity dates, standard status) against your local records. If a sentinel concept has changed, a vocabulary update is likely. ```bash theme={null} pip install omophub ``` ```python Python theme={null} import omophub client = omophub.OMOPHub() # Sentinel concepts - well-known, stable concepts from each vocabulary # If these change, the vocabulary has been updated SENTINELS = { "SNOMED": { "concept_name": "Type 2 diabetes mellitus", "search_term": "Type 2 diabetes mellitus", "expected_domain": "Condition", }, "RxNorm": { "concept_name": "Metformin", "search_term": "Metformin", "expected_domain": "Drug", }, "LOINC": { "concept_name": "Glucose [Mass/volume] in Blood", "search_term": "2339-0", "expected_domain": "Measurement", }, "ICD10CM": { "concept_name": "Type 2 diabetes mellitus without complications", "search_term": "E11.9", "expected_domain": "Condition", }, } # Your local vocabulary state (in production, read from your local OMOP database) local_state = { "SNOMED": {"last_concept_id": 201826, "last_checked": "2025-01-15"}, "RxNorm": {"last_concept_id": 1503297, "last_checked": "2025-01-15"}, "LOINC": {"last_concept_id": 3004501, "last_checked": "2025-01-15"}, "ICD10CM": {"last_concept_id": 45576876, "last_checked": "2025-01-15"}, } def check_vocabulary_freshness(): """Check if local vocabularies might be stale by querying OMOPHub sentinels.""" print("Vocabulary Freshness Check\n") alerts = [] for vocab_id, sentinel in SENTINELS.items(): print(f" Checking {vocab_id}...") try: # Step 1: Search for the sentinel concept results = client.search.basic( sentinel["search_term"], vocabulary_ids=[vocab_id], page_size=1, ) candidates = results.get("concepts", []) if results else [] if not candidates: print(f" -> Sentinel not found - possible vocabulary restructuring") alerts.append(f"{vocab_id}: sentinel concept not found") continue remote = candidates[0] remote_id = remote["concept_id"] remote_name = remote.get("concept_name", "Unknown") remote_valid_end = remote.get("valid_end_date", "N/A") remote_standard = remote.get("standard_concept", "N/A") local = local_state.get(vocab_id, {}) local_id = local.get("last_concept_id") print(f" Remote: {remote_name} (ID: {remote_id}, valid_end: {remote_valid_end}, standard: {remote_standard})") # Step 2: Check if the concept ID changed (rare but significant) if local_id and remote_id != local_id: print(f" ALERT: Concept ID changed from {local_id} to {remote_id}") alerts.append(f"{vocab_id}: sentinel concept ID changed") # Step 3: Check if the concept has been deprecated if remote_standard != "S": print(f" ALERT: Sentinel is no longer standard (standard_concept='{remote_standard}')") alerts.append(f"{vocab_id}: sentinel concept deprecated") # Step 4: Verify concept is still valid if remote_valid_end and remote_valid_end != "2099-12-31": print(f" WARNING: Concept has a non-default valid_end_date: {remote_valid_end}") if not any(vocab_id in a for a in alerts): print(f" OK: Sentinel matches expectations") except omophub.APIError as e: print(f" API error: {e.message}") alerts.append(f"{vocab_id}: API error during check") print(f"\n--- Summary ---") if alerts: print(f" {len(alerts)} alert(s) detected - consider updating from Athena:") for alert in alerts: print(f" - {alert}") print(f"\n To update: https://athena.ohdsi.org/vocabulary/list") else: print(f" All vocabularies appear current. Next check recommended in 2 weeks.") return alerts # Run the check alerts = check_vocabulary_freshness() ``` **The Key Insight:** This doesn't replace Athena downloads. It tells you *when* to do them. Instead of updating on a fixed schedule (quarterly, whether needed or not) or never (because it's too painful), you check programmatically and update only when something has changed. The sentinel approach is a lightweight proxy: for full version detection, you'd check multiple concepts per vocabulary or compare concept counts. ## 4. Use Case B: Detecting Deprecated Concepts in Your Existing OMOP Data This is where OMOPHub provides unique value that local vocabulary tables don't: checking your *existing* data against the *latest* vocabulary to find concepts that have been deprecated, merged, or replaced since you last updated. **The Scenario:** Your OMOP CDM has 50,000 unique condition concept IDs in the `condition_occurrence` table. Some of these may reference concepts that have been deprecated in the latest SNOMED release. You need to find them and map them to their current replacements. ```python Python theme={null} import omophub client = omophub.OMOPHub() def check_concepts_for_deprecation(concept_ids, batch_label=""): """ Check a list of OMOP concept IDs against OMOPHub to find deprecated ones. Returns list of deprecated concepts with replacement suggestions. """ print(f"\nChecking {len(concept_ids)} concepts for deprecation{f' ({batch_label})' if batch_label else ''}...\n") deprecated = [] valid = 0 errors = 0 for cid in concept_ids: try: # Step 1: Fetch the concept from OMOPHub concept = client.concepts.get(cid) if not concept: print(f" ID {cid}: NOT FOUND - may have been removed entirely") deprecated.append({"concept_id": cid, "status": "not_found", "replacement": None}) continue standard = concept.get("standard_concept") valid_end = concept.get("valid_end_date", "2099-12-31") name = concept.get("concept_name", "Unknown") # Step 2: Check if deprecated (non-standard or expired) if standard != "S": print(f" ID {cid}: DEPRECATED - '{name}' (standard_concept='{standard}')") # Step 3: Try to find the replacement via "Maps to" relationship replacement = None try: rels = client.concepts.relationships(cid) rel_list = ( rels if isinstance(rels, list) else rels.get("relationships", []) ) if rels else [] maps_to = [ r for r in rel_list if r.get("relationship_id") == "Maps to" and r.get("concept_id") != cid # Exclude self-maps ] if maps_to: replacement = maps_to[0] rep_name = replacement.get("concept_name", "Unknown") rep_id = replacement.get("concept_id") print(f" -> Replacement: '{rep_name}' (ID: {rep_id})") except omophub.APIError: pass deprecated.append({ "concept_id": cid, "concept_name": name, "status": "deprecated", "replacement_id": replacement.get("concept_id") if replacement else None, "replacement_name": replacement.get("concept_name") if replacement else None, }) else: valid += 1 except omophub.APIError: errors += 1 print(f"\n--- Results ---") print(f" Valid (standard): {valid}") print(f" Deprecated/removed: {len(deprecated)}") print(f" Errors: {errors}") if deprecated: print(f"\n Deprecated concepts requiring remapping:") for d in deprecated: rep = f" -> {d['replacement_name']} (ID: {d['replacement_id']})" if d.get("replacement_id") else " -> no replacement found" print(f" ID {d['concept_id']}: {d.get('concept_name', 'N/A')}{rep}") return deprecated # Example: check a sample of concept IDs from your condition_occurrence table # In production, query: SELECT DISTINCT condition_concept_id FROM condition_occurrence sample_condition_ids = [201826, 4329847, 316139, 999999999] # Last one is fake deprecated_concepts = check_concepts_for_deprecation(sample_condition_ids, batch_label="condition_occurrence") ``` **The Key Insight:** `client.concepts.get()` is a real SDK method. This pattern - iterate through your existing concept IDs, check each against OMOPHub, find the deprecated ones, look up replacements via relationships - is a genuine, high-value use of the API. You can't do this easily with local vocabulary tables if those tables are themselves stale. OMOPHub, with its always-current data, becomes a validation layer *on top of* your local installation. **For production use:** Batch this across your OMOP tables: `condition_occurrence.condition_concept_id`, `drug_exposure.drug_concept_id`, `measurement.measurement_concept_id`, etc. Run it monthly. Generate a deprecation report. Use the replacement concept IDs to plan your next vocabulary update and ETL re-mapping. ## 5. The Vocabulary Lifecycle Workflow Putting it all together: **Weekly: Freshness check** (Use Case A) * Run sentinel checks against OMOPHub * If alerts fire, schedule an Athena download **Monthly: Deprecation scan** (Use Case B) * Extract distinct concept IDs from your OMOP CDM tables * Check each against OMOPHub for deprecation * Generate a report of deprecated concepts and their replacements **Quarterly (or when alerts fire): Full vocabulary update** * Download latest vocabularies from Athena (athena.ohdsi.org) * Load into your local OMOP vocabulary tables * Re-run ETL validation against the updated vocabularies * Update your local state records for the next freshness check OMOPHub's role: steps 1 and 2 (detection). Athena's role: step 3 (the actual vocabulary files). Your database: step 3 (where the files get loaded). Each tool does what it's built for. ## 6. Conclusion: From Reactive to Proactive The Maintenance Tax isn't the vocabulary update itself - it's not knowing *when* you need one. Teams either update too often (wasting operational time on unchanged vocabularies) or too rarely (accumulating version drift that compounds with every ETL run). OMOPHub makes vocabulary lifecycle management proactive. Check freshness programmatically. Detect deprecated concepts before they corrupt your analyses. Know exactly when to pull the trigger on an Athena download, and know exactly which concepts need remapping when you do. Your local vocabulary tables remain the source of truth for your OMOP CDM. OMOPHub is the early warning system that keeps them honest. Start with the deprecation scan. Pull the distinct concept IDs from your biggest OMOP table, run them through `client.concepts.get()`, and see what comes back deprecated. That report alone is worth the integration. # FHIR → OMOP Standardization Source: https://docs.omophub.com/guides/workflows/fhir-to-omop The complete workflow from FHIR-coded clinical data to standardized OMOP CDM tables - resolve CodeableConcepts, assign domains, and load CDM tables. This guide walks through the complete path from FHIR-coded clinical data to populated OMOP CDM tables using OMOPHub. It covers vocabulary resolution, domain assignment, standard concept mapping, and CDM table placement - the four steps that every FHIR-to-OMOP transformation pipeline has to solve. If you're looking for a resource-by-resource cookbook (Condition → `condition_occurrence`, Observation → `measurement`, MedicationStatement → `drug_exposure`, and so on), see the [FHIR Integration](/guides/integration/fhir-integration) guide. This page focuses on the vocabulary standardization layer that sits at the heart of any FHIR-to-OMOP pipeline. ## 1. Why Vocabulary Resolution Is the Hard Part Converting FHIR resources to OMOP CDM tables is not primarily a schema transformation problem. The structural mapping - which FHIR fields go into which OMOP columns - is well-documented in the [HL7 FHIR-to-OMOP IG](https://build.fhir.org/ig/HL7/fhir-omop-ig/). The hard part is vocabulary standardization: taking the coded clinical concepts in your FHIR data and resolving them to the correct OMOP standard concepts. This is hard because: * FHIR `CodeableConcept` fields can contain codes from any vocabulary system - SNOMED CT, ICD-10-CM, LOINC, RxNorm, local hospital codes, or several simultaneously * OMOP requires a specific `*_concept_id` column in each clinical table to point at a **standard concept**, and the vocabulary domain determines which CDM table the record belongs to * The same clinical idea can appear as different codes in different systems, and a single ICD-10 code can map to multiple SNOMED concepts * Some FHIR codes are already standard OMOP concepts (most SNOMED codes), while others need mapping via `Maps to` relationships (ICD-10, NDC, local codes) OMOPHub handles this resolution layer so your pipeline doesn't have to maintain a local vocabulary database. See [Why OMOPHub vs Self-Hosting](/guides/production/why-omophub) for the comparison against hosting ATHENA yourself. ## 2. The Four-Step Flow Every FHIR-to-OMOP vocabulary resolution follows the same pattern. A FHIR resource contains one or more `CodeableConcept` or `Coding` elements. For example, a FHIR `Condition` resource might contain: ```json theme={null} { "resourceType": "Condition", "code": { "coding": [ { "system": "http://snomed.info/sct", "code": "44054006", "display": "Type 2 diabetes mellitus" }, { "system": "http://hl7.org/fhir/sid/icd-10-cm", "code": "E11.9", "display": "Type 2 diabetes mellitus without complications" } ] } } ``` This Condition has two codings for the same clinical idea: one in SNOMED CT, one in ICD-10-CM. Send the codings to OMOPHub's Concept Resolver. It handles vocabulary identification, concept lookup, `Maps to` traversal, and OHDSI vocabulary preference ranking in a single call: ```bash theme={null} curl -X POST https://api.omophub.com/v1/fhir/resolve/codeable-concept \ -H "Authorization: Bearer oh_your_api_key" \ -H "Content-Type: application/json" \ -d '{ "coding": [ { "system": "http://snomed.info/sct", "code": "44054006" }, { "system": "http://hl7.org/fhir/sid/icd-10-cm", "code": "E11.9" } ], "resource_type": "Condition" }' ``` The Resolver returns a `best_match` based on OHDSI vocabulary preference (SNOMED > RxNorm > LOINC > CVX > ICD-10 for conditions), plus `alternatives` and `unresolved` arrays. The important payload is nested under `best_match.resolution`: ```json theme={null} { "data": { "best_match": { "resolution": { "source_concept": { "concept_id": 45576876, "concept_code": "44054006", "concept_name": "Type 2 diabetes mellitus", "vocabulary_id": "SNOMED", "standard_concept": "S" }, "standard_concept": { "concept_id": 201826, "concept_name": "Type 2 diabetes mellitus", "vocabulary_id": "SNOMED", "domain_id": "Condition", "concept_class_id": "Clinical Finding", "standard_concept": "S" }, "mapping_type": "direct", "target_table": "condition_occurrence", "domain_resource_alignment": "aligned" } }, "alternatives": [ { "resolution": { "source_concept": { "concept_id": 45576876, "vocabulary_id": "ICD10CM", "concept_code": "E11.9" }, "standard_concept": { "concept_id": 201826, "concept_name": "Type 2 diabetes mellitus", "domain_id": "Condition" }, "mapping_type": "mapped" } } ], "unresolved": [] } } ``` Key things to notice: * Both the SNOMED code (already standard) and the ICD-10 code (mapped via `Maps to`) resolve to the same standard concept: `201826` * The SNOMED coding wins as `best_match` because SNOMED is the preferred vocabulary for the Condition domain in OHDSI conventions * `mapping_type` tells you what happened: `direct` (source was already standard), `mapped` (followed `Maps to`), `semantic_match` (fell back to text-based search), or `unmapped` * `target_table` tells you which CDM table the record belongs to - computed from the standard concept's domain, not from the FHIR resource type * `domain_resource_alignment` is `aligned` when the FHIR `resource_type` you declared matches the concept's OMOP domain - useful as a sanity signal for mis-coded data The `domain_id` in the standard concept determines which OMOP CDM table the record belongs to. This is critical, and it's vocabulary-driven, not FHIR-resource-driven. | domain\_id | Target OMOP CDM table | | ------------- | ---------------------- | | `Condition` | `condition_occurrence` | | `Drug` | `drug_exposure` | | `Measurement` | `measurement` | | `Observation` | `observation` | | `Procedure` | `procedure_occurrence` | | `Device` | `device_exposure` | | `Specimen` | `specimen` | | `Visit` | `visit_occurrence` | The FHIR resource type does NOT always determine the OMOP CDM table. A FHIR `Observation` resource carrying a blood glucose measurement (LOINC) maps to the `measurement` table, not `observation`. A FHIR `Condition` resource carrying a lab-derived finding might map to `measurement`. Always use the vocabulary domain (from `standard_concept.domain_id`, or read `resolution.target_table` directly) for table assignment. With the standard concept resolved and the target table identified, populate the CDM row: ```sql theme={null} INSERT INTO condition_occurrence ( person_id, condition_concept_id, -- 201826 (from standard_concept.concept_id) condition_start_date, -- from FHIR Condition.onsetDateTime condition_type_concept_id, -- 32817 (EHR) or per your convention condition_source_value, -- "44054006" (original source code) condition_source_concept_id -- 45576876 (from source_concept.concept_id) ) VALUES ( :person_id, 201826, :onset_date, 32817, '44054006', 45576876 ); ``` ## 3. Working with the Python SDK The same four-step flow, in Python: ```python theme={null} import omophub client = omophub.OMOPHub() # Step 1: Extract codings from your FHIR resource (you parse the JSON) fhir_condition = { "coding": [ {"system": "http://snomed.info/sct", "code": "44054006"}, {"system": "http://hl7.org/fhir/sid/icd-10-cm", "code": "E11.9"}, ], } # Step 2: Resolve result = client.fhir.resolve_codeable_concept( coding=fhir_condition["coding"], resource_type="Condition", ) res = result["best_match"]["resolution"] # Step 3: Read domain and table assignment print(f"Standard concept: {res['standard_concept']['concept_id']} ({res['standard_concept']['concept_name']})") print(f"Domain: {res['standard_concept']['domain_id']}") print(f"Target table: {res['target_table']}") print(f"Mapping type: {res['mapping_type']}") # Step 4: Use the resolved values in your ETL # insert into the appropriate CDM table with the standard + source concept IDs ``` If your pipeline already parses FHIR with `fhir.resources` or `fhirpy`, you can pass those `Coding` / `CodeableConcept` objects directly to the resolver via duck typing - see [Type Interoperability](/sdks/python/fhir#type-interoperability) in the Python SDK reference. Neither library is a required dependency. ## 4. Batch Processing: The ETL Pattern In a real ETL pipeline you're processing thousands of FHIR resources. Don't resolve one at a time - use the batch endpoint. Deduplicate first, then batch-resolve the unique codings in chunks of 100: ```python theme={null} import omophub import json client = omophub.OMOPHub() # Load a FHIR Bundle (e.g., from a Bulk FHIR export) with open("fhir_bundle.json") as f: bundle = json.load(f) # Step 1: Extract all unique codings across all resources in the bundle all_codings = set() for entry in bundle["entry"]: resource = entry["resource"] if resource["resourceType"] == "Condition" and "code" in resource: for c in resource["code"].get("coding", []): all_codings.add((c["system"], c["code"])) elif resource["resourceType"] == "MedicationRequest": med = resource.get("medicationCodeableConcept", {}) for c in med.get("coding", []): all_codings.add((c["system"], c["code"])) # ... handle other resource types print(f"Total resources: {len(bundle['entry']):,}") print(f"Unique codings: {len(all_codings):,}") # Step 2: Batch resolve unique codings (100 per call) codings_list = [{"system": s, "code": c} for s, c in all_codings] cache = {} for i in range(0, len(codings_list), 100): chunk = codings_list[i : i + 100] result = client.fhir.resolve_batch(chunk) for item in result["results"]: if "resolution" in item: src = item["resolution"]["source_concept"] cache[(src["vocabulary_id"], src["concept_code"])] = item["resolution"] else: # Failed coding - log for manual review print(f" Failed: {item['error']['code']} - {item['error']['message']}") # Steps 3-4: Apply the cache to every row in your full dataset # (pandas merge / SQL JOIN / dict lookup, depending on your pipeline) ``` The deduplication step is critical. A FHIR Bulk Export with 500,000 `Condition` resources might contain only 2,000 unique diagnosis codes. Map the 2,000, then join against the full dataset. See [Batch & Performance](/guides/production/batch-performance) for the full pattern. ## 5. Using the FHIR R4 Terminology Service If your pipeline already speaks FHIR (you're integrating with HAPI FHIR or EHRbase, or you're building a spec-conformant client), you can use OMOPHub's FHIR R4 terminology operations instead of the REST resolver: **`$lookup`** - get concept details for a code: ```bash theme={null} curl "https://fhir.omophub.com/fhir/r4/CodeSystem/\$lookup?\ system=http://snomed.info/sct&code=44054006" \ -H "Authorization: Bearer oh_your_api_key" ``` **`$translate`** - map between vocabularies: ```bash theme={null} curl "https://fhir.omophub.com/fhir/r4/ConceptMap/\$translate?\ system=http://hl7.org/fhir/sid/icd-10-cm&code=E11.9&\ target=http://snomed.info/sct" \ -H "Authorization: Bearer oh_your_api_key" ``` **`$validate-code`** - check if a code exists: ```bash theme={null} curl "https://fhir.omophub.com/fhir/r4/CodeSystem/\$validate-code?\ url=http://snomed.info/sct&code=44054006" \ -H "Authorization: Bearer oh_your_api_key" ``` These return standard FHIR `Parameters` responses and can be consumed directly by FHIR-aware clients. See the [FHIR Terminology Service overview](/api-reference/fhir-terminology/overview) for the full operation reference. **Concept Resolver vs FHIR Terminology Service - when to use which:** Use the **Concept Resolver** (`/v1/fhir/resolve*`) when you want the complete OMOP mapping chain in one call - source concept, standard concept, domain, target CDM table, mapping type. This is purpose-built for ETL pipelines and returns OMOPHub's native JSON envelope. Use the **FHIR R4 Terminology Service** (`/fhir/r4/*`) when you're integrating with FHIR infrastructure (HAPI FHIR, EHRbase, Firely) that expects spec-conformant FHIR operations and `OperationOutcome` error responses. Both use the same underlying vocabulary data. The difference is response format and how much resolution logic the server hands you in a single call. ## 6. Handling Edge Cases ### One-to-many mappings A single ICD-10 code can map to multiple SNOMED standard concepts (e.g. a combination diagnosis that splits into separate Condition and Observation concepts). The single-coding resolver returns `alternative_standard_concepts` alongside the primary `standard_concept`. For ETL pipelines, inspect that array and decide whether to write one row per alternative or pick the highest-quality match. ### Unmapped codes If a code doesn't map to any standard concept, the Resolver returns `mapping_type: "unmapped"` with no `standard_concept`. Your pipeline should: 1. Store the source code in the `*_source_value` field 2. Set `*_concept_id` to `0` (OMOP convention for unmapped) 3. Log the unmapped code for manual review - don't silently drop records ### Local / proprietary codes Hospital-specific codes with custom FHIR system URIs (e.g. `http://hospital.local/codes`) won't be in the OMOP vocabulary tables. Two options: * **Pass a `display` value with no `system`/`code`.** The Resolver falls back to semantic search over the display text, scoped to the `resource_type` domain, and returns `mapping_type: "semantic_match"` with a `similarity_score`. * **Pre-map your local codes to standard vocabularies** as part of your site configuration, then send standard codes to the Resolver. See [Collaborative Mapping](/guides/use-cases/collaborative-mapping) for the shared-mapping-file pattern. ## 7. The Complete Pipeline Architecture ``` ┌──────────────┐ ┌──────────────────┐ ┌──────────────────┐ │ FHIR Source │ │ OMOPHub │ │ OMOP CDM │ │ │ │ │ │ │ │ Condition │ │ 1. Deduplicate │ │ condition_ │ │ Observation │ │ unique codes │ │ occurrence │ │ Medication │────▶│ │────▶│ measurement │ │ Procedure │ │ 2. Batch resolve │ │ drug_exposure │ │ ... │ │ via Concept │ │ procedure_ │ │ │ │ Resolver │ │ occurrence │ │ │ │ │ │ observation │ │ │ │ 3. Cache results │ │ ... │ │ │ │ │ │ │ │ │ │ 4. Apply to full │ │ │ │ │ │ dataset │ │ │ └──────────────┘ └──────────────────┘ └──────────────────┘ ``` The first ETL run hits the Resolver most. Every subsequent run hits it less, because the mapping cache grows and only genuinely new codes need resolution. By the third or fourth run, the bottleneck stops being API calls and starts being whatever else your pipeline is doing. ## 8. What to Read Next Resource-by-resource mappings: Condition, Observation, MedicationRequest, Procedure, and more, with the exact Coding extraction logic for each. Full operation reference for the FHIR R4 Terminology Service: `$lookup`, `$translate`, `$validate-code`, `$expand`, `$subsumes`, `$find-matches`, `$closure`, `$diff`. Build validated mapping caches during development and apply them locally at production speed. Share mappings across teams via `source_to_concept_map` files. Deduplication, batch endpoints, cache patterns for ETL at scale. What OMOPHub does not do. FHIR-specific caveats, vocabulary exclusions, and what's on the roadmap. # Introduction Source: https://docs.omophub.com/introduction OMOPHub is a REST API for OHDSI ATHENA medical vocabularies - search 11M+ OMOP concepts across SNOMED CT, ICD-10, LOINC, RxNorm, and 100+ terminologies. OMOPHub is a REST API that gives you programmatic access to the full OHDSI ATHENA vocabulary set - SNOMED CT, ICD-10, LOINC, RxNorm, and 100+ medical terminologies covering 11 million standardized OMOP concepts. No multi-gigabyte downloads, no local PostgreSQL setup, no quarterly vocabulary maintenance. Get an API key and start querying. ## What You Can Do Full-text search with faceted filtering, fuzzy matching, autocomplete, and semantic similarity powered by OMOPHub's neural embeddings. Find concepts even when the phrasing doesn't match the canonical terminology. Send a FHIR system URI + code (or a full `CodeableConcept`) and get back the OMOP standard concept, domain assignment, mapping type, and CDM target table - all in one API call. Handles `Maps to` traversal automatically. Translate codes across SNOMED, ICD-10-CM, LOINC, RxNorm, HCPCS, NDC, and every other OMOP vocabulary. Single-code, batch (up to 100 per request), and `CodeableConcept` variants with OHDSI vocabulary preference ranking. Walk ancestor / descendant trees, expand concept sets, and explore non-hierarchical relationships (`Has ingredient`, `RxNorm has dose form`, etc.). Build phenotype definitions that catch all relevant codes, not just the ones you knew to look for. `$lookup`, `$validate-code`, `$translate`, `$expand`, `$subsumes`, `$find-matches`, `$closure`, plus the OMOP-specific `$diff` for vocabulary-release comparison. R4, R4B, R5, and R6 on the same endpoint. Plugs into HAPI FHIR, EHRbase, and any FHIR-aware client. MCP Server exposes 11 tools to Claude, Cursor, VS Code, and any MCP-compatible client. Eliminate hallucinated codes by grounding LLM outputs against the OMOP source of truth - multilingual queries are accepted and translated on the agent side. ## Who Uses OMOPHub * **ETL developers** mapping source data into OMOP CDM tables * **FHIR integrators** resolving clinical codes to standard OMOP concepts at the point of care * **Researchers** building phenotype definitions and concept sets * **Pharma RWE / HEOR teams** validating vocabulary coverage across studies * **AI / LLM builders** grounding clinical AI pipelines against real terminology * **openEHR / EHRbase teams** validating archetype terminology bindings against live vocabularies More than 250 teams across academic medical centers, pharma, and healthcare-technology companies use OMOPHub. ## Choose Your Starting Point Get your API key and make your first request in 5 minutes. The complete FHIR story - FHIR Resolver for ETL, FHIR R4/R5/R6 Terminology Service for clients, and when to use which. Spec-conformant `$lookup`, `$translate`, `$validate-code`, `$expand`, `$subsumes` and OMOP-specific operations. Connect Claude, Cursor, VS Code, or any MCP client to medical vocabularies. Validate openEHR template terminology bindings against OMOP vocabularies via FHIR. Point a HAPI FHIR server at OMOPHub as a remote terminology backend. `pip install omophub` - search, map, resolve, and traverse from Python. `install.packages("omophub")` - R6 client for vocabulary access in R workflows. ## Quick Example One API call per common pattern: ```bash Search theme={null} curl "https://api.omophub.com/v1/search/concepts?query=type+2+diabetes&vocabulary_ids=SNOMED&page_size=3" \ -H "Authorization: Bearer oh_your_api_key" ``` ```bash FHIR Resolve theme={null} 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" }' ``` ```python Python theme={null} import omophub client = omophub.OMOPHub() # Search by meaning (semantic embeddings) results = client.search.semantic("heart failure with reduced ejection fraction") # Resolve a FHIR code to its OMOP standard concept + CDM target table resolved = client.fhir.resolve( system="http://snomed.info/sct", code="44054006", resource_type="Condition", ) print(resolved["resolution"]["standard_concept"]["concept_name"]) print(resolved["resolution"]["target_table"]) # "condition_occurrence" # Walk the concept hierarchy descendants = client.hierarchy.descendants(201826, max_levels=2) ``` ```r R theme={null} library(omophub) client <- OMOPHubClient$new(api_key = "oh_your_api_key") # Get a concept concept <- client$concepts$get(201826) print(concept$concept_name) # "Type 2 diabetes mellitus" # Search concepts results <- client$search$basic("type 2 diabetes", vocabulary_ids = "SNOMED") # Map between vocabularies mappings <- client$mappings$get(201826, target_vocabulary = "ICD10CM") ``` ```bash FHIR R4 theme={null} curl "https://fhir.omophub.com/fhir/r4/CodeSystem/\$lookup?system=http://snomed.info/sct&code=44054006" \ -H "Authorization: Bearer oh_your_api_key" ``` The `FHIR Resolve` call is the single-API-call workflow no other terminology server offers: source code in, OMOP standard concept + CDM target table out, with `Maps to` traversal and semantic fallback handled server-side. See the [FHIR Integration guide](/guides/integration/fhir-integration) for the full response shape and batch variants. ## Platform at a Glance | | | | ---------------------- | ------------------------------------------------------------------------------------------------------- | | **Concepts** | 11M+ standardized OMOP concepts | | **Vocabularies** | 100+ including SNOMED CT, ICD-10, LOINC, RxNorm, NDC, HCPCS, ATC | | **FHIR system URIs** | 20 canonical vocabulary URIs + OMOP unified omnibus | | **FHIR operations** | `$lookup`, `$validate-code`, `$translate`, `$expand`, `$subsumes`, `$find-matches`, `$closure`, `$diff` | | **FHIR wire versions** | R4, R4B, R5, R6 on the same endpoint | | **Search** | Full-text, faceted, fuzzy, autocomplete, semantic | | **SDKs** | Python (PyPI), R (CRAN), MCP Server (npm) | | **MCP tools** | 11 tools for AI agent integration | | **Response time** | Sub-50ms typical | | **Vocabulary updates** | Synced with OHDSI ATHENA releases | ## Need Help? Full endpoint documentation with request / response examples. API uptime and performance. Manage API keys, usage, billing, and enterprise plans. Source for the SDKs, MCP Server, and reference implementations. # Quickstart - search, map, and resolve codes in 5 min Source: https://docs.omophub.com/quickstart Get up and running with the OMOPHub API in five minutes - search OMOP concepts, map across vocabularies, and resolve FHIR codes to standard CDM types. Get up and running with the OMOPHub API in 5 minutes. ## Prerequisites You'll need: * An OMOPHub account ([sign up here](https://dashboard.omophub.com/register)) * An API key ([generate one here](https://dashboard.omophub.com/api-keys)) * Python 3.10+, R 4.1+, or cURL ## Step 1: Install the SDK ```bash Python theme={null} pip install omophub ``` ```r R theme={null} install.packages("omophub") ``` ```bash cURL theme={null} # No installation needed - just your API key ``` ## Step 2: Set Up Authentication ```python Python theme={null} import omophub # Option 1: environment variable (recommended) # export OMOPHUB_API_KEY="oh_xxxxxxxxx" client = omophub.OMOPHub() # Option 2: pass directly client = omophub.OMOPHub(api_key="oh_xxxxxxxxx") ``` ```r R theme={null} library(omophub) # Option 1: environment variable (recommended) # Sys.setenv(OMOPHUB_API_KEY = "oh_xxxxxxxxx") client <- OMOPHubClient$new() # Option 2: pass directly client <- OMOPHubClient$new(api_key = "oh_xxxxxxxxx") ``` ```bash cURL theme={null} # Include in every request header curl -H "Authorization: Bearer oh_xxxxxxxxx" \ "https://api.omophub.com/v1/vocabularies" ``` Never hardcode your API key in source code. Use environment variables or a secrets manager in production. ## Step 3: Search for Concepts Search across 11M+ concepts by keyword, then by meaning: ```python Python theme={null} # Keyword search results = client.search.basic("hypertension", page_size=5) for concept in results["concepts"]: print(f"{concept['concept_id']}: {concept['concept_name']} ({concept['vocabulary_id']})") # Semantic search - find concepts by clinical meaning, not just keyword match results = client.search.semantic("high blood pressure that comes and goes", page_size=5) for concept in results["concepts"]: print(f"{concept['concept_id']}: {concept['concept_name']}") ``` ```r R theme={null} # Keyword search results <- client$search$basic("hypertension", page_size = 5) for (concept in results$concepts) { cat(sprintf("%s: %s (%s)\n", concept$concept_id, concept$concept_name, concept$vocabulary_id)) } # Semantic search - find concepts by clinical meaning results <- client$search$semantic( "high blood pressure that comes and goes", page_size = 5 ) ``` ```bash cURL theme={null} # Keyword search curl "https://api.omophub.com/v1/search/concepts?query=hypertension&page_size=5" \ -H "Authorization: Bearer oh_xxxxxxxxx" # Semantic search curl "https://api.omophub.com/v1/concepts/semantic-search?query=high+blood+pressure+that+comes+and+goes&page_size=5" \ -H "Authorization: Bearer oh_xxxxxxxxx" ``` Semantic search uses neural embeddings to match by clinical meaning. Try it when keyword search doesn't find what you need - it handles synonyms, abbreviations, and natural-language descriptions. ## Step 4: Get Concept Details Look up a specific concept by its OMOP `concept_id`: ```python Python theme={null} concept = client.concepts.get(201826) print(f"Name: {concept['concept_name']}") # Type 2 diabetes mellitus print(f"Code: {concept['concept_code']}") # 44054006 print(f"Vocabulary: {concept['vocabulary_id']}") # SNOMED print(f"Domain: {concept['domain_id']}") # Condition print(f"Standard: {concept['standard_concept']}") # S ``` ```r R theme={null} concept <- client$concepts$get(201826) cat(sprintf("Name: %s\n", concept$concept_name)) cat(sprintf("Code: %s\n", concept$concept_code)) cat(sprintf("Vocabulary: %s\n", concept$vocabulary_id)) cat(sprintf("Domain: %s\n", concept$domain_id)) ``` ```bash cURL theme={null} curl "https://api.omophub.com/v1/concepts/201826" \ -H "Authorization: Bearer oh_xxxxxxxxx" ``` ## Step 5: Navigate Hierarchies Walk the ancestor / descendant tree to expand a concept set: ```python Python theme={null} # Ancestors (parents) result = client.hierarchy.ancestors(201826, max_levels=2) for ancestor in result["ancestors"]: level = ancestor.get("hierarchy_level", 0) print(f"{' ' * level}{ancestor['concept_name']}") # Descendants (children) result = client.hierarchy.descendants(201826, max_levels=1) print(f"\n{len(result['descendants'])} child concepts") ``` ```r R theme={null} # Ancestors result <- client$hierarchy$ancestors(201826, max_levels = 2) for (ancestor in result$ancestors) { level <- if (is.null(ancestor$hierarchy_level)) 0 else ancestor$hierarchy_level cat(strrep(" ", level), ancestor$concept_name, "\n", sep = "") } # Descendants result <- client$hierarchy$descendants(201826, max_levels = 1) cat(sprintf("%d child concepts\n", length(result$descendants))) ``` ```bash cURL theme={null} # Ancestors curl "https://api.omophub.com/v1/concepts/201826/ancestors?max_levels=2" \ -H "Authorization: Bearer oh_xxxxxxxxx" # Descendants curl "https://api.omophub.com/v1/concepts/201826/descendants?max_levels=1" \ -H "Authorization: Bearer oh_xxxxxxxxx" ``` ## Step 6: Map Between Vocabularies Translate codes across SNOMED, ICD-10, LOINC, RxNorm, and every other OMOP vocabulary: ```python Python theme={null} # Map SNOMED "Type 2 diabetes" to ICD-10-CM result = client.mappings.get(201826, target_vocabulary="ICD10CM") for m in result["data"]["mappings"]: print(f"{m['relationship_id']}: {m['target_concept_name']} " f"({m['target_vocabulary_id']}: {m['target_concept_code']})") ``` ```r R theme={null} result <- client$mappings$get(201826, target_vocabulary = "ICD10CM") for (m in result$data$mappings) { cat(sprintf("%s: %s (%s: %s)\n", m$relationship_id, m$target_concept_name, m$target_vocabulary_id, m$target_concept_code )) } ``` ```bash cURL theme={null} curl "https://api.omophub.com/v1/concepts/201826/mappings?target_vocabularies=ICD10CM" \ -H "Authorization: Bearer oh_xxxxxxxxx" ``` For ETL workloads, the batch endpoint `client.mappings.map(target_vocabulary="...", source_concepts=[...])` maps up to 100 concepts per request and counts as a single API call against your quota. ## Step 7: Resolve a FHIR Code to OMOP This is the single call no other terminology server offers - send a FHIR `Coding` (system URI + code), get back the OMOP standard concept, domain, mapping type, and the exact CDM target table for ETL placement: ```python Python theme={null} result = client.fhir.resolve( system="http://snomed.info/sct", code="44054006", resource_type="Condition", ) res = result["resolution"] print(f"Standard concept: {res['standard_concept']['concept_id']} - {res['standard_concept']['concept_name']}") print(f"Domain: {res['standard_concept']['domain_id']}") print(f"Target table: {res['target_table']}") # condition_occurrence print(f"Mapping type: {res['mapping_type']}") # direct ``` ```r R theme={null} result <- client$fhir$resolve( system = "http://snomed.info/sct", code = "44054006", resource_type = "Condition" ) res <- result$resolution cat(sprintf("Standard concept: %d - %s\n", res$standard_concept$concept_id, res$standard_concept$concept_name)) cat(sprintf("Target table: %s\n", res$target_table)) cat(sprintf("Mapping type: %s\n", res$mapping_type)) ``` ```bash cURL theme={null} curl -X POST "https://api.omophub.com/v1/fhir/resolve" \ -H "Authorization: Bearer oh_xxxxxxxxx" \ -H "Content-Type: application/json" \ -d '{ "system": "http://snomed.info/sct", "code": "44054006", "resource_type": "Condition" }' ``` The Resolver handles URI → OMOP vocabulary lookup, `Maps to` traversal for non-standard codes, and semantic fallback for display-text-only inputs in a single call. For the full response shape, batch variants, and the `CodeableConcept` endpoint, see the [FHIR Integration guide](/guides/integration/fhir-integration). ## Next Steps The complete FHIR story - Resolver for ETL, Terminology Service for clients, and when to use which. `$lookup`, `$translate`, `$validate-code`, `$expand`, `$subsumes` reference. Connect Claude, Cursor, or VS Code to medical vocabularies via the MCP Server. Full SDK reference: search, concepts, hierarchy, mappings, FHIR resolver. R6 client for vocabulary access in R workflows. Complete endpoint documentation with request / response schemas. Real-world workflows: clinical coding, phenotype development, lab normalization, and more. ## Common Issues * Check that your API key is correct and starts with `oh_` * Ensure the `Authorization: Bearer ` header format is exact * Verify the key at [dashboard.omophub.com/api-keys](https://dashboard.omophub.com/api-keys) * Free tier: 2 requests per second, monthly call quota per plan * Use batch endpoints (concepts, mappings, semantic search) to reduce call count - each batch counts as one API call * Respect the `Retry-After` response header * See [Rate Limits](/api-reference/rate-limit) for plan-specific limits * Try broader search terms or remove filters * Use semantic search for natural-language queries (Step 3) * Check `vocabulary_id` spelling - it's `SNOMED`, not `SNOMED-CT`; `ICD10CM`, not `ICD-10-CM` * Confirm the target vocabulary is currently supported via `client.vocabularies.list()` # Python SDK - Working with OMOP medical concepts Source: https://docs.omophub.com/sdks/python/concepts Work with OMOP medical concepts in the OMOPHub Python SDK - retrieve, search, and inspect concept details, synonyms, and relationships programmatically. ## Get a Concept Retrieve a concept by its OMOP concept ID: ```python theme={null} concept = client.concepts.get(201826) print(concept["concept_name"]) # "Type 2 diabetes mellitus" print(concept["vocabulary_id"]) # "SNOMED" print(concept["domain_id"]) # "Condition" ``` ## Get by Vocabulary Code Look up a concept using a vocabulary-specific code: ```python theme={null} # Get SNOMED concept by its code concept = client.concepts.get_by_code("SNOMED", "44054006") print(concept["concept_name"]) # "Type 2 diabetes mellitus" # Get ICD-10-CM concept concept = client.concepts.get_by_code("ICD10CM", "E11") # Include synonyms and relationships concept = client.concepts.get_by_code( "SNOMED", "44054006", include_synonyms=True, include_relationships=True, ) print(concept["synonyms"]) # List of synonym names print(concept["relationships"]) # {"parents": [...], "children": [...]} # Specify vocabulary release version concept = client.concepts.get_by_code( "SNOMED", "44054006", vocab_release="2025.2", ) ``` ## Batch Get Concepts Retrieve multiple concepts in a single request (max 100): ```python theme={null} # Basic batch request result = client.concepts.batch([201826, 4329847, 1112807]) for concept in result["data"]["concepts"]: print(f"{concept['concept_id']}: {concept['concept_name']}") # With optional parameters result = client.concepts.batch( [201826, 4329847, 1112807], include_relationships=True, include_synonyms=True, include_mappings=True, vocabulary_filter=["SNOMED"], standard_only=False, # default ) # Check summary print(f"Retrieved: {result['data']['summary']['successful_retrievals']}") print(f"Failed: {result['data']['summary']['failed_retrievals']}") ``` ## Autocomplete Suggestions Get concept suggestions for autocomplete functionality: ```python theme={null} # Basic suggestions with pagination result = client.concepts.suggest("diab", page_size=10) for concept in result["data"]: print(concept["concept_name"]) # Check pagination metadata print(result["meta"]["pagination"]["total_items"]) print(result["meta"]["pagination"]["has_next"]) # Filter by vocabulary and domain result = client.concepts.suggest( "diab", vocabulary_ids=["SNOMED"], domain_ids=["Condition"], page_size=20, ) # Navigate pages result = client.concepts.suggest("diab", page=2, page_size=10) ``` ## Get Relationships Get relationships for a concept: ```python theme={null} # Get all relationships with pagination result = client.relationships.get(201826) for rel in result["data"]["relationships"]: print(f"{rel['relationship_id']}: {rel['concept_2']['concept_name']}") # Check pagination print(f"Page {result['meta']['pagination']['page']} of {result['meta']['pagination']['total_pages']}") print(f"Total relationships: {result['meta']['pagination']['total_items']}") # Filter by relationship type and vocabulary result = client.relationships.get( 201826, relationship_ids=["Is a", "Subsumes"], vocabulary_ids=["SNOMED"], standard_only=True, page=1, page_size=50 ) # Include reverse relationships result = client.relationships.get( 201826, include_reverse=True, domain_ids=["Condition"] ) ``` ## Get Related Concepts Find concepts related to a given concept: ```python theme={null} # Basic related concepts result = client.concepts.related(201826, page_size=10) for related in result["data"]: print(f"{related['concept_name']} ({related['relationship_id']}): {related['relationship_score']:.2f}") # Filter by relationship type and minimum score result = client.concepts.related( 201826, relationship_types=["Is a", "Maps to"], min_score=0.5, page_size=20 ) # Specify vocabulary release result = client.concepts.related( 201826, vocab_release="2025.1" ) ``` ## Get Recommended Concepts Get curated concept recommendations using the OHDSI Phoebe algorithm: ```python theme={null} # Basic recommendations for diabetes-related concepts result = client.concepts.recommended([201826, 4329847]) for source_id, recommendations in result["data"].items(): print(f"\nRecommendations for concept {source_id}:") for rec in recommendations[:5]: print(f" - {rec['concept_name']} ({rec['vocabulary_id']}) via {rec['relationship_id']}") # With filters result = client.concepts.recommended( [201826], relationship_types=["Has finding", "Associated finding"], vocabulary_ids=["SNOMED", "LOINC"], domain_ids=["Condition", "Measurement"], standard_only=True, page_size=50 ) # Access pagination metadata print(f"Page {result['meta']['pagination']['page']} of {result['meta']['pagination']['total_pages']}") print(f"Total recommendations: {result['meta']['pagination']['total_items']}") ``` ## Navigate Hierarchy ### Get Complete Hierarchy Get both ancestors and descendants in a single request: ```python theme={null} # Flat format (default) - separate arrays for ancestors and descendants result = client.hierarchy.get( 201826, max_levels=3, vocabulary_ids=["SNOMED"], ) print(f"Total ancestors: {result['data']['total_ancestors']}") print(f"Total descendants: {result['data']['total_descendants']}") # Graph format for visualization (D3.js, Cytoscape, etc.) graph = client.hierarchy.get( 201826, format="graph", max_levels=3, ) nodes = graph['data']['nodes'] # Concept nodes edges = graph['data']['edges'] # Relationship edges print(f"Nodes: {len(nodes)}, Edges: {len(edges)}") # Advanced filtering options result = client.hierarchy.get( 201826, domain_ids=["Condition"], relationship_types=["Is a"], max_results=100, include_invalid=False, ) ``` #### Hierarchy Get Parameters | Parameter | Type | Default | Description | | -------------------- | ---------- | -------- | ----------------------------------- | | `concept_id` | int | required | The concept ID | | `format` | string | "flat" | Response format ("flat" or "graph") | | `vocabulary_ids` | list\[str] | None | Filter to specific vocabularies | | `domain_ids` | list\[str] | None | Filter to specific domains | | `max_levels` | int | 10 | Maximum hierarchy levels (max 20) | | `max_results` | int | None | Maximum results per direction | | `relationship_types` | list\[str] | None | Relationship types to follow | | `include_invalid` | bool | True | Include deprecated/invalid concepts | ### Get Ancestors Find parent concepts in the hierarchy: ```python theme={null} result = client.hierarchy.ancestors( 201826, max_levels=3, vocabulary_ids=["SNOMED"], # Filter to specific vocabularies relationship_types=["Is a"], # Relationship types to follow include_distance=True, # Include hierarchy_level field include_paths=False, # Include path information include_invalid=False, # Exclude deprecated concepts page=1, page_size=100, ) # Access source concept info print(f"Ancestors of: {result['concept_name']}") # Access ancestors for ancestor in result["ancestors"]: level = ancestor.get("hierarchy_level", 0) print(f"{' ' * level}{ancestor['concept_name']}") # Access hierarchy summary print(f"Max depth: {result['hierarchy_summary']['max_hierarchy_depth']}") # Access pagination print(f"Page {result['meta']['pagination']['page']} of {result['meta']['pagination']['total_pages']}") ``` #### Ancestors Parameters | Parameter | Type | Default | Description | | -------------------- | ---------- | -------- | ---------------------------------------------- | | `concept_id` | int | required | The concept ID | | `vocabulary_ids` | list\[str] | None | Filter to specific vocabularies | | `max_levels` | int | None | Maximum hierarchy levels to traverse | | `relationship_types` | list\[str] | None | Relationship types to follow (default: "Is a") | | `include_paths` | bool | False | Include path information | | `include_distance` | bool | True | Include hierarchy\_level field | | `include_invalid` | bool | True | Include deprecated/invalid concepts | | `page` | int | 1 | Page number | | `page_size` | int | 100 | Results per page | ### Get Descendants Find child concepts in the hierarchy: ```python theme={null} result = client.hierarchy.descendants( 201826, max_levels=2, vocabulary_ids=["SNOMED"], # Filter to specific vocabularies relationship_types=["Is a"], # Relationship types to follow include_distance=True, # Include hierarchy_level field include_paths=False, # Include path information include_invalid=False, # Exclude deprecated concepts domain_ids=["Condition"], # Filter by domain page=1, page_size=100, ) # Access descendants for descendant in result["descendants"]: print(descendant["concept_name"]) # Access pagination print(f"Page {result['meta']['pagination']['page']} of {result['meta']['pagination']['total_pages']}") ``` #### Descendants Parameters | Parameter | Type | Default | Description | | -------------------- | ---------- | -------- | ---------------------------------------------- | | `concept_id` | int | required | The concept ID | | `vocabulary_ids` | list\[str] | None | Filter to specific vocabularies | | `max_levels` | int | 10 | Maximum hierarchy levels (max 20) | | `relationship_types` | list\[str] | None | Relationship types to follow (default: "Is a") | | `include_distance` | bool | True | Include hierarchy\_level field | | `include_paths` | bool | False | Include path information | | `include_invalid` | bool | True | Include deprecated/invalid concepts | | `domain_ids` | list\[str] | None | Filter by domains | | `page` | int | 1 | Page number | | `page_size` | int | 100 | Results per page | ## Get Relationship Types Get available relationship types from the OMOP CDM: ```python theme={null} # Get relationship types with pagination result = client.relationships.types(page=1, page_size=100) for rel_type in result["data"]: print(f"{rel_type['relationship_id']}: {rel_type['relationship_name']}") # Access pagination print(f"Total types: {result['meta']['pagination']['total_items']}") ``` ## With Options Include synonyms, relationships, hierarchy, and specify vocabulary release: ```python theme={null} # Get concept with synonyms concept = client.concepts.get( 201826, include_synonyms=True, ) # Get concept with synonyms and relationships concept = client.concepts.get( 201826, include_synonyms=True, include_relationships=True, ) print(concept["relationships"]) # {"parents": [...], "children": [...]} # Get concept with hierarchy information concept = client.concepts.get( 201826, include_hierarchy=True, ) print(concept["hierarchy"]) # Ancestor/descendant summary # Specify vocabulary release version concept = client.concepts.get( 201826, vocab_release="2025.2", ) ``` ### Parameters | Parameter | Type | Default | Description | | ----------------------- | ------ | -------- | -------------------------------------------- | | `concept_id` | int | required | The OMOP concept ID | | `include_relationships` | bool | False | Include related concepts (parents/children) | | `include_synonyms` | bool | False | Include concept synonyms | | `include_hierarchy` | bool | False | Include hierarchy information | | `vocab_release` | string | None | Specific vocabulary release (e.g., "2025.2") | ## Get Concept Relationships Get detailed relationships for a concept with filtering options: ```python theme={null} # Get all relationships with pagination result = client.concepts.relationships(201826) for rel in result["data"]["relationships"]: print(f"{rel['relationship_id']}: {rel['concept_2']['concept_name']}") # Filter by relationship type and vocabulary result = client.concepts.relationships( 201826, relationship_ids=["Is a", "Subsumes"], vocabulary_ids=["SNOMED"], standard_only=True, ) # Include reverse relationships result = client.concepts.relationships( 201826, include_reverse=True, domain_ids=["Condition"], ) ``` ### Relationships Parameters | Parameter | Type | Default | Description | | ------------------ | ---------- | -------- | ----------------------------------------------- | | `concept_id` | int | required | The concept ID | | `relationship_ids` | list\[str] | None | Filter by relationship type IDs | | `vocabulary_ids` | list\[str] | None | Filter by target vocabulary IDs | | `domain_ids` | list\[str] | None | Filter by target domain IDs | | `include_invalid` | bool | True | Include relationships to invalid concepts | | `standard_only` | bool | False | Only include relationships to standard concepts | | `include_reverse` | bool | False | Include reverse relationships | | `vocab_release` | string | None | Specific vocabulary release version | # Python SDK - Working with OMOP domains Source: https://docs.omophub.com/sdks/python/domains Work with OMOP domains in the OMOPHub Python SDK - list Condition, Drug, Measurement, and Procedure domains and query their concepts from Python code. ## List All Domains Get a list of all available OMOP domains: ```python theme={null} # Basic list result = client.domains.list() for domain in result["data"]["domains"]: print(domain["domain_id"]) # Include concept counts and statistics result = client.domains.list(include_stats=True) for domain in result["data"]["domains"]: print(f"{domain['domain_id']}: {domain['concept_count']} concepts") ``` ## Get Domain Concepts Retrieve all concepts within a specific domain: ```python theme={null} # Basic usage result = client.domains.concepts("Condition", page_size=100) for concept in result["data"]["concepts"]: print(f"{concept['concept_name']} ({concept['concept_id']})") # Filter by vocabulary result = client.domains.concepts( "Condition", vocabulary_ids=["SNOMED", "ICD10CM"], page_size=50 ) # Standard concepts only result = client.domains.concepts( "Drug", vocabulary_ids=["RxNorm"], standard_only=True, page_size=100 ) # Include invalid/deprecated concepts result = client.domains.concepts( "Procedure", include_invalid=True, page=1, page_size=50 ) # Pagination print(f"Page {result['meta']['pagination']['page']} of {result['meta']['pagination']['total_pages']}") print(f"Total: {result['meta']['pagination']['total_items']} concepts") ``` ### Parameters | Parameter | Type | Default | Description | | ----------------- | ---------- | -------- | --------------------------------------------- | | `domain_id` | str | required | Domain identifier (e.g., "Condition", "Drug") | | `vocabulary_ids` | list\[str] | None | Filter to specific vocabularies | | `standard_only` | bool | False | Return only standard concepts | | `include_invalid` | bool | True | Include deprecated concepts | | `page` | int | 1 | Page number | | `page_size` | int | 50 | Results per page (max 1000) | # Python SDK - Error handling and retries Source: https://docs.omophub.com/sdks/python/error-handling Handle OMOPHub API errors gracefully from the Python SDK - catch rate-limit, authentication, and validation exceptions with retry-ready patterns. ## Exception Types The SDK provides specific exception types for different error scenarios: ```python theme={null} import omophub client = omophub.OMOPHub(api_key="oh_xxx") try: concept = client.concepts.get(999999999) except omophub.NotFoundError as e: print(f"Not found: {e.message}") except omophub.AuthenticationError as e: print(f"Auth failed: {e.message}") except omophub.RateLimitError as e: print(f"Rate limited. Retry after {e.retry_after}s") except omophub.ValidationError as e: print(f"Invalid request: {e.message}") except omophub.APIError as e: print(f"API error {e.status_code}: {e.message}") ``` ## Exception Hierarchy ``` OMOPHubError # Base exception for all SDK errors ├── APIError # API returned an error response │ ├── AuthenticationError # 401 - Invalid or missing API key │ ├── NotFoundError # 404 - Resource not found │ ├── ValidationError # 400 - Invalid request parameters │ ├── RateLimitError # 429 - Too many requests │ └── ServerError # 500+ - Server-side error └── ConnectionError # Network connectivity issues ``` ## Error Properties All exceptions include helpful properties: ```python theme={null} try: concept = client.concepts.get(999999999) except omophub.APIError as e: print(e.message) # Human-readable error message print(e.status_code) # HTTP status code print(e.request_id) # Request ID for debugging ``` ## Rate Limiting Handle rate limits with automatic retry: ```python theme={null} try: results = client.search.basic("diabetes") except omophub.RateLimitError as e: print(f"Rate limited. Retry after {e.retry_after} seconds") import time time.sleep(e.retry_after) # Retry the request results = client.search.basic("diabetes") ``` The SDK automatically retries failed requests up to `max_retries` times (default: 3) with exponential backoff. ## Connection Errors Handle network issues: ```python theme={null} try: concept = client.concepts.get(201826) except omophub.ConnectionError as e: print(f"Network error: {e.message}") # Implement your retry logic or fallback ``` ## Best Practices ### Catch Specific Exceptions First ```python theme={null} try: concept = client.concepts.get(concept_id) except omophub.NotFoundError: # Handle missing concept concept = None except omophub.RateLimitError as e: # Wait and retry time.sleep(e.retry_after) except omophub.APIError as e: # Log and handle other API errors logger.error(f"API error: {e.message}") raise ``` ### Use Context Manager for Async ```python theme={null} async with omophub.AsyncOMOPHub(api_key="oh_xxx") as client: try: concept = await client.concepts.get(201826) except omophub.APIError as e: print(f"Error: {e.message}") ``` # Python SDK - FHIR Resolver for OMOP concepts Source: https://docs.omophub.com/sdks/python/fhir Resolve FHIR Coding and CodeableConcept values to OMOP standard concepts from the OMOPHub Python SDK with one call - ideal for Python-based ETL pipelines. ## Resolve a Single FHIR Coding Translate a FHIR `Coding` (system URI + code) to an OMOP standard concept and CDM target table: ```python theme={null} result = client.fhir.resolve( system="http://snomed.info/sct", code="44054006", resource_type="Condition", ) res = result["resolution"] print(res["standard_concept"]["concept_name"]) # "Type 2 diabetes mellitus" print(res["target_table"]) # "condition_occurrence" print(res["mapping_type"]) # "direct" ``` ## Resolve a Non-Standard Code (Maps to Traversal) ICD-10-CM and other classification codes are automatically mapped to their standard equivalents: ```python theme={null} result = client.fhir.resolve( system="http://hl7.org/fhir/sid/icd-10-cm", code="E11.9", ) res = result["resolution"] print(res["source_concept"]["vocabulary_id"]) # "ICD10CM" print(res["standard_concept"]["vocabulary_id"]) # "SNOMED" print(res["mapping_type"]) # "mapped" print(res["relationship_id"]) # "Maps to" print(res["target_table"]) # "condition_occurrence" ``` ## Text-Only Resolution (Semantic Search Fallback) When no structured code is available, pass the display text for semantic search: ```python theme={null} result = client.fhir.resolve( display="Blood Sugar", resource_type="Observation", ) res = result["resolution"] print(res["standard_concept"]["concept_name"]) # "Glucose [Mass/volume] in Blood" print(res["mapping_type"]) # "semantic_match" print(res["similarity_score"]) # 0.91 print(res["target_table"]) # "measurement" ``` ## Skip URI Resolution with vocabulary\_id If you already know the OMOP vocabulary, bypass the URI lookup: ```python theme={null} result = client.fhir.resolve( vocabulary_id="ICD10CM", code="E11.9", ) ``` ## Include Phoebe Recommendations Get related concepts for phenotype development alongside the resolution: ```python theme={null} result = client.fhir.resolve( system="http://snomed.info/sct", code="44054006", include_recommendations=True, recommendations_limit=5, ) for rec in result["resolution"]["recommendations"]: print(f" {rec['concept_name']} ({rec['domain_id']}) via {rec['relationship_id']}") # "Hyperglycemia (Condition) via Has finding" # "Diabetic retinopathy (Condition) via Associated finding" ``` ## Include Mapping Quality Signal Assess whether a resolution needs manual review: ```python theme={null} result = client.fhir.resolve( system="http://snomed.info/sct", code="44054006", include_quality=True, ) print(result["resolution"]["mapping_quality"]) # "high", "medium", "low", or "manual_review" ``` ## Batch Resolution Resolve up to 100 codings in a single call. Failed items are reported inline: ```python theme={null} result = client.fhir.resolve_batch([ {"system": "http://snomed.info/sct", "code": "44054006"}, {"system": "http://loinc.org", "code": "2339-0"}, {"system": "http://www.nlm.nih.gov/research/umls/rxnorm", "code": "197696"}, ]) print(result["summary"]) # {"total": 3, "resolved": 3, "failed": 0} for item in result["results"]: if "resolution" in item: res = item["resolution"] print(f"{res['source_concept']['concept_code']} -> {res['standard_concept']['concept_name']} -> {res['target_table']}") else: print(f"Failed: {item['error']['code']} - {item['error']['message']}") ``` Apply shared options to the entire batch: ```python theme={null} result = client.fhir.resolve_batch( [{"system": "http://snomed.info/sct", "code": "44054006"}], resource_type="Condition", include_quality=True, ) ``` ## CodeableConcept Resolution Resolve a FHIR `CodeableConcept` with multiple codings. The resolver picks the best match per OHDSI vocabulary preference (SNOMED > RxNorm > LOINC > CVX > ICD-10): ```python theme={null} result = client.fhir.resolve_codeable_concept( coding=[ {"system": "http://snomed.info/sct", "code": "44054006"}, {"system": "http://hl7.org/fhir/sid/icd-10-cm", "code": "E11.9"}, ], resource_type="Condition", ) best = result["best_match"]["resolution"] print(best["source_concept"]["vocabulary_id"]) # "SNOMED" (preferred) print(best["target_table"]) # "condition_occurrence" # Other resolved codings ranked by preference for alt in result["alternatives"]: print(f" Alt: {alt['resolution']['source_concept']['vocabulary_id']}") # Codings that failed to resolve for fail in result["unresolved"]: print(f" Failed: {fail['error']['code']}") ``` Falls back to the `text` field via semantic search when no coding resolves: ```python theme={null} result = client.fhir.resolve_codeable_concept( coding=[{"system": "http://loinc.org", "code": "99999-9"}], text="Blood Sugar", resource_type="Observation", ) # best_match resolved via semantic search; failed coding in unresolved ``` ## Async Usage All three methods are available on the async client: ```python theme={null} async with omophub.AsyncOMOPHub(api_key="oh_xxx") as client: result = await client.fhir.resolve( system="http://snomed.info/sct", code="44054006", ) batch = await client.fhir.resolve_batch([ {"system": "http://loinc.org", "code": "2339-0"}, ]) cc = await client.fhir.resolve_codeable_concept( coding=[{"system": "http://snomed.info/sct", "code": "44054006"}], ) ``` ## Type Interoperability All resolver methods accept any Coding-like input via duck typing. You can pass a plain dict, an `omophub.types.fhir.Coding` TypedDict, or any object exposing `.system` / `.code` attributes - including `fhir.resources.Coding` and `fhirpy` coding instances. `fhir.resources` is **never** a required dependency. ```python theme={null} # 1. Plain dict (no extra install) client.fhir.resolve( coding={"system": "http://snomed.info/sct", "code": "44054006"}, ) # 2. omophub's lightweight Coding TypedDict from omophub.types.fhir import Coding, CodeableConcept coding: Coding = {"system": "http://snomed.info/sct", "code": "44054006"} client.fhir.resolve(coding=coding) cc: CodeableConcept = { "coding": [ {"system": "http://snomed.info/sct", "code": "44054006"}, {"system": "http://hl7.org/fhir/sid/icd-10-cm", "code": "E11.9"}, ], "text": "Type 2 diabetes mellitus", } client.fhir.resolve_codeable_concept(cc) # 3. fhir.resources objects via duck typing (optional install) # pip install omophub[fhir-resources] from fhir.resources.R4B.coding import Coding as FhirCoding fhir_coding = FhirCoding(system="http://snomed.info/sct", code="44054006") client.fhir.resolve(coding=fhir_coding) ``` Mixed-shape inputs are fine inside a single batch call - dicts, TypedDicts, and `fhir.resources` objects can coexist: ```python theme={null} from fhir.resources.R4B.coding import Coding as FhirCoding result = client.fhir.resolve_batch([ {"system": "http://snomed.info/sct", "code": "44054006"}, FhirCoding(system="http://loinc.org", code="2339-0"), ]) ``` Explicit `system` / `code` kwargs always win if you pass both `coding=` and explicit kwargs - handy for overriding a single field without rebuilding the object. ## Connection Helpers For users who want to point an external FHIR client library at OMOPHub's FHIR Terminology Service directly, the SDK exposes two helpers: a URL builder and a pre-wired `fhirpy` client. ```python theme={null} from omophub import OMOPHub, get_fhir_server_url, get_fhirpy_client client = OMOPHub(api_key="oh_xxx") # Property on the client - returns the R4 base URL print(client.fhir_server_url) # "https://fhir.omophub.com/fhir/r4" # Helper for other FHIR versions print(get_fhir_server_url("r5")) # "https://fhir.omophub.com/fhir/r5" ``` ### fhirpy (optional extra) If you want to call FHIR operations directly - outside the SDK's Concept Resolver envelope - install `fhirpy` as an optional extra: ```bash theme={null} pip install omophub[fhirpy] ``` Then use the pre-configured client: ```python theme={null} from omophub import get_fhirpy_client fhir = get_fhirpy_client("oh_xxx") # Example: fetch the CodeSystem discovery Bundle for SNOMED bundle = fhir.resources("CodeSystem").search( url="http://snomed.info/sct", ).fetch() # Example: call $lookup directly via fhirpy lookup = fhir.execute( "CodeSystem/$lookup", method="GET", params={"system": "http://snomed.info/sct", "code": "44054006"}, ) ``` `get_async_fhirpy_client()` is the async counterpart. Use the Concept Resolver (`client.fhir.resolve`) when you want OMOP-enriched answers - standard concept ID, CDM target table, mapping quality. Use `fhirpy` against `client.fhir_server_url` when you need raw FHIR `Parameters` / `Bundle` responses for FHIR-native tooling. ## Error Handling The resolver raises standard SDK errors for API failures: ```python theme={null} from omophub import APIError try: result = client.fhir.resolve( system="http://www.ama-assn.org/go/cpt", code="99213", ) except APIError as e: print(e.status_code) # 403 print(e.message) # "vocabulary_restricted" ``` | Error Code | HTTP | Cause | | ------------------- | ---- | --------------------------------------------------------------------------- | | `unknown_system` | 400 | FHIR code system URI not recognized (includes a typo suggestion when close) | | `missing_input` | 400 | Neither `(system + code)` / `(vocabulary_id + code)` / `display` provided | | `concept_not_found` | 404 | No concept found via lookup or semantic search | See the [FHIR Resolver API Reference](/api-reference/fhir/resolve) for full response schemas and field descriptions. # Python SDK - Mapping concepts across vocabularies Source: https://docs.omophub.com/sdks/python/mappings Map concepts between OMOP vocabularies using the OMOPHub Python SDK - crosswalk SNOMED, ICD-10, LOINC, and RxNorm codes from Python applications. ## Get Mappings for a Concept Find how a concept maps to other vocabularies: ```python theme={null} result = client.mappings.get(201826) # Type 2 diabetes mellitus for mapping in result["data"]["mappings"]: print(f"{mapping['relationship_id']}: {mapping['target_concept_name']}") ``` ## Filter by Target Vocabulary Get mappings to a specific vocabulary only: ```python theme={null} result = client.mappings.get( 201826, target_vocabulary="ICD10CM", ) for mapping in result["data"]["mappings"]: print(f"{mapping['target_concept_id']}: {mapping['target_concept_name']}") ``` ## Include Invalid Mappings Include deprecated or invalid mappings in results: ```python theme={null} result = client.mappings.get( 201826, target_vocabulary="ICD10CM", include_invalid=True, ) ``` ## Use Specific Vocabulary Version Query mappings from a specific vocabulary release: ```python theme={null} result = client.mappings.get( 201826, target_vocabulary="ICD10CM", vocab_release="2025.1", ) ``` ## Map Multiple Concepts Map a batch of concept IDs to a target vocabulary: ```python theme={null} result = client.mappings.map( target_vocabulary="ICD10CM", source_concepts=[201826, 4329847], # Diabetes, MI ) for mapping in result["data"]["mappings"]: source = mapping["source_concept_id"] target = mapping["target_concept_id"] print(f"{source} -> {target}") ``` ## Map Using Vocabulary Codes Map concepts directly using vocabulary codes instead of OMOP concept IDs: ```python theme={null} result = client.mappings.map( target_vocabulary="RxNorm", source_codes=[ {"vocabulary_id": "SNOMED", "concept_code": "387517004"}, # Acetaminophen {"vocabulary_id": "SNOMED", "concept_code": "108774000"}, # Anastrozole ], ) for mapping in result["data"]["mappings"]: print(f"{mapping['source_concept_name']} -> {mapping['target_concept_name']}") ``` Use `source_codes` when you have vocabulary-specific codes (e.g., SNOMED codes from your source system). Use `source_concepts` when you already have OMOP concept IDs. You cannot use both parameters in the same request. ## Map with Specific Mapping Type Filter mappings by type: ```python theme={null} result = client.mappings.map( target_vocabulary="ICD10CM", source_concepts=[201826, 192671], mapping_type="direct", # "direct", "equivalent", "broader", "narrower" ) ``` ## Common Use Case: SNOMED to ICD-10 **Option 1: Direct mapping using vocabulary codes (recommended)** ```python theme={null} result = client.mappings.map( target_vocabulary="ICD10CM", source_codes=[ {"vocabulary_id": "SNOMED", "concept_code": "44054006"}, # Type 2 diabetes ], ) for m in result["data"]["mappings"]: print(f" -> {m['target_concept_name']} ({m['target_concept_code']})") ``` **Option 2: Using OMOP concept IDs** ```python theme={null} # First get the OMOP concept ID concept = client.concepts.get_by_code("SNOMED", "44054006") # Then map using the concept ID mappings = client.mappings.get( concept["concept_id"], target_vocabulary="ICD10CM", ) print(f"Mapping {concept['concept_name']}:") for m in mappings["data"]["mappings"]: print(f" -> {m['target_concept_name']} (confidence: {m['confidence']})") ``` # OMOPHub Python SDK - installation and quickstart Source: https://docs.omophub.com/sdks/python/overview Get started with the OMOPHub Python SDK - install, authenticate, and run your first concept search, mapping, and FHIR resolve from Python in minutes. ## Prerequisites * Python 3.10+ * [OMOPHub API key](https://dashboard.omophub.com/api-keys) ## Installation ```bash pip theme={null} pip install omophub ``` ```bash poetry theme={null} poetry add omophub ``` ## Quick Start ```python theme={null} import omophub client = omophub.OMOPHub(api_key="oh_xxxxxxxxx") # Get a concept concept = client.concepts.get(201826) print(concept["concept_name"]) # "Type 2 diabetes mellitus" # Search concepts results = client.search.basic("diabetes", page_size=5) for concept in results["concepts"]: print(f"{concept['concept_id']}: {concept['concept_name']}") ``` ## Async Usage ```python theme={null} import omophub import asyncio async def main(): async with omophub.AsyncOMOPHub(api_key="oh_xxx") as client: concept = await client.concepts.get(201826) print(concept["concept_name"]) asyncio.run(main()) ``` ## Configuration ```python theme={null} client = omophub.OMOPHub( api_key="oh_xxx", timeout=30.0, max_retries=3, vocab_version="2025.1", ) ``` You can also set your API key via environment variable: ```bash theme={null} export OMOPHUB_API_KEY=oh_xxxxxxxxx ``` ```python theme={null} # API key is read from environment client = omophub.OMOPHub() ``` ## Next Steps Get, batch, and explore concepts Search and autocomplete Map between vocabularies Handle API errors gracefully # Python SDK - Searching OMOP medical concepts Source: https://docs.omophub.com/sdks/python/search Search OMOP medical concepts across SNOMED, ICD-10, LOINC, and RxNorm from the OMOPHub Python SDK with keyword, semantic, and filtered queries. ## Basic Search Search for concepts by text: ```python theme={null} results = client.search.basic("diabetes mellitus", page_size=20) for concept in results["concepts"]: print(f"{concept['concept_id']}: {concept['concept_name']}") ``` ### Parameters | Parameter | Type | Default | Description | | ------------------- | ---------- | -------- | ---------------------------------------------- | | `query` | string | required | Search query string | | `vocabulary_ids` | list\[str] | None | Filter by vocabulary IDs | | `domain_ids` | list\[str] | None | Filter by domain IDs | | `concept_class_ids` | list\[str] | None | Filter by concept class IDs | | `standard_concept` | string | None | Filter by standard concept ("S", "C", or None) | | `include_synonyms` | bool | False | Search in synonyms | | `include_invalid` | bool | True | Include invalid concepts | | `min_score` | float | None | Minimum relevance score (0.0-1.0) | | `exact_match` | bool | False | Require exact match | | `page` | int | 1 | Page number (1-based) | | `page_size` | int | 20 | Results per page | | `sort_by` | string | None | Sort field | | `sort_order` | string | None | Sort order ("asc" or "desc") | ## Filter by Vocabulary Restrict search to specific vocabularies: ```python theme={null} results = client.search.basic( "heart attack", vocabulary_ids=["SNOMED", "ICD10CM"], ) ``` ## Filter by Domain Search within specific domains: ```python theme={null} results = client.search.basic( "aspirin", domain_ids=["Drug"], page_size=10, ) ``` ## Filter by Concept Class Search for specific concept classes: ```python theme={null} results = client.search.basic( "aspirin", concept_class_ids=["Clinical Drug", "Ingredient"], page_size=10, ) ``` ## Standard Concepts Only Filter to standard concepts: ```python theme={null} results = client.search.basic( "diabetes", standard_concept="S", # "S" = Standard, "C" = Classification vocabulary_ids=["SNOMED"], ) ``` ## Search with Synonyms Include concept synonyms in search: ```python theme={null} results = client.search.basic( "heart attack", include_synonyms=True, vocabulary_ids=["SNOMED"], ) ``` ## Combined Filters ```python theme={null} results = client.search.basic( "myocardial infarction", vocabulary_ids=["SNOMED"], domain_ids=["Condition"], standard_concept="S", include_synonyms=True, min_score=0.5, page_size=20, ) ``` ## Autocomplete Get suggestions as the user types: ```python theme={null} result = client.search.autocomplete("diab", page_size=10) for suggestion in result["suggestions"]: print(suggestion["suggestion"]) ``` ## Pagination ### Manual Pagination ```python theme={null} # Get first page results = client.search.basic("diabetes", page=1, page_size=50) # Access pagination metadata pagination = results["meta"]["pagination"] print(f"Total: {pagination['total_items']}") print(f"Pages: {pagination['total_pages']}") print(f"Has next: {pagination['has_next']}") # Get next page if pagination["has_next"]: results = client.search.basic("diabetes", page=2, page_size=50) ``` ### Auto-Pagination Iterator Iterate through all results automatically: ```python theme={null} count = 0 for concept in client.search.basic_iter("diabetes", page_size=100): print(concept["concept_name"]) count += 1 if count >= 500: # Limit for demo break ``` ## Advanced Search Use advanced search with additional filtering options and facets: ```python theme={null} results = client.search.advanced( "diabetes", vocabulary_ids=["SNOMED", "ICD10CM"], domain_ids=["Condition"], standard_concepts_only=True, page_size=50, ) # Access results for concept in results["data"]: print(f"{concept['concept_id']}: {concept['concept_name']}") # Access pagination print(f"Total: {results['meta']['pagination']['total_items']}") ``` ### Advanced Search Parameters | Parameter | Type | Default | Description | | ------------------------ | ----------- | -------- | ----------------------------- | | `query` | string | required | Search query string | | `vocabulary_ids` | list\[str] | None | Filter by vocabulary IDs | | `domain_ids` | list\[str] | None | Filter by domain IDs | | `concept_class_ids` | list\[str] | None | Filter by concept class IDs | | `standard_concepts_only` | bool | False | Only return standard concepts | | `include_invalid` | bool | True | Include invalid concepts | | `relationship_filters` | list\[dict] | None | Relationship-based filters | | `page` | int | 1 | Page number (1-based) | | `page_size` | int | 20 | Results per page | ### Relationship Filters Apply relationship-based filtering: ```python theme={null} results = client.search.advanced( "diabetes", relationship_filters=[ {"relationship_id": "Is a", "concept_id": 4116142} # Has parent concept ], standard_concepts_only=True, ) ``` ## Semantic Search Search for concepts using natural language with neural embeddings. Semantic search understands meaning, not just keywords. ```python theme={null} # Basic semantic search results = client.search.semantic("heart attack", page_size=10) # With filters results = client.search.semantic( "diabetes mellitus", vocabulary_ids=["SNOMED"], domain_ids=["Condition"], threshold=0.5, # Higher = more similar page_size=20, ) # Access results with similarity scores for concept in results["data"]["results"]: print(f"{concept['concept_name']}: {concept['similarity_score']:.2f}") ``` ### Semantic Search Parameters | Parameter | Type | Default | Description | | ------------------ | ---------- | -------- | ------------------------------- | | `query` | string | required | Natural language search query | | `vocabulary_ids` | list\[str] | None | Filter by vocabulary IDs | | `domain_ids` | list\[str] | None | Filter by domain IDs | | `standard_concept` | "S" \| "C" | None | Filter by standard concept flag | | `concept_class_id` | string | None | Filter by concept class | | `threshold` | float | 0.5 | Minimum similarity (0.0-1.0) | | `page` | int | 1 | Page number | | `page_size` | int | 20 | Results per page (max 100) | ### Semantic Search Iterator Iterate through all semantic search results with automatic pagination: ```python theme={null} # Iterate through all results for concept in client.search.semantic_iter("diabetes", page_size=50): print(concept["concept_name"]) # Collect first N results import itertools top_100 = list(itertools.islice( client.search.semantic_iter("diabetes"), 100 )) # With filters for concept in client.search.semantic_iter( "heart failure", vocabulary_ids=["SNOMED"], threshold=0.4, ): print(f"{concept['concept_id']}: {concept['concept_name']}") ``` ### Async Semantic Search ```python theme={null} # Async semantic search results = await client.search.semantic("heart attack", page_size=10) # Async iterator async for concept in client.search.semantic_iter("diabetes"): print(concept["concept_name"]) ``` ## Bulk Lexical Search Search for multiple queries in a single API call (up to 50 queries). Much more efficient than individual calls when you have many terms to look up. ```python theme={null} results = client.search.bulk_basic([ {"search_id": "q1", "query": "diabetes mellitus"}, {"search_id": "q2", "query": "hypertension"}, {"search_id": "q3", "query": "aspirin"}, ], defaults={"vocabulary_ids": ["SNOMED"], "page_size": 5}) for item in results["results"]: print(f"{item['search_id']}: {len(item['results'])} results ({item['status']})") ``` ### Bulk Search Parameters | Parameter | Type | Description | | --------------------------- | ---------- | -------------------------------- | | `searches` | list | List of search inputs (max 50) | | `searches[].search_id` | string | Unique ID to match results | | `searches[].query` | string | Search query | | `searches[].vocabulary_ids` | list\[str] | Per-search vocabulary filter | | `searches[].domain_ids` | list\[str] | Per-search domain filter | | `searches[].page_size` | int | Per-search result limit (1-100) | | `defaults` | dict | Shared defaults for all searches | ### With Shared Defaults ```python theme={null} # Defaults apply to all searches; per-search values override results = client.search.bulk_basic( [ {"search_id": "q1", "query": "diabetes"}, {"search_id": "q2", "query": "heart failure"}, {"search_id": "q3", "query": "metformin", "domain_ids": ["Drug"]}, # overrides default ], defaults={ "vocabulary_ids": ["SNOMED"], "domain_ids": ["Condition"], "page_size": 10, }, ) ``` ## Bulk Semantic Search Search for multiple natural-language queries using neural embeddings in a single call (up to 25 queries). ```python theme={null} results = client.search.bulk_semantic([ {"search_id": "s1", "query": "heart failure treatment options"}, {"search_id": "s2", "query": "type 2 diabetes medication"}, {"search_id": "s3", "query": "elevated blood pressure"}, ], defaults={"threshold": 0.5, "page_size": 10}) for item in results["results"]: print(f"{item['search_id']}: {item.get('result_count', 0)} results") for concept in item["results"]: print(f" {concept['concept_name']} ({concept['similarity_score']:.2f})") ``` ### Bulk Semantic Parameters | Parameter | Type | Description | | --------------------------- | ---------- | ------------------------------------- | | `searches` | list | List of search inputs (max 25) | | `searches[].search_id` | string | Unique ID to match results | | `searches[].query` | string | Natural language query (1-500 chars) | | `searches[].threshold` | float | Per-search similarity threshold (0-1) | | `searches[].page_size` | int | Per-search result limit (1-50) | | `searches[].vocabulary_ids` | list\[str] | Per-search vocabulary filter | | `defaults` | dict | Shared defaults for all searches | ### Async Bulk Search ```python theme={null} # Both bulk methods have async variants results = await client.search.bulk_basic([...]) results = await client.search.bulk_semantic([...]) ``` ## Find Similar Concepts Find concepts similar to a reference concept or query. Provide exactly one of: `concept_id`, `concept_name`, or `query`. ```python theme={null} # By concept ID similar = client.search.similar(concept_id=4329847) # MI # By concept name similar = client.search.similar(concept_name="Type 2 diabetes mellitus") # By natural language query similar = client.search.similar(query="elevated blood sugar") # With all options similar = client.search.similar( concept_id=4329847, algorithm="semantic", # or "lexical", "hybrid" similarity_threshold=0.7, page_size=50, vocabulary_ids=["SNOMED"], domain_ids=["Condition"], include_scores=True, include_explanations=True, ) # Access results for concept in similar["similar_concepts"]: print(f"{concept['concept_name']}: {concept['similarity_score']:.2f}") ``` ### Similar Concepts Parameters | Parameter | Type | Default | Description | | ---------------------- | ---------- | -------- | ----------------------------------- | | `concept_id` | int | None | Source concept ID | | `concept_name` | string | None | Source concept name | | `query` | string | None | Natural language query | | `algorithm` | string | "hybrid" | "semantic", "lexical", or "hybrid" | | `similarity_threshold` | float | 0.7 | Minimum similarity (0.0-1.0) | | `page_size` | int | 20 | Max results (max 1000) | | `vocabulary_ids` | list\[str] | None | Filter by vocabulary | | `domain_ids` | list\[str] | None | Filter by domain | | `standard_concept` | string | None | "S", "C", or "N" | | `include_invalid` | bool | None | Include invalid/deprecated concepts | | `include_scores` | bool | None | Include detailed scores | | `include_explanations` | bool | None | Include similarity explanations | ### Algorithm Comparison | Algorithm | Best For | Speed | | ---------- | ------------------------ | ------ | | `semantic` | Meaning-based similarity | Slower | | `lexical` | Text/string similarity | Faster | | `hybrid` | Balanced approach | Medium | ### Async Similar Concepts ```python theme={null} # Async version similar = await client.search.similar(concept_id=4329847) ``` # Python SDK - Working with OMOP vocabularies Source: https://docs.omophub.com/sdks/python/vocabularies Work with OMOP vocabularies in the OMOPHub Python SDK - list, describe, and query SNOMED, ICD-10, LOINC, RxNorm, and 100+ terminologies from Python. ## List All Vocabularies Get a paginated list of all available medical vocabularies: ```python theme={null} # Basic list result = client.vocabularies.list() for vocab in result["data"]["vocabularies"]: print(f"{vocab['vocabulary_id']}: {vocab['vocabulary_name']}") # Include statistics result = client.vocabularies.list(include_stats=True) for vocab in result["data"]["vocabularies"]: print(f"{vocab['vocabulary_id']}: {vocab.get('concept_count', 'N/A')} concepts") # Custom sorting and pagination result = client.vocabularies.list( sort_by="name", sort_order="asc", page=1, page_size=50 ) ``` ### Parameters | Parameter | Type | Default | Description | | ------------------ | ---- | ------- | ------------------------------------------ | | `include_stats` | bool | False | Include vocabulary statistics | | `include_inactive` | bool | False | Include inactive vocabularies | | `sort_by` | str | "name" | Sort field ("name", "priority", "updated") | | `sort_order` | str | "asc" | Sort order ("asc", "desc") | | `page` | int | 1 | Page number | | `page_size` | int | 20 | Results per page (max 1000) | ## Get Vocabulary Details Get detailed information about a specific vocabulary: ```python theme={null} vocab = client.vocabularies.get("SNOMED") print(f"Name: {vocab['data']['vocabulary_name']}") print(f"Version: {vocab['data']['vocabulary_version']}") print(f"Reference: {vocab['data']['vocabulary_reference']}") ``` ### Parameters | Parameter | Type | Default | Description | | --------------- | ---- | -------- | ------------------------------------------------- | | `vocabulary_id` | str | required | Vocabulary identifier (e.g., "SNOMED", "ICD10CM") | ### Response Fields Returns `vocabulary_id`, `vocabulary_name`, `vocabulary_reference`, `vocabulary_version`, `vocabulary_concept_id`. For detailed statistics, use `client.vocabularies.stats(vocabulary_id)`. ## Get Vocabulary Statistics Get statistical information about a vocabulary: ```python theme={null} stats = client.vocabularies.stats("SNOMED") print(f"Total concepts: {stats['total_concepts']}") print(f"Standard concepts: {stats['standard_concepts']}") ``` ## Get Domain Statistics Get statistics for a specific domain within a vocabulary: ```python theme={null} result = client.vocabularies.domain_stats("SNOMED", "Condition") print(f"Total concepts: {result['data']['total_concepts']}") print(f"Standard concepts: {result['data']['standard_concepts']}") # View concept class breakdown for cls in result["data"]["concept_classes"]: print(f"{cls['concept_class_id']}: {cls['concept_count']} concepts") ``` ### Parameters | Parameter | Type | Default | Description | | --------------- | ---- | -------- | ---------------------------------------------------------- | | `vocabulary_id` | str | required | Vocabulary identifier (e.g., "SNOMED", "ICD10CM") | | `domain_id` | str | required | Domain identifier (e.g., "Condition", "Drug", "Procedure") | ## Get Vocabulary Domains Get all standard OHDSI domains: ```python theme={null} result = client.vocabularies.domains() for domain in result["data"]["domains"]: print(f"{domain['domain_id']}: {domain['domain_name']}") ``` Returns all available domains with `domain_id`, `domain_name`, and `description`. ## Get Concept Classes Get all available concept classes: ```python theme={null} result = client.vocabularies.concept_classes() for cls in result["data"]: print(f"{cls['concept_class_id']}: {cls['concept_class_name']}") ``` Returns concept classes with `concept_class_id`, `concept_class_name`, and `concept_class_concept_id`. ## Get Vocabulary Concepts Retrieve concepts within a specific vocabulary with filtering and pagination: ```python theme={null} # Basic usage result = client.vocabularies.concepts("SNOMED", page_size=100) for concept in result["data"]["concepts"]: print(f"{concept['concept_name']} ({concept['concept_id']})") # Search within vocabulary result = client.vocabularies.concepts( "SNOMED", search="diabetes", standard_concept="S", page_size=50 ) # Include additional data result = client.vocabularies.concepts( "SNOMED", search="hypertension", include_relationships=True, include_synonyms=True ) # With sorting result = client.vocabularies.concepts( "RxNorm", sort_by="concept_id", sort_order="desc", page_size=100 ) # Include invalid/deprecated concepts result = client.vocabularies.concepts( "ICD10CM", include_invalid=True, page=1, page_size=50 ) # Pagination print(f"Page {result['meta']['pagination']['page']} of {result['meta']['pagination']['total_pages']}") print(f"Total: {result['meta']['pagination']['total_items']} concepts") ``` ### Parameters | Parameter | Type | Default | Description | | ----------------------- | ---- | -------- | --------------------------------------------------- | | `vocabulary_id` | str | required | Vocabulary identifier (e.g., "SNOMED", "ICD10CM") | | `search` | str | None | Search term to filter concepts by name or code | | `standard_concept` | str | "all" | Filter by standard concept status ("S", "C", "all") | | `include_invalid` | bool | True | Include invalid or deprecated concepts | | `include_relationships` | bool | False | Include concept relationships in response | | `include_synonyms` | bool | False | Include concept synonyms in response | | `sort_by` | str | "name" | Sort field ("name", "concept\_id", "concept\_code") | | `sort_order` | str | "asc" | Sort order ("asc", "desc") | | `page` | int | 1 | Page number | | `page_size` | int | 20 | Results per page (max 1000) | # R SDK - Working with OMOP medical concepts Source: https://docs.omophub.com/sdks/r/concepts Work with OMOP medical concepts in the OMOPHub R SDK - retrieve, search, and inspect concept details, synonyms, and relationships for R analytics code. ## Get a Concept Retrieve a concept by its OMOP concept ID: ```r theme={null} concept <- client$concepts$get(201826) print(concept$concept_name) # "Type 2 diabetes mellitus" print(concept$vocabulary_id) # "SNOMED" print(concept$domain_id) # "Condition" ``` ## Get by Vocabulary Code Look up a concept using a vocabulary-specific code: ```r theme={null} # Get SNOMED concept by its code concept <- client$concepts$get_by_code("SNOMED", "44054006") print(concept$concept_name) # "Type 2 diabetes mellitus" # Get ICD-10-CM concept concept <- client$concepts$get_by_code("ICD10CM", "E11") # Include synonyms and relationships concept <- client$concepts$get_by_code( "SNOMED", "44054006", include_synonyms = TRUE, include_relationships = TRUE ) print(concept$synonyms) # List of synonym names print(concept$relationships) # list(parents = [...], children = [...]) # Specify vocabulary release version concept <- client$concepts$get_by_code( "SNOMED", "44054006", vocab_release = "2025.2" ) ``` ## Batch Get Concepts Retrieve multiple concepts in a single request (max 100): ```r theme={null} # Basic batch request result <- client$concepts$batch(c(201826, 4329847, 1112807)) for (concept in result$data$concepts) { cat(sprintf("%s: %s\n", concept$concept_id, concept$concept_name)) } # With optional parameters result <- client$concepts$batch( c(201826, 4329847, 1112807), include_relationships = TRUE, include_synonyms = TRUE, include_mappings = TRUE, vocabulary_filter = c("SNOMED"), standard_only = FALSE # default ) # Check summary cat(sprintf("Retrieved: %d\n", result$data$summary$successful_retrievals)) cat(sprintf("Failed: %d\n", result$data$summary$failed_retrievals)) ``` ## Autocomplete Suggestions Get concept suggestions for autocomplete functionality: ```r theme={null} # Basic suggestions with pagination result <- client$concepts$suggest("diab", page_size = 10) for (concept in result$data) { print(concept$concept_name) } # Check pagination metadata print(result$meta$pagination$total_items) print(result$meta$pagination$has_next) # Filter by vocabulary and domain result <- client$concepts$suggest( "diab", vocabulary_ids = c("SNOMED"), domain_ids = c("Condition"), page_size = 20 ) # Navigate pages result <- client$concepts$suggest("diab", page = 2, page_size = 10) ``` ## Get Relationships Get relationships for a concept: ```r theme={null} # Get all relationships with pagination result <- client$relationships$get(201826) for (rel in result$data$relationships) { cat(sprintf("%s: %s\n", rel$relationship_id, rel$concept_2$concept_name)) } # Check pagination cat(sprintf("Page %d of %d\n", result$meta$pagination$page, result$meta$pagination$total_pages )) cat(sprintf("Total relationships: %d\n", result$meta$pagination$total_items)) # Filter by relationship type and vocabulary result <- client$relationships$get( 201826, relationship_ids = c("Is a", "Subsumes"), vocabulary_ids = "SNOMED", standard_only = TRUE, page = 1, page_size = 50 ) # Include reverse relationships result <- client$relationships$get( 201826, include_reverse = TRUE, domain_ids = c("Condition") ) ``` ## Navigate Hierarchy ### Get Complete Hierarchy Get both ancestors and descendants in a single request: ```r theme={null} # Flat format (default) - separate arrays for ancestors and descendants result <- client$hierarchy$get( 201826, max_levels = 3, vocabulary_ids = c("SNOMED") ) cat(sprintf("Total ancestors: %d\n", result$data$total_ancestors)) cat(sprintf("Total descendants: %d\n", result$data$total_descendants)) # Graph format for visualization (D3.js, Cytoscape, etc.) graph <- client$hierarchy$get( 201826, format = "graph", max_levels = 3 ) nodes <- graph$data$nodes # Concept nodes edges <- graph$data$edges # Relationship edges cat(sprintf("Nodes: %d, Edges: %d\n", length(nodes), length(edges))) # Advanced filtering options result <- client$hierarchy$get( 201826, domain_ids = c("Condition"), relationship_types = c("Is a"), max_results = 100, include_invalid = FALSE ) ``` #### Hierarchy Get Parameters | Parameter | Type | Default | Description | | -------------------- | --------- | -------- | ----------------------------------- | | `concept_id` | integer | required | The concept ID | | `format` | character | "flat" | Response format ("flat" or "graph") | | `vocabulary_ids` | character | NULL | Filter to specific vocabularies | | `domain_ids` | character | NULL | Filter to specific domains | | `max_levels` | integer | 10 | Maximum hierarchy levels (max 20) | | `max_results` | integer | NULL | Maximum results per direction | | `relationship_types` | character | NULL | Relationship types to follow | | `include_invalid` | logical | TRUE | Include deprecated/invalid concepts | ### Get Ancestors Find parent concepts in the hierarchy: ```r theme={null} result <- client$hierarchy$ancestors( 201826, max_levels = 3, vocabulary_ids = c("SNOMED"), # Filter to specific vocabularies relationship_types = c("Is a"), # Relationship types to follow include_distance = TRUE, # Include hierarchy_level field include_paths = FALSE, # Include path information include_invalid = FALSE, # Exclude deprecated concepts page = 1, page_size = 100 ) # Access source concept info cat(sprintf("Ancestors of: %s\n", result$concept_name)) # Access ancestors for (ancestor in result$ancestors) { level <- ancestor$hierarchy_level %||% 0 cat(sprintf("%s%s\n", strrep(" ", level), ancestor$concept_name)) } # Access hierarchy summary cat(sprintf("Max depth: %d\n", result$hierarchy_summary$max_hierarchy_depth)) # Access pagination cat(sprintf("Page %d of %d\n", result$meta$pagination$page, result$meta$pagination$total_pages )) ``` #### Ancestors Parameters | Parameter | Type | Default | Description | | -------------------- | --------- | -------- | ---------------------------------------------- | | `concept_id` | integer | required | The concept ID | | `vocabulary_ids` | character | NULL | Filter to specific vocabularies | | `max_levels` | integer | NULL | Maximum hierarchy levels to traverse | | `relationship_types` | character | NULL | Relationship types to follow (default: "Is a") | | `include_paths` | logical | FALSE | Include path information | | `include_distance` | logical | TRUE | Include hierarchy\_level field | | `include_invalid` | logical | TRUE | Include deprecated/invalid concepts | | `page` | integer | 1 | Page number | | `page_size` | integer | 100 | Results per page | ### Get Descendants Find child concepts in the hierarchy: ```r theme={null} result <- client$hierarchy$descendants( 201826, max_levels = 2, vocabulary_ids = c("SNOMED"), # Filter to specific vocabularies relationship_types = c("Is a"), # Relationship types to follow include_distance = TRUE, # Include hierarchy_level field include_paths = FALSE, # Include path information include_invalid = FALSE, # Exclude deprecated concepts domain_ids = c("Condition"), # Filter by domain page = 1, page_size = 100 ) # Access descendants for (descendant in result$descendants) { print(descendant$concept_name) } # Access pagination cat(sprintf("Page %d of %d\n", result$meta$pagination$page, result$meta$pagination$total_pages )) ``` #### Descendants Parameters | Parameter | Type | Default | Description | | -------------------- | --------- | -------- | ---------------------------------------------- | | `concept_id` | integer | required | The concept ID | | `vocabulary_ids` | character | NULL | Filter to specific vocabularies | | `max_levels` | integer | 10 | Maximum hierarchy levels (max 20) | | `relationship_types` | character | NULL | Relationship types to follow (default: "Is a") | | `include_distance` | logical | TRUE | Include hierarchy\_level field | | `include_paths` | logical | FALSE | Include path information | | `include_invalid` | logical | TRUE | Include deprecated/invalid concepts | | `domain_ids` | character | NULL | Filter by domains | | `page` | integer | 1 | Page number | | `page_size` | integer | 100 | Results per page | ## Get Relationship Types Get available relationship types from the OMOP CDM: ```r theme={null} # Get relationship types with pagination result <- client$relationships$types(page = 1, page_size = 100) for (rel_type in result$data) { cat(sprintf("%s: %s\n", rel_type$relationship_id, rel_type$relationship_name)) } # Access pagination cat(sprintf("Total types: %d\n", result$meta$pagination$total_items)) ``` ## With Options Include synonyms, relationships, hierarchy, and specify vocabulary release: ```r theme={null} # Get concept with synonyms concept <- client$concepts$get( 201826, include_synonyms = TRUE ) # Get concept with synonyms and relationships concept <- client$concepts$get( 201826, include_synonyms = TRUE, include_relationships = TRUE ) print(concept$relationships) # list(parents = [...], children = [...]) # Get concept with hierarchy information concept <- client$concepts$get( 201826, include_hierarchy = TRUE ) print(concept$hierarchy) # Ancestor/descendant summary # Specify vocabulary release version concept <- client$concepts$get( 201826, vocab_release = "2025.2" ) ``` ### Parameters | Parameter | Type | Default | Description | | ----------------------- | --------- | -------- | -------------------------------------------- | | `concept_id` | integer | required | The OMOP concept ID | | `include_relationships` | logical | FALSE | Include related concepts (parents/children) | | `include_synonyms` | logical | FALSE | Include concept synonyms | | `include_hierarchy` | logical | FALSE | Include hierarchy information | | `vocab_release` | character | NULL | Specific vocabulary release (e.g., "2025.2") | ## Get Concept Relationships Get detailed relationships for a concept with filtering options: ```r theme={null} # Get all relationships result <- client$concepts$relationships(201826) for (rel in result$data$relationships) { cat(sprintf("%s: %s\n", rel$relationship_id, rel$concept_2$concept_name)) } # Filter by relationship type and vocabulary result <- client$concepts$relationships( 201826, relationship_ids = c("Is a", "Subsumes"), vocabulary_ids = "SNOMED", standard_only = TRUE ) # Include reverse relationships result <- client$concepts$relationships( 201826, include_reverse = TRUE, domain_ids = c("Condition") ) ``` ### Relationships Parameters | Parameter | Type | Default | Description | | ------------------ | --------- | -------- | ----------------------------------------------- | | `concept_id` | integer | required | The concept ID | | `relationship_ids` | character | NULL | Filter by relationship type IDs | | `vocabulary_ids` | character | NULL | Filter by target vocabulary IDs | | `domain_ids` | character | NULL | Filter by target domain IDs | | `include_invalid` | logical | TRUE | Include relationships to invalid concepts | | `standard_only` | logical | FALSE | Only include relationships to standard concepts | | `include_reverse` | logical | FALSE | Include reverse relationships | | `vocab_release` | character | NULL | Specific vocabulary release version | ## Get Related Concepts Find concepts related to a given concept: ```r theme={null} # Basic related concepts result <- client$concepts$related(201826, page_size = 10) for (related in result$data) { cat(sprintf("%s (%s): score %.2f\n", related$concept_name, related$relationship_id, related$relationship_score )) } # Filter by relationship type and minimum score result <- client$concepts$related( 201826, relationship_types = c("Is a", "Maps to"), min_score = 0.5, page_size = 20 ) # Specify vocabulary release result <- client$concepts$related( 201826, vocab_release = "2025.1" ) ``` ## Get Recommended Concepts Get curated concept recommendations using the OHDSI Phoebe algorithm: ```r theme={null} # Basic recommendations for diabetes-related concepts result <- client$concepts$recommended(c(201826, 4329847)) for (source_id in names(result$data)) { cat(sprintf("\nRecommendations for concept %s:\n", source_id)) for (rec in head(result$data[[source_id]], 5)) { cat(sprintf(" - %s (%s) via %s\n", rec$concept_name, rec$vocabulary_id, rec$relationship_id )) } } # With filters result <- client$concepts$recommended( c(201826), relationship_types = c("Has finding", "Associated finding"), vocabulary_ids = c("SNOMED", "LOINC"), domain_ids = c("Condition", "Measurement"), standard_only = TRUE, page_size = 50 ) # Access pagination metadata cat(sprintf("Page %d of %d\n", result$meta$pagination$page, result$meta$pagination$total_pages )) cat(sprintf("Total recommendations: %d\n", result$meta$pagination$total_items)) ``` # R SDK - Working with OMOP domains Source: https://docs.omophub.com/sdks/r/domains Work with OMOP domains in the OMOPHub R SDK - list Condition, Drug, Measurement, and Procedure domains and pull their concepts directly into R tibbles. ## List All Domains Get a list of all available OMOP domains: ```r theme={null} # Basic list result <- client$domains$list() for (domain in result$data$domains) { print(domain$domain_id) } # Include concept counts and statistics result <- client$domains$list(include_stats = TRUE) for (domain in result$data$domains) { cat(sprintf("%s: %d concepts\n", domain$domain_id, domain$concept_count)) } ``` ## Get Domain Concepts Retrieve all concepts within a specific domain: ```r theme={null} # Basic usage result <- client$domains$concepts("Condition", page_size = 100) for (concept in result$data$concepts) { cat(sprintf("%s (%d)\n", concept$concept_name, concept$concept_id)) } # Filter by vocabulary result <- client$domains$concepts( "Condition", vocabulary_ids = c("SNOMED", "ICD10CM"), page_size = 50 ) # Standard concepts only result <- client$domains$concepts( "Drug", vocabulary_ids = "RxNorm", standard_only = TRUE, page_size = 100 ) # Include invalid/deprecated concepts result <- client$domains$concepts( "Procedure", include_invalid = TRUE, page = 1, page_size = 50 ) # Pagination cat(sprintf("Page %d of %d\n", result$meta$pagination$page, result$meta$pagination$total_pages )) cat(sprintf("Total: %d concepts\n", result$meta$pagination$total_items)) ``` ### Parameters | Parameter | Type | Default | Description | | ----------------- | --------- | -------- | --------------------------------------------- | | `domain_id` | character | required | Domain identifier (e.g., "Condition", "Drug") | | `vocabulary_ids` | character | NULL | Filter to specific vocabularies | | `standard_only` | logical | FALSE | Return only standard concepts | | `include_invalid` | logical | TRUE | Include deprecated concepts | | `page` | integer | 1 | Page number | | `page_size` | integer | 50 | Results per page (max 1000) | # R SDK - Error handling and retries Source: https://docs.omophub.com/sdks/r/error-handling Handle OMOPHub API errors gracefully from the R SDK - catch rate-limit, authentication, and validation errors with retry-ready patterns for OHDSI work. ## Error Handling with tryCatch The R SDK uses R's condition system for error handling. Use `tryCatch` to handle specific error types: ```r theme={null} library(omophub) client <- OMOPHubClient$new(api_key = "oh_xxx") tryCatch( { concept <- client$concepts$get(999999999) }, omophub_not_found = function(e) { cat(sprintf("Not found: %s\n", e$message)) }, omophub_auth_error = function(e) { cat(sprintf("Auth failed: %s\n", e$message)) }, omophub_rate_limit_error = function(e) { cat(sprintf("Rate limited. Retry after %ds\n", e$retry_after)) }, omophub_api_error = function(e) { cat(sprintf("API error %d: %s\n", e$status_code, e$message)) } ) ``` ## Error Types The SDK defines the following error conditions: ``` omophub_error # Base error for all SDK errors ├── omophub_api_error # API returned an error response │ ├── omophub_auth_error # 401 - Invalid or missing API key │ ├── omophub_not_found # 404 - Resource not found │ ├── omophub_validation_error # 400 - Invalid request parameters │ ├── omophub_rate_limit_error # 429 - Too many requests │ └── omophub_server_error # 500+ - Server-side error └── omophub_connection_error # Network connectivity issues ``` ## Error Properties All errors include helpful properties: ```r theme={null} tryCatch( { concept <- client$concepts$get(999999999) }, omophub_api_error = function(e) { print(e$message) # Human-readable error message print(e$status_code) # HTTP status code print(e$request_id) # Request ID for debugging } ) ``` ## Rate Limiting Handle rate limits with retry logic: ```r theme={null} tryCatch( { results <- client$search$basic("diabetes") }, omophub_rate_limit_error = function(e) { cat(sprintf("Rate limited. Retry after %d seconds\n", e$retry_after)) Sys.sleep(e$retry_after) # Retry the request results <- client$search$basic("diabetes") } ) ``` The SDK automatically retries failed requests up to `max_retries` times (default: 3) with exponential backoff. You typically don't need to handle transient errors manually. ## Connection Errors Handle network issues: ```r theme={null} tryCatch( { concept <- client$concepts$get(201826) }, omophub_connection_error = function(e) { cat(sprintf("Network error: %s\n", e$message)) # Implement your retry logic or fallback } ) ``` ## Best Practices ### Catch Specific Errors First ```r theme={null} tryCatch( { concept <- client$concepts$get(concept_id) }, omophub_not_found = function(e) { # Handle missing concept concept <- NULL }, omophub_rate_limit_error = function(e) { # Wait and retry Sys.sleep(e$retry_after) }, omophub_api_error = function(e) { # Log and handle other API errors warning(sprintf("API error: %s", e$message)) stop(e) } ) ``` ### Use withCallingHandlers for Logging ```r theme={null} withCallingHandlers( { results <- client$search$basic("diabetes") }, omophub_api_error = function(e) { # Log but don't handle - let it propagate message(sprintf("API call failed: %s", e$message)) } ) ``` ### Wrap in a Safe Function Create a helper function for common error handling: ```r theme={null} safe_get_concept <- function(client, concept_id) { tryCatch( { client$concepts$get(concept_id) }, omophub_not_found = function(e) { NULL }, omophub_api_error = function(e) { warning(sprintf("Failed to get concept %d: %s", concept_id, e$message)) NULL } ) } # Usage concept <- safe_get_concept(client, 201826) if (!is.null(concept)) { print(concept$concept_name) } ``` # R SDK - FHIR Resolver for OMOP concepts Source: https://docs.omophub.com/sdks/r/fhir Resolve FHIR Coding and CodeableConcept values to OMOP standard concepts from the OMOPHub R SDK with one call - ideal for R-based OHDSI pipelines. ## Resolve a Single FHIR Coding Translate a FHIR `Coding` (system URI + code) to an OMOP standard concept and CDM target table: ```r theme={null} result <- client$fhir$resolve( system = "http://snomed.info/sct", code = "44054006", resource_type = "Condition" ) res <- result$resolution cat(res$standard_concept$concept_name) # "Type 2 diabetes mellitus" cat(res$target_table) # "condition_occurrence" cat(res$mapping_type) # "direct" ``` ## Resolve a Non-Standard Code (Maps to Traversal) ICD-10-CM and other classification codes are automatically mapped to their standard equivalents: ```r theme={null} result <- client$fhir$resolve( system = "http://hl7.org/fhir/sid/icd-10-cm", code = "E11.9" ) res <- result$resolution cat(res$source_concept$vocabulary_id) # "ICD10CM" cat(res$standard_concept$vocabulary_id) # "SNOMED" cat(res$mapping_type) # "mapped" cat(res$relationship_id) # "Maps to" cat(res$target_table) # "condition_occurrence" ``` ## Text-Only Resolution (Semantic Search Fallback) When no structured code is available, pass the display text for semantic search: ```r theme={null} result <- client$fhir$resolve( display = "Blood Sugar", resource_type = "Observation" ) res <- result$resolution cat(res$standard_concept$concept_name) # "Glucose [Mass/volume] in Blood" cat(res$mapping_type) # "semantic_match" cat(res$similarity_score) # 0.91 cat(res$target_table) # "measurement" ``` ## Skip URI Resolution with vocabulary\_id If you already know the OMOP vocabulary, bypass the URI lookup: ```r theme={null} result <- client$fhir$resolve( vocabulary_id = "ICD10CM", code = "E11.9" ) ``` ## Include Phoebe Recommendations Get related concepts for phenotype development alongside the resolution: ```r theme={null} result <- client$fhir$resolve( system = "http://snomed.info/sct", code = "44054006", include_recommendations = TRUE, recommendations_limit = 5L ) for (rec in result$resolution$recommendations) { cat(sprintf(" %s (%s) via %s\n", rec$concept_name, rec$domain_id, rec$relationship_id)) } ``` ## Include Mapping Quality Signal Assess whether a resolution needs manual review: ```r theme={null} result <- client$fhir$resolve( system = "http://snomed.info/sct", code = "44054006", include_quality = TRUE ) cat(result$resolution$mapping_quality) # "high", "medium", "low", or "manual_review" ``` ## Batch Resolution Resolve up to 100 codings in a single call. Failed items are reported inline: ```r theme={null} result <- client$fhir$resolve_batch(list( list(system = "http://snomed.info/sct", code = "44054006"), list(system = "http://loinc.org", code = "2339-0"), list(system = "http://www.nlm.nih.gov/research/umls/rxnorm", code = "197696") )) cat(sprintf("Total: %d, Resolved: %d, Failed: %d\n", result$summary$total, result$summary$resolved, result$summary$failed)) for (item in result$results) { if (!is.null(item$resolution)) { res <- item$resolution cat(sprintf("%s -> %s -> %s\n", res$source_concept$concept_code, res$standard_concept$concept_name, res$target_table)) } else { cat(sprintf("Failed: %s - %s\n", item$error$code, item$error$message)) } } ``` Apply shared options to the entire batch: ```r theme={null} result <- client$fhir$resolve_batch( list(list(system = "http://snomed.info/sct", code = "44054006")), resource_type = "Condition", include_quality = TRUE ) ``` ## CodeableConcept Resolution Resolve a FHIR `CodeableConcept` with multiple codings. The resolver picks the best match per OHDSI vocabulary preference (SNOMED > RxNorm > LOINC > CVX > ICD-10): ```r theme={null} result <- client$fhir$resolve_codeable_concept( coding = list( list(system = "http://snomed.info/sct", code = "44054006"), list(system = "http://hl7.org/fhir/sid/icd-10-cm", code = "E11.9") ), resource_type = "Condition" ) best <- result$best_match$resolution cat(best$source_concept$vocabulary_id) # "SNOMED" (preferred) cat(best$target_table) # "condition_occurrence" # Other resolved codings ranked by preference for (alt in result$alternatives) { cat(sprintf(" Alt: %s\n", alt$resolution$source_concept$vocabulary_id)) } ``` Falls back to the `text` field via semantic search when no coding resolves: ```r theme={null} result <- client$fhir$resolve_codeable_concept( coding = list(list(system = "http://loinc.org", code = "99999-9")), text = "Blood Sugar", resource_type = "Observation" ) # best_match resolved via semantic search; failed coding in unresolved ``` ## Tibble Output for Batch Pass `as_tibble = TRUE` to `resolve_batch()` to get a flat [`tibble`](https://tibble.tidyverse.org/) with one row per input coding - ready to pipe into `dplyr` / `tidyr`: ```r theme={null} library(dplyr) tbl <- client$fhir$resolve_batch( list( list(system = "http://hl7.org/fhir/sid/icd-10-cm", code = "E11.9"), list(system = "http://hl7.org/fhir/sid/icd-10-cm", code = "I10"), list(system = "http://hl7.org/fhir/sid/icd-10-cm", code = "J45.909") ), as_tibble = TRUE ) tbl |> filter(status == "resolved") |> select(source_code, standard_concept_name, target_table) #> # A tibble: 3 x 3 #> source_code standard_concept_name target_table #> #> 1 E11.9 Type 2 diabetes mellitus condition_occurrence #> 2 I10 Essential hypertension condition_occurrence #> 3 J45.909 Asthma condition_occurrence ``` The tibble columns are: `source_system`, `source_code`, `source_concept_id`, `source_concept_name`, `standard_concept_id`, `standard_concept_name`, `standard_vocabulary_id`, `domain_id`, `target_table`, `mapping_type`, `similarity_score`, `status`, and `status_detail`. Failed rows are kept in-place with `status = "failed"` and the API error text in `status_detail` - do not silently drop them: ```r theme={null} tbl |> filter(status == "failed") |> select(source_code, status_detail) ``` The batch summary (`total` / `resolved` / `failed`) is attached as an attribute: ```r theme={null} attr(tbl, "summary") #> $total [1] 3 #> $resolved [1] 3 #> $failed [1] 0 ``` The default `as_tibble = FALSE` still returns the list-shaped `list(results, summary)` - existing code keeps working unchanged. ## Standalone Wrappers The R6 interface is always available: ```r theme={null} client$fhir$resolve(system = "http://snomed.info/sct", code = "44054006") ``` For pipe-friendly use, three standalone wrappers forward to the same R6 methods and take the client as their first argument: ```r theme={null} library(dplyr) # Equivalent to the R6 form client |> fhir_resolve( system = "http://snomed.info/sct", code = "44054006", resource_type = "Condition" ) tbl <- client |> fhir_resolve_batch( codings = list( list(system = "http://snomed.info/sct", code = "44054006"), list(system = "http://loinc.org", code = "2339-0") ), as_tibble = TRUE ) client |> fhir_resolve_codeable_concept( coding = list( list(system = "http://snomed.info/sct", code = "44054006"), list(system = "http://hl7.org/fhir/sid/icd-10-cm", code = "E11.9") ), resource_type = "Condition" ) ``` Pick whichever form reads better for the surrounding code - both are fully supported. ## FHIR Client Interop `omophub_fhir_url()` returns the OMOPHub FHIR Terminology Service base URL for a given FHIR version (`"r4"` default, plus `"r4b"`, `"r5"`, `"r6"`). Use it with [`httr2`](https://httr2.r-lib.org/) or [`fhircrackr`](https://cran.r-project.org/package=fhircrackr) when you want raw FHIR `Parameters` / `Bundle` responses instead of OMOPHub's Concept Resolver envelope. ```r theme={null} library(httr2) # Call CodeSystem/$lookup directly against OMOPHub's FHIR endpoint resp <- request(omophub_fhir_url()) |> req_url_path_append("CodeSystem/$lookup") |> req_url_query( system = "http://snomed.info/sct", code = "44054006" ) |> req_headers(Authorization = paste("Bearer", Sys.getenv("OMOPHUB_API_KEY"))) |> req_perform() params <- resp_body_json(resp) # Raw FHIR Parameters resource with the concept display, designations, etc. ``` R5/R6 endpoints work the same way: ```r theme={null} omophub_fhir_url("r5") #> "https://fhir.omophub.com/fhir/r5" ``` Use `client$fhir$resolve()` (or `fhir_resolve()`) when you want OMOP-enriched answers - standard concept, CDM target table, mapping quality. Use `omophub_fhir_url()` + `httr2` when you need raw FHIR responses for FHIR-native tooling. ## Error Handling The resolver signals errors using standard R conditions: ```r theme={null} tryCatch( client$fhir$resolve( system = "http://www.ama-assn.org/go/cpt", code = "99213" ), omophub_api_error = function(e) { cat(e$status_code) # 403 cat(e$message) # "vocabulary_restricted" } ) ``` | Error Code | HTTP | Cause | | ------------------- | ---- | --------------------------------------------------------------------------- | | `unknown_system` | 400 | FHIR code system URI not recognized (includes a typo suggestion when close) | | `missing_input` | 400 | Neither `(system + code)` / `(vocabulary_id + code)` / `display` provided | | `concept_not_found` | 404 | No concept found via lookup or semantic search | See the [FHIR Resolver API Reference](/api-reference/fhir/resolve) for full response schemas and field descriptions. # R SDK - Mapping concepts across vocabularies Source: https://docs.omophub.com/sdks/r/mappings Map concepts between OMOP vocabularies using the OMOPHub R SDK - crosswalk SNOMED, ICD-10, LOINC, and RxNorm codes directly from OHDSI R scripts. ## Get Mappings for a Concept Find how a concept maps to other vocabularies: ```r theme={null} result <- client$mappings$get(201826) # Type 2 diabetes mellitus for (mapping in result$data$mappings) { cat(sprintf("%s: %s\n", mapping$relationship_id, mapping$target_concept_name)) } ``` ## Filter by Target Vocabulary Get mappings to a specific vocabulary only: ```r theme={null} result <- client$mappings$get( 201826, target_vocabulary = "ICD10CM" ) for (mapping in result$data$mappings) { cat(sprintf("%s: %s\n", mapping$target_concept_id, mapping$target_concept_name)) } ``` ## Include Invalid Mappings Include deprecated or invalid mappings in results: ```r theme={null} result <- client$mappings$get( 201826, target_vocabulary = "ICD10CM", include_invalid = TRUE ) ``` ## Use Specific Vocabulary Version Query mappings from a specific vocabulary release: ```r theme={null} result <- client$mappings$get( 201826, target_vocabulary = "ICD10CM", vocab_release = "2025.1" ) ``` ## Map Multiple Concepts Map a batch of concept IDs to a target vocabulary: ```r theme={null} result <- client$mappings$map( target_vocabulary = "ICD10CM", source_concepts = c(201826, 4329847) # Diabetes, MI ) for (mapping in result$data$mappings) { source <- mapping$source_concept_id target <- mapping$target_concept_id cat(sprintf("%s -> %s\n", source, target)) } ``` ## Map Using Vocabulary Codes Map concepts directly using vocabulary codes instead of OMOP concept IDs: ```r theme={null} result <- client$mappings$map( target_vocabulary = "RxNorm", source_codes = list( list(vocabulary_id = "SNOMED", concept_code = "387517004"), # Acetaminophen list(vocabulary_id = "SNOMED", concept_code = "108774000") # Anastrozole ) ) for (mapping in result$data$mappings) { cat(sprintf("%s -> %s\n", mapping$source_concept_name, mapping$target_concept_name)) } ``` Use `source_codes` when you have vocabulary-specific codes (e.g., SNOMED codes from your source system). Use `source_concepts` when you already have OMOP concept IDs. You cannot use both parameters in the same request. ## Map with Specific Mapping Type Filter mappings by type: ```r theme={null} result <- client$mappings$map( target_vocabulary = "ICD10CM", source_concepts = c(201826, 192671), mapping_type = "direct" # "direct", "equivalent", "broader", "narrower" ) ``` ## Common Use Case: SNOMED to ICD-10 **Option 1: Direct mapping using vocabulary codes (recommended)** ```r theme={null} result <- client$mappings$map( target_vocabulary = "ICD10CM", source_codes = list( list(vocabulary_id = "SNOMED", concept_code = "44054006") # Type 2 diabetes ) ) for (m in result$data$mappings) { cat(sprintf(" -> %s (%s)\n", m$target_concept_name, m$target_concept_code)) } ``` **Option 2: Using OMOP concept IDs** ```r theme={null} # First get the OMOP concept ID concept <- client$concepts$get_by_code("SNOMED", "44054006") # Then map using the concept ID mappings <- client$mappings$get( concept$concept_id, target_vocabulary = "ICD10CM" ) cat(sprintf("Mapping %s:\n", concept$concept_name)) for (m in mappings$data$mappings) { cat(sprintf(" -> %s (confidence: %s)\n", m$target_concept_name, m$confidence)) } ``` ## Working with Mapping Results Convert mappings to a data frame for analysis: ```r theme={null} result <- client$mappings$get(201826) # Create a data frame from mappings mapping_df <- do.call(rbind, lapply(result$data$mappings, function(m) { data.frame( source_id = m$source_concept_id, source_name = m$source_concept_name, target_id = m$target_concept_id, target_name = m$target_concept_name, relationship = m$relationship_id, confidence = m$confidence, stringsAsFactors = FALSE ) })) print(mapping_df) ``` # OMOPHub R SDK - installation and quickstart Source: https://docs.omophub.com/sdks/r/overview Get started with the OMOPHub R SDK - install, authenticate, and run your first concept search, mapping, and FHIR resolve for OHDSI analytics in minutes. ## Prerequisites * R 4.1+ * [OMOPHub API key](https://dashboard.omophub.com/api-keys) ## Installation ```r CRAN theme={null} # Install from CRAN (recommended) install.packages("omophub") ``` ```r GitHub theme={null} # Install from GitHub devtools::install_github("omophub/omophub-R") ``` ## Quick Start ```r theme={null} library(omophub) client <- OMOPHubClient$new(api_key = "oh_xxxxxxxxx") # Get a concept concept <- client$concepts$get(201826) print(concept$concept_name) # "Type 2 diabetes mellitus" # Search concepts results <- client$search$basic("diabetes", page_size = 5) for (concept in results$data) { cat(sprintf("%s: %s\n", concept$concept_id, concept$concept_name)) } ``` ## Configuration ```r theme={null} client <- OMOPHubClient$new( api_key = "oh_xxx", timeout = 30, max_retries = 3, vocab_version = "2025.1" ) ``` You can also set your API key via environment variable: ```bash theme={null} export OMOPHUB_API_KEY=oh_xxxxxxxxx ``` ```r theme={null} # API key is read from environment client <- OMOPHubClient$new() ``` ## Features * **R6 Class Design** - Familiar object-oriented interface for R users * **Automatic Rate Limiting** - Built-in request throttling to respect API limits * **Automatic Retries** - Transient errors are automatically retried with exponential backoff * **Pagination Helpers** - Easy iteration through large result sets * **Tibble Integration** - Results can be converted to tibbles for tidyverse workflows ## Next Steps Get, batch, and explore concepts Search and autocomplete Map between vocabularies Handle API errors gracefully # R SDK - Searching OMOP medical concepts Source: https://docs.omophub.com/sdks/r/search Search OMOP medical concepts across SNOMED, ICD-10, LOINC, and RxNorm from the OMOPHub R SDK with keyword, semantic, and filtered queries for OHDSI. ## Basic Search Search for concepts by text: ```r theme={null} results <- client$search$basic("diabetes mellitus", page_size = 20) for (concept in results$data) { cat(sprintf("%s: %s\n", concept$concept_id, concept$concept_name)) } ``` ### Parameters | Parameter | Type | Default | Description | | ------------------- | --------- | -------- | ---------------------------------------------- | | `query` | character | required | Search query string | | `vocabulary_ids` | character | NULL | Filter by vocabulary IDs | | `domain_ids` | character | NULL | Filter by domain IDs | | `concept_class_ids` | character | NULL | Filter by concept class IDs | | `standard_concept` | character | NULL | Filter by standard concept ("S", "C", or NULL) | | `include_synonyms` | logical | FALSE | Search in synonyms | | `include_invalid` | logical | TRUE | Include invalid concepts | | `min_score` | numeric | NULL | Minimum relevance score (0.0-1.0) | | `exact_match` | logical | FALSE | Require exact match | | `page` | integer | 1 | Page number (1-based) | | `page_size` | integer | 20 | Results per page | | `sort_by` | character | NULL | Sort field | | `sort_order` | character | NULL | Sort order ("asc" or "desc") | ## Filter by Vocabulary Restrict search to specific vocabularies: ```r theme={null} results <- client$search$basic( "heart attack", vocabulary_ids = "SNOMED" ) # Multiple vocabularies results <- client$search$basic( "heart attack", vocabulary_ids = c("SNOMED", "ICD10CM") ) ``` ## Filter by Domain Search within specific domains: ```r theme={null} results <- client$search$basic( "aspirin", domain_ids = "Drug", page_size = 10 ) ``` ## Filter by Concept Class Search for specific concept classes: ```r theme={null} results <- client$search$basic( "aspirin", concept_class_ids = c("Clinical Drug", "Ingredient"), page_size = 10 ) ``` ## Standard Concepts Only Filter to standard concepts: ```r theme={null} results <- client$search$basic( "diabetes", standard_concept = "S", # "S" = Standard, "C" = Classification vocabulary_ids = "SNOMED" ) ``` ## Search with Synonyms Include concept synonyms in search: ```r theme={null} results <- client$search$basic( "heart attack", include_synonyms = TRUE, vocabulary_ids = "SNOMED" ) ``` ## Combined Filters ```r theme={null} results <- client$search$basic( "myocardial infarction", vocabulary_ids = "SNOMED", domain_ids = "Condition", standard_concept = "S", include_synonyms = TRUE, min_score = 0.5, page_size = 20 ) ``` ## Autocomplete Get suggestions as the user types: ```r theme={null} result <- client$search$autocomplete("diab", page_size = 10) for (suggestion in result$suggestions) { print(suggestion$suggestion) } # With vocabulary and domain filters result <- client$search$autocomplete( "diab", vocabulary_ids = c("SNOMED"), domains = c("Condition"), page_size = 10 ) ``` ## Pagination ### Manual Pagination ```r theme={null} # Get first page results <- client$search$basic("diabetes", page = 1, page_size = 50) # Access pagination metadata pagination <- results$meta cat(sprintf("Total: %d\n", pagination$total_items)) cat(sprintf("Pages: %d\n", pagination$total_pages)) cat(sprintf("Has next: %s\n", pagination$has_next)) # Get next page if (isTRUE(pagination$has_next)) { results <- client$search$basic("diabetes", page = 2, page_size = 50) } ``` ### Auto-Pagination with basic\_all() Fetch all results automatically and return as a tibble: ```r theme={null} # Fetch up to 5 pages of results all_results <- client$search$basic_all( "diabetes", page_size = 100, max_pages = 5, progress = TRUE # Show progress bar ) # Results are returned as a tibble print(nrow(all_results)) print(names(all_results)) # Use with dplyr library(dplyr) all_results %>% filter(vocabulary_id == "SNOMED") %>% select(concept_id, concept_name, domain_id) ``` The `basic_all()` method returns a tibble, making it easy to integrate with tidyverse workflows. ## Advanced Search Use advanced search with additional filtering options and facets: ```r theme={null} results <- client$search$advanced( "diabetes", vocabulary_ids = c("SNOMED", "ICD10CM"), domain_ids = "Condition", standard_concepts_only = TRUE, page_size = 50 ) # Access results for (concept in results$data) { cat(sprintf("%s: %s\n", concept$concept_id, concept$concept_name)) } # Access pagination cat(sprintf("Total: %d\n", results$meta$pagination$total_items)) ``` ### Advanced Search Parameters | Parameter | Type | Default | Description | | ------------------------ | --------- | -------- | ----------------------------- | | `query` | character | required | Search query string | | `vocabulary_ids` | character | NULL | Filter by vocabulary IDs | | `domain_ids` | character | NULL | Filter by domain IDs | | `concept_class_ids` | character | NULL | Filter by concept class IDs | | `standard_concepts_only` | logical | FALSE | Only return standard concepts | | `include_invalid` | logical | TRUE | Include invalid concepts | | `relationship_filters` | list | NULL | Relationship-based filters | | `page` | integer | 1 | Page number (1-based) | | `page_size` | integer | 20 | Results per page | ### Relationship Filters Apply relationship-based filtering: ```r theme={null} results <- client$search$advanced( "diabetes", relationship_filters = list( list(relationship_id = "Is a", concept_id = 4116142) # Has parent concept ), standard_concepts_only = TRUE ) ``` ## Semantic Search Search for concepts using natural language with neural embeddings. Semantic search understands meaning, not just keywords. ```r theme={null} # Basic semantic search results <- client$search$semantic("heart attack", page_size = 10) # With filters results <- client$search$semantic( "diabetes mellitus", vocabulary_ids = "SNOMED", domain_ids = "Condition", threshold = 0.5, # Higher = more similar page_size = 20 ) # Access results with similarity scores for (concept in results$data$results) { cat(sprintf("%s: %.2f\n", concept$concept_name, concept$similarity_score)) } ``` ### Semantic Search Parameters | Parameter | Type | Default | Description | | ------------------ | ---------- | -------- | ------------------------------- | | `query` | character | required | Natural language search query | | `vocabulary_ids` | character | NULL | Filter by vocabulary IDs | | `domain_ids` | character | NULL | Filter by domain IDs | | `standard_concept` | "S" \| "C" | NULL | Filter by standard concept flag | | `concept_class_id` | character | NULL | Filter by concept class | | `threshold` | numeric | 0.5 | Minimum similarity (0.0-1.0) | | `page` | integer | 1 | Page number | | `page_size` | integer | 20 | Results per page (max 100) | ### Auto-Pagination with semantic\_all() Fetch all semantic search results automatically and return as a tibble: ```r theme={null} # Fetch all results as tibble all_results <- client$search$semantic_all( "diabetes", page_size = 100, max_pages = 5, progress = TRUE ) # Returns tibble with concept_id, similarity_score, etc. print(all_results) # Use with dplyr library(dplyr) all_results %>% filter(similarity_score > 0.6) %>% select(concept_id, concept_name, similarity_score) # With filters all_results <- client$search$semantic_all( "heart failure", vocabulary_ids = "SNOMED", threshold = 0.4, page_size = 50 ) ``` The `semantic_all()` method returns a tibble, making it easy to integrate with tidyverse workflows. ## Bulk Lexical Search Search for multiple queries in a single API call (up to 50 queries): ```r theme={null} results <- client$search$bulk_basic(list( list(search_id = "q1", query = "diabetes mellitus"), list(search_id = "q2", query = "hypertension"), list(search_id = "q3", query = "aspirin") ), defaults = list(vocabulary_ids = list("SNOMED"), page_size = 5)) for (item in results$results) { cat(sprintf("%s: %d results (%s)\n", item$search_id, length(item$results), item$status)) } ``` ### Bulk Search Parameters | Parameter | Type | Description | | ------------------------------ | --------- | -------------------------------- | | `searches` | list | List of search inputs (max 50) | | `searches[[i]]$search_id` | character | Unique ID to match results | | `searches[[i]]$query` | character | Search query | | `searches[[i]]$vocabulary_ids` | list | Per-search vocabulary filter | | `defaults` | list | Shared defaults for all searches | ## Bulk Semantic Search Search for multiple natural-language queries using neural embeddings (up to 25 queries): ```r theme={null} results <- client$search$bulk_semantic(list( list(search_id = "s1", query = "heart failure treatment options"), list(search_id = "s2", query = "type 2 diabetes medication") ), defaults = list(threshold = 0.5, page_size = 10)) for (item in results$results) { cat(sprintf("%s: %d results\n", item$search_id, item$result_count)) } ``` ### Bulk Semantic Parameters | Parameter | Type | Description | | ------------------------- | --------- | ------------------------------------- | | `searches` | list | List of search inputs (max 25) | | `searches[[i]]$search_id` | character | Unique ID to match results | | `searches[[i]]$query` | character | Natural language query (1-500 chars) | | `searches[[i]]$threshold` | numeric | Per-search similarity threshold (0-1) | | `defaults` | list | Shared defaults for all searches | ## Find Similar Concepts Find concepts similar to a reference concept or query. Provide exactly one of: `concept_id`, `concept_name`, or `query`. ```r theme={null} # By concept ID similar <- client$search$similar(concept_id = 4329847) # MI # By concept name similar <- client$search$similar(concept_name = "Type 2 diabetes mellitus") # By natural language query similar <- client$search$similar(query = "elevated blood sugar") # With all options similar <- client$search$similar( concept_id = 4329847, algorithm = "semantic", # or "lexical", "hybrid" similarity_threshold = 0.7, page_size = 50, vocabulary_ids = "SNOMED", domain_ids = "Condition", include_scores = TRUE, include_explanations = TRUE ) # Access results for (concept in similar$similar_concepts) { cat(sprintf("%s: %.2f\n", concept$concept_name, concept$similarity_score)) } ``` ### Similar Concepts Parameters | Parameter | Type | Default | Description | | ---------------------- | --------- | -------- | ----------------------------------- | | `concept_id` | integer | NULL | Source concept ID | | `concept_name` | character | NULL | Source concept name | | `query` | character | NULL | Natural language query | | `algorithm` | character | "hybrid" | "semantic", "lexical", or "hybrid" | | `similarity_threshold` | numeric | 0.7 | Minimum similarity (0.0-1.0) | | `page_size` | integer | 20 | Max results (max 1000) | | `vocabulary_ids` | character | NULL | Filter by vocabulary | | `domain_ids` | character | NULL | Filter by domain | | `standard_concept` | character | NULL | "S", "C", or "N" | | `include_invalid` | logical | NULL | Include invalid/deprecated concepts | | `include_scores` | logical | NULL | Include detailed scores | | `include_explanations` | logical | NULL | Include similarity explanations | ### Algorithm Comparison | Algorithm | Best For | Speed | | ---------- | ------------------------ | ------ | | `semantic` | Meaning-based similarity | Slower | | `lexical` | Text/string similarity | Faster | | `hybrid` | Balanced approach | Medium | # R SDK - Working with OMOP vocabularies Source: https://docs.omophub.com/sdks/r/vocabularies Work with OMOP vocabularies in the OMOPHub R SDK - list, describe, and query SNOMED, ICD-10, LOINC, RxNorm, and 100+ terminologies from R scripts. ## List All Vocabularies Get a paginated list of all available medical vocabularies: ```r theme={null} # Basic list result <- client$vocabularies$list() for (vocab in result$data$vocabularies) { cat(sprintf("%s: %s\n", vocab$vocabulary_id, vocab$vocabulary_name)) } # Include statistics result <- client$vocabularies$list(include_stats = TRUE) for (vocab in result$data$vocabularies) { cat(sprintf("%s: %s concepts\n", vocab$vocabulary_id, vocab$concept_count)) } # Custom sorting and pagination result <- client$vocabularies$list( sort_by = "name", sort_order = "asc", page = 1, page_size = 50 ) ``` ### Parameters | Parameter | Type | Default | Description | | ------------------ | --------- | ------- | ------------------------------------------ | | `include_stats` | logical | FALSE | Include vocabulary statistics | | `include_inactive` | logical | FALSE | Include inactive vocabularies | | `sort_by` | character | "name" | Sort field ("name", "priority", "updated") | | `sort_order` | character | "asc" | Sort order ("asc", "desc") | | `page` | integer | 1 | Page number | | `page_size` | integer | 100 | Results per page (max 1000) | ## Get Vocabulary Details Get detailed information about a specific vocabulary: ```r theme={null} vocab <- client$vocabularies$get("SNOMED") cat(sprintf("Name: %s\n", vocab$data$vocabulary_name)) cat(sprintf("Version: %s\n", vocab$data$vocabulary_version)) cat(sprintf("Reference: %s\n", vocab$data$vocabulary_reference)) ``` ### Parameters | Parameter | Type | Default | Description | | --------------- | --------- | -------- | ------------------------------------------------- | | `vocabulary_id` | character | required | Vocabulary identifier (e.g., "SNOMED", "ICD10CM") | ### Response Fields Returns `vocabulary_id`, `vocabulary_name`, `vocabulary_reference`, `vocabulary_version`, `vocabulary_concept_id`. For detailed statistics, use `client$vocabularies$stats(vocabulary_id)`. ## Get Vocabulary Statistics Get statistical information about a vocabulary: ```r theme={null} stats <- client$vocabularies$stats("SNOMED") cat(sprintf("Total concepts: %d\n", stats$total_concepts)) cat(sprintf("Standard concepts: %d\n", stats$standard_concepts)) ``` ## Get Domain Statistics Get statistics for a specific domain within a vocabulary: ```r theme={null} result <- client$vocabularies$domain_stats("SNOMED", "Condition") cat(sprintf("Total concepts: %d\n", result$data$total_concepts)) cat(sprintf("Standard concepts: %d\n", result$data$standard_concepts)) # View concept class breakdown for (cls in result$data$concept_classes) { cat(sprintf("%s: %d concepts\n", cls$concept_class_id, cls$concept_count)) } ``` ### Parameters | Parameter | Type | Default | Description | | --------------- | --------- | -------- | ---------------------------------------------------------- | | `vocabulary_id` | character | required | Vocabulary identifier (e.g., "SNOMED", "ICD10CM") | | `domain_id` | character | required | Domain identifier (e.g., "Condition", "Drug", "Procedure") | ## Get Vocabulary Domains Get all standard OHDSI domains: ```r theme={null} result <- client$vocabularies$domains() for (domain in result$data$domains) { cat(sprintf("%s: %s\n", domain$domain_id, domain$domain_name)) } ``` Returns all available domains with `domain_id`, `domain_name`, and `description`. ## Get Concept Classes Get all available concept classes: ```r theme={null} result <- client$vocabularies$concept_classes() for (cls in result$data) { cat(sprintf("%s: %s\n", cls$concept_class_id, cls$concept_class_name)) } ``` Returns concept classes with `concept_class_id`, `concept_class_name`, and `concept_class_concept_id`. ## Get Vocabulary Concepts Retrieve concepts within a specific vocabulary with filtering and pagination: ```r theme={null} # Basic usage result <- client$vocabularies$concepts("SNOMED", page_size = 100) for (concept in result$data$concepts) { cat(sprintf("%s (%d)\n", concept$concept_name, concept$concept_id)) } # Search within vocabulary result <- client$vocabularies$concepts( "SNOMED", search = "diabetes", standard_concept = "S", page_size = 50 ) # Include additional data result <- client$vocabularies$concepts( "SNOMED", search = "hypertension", include_relationships = TRUE, include_synonyms = TRUE ) # With sorting result <- client$vocabularies$concepts( "RxNorm", sort_by = "concept_id", sort_order = "desc", page_size = 100 ) # Include invalid/deprecated concepts result <- client$vocabularies$concepts( "ICD10CM", include_invalid = TRUE, page = 1, page_size = 50 ) # Pagination cat(sprintf("Page %d of %d\n", result$meta$pagination$page, result$meta$pagination$total_pages )) cat(sprintf("Total: %d concepts\n", result$meta$pagination$total_items)) ``` ### Parameters | Parameter | Type | Default | Description | | ----------------------- | --------- | -------- | --------------------------------------------------- | | `vocabulary_id` | character | required | Vocabulary identifier (e.g., "SNOMED", "ICD10CM") | | `search` | character | NULL | Search term to filter concepts by name or code | | `standard_concept` | character | "all" | Filter by standard concept status ("S", "C", "all") | | `include_invalid` | logical | TRUE | Include invalid or deprecated concepts | | `include_relationships` | logical | FALSE | Include concept relationships in response | | `include_synonyms` | logical | FALSE | Include concept synonyms in response | | `sort_by` | character | "name" | Sort field ("name", "concept\_id", "concept\_code") | | `sort_order` | character | "asc" | Sort order ("asc", "desc") | | `page` | integer | 1 | Page number | | `page_size` | integer | 20 | Results per page (max 1000) | # Vocabulary Releases Source: https://docs.omophub.com/vocabulary-versions Learn about available OMOP vocabulary release versions, how to pin a specific release in API calls, and manage vocabulary updates from OHDSI Athena. The OMOPHub API supports multiple vocabulary release versions through a sophisticated schema-based versioning system. Each release contains the complete OHDSI vocabulary data for that time period, ensuring data consistency and enabling time-based analysis. ## Current Available Releases The following vocabulary releases are currently available in the system: | Version | Release Date | FHIR CodeSystem ID | Status | Default | Release Notes | | ---------- | ------------ | ------------------ | ------ | ------- | ----------------------------------------------------------------------------------------- | | **2026.1** | 2026-02-27 | `omop-v20260227` | Active | Yes | [Link](https://github.com/OHDSI/Vocabulary-v5.0/releases/tag/v20260227_1772176757.000000) | | **2025.2** | 2025-08-27 | `omop-v20250827` | Active | No | [Link](https://github.com/OHDSI/Vocabulary-v5.0/releases/tag/v20250827_1756288395.000000) | | **2025.1** | 2025-02-27 | `omop-v20250227` | Active | No | [Link](https://github.com/OHDSI/Vocabulary-v5.0/releases/tag/v20250227_1740652703.000000) | | **2024.2** | 2024-08-30 | `omop-v20240830` | Active | No | [Link](https://github.com/OHDSI/Vocabulary-v5.0/releases/tag/v20240830_1725004358.000000) | The **FHIR CodeSystem ID** column is what you pass to the FHIR Terminology Service for release-pinned instance reads and operations - e.g. `GET /fhir/r4/CodeSystem/omop-v20250827` or `CodeSystem/omop-v20250827/$lookup?...`. FHIR handlers accept both formats interchangeably: the OMOPHub version (`2025.2`) and the FHIR ID (`omop-v20250827`). The FHIR ID is computed deterministically from the release date (`omop-v` + `YYYYMMDD`). ## FHIR Code System URIs Each vocabulary release is served through both the REST API (by OMOP `vocabulary_id`) and the FHIR Terminology Service at `https://fhir.omophub.com/fhir/{r4,r4b,r5,r6}/` (by canonical FHIR system URI). The mapping below is stable across releases - only the underlying concepts change between versions. | FHIR System URI | OMOP `vocabulary_id` | | --------------------------------------------- | -------------------- | | `http://snomed.info/sct` | `SNOMED` | | `http://loinc.org` | `LOINC` | | `http://www.nlm.nih.gov/research/umls/rxnorm` | `RxNorm` | | `http://hl7.org/fhir/sid/icd-10` | `ICD10` | | `http://hl7.org/fhir/sid/icd-10-cm` | `ICD10CM` | | `http://hl7.org/fhir/sid/icd-10-pcs` | `ICD10PCS` | | `http://hl7.org/fhir/sid/icd-10-cn` | `ICD10CN` | | `http://hl7.org/fhir/sid/icd-10-gm` | `ICD10GM` | | `http://hl7.org/fhir/sid/icd-9-cm` | `ICD9CM` | | `http://hl7.org/fhir/sid/icd-9` | `ICD9Proc` | | `http://hl7.org/fhir/sid/icd-9-cn` | `ICD9ProcCN` | | `http://hl7.org/fhir/sid/icd-o-3` | `ICDO3` | | `http://hl7.org/fhir/sid/cvx` | `CVX` | | `http://hl7.org/fhir/sid/ndc` | `NDC` | | `http://www.nlm.nih.gov/research/umls/hcpcs` | `HCPCS` | | `http://www.whocc.no/atc` | `ATC` | | `http://unitsofmeasure.org` | `UCUM` | | `urn:iso:std:iso:11073:10101` | `MDC` | | `http://fdasis.nlm.nih.gov` | `UNII` | | `http://va.gov/terminology/medrt` | `MED-RT` | | `https://fhir-terminology.ohdsi.org` | OMOP unified omnibus | Source of truth is `apps/api/src/config/fhir-vocabularies.ts`. To get the live list from the running service, call `GET /fhir/r4/metadata?mode=terminology` - the `TerminologyCapabilities` resource enumerates every supported `codeSystem.uri`. ## Version Abbreviations For convenience, the API supports several abbreviations for version specification: | Abbreviation | Maps To | Description | | ------------ | -------- | ---------------------------------------- | | `latest` | `2026.1` | Always points to the most recent release | | `default` | `2026.1` | Same as latest - the system default | | `current` | `2026.1` | Alias for the default version | | `2026v1` | `2026.1` | Half-based abbreviation | | `2025v2` | `2025.2` | Half-based abbreviation | | `2025v1` | `2025.1` | Half-based abbreviation | | `2024v2` | `2024.2` | Half-based abbreviation | ## How to Select Vocabulary Versions You can specify which vocabulary version to use in your API requests using any of these methods: ### Method 1: Query Parameter (Recommended) Add the `vocab_release` parameter to any API endpoint: ```bash cURL theme={null} # Using specific version curl -X GET "https://api.omophub.com/v1/concepts/search?query=diabetes&vocab_release=2024.1" \ -H "Authorization: Bearer YOUR_API_KEY" # Using abbreviation curl -X GET "https://api.omophub.com/v1/concepts/search?query=diabetes&vocab_release=2024v1" \ -H "Authorization: Bearer YOUR_API_KEY" # Using latest (default behavior) curl -X GET "https://api.omophub.com/v1/concepts/search?query=diabetes&vocab_release=latest" \ -H "Authorization: Bearer YOUR_API_KEY" ``` ```javascript JavaScript theme={null} // Using specific version const response = await fetch('https://api.omophub.com/v1/concepts/search?query=diabetes&vocab_release=2024.1', { headers: { 'Authorization': 'Bearer YOUR_API_KEY', 'Content-Type': 'application/json' } }); // Using abbreviation const responseAbbrev = await fetch('https://api.omophub.com/v1/concepts/search?query=diabetes&vocab_release=2024v1', { headers: { 'Authorization': 'Bearer YOUR_API_KEY', 'Content-Type': 'application/json' } }); // Using latest (default) const responseLatest = await fetch('https://api.omophub.com/v1/concepts/search?query=diabetes&vocab_release=latest', { headers: { 'Authorization': 'Bearer YOUR_API_KEY', 'Content-Type': 'application/json' } }); ``` ```python Python theme={null} import requests headers = { 'Authorization': 'Bearer YOUR_API_KEY', 'Content-Type': 'application/json' } # Using specific version response = requests.get( 'https://api.omophub.com/v1/concepts/search', headers=headers, params={ 'query': 'diabetes', 'vocab_release': '2024.2' } ) # Using abbreviation response_abbrev = requests.get( 'https://api.omophub.com/v1/concepts/search', headers=headers, params={ 'query': 'diabetes', 'vocab_release': '2024v2' } ) # Using latest (default) response_latest = requests.get( 'https://api.omophub.com/v1/concepts/search', headers=headers, params={ 'query': 'diabetes', 'vocab_release': 'latest' } ) ``` ```r R theme={null} library(httr) # Using specific version response <- GET( "https://api.omophub.com/v1/concepts/search", add_headers(Authorization = "Bearer YOUR_API_KEY"), query = list( query = "diabetes", vocab_release = "2024.2" ) ) # Using abbreviation response_abbrev <- GET( "https://api.omophub.com/v1/concepts/search", add_headers(Authorization = "Bearer YOUR_API_KEY"), query = list( query = "diabetes", vocab_release = "2024v2" ) ) # Using latest (default) response_latest <- GET( "https://api.omophub.com/v1/concepts/search", add_headers(Authorization = "Bearer YOUR_API_KEY"), query = list( query = "diabetes", vocab_release = "latest" ) ) ``` ### Method 2: HTTP Header Use the `X-Vocab-Release` header: ```bash cURL theme={null} curl -X GET "https://api.omophub.com/v1/concepts/search?query=diabetes" \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "X-Vocab-Release: 2024.2" ``` ```javascript JavaScript theme={null} const response = await fetch('https://api.omophub.com/v1/concepts/search?query=diabetes', { headers: { 'Authorization': 'Bearer YOUR_API_KEY', 'Content-Type': 'application/json', 'X-Vocab-Release': '2024.2' } }); ``` ```python Python theme={null} import requests headers = { 'Authorization': 'Bearer YOUR_API_KEY', 'Content-Type': 'application/json', 'X-Vocab-Release': '2024.2' } response = requests.get( 'https://api.omophub.com/v1/concepts/search', headers=headers, params={'query': 'diabetes'} ) ``` ## Default Version Behavior When no version is specified: * **Default Version**: `2025.2` * **Fallback**: If the default version is unavailable, the system uses the latest available active version * **Caching**: Responses are cached with version-specific keys to ensure consistency ## Version Comparison Example Here's how to compare concept availability across versions: ```python Python theme={null} import requests def compare_concept_across_versions(concept_code, vocabulary_id): """Compare a concept across different vocabulary versions.""" headers = { 'Authorization': 'Bearer YOUR_API_KEY', 'Content-Type': 'application/json' } versions = ['2025.2', '2025.1', '2024.2'] results = {} for version in versions: response = requests.get( 'https://api.omophub.com/v1/concepts/search', headers=headers, params={ 'query': concept_code, 'vocabulary_ids': vocabulary_id, 'vocab_release': version, 'page_size': 1 } ) data = response.json() if data.get('data'): concept = data['data'][0] results[version] = { 'found': True, 'concept_name': concept['concept_name'], 'standard_concept': concept.get('standard_concept'), 'valid_start_date': concept.get('valid_start_date'), 'valid_end_date': concept.get('valid_end_date') } else: results[version] = {'found': False} return results # Example usage concept_comparison = compare_concept_across_versions('E11.9', 'ICD10CM') for version, info in concept_comparison.items(): if info['found']: print(f"Version {version}: {info['concept_name']}") else: print(f"Version {version}: Not found") ``` ```javascript JavaScript theme={null} async function compareConcept(conceptCode, vocabularyId) { const headers = { 'Authorization': 'Bearer YOUR_API_KEY', 'Content-Type': 'application/json' }; const versions = ['2026.1', '2025.2', '2025.1', '2024.2']; const results = {}; for (const version of versions) { try { const response = await fetch( `https://api.omophub.com/v1/concepts/search?query=${conceptCode}&vocabulary_ids=${vocabularyId}&vocab_release=${version}&page_size=1`, { headers } ); const data = await response.json(); if (data.data && data.data.length > 0) { const concept = data.data[0]; results[version] = { found: true, conceptName: concept.concept_name, standardConcept: concept.standard_concept, validStartDate: concept.valid_start_date, validEndDate: concept.valid_end_date }; } else { results[version] = { found: false }; } } catch (error) { results[version] = { found: false, error: error.message }; } } return results; } // Example usage compareConcept('E11.9', 'ICD10CM').then(results => { for (const [version, info] of Object.entries(results)) { if (info.found) { console.log(`Version ${version}: ${info.conceptName}`); } else { console.log(`Version ${version}: Not found`); } } }); ``` ## Version-Specific Features ### Temporal Queries Query concepts as they existed at specific points in time: ```bash theme={null} # Get diabetes concepts as they existed in 2024v2 curl -X GET "https://api.omophub.com/v1/concepts/search?query=diabetes&vocab_release=2024.2" \ -H "Authorization: Bearer YOUR_API_KEY" ``` ### Historical Analysis Compare vocabulary evolution over time: ```bash theme={null} # Compare concept relationships across versions curl -X GET "https://api.omophub.com/v1/concepts/123456/relationships?vocab_release=2024.2" \ -H "Authorization: Bearer YOUR_API_KEY" ``` ### Migration Support Validate concept mappings when upgrading between versions: ```bash theme={null} # Validate proposed mappings against OMOP standards curl -X POST "https://api.omophub.com/v1/mappings/validate" \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{"mappings":[{"source_concept_id":201826,"target_concept_id":1569178005,"mapping_type":"Maps to"}]}' ``` ## Version Support Policy ### Active Versions * **Current + 2 previous releases** are actively supported ### Deprecated Versions * **Removal notice** provided 90 days before discontinuation ## Troubleshooting ### Common Issues #### Version Not Found ```json theme={null} { "error": "VOCABULARY_VERSION_NOT_FOUND", "message": "Vocabulary version '2023.1' is not available", "available_versions": ["2026.1", "2025.2", "2025.1", "2024.2"] } ``` **Solution**: Use the `/v1/vocabularies/releases` endpoint to check available versions. #### Version Mismatch in Results If you receive unexpected results, verify the version is being applied correctly: ```bash theme={null} # Check which version was actually used curl -X GET "https://api.omophub.com/v1/concepts/search?query=diabetes&vocab_release=2024.2" \ -H "Authorization: Bearer YOUR_API_KEY" \ -v # Verbose mode shows response headers ``` Look for the `X-Vocab-Release-Used` header in the response to confirm the version. ## FAQ The system automatically uses the default version (`2026.1`). This provides the most recent vocabulary data and optimal performance through caching. No, each API request operates against a single vocabulary version. To compare across versions, make separate requests for each version. Major vocabulary updates typically occur twice a year (February and August), following the [OHDSI vocabulary release cycles](https://github.com/OHDSI/Vocabulary-v5.0/wiki/Release-planning).