UIDWatch API and webhooks
The API keeps a watchlist of Swiss company UIDs, gives you the events the Swiss Official Gazette of Commerce brings for them, posts them as signed webhooks, and starts a subscription. Mail settings, API keys, CSV downloads, password and account deletion are in the web app. Machine-readable: openapi.json · llms.txt. Using the API means accepting the terms.
Quick start
Sign up in the web app and create a key under Settings, or sign up over the API — the answer
holds a key that works at once. The trial lasts 30 days with
20 entries, and 2,500 once a person confirms the email address from
the mail we send; a PUT of a longer list before that ends in 403 limit_exceeded. Signing up over the API needs "accept_terms": true: it
accepts the terms on behalf of your business, including the data processing terms
in section 8.
curl -X POST https://uidwatch.tollwyn.com/api/v1/signup \
-H 'Content-Type: application/json' \
-d '{"email": "credit@example.com", "accept_terms": true}' Send the key in every request as Authorization: Bearer uw_…. Bodies are JSON with Content-Type: application/json.
Account
GET /api/v1/account tells what the key may do and whether monitoring runs:
{
"id": "6f1c…",
"email": "credit@example.com",
"email_verified": true,
"plan": "trial",
"monitoring": true,
"limit": 2500,
"usage": 812,
"trial_ends_at": "2026-11-02T08:14:03.000Z",
"last_scan_at": "2026-10-03T07:45:00.000Z",
"created_at": "2026-10-03T08:14:03.000Z"
} | Field | Meaning |
|---|---|
plan | trial, monitor (subscription) or paused (trial or subscription
ended). |
monitoring | Whether the list is being compared with the gazette. |
limit, usage | Entries the plan monitors, and entries on the list. |
trial_ends_at | End of the free trial. |
last_scan_at | When the gazette was last checked successfully; the sign of life of the monitoring. Null for a few minutes after a restart of the service, until the first check. |
Watchlist
An entry is a UID with your own reference (up to 200 characters) and an optional group (up to
100), for example the client a debtor belongs to. UIDs are accepted as CHE-123.456.789, CHE123456789 or with MWST/TVA/IVA; the check digit must match. Requests are all-or-nothing: one invalid UID
returns 422 with every problem listed.
curl -X POST https://uidwatch.tollwyn.com/api/v1/watchlist \
-H "Authorization: Bearer $UIDWATCH_KEY" \
-H 'Content-Type: application/json' \
-d '{"items": [
{"uid": "CHE-110.090.028", "reference": "Customer 10023", "group": "Client A"},
{"uid": "CHE-345.657.563 MWST", "reference": "Customer 10024"}
]}' PUT /api/v1/watchlist replaces the whole list (entries that stay keep their date) —
the way to sync from an ERP every night. GET /api/v1/watchlist pages through the
list with checked_at (initial check done) and monitored (within the
plan limit); DELETE /api/v1/watchlist/CHE-…?group=… removes one entry.
curl -X PUT https://uidwatch.tollwyn.com/api/v1/watchlist \
-H "Authorization: Bearer $UIDWATCH_KEY" \
-H 'Content-Type: application/json' \
-d @all-customers.json New entries get an initial check against the gazette's history in the risk rubrics; what it
finds is stored as events with "historical": true and never posted to webhooks. A summary
goes out by mail after the first check, and after later ones when they find something. Notices in
which a registry office names several companies in its text (BH05, BH07) are found from the day an
entry is added, not in its history.
Events
An event is one publication that concerns one entry. Poll with a cursor, or use webhooks, or
both. Filters: kind, historical, uid, group.
curl "https://uidwatch.tollwyn.com/api/v1/events?cursor=0&limit=100" \
-H "Authorization: Bearer $UIDWATCH_KEY"
# → {"events": [...], "next_cursor": "1842", "has_more": false}
# Store next_cursor and ask with it next time: each event comes exactly once. The event object
{
"id": "0b6c2f0e-9d1a-4f7e-8a51-3c2d1e0f9a7b",
"cursor": "1842",
"kind": "bankruptcy",
"historical": false,
"uid": "CHE-345.657.563",
"group": "Client A",
"reference": "Customer 10024",
"company_name": "D-Technic GmbH",
"changes": [],
"publication": {
"id": "95f0bd09-f191-41c7-8dec-c111fc40f410",
"number": "KK01-0000071221",
"date": "2026-10-02",
"rubric": "KK",
"sub_rubric": "KK01",
"title": {
"en": "Provisional announcement of bankruptcy D-Technic GmbH",
"de": "Vorläufige Konkursanzeige D-Technic GmbH",
"fr": "Avis provisoire de faillite D-Technic GmbH",
"it": "Avviso provvisorio di fallimento D-Technic GmbH"
},
"text": null,
"url": "https://www.shab.ch/#!/search/publications/detail/95f0bd09-f191-41c7-8dec-c111fc40f410",
"pdf_url": "https://amtsblattportal.ch/api/v1/publications/95f0bd09-f191-41c7-8dec-c111fc40f410/pdf"
},
"detected_at": "2026-10-02T00:52:11Z",
"test": false
} | Field | Meaning |
|---|---|
id | The event; the same in a retried webhook. |
cursor | Its place in your stream of events, for ?cursor=. |
kind | One of the kinds below. |
historical | True when the publication appeared before you added the entry: a finding of the initial check, or a publication of the last days found right after adding. |
uid, group, reference | Your entry; group and reference are empty strings when not set. |
company_name | The name in the publication. |
changes | For register changes (HR02): name, seat, address, purpose, capital, legal_form, uid, domicile_lost, bankruptcy_status, liquidation_status, other; else empty. |
publication.id, number | The publication in the gazette and its number. |
publication.date | The day of publication (YYYY-MM-DD). |
publication.rubric, sub_rubric | Where it appeared, for example KK and KK01. |
publication.title | The official title in English, German, French and Italian. |
publication.text | The register text of commercial register publications (rubric HR), else null. |
publication.url, pdf_url | The notice in the gazette and its signed PDF, the legally binding version. |
detected_at | When UIDWatch found it. |
test | True only for test events. |
Kinds
| Kind | Publications (sub-rubrics) |
|---|---|
bankruptcy | every bankruptcy notice (opening, call to creditors, suspension, schedules, auctions, closing, revocation, foreign bankruptcies), bankruptcy in composition proceedings, and register changes (HR02) that record a bankruptcy. KK01, KK02, KK03, KK04, KK05, KK06, KK07, KK08, KK09, KK10, KK11, KK12, NA05 |
composition | composition proceedings: moratoriums, creditors meetings, approval, schedules. NA01, NA02, NA03, NA04, NA06, NA07, NA08, NA09, NA10, NA11, NA12 |
debt_enforcement | notices of debt enforcement offices: payment orders and seizures they could not serve in person, auctions, sequestrations, bankruptcy warnings. SB01, SB02, SB03, SB04, SB05, SB06, SB07 |
liquidation | calls to creditors in liquidation, and register changes (HR02) that record a dissolution or add “in Liquidation” to the name. LS01, LS02, LS03, LS04, LS05, LS06, LS07, LS08 |
deletion | deletion from the commercial register. HR03 |
deficiency | registry office notices on organisational deficiencies and decisions before a forced dissolution or deletion, also when the office names several companies in its text. BH00, BH05, BH06, BH07 |
change | other register changes, mergers, demergers, transformations, capital reductions, transfers abroad. HR02, SR01, SR02, SR03, SR04, SR05, SR06 |
new_entry | new entries in the commercial register. HR01 |
A kind names a topic, not a direction: the revocation of a bankruptcy is a bankruptcy event too. The official title says which step it is.
Webhooks
Up to 5 endpoints per account, each with its own kinds. Every new event (not the history) is posted as the event object above. The URL must use https and a public address; redirects are not followed; an answer within 10 seconds with a 2xx status counts as delivered. Anything else is retried after 1 minute, 5 minutes, 30 minutes, 2 hours, 6 hours and 12 hours (about 21 hours in all), then the delivery counts as failed. The last 50 deliveries of each endpoint are listed under Settings.
curl -X POST https://uidwatch.tollwyn.com/api/v1/webhooks \
-H "Authorization: Bearer $UIDWATCH_KEY" \
-H 'Content-Type: application/json' \
-d '{"url": "https://erp.example.com/hooks/uidwatch", "kinds": ["bankruptcy", "liquidation", "deletion"]}'
# → {"id": "...", "url": "...", "kinds": [...], "secret": "whsec_..."} (the secret is shown only here) Each post carries UIDWatch-Signature: t=<unix seconds>,v1=<hex>, the
HMAC-SHA256 of t + "." + body with the endpoint's secret, and UIDWatch-Event-Id to recognise a retried event. Check the signature against the raw body
and refuse old timestamps:
import { createHmac, timingSafeEqual } from 'node:crypto';
// header: the UIDWatch-Signature header; rawBody: the request body exactly as received.
export function verify(header, rawBody, secret, toleranceSeconds = 300) {
const parts = Object.fromEntries(
String(header ?? '').split(',').map((part) => part.split('=', 2))
);
if (!/^\d+$/.test(parts.t ?? '') || !/^[0-9a-f]{64}$/.test(parts.v1 ?? '')) return false;
if (Math.abs(Date.now() / 1000 - Number(parts.t)) > toleranceSeconds) return false;
const expected = createHmac('sha256', secret).update(`${parts.t}.${rawBody}`).digest();
return timingSafeEqual(Buffer.from(parts.v1, 'hex'), expected);
} import hashlib, hmac, re, time
def verify(header: str, raw_body: bytes, secret: str, tolerance: int = 300) -> bool:
parts = dict(part.split("=", 1) for part in (header or "").split(",") if "=" in part)
t, v1 = parts.get("t", ""), parts.get("v1", "")
if not t.isdigit() or not re.fullmatch(r"[0-9a-f]{64}", v1):
return False
if abs(time.time() - int(t)) > tolerance:
return False
expected = hmac.new(secret.encode(), t.encode() + b"." + raw_body, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, v1) POST /api/v1/webhooks/{id}/test (or “Send test event” under Settings)
posts a sample event with "test": true at once (10 per minute at most).
Zapier, Make and n8n
The webhook is plain JSON, so it plugs into automation tools without code. Take the URL the tool gives you, add it as a webhook, send a test event, and map the fields.
- Zapier: trigger “Webhooks by Zapier → Catch Hook”; then for example “Slack →
Send Channel Message” with
company_name,kind,referenceandpublication.url, or “Google Sheets → Create Spreadsheet Row”. - Make: module “Webhooks → Custom webhook”, then “Microsoft Teams → Send a message” or any other module.
- n8n: a “Webhook” node (method POST, production URL), followed by a Code node that checks the signature as above, then Slack, Teams or a database.
Use a filter step on test to keep test events out of your channels.
Billing
Payment runs through Stripe. POST /api/v1/billing/checkout returns a Checkout link
(CHF 33 per month per package of 2,500 entries, 1–10
packages); a person — or an agent with its own payment method — opens it and pays. POST /api/v1/billing/portal returns the customer portal link to change packages, update
the card, download invoices or cancel.
curl -X POST https://uidwatch.tollwyn.com/api/v1/billing/checkout \
-H "Authorization: Bearer $UIDWATCH_KEY" \
-H 'Content-Type: application/json' -d '{"blocks": 2}'
# → {"url": "https://checkout.stripe.com/..."} a person opens it and paysMCP
Agents can use UIDWatch as a remote MCP server at https://uidwatch.tollwyn.com/mcp (Streamable HTTP). Its
tools are the API above, with the same key in the header Authorization: Bearer uw_…, the same limits and the same errors, which come back as
tool errors with status, code and message. Only uidwatch_signup works without a key: it accepts the terms on behalf
of your business, creates the account and returns its key once.
| Tool | What it does |
|---|---|
uidwatch_signup(email, via, accept_terms) | Sign up |
watchlist_replace(items) | Replace the watchlist |
watchlist_add(items) | Add to the watchlist |
watchlist_list(limit, offset, group) | List the watchlist |
watchlist_remove(uid, group) | Remove from the watchlist |
events_list(cursor, limit, kind, historical, uid, group) | List events |
webhook_create(url, kinds) | Add a webhook |
webhook_list() | List webhooks |
webhook_delete(id) | Delete a webhook |
webhook_test(id) | Test a webhook |
account_status() | Account status |
billing_checkout(blocks) | Subscribe |
An MCP client needs only the address and the header. Without a client, a tool call is one JSON-RPC post, no session needed:
curl -X POST https://uidwatch.tollwyn.com/mcp \
-H 'Content-Type: application/json' \
-H 'Accept: application/json, text/event-stream' \
-H "Authorization: Bearer $UIDWATCH_KEY" \
-d '{"jsonrpc": "2.0", "id": 1, "method": "tools/call",
"params": {"name": "events_list", "arguments": {"cursor": "0", "limit": 10}}}'
# → event: message
# data: {"jsonrpc": "2.0", "id": 1, "result": {"content": [{"type": "text",
# "text": "{\"events\": [...], \"next_cursor\": \"1842\", \"has_more\": false}"}]}}Limits and errors
- 120 requests per minute and key; sign-ups 10 per hour from one address; test events 10 per minute and account.
- Up to 25,000 entries and 10 MB per request.
- The plan limit counts entries (UID and group). Over the limit, the oldest entries up to the limit are monitored and adding is refused.
Errors are JSON: {"error": {"code": "…", "message": "…", "details": …}}.
| Status | Codes |
|---|---|
| 400 | invalid_json |
| 401 | unauthorized |
| 403 | limit_exceeded, monitoring_paused, too_many_webhooks, forbidden (a form post from another site) |
| 404 | not_found |
| 405 | method_not_allowed |
| 409 | email_taken, already_subscribed |
| 413 | payload_too_large |
| 422 | invalid_request, invalid_uid, invalid_url |
| 429 | rate_limited, with Retry-After |
| 500 | internal |
| 503 | billing_unavailable |
Data and timing
The source is the Swiss Official Gazette of Commerce (SOGC), read through the open API of the official gazettes portal. It publishes each working day shortly after midnight, Swiss time; we check every 15 minutes, so events of the day usually arrive around 1 a.m. Commercial register publications reach back to September 2018, bankruptcy, composition and liquidation notices to 2021, debt enforcement notices mainly to 2025, registry office notices to 2019. Publications about people without a UID are not matched. A publication withdrawn later stays as it was delivered. The legally binding version of every notice is its signed PDF.