KausateKausate Docs
Features

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 eventCodeINSOLVENCY_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" }
  }'
FieldMeaning
kausateIdThe company to watch
sourcesSource names, exactly as returned by discovery
scheduleCronHow often scheduled sources check. Optional, defaults to monthly
webhookUrlWhere events are delivered. Optional
categoriesFilterDeliver only these categories. Optional, defaults to all
eventCodesFilterDeliver only these event codes. Optional, defaults to all
autoDeactivateCategoriesCategories that switch the monitor off. Defaults to ["disappeared"]
metadataYour 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:

CategoryCovers
statusCompany status changes (active → liquidating, insolvency proceedings)
addressRegistered or business address changes
ownershipShareholder or UBO additions, removals, percentage changes
financialShare-capital changes, financial publications
legalRepresentativesDirector and authorized-signatory changes
otherName, legal form, activity changes
disappearedCompany 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 codeCategoryMeaning
NAME_CHANGEDotherLegal name changed
LEGAL_FORM_CHANGEDotherLegal form changed (GmbH → AG, Ltd → PLC)
ACTIVITIES_CHANGEDotherDeclared business activities changed
REGISTERED_ADDRESS_CHANGEDaddressRegistered office address changed
BUSINESS_ADDRESS_CHANGEDaddressOperating address changed
JURISDICTION_CHANGEDaddressRegistry jurisdiction changed (cross-border seat transfer)
COMPANY_STATUS_CHANGEDstatusStatus on the register changed
REGISTRATION_DATE_CHANGEDstatusRegistration date changed, usually a registry correction
VAT_REGISTRATION_CHANGEDstatusVAT registration granted or cancelled
FISCAL_INACTIVITY_CHANGEDstatusFiscal-inactivity flag set or cleared
SHARE_CAPITAL_CHANGEDfinancialShare-capital amount changed
COMPANY_DISSOLVEDdisappearedCompany dissolved
COMPANY_STRUCK_OFFdisappearedStruck off the register
COMPANY_LIQUIDATEDdisappearedLiquidation completed
COMPANY_NOT_FOUNDdisappearedRegister no longer returns the company
DIRECTOR_ADDEDlegalRepresentativesDirector or officer appointed
DIRECTOR_REMOVEDlegalRepresentativesDirector or officer left
DIRECTOR_ROLE_CHANGEDlegalRepresentativesAn existing director's role changed
AUTHORIZED_SIGNATORY_CHANGEDlegalRepresentativesSignatory powers changed
UBO_ADDEDownershipNew UBO declared
UBO_REMOVEDownershipUBO removed
UBO_PERCENTAGE_CHANGEDownershipAn existing UBO's stake changed
SHAREHOLDER_ADDEDownershipNew shareholder
SHAREHOLDER_REMOVEDownershipShareholder left
SHAREHOLDING_PERCENTAGE_CHANGEDownershipAn existing shareholder's stake changed
INSOLVENCY_PROTECTIVE_MEASURESstatusProtective measures ordered
INSOLVENCY_DISMISSED_FOR_LACK_OF_ASSETSstatusApplication dismissed for lack of assets
INSOLVENCY_OPENEDstatusInsolvency proceedings formally opened
INSOLVENCY_DECISIONstatusDecision issued in ongoing proceedings
INSOLVENCY_LIFTEDstatusProceedings lifted or closed
INSOLVENCY_DISTRIBUTION_PUBLISHEDfinancialDistribution to creditors published
INSOLVENCY_DISCHARGE_DECISIONstatusDischarge decision issued
INSOLVENCY_PLAN_SUPERVISEDstatusInsolvency plan or supervised proceedings
INSOLVENCY_OTHERotherInsolvency event not covered above
OTHERotherChange 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 Authorization header 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 scheduleCron for the monthly default (0 0 1 * *), or pass any valid five-field expression — 0 9 * * 1 for Monday mornings, 0 9 * * * for daily.
  • A monitor watching both company_report and shareholder_graph makes 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

  1. Is the monitor still on? Read GET /v2/monitors/{id} and check isActive. If auto-deactivation switched it off, deactivationReason says why.
  2. Was the event recorded? List the monitor's events with since set to your last known delivery. If the event is there, it was detected, and the problem is filtering or delivery.
  3. Do your filters allow it? The same response returns categoriesFilter and eventCodesFilter. An event matching neither is never delivered.
  4. 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.
  5. Still failing after a replay? The problem is on your side — DNS, TLS, firewall, or a slow handler. The deliveries array on the event carries lastError from 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

  • addresses diffs 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. But companyReport.basicInformation.addresses has 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 spurious REGISTERED_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, and UBO_PERCENTAGE_CHANGED are reserved for a future UBO source — no source emits them today. Ownership changes currently surface from shareholder_graph as SHAREHOLDER_ADDED, SHAREHOLDER_REMOVED, and SHAREHOLDING_PERCENTAGE_CHANGED. The same applies to BUSINESS_ADDRESS_CHANGED, JURISDICTION_CHANGED, and COMPANY_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 scheduleCron to get 0 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.

StatuscodeReasonFix
404resource.not_foundThe kausateId, monitor or event doesn't existRun a search first via /v2/companies/search/indexed, or re-list your monitors
422request.invalidThe request body is well-formed but unusable: unknown monitoring source, a source not applicable to that company, an invalid cron, or a stale eventCodesFilter valueRead 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

On this page