Map free-text medication mentions that OpenMed extracts to RxNorm — the U.S.
National Library of Medicine's normalized drug nomenclature. The unit of meaning
is the RxCUI (RxNorm Concept Unique Identifier): a stable integer that ties
together brand, generic, ingredient, strength, and dose form.
RxNorm and the RxNav REST API are fully public and free: no API key, no
license agreement, no rate-limit registration for normal use. Of every skill in
this terminology batch, this one has the highest value-to-friction ratio — start
here when grounding medications.
you need one stable code per drug for storage, analytics, or interoperability.
IN) from a prescribableproduct — SCD (Semantic Clinical Drug, generic) or SBD (Semantic Brand
Drug) — e.g. "metformin 500 MG Oral Tablet".
Medication/MedicationRequest coded with RxNorm.
If the source text is non-English or you need ATC/SNOMED links instead, seemapping-to-snomed; RxNorm itself is U.S.-centric.
Base URL: https://rxnav.nlm.nih.gov/REST. No auth. JSON via ?...&... paths
ending in nothing or .json depending on endpoint; the REST root returns XML by
default, so request JSON explicitly.
import requests
BASE = "https://rxnav.nlm.nih.gov/REST"
def rxcui_for(name: str) -> str | None:
"""Exact-match RxCUI lookup for a normalized drug name."""
r = requests.get(f"{BASE}/rxcui.json", params={"name": name}, timeout=10)
r.raise_for_status()
ids = r.json().get("idGroup", {}).get("rxnormId", [])
return ids[0] if ids else None
def approximate(name: str, max_entries: int = 3) -> list[dict]:
"""Fuzzy match for misspelled or abbreviated drug text."""
r = requests.get(
f"{BASE}/approximateTerm.json",
params={"term": name, "maxEntries": max_entries},
timeout=10,
)
r.raise_for_status()
return r.json().get("approximateGroup", {}).get("candidate", [])
print(rxcui_for("metformin")) # -> '6809' (ingredient)
print(approximate("metformin 500")) # fuzzy -> candidate RxCUIs
Resolve a full prescribable product (ingredient + strength + form) to an SCD:
# getApproximateMatch / getRxConceptProperties give term type (TTY)
def properties(rxcui: str) -> dict:
r = requests.get(f"{BASE}/rxcui/{rxcui}/properties.json", timeout=10)
r.raise_for_status()
return r.json().get("properties", {})
# Find the SCD ("metformin 500 MG Oral Tablet") from the ingredient:
def related_by_tty(rxcui: str, tty: str) -> list[dict]:
r = requests.get(
f"{BASE}/rxcui/{rxcui}/related.json", params={"tty": tty}, timeout=10
)
r.raise_for_status()
groups = r.json().get("relatedGroup", {}).get("conceptGroup", [])
out = []
for g in groups:
out.extend(g.get("conceptProperties", []) or [])
return out
Attach NDCs and check interactions (both public):
ndcs = requests.get(f"{BASE}/rxcui/{rxcui}/ndcs.json").json() # package codes
pharma_detection_superclinical).("metformin 500 mg tablet" → ingredient metformin, strength 500 MG,
form Oral Tablet).
/rxcui.json?name=. If empty, fall backto /approximateTerm.json.
IN ingredient — analytics, allergy lists, class rollups.SCD generic product / SBD brand product — orders, US Core Medication.BN brand name, PIN precise ingredient — display/lineage./rxcui/{rxcui}/properties.json and confirming thetty and name match expectations; record the score from approximate
matches as a confidence signal.
{system: "http://www.nlm.nih.gov/research/umls/rxnorm", code, display}.OpenMed's analyze_text returns a dict whose entities list contains, per
span, the keys text, label, confidence, start, end. Consume the
Pharmaceutical/Chemical entities directly:
import openmed, requests
note = "Patient on metformin 500 mg BID and atorvastatin 20 mg nightly."
result = openmed.analyze_text(
note,
model_name="pharma_detection_superclinical", # Pharmaceutical category
output_format="dict",
)
DRUG_LABELS = {"DRUG", "MEDICATION", "CHEM"} # OpenMed Pharmaceutical labels
for ent in result["entities"]:
if ent["label"] in DRUG_LABELS:
span = ent["text"] # e.g. "metformin"
rxcui = rxcui_for(span) or (
(approximate(span) or [{}])[0].get("rxcui")
)
print(span, "->", rxcui, f"(conf {ent['confidence']:.2f})")
Keep OpenMed's character offsets (start/end) alongside the RxCUI so every
code is traceable back to the exact source span — never store the raw note text
in your mapping table.
"500 mg" and "tablet" may be adjacent tokens. Reassemble using offsets before
querying for an SCD, or you will only get the ingredient.
multi-ingredient SCD; do not split them into two RxCUIs.
Lipitor (SBD/BN) and atorvastatin (IN/SCD) aredifferent RxCUIs of the same drug. Decide up front which TTY your pipeline
stores and map the other via /related.json.
approximateTerm will happily return a candidatefor garbage input. Gate on the returned score and re-validate with/properties.json before trusting it.
/rxcui/{rxcui}/historystatus.json to detectretired/remapped concepts; follow the remap rather than storing a dead code.
includes source vocabularies (e.g. some proprietary drug data) whose own
terms-of-use apply if you redistribute the full dataset — calling the live API
for normalization is unrestricted. Do not bundle UMLS to get RxNorm; RxNav is
the clean path.
de-identified drug string leaves the process to hit RxNav. Never send a raw
note containing PHI to the API.