# FDA Warning Letters, Recalls & Import Alerts Monitor (`tenable_kaleidoscopist/fda-enforcement-monitor`) Actor

Get new FDA warning letters, recalls, enforcement reports and import alerts in one dataset. Watch suppliers by name or FEI. Schedule it daily.

- **URL**: https://apify.com/tenable_kaleidoscopist/fda-enforcement-monitor.md
- **Developed by:** [Northline Data Systems](https://apify.com/tenable_kaleidoscopist) (community)
- **Categories:** News, Automation
- **Features:** Limited permissions
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

Pay per usage

This Actor is paid per platform usage. The Actor is free to use, and you only pay for the Apify platform usage, which gets cheaper the higher subscription plan you have.

Learn more: https://docs.apify.com/actors/running/actors-in-store.md#pay-per-usage

## 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

## FDA Warning Letters, Recalls & Import Alerts Monitor

One run pulls new FDA warning letters, drug, device and food recalls, enforcement reports and import alerts into a single table with the same columns. Put your suppliers or competitors on a watchlist (by name or FEI number), turn on "only new," and schedule it daily. You get just the records you haven't seen yet, each with a link back to the FDA page.

**Three ways to use it**

- **Monitor:** new records from the sources you pick, filtered by company, product or keyword.
- **Firm check:** paste up to 25 firm names or FEI numbers and see every warning letter, recall, enforcement report and import alert listing that matches.
- **Import alert changes:** watch alerts such as 66-80 or 66-40 and get told when a firm is added to or removed from a list.

**Pricing:** the Actor itself is free. Like any Actor, a run uses Apify platform resources, which are billed to your account under your normal Apify plan.

### Sources

| Source key | Where it comes from | What you get |
|---|---|---|
| `warning_letters` | fda.gov Warning Letters listing | Company, issuing office, subject, letter date, posted date, link to the letter |
| `import_alerts` | FDA Import Alerts index (accessdata.fda.gov) | Alerts published or updated in your date range: number, type (e.g. DWPE), name, link |
| `drug_enforcement` | FDA Enforcement Reports for drugs (openFDA) | Recall number, firm, product, class, reason, status, dates |
| `device_enforcement` | FDA Enforcement Reports for devices (openFDA) | Same fields |
| `food_enforcement` | FDA Enforcement Reports for food (openFDA) | Same fields |
| `device_recalls` | CDRH Medical Device Recalls (openFDA) | Recall number, firm, FEI number, product, reason, status, dates, link to the FDA recall page |

#### Overnight window for accessdata.fda.gov

The robots.txt for accessdata.fda.gov asks crawlers to visit only between 11 PM and 5 AM US Eastern. The Actor follows that. Import alert pages (the `import_alerts` source, the `import_alert_changes` mode, and the live import alert check in `firm_check`) are read only from 23:00 to 05:00 America/New_York. Outside that window they are skipped, and the log, the status message and `RUN_SUMMARY` say so and give the time the window opens. openFDA and fda.gov sources can run at any hour. If you use import alerts, schedule the run for around 00:30 Eastern.

### Modes

Choose a mode with the `mode` input.

| Mode | What it does |
|---|---|
| `monitor` (default) | Returns recent records from the sources you select. |
| `import_alert_changes` | Watches the import alerts you list (for example 66-80, GLP-1 APIs, or 66-40, drug CGMP) and reports what changed since the previous run: firms added to or removed from a Red, Green or Yellow List, firms moved between lists, changes to a firm's product codes, country-wide product codes added or removed, and the alert being republished. |
| `firm_check` | Looks up firm names or FEI numbers across warning letters, import alert firm lists, drug, device and food enforcement reports, and device recalls. Each match includes the source, record ID, date and link. |

#### import_alert_changes

- The first run for an alert only saves a baseline and returns no rows for it. The log, status message and `RUN_SUMMARY` (`baseline_alerts`) note this. Changes show up from the second run on, so this mode is meant to be scheduled, for example daily.
- State is kept in a named key-value store in your account, `fda-import-alert-state`, with one record per alert. Delete a record to re-baseline that alert.
- Every change found is also written to the `LAST_CHANGES` record in the run's key-value store. Up to **Max records** of them go to the dataset.
- Inputs: `importAlertNumbers` (default `["66-80"]`, up to 10 per run), `maxItems`, and optionally `watchlist` and `onlyWatchlistMatches`.

```json
{ "mode": "import_alert_changes", "importAlertNumbers": ["66-80", "66-40"] }
```

#### firm_check

- Inputs: `firms` (up to 25 names or 7 to 10 digit FEI numbers), `maxResultsPerSource` (default 10), `importAlertNumbers` (default `["66-80", "66-40"]`), `checkWarningLetters`, `checkImportAlerts`, `maxItems`.
- Name matching ignores case, punctuation and suffixes such as Inc, LLC and Ltd. Every word in your query has to appear in the firm name FDA lists. FDA's warning letter search is full-text, so a letter is kept only when its company name matches.
- FEI numbers can be matched only where FDA publishes them: device recalls (`fei_number`) and the text of warning letters (`match_type: fei_in_letter_text`). Enforcement reports and import alert pages don't show FEI numbers.
- Import alert matches come from the alerts you list, read live, plus any alerts already saved by `import_alert_changes` runs. The snapshot time is given in `RUN_SUMMARY`.
- There is no date filter. Everything the sources return is searched, newest first.
- A per-firm summary, including firms with no matches, is saved to `FIRM_CHECK_SUMMARY`.

```json
{ "mode": "firm_check", "firms": ["Hims & Hers Health", "Babikian Healthcare", "1000517638"] }
```

These two modes add the fields `mode`, `change_type`, `alert_number`, `list_type`, `country`, `address`, `previous_value`, `products`, `matched_query` and `match_type`. The dataset has `changes` and `firmCheck` table views for them.

### Why this one

Most FDA actors cover one source. This one reads six (fda.gov warning letters, the import alert index, and four openFDA endpoints) and returns them in one schema. That means a single schedule and a single watchlist for all of it. It follows fda.gov's crawl-delay and only reads accessdata.fda.gov pages in the overnight window that site's robots.txt asks for, so import-alert runs should be scheduled for around 00:30 Eastern.

### Typical uses

- Supplier and competitor watch: put supplier names or FEI numbers on a watchlist and return only the matches.
- A daily compliance feed: schedule a run with **Only new records** on, and send the results to Slack, email, Google Sheets or a webhook through Apify integrations, Make or Zapier.
- Research: pull 30 to 365 days of warning letters or recalls for a product category.
- Data pipelines: one stable JSON schema with source links, ready for a database or dashboard.

### Input

| Field | Type | Default | What it does |
|---|---|---|---|
| `mode` | text | `monitor` | `monitor`, `import_alert_changes` or `firm_check` |
| `sources` | list | `warning_letters`, `food_enforcement` | Which of the six sources to read (monitor mode) |
| `lookbackDays` | integer, 1 to 365 | `7` | How many days back to look. Ignored when `dateFrom` is set |
| `dateFrom` / `dateTo` | date (YYYY-MM-DD) | none | Optional fixed range, matched against `posted_date` |
| `watchlist` | list of text | empty | Company names, products, FEI numbers or keywords, matched case-insensitively |
| `onlyWatchlistMatches` | true/false | `false` | Return only records that match the watchlist |
| `onlyNew` | true/false | `false` | Skip records that earlier runs already returned (see Limits) |
| `maxItems` | integer, 1 to 5000 | `25` | The most records one run returns |
| `openFdaApiKey` | secret text | none | Optional free openFDA key for heavy use |

Example:

```json
{
  "sources": ["warning_letters", "device_enforcement", "device_recalls"],
  "lookbackDays": 14,
  "watchlist": ["Medtronic", "CGMP"],
  "onlyWatchlistMatches": true,
  "onlyNew": true,
  "maxItems": 200
}
```

### Output

Each record has the fields below. Missing values are `null`.

| Field | Meaning |
|---|---|
| `source` | The source key the record is reported under |
| `sources` | Every feed the record was found in this run, e.g. `["device_enforcement", "device_recalls"]` |
| `record_id` | FDA's ID: recall number (D-, Z-, F-, H-), `WL-` + letter ID, or `IA-` + alert number. A `tmp-` ID when FDA hasn't assigned one yet |
| `id_is_provisional` | `true` when `record_id` is a temporary `tmp-` ID |
| `title` | Short human-readable title |
| `firm_name` | Company named by FDA (not given for alert-level import alert rows in monitor mode) |
| `fei_number` | FDA Establishment Identifier, published only in device recalls |
| `product_type` | Drugs, Devices, Food, etc. For warning letters it is derived from the issuing office |
| `classification` | Recall class (Class I/II/III), or the alert type (e.g. DWPE) for import alerts |
| `reason` | Recall reason or warning-letter subject |
| `status` | Recall status, or for warning letters whether FDA posted a response or closeout letter |
| `issuing_office` | FDA office or report name |
| `event_date` | When the underlying event happened (see Dates below). `null` for import alert rows in `monitor` and `firm_check` |
| `posted_date` | When FDA posted, reported or published it (see Dates below). Date filters and sorting use this field |
| `url` | Link to the official FDA source |
| `fetched_at` | When the Actor read it (UTC) |
| `watchlist_match` / `matched_terms` | Whether, and on which terms, the record matched your watchlist |

#### Dates: `posted_date` vs `event_date`

| Mode / source | `posted_date` | `event_date` |
|---|---|---|
| `warning_letters` (monitor, firm_check) | Date the letter was posted on fda.gov | Letter issue date |
| `drug/device/food_enforcement` (monitor, firm_check) | Enforcement report date (`report_date`) | Recall initiation date |
| `device_recalls` (monitor, firm_check) | Date FDA posted the recall (`event_date_posted`) | Recall initiation date (`event_date_initiated`) |
| `import_alerts` in `monitor` (alert level) | Alert's publish/update date on the import alert index | `null` |
| `import_alerts` in `firm_check` | The firm's "Date Published" on the alert | `null` |
| `import_alert_changes` | The firm's "Date Published" on the alert; for product or republish rows, the alert's publish date | Date (UTC) this Actor detected the change |

In `monitor` mode, `lookbackDays`, `dateFrom` and `dateTo` filter on `posted_date`, so a recall initiated months ago appears when FDA reports it. `firm_check` has no date filter. When a device recall is merged across feeds, the dates come from the enforcement report.

Enforcement-report records link to the openFDA API record, because FDA publishes no stable web page for each one. Merged device recalls link to the public CDRH recall page.

#### Example output record

```json
{
  "source": "device_enforcement",
  "record_id": "Z-0190-2027",
  "title": "Class II: The ABL90 FLEX and ABL90 FLEX PLUS analyzers are intended for use by trained technologists, nurses, physicians and therapists. It is intended for use in a labor",
  "firm_name": "Radiometer Medical ApS",
  "fei_number": "3002807968",
  "product_type": "Devices",
  "classification": "Class II",
  "reason": "In vitro diagnostic analyzer had a risk of potentially inaccurate results requiring changes to instructions for use and: 1) Sample types, including whole blood samples no longer suitable for testing FHbF and ctBil, 2) Reportable ranges for some parameters, 3) Analyzer configuration, and 4) limitations and warnings.",
  "status": "Ongoing",
  "issuing_office": "FDA Enforcement Report (via openFDA)",
  "event_date": "2026-08-18",
  "posted_date": "2026-10-21",
  "url": "https://www.accessdata.fda.gov/scripts/cdrh/cfdocs/cfRES/res.cfm?id=222446",
  "fetched_at": "2026-10-28T01:56:36+00:00",
  "watchlist_match": false,
  "matched_terms": [],
  "sources": [
    "device_enforcement",
    "device_recalls"
  ],
  "id_is_provisional": false
}
```

See the Output tab for more sample records from every source.

### Limits

- Duplicates are merged only within a run. A device recall found in both openFDA device enforcement and CDRH device recalls is returned once, with `sources` listing both feeds. Separate runs don't check against each other unless **Only new records** is on.
- **Only new records** remembers up to the 50,000 most recent IDs in a named key-value store in your account. Deleting that store resets it.
- openFDA sometimes publishes a recall before FDA assigns its number. Those records get a stable `tmp-` ID and `id_is_provisional: true`. When the real number is assigned later, the same recall can come back once under it.
- fda.gov and accessdata.fda.gov ask crawlers to wait 30 seconds between requests, and the Actor does. Each extra page of about 100 warning letters adds about 30 seconds. openFDA sources are fast.
- openFDA allows 1,000 requests per day per IP without a key. Add your own free key for heavy use.
- The Actor reads listings only. It does not download letter bodies or attachments. In `monitor` mode import alerts are reported at the alert level. `import_alert_changes` and `firm_check` read firm lists only on the alert pages you choose, 30 seconds apart, so one run does not cover all of the roughly 150 alerts.
- Some alert pages are large (66-41 is about 6.7 MB with about 1,900 firms). Some alerts, such as 66-80, list products country-wide rather than naming firms, so changes show up as product rows. Firm entries on alert pages carry no FEI number. Red List parsing was tested on live pages. Green and Yellow List headings are detected by their wording but did not appear on the pages tested.
- Crawl hours: accessdata.fda.gov's robots.txt has `Visiting-hours: 23:00EDT-05:00EDT`. The Actor applies it as 23:00 to 05:00 America/New_York local time (EDT in summer, EST in winter) and never reads accessdata.fda.gov outside it. There is no override. A run still going at 05:00 stops reading import alert pages and lists the ones it skipped. `import_alert_changes` started outside the window exits without reading anything or changing saved state. `firm_check` outside the window still checks openFDA, warning letters and import alert snapshots saved by earlier runs. Device recall links to the CDRH recall page (`res.cfm`) are links only and are never fetched.
- FDA publishes on its own schedule, and openFDA can lag fda.gov by several days. Coverage is not guaranteed to be complete. Check the linked FDA record before relying on a result.

### Data source and disclaimer

Data comes from the U.S. Food and Drug Administration. openFDA data is CC0 public domain (https://open.fda.gov/terms/), and fda.gov content is public domain under FDA's Website Policies. Records are reproduced as published, with a link and the time they were fetched.

This Actor is not affiliated with or endorsed by FDA, and its output is not legal, regulatory or medical advice.

Published by Northline Data Systems.

# Actor input Schema

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

monitor returns recent records from the selected sources (default). import_alert_changes reports firms and products added to or removed from the selected FDA import alerts since the previous run; the first run only saves a baseline. firm_check looks up firm names or FEI numbers across all sources.

## `sources` (type: `array`):

(monitor mode) Public FDA sources to read. warning_letters is the fda.gov Warning Letters listing. import_alerts is the Import Alerts index (alert level). drug, device and food enforcement are FDA Enforcement Reports via openFDA. device_recalls is CDRH device recalls via openFDA. fda.gov and accessdata.fda.gov are read 30 seconds apart, as their robots.txt asks. accessdata.fda.gov (import alerts) is read only from 11 PM to 5 AM US Eastern, per its robots.txt; outside that window it is skipped.

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

(monitor mode) How many days back from today (UTC) to include, by the date FDA posted the record. Ignored if 'Date from' is set.

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

Optional start date (YYYY-MM-DD), matched against the date FDA posted or reported the record.

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

Optional end date (YYYY-MM-DD). Defaults to today (UTC).

## `watchlist` (type: `array`):

Company names, products, FEI numbers or keywords. Case-insensitive match on firm, title, reason, FEI number and record ID.

## `onlyWatchlistMatches` (type: `boolean`):

Return only records that match at least one watchlist term.

## `onlyNew` (type: `boolean`):

Skip records that earlier runs of this Actor in your account already returned. IDs are remembered in a named key-value store. Useful for scheduled runs.

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

The most records this run returns.

## `firms` (type: `array`):

Firm names or FEI numbers (7 to 10 digits), one per line, up to 25 per run. Names are matched on normalized words, ignoring case, punctuation and suffixes such as Inc, LLC and Ltd. FEI numbers can only be matched in device recalls and in warning letter text; enforcement reports and import alert pages do not show FEI numbers.

## `importAlertNumbers` (type: `array`):

FDA import alert numbers, such as 66-80 (GLP-1 APIs) or 66-40 (drug CGMP Red List). Up to 10 per run; alert pages are read 30 seconds apart. Defaults to \["66-80"] for import_alert_changes and \["66-80", "66-40"] for firm_check.

## `maxResultsPerSource` (type: `integer`):

The most matches returned for each firm from each source.

## `checkWarningLetters` (type: `boolean`):

Search the fda.gov warning letter listing for each firm (one request per firm, 30 seconds apart).

## `checkImportAlerts` (type: `boolean`):

Check firm lists on the selected import alerts plus any alerts saved by import_alert_changes runs.

## `openFdaApiKey` (type: `string`):

Optional free key from open.fda.gov for higher openFDA rate limits. Not required.

## Actor input object example

```json
{
  "mode": "monitor",
  "sources": [
    "warning_letters",
    "food_enforcement"
  ],
  "lookbackDays": 7,
  "watchlist": [],
  "onlyWatchlistMatches": false,
  "onlyNew": false,
  "maxItems": 25,
  "firms": [],
  "maxResultsPerSource": 10,
  "checkWarningLetters": true,
  "checkImportAlerts": true
}
```

# Actor output Schema

## `records` (type: `string`):

One row per real-world record. Duplicates across feeds are merged; see the sources field.

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

No description

## `changes` (type: `string`):

Rows from mode import_alert_changes.

## `firmCheck` (type: `string`):

Rows from mode firm_check.

## `firmSummary` (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 = {
    "mode": "monitor",
    "sources": [
        "warning_letters",
        "food_enforcement"
    ],
    "lookbackDays": 7,
    "maxItems": 25
};

// Run the Actor and wait for it to finish
const run = await client.actor("tenable_kaleidoscopist/fda-enforcement-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 = {
    "mode": "monitor",
    "sources": [
        "warning_letters",
        "food_enforcement",
    ],
    "lookbackDays": 7,
    "maxItems": 25,
}

# Run the Actor and wait for it to finish
run = client.actor("tenable_kaleidoscopist/fda-enforcement-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 '{
  "mode": "monitor",
  "sources": [
    "warning_letters",
    "food_enforcement"
  ],
  "lookbackDays": 7,
  "maxItems": 25
}' |
apify call tenable_kaleidoscopist/fda-enforcement-monitor --silent --output-dataset

```

## MCP server setup

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