# FDA Recall Checker API — Food, Drug & Device Recalls (`smartmoney-data/openfda`) Actor

Check products (keywords, brands, UPC, NDC) against official FDA food, drug and device recalls, or get only new recalls on a schedule. Also drug labels, NDC directory, 510(k) and adverse-event counts. openFDA data, no API key needed.

- **URL**: https://apify.com/smartmoney-data/openfda.md
- **Developed by:** [SmartMoney Data](https://apify.com/smartmoney-data) (community)
- **Categories:** E-commerce, Business, AI
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $2.00 / 1,000 recall / label / ndc / 510(k) records

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.

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

## What's an Apify Actor?

An Actor is a serverless cloud program that runs on the Apify platform. It has two run modes.
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.

Apify vocabulary and the platform model are defined once, in the agent quickstart at https://apify.com/agents.md.

## 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.

Do not guess an integration path. Every one of them is in the agent quickstart at https://apify.com/agents.md: the Apify MCP server, Agent Skills with the Apify CLI, the JavaScript and Python clients, the REST API, and the account-free path for an agent with no human to sign in. It also carries the rule on stating cost before the first paid run.

For examples already wired to this Actor's own input schema, see the [API](#api) section below.

Each client library has reference documentation the quickstart does not restate: [JavaScript/TypeScript](https://docs.apify.com/api/client/js/docs.md) (`npm install apify-client`) and [Python](https://docs.apify.com/api/client/python/docs.md) (`pip install apify-client`).

# README

## FDA Recall Checker: Watchlist, Monitor & openFDA Search

Check your **product list** (keywords, brands, **UPC/GTIN**, **NDC**) against official **FDA food, drug and device recalls and enforcement reports**. You can also **monitor only the NEW recalls** since your last run. The data comes from the official **api.fda.gov** (openFDA) API, which is public domain under CC0. You don't need an API key.

It's for e‑commerce sellers (Amazon, Shopify, eBay, Walmart), private-label brands, retailers, distributors, pharmacies, med‑device resellers and compliance / QA teams who need to know whether something they sell has been recalled.

> Unofficial tool. Not affiliated with or endorsed by the U.S. FDA. See **Data license & disclaimers** below.

### What it does

| Mode | Use it for | Output |
|------|------------|--------|
| **watchlist** (default) | "Is anything on my SKU list recalled?" | One row per matching recall, with `matched_items` (which of your products matched, how, and how confident) **plus one summary row per product** (`MATCHES_FOUND` / `NO_MATCH_FOUND`) for your audit trail |
| **monitor** | Scheduled daily or weekly alerts | Only recalls **not seen in previous runs**. State is kept in a named key-value store. Pair it with an Apify schedule and a webhook, Slack or email integration |
| **search** | Research and bulk export | Filtered recalls (keyword, firm, Class I/II/III, status, state, date), or reference data: **drug labels, NDC directory, 510(k) clearances** |
| **adverseEventCounts** | Safety-signal overview for a product | **Aggregated counts only** (top reactions, outcomes, event types, reports per month). It never returns patient-level reports |

Datasets (official openFDA endpoints): `food/enforcement`, `drug/enforcement`, `device/enforcement`, `device/recall`, `drug/label`, `drug/ndc`, `device/510k`. Adverse-event counts use `drug/event`, `device/event` and `food/event`.

#### Why this Actor

- **It checks your whole product list in one run.** You can mix plain keywords, `brand:`, `upc:` and `ndc:` items, or pass objects with your own `sku`.
- **Identifier matching.** UPC/GTIN is tried with and without leading zeros (UPC‑A, EAN‑13, GTIN‑14). NDC is tried as product and package codes, including the 11-digit billing format. Each match is then **re-checked in the record text** and labelled `exact`, `strong` or `api_match`.
- **Private-label aware.** Brand search covers both the recalling firm and the product description. That catches, for example, *"Trader Joe's Vegetable Fried Rice"* recalled by a co-manufacturer.
- **Monitoring that doesn't repeat itself.** A watermark plus seen-ID state, with an overlap window for records FDA posts late.
- **Complete paging.** The Actor pages with `skip` and then follows openFDA's `search_after` cursor (`Link` header), so it goes past the 25,000-record skip ceiling. In testing it exported all 29,415 food enforcement records in one run.
- **Clean flat fields plus the untouched raw record.** Dates come out as ISO `YYYY-MM-DD`. Optional dedupe, and retries with backoff on 429/5xx.
- **Privacy by default.** Fields naming individual people (device recall `additional_info_contact`, 510(k) `contact`) are removed. Adverse events are available as aggregate counts only.
- **Fair use.** Keyless by default, throttled to 3.5 requests/s (openFDA allows 240/min), with a request budget per run.

### Input examples

**Check a product list**

```json
{
  "mode": "watchlist",
  "products": [
    "peanut butter",
    "brand:Trader Joe's",
    "upc:632687615989",
    "ndc:27241-255-01",
    {"sku": "SKU-9", "brand": "Medtronic", "keyword": "insulin pump"}
  ],
  "dateFrom": "2024-01-01"
}
```

**Weekly monitor of Class I food and drug recalls in California (schedule it)**

```json
{
  "mode": "monitor",
  "endpoints": ["food/enforcement", "drug/enforcement"],
  "classifications": ["Class I"],
  "states": ["CA"],
  "monitorStateKey": "class1-ca",
  "lookbackDays": 30
}
```

**Search 510(k) clearances or NDC listings**

```json
{ "mode": "search", "endpoints": ["device/510k", "drug/ndc"], "keywords": ["pulse oximeter"], "dateFrom": "2025-01-01" }
```

**Adverse-event counts (aggregated)**

```json
{ "mode": "adverseEventCounts", "aeEndpoint": "drug/event", "aeProducts": ["ozempic"], "aeCountBy": "reaction", "aeTopN": 25 }
```

#### Key input fields

| Field | Default | Notes |
|-------|---------|-------|
| `mode` | `watchlist` | `watchlist`, `monitor`, `search`, `adverseEventCounts` |
| `products` | — | Plain text is a keyword phrase. 12–14 digits is read as a UPC/GTIN. `NNNNN-NNNN-NN` is read as an NDC. You can also use the prefixes `brand:`, `upc:`, `ndc:`, `keyword:` |
| `endpoints` | 4 recall datasets | Add `drug/label`, `drug/ndc`, `device/510k` for reference data |
| `dateFrom` / `dateTo` | — | Filters on the dataset's main date (enforcement `report_date`, device recall `event_date_posted`, …) |
| `keywords`, `firms`, `classifications`, `statuses`, `states`, `rawSearch` | — | Filters. `rawSearch` accepts an openFDA Lucene clause |
| `maxResults` / `maxResultsPerQuery` | 200 / 100 | Row caps (the Console form is prefilled with `maxResults: 50` for a small first run) |
| `dedupe` | `true` | One row per recall. Several matching products are merged into `matched_items` |
| `includeRaw` / `trimRawLists` | `true` / `true` | Raw openFDA JSON. Very long `openfda.*` ID lists are cut to 20 items (original lengths are recorded) |
| `includePersonContacts` | `false` | Keep named individual contacts (not recommended) |
| `apiKey` | — | **Optional**, free from openFDA. Raises the daily limit from 1,000 to 120,000 requests |
| `maxApiRequests` | 900 keyless / 20,000 keyed | Request budget per run |

### Output

A **recall row** (flattened; `raw` omitted here):

```json
{
  "record_type": "recall",
  "source_endpoint": "drug/enforcement",
  "record_id": "D-0836-2026",
  "record_date": "2026-09-16",
  "classification": "Class III",
  "status": "Ongoing",
  "recalling_firm": "Ajanta Pharma USA Inc",
  "product_description": "Fluphenazine Hydrochloride Tablets, USP, 1mg, 100-count bottles, ... NDC 27241-255-01.",
  "reason_for_recall": "Failed impurities/degradation specifications ...",
  "code_info": "Lot #: DJ23254, Exp. Date 11/30/2026; DJ10205, Exp. Date 04/30/2027.",
  "distribution_pattern": "Nationwide within the U.S",
  "recall_initiation_date": "2026-08-25",
  "product_ndcs": ["27241-252", "27241-255", "27241-254", "27241-253"],
  "upcs": ["0327241255014", "..."],
  "api_url": "https://api.fda.gov/drug/enforcement.json?search=recall_number:%22D-0836-2026%22",
  "matched_items": [{"item": "ndc:27241-255-01", "match_type": "ndc", "match_confidence": "exact",
                     "evidence": "NDC 27241-255-01 found in record"}],
  "license": "CC0 1.0 (public domain) per https://open.fda.gov/license/ — not FDA-endorsed; results unvalidated"
}
```

A **watchlist summary row**:

```json
{ "record_type": "watchlist_summary", "item": "peanut butter", "status": "MATCHES_FOUND",
  "match_count": 50, "total_available": 684, "truncated": true,
  "most_severe_classification": "Class I", "latest_match_date": "2026-06-10",
  "endpoints_checked": ["food/enforcement", "drug/enforcement", "device/enforcement", "device/recall"] }
```

The dataset has three views (**Recalls & records**, **Watchlist summary**, **Adverse-event counts**). The run summary (request counts, errors, dataset freshness, disclaimer) is saved as the `OUTPUT` key-value record.

### Pricing

This Actor uses pay-per-event pricing. You pay only for the events below; Apify platform usage is included.

| Event | When it's charged | Price |
|-------|-------------------|-------|
| `record` | Each recall, drug label, NDC or 510(k) row in the dataset | **$0.002** |
| `product-checked` | Each watchlist product that was checked (one summary row per product) | **$0.005** |
| `count-row` | Each aggregated adverse-event row (one per term or month, including a "no reports found" row) | **$0.001** |

Products that couldn't be checked because the run's request budget ran out (`NOT_CHECKED_BUDGET`) are not charged. The run summary in the `OUTPUT` record is free.

**Worked examples**

- Checking 500 SKUs and finding 20 matching recalls: 500 × $0.005 + 20 × $0.002 = **$2.54**. (A list this long needs your free openFDA `apiKey`: each product uses about 4 API requests, and keyless runs have a 900-request budget, enough for roughly 200 products.)
- The prefilled example (4 products against the four recall datasets, capped at 50 result rows) costs at most 50 × $0.002 + 4 × $0.005 = **$0.12**. Without the cap it would return 200 rows (the default `maxResults`), about $0.42.
- A weekly monitor of all four recall datasets typically finds about 100–300 new recalls (about 8,600 a year across the four datasets as of September 2026), which is about **$0.20–$0.60 a week**.
- Top 25 adverse-event reactions for 4 drugs: 100 × $0.001 = **$0.10**.

You can cap your spend with the maximum-cost-per-run setting in Apify Console; the Actor stops cleanly when the cap is reached.

### Rate limits & API key

These are openFDA's documented limits ([open.fda.gov/apis/authentication](https://open.fda.gov/apis/authentication/)):

- **Without a key:** 240 requests per minute and **1,000 requests per day per IP address**
- **With a free key:** 240 per minute and 120,000 per day per key

Cloud IP addresses can be shared, so if you see `429` errors, add your own free key in `apiKey`. Without a key, `count` queries with `limit=1000` are refused (the Actor caps them at 999). The Actor retries 429/5xx responses with backoff and stops cleanly when its request budget is reached, marking the run `PARTIAL_BUDGET`.

### Data license & disclaimers

- **License: CC0 1.0 public domain.** openFDA [Terms of Service](https://open.fda.gov/terms/): *"Unless otherwise noted, the content, data, documentation, code, and related materials on openFDA is public domain and made available with a Creative Commons CC0 1.0 Universal dedication. … You can copy, modify, distribute, and perform the work, even for commercial purposes, all without asking permission."* See also the [openFDA license page](https://open.fda.gov/license/).
- **No endorsement.** [License page](https://open.fda.gov/license/): *"When using or citing the work, you should not imply endorsement by the author or the affirmer."* This Actor is not affiliated with FDA.
- **Not for medical decisions.** openFDA API disclaimer: *"Do not rely on openFDA to make decisions regarding medical care. While we make every effort to ensure that data is accurate, you should assume all results are unvalidated."*
- **Recall status.** openFDA [enforcement docs](https://open.fda.gov/apis/food/enforcement/): *"FDA does not update the status of a recall after the recall has been classified according to its level of hazard. As such, the status of a recall (open, completed, or terminated) will remain unchanged after published in the Enforcement Reports."* Treat `status` as the status at publication.
- **Adverse events.** openFDA [food adverse events docs](https://open.fda.gov/apis/food/event/): *"a causal relationship cannot be established between product and reactions listed in a report."* The counts are voluntary reports and do not measure incidence.
- **No match does not mean safe.** Recall text is written by firms and FDA and isn't standardized. Identifiers may be missing or formatted differently. Use the results as a screening aid, not a certification.
- Attribution (requested by FDA, not required): *"Data provided by the U.S. Food and Drug Administration (https://open.fda.gov)."*

### Privacy

Recall and clearance records can name individual contact people. By default the Actor removes those fields (device recall `additional_info_contact`, 510(k) `contact`) from both the flat and the raw output; turn on `includePersonContacts` only if you have a legitimate need. Adverse-event data is returned as aggregate counts only, never as individual reports.

### Limitations

- **FDA only.** Covers FDA food, drug and device datasets on openFDA. CPSC, USDA FSIS and NHTSA recalls are not included.
- **Text matching is a screening aid.** Recall text isn't standardised and identifiers can be missing or formatted differently, so a keyword or brand search can return false matches and miss real ones. Check `match_confidence` and read the record.
- **Recall status is frozen at publication** (see the FDA quote above).
- **Keyless daily limit.** Without an API key, openFDA allows 1,000 requests per day per IP address, and cloud IPs can be shared. Add a free key for large or frequent runs.
- **Row caps.** `maxResults` and `maxResultsPerQuery` limit output; a watchlist summary shows `truncated: true` when more matches were available.
- **`device/recall` has no recall class**, so it is skipped when you filter by Class I/II/III.

### FAQ

**Do I need an openFDA account?** No. The Actor works without a key. A key is optional for heavy use.

**How fresh is the data?** Enforcement reports are published weekly. `OUTPUT.dataset_last_updated` shows openFDA's `last_updated` value for each dataset.

**Does it cover CPSC, USDA FSIS or NHTSA recalls?** Not yet. It covers FDA food, drug and device recalls only.

**Why was `device/recall` skipped when I filtered by class?** That dataset has no recall-class field. Use `device/enforcement` for Class I/II/III filtering.

# Actor input Schema

## `mode` (type: `string`):

watchlist = check your product list (keywords, brand:..., upc:..., ndc:...) against FDA recalls. monitor = only NEW recalls since the last run (state kept in a named key-value store). search = filtered query across endpoints. adverseEventCounts = aggregated adverse-event counts per product (no patient-level data).

## `products` (type: `array`):

One item per line. Plain text = keyword phrase. Prefix to force a type: `brand:Trader Joe's`, `upc:632687615989`, `ndc:27241-255-01`. 12–14 digit numbers are treated as UPC/GTIN, NNNNN-NNNN(-NN) as NDC. Used by watchlist mode.

## `endpoints` (type: `array`):

Which official api.fda.gov datasets to query. Default: the four recall/enforcement datasets.

## `dateFrom` (type: `string`):

Inclusive lower bound on the dataset's main date (report\_date for enforcement, event\_date\_posted for device recalls, effective\_time for labels, marketing\_start\_date for NDC, decision\_date for 510(k), receipt date for adverse events). In monitor mode this overrides the saved watermark.

## `dateTo` (type: `string`):

Inclusive upper bound on the main date.

## `lookbackDays` (type: `integer`):

Used when no dateFrom is set: search mode without any filters, and the first monitor run.

## `keywords` (type: `array`):

Phrases matched in product description, reason for recall, code info, and brand/generic names. Multiple keywords are OR'ed.

## `firms` (type: `array`):

Company names (word match, possessive-tolerant). OR'ed.

## `classifications` (type: `array`):

Class I (most serious), Class II, Class III. Enforcement datasets only.

## `statuses` (type: `array`):

e.g. Ongoing, Completed, Terminated (enforcement) or Open, Classified / Terminated (device recalls).

## `states` (type: `array`):

Two-letter state codes of the recalling firm, e.g. CA, NY.

## `rawSearch` (type: `string`):

Optional Lucene clause AND'ed to the query, e.g. `distribution_pattern:nationwide`. See https://open.fda.gov/apis/query-syntax/

## `maxResults` (type: `integer`):

Global cap on record rows pushed (watchlist summaries are extra, one per product). Default 200; the form is prefilled with 50 to keep a first test run small.

## `maxResultsPerQuery` (type: `integer`):

Cap per dataset (search/monitor) or per product per dataset (watchlist).

## `sortOrder` (type: `string`):

Sort by the dataset's main date.

## `dedupe` (type: `boolean`):

One row per recall/record even if several products or keywords match it (matches are merged into matched\_items).

## `includeRaw` (type: `boolean`):

Adds the untouched openFDA JSON under `raw` next to the flattened fields.

## `trimRawLists` (type: `boolean`):

Device records can carry hundreds of harmonized IDs (openfda.k\_number, fei\_number, registration\_number). When on, lists longer than 20 are cut to 20 and the original lengths recorded in openfda.\_truncated\_list\_lengths. Turn off for a byte-exact raw copy.

## `emitWatchlistSummary` (type: `boolean`):

Adds a status row per product (MATCHES\_FOUND / NO\_MATCH\_FOUND) for audit trails.

## `includePersonContacts` (type: `boolean`):

Off by default: removes fields naming individual people (device recall `additional_info_contact`, 510(k) `contact`) from both flattened and raw output.

## `labelMaxChars` (type: `integer`):

Truncate flattened label sections (warnings, indications…) to this length. Raw keeps full text.

## `monitorStateKey` (type: `string`):

Name for the saved monitor state (use different keys for different watch configurations).

## `monitorOverlapDays` (type: `integer`):

Re-scan this many days before the last watermark to catch late-posted records; already-seen IDs are skipped.

## `resetMonitorState` (type: `boolean`):

Forget previously seen records for this state key.

## `aeEndpoint` (type: `string`):

Aggregated counts only.

## `aeProducts` (type: `array`):

Drug brand/generic names, device brand/generic names, or food brand names.

## `aeCountBy` (type: `string`):

drug/event: reaction, seriousness, outcome, month. device/event: event\_type, product\_problem, month. food/event: reaction, outcome, month.

## `aeTopN` (type: `integer`):

How many most frequent terms to return per product (ignored for month, which returns all months).

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

Optional, free from https://open.fda.gov/apis/authentication/. Without a key openFDA allows 240 requests/min and 1,000 requests/day per IP (cloud IPs can be shared); with a key 120,000/day.

## `maxApiRequests` (type: `integer`):

Safety budget per run. Defaults to 900 without a key, 20,000 with a key.

## `requestsPerSecond` (type: `number`):

Throttle (hard-capped at 4/s = openFDA's 240/min).

## `pageSize` (type: `integer`):

Records per API request (openFDA max 1000). Large-record datasets are auto-capped (device recall 200, device enforcement 250, 510(k) 300, drug label 100) to keep memory low.

## Actor input object example

```json
{
  "mode": "watchlist",
  "products": [
    "peanut butter",
    "brand:Trader Joe's",
    "upc:632687615989",
    "ndc:27241-255-01"
  ],
  "endpoints": [
    "food/enforcement",
    "drug/enforcement",
    "device/enforcement",
    "device/recall"
  ],
  "lookbackDays": 30,
  "maxResults": 50,
  "maxResultsPerQuery": 100,
  "sortOrder": "newest",
  "dedupe": true,
  "includeRaw": true,
  "trimRawLists": true,
  "emitWatchlistSummary": true,
  "includePersonContacts": false,
  "labelMaxChars": 1500,
  "monitorStateKey": "default",
  "monitorOverlapDays": 14,
  "resetMonitorState": false,
  "aeEndpoint": "drug/event",
  "aeCountBy": "reaction",
  "aeTopN": 25,
  "requestsPerSecond": 3.5,
  "pageSize": 100
}
```

# Actor output Schema

## `recalls` (type: `string`):

Table of recalls/records with classification, firm, product, reason, dates and watchlist matches.

## `watchlist` (type: `string`):

One row per product checked: MATCHES\_FOUND / NO\_MATCH\_FOUND, count, most severe class.

## `adverseEvents` (type: `string`):

Aggregated adverse-event report counts by term.

## `runSummary` (type: `string`):

Status, request counts, errors and the data disclaimer.

# 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 = {
    "products": [
        "peanut butter",
        "brand:Trader Joe's",
        "upc:632687615989",
        "ndc:27241-255-01"
    ],
    "maxResults": 50
};

// Run the Actor and wait for it to finish
const run = await client.actor("smartmoney-data/openfda").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 = {
    "products": [
        "peanut butter",
        "brand:Trader Joe's",
        "upc:632687615989",
        "ndc:27241-255-01",
    ],
    "maxResults": 50,
}

# Run the Actor and wait for it to finish
run = client.actor("smartmoney-data/openfda").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 '{
  "products": [
    "peanut butter",
    "brand:Trader Joe'\''s",
    "upc:632687615989",
    "ndc:27241-255-01"
  ],
  "maxResults": 50
}' |
apify call smartmoney-data/openfda --silent --output-dataset

```

## MCP server setup

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

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/SjdpMXgogfV5u9nnv/builds/4dUpzkNIHwbculcck/openapi.json
