EUDAMED Certificate Status & Expiry Monitor
Pricing
$5.00 / 1,000 certificate observations
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
Maintained by CommunityActor stats
0
Bookmarked
2
Total users
1
Monthly active users
2 days ago
Last modified
Categories
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_stateanddays_to_expiryon every row, and anEXPIRING_SOONevent 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,CANCELLEDandREINSTATEDare 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
changesview.
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
| Event | Fires when |
|---|---|
FIRST_OBSERVATION | No prior complete observation exists for this certificate |
NO_CHANGE | Two complete observations agree |
CERTIFICATE_ADDED | A certificate appears in a portfolio that has a complete prior baseline |
CERTIFICATE_CHANGED | A meaningful source field without a narrower event changed, such as a type, issue date, version-state flag or authorised representative |
STATUS_CHANGED | The source status moved between two complete observations |
SUSPENDED REINSTATED WITHDRAWN RESTRICTED CANCELLED | The specific decision status, alongside STATUS_CHANGED |
VERSION_CHANGED | The version or revision moved |
EXPIRY_DATE_CHANGED | The source expiry date itself changed |
EXPIRING_SOON | The certificate crossed into your warning window since the last check |
EXPIRED | The certificate crossed from non-expired to expired |
MANUFACTURER_CHANGED | The certificate now names different manufacturer SRNs |
NOTIFIED_BODY_CHANGED | The certificate now names a different notified body |
MISSING_FROM_QUERY | A 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.
| Value | Meaning |
|---|---|
SUCCESS | EUDAMED answered completely; this observation is trustworthy |
NOT_FOUND | A complete, clean query found no match at check time |
PARTIAL | The certificate was read, but the row is less complete - the optional detail request failed, or the watch exceeded its safety cap |
FAILED | The 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_QUERYevents, 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
FAILEDrow 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 certificateZ-25-052-S-IX-E and manufacturer DE-MF-000006413. For each certificate, give me thecurrent status, the expiry date and how many days are left, and say what changed sincethe last check. Keep NOT_FOUND strictly separate from FAILED: NOT_FOUND means nomatching public record at this time, FAILED means we do not know. Do not inferregulatory 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.
Limitations and legal positioning
- 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_FOUNDdoes 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_QUERYis 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.UNKNOWNexpiry state is not validity. It means no trustworthy expiry date was read.- Verify anything consequential against the official EUDAMED record linked in
source_urlon 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-Afterwhen it happens anyway. certificateNumberandactorSrnare substring filters at the source -25-052matchesZ-25-052-S-IX-E, andDE-MFmatches hundreds of certificates. Every result is filtered client-side for exact equality, so a watch monitors what you asked for and nothing else.- The
certificateStatusfilter 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.