# FDA NDC Verifier (`pradio/fda-ndc`) Actor

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.

- **URL**: https://apify.com/pradio/fda-ndc.md
- **Developed by:** [Pradio Actors](https://apify.com/pradio) (community)
- **Categories:** Developer tools, Automation, Business
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $2.16 / 1,000 row judgeds

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.
Since this Actor supports Apify Store discounts, the price gets lower the higher subscription plan you have.

Learn more: https://docs.apify.com/actors/running/actors-in-store.md#pay-per-event

## What's an Apify Actor?

Actors are web data automations that power AI and operations. They run on the Apify platform to scrape websites, process data, connect APIs, and automate workflows.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.
Actors are written with capital "A".

## How to integrate an Actor?

If asked about integration, you help developers integrate Actors into their projects.
You adapt to their stack and deliver integrations that are safe, well-documented, and production-ready.
The best way to integrate Actors is as follows.

- **AI agents and MCP clients** — the [Apify MCP server](https://docs.apify.com/integrations/mcp.md) at `https://mcp.apify.com` (remote, streamable HTTP, OAuth on first use).
- **Agentic workflows and local Actor development** — [Agent Skills](https://apify.com/.well-known/agent-skills/index.json) with the [Apify CLI](https://docs.apify.com/cli/docs.md): `npm install -g apify-cli`, then `apify login`.
- **JavaScript/TypeScript projects** — the official [JS/TS client](https://docs.apify.com/api/client/js/docs.md): `npm install apify-client`.
- **Python projects** — the official [Python client](https://docs.apify.com/api/client/python/docs.md): `pip install apify-client`.
- **Any other language** — the [REST API](https://docs.apify.com/api/v2.md).

For usage examples, see the [API](#api) section below.

For more details, see Apify documentation as [Markdown index](https://docs.apify.com/llms.txt) and [Markdown full-text](https://docs.apify.com/llms-full.txt).

# README

## FDA NDC Verifier

### 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 `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:

```json
{
  "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 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 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?

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:

```json
{
  "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:

```bash
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_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

```bash
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](https://apify.com/pradio/imprint-contact) finds the legal-notice page on a company website and reads its contact details.

# Actor input Schema

## `ndcs` (type: `array`):

The NDC codes to verify: dashed (67414-449-10, 67414-449) or the 11-digit padded form. Each code produces one verdict row.

## `urls` (type: `array`):

Optional: page or endpoint URLs to mine for NDC codes. Every code-shaped token found on a fetched page is verified the same way.

## `maxItems` (type: `integer`):

The most codes one run judges. The run stops there and says how many were not returned.

## `maxItemsPerUrl` (type: `integer`):

The most codes taken from any one page in urls, so one dense page cannot starve the URLs behind it.

## `apiKey` (type: `string`):

Optional: a free openFDA API key, sent as api\_key on every request to the NDC Directory. Without one, openFDA allows 1,000 requests per IP per day; a free key raises that to 120,000/day. Get one at https://open.fda.gov/apis/authentication/

## Actor input object example

```json
{
  "ndcs": [
    "67414-449-10",
    "67414-402-00",
    "76204-027-01",
    "76420-136-30",
    "67414-449",
    "67414044910",
    "0000-0000-00"
  ],
  "urls": [],
  "maxItems": 100,
  "maxItemsPerUrl": 50
}
```

# Actor output Schema

## `rows` (type: `string`):

The verdict rows for this run, one per code.

## `summary` (type: `string`):

Counts for this run: fetched, pushed, charged, duplicates dropped, stopped early.

# API

You can run this Actor programmatically using our API. Below are code examples in JavaScript, Python, and CLI, as well as the OpenAPI specification and MCP server setup.

## JavaScript example

```javascript
import { ApifyClient } from 'apify-client';

// Initialize the ApifyClient with your Apify API token
// Replace the '<YOUR_API_TOKEN>' with your token
const client = new ApifyClient({
    token: '<YOUR_API_TOKEN>',
});

// Prepare Actor input
const input = {};

// Run the Actor and wait for it to finish
const run = await client.actor("pradio/fda-ndc").call(input);

// Fetch and print Actor results from the run's dataset (if any)
console.log('Results from dataset');
console.log(`💾 Check your data here: https://console.apify.com/storage/datasets/${run.defaultDatasetId}`);
const { items } = await client.dataset(run.defaultDatasetId).listItems();
items.forEach((item) => {
    console.dir(item);
});

// 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/js/docs

```

## Python example

```python
from apify_client import ApifyClient

# Initialize the ApifyClient with your Apify API token
# Replace '<YOUR_API_TOKEN>' with your token.
client = ApifyClient("<YOUR_API_TOKEN>")

# Prepare the Actor input
run_input = {}

# Run the Actor and wait for it to finish
run = client.actor("pradio/fda-ndc").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print(f"💾 Check your data here: https://console.apify.com/storage/datasets/{run.default_dataset_id}")
for item in client.dataset(run.default_dataset_id).iterate_items():
    print(item)

# 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/python/docs/quick-start

```

## CLI example

```bash
echo '{}' |
apify call pradio/fda-ndc --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,pradio/fda-ndc"
        }
    }
}
```

The hosted server signs you in with OAuth on first connect, so no API token belongs in this config. Clients without OAuth support can send an `Authorization: Bearer <APIFY_API_TOKEN>` header instead, using a token from API & Integrations in Apify Console (https://console.apify.com/settings/integrations).

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/actors/OPg6Pkzcks2NGt1is/builds/laETF7XMaRndrfDne/openapi.json
