One thesis: the NDC has no check digit, so no arithmetic on Earth can tell you a transposed NDC is wrong — which means "valid" is not one claim but a tier of claims, and an identifier layer that refuses to name which tier it is standing on will eventually sell you a verification it never performed.
If you own the standards-and-compliance surface of a pharma-serving product, you have had this conversation: a customer asks whether the system "validates NDCs," and the honest answer is a paragraph, not a checkbox. This post is that paragraph, made mechanical.
The grammar: ten digits, three segments, three layouts
A National Drug Code is 10 digits in three segments — labeler, product, package — in one of three layouts: 4-4-2, 5-3-2, or 5-4-1. The layout is not self-describing from the digits alone; the hyphens carry information. 0777-3105-02 (4-4-2) and a 5-3-2 code can collide as bare digit strings while naming different things. So the first validation tier is purely structural:
$ barcoding resolve 0777-3105-02 --json
{"scheme":"ndc","segments":{"labeler":"0777","product":"3105","package":"02"},
"layout":"4-4-2","canonical_11":"00777-3105-02","check":"none"}
Note "check":"none". That field is load-bearing. For a GTIN, verify recomputes a mod-10 and can say this string is internally consistent. For an NDC there is no such statement available — the scheme simply does not carry redundancy — and a resolver that prints "valid ✓" on an NDC without qualification is asserting something the identifier cannot support.
The 11-digit trap
Billing and reimbursement systems (the HIPAA transaction world) use an 11-digit NDC in a fixed 5-4-2 shape, produced by zero-padding whichever segment is short: 0777-3105-02 becomes 00777-3105-02. The padding position depends on the layout — pad a 4-digit labeler, a 3-digit product, or a 1-digit package — which is exactly the kind of rule that gets reimplemented wrong in every codebase that touches claims data. Strip the hyphens from an 11-digit form and try to reconstruct the 10-digit original without knowing the layout, and you have a genuine ambiguity. Canonicalization law here mirrors the GTIN's zero-pad-to-14 rule on /schemes/gtin: one canonical form (the 11-digit 5-4-2), with the source layout preserved as metadata rather than destroyed.
One drug, two schemes
US drug packages also carry the NDC embedded in GS1 identifiers: the historical drug UPC opens with a 3, and the GTIN-14 for a drug package wraps the NDC with packaging-level indicator digits and a GS1 check digit on the outside. Which produces the pleasing situation the registry handles as one row with two doors: the GS1 wrapper has a check digit (tier-1 verifiable arithmetic), while the NDC inside it does not. resolve on the GTIN-14 form checks the mod-10, extracts the embedded NDC, and reports both facts separately — a pass on the wrapper is never silently promoted into a pass on the contents.
Tier three: the registry
Since arithmetic runs out at structure, the strongest available claim for an NDC is registry presence — and here the rail is unusually good. openFDA's drug/ndc endpoint is updated daily and licensed CC0 (open.fda.gov/apis/drug/ndc). enrich on a canonicalized NDC answers from it, and every attribute travels in the provenance envelope:
{
"attribute": "genericName",
"value": "…",
"source": { "id": "openfda-drug-ndc", "dataset_version": "daily 2026-07-31",
"record": "https://api.fda.gov/drug/ndc.json?search=product_ndc:…",
"digest": "sha256:…" },
"license": "public-domain",
"fetched_at": "2026-07-31T18:20:11Z",
"confidence": 1.0
}
One honest limit, carried on the row rather than papered over: openFDA documents no NDC-to-UPC crosswalk field. The join from a retail OTC barcode to its NDC record has to come from evidence, not from a lookup that does not exist — so where the API offers that join it is a candidate with confidence and cited evidence, never a silent equivalence.
The validation ladder, stated as the API states it
The NDC forces the vocabulary the whole registry uses:
- Parseable — the string matches a layout of the scheme's grammar. (
resolve, exit 0) - Checksum-consistent — internal redundancy recomputes. Not available for NDC, and the output says
"check":"none"instead of faking a pass. - Registry-present — a licensed source currently lists this identifier. (
enrich, with the source and its date in the envelope) - Legitimately issued — the prefix or labeler code belongs to a real registrant. (pinned labeler tables; a different claim from #3, because registries lag)
Every scheme sits somewhere on this ladder. A GTIN reaches tier 2 offline; a VIN reaches tier 2 with a stronger algorithm; the NDC skips tier 2 entirely and is honest about it. For your RFP checkbox, that ladder is the demonstrable answer: not "we validate NDCs" but "structural validation locally; registry validation against openFDA's daily CC0 dataset, with the dataset version cited in every response" — a sentence an auditor can check, which is the only kind worth shipping. The serialized world one layer up — where DSCSA puts a GS1 DataMatrix with GTIN, serial, lot and expiry on every prescription package and the wrapper arithmetic does protect the payload — is its own post in this arc.
Run tier 1 yourself right now: npx barcoding.dev resolve any NDC off a package in the building — hyphenated, bare, or 11-digit — and read what the output claims and, more importantly, what it declines to claim. The scheme page is /schemes/ndc. For the hosted registry-tier enrichment rail in your product, tell us who you are.
Keys open the network half of the verb set. Get an API key — say what arrives at your door, and the provisioning order follows the list.
We answer in writing. We take at most five conversations a month, only when you ask for one, and only after you already have the written read.