# FDA Drug Establishment Registration & Supplier Change Monitor (`automa-flow/fda-drug-establishment-monitor`) Actor

Bulk-check drug manufacturing suppliers against FDA DECRS current and excluded registration files. Match exact FEI, DUNS or normalized firm names; monitor disappearance, reappearance, annual expiration, operation, name and address changes without turning source failures into false absences.

- **URL**: https://apify.com/automa-flow/fda-drug-establishment-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

$10.00 / 1,000 resolved supplier checks

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 Drug Establishment Registration & Supplier Change Monitor

Monitor FDA drug establishment registration status across your supplier portfolio.

Bulk-check up to 5,000 pharmaceutical manufacturing establishments against the official FDA Drug
Establishments Current Registration Site (DECRS), then rerun the same watchlist to detect
registration disappearance, reappearance, annual expiration, business-operation, firm-name and
address changes. One official bulk snapshot serves the whole batch: no FDA login, browser, proxy
or one-request-per-supplier loop.

This Actor checks an **administrative establishment-registration publication**. It is not an FDA
approval checker and does not prove product approval, GMP compliance, import admissibility,
inspection outcome, safety, quality or FDA endorsement.

### Use from AI agents with MCP

With access to this Actor, connect it directly at
`https://mcp.apify.com?tools=automa-flow/fda-drug-establishment-monitor`. Ask your
agent to check a supplier portfolio and report CURRENT, EXCLUDED, NOT\_FOUND, or
source failure without converting an unverified response into a compliance
claim.

#### Primary workflows

1. Verify drug manufacturing suppliers in bulk.
2. Monitor supplier registration daily or weekly.
3. Detect facilities disappearing from the current DECRS publication.
4. Watch annual registration expiration.
5. Detect business-operation, name and address changes.
6. Feed supplier observations and changes into webhooks, ERP, procurement or compliance systems.

#### Quick start

```json
{
  "establishments": [
    {
      "fei": "0000000360",
      "duns": "271408412",
      "name": "DSP",
      "reference": "supplier-fr-001"
    },
    {
      "duns": "039106281",
      "reference": "supplier-us-001"
    },
    {
      "name": "EUROAPI FRANCE",
      "reference": "manual-name-review"
    }
  ],
  "mode": "snapshot",
  "dueSoonDays": 90,
  "includeAddress": true
}
```

The Dataset always contains one reconciliation row per input. In this example the exact name
`EUROAPI FRANCE` is intentionally useful: FDA currently publishes two exact normalized matches,
so that row is `AMBIGUOUS`; the Actor never silently picks one.

### Exact matching rules

The priority is:

```text
FEI exact -> DUNS exact -> normalized firm-name exact
```

- FEI spaces, hyphens and leading zeroes are normalized for lookup.
- DUNS must resolve to exactly nine digits.
- Firm names normalize Unicode, case, punctuation and repeated whitespace. Accents remain
  significant; no fuzzy, phonetic or AI match is performed.
- If FEI returns multiple records and the input also provides DUNS, DUNS may disambiguate that FEI
  group. Otherwise the result is `AMBIGUOUS`.
- If exact identifiers conflict, the Actor returns `AMBIGUOUS` instead of trusting one silently.

Live source research on 2026-09-02 found 281 duplicated FEI values in the current file and one FEI
reused between unrelated current and excluded rows. DUNS was unique within the live current file.
For that reason a resolved row uses `fei:<FEI>|duns:<DUNS>` as its stable `source_id` when both are
published. FEI remains the primary identity component without being falsely presented as globally
unique.

### Truthful status semantics

| `match_status` | `registration_status` | Meaning |
| --- | --- | --- |
| `MATCHED` | `CURRENT` | One exact supplier record is positively present in the successfully validated current DECRS file. |
| `MATCHED` | `EXCLUDED` | One exact record is positively present in FDA's excluded file. FDA publishes no reason field, so the Actor asserts no cause. |
| `NOT_FOUND` | `NOT_CURRENT` | Neither validated file contains an exact match. This does not mean enforcement, non-compliance or a ban. |
| `AMBIGUOUS` | `UNKNOWN` | Multiple exact plausible records or conflicting identifiers; no fuzzy selection was made. |
| `FAILED` | `UNKNOWN` | Required source or processing state was not verified. This is never converted to absence. |
| `INVALID_INPUT` | `UNKNOWN` | This input item is invalid; other items continue. |

FDA says current establishments can disappear because a registration is inactivated for a
compliance/enforcement case, expires, is deregistered or is otherwise dropped from submission.
The bulk files do not provide a reliable reason. A disappearance event therefore says only that a
previously current exact observation is no longer current in the latest successful snapshot.

If the current ZIP fails download, CRC, schema, size or suspicious-row-drop validation, all valid
inputs receive `FAILED/UNKNOWN` and the Actor run fails. A corrupted file can never manufacture
thousands of `NOT_FOUND` or disappearance events. If only the excluded file fails, exact current
matches can still succeed, but unmatched inputs are `FAILED/UNKNOWN` because excluded membership
was not verified. `RUN_SUMMARY.sourceState` is `SOURCE_FAILED` whenever either snapshot fails,
while the Actor run itself fails globally only when the current registry is unusable.

### Input

| Field | Default | Description |
| --- | --- | --- |
| `establishments` | required | 1-5,000 objects. Each requires at least one of `fei`, `duns`, `name`; optional `reference` is echoed unchanged and is limited to 128 characters. |
| `mode` | `snapshot` | `snapshot` returns the present observation; `monitor` compares with the last successful state. |
| `monitorId` | — | Required in monitor mode. Reuse the same 1-64 character key for the same watchlist. |
| `onlyChanges` | `false` | In monitor mode, unchanged inputs still emit status rows but omit repeated profile details. |
| `dueSoonDays` | `90` | 1-365 day `EXPIRATION_DUE_SOON` threshold. |
| `includeAddress` | `true` | Include the official formatted address and conservative components. |

No concurrency, retry, proxy, browser or parser settings are exposed. Retries are bounded and
limited to network errors, 408, 429 and selected 5xx responses.

### Output

Every row carries the history-ready contract:

- `source`, stable `source_id`, official `source_url`
- UTC `scraped_at`, additive `schema_version`, semantic `fingerprint`
- `reference`, `query_fei`, `query_duns`, `query_name`
- explicit `match_status`, `matched_by`, `registration_status`
- published FEI, DUNS, firm name, operations, expiration and facility address when verified
- `changed`, `change_type`, exact `changes`, threshold `alerts`
- a safe machine-readable `error` for unverified outcomes

The fingerprint covers normalized semantic source fields only. It excludes timestamps, run IDs,
HTTP metadata, `reference`, `days_until_expiration` and source-download facts, so the same FDA
state produces the same hash on repeat runs. Business-operation order and irrelevant case changes
do not create false changes.

FDA supplies one formatted global address rather than reliable worldwide street/city/region
columns. The output preserves that value in `facility_address.formatted`, always extracts the FDA
country marker when present, and parses US street/city/state/postal components conservatively.
Other global subfields remain `null` instead of being invented.

Raw files also contain establishment contacts, emails, agents and registrant contacts. The Actor
does not retain, log, index, store or emit any of those fields.

### Monitor mode

```json
{
  "establishments": [
    {"fei": "0000000360", "duns": "271408412", "reference": "supplier-fr-001"},
    {"duns": "039106281", "reference": "supplier-us-001"}
  ],
  "mode": "monitor",
  "monitorId": "weekly-pharma-suppliers",
  "onlyChanges": true,
  "dueSoonDays": 120,
  "includeAddress": true
}
```

The first successful verified observation is `BASELINE` with `changed=false`. Later successful
runs may emit:

- `REGISTRATION_APPEARED`, `REGISTRATION_DISAPPEARED`, `REGISTRATION_REAPPEARED`
- `EXCLUDED_STATUS_CHANGED`
- `EXPIRATION_DATE_CHANGED`, `EXPIRATION_DUE_SOON`
- `BUSINESS_OPERATION_ADDED`, `BUSINESS_OPERATION_REMOVED`
- `FIRM_NAME_CHANGED`, `ADDRESS_CHANGED`
- `FEI_CHANGED`, `DUNS_CHANGED`

State lives only in an Apify KVS. The key namespace is `monitorId` plus normalized input identity:
FEI first, with DUNS included when the input supplies it so reused FEIs cannot collide. The value is
the last successful normalized observation. `FAILED`, `AMBIGUOUS` and invalid rows never overwrite
it. Source row-count/schema metadata is likewise updated only after a valid snapshot.

`onlyChanges=true` preserves one-input/one-row reconciliation. An unchanged row keeps status,
identity metadata, fingerprint and `UNCHANGED`, sets `details_omitted=true`, and omits repeated
supplier profile fields. Use the Dataset `changes` view for flattened event consumption.

### API example

Keep the API token in a secret or environment variable, never in a URL, file or log:

```bash
curl -X POST "https://api.apify.com/v2/acts/YOUR_USERNAME~fda-drug-establishment-monitor/runs?maxTotalChargeUsd=1" \
  -H "Authorization: Bearer $APIFY_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "establishments": [
      {"fei":"0000000360","duns":"271408412","reference":"supplier-fr-001"},
      {"duns":"039106281","reference":"supplier-us-001"}
    ],
    "mode":"snapshot",
    "dueSoonDays":90,
    "includeAddress":true
  }'
```

Poll the returned run, then read its `defaultDatasetId`. `RUN_SUMMARY` in the default KVS records
source ETags/Last-Modified values, compressed/uncompressed bytes, row counts, schema fingerprint,
retry/validation counters, Dataset/KVS operations and change totals.

### Daily/weekly schedule

First save and test one stable monitor input as an Apify Task. Then create a non-overlapping daily
schedule in Console, or call the official schedule API (replace placeholders):

```bash
curl -X POST "https://api.apify.com/v2/schedules" \
  -H "Authorization: Bearer $APIFY_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Daily FDA drug supplier registration watchlist",
    "isEnabled": true,
    "isExclusive": true,
    "cronExpression": "0 8 * * *",
    "timezone": "UTC",
    "actions": [{"type":"RUN_ACTOR_TASK","actorTaskId":"YOUR_TASK_ID"}]
  }'
```

For a Monday weekly review use `0 8 * * 1`. Keep the same `monitorId`. FDA says DECRS is updated
each business day, so running more frequently than daily usually adds fixed download cost without
new official source state.

### Webhook example

Attach a persistent successful-run webhook to the saved Task. The receiver reads the run's default
Dataset and handles rows whose `change_type == "CHANGED"` or whose `alerts` are non-empty:

```bash
curl -X POST "https://api.apify.com/v2/webhooks" \
  -H "Authorization: Bearer $APIFY_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "requestUrl": "https://procurement.example/webhooks/fda-supplier-change",
    "eventTypes": ["ACTOR.RUN.SUCCEEDED"],
    "condition": {"actorTaskId":"YOUR_TASK_ID"},
    "payloadTemplate": "{\"run_id\":\"{{resource.id}}\",\"dataset_id\":\"{{resource.defaultDatasetId}}\"}",
    "shouldInterpolateStrings": true,
    "description": "FDA supplier registration changes"
  }'
```

Also subscribe operational alerting to `ACTOR.RUN.FAILED`: failure means the current FDA snapshot
was not verified, not that every supplier disappeared.

### Source freshness and validation

Runtime data comes from the official `drls_reg.zip` and `drls_excluded.zip` files on
`accessdata.fda.gov`. Live research found tab-delimited text inside both ZIPs; current text uses
Windows-1252, and the source includes a leading space in its first header plus a harmless trailing
empty field on each row.

Before matching, the Actor validates HTTP status/content type, download length, ZIP member count,
uncompressed-size bound, CRC, exactly one text member, required columns, encoding, every retained
business field, exclusion flags and non-trivial volume. The current snapshot must have at least
5,000 rows and 500 KiB of TSV data, and cannot fall below 50% of the last successful row count.
The excluded snapshot must have at least five rows; both snapshots reject a row-count drop below
50% of their last successful snapshot. These conservative guards can also reject a legitimate
large contraction, which requires source verification before changing the safety thresholds.
A safe single renamed `.txt` member is accepted with a warning; missing or ambiguous files fail.

Research and reproducible live facts are in `experiments/fda-decrs/`. No raw contact data or cookies
are committed.

### Pricing

**$0.01 per resolved supplier check. Platform usage is included.** The same price applies
across Apify plans; there is no subscription or run-start fee.

The PPE event is `supplier_checked`: one event for an input exactly matched to one
`CURRENT` or `EXCLUDED` FDA record, or for an authoritative `NOT_FOUND` after both official files
validate. Both are successfully resolved supplier checks. There is no charge for ambiguous or
invalid inputs, retries, source failures, internal file downloads, Dataset writes or KVS work.

`UNCHANGED` checks in monitor mode are charged: the value is a fresh, verified observation,
not a guaranteed change. `onlyChanges` reduces repeated details, not the number of checks or
their price. Duplicate entries are separate requested checks and are charged individually;
deduplicate your watchlist if you want one check per supplier.

| Resolved checks per run | Maximum event charge |
| --- | --- |
| 1 | $0.01 |
| 10 | $0.10 |
| 100 | $1.00 |
| 1,000 | $10.00 |
| 5,000 | $50.00 |

For example, 100 suppliers checked on four weekly runs cost at most **$4**; checked on
30 daily runs, at most **$30**. Failed, invalid and ambiguous checks reduce those totals.
The saved Console **maximum total charge** is $1 (up to 100 resolved checks).
API callers must set `maxTotalChargeUsd` explicitly, as in the example above:
the run API does not inherit that saved Console ceiling. Increase it deliberately
for a larger watchlist. Apify can lower the requested ceiling to
your account's available budget. Inputs beyond the effective ceiling receive a free
`FAILED/UNKNOWN` row with `MAX_TOTAL_CHARGE_REACHED`; their monitor state is not advanced.
Apify may mark a budget-exhausted run `ABORTED`, so inspect its Dataset and summary too.
An abrupt platform termination can interrupt delivery before all reconciliation rows persist.

Successful hosted tests on 2026-09-05 verified snapshot and repeat-monitor behavior at 256 MiB,
without a browser or proxy. Final per-run compute and platform usage costs settle after the
Actor finishes: read them from `GET /v2/actor-runs/{runId}`. The in-run `RUN_SUMMARY` leaves
`computeUnits` and `platformCostUsd` null rather than guessing those final values.

#### Interrupted runs

Do not resurrect a partially delivered run to obtain the remaining suppliers. A delivery
checkpoint prevents replaying potentially charged output: a completed run keeps its existing
results, and an interrupted delivery reports `DELIVERY_UNCERTAIN` without repeating charges.
Inspect the existing Dataset, then start a new run for the unresolved inputs. Keep the same
`monitorId` to retain the last successfully saved state. Never overlap runs for one watchlist.

### Support

Use the Actor's Issues tab for support. Include the run ID, a public supplier identifier and
the relevant error code. Do not post API tokens, private supplier lists or contact details.

### Limitations and legal/privacy note

- DECRS does not include every FDA-related facility category. FDA specifically notes exclusions for
  human-drug compounding outsourcing facilities unless registered under another operation, and for
  wholesale distributors/third-party logistics providers reported under a different program.
- FDA says submitted registration/listing data is not product approval and may not have been
  verified by FDA.
- The excluded file proves only published excluded membership; it contains no reliable cause.
- Exact company-name matching can be ambiguous and is deliberately not fuzzy-resolved.
- Historical state is last-successful KVS state, not a historical database.

Legal/privacy risk is classified **LOW**: this is a public US government dataset, requires no login
or access-control bypass, and the Actor minimizes output to establishment-registration fields.
Users remain responsible for interpreting the signal correctly and for any downstream regulatory,
procurement or import decision.

# Actor input Schema

## `establishments` (type: `array`):

One row per supplier. FEI is tried first, then DUNS, then normalized exact firm name. Multiple exact records are AMBIGUOUS rather than fuzzy-matched.

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

snapshot returns the current official observation. monitor compares each normalized watchlist identity with its last successfully verified KVS state.

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

Required in monitor mode. Reuse the same 1-64 character watchlist key on every scheduled run. Allowed: letters, digits, dots, underscores and hyphens.

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

In monitor mode every input still emits a reconciliation row, but unchanged rows omit repeated supplier profile details. Changed, baseline, ambiguous and failed rows remain explicit.

## `dueSoonDays` (type: `integer`):

Add an EXPIRATION\_DUE\_SOON alert when a current registration expires within this many days.

## `includeAddress` (type: `boolean`):

Include the official formatted facility address. Global subfields are populated only when they can be parsed reliably; country is extracted from FDA's country marker.

## Actor input object example

```json
{
  "establishments": [
    {
      "fei": "0000000360",
      "reference": "supplier-fr-001"
    },
    {
      "duns": "039106281",
      "reference": "supplier-us-001"
    },
    {
      "name": "EUROAPI FRANCE",
      "reference": "name-review"
    }
  ],
  "mode": "snapshot",
  "onlyChanges": false,
  "dueSoonDays": 90,
  "includeAddress": true
}
```

# Actor output Schema

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

Exactly one row per input, including invalid, ambiguous and failed checks.

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

Flattened deterministic monitor changes ready for webhook consumers.

## `alerts` (type: `string`):

Flattened current EXPIRATION\_DUE\_SOON alerts.

## `diagnostics` (type: `string`):

Failure-focused projection; filter match\_status client-side.

## `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 = {
    "establishments": [
        {
            "fei": "0000000360",
            "duns": "271408412",
            "name": "DSP",
            "reference": "supplier-fr-001"
        }
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("automa-flow/fda-drug-establishment-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 = { "establishments": [{
            "fei": "0000000360",
            "duns": "271408412",
            "name": "DSP",
            "reference": "supplier-fr-001",
        }] }

# Run the Actor and wait for it to finish
run = client.actor("automa-flow/fda-drug-establishment-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 '{
  "establishments": [
    {
      "fei": "0000000360",
      "duns": "271408412",
      "name": "DSP",
      "reference": "supplier-fr-001"
    }
  ]
}' |
apify call automa-flow/fda-drug-establishment-monitor --silent --output-dataset

```

## MCP server setup

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