# FDA Recalls Scraper - Food, Drug & Device Recalls API, openFDA (`neverempty/fda-recalls-api`) Actor

Every FDA enforcement report (recall) for food, drugs and medical devices from the official openFDA API. Dates parsed to ISO, the recall class explained in one line, and the free-text distribution turned into a list of US states. No API key. Ongoing and terminated recalls.

- **URL**: https://apify.com/neverempty/fda-recalls-api.md
- **Developed by:** [NeverEmpty](https://apify.com/neverempty) (community)
- **Categories:** Developer tools, MCP servers
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

$8.00 / 1,000 recall 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?

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 Recalls Scraper - Food, Drug & Device Recalls API, openFDA

Every **FDA recall** (enforcement report) for **food**, **drugs** and **medical devices**, from the official
**openFDA** API, in one schema you can put straight into a monitor, a spreadsheet or a compliance review.

- **No API key, no scraping, no proxy.** US government data, public domain.
- **All three FDA lists in one run**, with the same column names for each.
- **Filter where it costs nothing**: by recall class, by keyword (product, reason or firm), by date window.
  The filter is applied by FDA, so you are not billed for rows you did not ask for.

### What this returns that the raw openFDA JSON does not

| Column | Why it matters |
|---|---|
| `reportDate`, `recallInitiationDate`, `centerClassificationDate`, `terminationDate` | The raw API returns `"20260508"`. These are real ISO dates you can sort and compare. Unreadable values stay `null` — never guessed. |
| `classificationMeaning` | `Class I` on its own means nothing to most readers. This is FDA's definition from 21 CFR 7.3, shortened to one line: for Class I, a reasonable probability that use of, or exposure to, the product will cause serious adverse health consequences or death. |
| `distributionStates`, `isNationwide`, `isInternational` | The raw field is free text: `"WI"`, `"FL and TX."`, `"Nationwide."`, `"The recalled product was distributed to the following states: MD, VA"`. This turns it into a sorted list of state codes and two flags. **The raw text is always kept in `distributionPattern`.** |
| `brandNames`, `genericNames`, `manufacturerNames`, `productNdc`, `substanceNames` | Pulled out of the `openfda` block. Food and device records never carry it, and only some drug records do (about 18% of drug recalls, measured over 17,899 records), so these stay `null` instead of becoming empty lists. |
| `dataLastUpdated` | The date FDA itself last refreshed the data, on every row, so you can tell "no recalls" from "not published yet". |

**On the state list:** it is only filled in when the text is actually written as a list of states. Prose such as
`"Distributed IN bulk OR by mail"` contains two valid state codes as English words, so it is left `null`
instead of claiming Indiana and Oregon. Measured over 1,200 real records: no record was given a state it did
not have, and about 1% of records that do list states are left `null` because the list is broken up by
something else (a foreign territory, a spelled-out state). The raw text is always in `distributionPattern`.

### Input

| Field | Type | Default | Meaning |
|---|---|---|---|
| `products` | array | `["food","drug","device"]` | Which FDA lists to read. |
| `sinceDays` | integer | `30` | How far back on the FDA report date. Ignored when a start and end date are set. |
| `beginDate` / `endDate` | string | `""` | A fixed window, `"2026-08-01"` to `"2026-08-31"`. Past years work. |
| `classifications` | array | `[]` (all) | `"Class I"`, `"Class II"`, `"Class III"`. |
| `keyword` | string | `""` | Only recalls whose product description, reason, or firm contain this phrase. |
| `maxRecordsPerProduct` | integer | `1000` | Upper bound per list, so a wide window cannot run up a bill. |
| `maxRetries` | integer | `4` | Retries when openFDA does not answer or rate-limits. |

### Output

One row per recall:

```json
{
  "rowType": "recall",
  "product": "food",
  "recallNumber": "H-1180-2026",
  "classification": "Class I",
  "classificationMeaning": "Reasonable probability that use of, or exposure to, the product will cause serious adverse health consequences or death.",
  "status": "Ongoing",
  "recallingFirm": "Green Jeeva LLC",
  "productDescription": "Moringa Powder",
  "reasonForRecall": "Moringa Powder ingredient positive for salmonella",
  "distributionPattern": "Nationwide.",
  "distributionStates": null,
  "isNationwide": true,
  "isInternational": false,
  "reportDate": "2026-07-29",
  "recallInitiationDate": "2026-06-30",
  "terminationDate": null,
  "dataLastUpdated": "2026-08-19"
}
```

### What it does when something is wrong

A recall feed that quietly answers "nothing was recalled" is the worst possible failure, so this Actor never does that.

- **No matching recall** → one row saying so, with the window and the FDA update date. **Not charged**, and the
  run does not fail: an empty week is a real answer.
- **openFDA rejecting the request** (a malformed window, an unusable keyword) → an error row with the reason,
  and the run fails if nothing at all could be read. It is never reported as "no recalls".
- **A keyword openFDA would silently ignore** — punctuation only, or non-ASCII, which it answers with the whole
  window or an error — stops the run before anything is billed, instead of returning everything you did not ask for.
- **One list failing** while the others work → the working rows are delivered, the failure is reported as its own
  row and in `RUN_ISSUES`.
- **More matches than your cap** → the log and `RUN_TRUNCATED` say how many of how many were returned.
- Rate limiting (HTTP 429) is retried with a pause; rejections are not retried.

### Timing you should know about

FDA publishes enforcement reports **weekly**, and the data lags. On 2026-08-31 the newest data in all three
lists was stamped `2026-08-19`. Every row carries `dataLastUpdated` so a monitor can tell the difference
between "nothing happened" and "FDA has not published yet". A daily schedule is still useful — it just means
new rows appear in weekly batches.

### Measured

Run on 2026-08-31, 45-day window, Class I only: **92 recalls in 1.8 seconds** (food 51, drug 6, device 35).
States were parsed for 58 of them, 32 were nationwide, 18 reached outside the US, and every date came back
as a valid ISO date.

The build is covered by 55 automated checks, and 54 deliberate defects were injected one at a time to confirm
those checks actually catch them.

### Billing

Pay per event: one charge per recall row delivered. The default `sinceDays` of 30 covers a rolling month, so a
**daily** schedule bills that whole month every day (about 240 rows a day at the time of writing). For daily
monitoring set `sinceDays` to 7 or less, or use a date window — FDA publishes weekly, so nothing is missed.

The "no recalls" row and rows that report a problem are **not** charged. Setting a maximum total charge on the
run stops delivery rather than handing rows over unpaid, and the run says so instead of reporting "no recalls".

### Support

Questions and bug reports: use the **Issues** tab on this Actor. Include the input you ran and the run id.

Data source: [openFDA](https://open.fda.gov/) enforcement reports — official US Food and Drug Administration
data, public domain. This Actor is not affiliated with the FDA. Recall data is not medical or legal advice;
for action on a specific recall, check the FDA's own listing for that recall number.

# Actor input Schema

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

Which FDA enforcement lists to read: "food", "drug", "device". Leave empty for all three. If you give values and none of them are usable, the run fails rather than quietly reading everything.

## `sinceDays` (type: `integer`):

How far back to look, counted on the FDA report date. 30 covers about a month of weekly enforcement reports. Ignored when you set a start and end date below.

## `beginDate` (type: `string`):

First FDA report date to include, as "2026-08-01". Use with the end date for a fixed window; past years work.

## `endDate` (type: `string`):

Last FDA report date to include, as "2026-08-31". Must be given together with the start date.

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

Keep only these classes: "Class I" (reasonable probability of serious harm or death), "Class II", "Class III". Leave empty for all of them.

## `keyword` (type: `string`):

Only recalls whose product description, reason for recall, or firm name contain this phrase. For example "peanut", "listeria" or a company name.

## `maxRecordsPerProduct` (type: `integer`):

Upper bound on how many recalls to return for each product type, so a wide window cannot run up a bill. When FDA has more than this, the run says so in the log and in RUN\_TRUNCATED instead of silently cutting.

## `maxRetries` (type: `integer`):

How many times to retry when openFDA does not answer or rate-limits the request. Rejected requests are not retried.

## Actor input object example

```json
{
  "products": [
    "food",
    "drug",
    "device"
  ],
  "sinceDays": 30,
  "beginDate": "",
  "endDate": "",
  "classifications": [],
  "keyword": "",
  "maxRecordsPerProduct": 1000,
  "maxRetries": 4
}
```

# Actor output Schema

## `results` (type: `string`):

One row per FDA enforcement report (recall), plus one row per endpoint that could not be read.

# 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("neverempty/fda-recalls-api").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("neverempty/fda-recalls-api").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 neverempty/fda-recalls-api --silent --output-dataset

```

## MCP server setup

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

```

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/rbiSvveAgMexgssYn/builds/sQarhcyhQhN8sG5Np/openapi.json
