# FDA Complete Response Letter Monitor & API (`automa-flow/fda-complete-response-letter-monitor`) Actor

Search and monitor FDA Complete Response Letters (CRLs) by company or NDA/BLA. Get structured letter text, provenance and typed new-letter alerts from the official openFDA API. A source outage is never reported as no letters.

- **URL**: https://apify.com/automa-flow/fda-complete-response-letter-monitor.md
- **Developed by:** [Vadim Bezrukov](https://apify.com/automa-flow) (community)
- **Categories:** Automation, Developer tools
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $10.00 / 1,000 watch target checkeds

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

## FDA Complete Response Letter Monitor & API

Watch companies and NDA/BLA application numbers for newly published FDA Complete Response Letters, or search the official openFDA CRL archive, without turning a source outage into “no letters”.

For regulatory-affairs and competitive-intelligence workflows, the Actor returns structured letters with OCR text, issuance dates and source provenance. Saved watch state distinguishes newly observed letters, historical additions and edits to previously seen records. Export JSON/CSV or pass typed events to your own webhook workflow. Coverage follows FDA's published archive and update cadence; this is not a real-time feed of every issued CRL.

The first monitor run for `NDA 210852` (the public default) publishes a truthful `BASELINE` even when **Changes only** is on. A later scheduled run of the same watchlist is the reason to run it again: it should stay quiet when the archive is unchanged, and it should never claim a 2019 letter was issued today merely because it appeared in the API today.

### Choose your workflow

| Goal | Settings | What you get |
| --- | --- | --- |
| Watch named sponsors | `monitor` + `companies` | `BASELINE` on the first run, then typed new/retroactive/updated letters |
| Watch NDA / BLA numbers | `monitor` + `applications` | Same contract, matched on `NDA 210852` including `/Original 1` variants |
| Look up the archive once | `search` + filters | Newest matching letters, no monitoring state |

Start from a ready-to-copy Task: [Monitor FDA CRLs for biotech companies](https://apify.com/automa-flow/fda-complete-response-letter-monitor/examples/monitor-fda-crls-for-biotech-companies), [Track CRLs by NDA / BLA application](https://apify.com/automa-flow/fda-complete-response-letter-monitor/examples/track-crls-by-nda-bla-application), or [Search the FDA Complete Response Letter database](https://apify.com/automa-flow/fda-complete-response-letter-monitor/examples/search-fda-complete-response-letter-database). Replace the public sample values with your own watchlist or search filters.

### Quick start

Default Store prefill:

```json
{
  "mode": "monitor",
  "monitorKey": "default",
  "applications": ["NDA 210852"],
  "changesOnly": true
}
```

On 2026-09-13 that application had six letters in the official archive. The first run returns six `BASELINE` rows for one watch charge (about $0.01005 total). Repeat exactly this input: an unchanged archive returns one `TARGET` row with `UNCHANGED`, for the same watch charge. Keep `monitorKey` stable when scheduling daily or weekly.

### Monitor new FDA Complete Response Letters

Set `mode` to `monitor`, keep `monitorKey` stable, and supply `applications` and/or `companies`. The Actor downloads the current official archive over HTTPS, matches each watch locally, and compares the result with the last successful observation in Key-Value Store. One bad watch does not wipe another watch’s baseline. A failed archive fetch leaves last-good state untouched.

Monitoring state persists across runs in the named store `fda-crl-monitor-state` in your Apify account. Keep the same `monitorKey` for repeat checks; use a different key for an independent watchlist or changed filtering scope. Run checks for the same key sequentially to avoid overlapping state writes. Incompatible saved state fails with `STATE_INCOMPATIBLE` and stays intact; use a new key to start over. Older builds saved state in each run's default store, so the first run after upgrading establishes a fresh baseline in the named store. Search does not open this store.

### Search the FDA CRL archive

Set `mode` to `search` and use application numbers, company names and optional letter-type, approval-status or issuance-date filters. `maxResults` caps delivered letters; lower it to control the bill. When that cap hides remaining matches, each delivered row sets `truncated` to true. Case-insensitive text filters and application suffixes are evaluated locally against the complete bounded archive. If that archive cannot be fetched (`ARCHIVE_TRUNCATED`), the search fails closed. Search does not write monitoring state.

For a small archive lookup, use:

```json
{"mode": "search", "filters": {"approvalStatuses": ["approved"]}, "maxResults": 5}
```

This returns up to five newest matching letters for at most $0.05005. Omit unused filter keys; dates must use `YYYY-MM-DD`.

Search delivers the newest matching letters first, including company searches. If every supplied watch is invalid, it returns only `INVALID_INPUT` rows without fetching the archive. Invalid application filters are rejected before source requests. Missing totals, totals changing between pages, and incomplete pagination are source failures, even for small archives.

### What data do you get?

Each successful letter row includes application numbers, letter type and issuance date, approval status, company name, FDA approver identity, full letter text, `file_name`, a stable `record_id`, `fingerprint`, `observed_at` / `scraped_at`, and `first_seen_at`. The separate company mailing-address and addressee fields are dropped; full OCR text may still contain those details as part of the public letter.

`letter_date` is the date printed on the letter. `first_seen_at` is when this Actor first stored that `record_id`. They are not interchangeable.

### Watch companies

Company matching is case-insensitive after punctuation is stripped. `Amgen Inc.` and `Amgen, Inc.` match. `Pfizer Inc.` does not match `Pfizer Ireland Pharmaceuticals`. There is no fuzzy or token-OR match. Curly apostrophes are normalized so `Dr. Reddy’s` and `Dr. Reddy's` are the same watch.

### Watch NDA / BLA applications

Provide `NDA 210852` or `BLA 761215`. Bare digits are rejected. A watch without a suffix matches `/Original 1` and `/S-05` variants of the same application number.

### Detect new and retroactively published CRLs

A letter whose `letter_date` is on or after the previous successful `meta.last_updated` is `NEW_CRL`. A letter that appears for the first time whose issuance date is earlier than that previous source date is `RETROACTIVE_CRL_ADDED`. If issuance date or source last-updated is missing, the Actor emits `NEW_SOURCE_RECORD` rather than guessing. Updates to an already stored `record_id` are `CRL_RECORD_UPDATED` with previous/current field values.

### Input

| Field | Purpose |
| --- | --- |
| `mode` | `monitor` (default) or `search` |
| `applications` | Up to 500 NDA/BLA/ANDA numbers |
| `companies` | Up to 200 sponsor names |
| `filters` | Optional letter type, approval status, issuance-date range |
| `monitorKey` | Independent watchlist namespace |
| `changesOnly` | After baseline, omit unchanged letters |
| `maxResults` | Search-mode delivery cap (1–1000) |
| `openfdaApiKey` | Optional; anonymous access is enough for normal use |

### Output

Dataset views: **New and changed CRLs**, **Complete Response Letters**, **Watch-target status**. `NO_RESULTS` is a verified empty match, including openFDA’s HTTP 404 `No matches found!`. `SOURCE_FAILED` is used for timeouts, 429/5xx after retries, HTML, invalid JSON, pagination corruption, archive collapse, a truncated full-archive snapshot (`ARCHIVE_TRUNCATED`) and records that cannot be normalized (`NORMALIZE_FAILED`). Those two statuses are never collapsed.

A `RUN_SUMMARY` record in the default Key-Value Store gives target counts, source request/retry counts, baseline/new/updated counts, Dataset row count and `sourceLastUpdated`.

`spendLimitReached` means the remaining budget could not cover a requested event. The Actor emits a `TARGET` status row with `PARTIAL` / `SPEND_LIMIT` instead of the omitted letters, and monitoring state does not advance. Previously delivered and charged checks remain successful. Each watch is charged only after all its output rows are saved. A Dataset write failure is not charged for that watch. An unexpected charge rejection after delivery fails the run and retains state.

### Scheduling and webhooks

Schedule the same Task daily or weekly with a fixed `monitorKey`. Dataset views only project columns; they do not hide rows. For a webhook or Dataset poll that should fire only on new letters, filter items where `record_type` is `CHANGE` and `change_types` contains `NEW_CRL`, `RETROACTIVE_CRL_ADDED` or `CRL_RECORD_UPDATED`. A later `changesOnly` run still writes one `TARGET` row with `UNCHANGED` per quiet watch, and the first run publishes `BASELINE`. A verified empty watch keeps last-good letter ids, so a later rematch is `UNCHANGED` rather than a false `NEW_CRL`. If an interrupted run already started Dataset or billing output, resurrection fails closed (`DELIVERY_UNCERTAIN`) and does not replay charges; start a new run for a fresh observation.

This is a copy-paste illustration, not a hosted integration promise.

1. Save the monitor JSON above as a Task. In Apify Schedules, use `0 6 * * *` in UTC for a daily 06:00 check and attach that Task.
2. Add an `ACTOR.RUN.SUCCEEDED` webhook pointing to your n8n/Make URL. Read `resource.defaultDatasetId` from the notification and fetch its items with your own Apify connection. Inspect `RUN_SUMMARY` first: a run can finish `SUCCEEDED` with only `UNCHANGED` watches, or fail after writing `SOURCE_FAILED` rows.
3. Keep Dataset rows where `record_type` is `CHANGE` and `change_types` contains `NEW_CRL`, `RETROACTIVE_CRL_ADDED` or `CRL_RECORD_UPDATED`.
4. Open `source_url` and the letter `file_name` for the human check. Do not treat a CRL as a trading or medical conclusion.

### API / MCP usage

Direct Actor MCP endpoint:

`https://mcp.apify.com?tools=automa-flow/fda-complete-response-letter-monitor`

After Store publication and indexing, anonymous MCP search can discover the Actor; execution requires the caller's authenticated access and budget. Hosted MCP execution has not yet been verified. Ask an agent to monitor `NDA 210852` and return new or retroactively added letters. The Actor requests limited Apify permissions for Dataset and state storage; source collection uses HTTPS to `api.fda.gov`, with no browser or source login.

```python
import os
from apify_client import ApifyClient

client = ApifyClient(os.environ["APIFY_TOKEN"])
run = client.actor("automa-flow/fda-complete-response-letter-monitor").call(
    run_input={
        "mode": "monitor",
        "monitorKey": "default",
        "applications": ["NDA 210852"],
        "changesOnly": True,
    },
    max_total_charge_usd=0.011,
)
for item in client.dataset(run["defaultDatasetId"]).iterate_items():
    print(item["status"], item.get("change_types"), item.get("application_numbers"))
```

### Pricing

Pay-per-event. Platform usage is included, not passed through. `apify-default-dataset-item` is off so Dataset rows are not billed twice.

| Event | When it is charged | Price |
| --- | --- | ---: |
| `apify-actor-start` | Automatic run start (512 MB = one event) | $0.00005 |
| `watch-target-checked` | Once per unique company or application successfully evaluated in monitor mode, including a verified empty match | $0.01 |
| `archive-record` | Once per delivered search letter | $0.01 |

Retries, invalid watches, source failures, duplicate watches, duplicate search record ids and change types have no custom charge. The platform start fee still applies to failed and empty runs. A verified empty monitor watch is charged once; an empty search has no archive-record charge. A first default run (`NDA 210852`) costs about $0.01005: start plus one watch check, even if six `BASELINE` letters are written. A later quiet run of the same watch costs the same. Search of 50 letters costs about $0.50005.

Set `maxTotalChargeUsd` to at least **$0.011** so the Store prefill can finish. That is a minimum budget, not a minimum bill.

Twenty watches cost $0.20005 per run (about $6.0015 for 30 daily runs). The input ceilings are 700 unique watches ($7.00005) or 1,000 search letters ($10.00005). The default $1 run cap covers 99 custom events plus start; raise it deliberately for larger jobs. Increasing `maxResults` alone does not raise that budget.

### Failure semantics

| Outcome | Meaning |
| --- | --- |
| `SUCCESS` | Verified letter or verified watch evaluation |
| `NO_RESULTS` | Official API answered; zero matching letters |
| `SOURCE_FAILED` | The source could not be verified, including `ARCHIVE_TRUNCATED` and `NORMALIZE_FAILED`. The Actor run fails after writing these rows. |
| `FAILED` | That watch could not be evaluated locally; others still run |
| `INVALID_INPUT` | That watch could not be parsed; others still run |
| `PARTIAL` | Letter delivery omitted because the remaining spend limit cannot cover it |

### Data source and limitations

Source: [openFDA Complete Response Letters](https://open.fda.gov/apis/transparency/completeresponseletters/) at `https://api.fda.gov/transparency/crl.json`. Observed 2026-09-13: 458 letters, `meta.last_updated` 2026-08-13, anonymous HTTPS JSON, no proxy or browser. Terms dedicate unmarked openFDA content to CC0 / public domain and allow commercial use. `file_name` is not unique; identity is `file_name` + `letter_date` + application numbers + `letter_type`. OCR text contains redactions and scan artifacts. The archive is incomplete relative to every CRL FDA has ever issued; FDA says it will keep adding letters and may publish historical batches.

This Actor is not a press-release catalyst feed. FDA warning-letter scrapers and ticker-mapped PDUFA calendars are different products.

### FDA / medical disclaimer

This Actor is not affiliated with, endorsed by, or an official product of the U.S. Food and Drug Administration. Do not rely on openFDA or this Actor for medical care, investment decisions, approval predictions or compliance conclusions. A Complete Response Letter is correspondence about an application in its then-current form. It does not by itself prove a product is unsafe, that a company is non-compliant, or that approval will fail or succeed. Assume results are unvalidated and can change when FDA updates the dataset.

### FAQ

**Do I need an openFDA API key?** No for normal watchlists. Anonymous use is currently 1,000 requests per IP per day. A free key raises that cap and is stored only as a secret input.

**Why did a 2019 letter show up as new?** If it was not in last-good state and its issuance date is before the previous source `last_updated`, the event is `RETROACTIVE_CRL_ADDED`, not “issued today”.

**Does empty output mean there are no CRLs?** Only a `NO_RESULTS` row after a successful source check means a verified empty match. `SOURCE_FAILED` means the check did not complete.

**Why is `truncated` true?** Search hit `maxResults` while more matching letters remain, or the search page itself was capped. A monitor or company-archive scan that cannot read the full archive fails instead of setting that flag.

**Will you score deficiencies or predict approvals?** No. That is out of scope for V1.

# Actor input Schema

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

monitor watches the supplied companies and application numbers against last-good state. search returns matching archive letters without writing monitoring state.

## `monitorKey` (type: `string`):

Stable name for this watchlist's last-good state. Independent keys keep separate baselines. 1-64 letters, digits, dots, underscores or hyphens.

## `applications` (type: `array`):

Exact application numbers to watch or search, for example NDA 210852 or BLA 761215. Type plus digits are required. A watch for NDA 210852 also matches NDA 210852/Original 1. Bare numbers are rejected. Maximum 500.

## `companies` (type: `array`):

Sponsor names to watch or search. Matching is case-insensitive after punctuation is stripped; Amgen Inc. and Amgen, Inc. match each other. There is no fuzzy or substring match. Maximum 200.

## `filters` (type: `object`):

Optional AND filters applied after company/application matching. Empty lists are ignored. Dates are letter issuance dates (YYYY-MM-DD), not the date FDA added the letter to the API.

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

Caps delivered archive rows in search mode. Monitor mode always evaluates the current official archive (hundreds of letters) so a watch cannot miss a letter. Minimum 1, maximum 1000.

## `changesOnly` (type: `boolean`):

Monitor mode: after the first successful run, omit unchanged letters. The first run always publishes BASELINE rows so the default Dataset is never empty. Unchanged later runs still write one TARGET row per watch so billing and status stay explicit.

## `openfdaApiKey` (type: `string`):

Optional. Anonymous access is enough for normal Actor use. A free key from open.fda.gov raises the daily cap. The key is never logged or written to the Dataset.

## Actor input object example

```json
{
  "mode": "monitor",
  "monitorKey": "default",
  "applications": [
    "NDA 210852",
    "BLA 761215"
  ],
  "companies": [
    "Bioprojet Pharma"
  ],
  "filters": {
    "letterTypes": [],
    "approvalStatuses": []
  },
  "maxResults": 100,
  "changesOnly": true
}
```

# Actor output Schema

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

No description

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

No description

## `letters` (type: `string`):

No description

## `targetStatus` (type: `string`):

No description

## `runSummary` (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",
    "monitorKey": "default",
    "applications": [
        "NDA 210852"
    ],
    "companies": [],
    "maxResults": 100,
    "changesOnly": true
};

// Run the Actor and wait for it to finish
const run = await client.actor("automa-flow/fda-complete-response-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 = {
    "mode": "monitor",
    "monitorKey": "default",
    "applications": ["NDA 210852"],
    "companies": [],
    "maxResults": 100,
    "changesOnly": True,
}

# Run the Actor and wait for it to finish
run = client.actor("automa-flow/fda-complete-response-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 '{
  "mode": "monitor",
  "monitorKey": "default",
  "applications": [
    "NDA 210852"
  ],
  "companies": [],
  "maxResults": 100,
  "changesOnly": true
}' |
apify call automa-flow/fda-complete-response-letter-monitor --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,automa-flow/fda-complete-response-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/HvIyBa429kGU7Wjgd/builds/yQQUfT4gdRjYYZfi9/openapi.json
