# openFDA Enforcement Recalls Scraper (`knotty_mistveil/openfda-enforcement-recalls`) Actor

Drug, device and food recall and enforcement reports from the official openFDA API: classification, firm, reason, dates, distribution.

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

## Pricing

$2.50 / 1,000 recall record returneds

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

## openFDA Enforcement Recalls Scraper

Pulls **drug, medical device and food recall records** from the FDA's own **openFDA enforcement-report API** (`api.fda.gov`), the public feed behind the FDA's weekly Enforcement Report. No API key, no account, no browser automation. You get one flat JSON record per recall with its classification (Class I/II/III severity), status, recalling firm and address, product description, reason for recall, distribution pattern, quantity, and the full timeline (initiated, classified, reported, terminated) — filtered by product type, a report-date window, keywords matched against the product description and reason for recall, US state, or FDA's own recall classification. This is a compliance and recall-monitoring feed: legal, insurance, retail and supply-chain teams buy it to watch which firms and products the FDA is pulling off the market.

### What you get

Every record contains:

| Field | Description |
| --- | --- |
| `id` | The FDA recall number as a string, e.g. `"D-1285-2020"` — globally unique across all three product types (prefixed `D-` drug, `Z-` device, `F-` food) — same value as `recallNumber`, present because every Actor in this series has an `id` |
| `source` | `openfda`, constant |
| `url` | A deep link into the openFDA API that returns this exact record (`.../enforcement.json?search=recall_number:"..."`). openFDA's enforcement reports have no stable public per-record web page — see *Data source and licence* |
| `productType` | `drug`, `device` or `food` — which endpoint this record came from |
| `recallNumber` | Same as `id` |
| `eventId` | FDA's internal recall-event id. Several `recallNumber`s (e.g. different lots or package sizes) can share one `eventId` when they were recalled together |
| `classification` | `Class I` (reasonable probability of serious harm or death), `Class II` (temporary/reversible harm) or `Class III` (unlikely to cause harm) |
| `status` | `Ongoing`, `Completed` or `Terminated` |
| `voluntaryMandated` | `Voluntary: Firm initiated` or `FDA Mandated` |
| `initialFirmNotification` | How the firm first notified the public/customers, e.g. `"Letter"`, `"Press Release"` |
| `recallingFirm` | Legal name of the firm that issued the recall |
| `address1` / `address2` / `city` / `state` / `postalCode` / `country` | Recalling firm's address. `state` is `null` for a non-US firm (FDA's own "N/A" sentinel is converted to `null`, not kept as the literal string) |
| `distributionPattern` | Where the product was distributed, FDA's own free text (can be a long list of countries for a device) |
| `productDescription` | What was recalled |
| `productQuantity` | Quantity recalled, e.g. `"33,280 bottles"`, or `null` when FDA has no quantity on file (`"N/A"` in about 10% of records — converted to `null`, not kept as text) |
| `reasonForRecall` | Why it was recalled |
| `codeInfo` / `moreCodeInfo` | Lot numbers, expiry dates, UPCs and similar identifying codes, FDA's own free text |
| `recallInitiationDate` | Date the firm began the recall, ISO `YYYY-MM-DD` |
| `centerClassificationDate` | Date FDA classified the recall's severity |
| `reportDate` | Date the recall was added to FDA's enforcement report — this is the date every input date filter matches against |
| `terminationDate` | Date FDA terminated the recall, or `null` while `status` is `Ongoing` |
| `openfdaBrandNames` / `openfdaGenericNames` / `openfdaManufacturerNames` / `openfdaSubstanceNames` / `openfdaProductNdcs` / `openfdaApplicationNumbers` | Supplementary drug identifiers openFDA sometimes links in from its own drug label/NDC databases (arrays, `[]` when not linked). Measured 5 October 2026 on a 1,000-record sample per endpoint: present on **16.8%** of drug records, **0%** of device and food records. None of these are GMDN codes — see *Data source and licence* |
| `fetchedAt` | ISO timestamp of the run that produced the record |

The dataset has four views: **Overview** (date, product type, classification, firm, product, status, state, link), **Recall reason** (recall #, product, reason, quantity, distribution), **Recalling firm** (full address, voluntary/mandated, notification method) and **Timeline and status** (initiated/classified/reported/terminated dates, event id).

### Input

| Input | Type | Default | Description |
| --- | --- | --- | --- |
| `productType` | string (enum) | `all` | `drug`, `device`, `food`, or `all` to query all three endpoints. |
| `dateFrom` / `dateTo` | string (`YYYY-MM-DD`) | none | Filters on `report_date`. Leave both empty to use `daysBack` instead. |
| `daysBack` | integer | `14` | Used only when `dateFrom`/`dateTo` are empty. Defaults to 14, not this series' usual 7, because openFDA's data updates weekly but can lag the calendar date by over a week — measured 5 October 2026, the newest `report_date` available was 12 days behind "today". A 14-day window reliably spans at least one update. |
| `keywords` | string\[] | none | Case-insensitive OR match against `productDescription` **or** `reasonForRecall`, e.g. `["lead","listeria"]`. Several keywords are ORed. |
| `states` | string\[] | all | Two-letter USPS codes for the **recalling firm's** state, e.g. `["CA"]`. A foreign firm has no US state and is excluded by this filter. |
| `classification` | string (enum) | none | `Class I`, `Class II` or `Class III`. Empty = every classification. |
| `maxItems` | integer | `20` | Stop after this many records; you are charged per record delivered. Defaults low (20, not this series' usual 500) because the default 14-day/all-types window already matches roughly 150 records nationwide (measured below), and a Store "Try it" run should be cheap and fast. |
| `apiKey` | string (secret) | none | Optional free openFDA key, raising your daily quota from 1,000 to 120,000 requests (the per-minute cap, 240, is identical either way). Get one at <https://open.fda.gov/apis/authentication/>. Falls back to the `OPENFDA_API_KEY` environment variable. The Actor works perfectly with no key at all. |

Bad input fails fast: an unparseable date, a reversed date range, an unknown 2-letter state code, or a `classification`/`productType` outside the allowed values stops the run with the validation message as its status, before any request is made.

Example — every Class I drug recall in the last 90 days:

```json
{ "productType": "drug", "daysBack": 90, "classification": "Class I", "maxItems": 200 }
```

Example — food recalls mentioning listeria or salmonella, any time in the last year:

```json
{ "productType": "food", "daysBack": 365, "keywords": ["listeria", "salmonella"], "maxItems": 500 }
```

Example — every device recall from California firms in the last 90 days:

```json
{ "productType": "device", "daysBack": 90, "states": ["CA"], "maxItems": 500 }
```

### Output example

A real record from a live run on 5 October 2026 (`{"maxItems": 2, "daysBack": 14}`), unedited:

```json
{
  "id": "D-0845-2026",
  "source": "openfda",
  "url": "https://api.fda.gov/drug/enforcement.json?search=recall_number%3A%22D-0845-2026%22",
  "productType": "drug",
  "recallNumber": "D-0845-2026",
  "eventId": "99654",
  "classification": "Class II",
  "status": "Ongoing",
  "voluntaryMandated": "Voluntary: Firm initiated",
  "initialFirmNotification": "Letter",
  "recallingFirm": "Safecor Health, LLC",
  "address1": "317 New Boston St",
  "address2": null,
  "city": "Woburn",
  "state": "MA",
  "postalCode": "01801-6231",
  "country": "United States",
  "distributionPattern": "Nationwide in the USA.",
  "productDescription": "Fluphenazine HCl Elixir, USP, 15 mg per 3 mL, Delivers: 10 mL, Oral Elixir, PAI, Alcohol 14% by volume, For Oral Use Only, Rx Only, Pkg by: Safecor Health, Woburn, MA 01801. NDC: 00121-0653-04",
  "productQuantity": null,
  "reasonForRecall": "Failed Stability Specifications",
  "codeInfo": "Lot#:26080483; Exp :03/13/2027 Lot#:25310327; Exp :08/12/2026 Lot#:25330414; Exp :08/26/2026 Lot#:25430340; Exp :11/22/2026",
  "moreCodeInfo": null,
  "recallInitiationDate": "2026-08-13",
  "centerClassificationDate": "2026-09-14",
  "terminationDate": null,
  "reportDate": "2026-09-23",
  "openfdaBrandNames": [],
  "openfdaGenericNames": [],
  "openfdaManufacturerNames": [],
  "openfdaSubstanceNames": [],
  "openfdaProductNdcs": [],
  "openfdaApplicationNumbers": [],
  "fetchedAt": "2026-10-05T03:01:43.323Z"
}
```

A drug record with openFDA's supplementary enrichment looks the same but carries non-empty `openfdaBrandNames`, `openfdaManufacturerNames`, `openfdaSubstanceNames` and `openfdaProductNdcs` (for example Teva's `D-1285-2020`, `openfdaApplicationNumbers: ["ANDA040422"]`). A terminated recall carries a real `terminationDate` instead of `null`. A foreign recalling firm (for example a Mexican hand-sanitizer recall, `D-0080-2021`) carries `state: null`, `postalCode: null` and `country` set to the real country.

Every run also writes a `SUMMARY` record to the key-value store with the filters used, how many recalls openFDA matched per product type, the API requests spent, and the `windowExhausted` completeness flag described below.

### Pricing

Pay per result: **US$0.0025 per recall record delivered** (US$2.50 per 1,000 records). You are charged only for records pushed to the dataset; a run that returns nothing, or that fails validation before its first request, costs nothing. Runs on the Apify free plan are limited by the platform's free-tier usage allowance.

### Data source and licence

- **Source.** `GET https://api.fda.gov/drug/enforcement.json`, `.../device/enforcement.json` and `.../food/enforcement.json` — the three openFDA enforcement-report endpoints operated by the US Food and Drug Administration, behind its weekly Enforcement Report. Documentation: <https://open.fda.gov/apis/drug/enforcement/>, <https://open.fda.gov/apis/device/enforcement/> and <https://open.fda.gov/apis/food/enforcement/> (read 5 October 2026). No key or account is required; every request in this Actor's testing and its smoke test succeeded unauthenticated.
- **Licence.** openFDA's own licence page states: *"Unless otherwise noted, the content, data, documentation, code, and related materials on openFDA is public domain and made available with a Creative Commons CC0 1.0 Universal dedication"* and *"You can copy, modify, distribute and perform the work, even for commercial purposes, all without asking permission"* (<https://open.fda.gov/license/>, read 5 October 2026).
- **GMDN caveat (does not apply to this feed).** The same licence page carries a separate restriction: *"Any data/information suppliers who wish to extract GMDN Content from this site in order to use it as a basis for creating and supporting commercial services ... must first obtain an appropriate license from The GMDN Agency. No such activity is permitted without a license."* GMDN (Global Medical Device Nomenclature) codes live in a different openFDA endpoint (`device/recall.json`), not the `device/enforcement.json` endpoint this Actor uses. Verified by inspecting 1,000 live device-enforcement records (5 October 2026): no GMDN field of any kind appears anywhere in this endpoint's response shape, on any product type, so no GMDN content is extracted or republished by this Actor.
- **Attribution.** No specific wording is mandated by the CC0 dedication. Cite it as "Source: openFDA" and link to <https://open.fda.gov>.
- **Not medical advice.** openFDA's own disclaimer applies to every response: *"Do not rely on openFDA to make decisions regarding medical care ... you should assume all results are unvalidated."* This Actor republishes the data as-is; it is a recall-monitoring and lead-generation feed, not a clinical or safety-and-soundness determination.
- **Personal data.** This dataset is about firms and products, not individuals — there are no named officers, employees or customers in the enforcement-report endpoints sampled while building this Actor.

### Limits and freshness

- **Volumes, measured 5 October 2026** against the live API: **17,988** total drug enforcement records, **40,057** device, **29,463** food (all-time, unfiltered). A 7-day `report_date` window on the most recent data then available (17–23 September 2026) held **11** drug, **88** device and **49** food recalls nationwide (**148** total).
- **Data lags "today" by more than a week.** Each endpoint's `meta.last_updated` read **2026-09-23** when queried on **2026-10-05** — a 12-day gap. openFDA's own API pages for all three endpoints state *"Frequency of API updates: Weekly"*, but the newest `report_date` on file is not guaranteed to be within the last 7 calendar days. This is why `daysBack` defaults to 14 here rather than this series' usual 7 — see the Input table.
- **Pagination: `limit` is capped at 1000, `skip` at 25000.** `limit=1001` answers HTTP 400 *"Limit cannot exceed 1000 results for search requests"*; `skip=25001` answers HTTP 400 *"Skip value must 25000 or less"* (both verified 5 October 2026). A page fetched at exactly `skip=25000` still returns a full `limit`-sized page, so the deepest record reachable by plain offset paging is index 25999 — **26,000 records per query** (per product type, per run). A query matching more than that cannot be read to its end this way; this Actor detects the case (`SUMMARY.perType.<type>.ceilingHit`) and warns rather than silently truncating. In practice this only matters for a very wide, unfiltered window — narrow `dateFrom`/`dateTo`, `keywords`, `states` or `classification` to stay well under it.
- **A zero-match query answers HTTP 404**, not an empty 200 page (`{"error":{"code":"NOT_FOUND","message":"No matches found!"}}`, verified 5 October 2026). This Actor treats that as a legitimate empty result, not a failure: a keyword, state or classification matching nothing costs nothing and exits cleanly.
- **Rate limits** (<https://open.fda.gov/apis/authentication/>, verified 5 October 2026): **240 requests/minute and 1,000 requests/day per IP** unauthenticated; **240/minute and 120,000/day per key** with the free optional key. The per-minute ceiling is identical either way, so this Actor paces every request at least 270ms apart (about 220/minute) regardless of whether a key is supplied, and retries 429/5xx responses with exponential back-off via `fetchWithRetry`.
- **`state` and the keyword fields are case-insensitive**, unlike some of this series' other registries: `state:"ca"` and `state:"CA"` matched the identical 1,111 drug records in testing, and `product_description`/`reason_for_recall` behaved the same way.
- **Every filter is applied by openFDA, not after the fact.** The date window, state, classification and keywords are all sent as `search=` query-string clauses, so `SUMMARY.matched` is openFDA's own count of the full result set and you are never charged for a record the Actor fetched and discarded. `SUMMARY.windowExhausted` is `true` only when every queried product type's full matched set was actually delivered; `maxItems`, the openFDA pagination ceiling, or the Apify spend limit each set it to `false`, and the run log plus `SUMMARY.perType` name which one.

### Use cases

- **Recall and compliance monitoring for legal, insurance and risk teams.** Filter by keyword or classification to watch for Class I recalls in a product category a client manufactures, distributes or insures.
- **Retail and supply-chain due diligence.** Filter by state or firm name before onboarding a supplier, or cross-check an existing vendor list against recent drug/device/food recalls.
- **Consumer-safety and journalism research.** Pull a keyword- or date-bounded slice (e.g. a specific contaminant or product category) to track how often and how severely a given issue recurs across firms.

### Support

Open an issue on the Actor's Issues tab on Apify Store. Replies within 14 days. Bug reports that name the run id and the input used are fixed fastest.

# Actor input Schema

## `productType` (type: `string`):

Which openFDA enforcement endpoint to query: drug, device or food recalls, or all three.

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

YYYY-MM-DD. Keep only recalls whose FDA report_date (the date the recall was classified and added to the enforcement report, not the day the problem happened) is on or after this date. Leave empty to use "Days back" instead.

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

YYYY-MM-DD. Keep only recalls reported on or before this date. Leave empty to use today (or "Days back" from it).

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

Used only when Date from/Date to are empty: look back this many days from today (or from Date to). Defaults to 14, not 7, because openFDA's enforcement data updates weekly but can sit for over a week before a given recall's report_date is actually current (measured 5 October 2026: the newest report_date available was 12 days behind the calendar date) - a 14-day window reliably covers at least one full update cycle. That window held 11 drug, 88 device and 49 food recalls nationwide (148 total) when measured 5 October 2026.

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

Case-insensitive OR match against the product description AND the reason for recall, e.g. \["lead", "listeria"]. Several keywords are ORed; each keyword is matched in either field. Empty = no keyword filter.

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

Two-letter USPS codes for the recalling firm's state, e.g. \["CA"], \["NY","TX"]. Several codes are ORed. A foreign recalling firm has no US state and is excluded by this filter. Empty = every state (including foreign firms).

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

FDA's recall-severity class: Class I (reasonable probability of serious harm or death), Class II (temporary or reversible harm) or Class III (unlikely to cause harm). Empty = every classification.

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

Stop after this many recall records. You are charged per record delivered, so this is also the cost cap. Defaults low (20, not this series' usual 500) because the default 7-day/all-types window already matches roughly 150 records nationwide, and a Store "Try it" run should be cheap and fast. Results are ordered by ascending report_date, then event_id, within each product type queried.

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

A free openFDA API key raises your daily quota from 1,000 to 120,000 requests (the per-minute cap, 240, is the same with or without a key) - get one at https://open.fda.gov/apis/authentication/. Leave empty to run unauthenticated; also falls back to the OPENFDA_API_KEY environment variable so an operator can test without pasting a key into the input.

## Actor input object example

```json
{
  "productType": "all",
  "daysBack": 14,
  "keywords": [],
  "states": [],
  "maxItems": 20
}
```

# Actor output Schema

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

All recall records returned by the run, one JSON object each. Append ?format=csv or ?format=xlsx to download a spreadsheet.

## `summary` (type: `string`):

Counts per product type, the filters used, how many recalls openFDA matched, the API requests spent and the completeness flag, written at the end of the run.

# 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 = {
    "productType": "all",
    "dateFrom": "",
    "dateTo": "",
    "daysBack": 14,
    "keywords": [],
    "states": [],
    "maxItems": 20
};

// Run the Actor and wait for it to finish
const run = await client.actor("knotty_mistveil/openfda-enforcement-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 = {
    "productType": "all",
    "dateFrom": "",
    "dateTo": "",
    "daysBack": 14,
    "keywords": [],
    "states": [],
    "maxItems": 20,
}

# Run the Actor and wait for it to finish
run = client.actor("knotty_mistveil/openfda-enforcement-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 '{
  "productType": "all",
  "dateFrom": "",
  "dateTo": "",
  "daysBack": 14,
  "keywords": [],
  "states": [],
  "maxItems": 20
}' |
apify call knotty_mistveil/openfda-enforcement-recalls --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,knotty_mistveil/openfda-enforcement-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/udhesbine1mi9pfCI/builds/hdawYXHSFaedivNpD/openapi.json
