# EU GMP Non-Compliance & Certificate Monitor (EudraGMDP) (`goobertje/eu-gmp-compliance-monitor`) Actor

Monitor the public EudraGMDP database for GMP non-compliance statements and GMP certificates, with filters, delta mode (only new records) and optional webhook alerts. Built for pharma QA teams and consultants.

- **URL**: https://apify.com/goobertje/eu-gmp-compliance-monitor.md
- **Developed by:** [goober](https://apify.com/goobertje) (community)
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $4.00 / 1,000 record scrapeds

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?

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

## EU GMP non-compliance and certificate alerts (EudraGMDP API)

Find out which medicine manufacturing sites in the EU and abroad were just declared non-compliant with Good Manufacturing Practice (GMP). This Actor reads the public EudraGMDP database of the European Medicines Agency (EMA) and returns clean JSON: statements of GMP non-compliance and GMP certificates, with site, country, dates and the inspectorate's findings.

It is for regulatory affairs, procurement, supply-chain and pharmacovigilance teams, analysts and journalists. A non-compliance statement is often one of the earliest public signals that supply of a medicine may be interrupted.

### Why use this Actor

- **Official source**: the EMA's public EudraGMDP database, not a scraped third-party copy.
- **No stateful web form**: EudraGMDP needs a session and a multi-step search. You send one JSON input and get rows.
- **Delta mode**: `onlyNew` returns only records you have not seen before. Good for scheduled alerts.
- **Webhook summary**: optional Discord, Slack or generic JSON message after each run.
- **Light and polite**: plain HTTP, no browser, no proxies, about 1 request per second.

### Quick start

Click **Start** with the prefilled input. It takes about 10 seconds and returns the 5 newest statements of GMP non-compliance of the last 90 days, with details:

```json
{ "mode": "gmpNonCompliance", "maxItems": 5, "includeDetails": true }
```

You get a dataset with one row per statement. Open the *Overview* view for a table, or the JSON for every field.

### What you get

One dataset item per record. Fields that the source does not provide are `null`.

| Field | Meaning |
|---|---|
| `recordType`, `documentNumber`, `eudragmdpReference`, `documentType`, `miaNumber` | Identity of the document (`miaNumber` is the site authorisation number, when listed). `eudragmdpReference` is the stable EudraGMDP ID |
| `siteName`, `siteAlternativeName`, `siteAddress`, `additionalDetails`, `city`, `postcode`, `country` | The manufacturing or importing site |
| `omsOrganisationId`, `omsLocationId`, `dunsNumber` | Registry identifiers (DUNS from details only) |
| `inspectionEndDate`, `issueDate` | `YYYY-MM-DD`. `issueDate` of a certificate comes from the detail page |
| `issuingAuthority`, `legalBasis`, `scope`, `outcome`, `operations[]` | Detail page: who issued it, under which article, scope, `compliant` or `non-compliant`, the listed GMP operations |
| `natureOfNonCompliance`, `actionTaken`, `additionalComments` | Non-compliance statements: the inspectorate's findings and measures |
| `detailsIncluded`, `isNew`, `source`, `scrapedAt` | Run metadata. `isNew` is `true` in delta mode |

#### Input options

| Field | Meaning |
|---|---|
| `mode` | `gmpNonCompliance` (default) or `gmpCertificates` |
| `since`, `until` | `YYYY-MM-DD`. Issue date for non-compliance statements; **inspection end date** for certificates (EudraGMDP cannot filter certificates by issue date). Default: last 90 days |
| `companyName` | Text that must appear in the site name or alternative name |
| `countries` | ISO-2 codes or English names, e.g. `["IN","China"]`. Non-compliance: filter. Certificates: one search per country, default all EEA countries. `third` (all third countries) and `all` also work for certificates but EudraGMDP needs 1-3 minutes to answer |
| `activeSubstance` | Keyword that must occur in the site name or the statement text. EudraGMDP has **no structured substance field**, so this only finds records whose text names the substance |
| `includeDetails` | Read each record's detail page (default true, one extra request per record) |
| `onlyNew`, `stateStoreName` | Delta mode and the name of the persistent store |
| `webhookUrl` | Optional https webhook (secret) |
| `maxItems` | Cap on billed records per run (default 50, prefill 5) |
| `maxDetailRequests` | Cap on detail pages per run (default 300) |
| `requestDelaySecs` | Delay between requests, minimum 1 |

#### Sample item (real run, 2026-10-19; long texts shortened with ...)

```json
{
  "recordType": "gmpNonCompliance",
  "documentNumber": "MT/004NCR/2026",
  "eudragmdpReference": "188112",
  "siteName": "Glukem Biocare Private Limited",
  "siteAddress": "Plot No 3a Biotech Park, Phase III Karkapatla, Markook",
  "city": "Siddipet",
  "postcode": "502281",
  "country": "India",
  "omsOrganisationId": "ORG-100057915",
  "omsLocationId": "LOC-100104543",
  "inspectionEndDate": "2026-09-15",
  "issueDate": "2026-10-16",
  "issuingAuthority": "Malta",
  "dunsNumber": "87-368-0583",
  "legalBasis": "Issued following an inspection in accordance with Art. 111(7) of Directive 2001/83/EC as amended",
  "scope": "Human Medicinal Products",
  "outcome": "non-compliant",
  "operations": [
    "1 NON-COMPLIANT MANUFACTURING OPERATIONS",
    "1.2 Non-sterile products",
    "1.2.1.17 Other: Manufacture of intermediates (pellets)(en)",
    "1.6.3 Chemical/Physical"
  ],
  "natureOfNonCompliance": "The inspection resulted in a critical deficiency related to contamination risk in the Fluid Bed Coating (FBC) equipment ...",
  "actionTaken": "Revocation of the marketing authorisation(s)\nMember states should consider not initiating ... Prohibition of supply ...",
  "detailsIncluded": true,
  "isNew": true,
  "scrapedAt": "2026-10-18T22:24:54Z"
}
```

A complete real output with one non-compliance statement and one GMP certificate is in `sample_output.json` in the project folder.

### Pricing

Pay per event: **one `record-scraped` event per dataset item, $4.00 per 1,000 records** ($0.004 each).

- Records skipped by a filter, by delta mode or because of an error are never pushed and never charged.
- `maxItems` caps the cost of a run, and the run also stops when your maximum charge per run is reached.
- Example: 50 records cost 50 x $0.004 = $0.20. Reading details adds run time (about 1 second per record), not extra events.

### Use with AI agents (MCP, Claude, Cursor, n8n, Make)

AI agents can call this Actor through the Apify MCP server. Add this to your MCP client config (you sign in with OAuth in the browser on first use):

```json
{ "mcpServers": { "apify": { "url": "https://mcp.apify.com?tools=goobertje/eu-gmp-compliance-monitor" } } }
```

Example prompts and the input each one maps to:

1. "Which sites in India or China got a GMP non-compliance statement since 1 September 2026?"
   ```json
   { "mode": "gmpNonCompliance", "since": "2026-09-01", "countries": ["IN", "CN"], "maxItems": 50 }
   ```
2. "Does any GMP non-compliance statement from the last 90 days mention paracetamol?"
   ```json
   { "mode": "gmpNonCompliance", "activeSubstance": "paracetamol", "includeDetails": true, "maxItems": 20 }
   ```
3. "List the Belgian GMP certificates of Universiteit Gent since January 2026."
   ```json
   { "mode": "gmpCertificates", "countries": ["BE"], "companyName": "Gent", "since": "2026-01-01", "maxItems": 20 }
   ```

In n8n, Make or Zapier, use the Apify node or module, pick this Actor and paste the same JSON.

### Use cases

- **Supply-risk alerts**: a daily run with `onlyNew` and a webhook tells you when a site in your supply chain is declared non-compliant.
- **Supplier due diligence**: look up a manufacturer by name across non-compliance statements and GMP certificates.
- **Regulatory affairs and procurement dashboards**: feed the JSON into a sheet or BI tool.
- **Journalism and research**: track enforcement activity per country or inspectorate.

### FAQ

**Where does the data come from, and is it allowed?**
From the public EudraGMDP database of the EMA. Its `robots.txt` returns 404 (no restrictions) and the Actor still fetches and honours it. The EMA legal notice allows reuse, also commercial, with the EMA acknowledged as source. See the legal section below.

**How fresh is it?**
Each run reads the live database. The national authorities fill it, so entries can appear weeks after the inspection.

**How much does a typical run cost?**
The prefilled run of 5 records costs $0.02. A daily alert run that returns a handful of new records costs a few cents.

**What happens if EudraGMDP blocks me or changes?**
The Actor stops after repeated 403 or 429 answers and never tries to get around a block. Heavy searches are never re-sent. If the page layout changes, parsing may return fewer or empty fields (never invented values); please report it.

**Can I schedule it and get only new records?**
Yes. Set `onlyNew` to `true` and keep the same `stateStoreName`. Example schedule input: `{"mode":"gmpNonCompliance","onlyNew":true,"stateStoreName":"my-gmp-watch","webhookUrl":"https://hooks.slack.com/services/...","countries":["IN","CN"]}`. The first run is capped by `maxItems` (default 50): with more than 50 records in the window, catching up takes several runs, newest first (raise `maxItems` to go faster). A record pushed without details because `maxDetailRequests` ran out is not remembered and is reported again later. Without `onlyNew` nothing is remembered and the webhook says "records", not "new records".

**What is not included?**
Belgian shortage notices and manufacturing-authorisation records.

### Limits and honest caveats

- **Data freshness**: entries appear when the authorities publish them.
- **Certificates are heavy to search**: one country at a time (a single country answers in a few seconds). Without `countries` the EEA-wide search runs, which EudraGMDP answers in about 10 seconds. Use `since`, `countries` and `companyName` to keep runs short.
- **Date meaning differs by mode** (see input options).
- **Active substance**: text match only. Absence of a match does not prove a substance is not affected.
- **Speed**: about 1 record per second with details, because requests are throttled to 1 per second.

### Legal and compliance

- Source: the public EudraGMDP database, `https://eudragmdp.ema.europa.eu/`. The site publishes no `robots.txt` restrictions (the file returns 404) and the Actor still fetches and honours it on every run. It uses only the public search pages, no login and no workaround of any protection.
- The EMA legal notice states that information on EMA web pages may be reproduced and distributed, also for commercial purposes, provided that EMA is acknowledged as the source. Every item carries `source: "EudraGMDP (EMA), public database"`; please keep that attribution when you republish.
- EudraGMDP content is provided by the national competent authorities; the EMA accepts no liability for it. This Actor reproduces it as published and gives no assurance of completeness or accuracy. Check the original database before taking regulatory, legal or commercial decisions.
- Records can name companies and sites. The data is public regulatory information; using it is your responsibility under the laws that apply to you.
- **Not affiliated with, endorsed by or sponsored by the European Medicines Agency, the European Commission or any national authority.**

Found a problem or want a field added? Open an issue on the Actor page.

# Actor input Schema

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

gmpNonCompliance: statements of GMP non-compliance (an early-warning signal for supply problems). gmpCertificates: GMP certificates, searched per country (default: all EEA countries). Example: "gmpNonCompliance".

## `since` (type: `string`):

Only records from this date on, as YYYY-MM-DD. For gmpNonCompliance this is the issue date of the statement; for gmpCertificates it is the inspection end date (EudraGMDP cannot filter certificates by issue date). Leave empty for the last 90 days. Example: "2026-01-01".

## `until` (type: `string`):

Only records up to and including this date, as YYYY-MM-DD (same date meaning as Since). Leave empty for no upper bound. Example: "2026-06-30".

## `companyName` (type: `string`):

Case-insensitive text that must appear in the site name or its alternative name (the manufacturer, importer or site, not the marketing authorisation holder). Example: "Teva".

## `countries` (type: `array`):

Country of the site, as ISO-2 codes or English names. For gmpNonCompliance this filters the results (empty = all countries). For gmpCertificates every country is one search (empty = all EEA countries); the special values eea, third (all third countries) and all are accepted there, but third and all make EudraGMDP think for 1 to 3 minutes. Example: \["IN", "China", "DE"].

## `activeSubstance` (type: `string`):

Case-insensitive keyword that must appear in the site name or in the statement text (scope, listed operations, nature of non-compliance, action taken, comments). EudraGMDP has no structured active-substance field, so only records whose text names the substance match. Needs one detail-page request per candidate record (see Max detail requests). Example: "paracetamol".

## `includeDetails` (type: `boolean`):

Open the detail page of each record to add issuing authority, legal basis, scope, listed operations, DUNS number and, for non-compliance statements, the nature of non-compliance, action taken and comments. Costs one extra request (about 1 second) per record. Example: true.

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

Skip records already reported by a previous run that used the same State store name. Use it for scheduled alerting. Records are remembered only after they were pushed with the details you asked for. Example: true.

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

Name of the persistent key-value store holding the records already reported (used by Only new records). Use different names for different watchlists. Only lowercase letters, digits and hyphens (not at the start or end). Example: "eu-gmp-monitor-state".

## `webhookUrl` (type: `string`):

Optional https URL that receives a short summary of the records pushed in this run. Works with Discord, Slack or any generic JSON webhook. Treated as a secret because it contains a token. Example: "https://hooks.slack.com/services/T000/B000/XXXX".

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

Maximum number of records (dataset items, each one billed) a run outputs; the run stops once reached. Default 50 (the Start-button prefill is 5). With Only new records, the first run is limited by this cap too, so catching up on a large backlog takes several runs. Example: 50.

## `maxDetailRequests` (type: `integer`):

Upper bound on detail-page requests per run (it also bounds the work of the active substance filter). Records beyond the bound are still output without details, unless an active substance filter needs them, in which case the run ends. Example: 300.

## `requestDelaySecs` (type: `number`):

Pause between requests to EudraGMDP to stay polite. Minimum 1, maximum 30. Example: 1.0.

## Actor input object example

```json
{
  "mode": "gmpNonCompliance",
  "countries": [],
  "includeDetails": true,
  "onlyNew": false,
  "stateStoreName": "eu-gmp-monitor-state",
  "maxItems": 5,
  "maxDetailRequests": 300,
  "requestDelaySecs": 1
}
```

# Actor output Schema

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

GMP non-compliance statements or GMP certificates with site, country, dates and (optionally) the findings of the issuing authority.

# 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 = {
    "maxItems": 5
};

// Run the Actor and wait for it to finish
const run = await client.actor("goobertje/eu-gmp-compliance-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 = { "maxItems": 5 }

# Run the Actor and wait for it to finish
run = client.actor("goobertje/eu-gmp-compliance-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 '{
  "maxItems": 5
}' |
apify call goobertje/eu-gmp-compliance-monitor --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,goobertje/eu-gmp-compliance-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/CN3vs0pg4qEHttUQb/builds/lDOTv1co0yxavdyWT/openapi.json
