Blog

A Practical Architecture: RxNorm + SPL for Interaction APIs

Why identifier normalization and label evidence should be treated as separate layers in medication safety APIs.

Published Mar 6, 2026Updated Mar 13, 202613 min read
RxNormRxNavopenFDAAPI Design

Split normalization from evidence extraction

RxNorm APIs are designed for medication concept and identifier operations (such as name lookup and RxCUI mapping). openFDA label endpoints expose evidence-bearing label text, including interaction sections. These two capabilities serve fundamentally different purposes, and treating them as separate architectural layers produces cleaner, more maintainable systems.

Keeping these layers separate in your architecture improves maintainability and makes audit trails clearer. When a normalization issue arises (a drug name did not resolve correctly), you debug the normalization layer. When an extraction issue arises (an interaction was missed or misclassified), you debug the extraction layer. Coupling these concerns makes both harder to reason about.

This separation also allows each layer to evolve independently. RxNorm updates its terminology monthly. FDA labels update on their own schedule. Your extraction logic may change as you improve accuracy. Each layer can be versioned, tested, and deployed without affecting the others.

The RxNorm normalization stage in detail

The RxNorm API, maintained by the National Library of Medicine, provides several endpoints for drug name resolution. The primary goal of the normalization stage is to convert a free-text drug input (which may be a brand name, generic name, abbreviation, or misspelling) into a stable RxCUI (RxNorm Concept Unique Identifier).

The recommended resolution strategy uses a tiered approach. First, attempt an exact match using the /rxcui.json endpoint with the search=2 parameter, which enables normalized matching. This handles straightforward inputs like 'metformin' or 'Lipitor'. If no result is returned, fall back to the /approximateTerm.json endpoint, which provides fuzzy matching with configurable result limits. This handles misspellings, partial names, and non-standard abbreviations.

The /approximateTerm.json endpoint is particularly valuable for user-facing applications where input quality varies. It returns a ranked list of candidates with scores, allowing your system to either select the top match automatically (for high-confidence scores) or present disambiguation options to the caller.

Once you have an RxCUI, you can use additional RxNorm endpoints to retrieve related concepts. The /allrelated.json endpoint returns related drug forms, ingredients, and brand/generic mappings. The /ndcs.json endpoint maps an RxCUI to its associated NDC codes. These cross-references are useful for building comprehensive lookups against openFDA, which supports queries by RxCUI, NDC, generic name, and brand name.

  • Exact match: GET /rxcui.json?name={input}&search=2 -- returns RxCUI if the name matches a known concept after normalization.
  • Fuzzy match: GET /approximateTerm.json?term={input}&maxEntries=10 -- returns ranked candidates with confidence scores for typo-tolerant resolution.
  • Related concepts: GET /allrelated.json?rxcui={id} -- retrieves ingredient, brand, generic, and dose-form relationships for a given RxCUI.
  • NDC mapping: GET /ndcs.json?rxcui={id} -- maps an RxCUI to its associated National Drug Code identifiers.
  • RxNorm rate limits: 20 requests per second per IP address, no API key required.

openFDA label retrieval: query patterns and key fields

Once normalization produces an RxCUI, the next stage retrieves the corresponding FDA label from the openFDA /drug/label.json endpoint. The most reliable query path is by RxCUI: search=openfda.rxcui:{rxcui}&sort=effective_time:desc&limit=1. This returns the most recently updated label for the drug.

If an RxCUI query returns no results (which can happen for older or less common drugs), a fallback chain improves coverage. Try querying by NDC (search=openfda.product_ndc:{ndc}), then by generic name (search=openfda.generic_name:{name}), then by brand name (search=openfda.brand_name:{name}). Each step is progressively less precise but increases the chance of finding a label.

The key fields for interaction work are drug_interactions, warnings, contraindications, and boxed_warning. The drug_interactions field contains Section 7 text, which is the primary source for interaction statements. However, significant interaction information sometimes appears in the warnings or contraindications sections as well, particularly for severe or life-threatening interactions. A thorough extraction pipeline reads all four fields.

Each label result includes an openfda sub-object with cross-reference identifiers: spl_set_id (stable across label versions), rxcui (array of associated RxCUIs), product_ndc (array of NDC codes), generic_name, and brand_name. The spl_set_id is the recommended cache key because it remains constant even as the label content is updated.

  • Primary query: search=openfda.rxcui:{rxcui}&sort=effective_time:desc&limit=1
  • Fallback 1: search=openfda.product_ndc:{ndc}&sort=effective_time:desc&limit=1
  • Fallback 2: search=openfda.generic_name:"{name}"&sort=effective_time:desc&limit=1
  • Fallback 3: search=openfda.brand_name:"{name}"&sort=effective_time:desc&limit=1
  • Key interaction fields: drug_interactions, warnings, contraindications, boxed_warning
  • Cache key: openfda.spl_set_id with effective_time for freshness tracking

Extraction patterns for drug interaction text

The drug_interactions field in FDA labels is unstructured English prose written for healthcare professionals. Converting this text into structured interaction pairs requires a multi-pass extraction approach.

A deterministic first pass handles the straightforward patterns. Sentence splitting isolates individual statements. Keyword triggers identify interaction-relevant sentences: phrases like 'should not be used concomitantly with', 'may increase the risk of', 'co-administration with', and 'is contraindicated with' are reliable signals. Named entity recognition (NER) then identifies drug names, drug classes, and enzyme references (like CYP3A4 inhibitors) within those sentences.

The deterministic pass catches the explicit, well-structured interaction statements. However, FDA label text often describes interactions indirectly, through pharmacokinetic mechanisms, class-level warnings, or conditional statements that require interpretation. This is where a second pass using a large language model (LLM) adds value.

The LLM pass takes the label text and produces structured JSON for each identified interaction: the target drug or drug class, the mechanism of interaction, the clinical recommendation, and a severity assessment. Providing the LLM with a constrained output schema and requiring it to cite specific sentences from the source text improves both accuracy and auditability.

Combining both passes provides the best coverage. The deterministic pass is fast, predictable, and easy to test. The LLM pass catches nuanced interactions that rule-based systems miss. Running both and merging results, with the deterministic pass serving as a baseline and the LLM pass adding incremental findings, produces more complete extraction than either approach alone.

  • Sentence splitting: break drug_interactions text into individual statements for granular processing.
  • Keyword triggers: identify interaction-relevant sentences using phrases like 'concomitant use', 'co-administration', 'contraindicated with', 'may increase/decrease'.
  • Named entity recognition: extract drug names, drug classes (e.g., 'NSAIDs', 'MAO inhibitors'), and enzyme references (e.g., 'CYP3A4 inhibitors').
  • LLM structuring: pass label text to a language model with a constrained output schema to extract interaction pairs, mechanisms, and recommendations.
  • Evidence linking: retain the source sentence for each extracted interaction to support citation in API responses.

Severity classification approaches

FDA labels do not use a standardized severity scale for drug interactions. The text describes interactions in clinical terms, leaving severity classification as an engineering decision for API builders. A practical severity model uses five levels: contraindicated, major, moderate, minor, and unknown.

Contraindicated interactions are the most straightforward to classify. Labels use explicit language: 'is contraindicated', 'must not be used', 'do not co-administer'. These phrases map directly to the highest severity level. Boxed warnings that mention specific drug interactions also typically warrant contraindicated or major classification.

Major and moderate classifications require more judgment. Signals for major severity include language about life-threatening outcomes, hospitalization risk, or irreversible effects. Moderate severity applies to interactions that require monitoring, dose adjustment, or have clinically significant but manageable consequences. Minor interactions are typically pharmacokinetic observations with minimal clinical impact.

A deterministic rules-based classifier handles the clear cases: explicit contraindication language maps to contraindicated, boxed warning mentions map to major, and so on. For ambiguous cases, an LLM classifier that has been prompted with your severity definitions and examples can provide consistent classification. The unknown level serves as a fallback for interactions where the label text does not provide enough information to assess severity confidently.

Whichever approach you use, documenting your severity policy explicitly is important. Downstream consumers need to understand what each level means and how it was derived. The /docs page on this site describes the severity levels used in the RxLabelGuard API response schema.

Reference architecture pattern

A common pattern is: input normalization, then evidence retrieval, then interaction extraction, then severity policy, then response shaping. This five-stage pipeline lets teams independently test each stage and reduce silent failure modes.

In concrete terms, a request flows through: (1) RxNorm resolution converts input drug names to RxCUIs, (2) openFDA label retrieval fetches the drug_interactions text for each drug, (3) extraction identifies interaction pairs from the text, (4) severity classification assigns a level to each pair, and (5) response shaping formats the results with citations and metadata into a stable API contract.

Each stage has its own failure modes and its own testing surface. The normalization stage might fail to resolve an unusual drug name. The retrieval stage might hit rate limits or return no label. The extraction stage might miss an interaction or produce a false positive. The severity stage might misclassify. By isolating these stages, you can build targeted test suites and monitoring for each one.

  • Stage 1 - Normalization: map free text drug input to stable RxCUI identifiers via RxNorm API.
  • Stage 2 - Evidence retrieval: fetch label text from openFDA keyed by RxCUI, with NDC and name fallbacks.
  • Stage 3 - Extraction: identify interaction pairs using deterministic NER plus LLM structuring.
  • Stage 4 - Severity policy: classify each interaction as contraindicated, major, moderate, minor, or unknown.
  • Stage 5 - Response shaping: format results with severity, mechanism, recommendation, and evidence citations.

Caching strategy: spl_set_id, TTL, and freshness checks

Drug labels are updated infrequently, making aggressive caching both safe and beneficial for performance. The recommended cache key is the spl_set_id, which is a stable UUID assigned to a product's labeling that persists across label revisions. This is preferable to caching by drug name or RxCUI, which can map to multiple labels.

Each cache entry should store: the raw label JSON (or at minimum the interaction-relevant sections), the effective_time from the label, the extraction results (structured interaction pairs), and a timestamp for when the entry was last refreshed. The effective_time field is critical because it allows you to detect when a newer label version has been published without re-running extraction.

A practical TTL strategy uses two tiers. Short-lived cache entries (1-24 hours) apply to the label-to-extraction mapping, so that extraction results stay current when labels are updated. Long-lived cache entries (7-30 days) apply to the RxCUI-to-spl_set_id mapping, which changes rarely. This tiered approach minimizes unnecessary API calls while keeping interaction data fresh.

Background freshness checks compare your cached effective_time against the latest version from openFDA or DailyMed. When a newer version is detected, the cache entry is invalidated and re-fetched on the next request. This approach avoids stale data without requiring eager re-processing of the entire cache. The /how-it-compares page on this site discusses how different data sources handle versioning and freshness.

A concrete implementation example

To make this architecture tangible, here is a pseudocode-level walkthrough of how a drug interaction check request flows through the pipeline. This example checks for interactions between two drugs provided as free-text names.

The request handler receives two drug names. It calls the normalization service, which queries RxNorm /rxcui.json for each name. If exact match fails, it falls back to /approximateTerm.json. Each name resolves to an RxCUI. If either name fails to resolve, the response includes a resolution_failed status for that drug with suggested alternatives.

With RxCUIs in hand, the handler checks the cache for each drug's label by spl_set_id. For cache misses, it queries openFDA /drug/label.json by RxCUI, extracts the drug_interactions text, runs the extraction pipeline (deterministic pass, then LLM pass), stores the structured results in cache, and proceeds. For cache hits, it uses the stored extraction results directly.

The handler then performs cross-matching: for each interaction pair extracted from Drug A's label, it checks whether Drug B (or Drug B's drug class) is mentioned as a target. It performs the same check in reverse using Drug B's label. Matched interactions are deduplicated, severity-classified, and assembled into the response object with evidence citations linking back to the source label section and spl_set_id.

This flow involves multiple external API calls (RxNorm, openFDA) and a potential LLM invocation for extraction. In steady state, caching means most requests only hit the cross-matching logic and return in milliseconds. Cold-cache requests may take 2-5 seconds depending on external API latency and extraction complexity.

Testing and validation strategies for interaction pipelines

Testing an interaction pipeline requires coverage at multiple levels: unit tests for each stage, integration tests for the full pipeline, and validation against known interaction pairs.

Unit tests for the normalization stage verify that common drug names, brand names, misspellings, and abbreviations resolve to the correct RxCUI. A fixture set of 50-100 drug names with expected RxCUIs provides a stable regression suite. Tests should also cover failure cases: names that should not resolve, ambiguous names that should return multiple candidates, and empty or malformed inputs.

Unit tests for the extraction stage verify that known interaction statements in label text are correctly identified and structured. Building a test corpus of 20-30 label excerpts with manually annotated expected interactions provides ground truth for precision and recall measurement. Each test case should verify the target drug, mechanism, and severity classification.

Integration tests run the full pipeline end-to-end with mocked external APIs. These verify that the stages compose correctly: a drug name goes in, structured interactions come out. Mock responses for RxNorm and openFDA should be captured from real API calls and stored as fixtures, so tests remain realistic without depending on external services.

Validation against a reference set of known drug interactions provides a higher-level quality check. Pharmacological references document well-established interactions (such as warfarin with aspirin, or metformin with contrast dye). A validation suite that checks whether your pipeline correctly identifies these known pairs, with appropriate severity levels, catches regressions that unit tests might miss.

The /faqs page on this site addresses common questions about how interaction results are validated and how teams can evaluate the accuracy of API-derived interaction data.

  • Normalization tests: verify RxCUI resolution for common names, brand names, misspellings, and edge cases.
  • Extraction tests: verify that known interaction statements produce correct structured output with target, mechanism, and severity.
  • Integration tests: run the full pipeline with mocked external APIs to verify stage composition.
  • Validation suite: check pipeline output against a curated set of well-established drug interaction pairs.
  • Regression tracking: monitor precision and recall metrics over time as extraction logic evolves.
  • Cache behavior tests: verify that cache hits, misses, and invalidation work correctly under concurrent load.

Why this matters for regulated environments

Labeling regulations define where interaction content belongs in official product labeling. Section 7 of the FDA's Physician Labeling Rule is the designated location for drug interaction information, and the FDA's SPL format requires this content to be electronically tagged and searchable.

Aligning your API output to source citations and a deterministic policy layer can simplify internal review, QA, and customer trust discussions. When a customer asks 'where did this interaction come from?', you can point to a specific sentence in a specific label version identified by spl_set_id and effective_time. This traceability is not just good engineering practice; it is often a requirement for clinical decision support systems operating in regulated healthcare environments.

The separation of normalization, extraction, and severity policy also supports audit requirements. Each stage can be logged independently, providing a clear record of how an input was resolved, which label text was consulted, which interactions were extracted, and how severity was assigned. This audit trail is valuable for both internal quality reviews and external compliance assessments.

References

  1. RxNav APIs (U.S. National Library of Medicine (NLM); accessed Mar 6, 2026)
  2. RxNorm API (U.S. National Library of Medicine (NLM); accessed Mar 6, 2026)
  3. openFDA Drug Label Endpoint (U.S. Food and Drug Administration (FDA); accessed Mar 6, 2026)
  4. openFDA Drug Label Searchable Fields (U.S. Food and Drug Administration (FDA); accessed Mar 6, 2026)
  5. How Do I Use Prescription Drug Labeling (U.S. Food and Drug Administration (FDA); accessed Mar 6, 2026)
  6. openFDA Authentication (U.S. Food and Drug Administration (FDA); accessed Mar 6, 2026)
  7. The FDA Announces New Prescription Drug Information Format (U.S. Food and Drug Administration (FDA); accessed Mar 6, 2026)
  8. DailyMed (U.S. National Library of Medicine (NLM); accessed Mar 6, 2026)