# FDA Recall Scraper (`datalayer/fda-recall-enforcement`) Actor

The FDA gives the recall reason only as free text, so nobody can query it. This classifies it into root causes — undeclared allergen, sterility failure, nitrosamine impurity, software defect, component recall — and scores firms that keep failing the same way. 89.4% classified over 900 live recalls.

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

## Pricing

from $0.70 / 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/platform/actors/running/actors-in-store#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 Recall Scraper — Root Cause Classified

Scrape every **FDA drug, device and food recall** from openFDA, with the one
thing the FDA does not give you as a field: **why the product was actually
pulled**.

The FDA records the reason as a paragraph of free text. Every other scraper
hands that paragraph straight through, so you can filter by Class I but you
cannot ask *"show me every sterility failure this quarter"* or *"which firms keep
shipping undeclared allergens"* — which is the whole question a compliance,
quality or supply-chain team is trying to answer.

### What you get that a normal FDA scraper cannot give you

**Root cause, classified.** Seventeen categories derived from the reason text:

`undeclared_allergen` · `microbial_contamination` · `sterility_failure` ·
`foreign_material` · `particulate_matter` · `nitrosamine_impurity` ·
`chemical_contamination` · `impurity_degradation` · `potency_variance` ·
`dissolution_stability` · `spoilage_degradation` · `cross_contamination` ·
`product_mixup` · `formulation_error` · `specification_failure` ·
`mislabeling` · `packaging_defect` · `software_defect` · `device_malfunction` ·
`cgmp_deviation` · `unapproved_product` · `adulteration` · `component_recall`

**Measured 2026-08-14 over 900 live recalls: 89.4% classified** — drugs 100%,
food 98.0%, devices 70.3%. Devices are the weak case and honestly so: many
device narratives say only *"Firm X issued a recall for specific items and lots"*
and name no defect at all. Those stay `unclassified` rather than being guessed,
because a wrong category in a compliance dataset is worse than a gap.

**`component_recall` is worth calling out.** It marks a kit pulled because a
*supplier's* component was recalled, not because of the firm's own failure. If
you are mapping supply-chain exposure, that distinction is the point.

**Repeat-offender scoring.** Per firm: recall count, Class I share, distinct FDA
events, dominant root cause, and `isRepeatSameCause` — the firm failed the same
way at least twice. A firm that keeps failing identically has a systemic problem;
one that fails differently every time has bad luck.

**Blast radius.** `distribution_pattern` is also free text — `"CA, WA, OR."` or
`"Nationwide and Canada"`. It is parsed into `statesAffected`, `isNationwide` and
`isInternational`. Severity is the first question; how far it went is the second,
and the FDA does not answer it as a field.

**Quantity.** `product_quantity` reads `"2 cases (1 pc/bx, 48bx/cs)"`. The
leading count and unit are parsed out. The parenthetical breakdown describes
packing, not volume, so it is deliberately **not** multiplied out — that would
invent a number.

**Time to close.** `daysToClassification` and `daysToTermination`, computed from
the raw dates. Device enforcement records carry no termination date at all, so
that field is null for devices by design rather than by omission.

### Who uses this

- **Pharma, medtech and food quality teams** — benchmarking their own recall
  profile against competitors.
- **Supply chain and procurement** — screening suppliers before onboarding, and
  catching downstream component recalls.
- **Regulatory and compliance consultants** — quantifying enforcement trends by
  cause rather than by class.
- **Litigation and insurance analysts** — finding firms with a repeated,
  documented failure mode.

### Input

| Field | Type | Default | Notes |
|---|---|---|---|
| `productTypes` | array | `["drug","device","food"]` | Three independent endpoints |
| `classifications` | array | `[]` | `Class I` = serious injury or death |
| `rootCauses` | array | `[]` | Filter on the derived category — the FDA has no such field |
| `searchTerm` | string | `""` | Matched against reason and product description |
| `firm` | string | `""` | Restrict to one company |
| `state` | string | `""` | Two-letter state of the recalling firm |
| `status` | string | `""` | `Ongoing` recalls are still in the field |
| `fromDate` / `toDate` | string | `2025-01-01` / `""` | ISO dates, on the FDA report date |
| `maxRecordsPerType` | integer | `200` | openFDA pages at 1000, offsets to 25,000 |
| `minRecallsForSummary` | integer | `1` | Set to 2+ for firms with a pattern, not an incident |
| `apiKey` | string | `""` | **Optional.** Raises the shared 1,000/day/IP anonymous quota to 120,000 |

Root-cause filtering is applied **after** the firm rollup is computed, so
summaries always describe the firm rather than the filter.

### Output

**`recall`** — `classification`, `isClassI`, `rootCause`, `reasonForRecall`,
`recallingFirm`, `productDescription`, `status`, `statesAffected`,
`isNationwide`, `quantity`, `daysToTermination`, `isFdaMandated`, plus brand,
generic, NDC and device fields from the openFDA block.

**`firm_summary`** — `recalls`, `classIRecalls`, `classISharePercent`,
`distinctEvents`, `topRootCause`, `rootCauseBreakdown`, `isRepeatSameCause`,
`ongoingRecalls`, `medianDaysToTermination`, `nationwideRecalls`.

### Reliability

- **A query matching nothing returns HTTP 404**, not an empty list. That is an
  empty result set, not a failure — treating it as an error would turn "nothing
  was recalled this week" into a failed run. Handled.
- One product type failing never fails the run.
- Firm names are escaped before they reach openFDA's Lucene syntax, so a quote
  in a company name cannot break the query.

### Limits

- Only recalls the FDA has **classified**. Very recent recalls appear in FDA
  press releases before they appear here.
- `distinctEvents` matters: one FDA event often spans many recall numbers, so
  twenty rows can be one incident.
- Anonymous callers share 1,000 requests per day per IP. Add a free openFDA key
  if you run this at volume.

### Pricing

Pay per event. $0.001 per recall, $0.05 per firm summary.

# Actor input Schema

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

Which FDA enforcement endpoints to read. Each is independent — a gap in one does not affect the others.

## `searchTerm` (type: `string`):

Matched against the recall reason and the product description, e.g. insulin, catheter, peanut.

## `firm` (type: `string`):

Restrict to one company, e.g. Abbott. Matched against the recalling firm name.

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

Class I means a reasonable probability of serious injury or death. Leave empty for all three classes.

## `rootCauses` (type: `array`):

Filter by the category this actor derives from the free-text recall reason. This field does not exist at the FDA. Leave empty for all.

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

Two-letter US state of the recalling firm, e.g. CA. Leave empty for all.

## `status` (type: `string`):

Ongoing recalls are the ones still in the field.

## `fromDate` (type: `string`):

ISO date, e.g. 2025-01-01. Filters on the FDA report date. Leave empty for no lower bound.

## `toDate` (type: `string`):

ISO date. Leave empty for no upper bound.

## `includeRecalls` (type: `boolean`):

Return one row per recall. Turn off to get only the per-firm rollup.

## `includeFirmSummary` (type: `boolean`):

Return one row per recalling firm: recall count, Class I share, dominant root cause, repeat-offender flag.

## `minRecallsForSummary` (type: `integer`):

Set to 2 or more to see only firms with a pattern rather than a single incident.

## `maxRecordsPerType` (type: `integer`):

openFDA serves 1000 per page and allows paging to 25,000.

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

Not required. Anonymous callers share 1,000 requests per day per IP; a free key from open.fda.gov raises that to 120,000. Deliberately has no default: Apify rejects an input schema that gives a default to an isSecret field.

## `proxyConfiguration` (type: `object`):

Optional. openFDA is a public API and does not require a proxy.

## Actor input object example

```json
{
  "productTypes": [
    "drug",
    "device",
    "food"
  ],
  "searchTerm": "",
  "firm": "",
  "classifications": [],
  "rootCauses": [],
  "state": "",
  "status": "",
  "fromDate": "2025-01-01",
  "toDate": "",
  "includeRecalls": true,
  "includeFirmSummary": true,
  "minRecallsForSummary": 1,
  "maxRecordsPerType": 200,
  "proxyConfiguration": {
    "useApifyProxy": false
  }
}
```

# Actor output Schema

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

Every recall with the reason classified, blast radius and days to termination.

## `firms` (type: `string`):

Per-firm rollup: recall count, Class I share, dominant root cause and repeat-offender flag.

## `all` (type: `string`):

Recalls and firm summaries together.

## `csv` (type: `string`):

Everything as a CSV download.

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

Counts, root-cause coverage, and any endpoint that could not be reached.

# 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 = {
    "maxRecordsPerType": 200
};

// Run the Actor and wait for it to finish
const run = await client.actor("datalayer/fda-recall-enforcement").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 = { "maxRecordsPerType": 200 }

# Run the Actor and wait for it to finish
run = client.actor("datalayer/fda-recall-enforcement").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 '{
  "maxRecordsPerType": 200
}' |
apify call datalayer/fda-recall-enforcement --silent --output-dataset

```

## MCP server setup

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

```

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/k5Y5FohxHf33Aewlc/builds/Qy9qSU8y1K6Sievnh/openapi.json
