FDA NDC Verifier
Pricing
from $2.16 / 1,000 row judgeds
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
Maintained by CommunityActor stats
0
Bookmarked
2
Total users
1
Monthly active users
a day ago
Last modified
Categories
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
| Buyer | What they run it for |
|---|---|
| A developer who needs an FDA NDC API | Paste the list and get a dataset of verdicts, instead of writing the openFDA lookup loop |
| A pharmacy, claims or catalog team | Confirm every NDC on a list still resolves to a listed product before it is billed or published |
| A data team maintaining a product table | Fill in brand name, generic name, labeler, dosage form and ingredients for each code on file |
| Anyone with pages rather than lists | Point 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
urlsis 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, withactiveset 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.
maxItemsstops 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:
status | What it means | Charged? |
|---|---|---|
ok | Listed, and the listing has not expired | yes |
expired | Listed, but its listing_expiration_date has passed | yes |
package_unlisted | The product is listed, but no listed package matches the code's package segment | yes |
not_found | No listing matches the code, or the input was not NDC-shaped | no |
bad_url | A urls page could not be read | no |
fetch_failed | The Directory could not be read for this code | no |
Every field a row carries:
| Field | What it holds |
|---|---|
ndc | The 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_url | The urls page the code was read from. Null on pasted codes. |
status | The verdict, from the table above. |
reason | One sentence naming what matched and who listed it, or why nothing did. |
error | The transport message on a bad_url or fetch_failed row. Otherwise null. |
found | True when a listing exists in the Directory for this code. |
active | True when the listing is found and its expiration date has not passed; false marks an expired or unlisted code. |
product_ndc | The product code of the matched listing, as labeler and product segments. |
brand_name | The listing's brand name. |
generic_name | The listing's generic name. |
labeler_name | The firm that listed the product. |
dosage_form | The listing's dosage form, such as TABLET or SOLUTION. |
product_type | The listing's product type, such as HUMAN OTC DRUG or HUMAN PRESCRIPTION DRUG. |
marketing_category | The listing's marketing category, such as OTC MONOGRAPH DRUG or ANDA. |
listing_expiration_date | The listing's expiry date, as the Directory carries it (YYYYMMDD). |
is_finished_dosage_form | True when the listing is a finished dosage form rather than a bulk ingredient. |
active_ingredients | Each active ingredient with its strength, such as "ACETAMINOPHEN 500 mg/1". |
package_description | The package description: the matched package's own text, else the first one listed. |
row_type | ROW on a data row, ITEM_STATUS on an uncharged miss, NOTHING_TO_JUDGE or STOPPED_EARLY on a status row. |
rowsFetched | Status rows only: how many codes the run collected before de-duplication and the cap. |
rowsReturned | Status rows only: how many data rows landed in the dataset. |
rowsRemaining | Status 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 isnot_found,bad_urlorfetch_failedare 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 checked | Judged rows | Cost | Needs a key? |
|---|---|---|---|
| 100 | 100 | $0.40 | no |
| 1,000 | 1,000 | $4.00 | free apiKey |
| 10,000 | 10,000 | $40.00 | free 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?
- Open the Actor and press Start.
- Paste your NDC codes into NDCs: dashed forms like
67414-449-10, or the 11-digit form like67414044910. - Optionally add page addresses to Urls and set a cap under Maximum items.
- For lists past the register's keyless daily quota, paste a free openFDA key under openFDA API key.
- Press Start. Judged rows land in the run's dataset as they are verified, and a
RUN_SUMMARYentry 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
| Field | Type | Default | What it does |
|---|---|---|---|
ndcs | array | 7 example codes | The NDC codes to verify. Required. |
urls | array | [] | Pages to fetch and mine for NDC-shaped codes. Optional; an empty list is fine. |
maxItems | integer | 100 | The most rows one run judges. |
maxItemsPerUrl | integer | 50 | The most codes taken from any one urls page, so one dense page cannot starve the rest. |
apiKey | string | none | A 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_JUDGEis one status row, never charged, pushed when the run had nothing to check: both input lists empty, or every code a duplicate. It carriesrowsFetched,rowsReturnedandrowsRemaining.STOPPED_EARLYis 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_SUMMARYis 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
urlspage, 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_urlanderrorare empty on most rows:source_urlfills only for codes read off aurlspage, anderroronly 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
maxItemsfor bigger lists. - openFDA without a key allows 1,000 requests per IP per day; a free
apiKeyraises 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.