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.
IN) from a prescribable
product — 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, see
mapping-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, strength 500 MG,
form Oral Tablet)./rxcui.json?name=. If empty, fall back
to /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 the
tty 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.
Lipitor (SBD/BN) and atorvastatin (IN/SCD) are
different 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 candidate
for garbage input. Gate on the returned score and re-validate with
/properties.json before trusting it./rxcui/{rxcui}/historystatus.json to detect
retired/remapped concepts; follow the remap rather than storing a dead code.