Skip to content

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, returns None on 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)

  1. Wrap everything in the MCP server. Engines call resolve_drug() etc.; the server owns the HTTP, the cache, the keys, the fallback. Engines never see api.fda.gov.
  2. 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").
  3. 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).
  4. Throttle + key. RxNav ≤ 20 req/sec; OpenFDA → use the free key for 120K/day. Add a token-bucket limiter in the MCP server.
  5. 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 + None fallback is the seed of this.)
  6. 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.)
  7. 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.
  8. 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
Path to production: move this into the MCP server · add the cache (Redis/pg) · add OpenFDA (with the key) + RxClass + DailyMed · add the token-bucket + circuit breaker · keep the seeded/curated KB as the always-on fallback. The demo's 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).