UIDWatch

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"
}
FieldMeaning
plantrial, monitor (subscription) or paused (trial or subscription ended).
monitoringWhether the list is being compared with the gazette.
limit, usageEntries the plan monitors, and entries on the list.
trial_ends_atEnd of the free trial.
last_scan_atWhen 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
}
FieldMeaning
idThe event; the same in a retried webhook.
cursorIts place in your stream of events, for ?cursor=.
kindOne of the kinds below.
historicalTrue 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, referenceYour entry; group and reference are empty strings when not set.
company_nameThe name in the publication.
changesFor register changes (HR02): name, seat, address, purpose, capital, legal_form, uid, domicile_lost, bankruptcy_status, liquidation_status, other; else empty.
publication.id, numberThe publication in the gazette and its number.
publication.dateThe day of publication (YYYY-MM-DD).
publication.rubric, sub_rubricWhere it appeared, for example KK and KK01.
publication.titleThe official title in English, German, French and Italian.
publication.textThe register text of commercial register publications (rubric HR), else null.
publication.url, pdf_urlThe notice in the gazette and its signed PDF, the legally binding version.
detected_atWhen UIDWatch found it.
testTrue only for test events.

Kinds

KindPublications (sub-rubrics)
bankruptcyevery 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
compositioncomposition proceedings: moratoriums, creditors meetings, approval, schedules. NA01, NA02, NA03, NA04, NA06, NA07, NA08, NA09, NA10, NA11, NA12
debt_enforcementnotices 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
liquidationcalls 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
deletiondeletion from the commercial register. HR03
deficiencyregistry 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
changeother register changes, mergers, demergers, transformations, capital reductions, transfers abroad. HR02, SR01, SR02, SR03, SR04, SR05, SR06
new_entrynew 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, reference and publication.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 pays

MCP

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.

ToolWhat 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": …}}.

StatusCodes
400invalid_json
401unauthorized
403limit_exceeded, monitoring_paused, too_many_webhooks, forbidden (a form post from another site)
404not_found
405method_not_allowed
409email_taken, already_subscribed
413payload_too_large
422invalid_request, invalid_uid, invalid_url
429rate_limited, with Retry-After
500internal
503billing_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.