API reference
Build on the UdiWatch API.
Watch public EUDAMED UDI-DI and manufacturer SRN records. Search, store a baseline, and read a diff when a watched field changes. Each route has its own page.
Not an official European Commission service. Records can be missing, late, or wrong because the upstream register is. Do not treat a response as a certificate of conformity.
Quickstart
Create an account in the app, or with POST /v1/trial. The first response contains the only copy of the API key. Store it. A later login returns a session, not the key.
BASE=https://udiwatch.com
curl -sS -X POST "$BASE/v1/trial" \
-H 'Content-Type: application/json' \
-d '{"email":"[email protected]","password":"at-least-8"}'
export UW_KEY='uw_…'
curl -sS "$BASE/v1/search?q=16977660098955"
curl -sS -X POST "$BASE/v1/watches" \
-H "X-API-Key: $UW_KEY" -H 'Content-Type: application/json' \
-d '{"subject":"16977660098955"}'
curl -sS -X POST "$BASE/v1/sync" \
-H "X-API-Key: $UW_KEY" -H 'Content-Type: application/json' \
-d '{}'
The same routes answer on https://udiwatch.com, https://app.udiwatch.com, and https://docs.udiwatch.com. Use one host and stay on it. A POST with no body is rejected upstream with 403, so send {} when you have no fields.
Conventions
- JSON in, JSON out, UTF-8. Send Content-Type: application/json on POST.
- Times are ISO-8601 UTC.
- A subject that matches CC-XX-digits (for example AT-MF-000000252) is an SRN. Anything else is a UDI-DI. Spaces are stripped. SRNs are uppercased.
- CORS is open on /v1/* and /mcp.
- There is no /v2. The live contract is openapi.json.
Authentication
Two credentials exist. They are not interchangeable.
| Credential | Header | Acts as |
|---|---|---|
| API key uw_… | X-API-Key or Authorization: Bearer uw_… | The account. Can manage users, keys, and the webhook. |
| Session us_… | Authorization: Bearer us_… | One user. An owner can manage the account. A member can use watches, not users, keys, or the webhook. |
Public reads need no credential: search, one device, one actor, OpenAPI, health, and MCP initialize / tools/list / device search tools.
Errors
Error bodies are {"error": "…"}. The message is the one you should show. Do not match on it if a status code exists.
| Status | When |
|---|---|
| 400 | Bad email, short password, empty subject, webhook that is not public https, bad id. |
| 401 | Missing or unknown key or session. Login with a wrong password. |
| 402 | The period has ended. Sync and new watches stop. A cancel during an open trial does not end access early. |
| 403 | A member tried to manage users, keys, or the webhook. |
| 404 | Unknown path, or a UDI that is not in the public rows we could read. |
| 409 | Watch cap, user cap, or API-key cap. |
| 429 | Rate limit. |
Limits
| Limit | Value |
|---|---|
| Trial creates | 8 per IP per hour |
| Logins | 20 per IP per hour |
| Search | 40 per IP per minute |
| MCP | 60 calls per IP per minute |
| Watches | 25 on a trial |
| Users and active API keys | 10 each |
| CSV import | 500 lines, still inside the watch cap |
| Audit trail | Newest 100 on REST, newest 50 on MCP list_changes |
Watches and diffs
A watch is one UDI-DI or one SRN on one account. Adding it reads the register immediately and stores that snapshot. That first read is a baseline and does not create an event. The next successful read compares fields and writes an event only when something changed.
- A new value that is empty does not count as a change.
- If the new read failed, certificate diffs are skipped. A timeout is not “certificate removed”.
- No expiry in the source means no expiry in the payload.
- Watching the same subject again does not reset the baseline.
Questions about a key: [email protected].

UdiWatch