# US-FDA-monitor (`zeekr011/us-fda-monitor`) Actor

Query and monitor official U.S. FDA enforcement recalls for drugs, medical devices, and food with normalized, traceable Dataset results.

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

## Pricing

$4.00 / 1,000 result items

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?

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

## US FDA Recall Monitor

Query and monitor official U.S. Food and Drug Administration (FDA) enforcement
records for drugs, medical devices, and food. Get normalized, traceable rows
from the public [openFDA API](https://open.fda.gov/) without writing pagination,
filtering, or change-detection code.

Use it for compliance checks, product-safety research, pharmacovigilance
workflows, regulatory dashboards, and scheduled recall feeds. Each row keeps
the source identity, recall details, official API URL, change metadata, and a
stable idempotency key.

### Before you run

- For a one-off API call, use `lookup` or `export`. `monitor` is stateful and
  returns only new or changed rows after its snapshot has been established.
- Actor usage costs **$0.004 per result**, equivalent to **$4 per 1,000
  results**, under Pay Per Event pricing. Runs with zero results have no
  `result-item` usage charge. See the **Pricing** tab for the current terms.
- Data is returned from openFDA at fetch time. This Actor does not provide a
  real-time freshness guarantee or an SLA for upstream publication delays.
- Start with one product type and a small `maxResults`; `maxResults: 0` can
  traverse a large result set and is not an unlimited historical archive.

### What this Actor does

- Queries the official openFDA `drug/enforcement`, `device/enforcement`, and
  `food/enforcement` datasets.
- Filters by product type, recall number, recalling firm, FDA classification,
  status, state, keyword, and inclusive report-date windows.
- Normalizes FDA dates such as `YYYYMMDD` to `YYYY-MM-DD` and flattens selected
  brand and generic name arrays.
- Runs as a one-off current lookup, a bounded export, or an incremental monitor.
- Writes structured records to the run's Apify Dataset, with JSON, CSV, and
  Excel download views generated from that same Dataset.
- Keeps monitor fingerprints in a named Apify Key-Value Store and emits
  `ADDED` or `UPDATED` records for a stable query scope.

### Why teams use it

This is a source-first FDA data feed: clean enough for automation, explicit
enough for audit trails, and small enough to start with a single bounded query.
It uses the government's machine-readable endpoint rather than fragile browser
automation, while leaving the original FDA values available for downstream
review.

Typical use cases include:

- Monitoring Class I, Class II, or Class III enforcement activity.
- Checking a product category or recalling firm over a defined date window.
- Building a research or compliance dataset for a BI tool or data warehouse.
- Scheduling a recurring monitor that emits only newly seen or changed rows.

### Data source and scope

| Product type | Official endpoint | Data returned |
| --- | --- | --- |
| `drug` | [`api.fda.gov/drug/enforcement.json`](https://api.fda.gov/drug/enforcement.json) | Drug enforcement and recall records |
| `device` | [`api.fda.gov/device/enforcement.json`](https://api.fda.gov/device/enforcement.json) | Medical-device enforcement and recall records |
| `food` | [`api.fda.gov/food/enforcement.json`](https://api.fda.gov/food/enforcement.json) | Food enforcement and recall records |

The Actor reads public openFDA enforcement data. An upstream openFDA API key
is optional and can be supplied for higher limits; it is not included in
Dataset output. The Actor does not query adverse-event, labeling, approval, or
510(k) datasets, and it does not independently validate clinical, legal, or
product-safety claims.

The source is current at fetch time, but it is not a complete historical
archive owned by this Actor. Use an explicit `reportedSince`/`reportedUntil`
window for a reproducible historical export and retain the resulting Dataset
in your own workflow when long-term archival is required.

#### Request and resource notes

- Selected product-type endpoints are queried sequentially. A multi-type query
  can therefore make several paginated upstream request series.
- `maxResults` limits normalized records considered before the monitor diff is
  applied. In `monitor` mode, the number of emitted events can be lower or zero.
- `maxResults: 0` removes the Actor-side result cap but still respects the
  per-endpoint pagination ceiling; it does not fetch an unlimited archive.
- `apiKey` changes the openFDA upstream limit only. It is not an alternative to
  the Apify API token used to start a run or read a private Dataset.

### Run modes

| Mode | What it returns | State behavior |
| --- | --- | --- |
| `lookup` | Matching current source records as `CURRENT` | Does not read or change monitor state |
| `export` | A bounded current-source result set as `CURRENT` | Does not read or change monitor state |
| `monitor` | New and changed records as `ADDED` or `UPDATED` | Reads and updates the named Key-Value Store |

`lookup` is useful for a focused check, `export` for a defined report-date
window, and `monitor` for a recurring workflow. The default mode is
`monitor`; when it has no explicit `reportedSince`, the Actor uses
`lookbackDays` before today as the fetch window.

### Quick start

#### Apify Console

1. Open the Actor and select **Input**.
2. Choose `lookup`, `export`, or `monitor`.
3. Select one or more `productTypes` and add a precise filter.
4. Keep `maxResults` small for the first run, then click **Start**.
5. Open the run's Dataset or use the JSON, CSV, and Excel links in the Output
   tab.

#### Apify CLI

After installing and authenticating the Apify CLI, run a bounded lookup:

```bash
apify call <ACTOR_ID> \
  --input '{"mode":"lookup","productTypes":["drug"],"classification":"Class I","maxResults":10}' \
  --output-dataset
```

`<ACTOR_ID>` can be the Actor ID or an authorized Actor name. A valid query
with no matches completes successfully with zero Dataset items.

#### REST API

Start a run with a JSON body:

```bash
curl -X POST \
  -H "Authorization: Bearer $APIFY_TOKEN" \
  -H "Content-Type: application/json" \
  --data '{
    "mode": "lookup",
    "productTypes": ["food"],
    "keyword": "allergen",
    "reportedSince": "2025-01-01",
    "reportedUntil": "2025-12-31",
    "maxResults": 100
  }' \
  "https://api.apify.com/v2/acts/<ACTOR_ID>/runs?waitForFinish=60"
```

The response includes the run status and `defaultDatasetId`. Read the same
Dataset through the Apify API:

```text
GET https://api.apify.com/v2/datasets/<DATASET_ID>/items?format=json&clean=true
GET https://api.apify.com/v2/datasets/<DATASET_ID>/items?format=csv&clean=true&attachment=true
GET https://api.apify.com/v2/datasets/<DATASET_ID>/items?format=xlsx&clean=true&attachment=true
```

Use `limit` and `offset` for pagination. Keep `$APIFY_TOKEN` in an environment
variable; never put it in Actor input, source code, logs, or Dataset records.

#### API run lifecycle

The POST request starts an Apify run; it does not return Dataset rows directly.
If `waitForFinish=60` returns a non-terminal status such as `RUNNING`, keep the
returned `runId` and poll `GET /v2/actor-runs/<RUN_ID>` or use a Webhook. Read
`defaultDatasetId` after the run reaches `SUCCEEDED`. A successful empty Dataset
is valid; in `monitor` mode it can mean that no record changed, not that the
source had no matching records.

### Input API reference

The input is a JSON object. The [Input schema](./.actor/input_schema.json) is
the machine-readable contract used by the Console and API.

| Field | Type / default | Description |
| --- | --- | --- |
| `mode` | enum, `"monitor"` | `monitor`, `lookup`, or `export`. |
| `productTypes` | array; default `drug`, `device`, `food` | FDA enforcement datasets to query. Allowed values are `drug`, `device`, and `food`; the array must not be empty. |
| `keyword` | string | Partial terms matched locally across product description, recall reason, recalling firm, code information, and more-code information. All normalized terms must be present. |
| `recallNumber` | string | Exact recall number match after case and punctuation normalization. |
| `recallingFirm` | string | Case-insensitive partial recalling-firm match. |
| `classification` | enum | `Class I`, `Class II`, or `Class III`. |
| `status` | enum | `Ongoing`, `Completed`, or `Terminated`. |
| `state` | string | Firm state value, normally a two-letter U.S. state code such as `NJ`. |
| `reportedSince` | `YYYY-MM-DD` | Inclusive lower bound for `report_date`. |
| `reportedUntil` | `YYYY-MM-DD` | Inclusive upper bound for `report_date`. |
| `lookbackDays` | integer, `14` | Monitor-only lookback when `reportedSince` is omitted. Range: `0`–`3650`. |
| `maxResults` | integer, `500` | Maximum normalized records considered/emitted. Range: `0`–`25000`; in `monitor` the final event count can be lower. `0` removes the Actor-side cap within the per-endpoint pagination ceiling. |
| `emitInitialSnapshot` | boolean, `true` | In `monitor`, emit first-seen records as `ADDED`; set `false` to seed state without emitting the initial snapshot. |
| `monitorId` | string | Optional stable namespace for separate monitors sharing one state store. |
| `stateStoreName` | string, `"us-fda-monitor-state"` | Named Apify Key-Value Store used for monitor fingerprints. |
| `apiKey` | secret string | Optional openFDA API key for higher upstream limits. It is not written to output. |

#### Query behavior and validation

- Dates must be real calendar dates in `YYYY-MM-DD`; invalid dates such as
  `2026-02-30` are rejected.
- `reportedSince` cannot be after `reportedUntil`.
- Filters are sent to openFDA where supported and then applied again after
  normalization before `maxResults` is applied.
- Product types are queried sequentially. A multi-type query can therefore
  make one upstream request series per selected product type.
- The Actor paginates each endpoint in bounded batches and stops at its
  configured result limit. `maxResults: 0` does not mean an unlimited archive;
  openFDA and the Actor still impose pagination ceilings.
- A valid query with no matches returns `SUCCEEDED` with an empty Dataset.
  Invalid input, malformed upstream JSON, or an unrecoverable upstream error
  fails the run with an explanatory message.

### Example inputs

#### Current Class I drug recalls

```json
{
  "mode": "lookup",
  "productTypes": ["drug"],
  "classification": "Class I",
  "maxResults": 25
}
```

#### Historical food enforcement export

```json
{
  "mode": "export",
  "productTypes": ["food"],
  "reportedSince": "2015-01-01",
  "reportedUntil": "2015-12-31",
  "maxResults": 1000
}
```

#### Monitor a topic across all FDA product types

```json
{
  "mode": "monitor",
  "productTypes": ["drug", "device", "food"],
  "keyword": "listeria",
  "lookbackDays": 14,
  "maxResults": 500,
  "monitorId": "listeria-watch"
}
```

#### Valid empty result and invalid input

An exact lookup for a number that is not in the selected source is a valid
request and returns an empty Dataset:

```json
{
  "mode": "lookup",
  "productTypes": ["drug"],
  "recallNumber": "not-a-real-recall",
  "maxResults": 1
}
```

An input such as `{"reportedSince":"2026-02-30"}` is rejected before an
upstream request because the date is not a real calendar date.

For abnormal upstream behavior, the Actor retries bounded transient failures
such as `429`, `5xx`, network errors, and timeouts. Malformed JSON and other
unrecoverable responses fail the run with an explanatory error; they are not
silently converted to an empty Dataset.

### Output API reference

Each Dataset item is one normalized FDA enforcement record. Missing source
values are represented by an empty string, an empty array, or `null` for date
fields, according to the field contract.

#### Representative lookup output

The following is a trimmed example showing the output shape; the complete row
contains every field listed below. Hash values are placeholders for the actual
64-character values generated by the run.

```json
{
  "source": "fda",
  "sourceRecordId": "drug:D-1234-2026",
  "recordType": "recall",
  "changeType": "CURRENT",
  "changedFields": [],
  "productType": "drug",
  "recallNumber": "D-1234-2026",
  "title": "Example Drug 10mg Tablets",
  "productDescription": "Example Drug 10mg Tablets, 100-count bottle",
  "recallingFirm": "Example Pharma Inc.",
  "classification": "Class I",
  "status": "Ongoing",
  "reasonForRecall": "Failed dissolution specifications",
  "reportDate": "2026-01-15",
  "brandNames": ["Example"],
  "genericNames": ["Example drug"],
  "sourceUrl": "https://api.fda.gov/drug/enforcement.json?search=recall_number:D-1234-2026",
  "detectedAt": "2026-09-02T00:00:00.000Z",
  "contentHash": "<sha256>",
  "idempotencyKey": "<sha256>"
}
```

For `monitor`, the same content fields are accompanied by `ADDED` or
`UPDATED`; `changedFields` lists normalized fields that changed since the prior
snapshot. `CURRENT` is used by `lookup` and `export`.

#### Identity and monitor metadata

| Field | Type | Description |
| --- | --- | --- |
| `source` | string | Always `fda`. |
| `sourceRecordId` | string | Stable identity formed from product type and the FDA `recall_number` or `event_id`. |
| `recordType` | string | Always `recall` for this Actor. |
| `changeType` | string | `ADDED`, `UPDATED`, or `CURRENT`. |
| `changedFields` | string\[] | Normalized fields changed since the previous monitor snapshot; empty for `ADDED` and `CURRENT`. |
| `detectedAt` | ISO timestamp | Time this Actor produced the row. |
| `contentHash` | string | SHA-256 hash of the normalized source snapshot, excluding derived monitor metadata. |
| `idempotencyKey` | string | Stable key for monitor scope, source record, and content version. Use it to deduplicate retries. |

#### FDA record fields

| Field | Type | Description |
| --- | --- | --- |
| `productType` | string | `drug`, `device`, or `food`. |
| `recallNumber` | string | FDA recall number when supplied. |
| `eventId` | string | FDA event identifier when supplied. |
| `title` | string | Short title derived from the product description, capped at 240 characters. |
| `productDescription` | string | FDA product description. |
| `recallingFirm` | string | Firm initiating or named in the recall. |
| `city` | string | Firm city. |
| `state` | string | Firm state. |
| `country` | string | Firm country. |
| `classification` | string | FDA recall classification. |
| `status` | string | FDA recall status. |
| `voluntaryMandated` | string | FDA voluntary/mandated value. |
| `distributionPattern` | string | Reported distribution pattern. |
| `productQuantity` | string | Reported quantity. |
| `reasonForRecall` | string | FDA reason for recall. |
| `recallInitiationDate` | string or null | Normalized initiation date in `YYYY-MM-DD`. |
| `centerClassificationDate` | string or null | Normalized FDA center classification date. |
| `terminationDate` | string or null | Normalized termination date when supplied. |
| `reportDate` | string or null | Normalized FDA `report_date`. |
| `codeInfo` | string | Product lot, code, or identifying information. |
| `moreCodeInfo` | string | Additional code information. |
| `brandNames` | string\[] | Brand names from the source's `openfda` object when supplied. |
| `genericNames` | string\[] | Generic names from the source's `openfda` object when supplied. |
| `sourceUrl` | string | Official openFDA endpoint/query URL used for the record. |

#### Dataset and output links

The Actor pushes structured rows only. Apify derives these representations from
the same Dataset:

- JSON: `.../items?format=json&clean=true`
- CSV: `.../items?format=csv&clean=true&attachment=true`
- Excel: `.../items?format=xlsx&clean=true&attachment=true`

For non-empty runs, this Actor names the current Dataset
`us-fda-monitor-results-<runId>` after all rows are delivered. The Dataset ID
does not change, so the Output links remain valid. Empty runs are not renamed
into a long-lived named Dataset.

The other Actors in this product family do not automatically rename their
Datasets. If you use all three, retain the returned Dataset IDs immediately or
copy/name the results according to your account's retention policy.

### Monitoring and delivery semantics

In `monitor` mode:

1. Records are normalized and deduplicated by `sourceRecordId`.
2. The Actor compares the normalized content with the snapshot in
   `stateStoreName`.
3. New records become `ADDED`; changed records become `UPDATED` with
   `changedFields`.
4. Dataset delivery completes before the checkpoint is written.

This is safe at-least-once delivery. If Dataset delivery succeeds but the
checkpoint write fails, a later run may emit the same event again; downstream
consumers should deduplicate with `idempotencyKey`. Records that disappear from
a later rolling source window are not emitted as `REMOVED`, because absence may
reflect pagination or the selected date/filter scope rather than an official
withdrawal.

For scheduled monitoring, keep the same `monitorId`, `stateStoreName`, and
query scope. The rolling date calculated from `lookbackDays` is not itself used
to create a new state namespace every day.

The HTTP layer starts requests serially within one process, waits one second by
default between request starts, retries transient network errors, `429`, and
`5xx` responses with bounded exponential backoff and jitter, and honors
`Retry-After` when supplied. These locks and limits are process-local; they do
not provide account-wide rate limiting or cross-container exactly-once
delivery.

Do not interpret an empty `monitor` Dataset as a negative safety result. Use
`lookup` or `export` when the application needs the current matching records on
every call.

### Reliability and limitations

- openFDA fields and availability can change. The Actor preserves the source
  values it maps, but it does not create a complete FDA data warehouse.
- A broad multi-product or uncapped query can make many upstream requests and
  consume more runtime and platform resources. Start with one product type,
  one date window, and a small `maxResults`.
- The Actor reports FDA enforcement data; it does not determine whether a
  product is clinically safe, whether a remedy was completed, or whether a
  recall applies to a particular person or inventory item.
- Public data and API availability are subject to FDA/openFDA terms and
  operational limits. Review the current source guidance before operating at
  scale.

### Local development

Requirements: Node.js 20 or newer.

```bash
npm install
npm test
npm run build
apify validate-schema
```

`npm test` includes simulated `429`/`Retry-After`, `5xx`, timeout, rate-limit,
state-order, and delivery-failure cases. FDA currently has no real-source
`test:real` script in this package; use a bounded `apify run` or API smoke when
you need to verify a live current record, historical record, empty result, or
source response. Do not use an unbounded export for smoke testing.

### Attribution

This Actor consumes public FDA data through openFDA. Preserve `sourceUrl`,
`productType`, and `recallNumber` when passing records downstream so users can
inspect the authoritative source context.

# Actor input Schema

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

Choose whether to emit changes, current matches, or a historical export.

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

FDA enforcement datasets to query.

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

Partial terms matched against product description, recall reason, and recalling firm.

## `recallNumber` (type: `string`):

Exact FDA recall number, such as D-0769-2026.

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

Partial, case-insensitive recalling-firm name.

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

FDA recall classification.

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

Current FDA recall status.

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

Two-letter US state code.

## `reportedSince` (type: `string`):

YYYY-MM-DD. In monitor mode, omitted means lookbackDays before today.

## `reportedUntil` (type: `string`):

Inclusive report date in YYYY-MM-DD format.

## `lookbackDays` (type: `integer`):

Days before today used when monitor mode has no explicit start date.

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

Maximum records emitted; zero means all results available within the openFDA pagination ceiling.

## `emitInitialSnapshot` (type: `boolean`):

Emit all first-seen records during the first monitor run.

## `monitorId` (type: `string`):

Optional stable namespace for multiple independent monitors in one KVS.

## `stateStoreName` (type: `string`):

Named Apify Key-Value Store used for monitor fingerprints.

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

Optional secret key for higher openFDA limits.

## Actor input object example

```json
{
  "mode": "monitor",
  "productTypes": [
    "drug",
    "device",
    "food"
  ],
  "lookbackDays": 14,
  "maxResults": 500,
  "emitInitialSnapshot": true,
  "stateStoreName": "us-fda-monitor-state"
}
```

# Actor output Schema

## `json` (type: `string`):

No description

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

No description

## `excel` (type: `string`):

No description

# 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 = {};

// Run the Actor and wait for it to finish
const run = await client.actor("zeekr011/us-fda-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 = {}

# Run the Actor and wait for it to finish
run = client.actor("zeekr011/us-fda-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 '{}' |
apify call zeekr011/us-fda-monitor --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,zeekr011/us-fda-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/JLjYTcHHSLUTpqOOJ/builds/VljiffgutdydRBn0k/openapi.json
