# FDA Recalls Scraper (Food Enforcement Data) (`steadfetch/fda-recall-monitor`) Actor

Track official FDA food enforcement recalls from openFDA: recall number, event, report date, classification, status, product, reason, recalling firm, distribution. For recall monitoring, food-safety alerts and building a recall database. Public CC0 data, clean JSON, pay per event.

- **URL**: https://apify.com/steadfetch/fda-recall-monitor.md
- **Developed by:** [Steadfetch](https://apify.com/steadfetch) (community)
- **Categories:** News, Other
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $5.00 / 1,000 recall delivereds

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

## steadfetch/fda-recall-monitor

Monitor official FDA food enforcement recalls through the openFDA API: recall number, event, report date, classification, status, product, reason, recalling firm, distribution and location as structured JSON. Public CC0 data, no API key, no browser, pay per event.

### What it does

Extract food and supplement recall records from the official openFDA food enforcement endpoint (`https://api.fda.gov/food/enforcement.json`). Filter by report-date window, FDA classification (Class I, Class II or Class III), US state and keyword, and receive one dataset item per recall. The source is a public JSON API, not a scraped web page: no login, no CAPTCHA and no headless browser.

The endpoint is key-free and publishes 29,485 food enforcement records at the time of writing (openFDA metadata, 2026-10-20). The data is released under CC0 and, per openFDA, does not contain personally identifiable information. The Actor only emits company and product fields and deliberately drops the firm street address and postal code present in the raw record, so no person-level or private-address data is returned.

Monitor mode is on by default. The Actor remembers the recall numbers already delivered for the same classification/state/keyword stream in its key-value store, and only pushes and charges recalls it has not delivered before. Set `monitorMode: false` for a one-off historical backfill.

**Input:** `since` (`YYYY-MM-DD`, default 30 days before `until`/today), `until` (`YYYY-MM-DD`, default today), `classification` (`Class I`, `Class II`, `Class III`), `state` (two-letter code), `keyword`, `maxResults` (default 1,000, maximum 5,000), `monitorMode` (default true) and `strictSchema` (default true).

**Output:** one dataset item per recall with `recall_number`, `event_id`, `report_date`, `classification`, `status`, `product_description`, `reason_for_recall`, `recalling_firm`, `distribution_pattern`, `product_quantity`, `voluntary_mandated`, `city`, `state`, `country` and `source_url`, plus the common `url` and `scrapedAt` envelope. Format: JSON dataset.

### Example output

Real record from the committed fixture, which was saved from the official API on 2026-10-20:

```json
{
    "recall_number": "H-1407-2026",
    "event_id": "99887",
    "report_date": "2026-10-07",
    "classification": "Class I",
    "status": "Ongoing",
    "product_description": "Todd's Seeds Salad Sprouting Mixture - Broccoli, Mustard, Alfalfa and Cabbage, packaged in heat-sealed resealable bags in the following sizes: 1/4 lb., 1/2 lb., and 1 lb.",
    "reason_for_recall": "Products contain alfalfa seed recalled by the supplier and may be contaminated with E. coli and/or Salmonella (downstream recall)",
    "recalling_firm": "TODD'S SEEDS LLC",
    "distribution_pattern": "Nationwide; through Amazon or direct to consumer",
    "product_quantity": "37,481 retail units (TOTAL)",
    "voluntary_mandated": "Voluntary: Firm initiated",
    "city": "Cookeville",
    "state": "TN",
    "country": "United States",
    "source_url": "https://api.fda.gov/food/enforcement.json?search=recall_number:%22H-1407-2026%22",
    "url": "https://api.fda.gov/food/enforcement.json?search=recall_number:%22H-1407-2026%22",
    "scrapedAt": "2026-10-20T08:00:00.000Z"
}
```

Missing optional values are `null`; incomplete records that lack a required field (recall number, valid report date, classification, product description or recalling firm) are dropped instead of filled with made-up values.

openFDA occasionally publishes a recall as `Not Yet Classified`. That value is passed through unchanged instead of being guessed or mapped to a class; the `classification` input only filters the three final classes (`Class I`, `Class II`, `Class III`).

### Pricing

Pay-per-event pricing (`.actor/pay_per_event.json`, referenced from `actor.json` as `payPerEvent`):

| Event | Charged when | Price |
| --- | --- | --- |
| `apify-actor-start` | a run starts (synthetic, automatic) | 0.00005 USD per run (Apify-recommended default; includes 5 seconds of free compute) |
| `apify-default-dataset-item` | a validated recall is saved to the dataset (synthetic, automatic) | 0.005 USD per recall = **5.00 USD per 1,000 recalls** |

There is no custom per-item event and no `Actor.charge()` call: the platform already charges `apify-default-dataset-item` per pushed item, so adding another event would bill users twice. Failed requests, retries, empty results and recalls already delivered in monitor mode are not charged per recall.

The price sits inside the direct-competitor band for this source (about 8-50 USD per 1,000 events) and inside Steadfetch's 1-10 USD band, entering below the competition.

#### Cost per 1,000 results

The Actor uses plain HTTP and JSON: no browser and no proxy. Local measurement on 2026-10-20 over a 1,000-result request: one API request, about 1.5 seconds of wall time and 7 milliseconds of parsing, yielding 999 validated recalls (one source record had an empty recall number and was dropped). At 256 MB and Apify's USD 0.20 per GB-hour compute rate, that is **under 0.01 USD per 1,000 results**, leaving a gross margin above 99 percent. The first paid platform run will replace this local measurement with the billed figure.

### Monitor mode and dedupe

- Recalls are keyed by `recall_number`, the FDA identifier for the enforcement record. Duplicates inside one response are removed before validation.
- With `monitorMode: true` (default), the Actor reads its key-value store, skips recall numbers already delivered for the same classification/state/keyword stream and updates the store only with recalls it actually pushed.
- Date bounds are not part of the stream identity, so moving the default 30-day window forward keeps recognising earlier deliveries.
- A different classification, state or keyword is a different stream and starts with its own history.
- `monitorMode: false` ignores and does not update the store, which is what you want for a complete historical backfill.
- No dataset item means no per-result charge; the automatic `apify-actor-start` event still applies once per run.

### Input reference

Minimal run:

```json
{
    "since": "2026-09-01",
    "maxResults": 10
}
```

Filtered monitor:

```json
{
    "since": "2026-09-01",
    "until": "2026-10-20",
    "classification": "Class I",
    "state": "FL",
    "keyword": "allergen",
    "maxResults": 1000
}
```

| Field | Type | Description |
| --- | --- | --- |
| `since` | string | Inclusive `YYYY-MM-DD` lower bound for `report_date`. Defaults to 30 days before `until`/today. |
| `until` | string | Inclusive `YYYY-MM-DD` upper bound for `report_date`. Defaults to today (UTC). |
| `classification` | string | One of `Class I`, `Class II`, `Class III`. |
| `state` | string | Two-letter US state code of the recalling firm, e.g. `FL`. |
| `keyword` | string | Case-insensitive match against product description, reason for recall and recalling firm. |
| `maxResults` | integer | Default 1,000, maximum 5,000. |
| `monitorMode` | boolean | Default `true`; only emit and charge new recalls. Disable for a backfill. |
| `strictSchema` | boolean | Default `true`; fail the run on a schema violation. |

### FAQ

**Do I need an openFDA API key?**
No. The food enforcement endpoint is key-free. The Actor sends only plain HTTP requests to the official host.

**Does it need a browser or a proxy?**
No. openFDA returns structured JSON, so plain HTTP is enough and roughly 10 times cheaper than a browser-based Actor.

**When am I charged?**
Once per run start (`apify-actor-start`) and once per validated recall saved to the dataset (`apify-default-dataset-item`). Failed requests, retries, empty results and already-delivered recalls are not charged per result.

**What happens when there are no matching recalls?**
openFDA returns `NOT_FOUND`; the Actor treats it as an empty result, exits successfully and pushes nothing. Only the automatic start event applies.

**Does monitor mode keep charging the same recalls?**
No. Recalls already delivered for the same classification/state/keyword stream are skipped and not pushed, so they are not charged again.

**Does it return personal data?**
No. Only product and company-level fields are emitted. The raw record's street address and postal code are dropped, and no person-level fields are requested.

**Why does a run return fewer items than `maxResults`?**
The report-date window or filters may match fewer records, or an incomplete source record may have been dropped. The Actor never invents values to fill a gap.

**Is the output schema stable?**
Yes. New fields are additive and the required fields in `.actor/dataset_schema.json` do not change.

### Maintenance

**Maintenance status:** the Actor is registered for daily catalog QA and is tested daily once published. The label `Tested daily. Last verified: {YYYY-MM-DD}.` will be published only after the first successful scheduled run is logged. Until then no verified date is claimed.

The source is the versioned official openFDA API. Committed fixtures and schema tests catch response-shape changes in CI, and any break in the daily run is reported as an issue in the Steadfetch Actors repository.

### Keywords

fda recall, food recall, openFDA, food enforcement, recall monitor, product recall, allergen recall, compliance data, supply chain risk, JSON dataset

### Development

```bash
npm install
npm test                # lint + typecheck + offline Vitest suites (no Apify credit)
npm run publish:check   # Steadfetch publish gate
npm run build && npm run verify:build   # production entrypoint guard
npm run schemas:validate
npm run smoke           # local Apify run (no platform credit)
```

The default test suite runs against a committed real API response and never touches the network or the Apify platform. `src/extract.ts` is pure parsing (no network, no Apify imports), `src/fda-client.ts` is the key-free HTTP client with retries, and `src/monitor-state.ts` holds the cross-run dedupe logic.

Project structure:

```
.actor/actor.json          Actor definition (payPerEvent, dataset schema, output schema)
.actor/input_schema.json   INPUT options (date window, classification, state, keyword, maxResults, monitorMode)
.actor/dataset_schema.json Item JSON Schema (fields) + table view for the Store
.actor/pay_per_event.json  PPE pricing (two synthetic platform events)
src/fda-client.ts          openFDA HTTP client: query building, pagination, retries
src/extract.ts             Pure normalization and filtering (fixture-tested, no network)
src/monitor-state.ts       Cross-run dedupe by recall_number per filter stream
src/main.ts                Entry point: fetch, validate, dedupe, push, save state
src/validate-output.ts     Ajv validator shared by the run and the tests
test/                      Offline fixtures and Vitest suites
```

# Actor input Schema

## `since` (type: `string`):

Inclusive lower bound for the recall report date (YYYY-MM-DD). Defaults to 30 days before until/today.

## `until` (type: `string`):

Inclusive upper bound for the recall report date (YYYY-MM-DD). Defaults to today (UTC).

## `classification` (type: `string`):

Only return recalls with this FDA classification.

## `state` (type: `string`):

Only return recalls whose recalling firm is in this two-letter US state code.

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

Case-insensitive keyword matched against product description, reason for recall and recalling firm.

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

Maximum number of recalls delivered to the dataset.

## `monitorMode` (type: `boolean`):

When enabled, recalls already delivered by a previous run with the same classification/state/keyword filters are not pushed or charged. Disable for a full backfill.

## `strictSchema` (type: `boolean`):

Fail the run when an item does not match the output schema. Keep enabled: Steadfetch never delivers unvalidated data.

## Actor input object example

```json
{
  "since": "2026-09-01",
  "until": "2026-10-20",
  "state": "FL",
  "keyword": "allergen",
  "maxResults": 10,
  "monitorMode": true,
  "strictSchema": true
}
```

# Actor output Schema

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

Dataset items validated against the item schema in dataset_schema.json (fields).

# 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 = {
    "since": "2026-10-01",
    "maxResults": 10,
    "monitorMode": false
};

// Run the Actor and wait for it to finish
const run = await client.actor("steadfetch/fda-recall-monitor").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 = {
    "since": "2026-10-01",
    "maxResults": 10,
    "monitorMode": False,
}

# Run the Actor and wait for it to finish
run = client.actor("steadfetch/fda-recall-monitor").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 '{
  "since": "2026-10-01",
  "maxResults": 10,
  "monitorMode": false
}' |
apify call steadfetch/fda-recall-monitor --silent --output-dataset

```

## MCP server setup

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

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/uZqnQyJTf9R8wmUyg/builds/f2ABFoTV2TTZGM9ct/openapi.json
