← All articles
TechnicalJune 11, 2026

NDC → RxNorm → SNOMED: Crosswalk Any Drug Code in One API Call

Stop stitching RxNav calls to NDC flat files. Here's how to crosswalk a drug code across NDC, RxNorm, and SNOMED in a single API call — with working TypeScript.

NDC to RxNorm to SNOMED drug code crosswalk

You have an NDC from a pharmacy claim. The clinical decision support engine downstream speaks RxNorm. The patient summary you're building wants SNOMED CT. These are three different code systems maintained by three different organizations, and getting from one to another is the kind of plumbing that quietly eats a sprint.

The usual approach is to wire up the NLM's RxNav API for the RxCUI, parse a quarterly NDC flat file to fill the gaps, and then hand-maintain a UMLS-derived table to reach SNOMED. Three integrations, three update cadences, three places for the mapping to drift.

FHIRfly stores these code systems as a linked graph, not 22 isolated lookups. The NDC → RxCUI → SNOMED chain is pre-computed and returned inline. This tutorial shows how to walk it in either direction with the TypeScript SDK and the REST API. You'll have working code in a few minutes.

The Drug Code Graph

Three identifiers, three jobs:

  • NDC (National Drug Code) — the FDA's identifier for a specific drug package on the US market. Manufacturer-specific. The code on the bottle.
  • RxCUI (RxNorm Concept Unique Identifier) — the NLM's normalized drug concept. One RxCUI ("atorvastatin 40 MG Oral Tablet") maps to hundreds of NDCs from different labelers.
  • SNOMED CT — the clinical terminology used in problem lists, summaries, and FHIR Medication resources internationally.

The relationships are many-to-one and many-to-many: many NDCs collapse to one RxCUI; an RxCUI links out to a SNOMED concept. FHIRfly derives the SNOMED mapping through the RxCUI chain (UMLS), so every NDC carries its normalized concept and its clinical concept without a second round trip.

Setup

npm install @fhirfly-io/terminology
import { Fhirfly } from "@fhirfly-io/terminology";

const client = new Fhirfly({ apiKey: process.env.FHIRFLY_API_KEY! });

Get a free API key from the FHIRfly dashboard — 10,000 requests/month, no credit card.

NDC → RxNorm + SNOMED in One Call

Start with an NDC and request the standard shape. The response carries the RxCUI and the derived SNOMED mapping inline:

const { data } = await client.ndc.lookup("70518-4321", { shape: "standard" });

console.log(data.generic_name);
// "Atorvastatin calcium"

console.log(data.rxcui);
// ["617311"]   ← the normalized RxNorm concept

console.log(data.snomed);
// [
//   {
//     concept_id: "1145421000",
//     display: "Atorvastatin (as atorvastatin calcium) 40 mg oral tablet",
//     map_type: "equivalent",
//     map_source: "derived-rxnorm"
//   }
// ]

One lookup, three code systems. The map_source: "derived-rxnorm" tells you the SNOMED concept was reached through the RxCUI chain — useful provenance when you're auditing how a clinical code ended up on a record.

You also get a ready-to-use FHIR coding for the NDC itself:

console.log(data.fhir_coding);
// {
//   system: "http://hl7.org/fhir/sid/ndc",
//   code: "70518-4321",
//   display: "Atorvastatin calcium"
// }

RxNorm → NDC: Every Package for a Drug

The graph runs both ways. Hand the RxCUI back to the RxNorm endpoint and you get every NDC that normalizes to that concept — useful for "find all packages of this drug" or expanding a formulary entry into billable codes:

const { data } = await client.rxnorm.lookup("617311", { shape: "standard" });

console.log(data.name, data.tty);
// "atorvastatin 40 MG Oral Tablet"  "SCD"

console.log(data.ndcs.length);
// 365

console.log(data.ndcs.slice(0, 3));
// ["00093505810", "00093505898", "00378212177"]

That single RxCUI fans back out to 365 NDC packages from different labelers — the reverse of the collapse you saw in the first call.

A Note on Term Types

RxNorm concepts have a term type (tty) that determines which fields are populated. This trips people up, so it's worth being explicit:

  • Drug conceptsSCD (clinical drug), SBD (branded drug) — carry ndcs. That's where the package mappings live.
  • Brand and ingredient conceptsBN (brand name), IN (ingredient) — carry brand_names and ingredients.

So to get the brand and active ingredient, walk to the brand concept:

const { data } = await client.rxnorm.lookup("153165", { shape: "standard" });

console.log(data.name, data.tty);
// "Lipitor"  "BN"

console.log(data.brand_names);
// ["Lipitor"]

console.log(data.ingredients);
// [{ rxcui: "83367", name: "atorvastatin", tty: "IN" }]

If you query a drug concept and find brand_names empty, you're at the wrong altitude in the graph — follow the ingredient's RxCUI, not the drug's.

SNOMED → Clinical Concept

The SNOMED concept_id from the first call is a real, resolvable code. Look it up to get the fully specified name and preferred term for a problem list or Medication resource:

const { data } = await client.snomed.lookup("1145421000");

console.log(data.preferred_term);
// "Atorvastatin (as atorvastatin calcium) 40 mg oral tablet"

console.log(data.fsn);
// "Product containing precisely atorvastatin (as atorvastatin calcium)
//  40 milligram/1 each conventional release oral tablet (clinical drug)"

console.log(data.semantic_tag);
// "clinical drug"

That completes the chain: NDC 70518-4321 → RxCUI 617311 → SNOMED 1145421000, with display strings you can render at every hop.

Putting It Together: A Crosswalk Helper

In practice you want one function that takes any NDC and returns the whole picture. Here's a typed helper that walks the graph and degrades gracefully when a hop is missing:

import { Fhirfly } from "@fhirfly-io/terminology";

const client = new Fhirfly({ apiKey: process.env.FHIRFLY_API_KEY! });

interface DrugCrosswalk {
  ndc: string;
  name: string | null;
  rxcui: string | null;
  snomed: { code: string; display?: string } | null;
}

async function crosswalkNdc(ndc: string): Promise<DrugCrosswalk | null> {
  try {
    const { data } = await client.ndc.lookup(ndc, { shape: "standard" });
    return {
      ndc: data.ndc,
      name: data.generic_name ?? data.brand_name ?? null,
      rxcui: data.rxcui[0] ?? null,
      snomed: data.snomed?.[0]
        ? { code: data.snomed[0].concept_id, display: data.snomed[0].display }
        : null,
    };
  } catch (err) {
    // not_found / invalid NDC — return null and let the caller decide
    return null;
  }
}

const result = await crosswalkNdc("70518-4321");
// {
//   ndc: "70518-4321",
//   name: "Atorvastatin calcium",
//   rxcui: "617311",
//   snomed: { code: "1145421000", display: "Atorvastatin ... 40 mg oral tablet" }
// }

No second service, no local mapping table — the RxCUI and SNOMED concept come back on the first response.

Batch Crosswalking a Claims File

Enriching a pharmacy claims export? Don't loop the single-lookup endpoint. The batch endpoint takes up to 500 NDCs per request and returns the same standard shape — RxCUI and SNOMED included — with a per-item status so one bad code doesn't sink the batch:

const response = await client.ndc.lookupMany(
  ["70518-4321", "70518-3783", "70518-3848", "0000-0000-00"],
  { shape: "standard" }
);

for (const item of response.results) {
  if (item.status === "ok") {
    console.log(`${item.input} → RxCUI ${item.data.rxcui[0]}`);
  } else {
    console.log(`${item.input} → ${item.status}`);
  }
}
// "70518-4321 → RxCUI 617311"
// "70518-3783 → RxCUI 617310"
// "70518-3848 → RxCUI 259255"
// "0000-0000-00 → not_found"

Three NDCs from the same labeler, three different RxCUIs — that's the normalization doing real work: each strength collapses to its own clinical concept.

REST API (Without the SDK)

The crosswalk is just response fields, so any language works. Read rxcui and snomed straight off the standard shape:

# NDC → RxCUI + SNOMED
curl -H "x-api-key: $FHIRFLY_API_KEY" \
  "https://api.fhirfly.io/v1/ndc/70518-4321?shape=standard"

# RxCUI → NDCs (reverse)
curl -H "x-api-key: $FHIRFLY_API_KEY" \
  "https://api.fhirfly.io/v1/rxnorm/617311?shape=standard"

# Resolve the SNOMED concept
curl -H "x-api-key: $FHIRFLY_API_KEY" \
  "https://api.fhirfly.io/v1/snomed/1145421000"

# Batch NDC crosswalk
curl -X POST -H "x-api-key: $FHIRFLY_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"codes": ["70518-4321", "70518-3783"]}' \
  "https://api.fhirfly.io/v1/ndc/_batch?shape=standard"

Why This Is One Call, Not Five

The point isn't that any single lookup is hard — it's that the joins are. RxNav gives you the RxCUI but not the FDA package detail. The FDA NDC file gives you the package but not the SNOMED concept. The UMLS gives you the SNOMED mapping but ships as a multi-gigabyte relational dump you have to load and refresh.

FHIRfly runs those joins as an enrichment pipeline and denormalizes the result onto each record. The map_source: "derived-rxnorm" field keeps the derivation honest — you always know the SNOMED concept came through the RxCUI chain rather than a direct assertion. And the source data is public domain:

const { meta } = await client.ndc.lookup("70518-4321", { shape: "standard" });

console.log(meta.legal);
// {
//   license: "public_domain",
//   attribution_required: false,
//   source_name: "FDA NDC Directory",
//   citation: "FDA NDC Directory. Accessed 2026-06-11 via FHIRfly."
// }

Key Takeaways

  • One lookup, three code systems — the standard NDC shape returns the RxCUI and a derived SNOMED concept inline
  • The graph runs both ways — an RxCUI fans back out to every NDC package that normalizes to it
  • Mind the term typendcs live on drug concepts (SCD/SBD); brand_names and ingredients live on brand and ingredient concepts (BN/IN)
  • Provenance is explicitmap_source: "derived-rxnorm" tells you how the SNOMED concept was reached
  • Batch up to 500 NDCs per request with per-item status for claims enrichment
  • Public domain source data — no licensing restrictions on what you build

Next Steps

  1. Get a free API key from the FHIRfly dashboard
  2. Install the SDK: npm install @fhirfly-io/terminology
  3. Run crosswalkNdc() against your own NDC codes
  4. See the NDC, RxNorm, and SNOMED API docs for the full parameter reference
Tagsndcrxnormsnomedtutorialapi
Written by The FHIRfly Team — healthcare data, AI, and interoperability folks building better clinical coding APIs.

Build it on real terminology

Try any endpoint live — no sign-up required.

© 2026 FHIRfly.io LLC. All rights reserved. · Terminology data sourced from official registries, updated daily.