# FDA Recalls Scraper (openFDA) — Food, Drug & Device (`northbell/openfda-recalls-scraper`) Actor

US FDA recalls from the official openFDA enforcement reports: food, drugs and medical devices with firm, classification, reason, quantity, distribution, dates and status. Filter by keyword, firm, class, state and date, and flag recalls new or changed since your last run.

- **URL**: https://apify.com/northbell/openfda-recalls-scraper.md
- **Developed by:** [Northbell](https://apify.com/northbell) (community)
- **Categories:** Automation, Business
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $1.60 / 1,000 recalls

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?

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

Search US recalls in the **FDA enforcement reports** (via the official **openFDA** API) by **keyword, recalling firm, classification, status, the firm's state or country and report date**, for **food, drugs and medical devices**, and get one clean row per recall: **recall number, event id, product type, class, status, recalling firm and its city / state / country, product description, reason for the recall, quantity, distribution, lot / code info, the recall, classification and termination dates, voluntary or mandated, how the firm was notified**, with a link to the record in openFDA. **$2.00 per 1,000 recalls**, no login, no API key needed.

Run it again — or put it on a daily schedule — and every row also tells you **whether the recall is new since the last run (`isNew`) and whether its status changed, for example Ongoing to Terminated (`statusChanged`)**. Set *What to return* to *New or changed* and you pay only for those.

Looking for vehicle recalls? See the [NHTSA Vehicle Recalls](https://apify.com/northbell/nhtsa-vehicle-recalls) Actor.

> Data provided by the U.S. Food and Drug Administration (https://open.fda.gov). This Actor is not an FDA product and is not endorsed by the FDA. Do not use it for decisions about medical care. See *Notes*.

### What you get

| Field | What it is |
|---|---|
| `recallNumber`, `eventId` | the FDA recall number (`F-0276-2017`, `D-0879-2026`, `Z-0004-2027`; F or H = food, D = drug, Z = device) and the id of the recall event it belongs to (one event can have many recalls) |
| `productType` | `food`, `drug` or `device` |
| `classification` | `Class I`, `Class II` or `Class III`, the FDA's hazard level |
| `status` | `Ongoing`, `Completed` or `Terminated` as openFDA lists it (`Pending` is a documented value; no record had it on 16 Oct 2026) |
| `recallingFirm`, `city`, `state`, `country` | the company that recalled the product and where it is located. `state` is `null` when openFDA has none or writes `N/A` (in the newest 3,000 records, 80 of the 82 food, all 312 drug and all 493 device records from firms outside the US had none). Street address and postal code are not returned |
| `productDescription`, `reasonForRecall` | the product and why it was recalled, as the firm and FDA wrote it |
| `productQuantity`, `distributionPattern` | how much was recalled (free text: `1,990 bottles`) and where it went (`FL, MI, MS, and OH.`, `Nationwide`) |
| `codeInfo`, `codeInfoTruncated` | lot / serial numbers and dates on the product, cut at 1,000 characters (`codeInfoTruncated` says so; of the newest 3,000 records, 685 device, 44 drug and 18 food ones had longer code info) |
| `recallInitiationDate`, `centerClassificationDate`, `reportDate`, `terminationDate` | when the recall started, when the FDA classified it, the date of the enforcement report it appeared in, and when it was terminated (`null` while it is open), as `YYYY-MM-DD` |
| `voluntaryMandated`, `initialFirmNotification` | who started the recall (`Voluntary: Firm initiated`, `FDA Mandated`) and how the firm told its customers (`Letter`, `E-Mail` …) |
| `url` | the recall's record in the openFDA API (JSON) |
| `isNew`, `statusChanged`, `previousStatus`, `previousRunAt` | `true` / `false` whether the recall was not in your previous run of the same search, whether its status differs from that run, the status it had then, and when that run was (`null` the first time) |
| `observedAt` | when the row was collected |

### Run it again and again

- **On a schedule** — save your input as a task and add a **Schedule**: the same search every week. openFDA is updated weekly. Send each run to Google Sheets, Slack or email with an integration on the task.
- **Watch for changes** — Set **What to return** to *New or changed*; a recall is new the first time your search returns it, and changed when its status is not the one the previous run saw.
- **In bulk** — Put several keywords, firms or classes in one run; a recall that matches any of the values in a field is returned once.

### Use it for

- **Supplier and product monitoring** — a weekly run with your suppliers' names or your ingredients as keywords, *New or changed*, sent to Slack or email
- **Compliance and quality teams** — Class I recalls in your product area, with reason, quantity and distribution
- **Market research** — which firms recall most, in which class and for what reasons
- **Insurance and legal research** — a firm's recall history with dates and status

### Input

```json
{
  "productTypes": ["food", "drug"],
  "keywords": ["listeria", "undeclared milk"],
  "recallingFirms": [],
  "classifications": ["Class I"],
  "statuses": [],
  "states": [],
  "countries": [],
  "reportedWithinDays": 90,
  "output": "new-or-changed",
  "maxResults": 200
}
```

- **productTypes** — `food`, `drug`, `device`. Read in the order you list them, newest report date first within each. Empty means all three.
- **keywords** — words or phrases looked for in `productDescription` and `reasonForRecall`; a recall matching any keyword is returned.
- **recallingFirms** — company names; a name matches whole words in the firm's name, not case sensitive (`Stryker` finds `Stryker Corporation`).
- **classifications**, **statuses** — pick any. A search for `Ongoing` stops returning a recall once it is `Terminated`, so leave *statuses* empty if you want to see status changes.
- **states**, **countries** — where the *recalling firm* is located (two-letter US state codes; country names as the FDA writes them). It is not where the product was sold; that is in `distributionPattern`.
- **reportDateSince** (`2026-01-01`), **reportedWithinDays** — limit by the date of the enforcement report. If you give both, the later date is used. A date also makes *New or changed* runs much faster, see below.
- **maxResults** — total rows (default 200, up to 100,000).
- **apiKey** — optional, your own free openFDA key. See *Rate limits*.

Within a field the values are "any of", and the fields are combined with "and": the input above is *(listeria or undeclared milk) and Class I*.

"Is it new" is remembered per product type and per set of conditions (keywords, firms, classes, statuses, states, countries) in a named key-value store in your Apify account. The date, *maximum recalls*, *what to return* and the key can change between runs without losing the history. A product type you search for the first time has `isNew: null` on that run, and a search that found nothing at that time gets a baseline too, so its first recall later is `isNew: true`. With a *Maximum recalls* cap, only the recalls that were returned are remembered, and the older ones the cap left unread are not counted as new next time; only a recall reported after the newest one that run saw is new (so a capped run followed by *New or changed* never charges for old recalls). Up to 100,000 recall numbers are remembered per product type and search, oldest report dates dropped first.

### Large result sets

openFDA lets a search read at most 25,000 records by paging (a `skip` above 25,000 is refused with HTTP 400, and one request returns at most 1,000). Food (29,485 records on 16 Oct 2026) and devices (40,249) are over that, drugs (18,010) are not. When a search needs more than that, the Actor splits the report-date range in two, newest half first, and keeps halving a half that still has more than 25,000 matches; each record is read once. A cap of up to 25,000 rows, or a narrower search, needs no split at all.

*What to return* = *New* or *New or changed* has to compare every matching recall with the last run, so it reads all of them, and for a broad search that is many requests: reading all 40,248 numbered device records took 45 requests and 106 seconds in a test on 16 Oct 2026 without a key (3 splits). Set *Reported since* (or *within N days*) to keep these runs short. A recall keeps its report date when its status changes (F-0276-2017 still has its 2016 report date), so a date filter only shows status changes for recalls inside the window.

### Pricing

**$2.00 per 1,000 recalls**, plus $0.01 per run. Error rows (a search openFDA could not answer) are free. With *New* or *New or changed*, recalls you have already seen are skipped and not charged. The $0.01 run-start fee is the one charge every run pays, even one that returns nothing.

### Rate limits

openFDA documents these limits ([authentication page](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.** The Actor works without a key and waits 0.4 seconds between requests (about 150 per minute). A run of 200 rows is one request; every 1,000 rows more is one more. The limit without a key is per IP address, so if other runs use the same address the daily allowance is shared; if you see rate-limit errors, add your own free key as `apiKey`. The key is sent in an `Authorization` header, never in the URL, and is not logged or written to the dataset. If openFDA answers 429, 5xx or not at all the Actor waits and retries twice; if a rate limit persists it stops with a free error row and the run is marked failed when nothing at all could be read.

### Notes

- Source: the public openFDA enforcement report endpoints `api.fda.gov/food/enforcement.json`, `/drug/enforcement.json` and `/device/enforcement.json`, which serve data from the FDA's Recall Enterprise System. The openFDA [terms of service](https://open.fda.gov/terms/) and [license](https://open.fda.gov/license/) put the data under a CC0 public-domain dedication (commercial use allowed without asking; credit is requested, not required). Suggested credit: *Data provided by the U.S. Food and Drug Administration (https://open.fda.gov)*. The terms say the FDA may limit access to manage load and prevent abuse; the Actor stays under the documented limits.
- openFDA's disclaimer, which comes with every answer: *"Do not rely on openFDA to make decisions regarding medical care."* The enforcement pages add that the data should not be used to issue alerts to the public or to track the lifecycle of a recall, and that the FDA does not update a recall's status after it has been classified. Use this Actor for research and monitoring, and check the recall on the FDA's own site before you act on a row.
- About that last point: in the data there are recalls whose termination date is years after their report date (F-0276-2017 was reported 2016-11-02 and terminated 2024-09-23), so statuses are updated, at least in some cases. I did not measure how promptly or completely. `statusChanged` means *openFDA's value differs from your last run*, nothing more.
- Data freshness: openFDA says it is updated weekly. On 16 Oct 2026 its answers said the data was last updated on 7 Oct 2026, and the newest report date was 2026-10-07 in all three endpoints. The Actor puts this date in the run summary (`dataLastUpdated`). The earliest report date I found in all three endpoints is 2012-06-20.
- On 16 Oct 2026 the statuses were: food 27,958 Terminated, 1,079 Ongoing, 448 Completed; drugs 14,810, 2,764 and 436; devices 25,333, 14,291 and 625.
- Records are one per product, not one per event: one `eventId` can have many recall numbers. The Actor returns one row per recall number. Recalls the FDA has not classified yet (`Not Yet Classified`: 2 food, 2 drug and 1 device record on 16 Oct 2026) have an empty recall number or `N/A`; they are skipped and counted in the run summary (`skippedNoNumber`), because they cannot be compared between runs.
- Texts are the firm's and the FDA's wording. No contact fields, no street address and no postal code are returned, and e-mail addresses and North American phone numbers in the texts are removed (in the newest 3,000 records of each endpoint, 25 food and 26 drug records had one; none of the devices). Lot codes and part numbers that only look like a phone number or e-mail are kept. Free text can still contain a name the firm wrote there (I found a doctor's name in a device description), and a firm's own name can be a person's name; the Actor does not try to remove those.
- `state` and `country` are the recalling firm's location. The Actor reads only the three enforcement endpoints, not the other openFDA datasets (some of which carry licensed third-party terms).

### For AI agents

`{"productTypes": ["food"], "keywords": ["listeria"], "classifications": ["Class I"], "reportedWithinDays": 365, "maxResults": 5}` returns up to 5 rows, newest report date first, each with `recallNumber`, `recallingFirm`, `classification`, `status`, `productDescription`, `reasonForRecall` and `reportDate`. Rows have `type`: `recall` or `error`. Fields that do not apply to a recall are `null`, never missing. Schedule the same input weekly with `"output": "new-or-changed"` to get only the recalls that appeared or changed status since the previous run. For vehicle recalls use the NHTSA Vehicle Recalls Actor.

# Actor input Schema

## `productTypes` (type: `array`):

Which FDA enforcement reports to search: food, drugs, medical devices. They are read in the order you pick, newest report date first within each. Leave empty for all three.

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

Words or phrases to find in the product description or in the reason for the recall. One per line; a recall that matches any line is returned. Keep them short, e.g. "listeria", "undeclared milk" or "pacemaker". Leave empty to skip.

## `recallingFirms` (type: `array`):

Only recalls by these companies, one name per line, as the FDA writes it, e.g. "Teva" or "Stryker". A name matches whole words in the firm name (not case sensitive), so "Stryker" finds "Stryker Corporation". Leave empty for all firms.

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

Only recalls the FDA classified as these hazard levels. Leave empty for all three.

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

Only recalls with these statuses as openFDA lists them. Leave empty for all. Note that a search for Ongoing no longer returns a recall once it is Terminated, so leave this empty if you want to see status changes.

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

Only recalls by firms located in these US states, as two-letter codes, one per line: CA, TX, NY. This is where the recalling firm is, not where the product was sold. Leave empty for all.

## `countries` (type: `array`):

Only recalls by firms located in these countries, one per line, as the FDA writes them: United States, Canada, India. This is where the recalling firm is, not where the product was sold. Leave empty for all.

## `reportDateSince` (type: `string`):

Only recalls whose FDA enforcement report date is on or after this day, as YYYY-MM-DD, e.g. 2026-01-01. If you also set "Reported within the last N days", the later of the two dates is used. Leave empty for no start date.

## `reportedWithinDays` (type: `integer`):

Only recalls in FDA enforcement reports from the last N days. openFDA is updated weekly, so a value under 7 may return nothing. Leave empty for no limit.

## `output` (type: `string`):

With the last two, recalls you already got in an earlier run of the same search (and whose status has not changed) are skipped and not charged. The first run returns everything, because there is nothing to compare with yet. These modes read every matching recall to compare it, so narrow the search with a date to keep the run short.

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

How many recall rows to return in total, product types in the order you picked them, newest report date first within a type. You pay per row returned.

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

Optional. Your own free key from open.fda.gov/apis/authentication. Without a key openFDA allows 240 requests per minute and 1,000 per day per IP address, and Apify runs share IP addresses; with a key, 240 per minute and 120,000 per day. The key is sent to openFDA in a request header only; the Actor never logs it or writes it to the dataset.

## Actor input object example

```json
{
  "productTypes": [
    "food"
  ],
  "keywords": [
    "listeria"
  ],
  "output": "all",
  "maxResults": 200
}
```

# Actor output Schema

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

Recall number, product type, firm, classification, status, product, reason and dates for each recall.

# 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 = {
    "productTypes": [
        "food"
    ],
    "keywords": [
        "listeria"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("northbell/openfda-recalls-scraper").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 = {
    "productTypes": ["food"],
    "keywords": ["listeria"],
}

# Run the Actor and wait for it to finish
run = client.actor("northbell/openfda-recalls-scraper").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 '{
  "productTypes": [
    "food"
  ],
  "keywords": [
    "listeria"
  ]
}' |
apify call northbell/openfda-recalls-scraper --silent --output-dataset

```

## MCP server setup

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

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/tH6NHkPi84FJb9kYY/builds/m2o37wyPEevURthhR/openapi.json
