# FDA Warning Letter Monitor - Supplier & Closeout Changes (`maydit/fda-warning-letter-monitor`) Actor

Monitor FDA warning letters and response or closeout changes for supplier review. Bounded public windows, literal matches and saved comparisons. No compliance verdicts or letter-body scraping.

- **URL**: https://apify.com/maydit/fda-warning-letter-monitor.md
- **Developed by:** [Brandt May](https://apify.com/maydit) (community)
- **Categories:** Automation
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $1.80 / 1,000 warning letter observations

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

![FDA supplier and follow-up monitoring](https://api.apify.com/v2/key-value-stores/cCyrofyNNh9lwffXB/records/fda-warning-letter-monitor-663611836d73.png?signature=lq9S0ireTWbTMvF8hnWb)

## FDA Warning Letter Monitor

Turn FDA's warning-letter table into a repeatable supplier-review feed. Collect company names, published subjects, dates and original document links, then compare later observations to find new records and changed response or closeout links.

**Coverage:** one selected window of up to 100 FDA table records per run. Filters apply inside that window. This monitors published records; it does not decide whether a supplier is compliant, whether allegations are proven, or whether an issue remains unresolved.

### Start with five recent letters

```json
{
  "sortBy": "postedDate",
  "maxSourceRecords": 25,
  "maxResults": 5
}
```

Run the Actor, then open **Saved observations**, **Run report** or **CSV export** in Output. No API key, login or proxy setup is required. FDA currently requests a 30-second crawl delay; the Actor honors it before its single table request. Allow at least 90 seconds of platform timeout. Typical successful local checks take about 32 seconds; availability can vary.

**Cost:** $0.003 per saved letter observation at the FREE/BRONZE base rate, plus $0.00005 per Actor start. Five rows cost $0.01505 in Actor events. SILVER and GOLD+ rates are lower; check the Store's current Pricing tab for your plan. An unchanged observation is still a saved, charged row unless `onlyChanges` is enabled. Errors and skipped unchanged rows are not paid result rows. Platform charges, where applicable, are separate.

### What you get

- A spreadsheet-ready row per warning letter, with company, issuing office, subject, posted date and issue date.
- Distinct URLs and table-posted dates for a linked response and a linked closeout.
- Literal supplier-name or topic matches, with the matching field and a short excerpt.
- Saved comparisons: `new`, `changed` or `unchanged`, changed fields, prior values and prior observation time.
- A readable `RUN_REPORT` and machine-readable `SUMMARY` showing source bounds, filtering, saved rows and any errors or limits.

For example, this abbreviated record reflects an official closeout-window observation retained during development:

```json
{
  "companyName": "Talla Imports, LLC",
  "marcsCmsId": "699044",
  "postedDate": "2025-02-04",
  "letterIssueDate": "2025-01-13",
  "subject": "Foreign Supplier Verification Program (FSVP)",
  "hasPostedResponse": false,
  "hasPostedCloseout": true,
  "closeoutPostedDate": "2026-10-05",
  "comparisonStatus": "not_requested"
}
```

The complete row also includes the original warning and follow-up links, source request, observation time and comparison fields. `closeoutPostedDate` is the date shown in FDA's table; it is not an inferred date from the document URL or a current compliance status.

### Three ready-to-use workflows

#### 1. Recently posted warning letters

Use the quick-start input above. It reads the first 25 records in descending posted-date order and saves at most five. Default `{}` uses the same window and saves up to ten.

#### 2. Supplier-verification closeout updates

```json
{
  "sortBy": "closeoutPostedDate",
  "maxSourceRecords": 25,
  "keywords": ["Foreign Supplier Verification"],
  "maxResults": 5
}
```

This looks for FSVP subjects within the 25 most recent closeout-link dates. It returns the underlying warning-letter records and their follow-up links. Dated entries may be followed by records with no linked closeout if the requested window extends beyond them.

#### 3. A saved comparison for the recent window

```json
{
  "sortBy": "postedDate",
  "maxSourceRecords": 25,
  "maxResults": 100,
  "comparePrevious": true,
  "onlyChanges": false,
  "snapshotName": "fda-latest-window"
}
```

The first run establishes history after each row is confirmed saved. Run the same input again to inspect prior values. Set `onlyChanges` to `true` to save only new or changed observations; a fully unchanged run correctly returns zero rows.

For a supplier watchlist, add `companyNames`, for example `["Eurofins", "Hanlim"]`. Matching is a case-insensitive substring of the published company name, without entity resolution. A supplier outside the selected window will not be checked. The Actor does not search the complete archive for each supplier.

### Input reference

| Field | Meaning |
| --- | --- |
| `sortBy` | `postedDate` (default), `letterIssueDate`, `responsePostedDate` or `closeoutPostedDate`; descending publisher date. |
| `maxSourceRecords` | First 1–100 source records, default 25. One request; no pagination or full-archive claim. |
| `companyNames` | Up to 30 literal company-name substrings. Any name can match. Empty means no name filter. |
| `keywords` | Up to 30 literal terms across company, subject and issuing office. Any term can match. If names are supplied too, both filter groups must match. |
| `maxResults` | Confirmed saved rows, default 10, maximum 1,000. Cannot exceed the matching source window in practice. |
| `comparePrevious` / `onlyChanges` | Enable comparisons; the latter also skips unchanged rows. Both default false. |
| `snapshotName` | Comparison namespace, default `default`. Sort, window size and normalized filters also scope history. |
| `stateStoreName` | Dedicated named storage, default `maydit-fda-warning-letter-history`; accessed only for comparisons. |
| `maxRunSeconds` | Own budget, default 240, range 30–900, also bounded by platform timeout minus 25 seconds. A 30-second budget can expire during FDA's crawl delay. |

### Comparison and coverage rules

The stable row identity is `letterUrl`. The MARCS-CMS identifier is extracted only when it is present in that official URL; it is otherwise null. Comparison covers the company, dates, issuing office, subject and linked follow-up fields. Observation timestamps and match excerpts are excluded from change detection.

**`new` means first saved observation in this comparison scope, not first FDA publication.** FDA can return different records tied on a date at the source-window boundary, even when a secondary sort is requested. A repeat can therefore discover older, previously unseen records without a new FDA publication. Use `postedDate` and `letterIssueDate` for publisher chronology. Exact letter-URL watchlists are not supported.

History advances only for confirmed saved rows. Existing observations outside a later window remain stored. Nothing leaving a window is treated as a removal, withdrawal, resolution or clean bill of health. Use **one sequential writer per identical history scope**. The Actor rereads history before committing and refuses a known conflict, but this is not an atomic multi-writer store. Incompatible history is left untouched; choose a new namespace.

`sourceWindowHasMore` indicates records beyond the selected window. `sourceWindowComplete` means the requested window was successfully fetched and validated, not that the archive was complete. `stoppedOnMaxResults` is a deliberate output cap; `status: "ok"` can accompany that cap. Time, spending, history or source failures are separately marked as incomplete. Always inspect the report before treating an empty dataset as meaningful.

### Source context and limits

The source is [FDA's public Warning Letters table](https://www.fda.gov/inspections-compliance-enforcement-and-criminal-investigations/compliance-actions-and-activities/warning-letters), including the same public JSON table transport used by that page. This transport is not advertised as a versioned public API and may change. The Actor validates the entire returned window before saving rows and stops on inconsistent counts, links, dates or ordering.

FDA identifies warning letters as a resource for [firm and supplier evaluation under FSMA](https://www.fda.gov/food/food-safety-modernization-act-fsma/firmsupplier-evaluation-resources-fsma-rules). FDA also explains the distinction between [warning and closeout letters](https://www.fda.gov/inspections-compliance-enforcement-and-criminal-investigations/warning-letters/about-warning-and-close-out-letters). Subsequent interactions may change the regulatory status of matters in a letter. A missing published response does not show that a firm failed to respond. A closeout is a specific published FDA follow-up, not a universal supplier certification.

The Actor does not fetch letter bodies, attachments, contact details, inspection reports, import-alert lists or response/closeout document text. Source updates and posting can be delayed. This is published-record monitoring for human review, not legal or regulatory advice.

### Troubleshooting and support

- **No rows:** check the selected window, literal filters and `unchangedSkipped` before widening the source window.
- **Time limit:** use at least a 90-second platform timeout and keep enough time for FDA's crawl delay. Short budgets do not skip that delay.
- **403, 429, invalid source or robots refusal:** the Actor stops without retries or bypass. Retry later after reviewing `SUMMARY`; the failed request produces no paid result rows.
- **History conflict:** schedule sequential runs or assign a separate store/namespace. Do not reset another workflow's history.

For support, open an issue on this Actor's Store page with the run URL and a redacted input. Do not include credentials or private supplier notes.

# Actor input Schema

## `sortBy` (type: `string`):

Descending publisher date, with company name as the second requested sort. Response and closeout sorting still returns warning-letter rows with distinct follow-up links; blanks can follow dated entries.

## `maxSourceRecords` (type: `integer`):

Read the first 1-100 records in the chosen date order. Company and keyword filters apply only within this window. A missing match is not a clean supplier assessment.

## `companyNames` (type: `array`):

Optional literal, case-insensitive substrings; at least one must match the published company name. Up to 30 values of 120 characters. No fuzzy entity matching.

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

Optional literal, case-insensitive terms across company, subject and issuing office. Any keyword matches; when supplier names are also given, both groups must match. Up to 30 terms.

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

Confirmed dataset rows only. A result cap can leave matching source rows unsaved. Unchanged rows skipped by onlyChanges do not consume this limit.

## `comparePrevious` (type: `boolean`):

First saved observations are new; later runs show changed or unchanged fields and previous values. Only confirmed saved rows advance history.

## `onlyChanges` (type: `boolean`):

Also enables comparisons. An unchanged run can legitimately save zero rows. A row leaving this bounded window is never reported as removed or resolved.

## `snapshotName` (type: `string`):

1-80 letters, numbers, underscores or hyphens. The sort, source-window size and normalized filters are also part of the history scope. Use one sequential writer per scope.

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

3-63 letters, numbers or hyphens. Used only for comparisons. Choose a dedicated store for each workflow when isolation is needed.

## `maxRunSeconds` (type: `integer`):

Includes the publisher crawl delay, currently 30 seconds, and requests/history. Use at least 90 seconds of platform timeout. The Actor reserves 25 seconds before that platform deadline; a 30-second own budget may expire before results.

## Actor input object example

```json
{
  "sortBy": "postedDate",
  "maxSourceRecords": 25,
  "companyNames": [],
  "keywords": [],
  "maxResults": 10,
  "comparePrevious": false,
  "onlyChanges": false,
  "snapshotName": "default",
  "stateStoreName": "maydit-fda-warning-letter-history",
  "maxRunSeconds": 240
}
```

# Actor output Schema

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

Real source records only; one row per warning letter in this run.

## `runReport` (type: `string`):

Saved rows, bounds, source issues and history outcome.

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

Actual saved counts, source-window size, filtered rows and limits.

## `csvExport` (type: `string`):

Download saved observations for supplier-review spreadsheets.

# 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 = {
    "maxSourceRecords": 25,
    "maxResults": 10
};

// Run the Actor and wait for it to finish
const run = await client.actor("maydit/fda-warning-letter-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 = {
    "maxSourceRecords": 25,
    "maxResults": 10,
}

# Run the Actor and wait for it to finish
run = client.actor("maydit/fda-warning-letter-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 '{
  "maxSourceRecords": 25,
  "maxResults": 10
}' |
apify call maydit/fda-warning-letter-monitor --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,maydit/fda-warning-letter-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/xX99PEdyrZCzEpYOS/builds/v9jVohJpg7Q1yrpUK/openapi.json
