# GMP Enforcement Watch (EudraGMDP + FDA) (`atlas-data/gmp-enforcement-watch`) Actor

Watches your pharma supplier sites across EU and US enforcement sources: EudraGMDP GMP non-compliance reports plus FDA warning letters, with optional FDA inspection classifications via your own free FDA API key. Verbatim regulator wording, provenance deep-links, bounded incremental state.

- **URL**: https://apify.com/atlas-data/gmp-enforcement-watch.md
- **Developed by:** [Atlas](https://apify.com/atlas-data) (community)
- **Categories:** Business, Developer tools
- **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?

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

## GMP Enforcement Watch (EudraGMDP + FDA)

Marketplace-native Apify Actor for **pharma sourcing and QA teams**: register your contract
API / drug-product manufacturer sites once and get a deduplicated, watchlist-matched feed of GMP
**enforcement events** fused across jurisdictions — with verbatim regulator wording, provenance
deep-links to every original document, and bounded incremental state.

> **Decision-support output only.** This Actor reproduces verbatim excerpts from official
> regulators and adds no interpretation. It is **not legal, regulatory or compliance advice**, and
> it does not guarantee completeness of any source. Always verify against the linked official
> document before acting.

### What it watches

| Source | Jurisdiction | Events | Access model |
| --- | --- | --- | --- |
| **EU EudraGMDP** — GMP non-compliance reports (`eudragmdp.ema.europa.eu`, public read-only area) | EEA | Statement-of-non-compliance publications per site: report number, MIA number, OMS location id, address, inspection end date, issue date, verbatim nature-of-non-compliance excerpt from the report detail page | Official public search/detail functions used anonymously at a polite ~1 req/s pace with an honest User-Agent. No access controls are bypassed. |
| **US FDA — Warning Letters database** (`fda.gov`) | US | Warning letters posted inside the window. Two official channels are fused: the live letter index on the database landing page (freshest letters, deep links, FEI) and the paginated Excel export (historical backfill; its cache lags the live DB and aliases page numbers, so backfill depth is bounded by `fdaMaxPages`). Letters found via the index carry no subject excerpt (it lives on the letter page); export rows carry the regulator's own subject classification. Duplicates across channels are collapsed. | Public pages/export fetched anonymously at a polite pace. |
| **US FDA — Data Dashboard API** `inspections_classifications` *(optional)* | US | NAI/VAI/OAI inspection classifications for drug facilities in the window | Credential-gated by FDA. Provide **your own free registered key** via `ddapiAuthorizationUser`/`ddapiAuthorizationKey`; without them this source is skipped. Docs: https://datadashboard.fda.gov/oii/api/index.htm |

Sources that the underlying opportunity research also mentioned but that are **not** in v1:
FDA import-alert firm lists (their legacy accessdata endpoints were retired; the replacement
dashboard is credential-gated), WHO prequalification notices, and national competent-authority
pages. These remain explicit future work, not silent gaps.

### Input

- `watchlist` — array of `{ "name": "...", "country": "..." }`. Matching is deliberately
  conservative: names are normalized (diacritics, case, legal-form suffixes such as GmbH/S.r.l./
  Pvt. Ltd.) and scored by token similarity against each event's site name; a country mismatch on
  either side blocks a match, and near-tie scores are flagged `ambiguous` for human review instead
  of being asserted.
- `mode` — `incremental` emits only events not seen in previous runs (default); `full` re-emits
  everything found inside the windows (use for backfills/replays).
- Windows & budgets: `euWindowDays`, `fdaWindowDays`, `maxDrilldowns` (per-run politeness cap on
  EU detail fetches), `fdaMaxPages` (each export page = 1000 rows), `ddapiMaxRows`.
- `minMatchScore` (default 0.8) and `includeUnmatchedEvents` (default true — keep it on: unmatched
  events make "quiet" periods auditable instead of ambiguous).
- `maxItems` caps emitted event records; anything cut off is *not* committed as seen, so the next
  incremental run re-emits exactly those.
- `minRequestIntervalMs` politeness floor (default 1000).

### Output

Dataset records (`type`):

1. `enforcement_event` — one per detected event: stable 40-hex `recordId`, `sourceId`,
   `eventType`, site name/country/city, `eventDate`, `publishedDate`, `detailUrl`,
   `verbatimExcerpt` (capped regulator wording), watchlist match block (`matched`, `matchScore`,
   `ambiguous`, candidates) and a `provenance` object (source label, attribution, decision-support
   notice, fetched endpoints, timestamp).
2. `source_status` — one heartbeat per swept source per run (`ok`, error text, fetched counts,
   window). Status rows are metadata and never count against `maxItems`, so a quiet-but-healthy
   sweep still produces output.

The key-value store holds `OUTPUT` / `LAST_RUN_SUMMARY` with per-source stats for monitoring.

### Reliability model

- Per-source failure isolation: one regulator being down never fails the whole run; all sources
  failing fails loudly after persisting the summary.
- Retries with exponential backoff honor `Retry-After`; response size ceilings and timeouts guard
  every request; parsers are shape-conservative and raise explicit errors on layout changes rather
  than emitting silently-wrong records.
- Dataset rows are written **before** durable state commits; budget-suppressed events stay unseen
  and re-emit exactly once on replay. State documents are versioned, structurally validated and
  pruned per source (bounded growth).
- Zero-result sweeps are visible as `fetchedCount: 0` status rows — quiet means quiet, broken
  means broken.

### Compliance posture & known limitations (read before relying on this)

- **Attribution**: every record carries source attribution to EMA/EudraGMDP or FDA. EU content
  originates from EEA National Competent Authorities; US content is U.S. government work.
- **No PPE pricing**: this Actor is published without pay-per-event monetization. The underlying
  research flagged a written EMA confirmation for programmatic access as a precondition for
  charging customers; that confirmation has **not** been obtained, so monetization stays off and
  collection remains plain anonymous public-area browsing at polite rates.
- **Matching is heuristic**: name-based entity resolution can miss variants or over-match similar
  names; country guards and ambiguity flagging reduce but do not eliminate this. Review `ambiguous`
  rows before operational decisions.
- **Coverage caveats**: FDA warning-letter export pages are consumed oldest-first up to
  `fdaMaxPages` — very high-volume windows may need more pages; letters whose posted date cannot be
  parsed are skipped and counted; deep links resolve only when the landing-page index lists the
  letter. EudraGMDP excludes information NCAs legally keep non-public.
- Personal data: outputs carry company/site-level regulatory facts only.

### Usage

```json
{
  "watchlist": [
    { "name": "Aculife Healthcare Private Limited", "country": "India" },
    { "name": "Avlab S.r.l.", "country": "Italy" }
  ],
  "mode": "incremental",
  "euWindowDays": 92,
  "includeEuDetails": true,
  "fdaWindowDays": 92,
  "fdaMaxPages": 2,
  "includeUnmatchedEvents": true,
  "maxItems": 200,
  "minRequestIntervalMs": 1000
}
```

Run locally: `npm install && npm run smoke:local` (uses `test-input.json`). Live smoke test:
`npm run test:smoke`.

### Development

```
npm ci
npm run lint && npm run typecheck && npm run build
npm test                 # unit + integration + adversarial + schema (no network)
npm run test:smoke       # opt-in live-source smoke (SMOKE=1)
```

### Disclaimer

This Actor is an independent tool and is not affiliated with or endorsed by the European Medicines
Agency, any National Competent Authority, or the U.S. Food and Drug Administration. Outputs are
machine-collected decision support with no compliance, legal or scientific guarantees; users remain
responsible for verifying every fact against the linked official sources.

# Actor input Schema

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

Sites to watch. Matching is conservative: normalized company-name similarity with optional country guard; ambiguous matches are flagged for review instead of asserted.

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

incremental: only events not seen in any previous run are emitted (a per-source status row is always written). full: re-emit every event found inside the source windows regardless of stored state — use for the first run, backfills or downstream replays.

## `euWindowDays` (type: `integer`):

How many days back to query EudraGMDP non-compliance reports by issue date on every run.

## `includeEuDetails` (type: `boolean`):

Open each new EudraGMDP non-compliance report (up to 'Max EU detail fetches') and extract the verbatim nature-of-non-compliance text.

## `maxDrilldowns` (type: `integer`):

Upper bound of EudraGMDP report-detail pages fetched per run (politeness budget). Reports whose details were skipped are still emitted without the verbatim excerpt.

## `fdaWindowDays` (type: `integer`):

Only warning letters posted within this many days are considered.

## `fdaMaxPages` (type: `integer`):

Historical-backfill budget for the official FDA warning-letters Excel export (1000 rows per page; the cached export lags the live database). Fresh letters always come from the live landing-page index regardless of this cap.

## `minMatchScore` (type: `number`):

Conservative name-matching threshold between 0.5 and 1. Events scoring below it stay unmatched (flagged, not dropped); near-ties are marked ambiguous for human review instead of auto-matched.

## `includeUnmatchedEvents` (type: `boolean`):

Also emit enforcement events that did NOT match any watchlist entry (flagged matched=false). This keeps the dataset auditable and proves quiet periods are real. Turn off to store watchlist hits only.

## `ddapiAuthorizationUser` (type: `string`):

Optional. Your own Authorization-User email issued free by FDA for its Data Dashboard API (https://datadashboard.fda.gov/oii/api/index.htm). When set together with the key, official inspection classifications (NAI/VAI/OAI) are fused in as a third source. Leave empty to run on EU + FDA warning letters only.

## `ddapiAuthorizationKey` (type: `string`):

Optional companion secret for the FDA Data Dashboard API user above. Stored only for the duration of the run; never echoed into outputs.

## `ddapiMaxRows` (type: `integer`):

Row cap per FDA Data Dashboard API request when DDAPI credentials are provided.

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

Upper bound on event records saved this run. Events cut off by this cap are NOT committed as seen, so an incremental replay re-emits exactly them. Per-source status rows are metadata and never count against this cap.

## `minRequestIntervalMs` (type: `integer`):

Politeness floor between requests to the same source host (~1 request/second default; 0 disables pacing for tests only — not recommended against public regulators).

## Actor input object example

```json
{
  "watchlist": [
    {
      "name": "Aculife Healthcare Private Limited",
      "country": "India"
    },
    {
      "name": "Avlab S.r.l.",
      "country": "Italy"
    },
    {
      "name": "Laboratorios Eurisko S.L.",
      "country": "Spain"
    }
  ],
  "mode": "full",
  "euWindowDays": 120,
  "includeEuDetails": true,
  "maxDrilldowns": 6,
  "fdaWindowDays": 120,
  "fdaMaxPages": 1,
  "minMatchScore": 0.8,
  "includeUnmatchedEvents": true,
  "ddapiAuthorizationUser": "",
  "ddapiAuthorizationKey": "",
  "ddapiMaxRows": 500,
  "maxItems": 100,
  "minRequestIntervalMs": 800
}
```

# Actor output Schema

## `results` (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 = {
    "watchlist": [
        {
            "name": "Aculife Healthcare Private Limited",
            "country": "India"
        },
        {
            "name": "Avlab S.r.l.",
            "country": "Italy"
        },
        {
            "name": "Laboratorios Eurisko S.L.",
            "country": "Spain"
        }
    ],
    "mode": "full",
    "euWindowDays": 120,
    "includeEuDetails": true,
    "maxDrilldowns": 6,
    "fdaWindowDays": 120,
    "fdaMaxPages": 1,
    "minMatchScore": 0.8,
    "includeUnmatchedEvents": true,
    "ddapiAuthorizationUser": "",
    "ddapiAuthorizationKey": "",
    "ddapiMaxRows": 500,
    "maxItems": 100,
    "minRequestIntervalMs": 800
};

// Run the Actor and wait for it to finish
const run = await client.actor("atlas-data/gmp-enforcement-watch").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 = {
    "watchlist": [
        {
            "name": "Aculife Healthcare Private Limited",
            "country": "India",
        },
        {
            "name": "Avlab S.r.l.",
            "country": "Italy",
        },
        {
            "name": "Laboratorios Eurisko S.L.",
            "country": "Spain",
        },
    ],
    "mode": "full",
    "euWindowDays": 120,
    "includeEuDetails": True,
    "maxDrilldowns": 6,
    "fdaWindowDays": 120,
    "fdaMaxPages": 1,
    "minMatchScore": 0.8,
    "includeUnmatchedEvents": True,
    "ddapiAuthorizationUser": "",
    "ddapiAuthorizationKey": "",
    "ddapiMaxRows": 500,
    "maxItems": 100,
    "minRequestIntervalMs": 800,
}

# Run the Actor and wait for it to finish
run = client.actor("atlas-data/gmp-enforcement-watch").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 '{
  "watchlist": [
    {
      "name": "Aculife Healthcare Private Limited",
      "country": "India"
    },
    {
      "name": "Avlab S.r.l.",
      "country": "Italy"
    },
    {
      "name": "Laboratorios Eurisko S.L.",
      "country": "Spain"
    }
  ],
  "mode": "full",
  "euWindowDays": 120,
  "includeEuDetails": true,
  "maxDrilldowns": 6,
  "fdaWindowDays": 120,
  "fdaMaxPages": 1,
  "minMatchScore": 0.8,
  "includeUnmatchedEvents": true,
  "ddapiAuthorizationUser": "",
  "ddapiAuthorizationKey": "",
  "ddapiMaxRows": 500,
  "maxItems": 100,
  "minRequestIntervalMs": 800
}' |
apify call atlas-data/gmp-enforcement-watch --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,atlas-data/gmp-enforcement-watch"
        }
    }
}

```

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/cvF34Y5tBLyhHtbNT/builds/lYH5RhCDdXlmO3lh4/openapi.json
