# FDA Recalls Watch & API: Food, Drug & Device (openFDA) (`mouadapi/openfda-recalls`) Actor

FDA recalls API and food recall monitor: new and changed food, drug and medical device recalls from the official openFDA API since your last run, or every match; failed or unchanged rows are never charged. Not affiliated with or endorsed by the U.S. FDA.

- **URL**: https://apify.com/mouadapi/openfda-recalls.md
- **Developed by:** [COMPASS DEV](https://apify.com/mouadapi) (community)
- **Stats:** 1 total users, 0 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $1.00 / 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

openFDA Recalls Watch returns U.S. FDA food, drug and device recalls from the official openFDA enforcement API as flat rows — only the recalls that are new or changed since your last run (watch mode), or every recall that matches your filters (export mode) — and never charges for failed results or unchanged recalls.

Not affiliated with or endorsed by the U.S. FDA.

**FDA recall monitor and export API**: watch **FDA food recalls, drug recalls and medical device recalls** (enforcement
reports) and get back **only what is new or changed** — a new recall, or a recall whose status, classification,
termination date or quantity changed, with the **old and new values side by side**. Or export every recall that matches
your filters: product type, Class I / II / III, status, date range, the firm's state or country, a keyword in the product
or reason, or the recalling firm.

Built for compliance, food safety, retail and AI-agent workflows that must react to recalls: run it on a schedule and
act only on the rows that come back. **You are never charged for unchanged recalls, empty searches or failed results** —
never charged for failed results. Every row has its **source URL** (`url`) and **fetch time** (`scrapedAt`).

**Why this one:**

- **Watch mode with a memory:** each named watch list remembers the recalls it has seen, so a daily run returns only new
  recalls and real changes (`changeType`: `new` or `changed`, with `changedFields` and the `previous…` values).
- **Catches late changes:** recalls reported before your date window are re-checked while they are open, so a
  termination months later still comes back as a change.
- **Official source only:** the documented openFDA API (CC0 public data), one request at a time, no scraping.
- **Unchanged recalls are free:** a watch run where nothing changed costs only Apify's small Actor-start charge.

### Quick start

- **One call, current recalls:** `{"productTypes": ["food"]}` returns the **food recalls reported in the last 30 days**
  (export mode), every time you call it.
- **Watch for changes:** give a watch list name. The form is filled in like this (it also ticks "include unchanged
  recalls", which come back free):

  ```json
  {
    "mode": "watch",
    "stateName": "example-watchlist",
    "productTypes": ["food"],
    "daysBack": 30,
    "includeUnchanged": true
  }
  ```

  The first run is the **baseline**: one row per recall (`changeType: "baseline"`). Run it again later (or add an Apify
  schedule, e.g. daily): only new recalls (`new`) and changed ones (`changed`) come back; unchanged recalls are left out
  (free), or returned free as `unchanged` with `includeUnchanged`.

**Which mode runs:** an explicit `mode` always wins. Without one, a watch list name (`stateName`) means watch and no name
means export, so the same call returns data every time. `mode: "watch"` without a name uses the watch list `default`.

### Use cases

- **Food safety and retail:** watch Class I food recalls (`classifications: ["Class I"]`) and pull products from shelves
  the day FDA publishes them.
- **Supplier and brand monitoring:** watch a firm (`recallingFirm`) or a keyword (`keyword`: "peanut", "listeria",
  "pacemaker") across food, drugs and devices.
- **Compliance records:** export all device recalls for a period (`mode: "export"`, `dateFrom`, `dateTo`) as a flat table.
- **Track specific recalls:** list recall numbers (`recallNumbers`) and get a row when their status or termination
  date changes.

### What it does

- Searches the openFDA **enforcement reports** (FDA's weekly recall reports) for food, drug and device, with your
  filters, newest reports first, 1,000 records per request (the API maximum).
- **Watch mode** (with a watch list name, or `mode: "watch"`): compares every recall with what the watch list
  (`stateName`) saw at the last run:

  - `baseline` — the first run of a watch list (charged);
  - `new` — a recall that appeared since the last run (charged);
  - `changed` — its status, classification, termination date or product quantity changed (charged), with
    `changedFields` and `previousRecallStatus`, `previousClassification`, `previousTerminationDate`,
    `previousProductQuantity`;
  - `unchanged` — not returned (free), or returned free with `includeUnchanged`.

  Recalls the list already knows that are still open are **re-checked by number** even when they fall outside your date
  window, so later changes are not missed.
- **Export mode** (no watch list name, or `mode: "export"`): every recall that matches the filters (charged), up to
  `maxItems`.
- **Recall numbers** you list are looked up directly: an unknown number gives a free `no_data` row, an invalid one a
  free `failed` row.
- Leaves out the firm's street address and postal code. A firm name that is, or may be, a person's name is left out of
  every field, and the recall is kept (`recallingFirm` is null, `recallingFirmOmitted: true`; see Known issues).

### Input

| Field | What it does | Default |
|---|---|---|
| `mode` | `watch` (new and changed recalls since the last run) or `export` (every match) | watch if `stateName` is given, else export |
| `stateName` | Watch list name; runs with the same name compare with each other (gives watch mode) | — (`mode: "watch"` without a name uses `default`) |
| `productTypes` | `food`, `drug`, `device` (empty = only the recall numbers you list) | `food` |
| `classifications` | `Class I`, `Class II`, `Class III` (empty = all) | all |
| `statuses` | `Ongoing`, `Completed`, `Terminated`, `Pending` (empty = all) | all |
| `daysBack` | Recalls whose date is within the last N days (ignored with `dateFrom` / `dateTo`) | `30` |
| `dateFrom`, `dateTo` | Fixed date range (YYYY-MM-DD) | — |
| `dateField` | `reportDate` (FDA published the report) or `recallInitiationDate` (firm started the recall) | `reportDate` |
| `states`, `countries` | The recalling firm's US state (e.g. `CA`) or country (e.g. `United States`) | — |
| `keyword` | A word or phrase in the product description or the reason for recall | — |
| `recallingFirm` | A word or phrase of the firm's name | — |
| `recallNumbers` | Specific recalls, e.g. `H-0123-2026` (food), `D-0456-2026` (drug), `Z-0789-2026` (device) | — |
| `includeUnchanged` | Watch mode: also return unchanged recalls, free | `false` |
| `maxItems` | Most charged rows per run; in watch mode the rest come in the next run | `100` |
| `apiKey` | Optional: your own free openFDA API key (secret; never logged or output) | — |

Example: watch Class I recalls of food and drugs that mention listeria, in a watch list of their own:

```json
{
  "stateName": "listeria-class-1",
  "productTypes": ["food", "drug"],
  "classifications": ["Class I"],
  "keyword": "listeria",
  "daysBack": 90
}
```

### Output

One row per recall (flat, ready for CSV, Excel or an agent). A `changed` row from watch mode:

```json
{
  "status": "ok",
  "error": null,
  "attempts": 1,
  "input": "watch list \"default\" (re-check)",
  "mode": "watch",
  "watchList": "default",
  "recallNumber": "H-0123-2026",
  "productType": "food",
  "classification": "Class I",
  "recallStatus": "Terminated",
  "recallInitiationDate": "2026-08-12",
  "reportDate": "2026-08-20",
  "terminationDate": "2026-09-25",
  "recallingFirm": "Example Foods Inc.",
  "recallingFirmOmitted": false,
  "city": "Springfield",
  "state": "IL",
  "country": "United States",
  "productDescription": "Example brand peanut butter cookies, 12 oz",
  "reasonForRecall": "Undeclared peanut",
  "distributionPattern": "Nationwide",
  "productQuantity": "1,200 cases",
  "voluntaryMandated": "Voluntary: Firm initiated",
  "eventId": "98765",
  "changeType": "changed",
  "changedFields": "recallStatus, terminationDate",
  "previousRecallStatus": "Ongoing",
  "previousClassification": "Class I",
  "previousTerminationDate": null,
  "previousProductQuantity": "1,200 cases",
  "url": "https://api.fda.gov/food/enforcement.json?search=recall_number%3A%22H-0123-2026%22",
  "source": "openFDA food enforcement reports (U.S. Food and Drug Administration)",
  "license": "CC0 1.0 public domain dedication (openFDA terms); not endorsed by the U.S. FDA",
  "licenseUrl": "https://open.fda.gov/license/",
  "dataUpdatedAt": "2026-09-23",
  "scrapedAt": "2026-09-30T06:30:00.000Z"
}
```

- `status`: `ok` (a recall), `no_data` (nothing to return: no match, no changes since the last run, or an unknown recall
  number — free) or `failed` (invalid input or no answer — free), with `error` and `attempts`.
- `url` is the **source URL** (the openFDA query that returns this recall); `scrapedAt` is the **fetch time**;
  `dataUpdatedAt` is the day openFDA last refreshed the data.
- Dates are ISO 8601 (YYYY-MM-DD). Values are exactly as FDA publishes them.

### Pricing

Pay per recall returned (event `recall`): baseline, new, changed and exported recalls.

| Apify plan | Per recall | Per 1,000 recalls |
|---|---|---|
| Free | $0.0015 | $1.50 |
| Bronze | $0.0013 | $1.30 |
| Silver | $0.00115 | $1.15 |
| Gold (and Platinum, Diamond) | $0.0010 | $1.00 |

- **Never charged for failed results**, empty searches, unknown recall numbers or unchanged recalls. A watch run where
  nothing changed costs only Apify's small Actor-start charge.
- Your maximum charge per run is respected: the run stops before it and outputs only what it could charge.
- No usage fees on top: the price per recall covers the platform's compute.

### Use it from AI agents

- **MCP:** add the Actor through the Apify MCP server (`https://mcp.apify.com?actors=mouadapi/openfda-recalls`), then ask
  e.g. *"Which FDA Class I food recalls are new since yesterday?"* Each row says `ok`, `no_data` or `failed`, and
  `changeType` says what happened.
- **API:** one call returns the rows:

```bash
curl -X POST "https://api.apify.com/v2/acts/mouadapi~openfda-recalls/run-sync-get-dataset-items?token=$APIFY_TOKEN" \
  -H "Content-Type: application/json" -d '{"stateName": "agent-food", "productTypes": ["food"], "classifications": ["Class I"]}'
```

- **x402 payments:** the Actor is pay-per-event only, with no usage fees, limited permissions and no Standby mode, so
  agents can pay per recall with x402.
- Flat rows with the source URL and fetch time on each, ready to cite.

The same call from JavaScript (the Apify client):

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

const client = new ApifyClient({ token: process.env.APIFY_TOKEN });
const run = await client.actor('mouadapi/openfda-recalls').call({ productTypes: ['food'] });
const { items } = await client.dataset(run.defaultDatasetId).listItems();
console.log(items);
```

### Limits

- openFDA's own limits: without an API key, **1,000 requests a day per IP address** (and 240 a minute). Apify runs share
  IP addresses, so other users' openFDA calls count too. **For large or frequent runs, add your free openFDA key**
  (`apiKey`): 120,000 requests a day.
- One request at a time (at most 2 a second), 1,000 recalls per request. A typical watch run needs 1–5 requests.
- Rows are saved and charged one at a time (so a stopped run never charges a row you didn't get): about 8 rows a second,
  so 100 rows take seconds and a 2,000-row export about 4 minutes.
- On "too many requests" (HTTP 429) the run pauses (1, 2, then 4 minutes, or longer if openFDA asks) at most 3 times,
  then stops with a clear message; if openFDA asks for a wait of more than 5 minutes (a daily limit), it stops at once.
  It never switches IP address or proxy.
- One filter search reads at most 26,000 recalls per product type (the API's paging limit); `maxItems` is at most 10,000.
- FDA publishes enforcement reports weekly, so a daily watch run mostly finds nothing new between weekly updates
  (`dataUpdatedAt` shows the last update).
- No street addresses, postal codes or personal names are returned.

### Known issues

- **How names are handled:** a firm name that is, or may be, a person's name (judged by its shape, never by a list of
  first names: "Jane Doe", "Smith, John", "Dr. Jane Doe", "Jane Doe dba Doe Farms", and, when in doubt, two- or
  three-word names without a business word such as "Fresh Express") is left out: `recallingFirm` is null and
  `recallingFirmOmitted` is true. The same name is also removed from every other text field of that recall (product
  description, reason, distribution pattern, quantity…); a field that still holds a word of the name is null. The recall
  itself is kept and charged as usual. Names with a business word (Inc, LLC, Foods, Pharmacy…) are kept.
- Recall numbers starting with H or F are looked up in food, D in drug and Z in device; other letters are tried in all
  three (up to 3 requests).
- openFDA lists a few recalls without a recall number (blank, or "N/A" while not yet classified; about 1 in 1,000
  records): they are skipped (counted in the run report) until they get a number; then they come back as `new` recalls.
- Changing the filters of a watch list restarts its baseline: recalls it doesn't know yet come back as `baseline` rows.

### FAQ

**How much does it cost?** $1.50 per 1,000 recalls returned on the Free plan, down to $1.00 on Gold, plus Apify's small
Actor-start charge per run. Watching Class I food recalls daily (a few new ones a week) costs a few cents a month.

**Am I charged for recalls that didn't change?** No. Unchanged recalls, empty searches, unknown recall numbers and
failed checks are free.

**Do I need an openFDA API key?** No. It runs without one, within the limits openFDA publishes for calls without a key.
For large or frequent runs, add your free openFDA key (open.fda.gov/apis/authentication): it raises the daily limit from
1,000 requests per IP address to 120,000 per key, and is stored as a secret, never logged or output.

**Is this allowed?** openFDA data is published under CC0 (public domain) and may be reused, including commercially. This
tool is not affiliated with or endorsed by the U.S. FDA. The Actor uses only the documented openFDA API, one request at a time.

Also by the same author: [Wikipedia Article Watch & Scraper API](https://apify.com/mouadapi/wikipedia-articles),
[DNS Lookup & SSL Certificate Checker](https://apify.com/mouadapi/dns-ssl-checker) and
[Woolworths Price Scraper & Monitor](https://apify.com/mouadapi/woolworths-price-monitor).

# Actor input Schema

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

watch: per named watch list, return only recalls that are new or changed since the last run (the first run returns every recall as a baseline row). export: return every recall that matches the filters. If you leave it empty: a watch list name means watch, no name means export (the same call returns data every time).

## `stateName` (type: `string`):

Name of your watch list. Giving a name turns on watch mode (unless mode says otherwise). Runs with the same name compare with each other; the list is kept in your own Apify storage. Use one name per set of filters (e.g. "food-class-1"). Watch mode without a name uses the list "default".

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

Which FDA enforcement reports to search: food (incl. dietary supplements and cosmetics reported with food), drug, device. Leave empty to check only the recall numbers you list.

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

Only these classes (empty = all). Class I is the most serious (a reasonable chance of serious harm).

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

Only recalls with these statuses (empty = all).

## `daysBack` (type: `integer`):

Recalls whose date (see Date field) is within the last N days. Ignored when you set Date from or Date to.

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

Fixed start date (YYYY-MM-DD), instead of Days back.

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

Fixed end date (YYYY-MM-DD), instead of Days back.

## `dateField` (type: `string`):

Which date Days back / Date from / Date to apply to: reportDate (the date FDA published the enforcement report) or recallInitiationDate (the date the firm started the recall).

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

Only recalls by firms in these states (two-letter codes, e.g. CA, NY).

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

Only recalls by firms in these countries (as FDA writes them, e.g. United States, Canada).

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

A word or phrase to find in the product description or the reason for recall (e.g. peanut, listeria, pacemaker).

## `recallingFirm` (type: `string`):

Only recalls by this firm (a word or phrase of its name).

## `recallNumbers` (type: `array`):

Specific recalls to return or watch, one per line (e.g. H-0123-2026 food, D-0456-2026 drug, Z-0789-2026 device). Each gives one row; an unknown number gives a free no\_data row, an invalid one a free failed row.

## `includeUnchanged` (type: `boolean`):

Watch mode: also return recalls that did not change since the last run, as free rows (changeType unchanged).

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

Most charged rows (baseline, new, changed or exported recalls) per run. In watch mode, the rest come in the next run.

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

Optional: your own free openFDA API key (open.fda.gov/apis/authentication). It runs without one; for large or frequent runs, add your key. Without a key the limit is 1,000 requests a day per IP address, shared with other users of the same IP; with your key it is 120,000 a day. Stored as a secret; never logged or output.

## Actor input object example

```json
{
  "mode": "watch",
  "stateName": "example-watchlist",
  "productTypes": [
    "food"
  ],
  "daysBack": 30,
  "dateField": "reportDate",
  "includeUnchanged": true,
  "maxItems": 100
}
```

# Actor output Schema

## `dataset` (type: `string`):

Dataset with one row per returned recall (plus free no\_data / failed rows)

## `runReport` (type: `string`):

Summary of the run (counts, charged and free rows, stop reason)

# 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 = {
    "mode": "watch",
    "stateName": "example-watchlist",
    "includeUnchanged": true
};

// Run the Actor and wait for it to finish
const run = await client.actor("mouadapi/openfda-recalls").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 = {
    "mode": "watch",
    "stateName": "example-watchlist",
    "includeUnchanged": True,
}

# Run the Actor and wait for it to finish
run = client.actor("mouadapi/openfda-recalls").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 '{
  "mode": "watch",
  "stateName": "example-watchlist",
  "includeUnchanged": true
}' |
apify call mouadapi/openfda-recalls --silent --output-dataset

```

## MCP server setup

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

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/WJ9BYL6x3eLKIrjmd/builds/LEmLh1c1uVcar5qVk/openapi.json
