Monitoring & Webhooks
Watch a company for change and receive a webhook when something happens
Overview
A monitor watches one company and records an event every time something
about it changes. Each event carries a specific eventCode — INSOLVENCY_OPENED,
DIRECTOR_ADDED, COMPANY_DISSOLVED — and a broader category such as status,
ownership, or disappeared. Set a webhookUrl and every event is delivered to
your endpoint as it happens; with or without one, every event stays queryable
through the API.
Event codes mean the same thing in every jurisdiction. Code that routes on
INSOLVENCY_OPENED works for a German company and a Spanish one alike, and keeps
working as new countries are added — there is no per-country mapping table to
maintain on your side.
To exercise a webhook receiver before you have a real company to watch, use the Sandbox. Its monitoring sources fire on every check, so you can test payload parsing and retries on demand.
Create a monitor
1. Find the sources for the company
A monitor watches a company through one or more sources. Which sources are available depends on the company's jurisdiction, so ask for them by company rather than assuming:
curl "https://api.kausate.com/v2/monitors/sources?kausateId=co_de_UJybUvCx7CrPyqCtsDXsS" \
-H "X-API-Key: your_api_key"{
"sources": [
{
"name": "company_report",
"dataSource": "de-handelsregister",
"integrationName": "Handelsregister",
"jurisdictions": null,
"mode": "per_company_pull",
"producesCategories": [
"address",
"disappeared",
"financial",
"legalRepresentatives",
"other",
"status"
],
"requiredCapability": "company_report"
},
{
"name": "de_insolvenzbekanntmachungen.feed",
"dataSource": "de-insolvenzbekanntmachungen",
"integrationName": "Insolvenzbekanntmachungen",
"jurisdictions": ["de"],
"mode": "global_feed",
"producesCategories": ["financial", "other", "status"],
"requiredCapability": null
}
]
}producesCategories tells you what kinds of event each source can emit, and
integrationName identifies the register the source reads. Pass name verbatim
when you create the monitor.
2. Create the monitor
curl -X POST https://api.kausate.com/v2/monitors \
-H "X-API-Key: your_api_key" \
-H "Content-Type: application/json" \
-d '{
"kausateId": "co_de_UJybUvCx7CrPyqCtsDXsS",
"sources": ["company_report", "de_insolvenzbekanntmachungen.feed"],
"scheduleCron": "0 9 * * *",
"webhookUrl": "https://your-server.com/webhooks/kausate",
"categoriesFilter": ["status", "ownership", "disappeared"],
"metadata": { "caseId": "CASE-12345", "team": "compliance" }
}'| Field | Meaning |
|---|---|
kausateId | The company to watch |
sources | Source names, exactly as returned by discovery |
scheduleCron | How often scheduled sources check. Optional, defaults to monthly |
webhookUrl | Where events are delivered. Optional |
categoriesFilter | Deliver only these categories. Optional, defaults to all |
eventCodesFilter | Deliver only these event codes. Optional, defaults to all |
autoDeactivateCategories | Categories that switch the monitor off. Defaults to ["disappeared"] |
metadata | Your own JSON object, echoed back on every event. Optional, 4 KB max |
See Create Monitor for the full schema.
3. Receive events
Each detected change is POSTed to webhookUrl:
{
"event": "monitor.change_detected",
"monitor_id": "2859d110-ea00-4c49-8547-f359672d20f3",
"kausate_id": "co_de_UJybUvCx7CrPyqCtsDXsS",
"event_id": "61197641-56f6-4c10-90cb-716aca5a5752",
"event_code": "COMPANY_STATUS_CHANGED",
"category": "status",
"severity": "info",
"detected_at": "2026-08-06T06:52:41.397800+00:00",
"diff_path": "companyReport.basicInformation.status.normalized",
"before": "active",
"after": "inactive",
"metadata": {
"caseId": "CASE-12345",
"team": "compliance",
"source": "company_report",
"api_url": "https://api.kausate.com/v2/monitors/2859d110-ea00-4c49-8547-f359672d20f3/events/61197641-56f6-4c10-90cb-716aca5a5752"
}
}before and after hold the values that changed, and diff_path points at the
field inside the company report — for example
companyReport.basicInformation.status.normalized. When the change is to a list
of people or shareholdings, the path identifies the element that changed:
companyReport.relationships.legalRepresentatives[name='Jane Doe'].
Events from a continuous feed work slightly differently: the publication itself
is the change, so before is null, diff_path is null, and after carries
the full publication.
Sources
There are two kinds of source, and the difference decides how quickly you hear about a change.
Scheduled sources (mode: "per_company_pull") re-read the company on the
cron you set and report what moved since last time. company_report covers
status, addresses, directors, share capital, name, and legal form.
shareholder_graph covers ownership. Both are available for any company whose
jurisdiction supports the underlying capability, which is why you discover them
per company rather than from a fixed list.
The first check records a baseline. Events start from the second check onward.
Continuous feeds (mode: "global_feed") track an official publication stream
— insolvency notices, for instance — and raise an event as soon as a publication
naming your company appears. Feeds are tied to one jurisdiction and run on their
own cadence, so scheduleCron does not apply to them.
The set of sources grows as new jurisdictions and feeds ship. Read it at
runtime from GET /v2/events/sources for every
registered source, or GET /v2/monitors/sources
for the ones that apply to a given company. New sources appear there without
any change on your side, as long as you have not hardcoded the list.
Categories and event codes
Every event belongs to exactly one of seven categories:
| Category | Covers |
|---|---|
status | Company status changes (active → liquidating, insolvency proceedings) |
address | Registered or business address changes |
ownership | Shareholder or UBO additions, removals, percentage changes |
financial | Share-capital changes, financial publications |
legalRepresentatives | Director and authorized-signatory changes |
other | Name, legal form, activity changes |
disappeared | Company dissolved, struck off, liquidated, or no longer found |
Route on whichever level fits. Use category for broad fan-out — alert your ops
team on every disappeared event. Use eventCode when you need one specific
transition, such as INSOLVENCY_OPENED but not every other insolvency
publication.
match payload["category"]:
case "disappeared":
ops_alert(payload)
case "ownership":
compliance_review(payload)
case "status" if payload["event_code"].startswith("INSOLVENCY_"):
risk_team(payload)
case _:
archive(payload)The full set of codes:
| Event code | Category | Meaning |
|---|---|---|
NAME_CHANGED | other | Legal name changed |
LEGAL_FORM_CHANGED | other | Legal form changed (GmbH → AG, Ltd → PLC) |
ACTIVITIES_CHANGED | other | Declared business activities changed |
REGISTERED_ADDRESS_CHANGED | address | Registered office address changed |
BUSINESS_ADDRESS_CHANGED | address | Operating address changed |
JURISDICTION_CHANGED | address | Registry jurisdiction changed (cross-border seat transfer) |
COMPANY_STATUS_CHANGED | status | Status on the register changed |
REGISTRATION_DATE_CHANGED | status | Registration date changed, usually a registry correction |
VAT_REGISTRATION_CHANGED | status | VAT registration granted or cancelled |
FISCAL_INACTIVITY_CHANGED | status | Fiscal-inactivity flag set or cleared |
SHARE_CAPITAL_CHANGED | financial | Share-capital amount changed |
COMPANY_DISSOLVED | disappeared | Company dissolved |
COMPANY_STRUCK_OFF | disappeared | Struck off the register |
COMPANY_LIQUIDATED | disappeared | Liquidation completed |
COMPANY_NOT_FOUND | disappeared | Register no longer returns the company |
DIRECTOR_ADDED | legalRepresentatives | Director or officer appointed |
DIRECTOR_REMOVED | legalRepresentatives | Director or officer left |
DIRECTOR_ROLE_CHANGED | legalRepresentatives | An existing director's role changed |
AUTHORIZED_SIGNATORY_CHANGED | legalRepresentatives | Signatory powers changed |
UBO_ADDED | ownership | New UBO declared |
UBO_REMOVED | ownership | UBO removed |
UBO_PERCENTAGE_CHANGED | ownership | An existing UBO's stake changed |
SHAREHOLDER_ADDED | ownership | New shareholder |
SHAREHOLDER_REMOVED | ownership | Shareholder left |
SHAREHOLDING_PERCENTAGE_CHANGED | ownership | An existing shareholder's stake changed |
INSOLVENCY_PROTECTIVE_MEASURES | status | Protective measures ordered |
INSOLVENCY_DISMISSED_FOR_LACK_OF_ASSETS | status | Application dismissed for lack of assets |
INSOLVENCY_OPENED | status | Insolvency proceedings formally opened |
INSOLVENCY_DECISION | status | Decision issued in ongoing proceedings |
INSOLVENCY_LIFTED | status | Proceedings lifted or closed |
INSOLVENCY_DISTRIBUTION_PUBLISHED | financial | Distribution to creditors published |
INSOLVENCY_DISCHARGE_DECISION | status | Discharge decision issued |
INSOLVENCY_PLAN_SUPERVISED | status | Insolvency plan or supervised proceedings |
INSOLVENCY_OTHER | other | Insolvency event not covered above |
OTHER | other | Change without a more specific code |
A few codes are reserved for capabilities that no source emits yet:
UBO_ADDED, UBO_REMOVED, UBO_PERCENTAGE_CHANGED, BUSINESS_ADDRESS_CHANGED,
JURISDICTION_CHANGED, and COMPANY_NOT_FOUND. Ownership changes reach you today
as SHAREHOLDER_ADDED, SHAREHOLDER_REMOVED, and
SHAREHOLDING_PERCENTAGE_CHANGED. Handle the reserved codes if they arrive, but
do not build a feature that depends on them.
Rather than copying the table, fetch it from
GET /v2/events/taxonomy at startup
and build your routing table from the response. Codes added in later releases are
then picked up automatically.
Filtering deliveries
categoriesFilter and eventCodesFilter decide what reaches your webhook. Set
both and an event must satisfy each independently to be delivered.
Filters are not a knob you can widen later. Neither filter backfills, and for continuous feeds a filtered-out event is never recorded at all — it cannot be recovered afterwards. Events from scheduled sources are always recorded and stay queryable even when a filter kept them off your webhook. If you might want a category later, subscribe to it now and discard it on your side.
Turning a monitor off automatically
By default, any event in the disappeared category switches the monitor off and
records a deactivationReason. A company that no longer exists stops costing you
scheduled checks that cannot surface anything new, and the events already
collected stay queryable.
Change the behaviour when you create the monitor:
// also switch off when insolvency proceedings open
"autoDeactivateCategories": ["disappeared", "status"]
// never switch off (an empty list is different from omitting the field)
"autoDeactivateCategories": []A switched-off monitor is preserved: you can still read it, list its events, and
replay them. It simply stops checking. Use
DELETE /v2/monitors/{id} to remove it
entirely.
Correlating events with your own records
The optional metadata object you set on a monitor is echoed back on every event
it produces, so you do not need a separate monitor-ID-to-case-ID lookup:
"metadata": { "caseId": "CASE-12345", "team": "compliance" }It comes back verbatim on monitor responses, and merged into each event's
metadata alongside Kausate's own annotations — source on every event,
external_id and publication details on feed events, and api_url on webhook
deliveries. Kausate's keys win on a name collision, so avoid source,
external_id, jurisdiction, native_publication_type, and api_url if you
need your own values back unchanged. The object must be JSON and 4 KB or smaller.
Webhook delivery
Monitor events are POSTed to the webhookUrl on the monitor with two headers,
Content-Type: application/json and Kausate-Version.
Delivery is at least once. A failed delivery is retried with exponential
backoff, starting at one second and settling at one attempt every four hours, for
up to two days. A brief outage on your side will not drop events — but the same
event can arrive twice, so deduplicate on event_id, which is stable for the life
of the event including across replays. Return a 2xx within 30 seconds and move
slow work into your own queue; anything else counts as a failure and triggers a
retry.
If your endpoint is down for longer than two days, the delivery is abandoned. The event itself is not lost — replay it once you are back up.
Changing webhookUrl on an existing monitor affects future events only. Past
events stay queryable but are not re-sent.
Each monitor carries its own webhookUrl. To route everything to one endpoint,
set the same URL on every monitor.
Authenticating the requests
Monitor webhooks are not signed. Unlike the order-result webhooks
described in the Webhooks reference, there is no
HMAC signature on a monitor.change_detected payload, so your endpoint has to
authenticate the request another way.
- Recommended — host the receiver on an unguessable HTTPS path, such as
https://your-app.example.com/webhooks/kausate-monitors/<random-token>, and reject every other path. Treat the path as a shared secret. - Stronger — put a reverse proxy in front of the receiver that injects a
static
Authorizationheader for requests on the monitor-webhook path, and check it in your handler. Monitor webhooks do not carry custom headers, so this has to live in your infrastructure.
Either way the secret stays out of your application code. You can additionally restrict your endpoint to Kausate's outbound IP addresses, but treat that as a second layer rather than your only control.
Event history and replay
Every event a monitor records is queryable, including ones a filter kept off your webhook. This is how you recover after an outage, backfill a category you only just started caring about, or rebuild a downstream system from history.
# Filter by eventCode, category, since, and until
curl "https://api.kausate.com/v2/monitors/$MONITOR_ID/events?limit=100&category=status&since=2026-05-01T00:00:00Z" \
-H "X-API-Key: your_api_key"Each event includes a deliveries array recording what happened to it —
status, target, deliveredAt, and lastError — so you can confirm a delivery
without waiting for the push.
Re-send a single event with
POST /v2/monitors/{id}/events/{eventId}/replay,
which returns 202 {"status":"queued"}:
curl -X POST "https://api.kausate.com/v2/monitors/$MONITOR_ID/events/$EVENT_ID/replay" \
-H "X-API-Key: your_api_key"A replay reuses the original event_id, so an idempotent receiver deduplicates it
correctly. There is no bulk replay — loop with your own rate-limit-aware backoff
if you need to re-send many events.
Choosing a schedule
scheduleCron controls how often scheduled sources re-read the company. Every
scheduled source makes its own fresh registry call on each run, so the cron and
the number of scheduled sources together decide how much work a monitor does.
- Omit
scheduleCronfor the monthly default (0 0 1 * *), or pass any valid five-field expression —0 9 * * 1for Monday mornings,0 9 * * *for daily. - A monitor watching both
company_reportandshareholder_graphmakes two registry calls per run, not one. Subscribe only to the sources you act on. - Continuous feeds ignore the cron entirely. A monitor with only feed sources never runs a scheduled check.
- Leave auto-deactivation on so dissolved companies stop being checked.
Troubleshooting
My webhook is not firing
- Is the monitor still on? Read
GET /v2/monitors/{id}and checkisActive. If auto-deactivation switched it off,deactivationReasonsays why. - Was the event recorded? List the monitor's events with
sinceset to your last known delivery. If the event is there, it was detected, and the problem is filtering or delivery. - Do your filters allow it? The same response returns
categoriesFilterandeventCodesFilter. An event matching neither is never delivered. - Is your endpoint healthy? Replay the event and watch your server logs for the new attempt. Any non-2xx response, or one taking longer than 30 seconds, counts as a failure.
- Still failing after a replay? The problem is on your side — DNS, TLS,
firewall, or a slow handler. The
deliveriesarray on the event carrieslastErrorfrom the last attempt.
I am receiving the same event twice
Expected — delivery is at least once. Deduplicate on event_id.
An address change fired but the address looks the same
Directors and shareholdings are compared by identity, so reordering them between
checks does not produce spurious events. Addresses are compared as a whole list,
so a register that returns the same addresses in a different order can raise a
REGISTERED_ADDRESS_CHANGED that is not a real move. Treat that event as a prompt
to re-read the company report rather than as proof the address changed.
A feed event matched the wrong company
Feeds match publications to companies on the identifiers the publication carries.
Two companies sharing an identifier across different sub-registers, or a
publication still using an identifier from before a transfer, can produce a false
match. Check metadata.external_id and the publication in after, and contact
support if the mismatch is systematic.
before is null
Expected for feed events — the publication itself is the change, so there is no previous state. Also expected on the very first event from a scheduled source, where the baseline is the first check.
My monitor switched itself off and I want it back
Recreate it with autoDeactivateCategories: [], or contact support to reactivate
the existing one. Recreating is usually faster.
Errors
addressesdiffs are still order-sensitive. Directors and shareholder-graph nodes/edges are diffed by identity key, so reordering them between ticks does not fire spurious events. ButcompanyReport.basicInformation.addresseshas no identity key — it's compared as a whole list — so a registry that returns the same addresses in a different order across two ticks can still fire a spuriousREGISTERED_ADDRESS_CHANGED. Treat that one as a trigger to re-read the report and reconcile, not as proof the address actually changed.- Some event codes are defined but not yet emitted.
UBO_ADDED,UBO_REMOVED, andUBO_PERCENTAGE_CHANGEDare reserved for a future UBO source — no source emits them today. Ownership changes currently surface fromshareholder_graphasSHAREHOLDER_ADDED,SHAREHOLDER_REMOVED, andSHAREHOLDING_PERCENTAGE_CHANGED. The same applies toBUSINESS_ADDRESS_CHANGED,JURISDICTION_CHANGED, andCOMPANY_NOT_FOUND. Handle them defensively if they appear, but don't build a feature that depends on them firing. - Cron is optional and defaults to monthly. Omit
scheduleCronto get0 0 1 * *(midnight UTC on the 1st), or pass any valid expression (e.g.0 0 * * *) — it's ignored when no per-company source is present. - No org-wide monitor webhook. Each monitor specifies its own
webhookUrl. To route all monitors to one endpoint, set the same URL on every monitor at create time. - No webhook signing. See the Webhook security section above for the recommended workarounds.
- Replay is one event at a time. There's no bulk replay endpoint; if you need to replay 1000 events, script the loop yourself with rate-limit-aware backoff.
Common errors
Branch on the code field of the error body — never on the wording of
detail. See the Errors guide for the body shape and the
prefix-matching contract.
| Status | code | Reason | Fix |
|---|---|---|---|
404 | resource.not_found | The kausateId, monitor or event doesn't exist | Run a search first via /v2/companies/search/indexed, or re-list your monitors |
422 | request.invalid | The request body is well-formed but unusable: unknown monitoring source, a source not applicable to that company, an invalid cron, or a stale eventCodesFilter value | Read detail for which one; discover sources via /v2/events/sources or /v2/monitors/sources?kausateId=…, codes via /v2/events/taxonomy |
422 covers every request-shape failure, so code alone does not distinguish
the four causes — detail names the offending field. Everything above is
client-fixable; nothing here should be retried unchanged.
Last updated on