Medication API Integration Guide — where the APIs plug in & how to make them work¶
Date: 2026-06-04 · From: Gene (Pharmacy Ops) · For: Faraz + Emil
What this answers: where each external API plugs into the engine, how to call it (endpoints, keys, rate limits), and how to make it work properly in production (caching, retries, fallback, the PHI boundary). The demo already has a working reference call (try_rxnorm()); this is the production version.
The one rule that prevents 90% of the pain: engines never call the public APIs directly. They call the MCP server (the knowledge layer's one door, Sprint-B), which calls the external APIs, caches the result, and falls back when an API is slow/down. One resolver, called once, cached — not five engines hammering RxNorm.
1 · Where each API plugs into the pipeline¶
input → [NORMALIZE] ───────────────► [SMART-MATCH] ──► [SIG/context] ──► confirm → object → downstream
│ RxNorm/RxNav (identity) │ modal-priors │ DailyMed/SPL
│ OpenFDA NDC (NDC→identity) │ (our table) │ (food/timing)
│ RxClass (drug classes) │ │
└──────────── all wrapped behind the MCP server (cache + fallback) ──────────────┘
(provenance/ingest path) NCPDP SCRIPT / Surescripts → same medication object [future]
(reasoning path) LLM API → only AFTER PHI redaction → behind the deterministic rail
| Pipeline stage | API | MCP tool it backs |
|---|---|---|
| Normalize (name/NDC → identity) | RxNorm / RxNav + OpenFDA NDC | resolve_drug() |
| Drug-class linking (duplicate / interaction / FRID) | RxClass (part of RxNav) | graph_query() |
| Dose-time context (food/timing) | DailyMed SPL / openFDA label | tolerance() / semantic_search() |
| Provenance ingest (verified e-Rx) | NCPDP SCRIPT / Surescripts (future) | feeds the object directly |
| Reasoning / phrasing | LLM API (self-host or behind redaction) | the model layer, behind the rail |
2 · Per-API: endpoint, auth, rate limit, how to call¶
RxNorm / RxNav (identity + drug classes) — free, no key¶
- Base:
https://rxnav.nlm.nih.gov/REST - Name → RXCUI:
GET /rxcui.json?name=metformin&search=2 - NDC → RXCUI:
GET /rxcui.json?idtype=NDC&id=00093104801 - RXCUI → attributes/related:
GET /rxcui/{rxcui}/allrelated.json(forms/strengths) - Drug classes (for flags):
GET /rxclass/class/byRxcui.json?rxcui={rxcui}&relaSource=ATC(or MEDRT/VA) - Rate limit: NLM asks ≤ 20 requests/sec per IP — throttle + cache. No key.
- Reference call: the demo's
try_rxnorm()(name → rxcui, 2.5 s timeout, returnsNoneon failure).
OpenFDA — NDC Directory + drug label — free; get the free API key¶
- NDC → product:
GET https://api.fda.gov/drug/ndc.json?search=product_ndc:"00093-1048-01" - Label (food/dosage text):
GET https://api.fda.gov/drug/label.json?search=openfda.rxcui:{rxcui} - Auth: none required, but without a key = 240 req/min + 1,000/day; with a free key = 240 req/min + 120,000/day. Get the key (api.fda.gov → API basics) and put it in
?api_key=…. Verify current limits at request time — they change.
DailyMed SPL (package inserts, food/timing) — free (v2 / Q3)¶
- Base:
https://dailymed.nlm.nih.gov/dailymed/services/v2 - By RXCUI/NDC:
GET /spls.json?rxcui={rxcui}→ SPL set → pull "Dosage & Administration." - Use to populate the dose-time context payload (with-food / empty-stomach / separate-from-X).
NCPDP SCRIPT / Surescripts (verified e-Rx ingest) — commercial / certification (future)¶
- Not a REST call you "plug in" — it's an integration + certification (Surescripts). Incoming SCRIPT (SCS) messages map to the same medication object (that's why we built structured now). Plan: a mapping layer, not a rewrite. See the Structured SIG Schema spec's Surescripts strategy.
FDB / Medi-Span (clinical depth: full DDI, Beers) — commercial ($15–100K/yr) (future, Q4)¶
- Only when the enterprise pitch needs full CDS. The RxNorm/RxClass + our curated KB cover the wellness-lane needs first.
The LLM API (reasoning) — behind redaction + the rail¶
- Self-host a Llama-class model for any PHI-touching path; or call a hosted API only after PHI redaction (Presidio reverse-proxy — drug names aren't PHI, but member context is). Every clinical fact comes from the MCP server (grounded), never the model's memory. Output always passes the deterministic rail before a member sees it.
3 · How to make them work properly (the production rules)¶
- Wrap everything in the MCP server. Engines call
resolve_drug()etc.; the server owns the HTTP, the cache, the keys, the fallback. Engines never seeapi.fda.gov. - Cache-first, aggressively. Drug identity (RXCUI, NDC→product, classes) rarely changes — cache by
(name|ndc)with a long TTL (days/weeks). A resolved drug should hit the network once, ever. This is also what makes 6M users affordable (the scale KB's "cache-as-cost"). - Structured-first. Resolve identity from our cached table / curated KB first; only call the live API for a new/unknown drug (the demo does exactly this — seeded knowledge, live call as enrichment).
- Throttle + key. RxNav ≤ 20 req/sec; OpenFDA → use the free key for 120K/day. Add a token-bucket limiter in the MCP server.
- Timeouts + retries + circuit breaker. Short timeout (≈2–3 s), 1 retry with backoff, and a circuit breaker: if an API is failing, stop calling it and serve cached/seeded for a cooldown. (The demo's 2.5 s timeout +
Nonefallback is the seed of this.) - Graceful fallback — never block the user. If the API is slow/down, fall back to cached → seeded → manual entry. A med never fails to enter because RxNorm hiccupped. (This is the "never invent" guardrail's safe partner.)
- The PHI boundary. These public drug APIs take drug names/NDCs — not PHI — so they're safe to call. The member context (who, their list) stays internal; only the LLM path needs PHI redaction. De-identify before anything hits the learning store.
- Observability. Log per-API: latency (p95/p99), error rate, cache-hit rate, and which calls fell back. Alert on error-rate or fallback-rate spikes (an API change usually shows here first).
4 · The reference implementation (already in the demo)¶
main.py → try_rxnorm() is the working pattern, in miniature:
def try_rxnorm(name): # live enrichment; never blocks the demo
try:
r = requests.get("https://rxnav.nlm.nih.gov/REST/rxcui.json",
params={"name": name, "search": 2}, timeout=2.5)
ids = r.json().get("idGroup", {}).get("rxnormId")
return ids[0] if ids else None
except Exception:
return None # → fall back to seeded/cached
data/knowledge.json is the shape the APIs fill — in production that table is the cache + curated KB, the APIs refresh it.
5 · Quick start checklist for Faraz¶
- [ ] Stand up the MCP server (Sprint B) as the single API wrapper.
- [ ] Wire RxNorm/RxNav (no key) →
resolve_drug+graph_query(RxClass). - [ ] Get the free OpenFDA API key → NDC + label.
- [ ] Add a cache (long TTL on identity) + token-bucket throttle + circuit breaker.
- [ ] Keep the curated KB / seeded table as the always-on fallback (never block entry).
- [ ] Defer DailyMed (Q3), NCPDP/Surescripts + FDB (future) — the object is already shaped for them.
Companions: AI_Foundation_Sprint_A-B_Specs (the MCP tools the APIs back) · Structured_Sig_Schema_v1/Spec.md §3.2 (the original dependency table) · the running demo (~/Desktop/rx360_med_intel_demo/main.py, try_rxnorm).