EUDAMED Certificate Status & Expiry Monitor avatar

EUDAMED Certificate Status & Expiry Monitor

Pricing

$5.00 / 1,000 certificate observations

Go to Apify Store
EUDAMED Certificate Status & Expiry Monitor

EUDAMED Certificate Status & Expiry Monitor

Monitor public EU MDR/IVDR certificates by certificate number, manufacturer SRN or notified body and detect status, version, portfolio and expiry changes over time. A NOT_FOUND here never hides a EUDAMED failure, and a certificate leaving a query is never reported as a withdrawal.

Pricing

$5.00 / 1,000 certificate observations

Rating

0.0

(0)

Developer

Vadim Bezrukov

Vadim Bezrukov

Maintained by Community

Actor stats

0

Bookmarked

2

Total users

1

Monthly active users

2 days ago

Last modified

Share

Monitor public EU MDR/IVDR certificates by certificate number, manufacturer SRN, or notified body and detect status, version, portfolio, and expiry changes over time.

You give it a watchlist. Every run tells you what the certificate state is now, what moved since the last trustworthy check, and which certificates need attention because they were suspended, withdrawn, restricted, re-versioned, or are running out of time.

It is not another EUDAMED export. Exports leave you diffing two spreadsheets. This keeps the last good state per certificate and gives you the events.

What you can do with it

  • Watch a supplier's certificates. One manufacturer SRN returns that manufacturer's whole public certificate portfolio, and tells you when a certificate is added to it or leaves it.
  • Watch a notified body. Notified-body designations move. A notified-body watch shows the portfolio and flags certificates changing hands.
  • Run an expiry desk. expiry_state and days_to_expiry on every row, and an EXPIRING_SOON event on the run where a certificate crosses your warning window - not on every run after it.
  • Catch a suspension the day it appears. SUSPENDED, WITHDRAWN, RESTRICTED, CANCELLED and REINSTATED are first-class events, with the notified body's own status-change reason attached.
  • Keep scheduled compliance evidence. Every row is an append-only observation with a timestamp, a stable identifier and a link to the official EUDAMED record.
  • Drive it from an agent or a script. Deterministic JSON, a documented schema, and a webhook-ready changes view.

How the monitoring works

watchlist -> public EUDAMED JSON API -> exact-match filter -> normalized certificate state
-> semantic fingerprint -> last-good state in the Key-Value Store
-> status / version / expiry / portfolio events -> Dataset + webhook

Three things make it a monitor rather than a diff:

A stable identity. EUDAMED gives each certificate a ULID that survives every new version, and a UUID that changes with each one. This Actor keys on the ULID. Keying on the UUID - the obvious mistake - would report every routine amendment as one certificate disappearing and another arriving.

A semantic fingerprint. A sha256 over the certificate's meaningful state: identity, number, status, type, legislation, dates, version, revision, manufacturer, notified body. Not the observation time, not the version UUID, and deliberately not days-to-expiry - that moves every day, and including it would report a change on every certificate, daily, forever.

A refusal to guess. A failed, rate-limited, unreadable, truncated or suspicious check never overwrites a good baseline and never produces an event. A EUDAMED outage cannot manufacture a suspension.

Sample input

{
"watchItems": [
{ "type": "certificate", "value": "Z-25-052-S-IX-E", "label": "Critical supplier certificate" },
{ "type": "manufacturer_srn", "value": "DE-MF-000006413", "label": "Supplier A" },
{ "type": "notified_body_srn", "value": "0633", "label": "NB portfolio" }
],
"monitorChanges": true,
"monitorId": "primary-compliance-watch",
"emitUnchanged": true,
"includeDetails": true,
"expiryWarningDays": 90,
"maxCertificatesPerWatch": 1000
}

label is yours and comes back on every row, so alerts route themselves.

A company name is rejected, not guessed into an SRN - guessing would monitor a different manufacturer and you would never know.

Sample output

One row per certificate observation. Trimmed to the interesting fields:

{
"watch_type": "notified_body_srn",
"watch_value": "0633",
"watch_label": "NB portfolio",
"check_status": "SUCCESS",
"event_type": "STATUS_CHANGED",
"change_types": ["STATUS_CHANGED", "SUSPENDED", "VERSION_CHANGED"],
"changed_fields": ["certificate_status", "version_number"],
"certificate_ulid": "01HF40MRXWKNQ07K756T8R7AES",
"certificate_number": "Z-25-052-S-IX-E",
"certificate_status": "SUSPENDED",
"certificate_status_code": "refdata.certificate-status.suspended",
"status_change_reasons": ["refdata.status-change-reason-suspended.failure-contractual-obligations"],
"applicable_legislation": "MDR",
"expiry_date": "2030-09-16",
"days_to_expiry": 1471,
"expiry_state": "VALID",
"version_number": 2,
"manufacturer_srn": "DE-MF-000006413",
"notified_body_srn": "0633",
"previous": { "certificate_status": "ISSUED", "version_number": 1 },
"fingerprint": "…",
"source_url": "https://ec.europa.eu/tools/eudamed/#/screen/search-certificate/…",
"scraped_at": "2026-09-06T06:00:00Z"
}

examples/sample_output.json carries the full 53-field shape for seven different outcomes, including a verified no-result and a source failure side by side.

Event semantics

EventFires when
FIRST_OBSERVATIONNo prior complete observation exists for this certificate
NO_CHANGETwo complete observations agree
CERTIFICATE_ADDEDA certificate appears in a portfolio that has a complete prior baseline
CERTIFICATE_CHANGEDA meaningful source field without a narrower event changed, such as a type, issue date, version-state flag or authorised representative
STATUS_CHANGEDThe source status moved between two complete observations
SUSPENDED REINSTATED WITHDRAWN RESTRICTED CANCELLEDThe specific decision status, alongside STATUS_CHANGED
VERSION_CHANGEDThe version or revision moved
EXPIRY_DATE_CHANGEDThe source expiry date itself changed
EXPIRING_SOONThe certificate crossed into your warning window since the last check
EXPIREDThe certificate crossed from non-expired to expired
MANUFACTURER_CHANGEDThe certificate now names different manufacturer SRNs
NOTIFIED_BODY_CHANGEDThe certificate now names a different notified body
MISSING_FROM_QUERYA certificate in the last complete portfolio result is absent from this one

Two deliberate absences. There is no REFUSED event: no such status exists in the public data, and inventing one would be a mapping this source cannot support. There is a CANCELLED event, because 72 live certificates use that status.

EXPIRING_SOON and EXPIRED report the crossing, not the standing state. The standing state is always in expiry_state, so nothing is hidden - only the alert is reserved for real movement.

Before a MISSING_FROM_QUERY is reported, the Actor makes one bounded check of whether EUDAMED still publishes that certificate at all, and says which it found.

Status and failure semantics

check_status is the field to branch on.

ValueMeaning
SUCCESSEUDAMED answered completely; this observation is trustworthy
NOT_FOUNDA complete, clean query found no match at check time
PARTIALThe certificate was read, but the row is less complete - the optional detail request failed, or the watch exceeded its safety cap
FAILEDThe check could not be completed: a timeout, a rate limit, an unreadable response, a rejected watch item, or the suspicious-drop guard

NOT_FOUND is never a source failure, and a source failure is never NOT_FOUND. The two states cannot collapse into each other anywhere in this Actor. An HTTP 200 carrying HTML - what the Commission's edge filter and its maintenance pages return - is classified as a source failure, never as an empty result.

Failure never propagates into your history:

  • a failed or guarded check leaves the previous baseline untouched;
  • a partially paginated portfolio produces no removal events;
  • if most of a portfolio vanishes at once, the run reports a source failure rather than dozens of MISSING_FROM_QUERY events, and keeps the old baseline so the next run can still tell you the truth;
  • one bad watch item never kills the batch - it becomes one FAILED row with a reason.

Pricing

Pay per event: $0.005 per certificate_observation, charged once per unique certificate EUDAMED answered for completely.

Never charged: rejected watch items, duplicates removed before any request, the same certificate matched by a second watch item, pagination requests, detail requests, retries, NOT_FOUND, MISSING_FROM_QUERY, PARTIAL, FAILED, timeouts, rate limits, unreadable responses, certificates held back by the guard, and - with emitUnchanged off - certificates that did not change.

A first baseline observation is charged: it is current certificate data delivered to you.

With emitUnchanged off, a quiet day on a 1,000-certificate watchlist returns nothing and costs nothing, which is what makes a standing daily schedule cheap to keep.

Hosted validation on Apify charged exactly 100 events for 100 complete certificate observations. That run used $0.000315 of platform resources against $0.50 gross event revenue. A separate 1,065-certificate source-scale run completed without retries, rate limits or source failures and converted every observation beyond the account charge cap into a free explicit MAX_TOTAL_CHARGE_REACHED diagnostic.

API

curl -X POST "https://api.apify.com/v2/acts/<username>~eudamed-certificate-status-expiry-monitor/runs?token=$APIFY_TOKEN" \
-H 'Content-Type: application/json' \
-d '{"watchItems":[{"type":"notified_body_srn","value":"0633","label":"NB portfolio"}],"emitUnchanged":false,"monitorId":"nb-0633-primary"}'

Read the changes projection after running with emitUnchanged: false:

$curl "https://api.apify.com/v2/datasets/<datasetId>/items?view=changes&clean=true&token=$APIFY_TOKEN"

Dataset views are projections, not server-side filters. Views: overview, changes, expiry, diagnostics. Run health, source counters and billing are in the RUN_SUMMARY record of the run's Key-Value Store.

Scheduling and webhooks

Save your watchlist as a Task, schedule it daily or weekly, and set emitUnchanged to false. Each Task gets its own state history automatically. For independent API-driven monitors, set a stable and distinct monitorId. Add a webhook on ACTOR.RUN.SUCCEEDED pointing at your own endpoint:

{
"eventTypes": ["ACTOR.RUN.SUCCEEDED"],
"requestUrl": "https://example.com/hooks/eudamed",
"payloadTemplate": "{\"runId\":{{resource.id}},\"datasetId\":{{resource.defaultDatasetId}}}"
}

Then fetch ?view=changes&clean=true from that dataset. A run with nothing to report sends a webhook with an empty changes view, which is a cheap heartbeat that the watch is alive.

Use with AI agents through Apify MCP

Expose the Actor as a typed tool in an authenticated MCP-compatible client:

https://mcp.apify.com?tools=automa-flow/eudamed-certificate-status-expiry-monitor

Example prompt:

Run automa-flow/eudamed-certificate-status-expiry-monitor for certificate
Z-25-052-S-IX-E and manufacturer DE-MF-000006413. For each certificate, give me the
current status, the expiry date and how many days are left, and say what changed since
the last check. Keep NOT_FOUND strictly separate from FAILED: NOT_FOUND means no
matching public record at this time, FAILED means we do not know. Do not infer
regulatory validity - quote the status and link the official record.

Because every row carries a stable source_id, scraped_at and fingerprint, runs from different weeks join to each other without any extra work.

  • This Actor is independent and not affiliated with the European Commission, EUDAMED, or any notified body, and is not certified or endorsed by them.
  • The source is public EUDAMED certificate information published by the Commission.
  • Results represent the state of the source at observation time, nothing more. EUDAMED can change after a run.
  • Output is not legal, regulatory, conformity-assessment, procurement, or medical advice.
  • NOT_FOUND does not prove a certificate is invalid. It means a complete query found no matching public record at that moment. Certificates appear only once a notified body registers them.
  • MISSING_FROM_QUERY is not withdrawal or revocation. It is an observation about a query result. EUDAMED states withdrawal in the certificate status field, and this Actor reports that separately.
  • UNKNOWN expiry state is not validity. It means no trustworthy expiry date was read.
  • Verify anything consequential against the official EUDAMED record linked in source_url on every row.
  • EUDAMED completeness and data quality are still evolving after the Notified Bodies and Certificates module became mandatory on 28 May 2026. Absence of a certificate from the public register is not evidence about the certificate itself.

The Actor collects organisation and regulatory identifiers only - manufacturer SRN and name, notified-body number, certificate numbers, statuses, dates and versions. No personal contact data is collected, and the certificate-document metadata EUDAMED exposes is deliberately not read.

Technical details

  • Public EUDAMED JSON API over plain HTTPS. No login, no cookies, no browser, no proxy, no CAPTCHA solving, no access-control circumvention.
  • The source serves about 60 requests per minute. The Actor paces itself under that budget run-wide rather than discovering the limit by being rate-limited, and honours Retry-After when it happens anyway.
  • certificateNumber and actorSrn are substring filters at the source - 25-052 matches Z-25-052-S-IX-E, and DE-MF matches hundreds of certificates. Every result is filtered client-side for exact equality, so a watch monitors what you asked for and nothing else.
  • The certificateStatus filter that appears in third-party documentation of this API does not filter; a request carrying it returns the whole register. This Actor does not use it.
  • Detail enrichment is selective: AMENDED, SUPPLEMENTED, SUSPENDED, REINSTATED, WITHDRAWN, RESTRICTED and CANCELLED certificates get one detail request because the live survey found decision or status-change data in these states. ISSUED and REISSUED stay search-only.
  • Measured live on 2026-09-06 with the seven-status policy: 1,065 certificates in 454.56 s, using 410 requests (32 search and 378 detail), 7.2 MB peak memory, zero rate limits, retries or failed queries.
  • State lives in one bounded Key-Value Store record per Task or monitorId: the last complete observation per certificate and the last complete membership per portfolio watch. Writes are verified and retried when overlapping runs race. No database or raw payloads retained.