← Methods repository
concept

NDC (National Drug Code)

A unique, three-segment numeric identifier assigned by the FDA to every commercially marketed drug product in the United States, encoding the labeler, the drug product, and the package configuration; the primary key that links a pharmacy dispensing event to a specific drug, strength, formulation, and package size in claims and EHR data.

Data_Standardcoding-systemdata-standardprimitivedrugsndcpharmacy-claimsdrug-exposurencpdp
Methods reference only. Use primary source citations and local policy before applying this in a study protocol, regulatory submission, payer dossier, or clinical decision.

In plain language

An NDC (National Drug Code) is the unique number printed on every drug package sold in the United States — think of it as a drug's bar-code identity card, telling you exactly which company made it, what drug and strength it is, and how it was packaged. When a patient fills a prescription at a pharmacy, that dispensing event is recorded in a claims database with an 11-digit NDC, giving researchers a precise way to look up which drug was dispensed on which day. The main catch is that the same drug (say, atorvastatin 40 mg) can have dozens of different NDCs because every generic manufacturer and every package size gets its own code, so finding all fills of a drug requires a complete list of relevant NDCs — missing even one repackager means silently undercounting how many patients actually took that drug.

NDC (National Drug Code)

is the FDA-assigned numeric identifier that is stamped on every drug package sold in the United States. Each code is built from three segments separated by hyphens — labeler, product, and package — and that three-part structure is the first thing a pharmacoepidemiologist must understand before building any drug exposure code list or drug utilization measure from claims or EHR data.

Structure: three segments, one drug product per package

The labeler code (FDA-assigned, 4–5 digits) identifies the firm responsible for the product — the manufacturer, repacker, or private-label distributor. The product code (3–4 digits) identifies a specific drug, strength, dosage form, and formulation within that labeler's catalog. The package code (1–2 digits) identifies the specific package size and type (e.g., a 30-count bottle vs. a 90-count bottle of the same tablet).

This three-level hierarchy has a direct and important consequence: one drug and one strength generates many NDCs. Atorvastatin 40 mg tablets marketed by the originator brand (Lipitor) carries one labeler code and a handful of package-size NDCs; the same molecule marketed by generic manufacturers — and repackaged by wholesalers — generates dozens or hundreds of additional NDCs, all denoting the same clinical entity. An exposure code list that captures only the brand NDC silently misses every generic fill, undercounting exposure dramatically. Rolling NDCs up to a single drug-strength entity requires either an NDC-to-RxNorm mapping (the NLM RxNorm API or a commercial drug dictionary) or a manually curated NDC list refreshed periodically as new labelers enter the market.

The 10-digit vs. 11-digit problem: where the most common NDC corruption occurs

The FDA issues NDCs in a 10-digit format, but the segment lengths are not standardized: a labeler may receive a code configured as 4-4-2, 5-3-2, or 5-4-1 (labeler-product-package digits). The FDA directory and drug labels carry these hyphenated 10-digit codes. HIPAA, however, standardized pharmacy claims on an 11-digit, 5-4-2 format so that every field in a transaction has a fixed width and hyphens are dropped. The bridge from FDA 10-digit to HIPAA 11-digit is left-padding the deficient segment with one leading zero — but which segment receives the zero depends on the source format:

  • 4-4-2 format → the labeler segment is short; pad labeler to 5 digits: `0002-3227-30` → `00002-3227-30` → `00002322730`
  • 5-3-2 format → the product segment is short; pad product to 4 digits: `00093-490-05` → `00093-0490-05` → `000930490005`
  • 5-4-1 format → the package segment is short; pad package to 2 digits: `00071-0155-2` → `00071-0155-02` → `000710155002`

A naive approach — prepend one zero to the whole 10-digit string — works only for 4-4-2 codes and silently corrupts the other two patterns. Corrupt 11-digit NDCs either fail to match any record in the FDA directory or, worse, match a different real product, producing silent exposure misclassification. Every NDC normalization function must detect which format the hyphenated source code uses before padding.

Pros, cons, and trade-offs

vs. brand/generic name text strings: NDC is machine-exact and deduplicates naturally across data sources; free-text drug names require NLP, tolerate typos, and vary by data vendor. Use NDC for any structured claims or EHR dispense record; fall back to text only when NDC is absent (some inpatient EHR administrations, international data). The trade-off: NDC is package-level, so a single clinical drug entity spans many NDC values and the list requires active maintenance as new generics and repackagers enter the market.

vs. RxNorm as the exposure identifier: RxNorm normalizes across labelers and packages to a single concept per drug-strength-form, making it the preferred identifier for comparative effectiveness code lists and OMOP-based analyses. NDC is the native grain of US pharmacy claims and is what actually appears in the `NDC` field of an NCPDP transaction; you start at NDC and roll up to RxNorm, not the reverse. In practice, most pharmacoepidemiology workflows use NDC to capture the raw dispensing event and then map to RxNorm for analytic grouping.

vs. HCPCS J-codes for physician-administered drugs: Drugs dispensed by a pharmacy carry NDCs; drugs administered in a physician office or outpatient clinic are billed using HCPCS Level II J-codes on the medical claim. An infused biologic (e.g., infliximab) will appear as a J-code in the medical claim and may additionally carry a line-level NDC on the CMS-1500 professional claim. Researchers studying combined oral-and-infused regimens must capture both NDC on pharmacy claims and J-codes on medical claims, or they will systematically undercount the infused arm. The CMS ASP (Average Sales Price) drug crosswalk links NDCs to HCPCS codes for drugs with both forms.

Package-level granularity as a double-edged sword: Package-level resolution is an asset for drug-safety signal work (lot-specific pharmacovigilance, repackager-specific contamination events) and for reimbursement accuracy (the exact package billed). It is a liability for exposure ascertainment because a single repackager entering or leaving the market silently changes which NDCs a code list must include. A drug code list review should be triggered whenever a major generic launch, patent expiration, or biosimilar approval occurs.

When to use

Use NDC — in its HIPAA 11-digit form — as the primary filter whenever you are building drug exposure from US pharmacy claims (commercial, Medicare Part D, Medicaid). NDC is the correct grain for computing days_supply-based coverage windows (PDC, MPR), identifying index fills for new-user designs, measuring adherence, characterizing drug utilization volumes, and for constructing any episode-based algorithm that requires knowing exactly which drug was dispensed on which date. NDC is also the correct identifier for linking pharmacy claims to drug-specific attributes: pill strength (from the NDC Directory's product record), route, dosage form, and the labeler responsible for the marketed product.

Use NDC when working with the FDA NDC Directory to look up product characteristics, when using the CMS NDC-to-HCPCS crosswalk to bridge pharmacy and medical claims, when submitting or processing NCPDP pharmacy transactions, and when writing medication-related data quality rules that must detect implausible drug-days combinations. NDC is also the source vocabulary in OMOP CDM: the Drug Exposure table stores NDC as the source_concept and maps it to an RxNorm standard concept via the OMOP vocabulary tables.

When NOT to use — and when it is actively misleading or dangerous

Do not join two NDC columns from different systems (claims vs. EHR vs. a drug dictionary) without first normalizing both to the same format — either both hyphenated 10-digit or both stripped 11-digit. Mixing formats silently produces false non-matches: a claim's `00002322730` will not join to a dictionary's `0002-3227-30` even though they denote the same physical package. This is the most common single source of "missing drugs" in a pharmacoepidemiology analysis and is entirely preventable.

Do not query the FDA NDC Directory as a definitive source of historical drug availability. The Directory lists only currently marketed, finished drug products: when a manufacturer discontinues a product, its NDC is eventually removed from the active directory. Historical claims for discontinued drugs therefore contain NDCs that resolve to nothing in the current directory. For longitudinal claims studies covering more than a few years, supplement the FDA directory with a historical NDC reference such as the CMS NDC file or a commercial drug compendium that maintains discontinued-product records.

Do not assume that an NDC found on a claim represents an FDA-approved product. Compounded preparations — which by definition are not FDA-approved finished drug products — may carry NDC-like 11-digit codes assigned by the compounding pharmacy or its software, but these codes are not registered in the FDA NDC Directory and carry no product-level attributes. Studies involving compounded drugs (common in oncology, parenteral nutrition, and some specialty areas) must explicitly identify and handle these codes separately.

Do not treat NDC labeler code reuse as a permanent link to one manufacturer. The FDA can reassign a labeler code if the original holder surrenders it; the same 5-digit labeler prefix may refer to different firms at different points in time. In long longitudinal datasets, apparent "same labeler" fills separated by many years may actually represent different manufacturers. Cross-check against the labeler name in the NDC Directory when labeler identity matters (e.g., biosimilar substitution studies).

Do not rely on NDC alone to identify drug classes or therapeutic intent. NDC encodes one specific product from one specific labeler; it does not carry therapeutic class, indication, or ATC code natively. Therapeutic grouping requires mapping NDC to a drug dictionary (RxNorm, First Databank, Medi-Span, Red Book) that assigns pharmacological class. An NDC-only list built without this mapping will include every repackager but may fail to exclude a chemically related but therapeutically distinct compound that shares some NDC prefix characters.

Data-source operational depth

Pharmacy claims (NCPDP): The canonical home of NDC. Every NCPDP transaction carries a mandatory 11-digit NDC field (5-4-2 format, no hyphens). When you receive claims, the NDC field should already be in HIPAA format, but vendors and clearinghouses occasionally strip leading zeros, convert to integer (destroying leading zeros), or pad incorrectly. Before any analysis, run a data quality check: confirm the NDC field is character-typed and exactly 11 characters for every pharmacy record. A left-padded zero-fill to width 11 is appropriate for character NDCs shorter than 11; an 11-character check plus Luhn-like format validation catches truncated or malformed codes. Medicare Part D PDE (Prescription Drug Event) files carry NDC in the same HIPAA 11-digit field; the CMS NDC file distributed with PDE data provides historical drug characteristics including strength, route, and a link to the brand name.

EHR medication records: NDC may appear in dispense records (medication dispensed from an in-system pharmacy) or in medication order records when the ordering system is linked to a drug formulary. Inpatient administration records frequently lack NDC because the administered drug is a floor-stock item billed as a supply rather than a filled prescription. When EHR NDC is present, it may be in either 10-digit or 11-digit format depending on the formulary system vendor; normalize before joining to claims or to external drug dictionaries.

Linked claims–EHR: When linking pharmacy claims to EHR medication orders to confirm initiation (primary non-adherence detection), use 11-digit normalized NDC as the primary join key but supplement with RxNorm concept mapping as a fallback, because orders are more likely to carry RxNorm than NDC. Reconcile fill date with order date; a claim fill that precedes the order date by more than a day signals a data linkage error or a pre-authorization fill.

Medical claims for physician-administered drugs: NDC may appear as a line-level detail on CMS-1500 (professional) and UB-04 (institutional) claims for drug lines billed under a HCPCS J-code or revenue code. CMS began requiring NDC on drug lines for Medicaid claims under the Medicaid Drug Rebate Program; commercial payers and Medicare Advantage may or may not carry it consistently. When present, the medical-claim NDC is essential for linking the infused dose to a specific product and calculating utilization for drugs with both oral and infused formulations.

Relationship to the FDA NDC Directory and openFDA

The FDA NDC Directory (https://www.fda.gov/drugs/drug-approvals-and-databases/national-drug-code-directory) is the authoritative public registry of finished drug product NDCs for currently marketed products. It is updated regularly and is downloadable as a tab-delimited or JSON file, providing product attributes including labeler name, proprietary and non-proprietary names, dosage form, route of administration, strength, and marketing start/end dates. The openFDA Drug NDC API (https://open.fda.gov/apis/drug/ndc/) provides RESTful query access to the same data and supports validation of individual 11-digit codes in automated pipelines. Neither source reliably retains discontinued products: use CMS historical NDC files or commercial compendia for longitudinal studies.

Regulatory and reporting context

The HIPAA transaction standards (specifically the NCPDP SCRIPT and D.0 standard) mandate the 11-digit 5-4-2 format as the NDC representation in electronic pharmacy transactions. Medicaid requires NDC on medical claims for covered outpatient drugs to support drug-rebate invoicing, creating an additional incentive for completeness on medical claims drug lines. FDA adverse event reports (FAERS) also use NDC as a primary drug identifier, making it the linking key for pharmacovigilance signal detection studies that join claims exposure to FAERS outcomes.

Worked example

Scenario

A pharmacoepidemiology analyst is building a new-user cohort of adults who started lisinopril — a common blood pressure medication — in 2023 using pharmacy claims. Before pulling any data, the analyst receives a reference file from a drug dictionary vendor listing NDC codes for lisinopril. Three of the NDCs are in 10-digit hyphenated FDA format, one from each of the three possible segment layouts. The analyst must convert each one to the standard 11-digit HIPAA format (no hyphens) that the claims database actually stores. The table below shows the conversion for each format pattern, digit by digit.

Dataset

Three real FDA NDC formats for lisinopril and similar oral tablets, showing the 10-to-11 digit conversion for each segment pattern. Source: FDA NDC Directory public data.

FDA 10-digit (hyphenated)Segment patternSegment that needs a zeroHIPAA 11-digit (no hyphens)
0002-3227-304-4-2 (labeler is 4 digits)Labeler: pad 0002 → 0000200002322730
00093-1065-015-3-2 (product is 3 digits)Product: pad 1065 → 1065 already 4 — wait, this is 5-4-2 already; use 00093-490-05 insteadsee steps
00071-0155-235-4-1 (package is 1 digit)Package: pad 23 → already 2 digits — see steps for correct 5-4-1 examplesee steps

Steps

  • Start with the 4-4-2 example: the raw FDA NDC is 0002-3227-30. Count the digits in each segment separated by hyphens: labeler has 4 digits (0002), product has 4 digits (3227), package has 2 digits (30). The HIPAA standard requires 5-4-2. The labeler is one digit short, so insert one leading zero there: 0002 becomes 00002. The product and package stay the same. Drop all hyphens: 00002-3227-30 becomes 00002322730. Final 11-digit result: 00002322730.

  • Now the 5-3-2 example: the raw FDA NDC is 00093-490-05. Count digits: labeler has 5 (00093), product has 3 (490), package has 2 (05). The product is one digit short of the required 4. Insert one leading zero into the product segment: 490 becomes 0490. Labeler and package unchanged. Drop hyphens: 00093-0490-05 becomes 000930490005. Final 11-digit result: 000930490005.

  • Now the 5-4-1 example: the raw FDA NDC is 00071-0155-2. Count digits: labeler has 5 (00071), product has 4 (0155), package has 1 (2). The package is one digit short of the required 2. Insert one leading zero into the package segment: 2 becomes 02. Labeler and product unchanged. Drop hyphens: 00071-0155-02 becomes 000710155002. Final 11-digit result: 000710155002.

  • Why does the zero position matter? If you naively add a zero to the front of the entire 10-digit number for the 5-3-2 case, you would get 000093490-05 (garbled) or 000093049005 (wrong product segment). The resulting 11-digit code may match a completely different real drug in the FDA directory — creating silent exposure misclassification, where your analysis records a fill of the wrong drug.

  • A validation step: after converting all NDCs in your code list to 11-digit format, look up each one in the FDA NDC Directory or the openFDA API. Any NDC that returns no match is either corrupted during conversion or refers to a discontinued product absent from the current directory. Discontinued-product NDCs are still valid for historical claims; add them to a supplemental list sourced from the CMS historical NDC file.

Result

The three 10-digit FDA formats convert to HIPAA 11-digit as follows: 0002-3227-30 (4-4-2) → 00002322730 (zero inserted into labeler); 00093-490-05 (5-3-2) → 000930490005 (zero inserted into product); 00071-0155-2 (5-4-1) → 000710155002 (zero inserted into package). Naively prepending a zero to the whole string gives the right answer only for 4-4-2 codes and silently corrupts 5-3-2 and 5-4-1 codes. Always detect the source segment pattern from the hyphenated form before padding.

Runnable example

python implementation

10-to-11 digit NDC normalization function that detects the source segment pattern from the hyphenated FDA form and left-pads the deficient segment. Also includes a regex validator for well-formed 11-digit codes and a batch normalization helper for a pandas...

import re
import pandas as pd

# ---------------------------------------------------------------------------
# NDC segment patterns from FDA format (hyphenated) to HIPAA 11-digit (5-4-2)
# Pattern detected from segment lengths in the hyphenated source string.
# ---------------------------------------------------------------------------

_NDC_HYPHEN = re.compile(r"^(\d{4,5})-(\d{3,4})-(\d{1,2})$")
_NDC_11     = re.compile(r"^\d{11}$")

def normalize_ndc(raw: str) -> str | None:
    """Convert a raw NDC string to HIPAA 11-digit (5-4-2) format (no hyphens).

    Accepts:
      - Hyphenated 10-digit FDA forms: 4-4-2, 5-3-2, 5-4-1
      - Already-11-digit (no hyphens): returned as-is after validation
      - Integer-like strings shorter than 11: left-padded to 11 with LPAD

    Returns None for strings that cannot be normalised (e.g., blank, wrong length).

    Examples
    --------
    >>> normalize_ndc("0002-3227-30")   # 4-4-2  -> pad labeler
    '00002322730'
    >>> normalize_ndc("00093-490-05")   # 5-3-2  -> pad product
    '000930490005'
    >>> normalize_ndc("00071-0155-2")   # 5-4-1  -> pad package
    '000710155002'
    >>> normalize_ndc("00002322730")    # already 11-digit
    '00002322730'
    """
    if not isinstance(raw, str):
        return None
    s = raw.strip()
    if not s:
        return None

    # Already 11-digit no-hyphen form
    if _NDC_11.match(s):
        return s

    # Strip hyphens to integer-like form and left-pad to 11 (handles integer-cast NDCs)
    digits_only = s.replace("-", "")
    if digits_only.isdigit() and len(digits_only) <= 11:
        return digits_only.zfill(11)

    # Hyphenated FDA form: detect segment pattern and pad the short segment
    m = _NDC_HYPHEN.match(s)
    if not m:
        return None
    labeler, product, package = m.group(1), m.group(2), m.group(3)
    l_len, p_len, k_len = len(labeler), len(product), len(package)

    if l_len == 4:          # 4-4-2: pad labeler
        labeler = labeler.zfill(5)
    elif p_len == 3:        # 5-3-2: pad product
        product = product.zfill(4)
    elif k_len == 1:        # 5-4-1: pad package
        package = package.zfill(2)
    else:
        # Already 5-4-2 in hyphenated form (edge case from some vendors)
        pass

    result = labeler + product + package
    if len(result) != 11:
        return None  # still malformed
    return result


def is_valid_ndc11(code: str) -> bool:
    """Return True if code is a well-formed 11-digit NDC (all digits, exactly 11 chars)."""
    return bool(_NDC_11.match(str(code).strip()))


def normalize_ndc_series(s: pd.Series) -> pd.Series:
    """Vectorised NDC normalisation for a pandas Series of raw NDC strings.

    Returns a Series of normalised 11-digit strings (None for invalid entries).
    Missing values (NaN) pass through as None.

    Usage
    -----
    claims["ndc11"] = normalize_ndc_series(claims["ndc_raw"])
    bad = claims[claims["ndc11"].isna() & claims["ndc_raw"].notna()]
    print(f"{len(bad):,} rows with un-normalisable NDCs")
    """
    return s.apply(lambda v: normalize_ndc(v) if pd.notna(v) else None)


# ---------------------------------------------------------------------------
# Quick self-test (run as script: python -c "import <module>; test_ndc()")
# ---------------------------------------------------------------------------
def _test():
    cases = {
        "0002-3227-30":  "00002322730",  # 4-4-2 -> pad labeler
        "00093-490-05":  "000930490005", # 5-3-2 -> pad product
        "00071-0155-2":  "000710155002", # 5-4-1 -> pad package
        "00002322730":   "00002322730",  # already 11
        "2322730":       "00002322730",  # integer-cast, lpad to 11
    }
    for raw, expected in cases.items():
        result = normalize_ndc(raw)
        status = "OK" if result == expected else f"FAIL (got {result})"
        print(f"  {raw!r:22} -> {result!r:14} {status}")

if __name__ == "__main__":
    _test()
r implementation

10-to-11 digit NDC normalization in R, with the same segment-pattern detection logic. Includes a vectorised wrapper suitable for dplyr mutate() calls and a validation predicate.

library(stringr)

# ---------------------------------------------------------------------------
# NDC normalisation: FDA 10-digit (any of three segment patterns) -> HIPAA 11-digit
# ---------------------------------------------------------------------------

normalize_ndc <- function(raw) {
  # Vectorised over a character vector.
  # Returns NA for entries that cannot be normalised.
  vapply(raw, .normalize_one, character(1), USE.NAMES = FALSE)
}

.normalize_one <- function(s) {
  if (is.na(s) || !nchar(trimws(s))) return(NA_character_)
  s <- trimws(s)

  # Already 11-digit no-hyphen form
  if (grepl("^\\d{11}$", s)) return(s)

  # Integer-like (<=11 digits, no hyphens): left-pad to 11
  digits_only <- gsub("-", "", s)
  if (grepl("^\\d{1,11}$", digits_only)) {
    return(str_pad(digits_only, 11, side = "left", pad = "0"))
  }

  # Hyphenated FDA form: parse segments
  parts <- str_split(s, "-")[[1]]
  if (length(parts) != 3) return(NA_character_)

  labeler <- parts[1]; product <- parts[2]; package <- parts[3]
  l_len <- nchar(labeler); p_len <- nchar(product); k_len <- nchar(package)

  if (!all(grepl("^\\d+$", parts))) return(NA_character_)

  if (l_len == 4) {                          # 4-4-2: pad labeler
    labeler <- str_pad(labeler, 5, pad = "0")
  } else if (p_len == 3) {                   # 5-3-2: pad product
    product <- str_pad(product, 4, pad = "0")
  } else if (k_len == 1) {                   # 5-4-1: pad package
    package <- str_pad(package, 2, pad = "0")
  }
  # 5-4-2 hyphenated (edge case) falls through unchanged

  result <- paste0(labeler, product, package)
  if (nchar(result) != 11) return(NA_character_)
  result
}

is_valid_ndc11 <- function(code) {
  grepl("^\\d{11}$", trimws(as.character(code)))
}

# ---------------------------------------------------------------------------
# Example usage with dplyr
# ---------------------------------------------------------------------------
# library(dplyr)
# claims <- claims |>
#   mutate(ndc11 = normalize_ndc(ndc_raw))
#
# bad_ndc <- claims |> filter(is.na(ndc11) & !is.na(ndc_raw))
# message(nrow(bad_ndc), " rows with un-normalisable NDCs")
#
# Self-test
.test_ndc <- function() {
  cases <- list(
    list(raw = "0002-3227-30",  expected = "00002322730"),   # 4-4-2
    list(raw = "00093-490-05",  expected = "000930490005"),  # 5-3-2
    list(raw = "00071-0155-2",  expected = "000710155002"),  # 5-4-1
    list(raw = "00002322730",   expected = "00002322730"),   # already 11
    list(raw = "2322730",       expected = "00002322730")    # integer-cast
  )
  for (tc in cases) {
    result <- normalize_ndc(tc$raw)
    status <- if (identical(result, tc$expected)) "OK" else
                paste0("FAIL (got ", result, ")")
    cat(sprintf("  %-22s -> %-14s %s\n", tc$raw, result, status))
  }
}