FDA NDC Verifier avatar

FDA NDC Verifier

Pricing

from $2.16 / 1,000 row judgeds

Go to Apify Store
FDA NDC Verifier

FDA NDC Verifier

Verify NDC codes against the FDA NDC Directory: each code gets a verdict (listed, expired, package unlisted or not found) plus the listing’s labeler, brand name, generic name, dosage form and active ingredients.

Pricing

from $2.16 / 1,000 row judgeds

Rating

0.0

(0)

Developer

Pradio Actors

Pradio Actors

Maintained by Community

Actor stats

0

Bookmarked

2

Total users

1

Monthly active users

a day ago

Last modified

Share

What does FDA NDC Verifier do?

FDA NDC Verifier checks National Drug Codes against the FDA NDC Directory. Each code comes back as one row: the verdict, plus the brand name, labeler, dosage form and package description the Directory holds. Paste a list of codes, or give it pages to pull codes from, and press Start. A code that matches nothing costs nothing, and its row still tells you why.

In the last measured run over 95 codes drawn from labeler catalogs and claims-style lists, never used to tune it, every code came back with a verdict. 5 were the same package in another layout, judged and billed once, leaving 90 judged: 56 resolved to a live listing, 10 to a listed product whose package segment was not listed, and 24 found no listing; each row says which and why. Each judged row costs $0.004, and only judged rows are charged.

Who uses FDA NDC Verifier

BuyerWhat they run it for
A developer who needs an FDA NDC APIPaste the list and get a dataset of verdicts, instead of writing the openFDA lookup loop
A pharmacy, claims or catalog teamConfirm every NDC on a list still resolves to a listed product before it is billed or published
A data team maintaining a product tableFill in brand name, generic name, labeler, dosage form and ingredients for each code on file
Anyone with pages rather than listsPoint urls at supplier or labeler pages and verify every NDC-shaped code found on them

Features

  • Every NDC layout accepted. Dashed 5-3-2, 4-4-2 and 5-4-2 forms and the 11-digit padded form all expand to the layouts labelers list under, and the form as written is tried first.
  • Package-level checks. A code with a package segment is matched against the listing's own package codes, so an unlisted package is told apart from a wrong product.
  • Codes straight off a page. Each entry in urls is fetched and mined for NDC-shaped codes, up to 50 per page.
  • Expiry-aware verdicts. A listing whose expiration date has passed comes back expired, with active set to false.
  • De-duplicated before billing. A repeated code is judged once and never billed twice.
  • No login, no browser. Plain requests to the public openFDA endpoint, paced well under the rate it allows. An optional free openFDA API key (apiKey) raises the daily request quota from 1,000 to 120,000.
  • A cap you set. maxItems stops the run where you say.

What you can count on

  • You pay only for a row the Directory judged. A miss still lands in your dataset as an uncharged ITEM_STATUS row that says why: a code that is not NDC-shaped, a code with no listing, or a lookup the Directory refused or could not answer.
  • Every row is charged only after it is written to your dataset. A row you cannot see is never billed.
  • A run that finds nothing returns one NOTHING_TO_JUDGE row that says so, never an empty dataset.
  • A spending limit stops the run cleanly. One final row says how many rows were returned and how many were not.
  • Every run writes a summary with rowsFetched, rowsPushed, rowsCharged and duplicatesDropped, so a short run and a broken one are easy to tell apart.
  • If the Directory changes its answers, the run fails with the error in the log. It never returns rows full of nulls and calls it success.
  • No value is invented. A field the Directory does not show is null, and this page says which fields those are. Names and descriptions come back verbatim: a labeler's own filing, typo included, is what the Directory holds.

What data does FDA NDC Verifier return?

One row per code you give it. This is a real row from a run:

{
"ndc": "76204-027-01",
"source_url": null,
"status": "ok",
"reason": "package listing 76204-027-01 is in the FDA NDC Directory, listed by Ritedose Pharmaceuticals, LLC (listing expires 20271231)",
"error": null,
"found": true,
"active": true,
"product_ndc": "76204-027",
"brand_name": "Albuterol Sulfate Inhalation Solution, 0.5%",
"generic_name": "Albuterol Sulfate",
"labeler_name": "Ritedose Pharmaceuticals, LLC",
"dosage_form": "SOLUTION",
"product_type": "HUMAN PRESCRIPTION DRUG",
"marketing_category": "ANDA",
"listing_expiration_date": "20271231",
"is_finished_dosage_form": true,
"active_ingredients": [
"ALBUTEROL SULFATE 2.5 mg/.5mL"
],
"package_description": "30 POUCH in 1 CARTON (76204-027-01) / 1 VIAL in 1 POUCH (76204-027-11) / .5 mL in 1 VIAL",
"row_type": "ROW"
}

status is the verdict on the code:

statusWhat it meansCharged?
okListed, and the listing has not expiredyes
expiredListed, but its listing_expiration_date has passedyes
package_unlistedThe product is listed, but no listed package matches the code's package segmentyes
not_foundNo listing matches the code, or the input was not NDC-shapedno
bad_urlA urls page could not be readno
fetch_failedThe Directory could not be read for this codeno

Every field a row carries:

FieldWhat it holds
ndcThe code exactly as you supplied it. Rows are de-duplicated on the code itself, in any layout: the same package pasted two ways is judged and billed once. Null on a row about a urls page that could not be read; source_url names that page.
source_urlThe urls page the code was read from. Null on pasted codes.
statusThe verdict, from the table above.
reasonOne sentence naming what matched and who listed it, or why nothing did.
errorThe transport message on a bad_url or fetch_failed row. Otherwise null.
foundTrue when a listing exists in the Directory for this code.
activeTrue when the listing is found and its expiration date has not passed; false marks an expired or unlisted code.
product_ndcThe product code of the matched listing, as labeler and product segments.
brand_nameThe listing's brand name.
generic_nameThe listing's generic name.
labeler_nameThe firm that listed the product.
dosage_formThe listing's dosage form, such as TABLET or SOLUTION.
product_typeThe listing's product type, such as HUMAN OTC DRUG or HUMAN PRESCRIPTION DRUG.
marketing_categoryThe listing's marketing category, such as OTC MONOGRAPH DRUG or ANDA.
listing_expiration_dateThe listing's expiry date, as the Directory carries it (YYYYMMDD).
is_finished_dosage_formTrue when the listing is a finished dosage form rather than a bulk ingredient.
active_ingredientsEach active ingredient with its strength, such as "ACETAMINOPHEN 500 mg/1".
package_descriptionThe package description: the matched package's own text, else the first one listed.
row_typeROW on a data row, ITEM_STATUS on an uncharged miss, NOTHING_TO_JUDGE or STOPPED_EARLY on a status row.
rowsFetchedStatus rows only: how many codes the run collected before de-duplication and the cap.
rowsReturnedStatus rows only: how many data rows landed in the dataset.
rowsRemainingStatus rows only: how many collected codes were not returned.

Status rows also carry reason, rowsFetched, rowsReturned and rowsRemaining, so an empty or cut-short run explains itself in the dataset.

In the last measured run over 95 codes drawn from labeler catalogs and claims-style lists, never used to tune it, every code came back with a verdict. 5 were the same package in another layout and were judged once, so 90 codes were judged: 56 resolved to a live listing, 10 named a listed product whose package segment was not listed, and 24 found no listing at all. The list includes withdrawn-market and malformed codes, so misses are part of the measurement. On the 56 resolved rows every field filled except listing_expiration_date (55 of 56). Two fields stay null on most rows by design: source_url fills only when a code was read off a urls page, and error only when a miss carries a transport message. The dataset's Overview view leaves them out; the All fields view and every export carry them, so all rows keep the same shape.

Pricing

Two events are charged, both visible on the Actor's pricing tab:

  • $0.004 per judged row (row-judged), billed only after the row is written to your dataset. A miss is free. Rows whose status is not_found, bad_url or fetch_failed are pushed so you see the verdict, and are never billed. Status rows and dropped duplicates are never billed either. Higher-volume tiers pay less per row; the pricing tab lists each one.
  • $0.00005 per run start (apify-actor-start), Apify's platform charge. This Actor's memory ceiling is 256 MB, so it is billed exactly once per run.

The most a list can cost. The table prices every code judged, and a code judged a miss is never billed:

Codes checkedJudged rowsCostNeeds a key?
100100$0.40no
1,0001,000$4.00free apiKey
10,00010,000$40.00free apiKey

The "needs a key" column is the register's quota, not ours. Without a key, openFDA allows 1,000 requests per IP per day, and one code can cost up to four lookups while its layouts are tried. A keyless day covers roughly the first 250 codes. A free apiKey raises the cap to 120,000 requests/day and every row in the table is then a day's work. Codes that resolve to nothing stay free, so a list with misses costs less than the table shows.

How do I use FDA NDC Verifier?

  1. Open the Actor and press Start.
  2. Paste your NDC codes into NDCs: dashed forms like 67414-449-10, or the 11-digit form like 67414044910.
  3. Optionally add page addresses to Urls and set a cap under Maximum items.
  4. For lists past the register's keyless daily quota, paste a free openFDA key under openFDA API key.
  5. Press Start. Judged rows land in the run's dataset as they are verified, and a RUN_SUMMARY entry lands in the run's key-value store.

Example input:

{
"ndcs": [
"67414-449-10",
"76204002701",
"67414-449",
"0000-0000-00"
],
"urls": [],
"maxItems": 100
}

The last code resolves to nothing; its row is pushed free so you still see the verdict.

Through the Apify API:

$curl -X POST "https://api.apify.com/v2/acts/Pradio~fda-ndc/run-sync-get-dataset-items?token=YOUR_APIFY_TOKEN" -H "Content-Type: application/json" -d '{"ndcs": ["67414-449-10"], "urls": []}'

Input

FieldTypeDefaultWhat it does
ndcsarray7 example codesThe NDC codes to verify. Required.
urlsarray[]Pages to fetch and mine for NDC-shaped codes. Optional; an empty list is fine.
maxItemsinteger100The most rows one run judges.
maxItemsPerUrlinteger50The most codes taken from any one urls page, so one dense page cannot starve the rest.
apiKeystringnoneA free openFDA API key, sent as api_key on every request to the Directory. Raises the daily quota from 1,000 to 120,000 requests.

ndcs

The identifier list, in any NDC layout the Directory uses: dashed 5-3-2, 4-4-2 and 5-4-2 forms, a two-segment product code like 67414-449, or the 11-digit padded form like 67414044910. Each code expands to the layouts labelers list under, so a code written one way still finds a listing stored another way. Anything that is not NDC-shaped comes back not_found, free, with the reason on the row.

urls

Each address is fetched once and scanned for NDC-shaped codes, up to maxItemsPerUrl (default 50) codes per page. Every code found is verified like a pasted one, and the page's URL lands in the row's source_url field. A page that cannot be read yields one bad_url row with the transport message in error. A page with no NDC-shaped codes yields one not_found row carrying the address.

apiKey

Optional. openFDA serves the Directory without a key at 1,000 requests per IP per day. One judged code can cost up to four lookups while its layouts are tried, so a keyless day covers roughly the first 250 codes. A free key from openFDA's authentication page raises the cap to 120,000 requests a day. It is sent as api_key on every register request, never on a urls page fetch.

maxItems

The cap on judged rows, default 100. Collecting codes and judging them both stop at the cap, and the run's log notes it. Raise it for longer lists.

Output

A finished run's dataset holds one judged row per code. A billed verdict carries row_type: "ROW"; a free miss carries row_type: "ITEM_STATUS". Three other row types explain a run that did not simply fill:

  • NOTHING_TO_JUDGE is one status row, never charged, pushed when the run had nothing to check: both input lists empty, or every code a duplicate. It carries rowsFetched, rowsReturned and rowsRemaining.
  • STOPPED_EARLY is one status row, never charged, pushed when your charge limit ended the run. It says how many rows were returned and how many were not, so finishing and being cut short never produce the same dataset.
  • RUN_SUMMARY is not a dataset row but an entry in the run's key-value store: rowsFetched, rowsPushed, rowsCharged, duplicatesDropped, stoppedEarly, and the run's start and finish times.

A zero result means what it says: the run answered and there was nothing to return. You get one explained row, not an empty dataset and not a silent crash.

What can you do with the data?

Clean a code list before it is used. A pharmacy or claims team runs its NDC file before submission week. Rows where active is true go through, expired and package_unlisted rows get a second look, and every miss already says why.

Check the codes a page publishes. Point urls at a supplier or labeler catalog page and get a verdict per NDC-shaped code on it. The page's URL sits on each row's source_url, so codes from several pages stay sorted apart.

Fill in a product table. A data team holding a spreadsheet of codes gets brand name, generic name, labeler, dosage form, marketing category and ingredients for each one, in the shape the Directory publishes them.

Catch listings before they lapse. Re-run a saved list on a schedule. listing_expiration_date sits on every found row, and a listing whose date has passed comes back expired, so nothing slips quietly.

Use FDA NDC Verifier with AI agents

$claude mcp add --transport http apify "https://mcp.apify.com?tools=Pradio/fda-ndc"

Paste that line to give an MCP-aware agent this Actor. It can then run verifications and read the dataset itself.

Personal data

This Actor reads the public FDA NDC Directory on fda.gov and verifies identifier rows. Its input is a list of NDC codes to check, not a list of people. No output field carries a private individual's name, email, phone or address; the labeler_name on a row is the firm that listed the product. The run collects and emits no personal data.

Limits

  • It verifies codes you supply. There is no search by brand, ingredient or labeler name; the lookup key is the NDC itself.
  • Only NDC-shaped input is judged. Anything else comes back not_found, free, with the reason on the row.
  • At most 50 codes are taken from one urls page, and pages are fetched as plain requests. A page that only builds its content inside a browser yields no codes.
  • The row keeps the listing facts in the table above. The Directory's entry carries more keys this Actor does not return, such as the full packaging list, the pharmacologic class, the application number and the SPL id.
  • source_url and error are empty on most rows: source_url fills only for codes read off a urls page, and error only on a miss with a transport message.
  • Errors run toward telling you. If the Directory cannot be read at all, the run fails with the error in the log rather than writing a dataset of misses.
  • The default cap is 100 judged rows per run. Raise maxItems for bigger lists.
  • openFDA without a key allows 1,000 requests per IP per day; a free apiKey raises that to 120,000/day. One code can cost up to four lookups while its layouts are tried, so a keyless run covers roughly the first 250 codes a day.

Troubleshooting

I pasted 50 codes but only 47 rows came back. Rows are de-duplicated on the code itself before anything is pushed, so the same code in another layout counts once, and maxItems caps the run. The RUN_SUMMARY entry in the run's key-value store carries rowsFetched, rowsPushed and duplicatesDropped, which adds the count up.

My dataset has a single row whose row_type is NOTHING_TO_JUDGE. The run had nothing to check: both input lists were empty, or every code was a duplicate. That row is the answer. It is not charged, and it is not a crash.

The run failed with "register unreachable" instead of returning rows. Every lookup failed, so the run stopped rather than writing a dataset of fetch_failed misses. The first error is in the run's log; the Directory or the network was not answering.

A code I know is real came back package_unlisted. The product is listed, but the package segment of your code matches none of the package codes the labeler publishes for it. The reason names the listed packages. Try the two-segment product form to confirm the product itself resolves.

Something else look wrong? Report a problem on the Actor's Issues tab and include the run's RUN_SUMMARY. It carries the counts that tell a short run from a broken one.

FAQ

Can I use integrations with FDA NDC Verifier? Yes. It is a normal Apify Actor, so its dataset works with Apify's integrations, including Zapier, Make, n8n and Google Sheets, and runs can be started on a schedule or by webhook.

Can I use FDA NDC Verifier with the Apify API? Yes. Start a run through the actor runs endpoint and read the rows through the dataset API. The curl line under "How do I use FDA NDC Verifier?" returns the dataset items directly.

Can I use FDA NDC Verifier through an MCP server? Yes. The Apify MCP server exposes it as a tool. The one-line setup is under "Use FDA NDC Verifier with AI agents".

Is it legal to scrape the FDA NDC Directory? The Actor reads the FDA's public openFDA API, which publishes the NDC Directory for public use. It sends a small number of lookups per code, paced well under the rate the API allows, and it collects only directory facts, no personal data. What you do with verified rows is your responsibility.

Release notes

  • 0.1.18 (2026-09-14): first public listing. NDC codes judged against the FDA NDC Directory over the public openFDA API, one verdict row per code.

Not affiliated

FDA NDC Verifier is an unofficial tool. It is not affiliated with, endorsed or sponsored by the U.S. Food and Drug Administration or the openFDA project, and it is not an official FDA service. It reads the public FDA NDC Directory over the public openFDA API.

More from this publisher: Imprint Contact Scraper finds the legal-notice page on a company website and reads its contact details.