Querying OpenFDA drug labels, NDC, and recalls
Once OpenMed has pulled a drug name out of a note, you often need authoritative
product facts: the boxed warning, approved indications, dosage form /
route, package NDC codes, and whether the product is under recall. The
FDA's OpenFDA API exposes the Structured Product Labeling (SPL), the NDC
directory, and enforcement (recall) reports — all public and free.
This skill is enrichment: it attaches regulatory facts to an extracted drug. It
is not clinical decision support — a label lookup informs a human, it does
not prescribe.
When to use
- You extracted a drug and need its boxed warning or indications for
display, alerting, or expectedness checks.
- You need NDC package codes, dosage form, or route for a product.
- You want to know if a drug/lot is under an open recall (enforcement).
- You want to map a brand name to its generic ingredient and RxCUI via the
label's
openfda block.
The three endpoints
| Endpoint |
Use |
Key fields |
https://api.fda.gov/drug/label.json |
SPL prescribing info |
boxed_warning, indications_and_usage, warnings, dosage_and_administration, openfda.brand_name, openfda.generic_name, openfda.rxcui, openfda.product_ndc |
https://api.fda.gov/drug/ndc.json |
NDC directory |
product_ndc, generic_name, brand_name, dosage_form, route, active_ingredients |
https://api.fda.gov/drug/enforcement.json |
Recalls |
product_description, reason_for_recall, classification (Class I/II/III), recalling_firm, status, recall_initiation_date |
No key needed to try it (240 req/min, 1,000/day per IP). A free api_key= raises
the daily cap to 120,000.
Quick start (real OpenFDA queries)
import requests
def openfda(endpoint: str, search: str, limit: int = 1) -> list[dict]:
url = f"https://api.fda.gov/drug/{endpoint}.json"
r = requests.get(url, params={"search": search, "limit": limit}, timeout=30)
if r.status_code == 404: # OpenFDA returns 404 for zero matches
return []
r.raise_for_status()
return r.json().get("results", [])
# 1) Label: boxed warning + indications for a generic drug.
label = openfda("label", 'openfda.generic_name:"warfarin"')
if label:
rec = label[0]
print("Boxed warning:", rec.get("boxed_warning", ["(none)"])[0][:200])
print("Indication:", rec.get("indications_and_usage", ["(none)"])[0][:200])
print("RxCUI:", rec.get("openfda", {}).get("rxcui"))
# 2) NDC: package codes, form, route.
ndc = openfda("ndc", 'generic_name:"warfarin"', limit=5)
for rec in ndc:
print(rec["product_ndc"], rec.get("dosage_form"), rec.get("route"))
# 3) Enforcement: open recalls for a product.
recalls = openfda("enforcement",
'product_description:"warfarin"+AND+status:"Ongoing"', limit=5)
for rec in recalls:
print(rec["classification"], "-", rec["reason_for_recall"][:120])
Workflow
- Normalize the drug name first. Use
openmed.analyze_text to get the span,
then prefer the RxNorm ingredient (see normalizing-rxnorm) as your query
term — openfda.generic_name and the NDC generic_name index on the
ingredient, so a normalized name hits far more records than raw note text.
- Query
/drug/label with openfda.generic_name:"<ingredient>" (or
openfda.rxcui:"<rxcui>" for an exact product). Read boxed_warning,
indications_and_usage, warnings_and_cautions.
- Query
/drug/ndc for package-level codes, dosage form, and route.
- Query
/drug/enforcement filtered to status:"Ongoing" to surface open
recalls; gate alerts on classification (Class I = most serious).
- Cache results — labels change rarely; you do not need to re-query per note.
- Attach the facts to the extracted drug keyed by RxCUI/NDC for traceability.
Hand-off to / from OpenMed
OpenMed's analyze_text returns a dict; result["entities"] items carry
text, label, confidence, start, end.
- From
extracting-clinical-entities: Pharmaceutical/Chemical entities are
the query seeds. From normalizing-rxnorm: pass the RxCUI to
openfda.rxcui:"..." for an exact label match.
- To
reporting-adverse-events: the boxed warning / indications support an
expectedness judgment (is this reaction labeled?). To
detecting-pv-signals: confirm whether a disproportionality signal is already
on-label before escalating.
- OpenMed runs NER on-device; only a de-identified drug name or RxCUI
leaves the process to hit OpenFDA. Never send a raw note containing PHI to
the API — de-identify with
openmed.deidentify first if you must derive the
query from patient text.
Edge cases & gotchas
- OpenFDA returns 404 for an empty result set, not an empty
results list —
handle it as "no match" (the helper above does).
- Multi-value fields are lists.
boxed_warning, indications_and_usage, and
most SPL sections are arrays of strings (rec["boxed_warning"][0]). Many
products have no boxed warning — the key is simply absent.
- Brand vs generic.
openfda.brand_name and openfda.generic_name differ;
query the generic (ingredient) for coverage, the brand for a specific product.
- Labels are SPL snapshots, not real-time. OpenFDA mirrors DailyMed SPL; a
brand-new labeling change may lag. For the definitive current label, cross-check
DailyMed.
- NDC formats vary (
product_ndc is the 2-segment labeler-product code;
package NDCs add a third segment). Normalize before joining to claims data.
- Recall
status is one of Ongoing, Completed, Terminated — filter to
Ongoing for active risk; classification Class I > II > III by severity.
- Public and free, but rate-limited. Register a free key and cache; do not
hammer the API per-note in a batch pipeline.
Standards & references
1---2name: querying-openfda-labels3description: Looks up FDA drug labels, NDC directory entries, indications, boxed warnings, and recalls/enforcement actions via the free public OpenFDA API to enrich drugs that OpenMed extracts. Use when the user wants the prescribing information for a drug, its boxed warning, approved indications, dosage forms and routes, package NDC codes, RxCUI, or whether a product has an open recall. Trigger keywords: OpenFDA, drug label, SPL, prescribing information, boxed warning, black box warning, indications, NDC, package code, recall, enforcement, Class I recall, drug enrichment. Pairs adjacent to OpenMed NER: take a drug name (or RxNorm RxCUI) from openmed.analyze_text and resolve its label, NDC, and recall status. OpenFDA is public and free — no license barrier; send only de-identified drug names, never raw clinical notes.4license: Apache-2.05---67# Querying OpenFDA drug labels, NDC, and recalls89Once OpenMed has pulled a drug name out of a note, you often need authoritative10product facts: the **boxed warning**, approved **indications**, **dosage form /11route**, package **NDC** codes, and whether the product is under **recall**. The12FDA's **OpenFDA** API exposes the Structured Product Labeling (SPL), the NDC13directory, and enforcement (recall) reports — all **public and free**.1415This skill is enrichment: it attaches regulatory facts to an extracted drug. It16is **not** clinical decision support — a label lookup informs a human, it does17not prescribe.1819## When to use2021- You extracted a drug and need its **boxed warning** or **indications** for22 display, alerting, or expectedness checks.23- You need **NDC** package codes, dosage form, or route for a product.24- You want to know if a drug/lot is under an **open recall** (enforcement).25- You want to map a brand name to its generic ingredient and **RxCUI** via the26 label's `openfda` block.2728## The three endpoints2930| Endpoint | Use | Key fields |31| --- | --- | --- |32| `https://api.fda.gov/drug/label.json` | SPL prescribing info | `boxed_warning`, `indications_and_usage`, `warnings`, `dosage_and_administration`, `openfda.brand_name`, `openfda.generic_name`, `openfda.rxcui`, `openfda.product_ndc` |33| `https://api.fda.gov/drug/ndc.json` | NDC directory | `product_ndc`, `generic_name`, `brand_name`, `dosage_form`, `route`, `active_ingredients` |34| `https://api.fda.gov/drug/enforcement.json` | Recalls | `product_description`, `reason_for_recall`, `classification` (Class I/II/III), `recalling_firm`, `status`, `recall_initiation_date` |3536No key needed to try it (240 req/min, 1,000/day per IP). A free `api_key=` raises37the daily cap to 120,000.3839## Quick start (real OpenFDA queries)4041```python42import requests4344def openfda(endpoint: str, search: str, limit: int = 1) -> list[dict]:45 url = f"https://api.fda.gov/drug/{endpoint}.json"46 r = requests.get(url, params={"search": search, "limit": limit}, timeout=30)47 if r.status_code == 404: # OpenFDA returns 404 for zero matches48 return []49 r.raise_for_status()50 return r.json().get("results", [])5152# 1) Label: boxed warning + indications for a generic drug.53label = openfda("label", 'openfda.generic_name:"warfarin"')54if label:55 rec = label[0]56 print("Boxed warning:", rec.get("boxed_warning", ["(none)"])[0][:200])57 print("Indication:", rec.get("indications_and_usage", ["(none)"])[0][:200])58 print("RxCUI:", rec.get("openfda", {}).get("rxcui"))5960# 2) NDC: package codes, form, route.61ndc = openfda("ndc", 'generic_name:"warfarin"', limit=5)62for rec in ndc:63 print(rec["product_ndc"], rec.get("dosage_form"), rec.get("route"))6465# 3) Enforcement: open recalls for a product.66recalls = openfda("enforcement",67 'product_description:"warfarin"+AND+status:"Ongoing"', limit=5)68for rec in recalls:69 print(rec["classification"], "-", rec["reason_for_recall"][:120])70```7172## Workflow73741. **Normalize the drug name first.** Use `openmed.analyze_text` to get the span,75 then prefer the **RxNorm ingredient** (see `normalizing-rxnorm`) as your query76 term — `openfda.generic_name` and the NDC `generic_name` index on the77 ingredient, so a normalized name hits far more records than raw note text.782. **Query `/drug/label`** with `openfda.generic_name:"<ingredient>"` (or79 `openfda.rxcui:"<rxcui>"` for an exact product). Read `boxed_warning`,80 `indications_and_usage`, `warnings_and_cautions`.813. **Query `/drug/ndc`** for package-level codes, dosage form, and route.824. **Query `/drug/enforcement`** filtered to `status:"Ongoing"` to surface open83 recalls; gate alerts on `classification` (Class I = most serious).845. **Cache** results — labels change rarely; you do not need to re-query per note.856. **Attach the facts to the extracted drug** keyed by RxCUI/NDC for traceability.8687## Hand-off to / from OpenMed8889OpenMed's `analyze_text` returns a `dict`; `result["entities"]` items carry90`text`, `label`, `confidence`, `start`, `end`.9192- **From** `extracting-clinical-entities`: Pharmaceutical/Chemical entities are93 the query seeds. **From** `normalizing-rxnorm`: pass the RxCUI to94 `openfda.rxcui:"..."` for an exact label match.95- **To** `reporting-adverse-events`: the boxed warning / indications support an96 **expectedness** judgment (is this reaction labeled?). **To**97 `detecting-pv-signals`: confirm whether a disproportionality signal is already98 on-label before escalating.99- OpenMed runs NER **on-device**; only a **de-identified drug name or RxCUI**100 leaves the process to hit OpenFDA. **Never** send a raw note containing PHI to101 the API — de-identify with `openmed.deidentify` first if you must derive the102 query from patient text.103104## Edge cases & gotchas105106- **OpenFDA returns 404 for an empty result set**, not an empty `results` list —107 handle it as "no match" (the helper above does).108- **Multi-value fields are lists.** `boxed_warning`, `indications_and_usage`, and109 most SPL sections are arrays of strings (`rec["boxed_warning"][0]`). Many110 products have *no* boxed warning — the key is simply absent.111- **Brand vs generic.** `openfda.brand_name` and `openfda.generic_name` differ;112 query the generic (ingredient) for coverage, the brand for a specific product.113- **Labels are SPL snapshots, not real-time.** OpenFDA mirrors DailyMed SPL; a114 brand-new labeling change may lag. For the definitive current label, cross-check115 DailyMed.116- **NDC formats vary** (`product_ndc` is the 2-segment labeler-product code;117 package NDCs add a third segment). Normalize before joining to claims data.118- **Recall `status`** is one of `Ongoing`, `Completed`, `Terminated` — filter to119 `Ongoing` for active risk; `classification` Class I > II > III by severity.120- **Public and free, but rate-limited.** Register a free key and cache; do not121 hammer the API per-note in a batch pipeline.122123## Standards & references124125- OpenFDA drug label API: https://open.fda.gov/apis/drug/label/126- OpenFDA NDC directory API: https://open.fda.gov/apis/drug/ndc/127- OpenFDA drug enforcement (recalls) API: https://open.fda.gov/apis/drug/enforcement/128- OpenFDA query syntax & rate limits: https://open.fda.gov/apis/query-syntax/ , https://open.fda.gov/apis/authentication/129- FDA Structured Product Labeling (SPL): https://www.fda.gov/industry/structured-product-labeling-resources130- DailyMed (authoritative labels): https://dailymed.nlm.nih.gov/dailymed/