Monitoring & Webhooks
10–50 credits per 30-day cycle, depending on cadenceA signal is a commercial-register change, normalized into one consistent shape. The Signals API lets you search those changes across every company. Monitoring works the other way round: you pick one company, we check it on a schedule, and every new change arrives as a signed webhook. Under data.signal that webhook carries the complete signal in exactly the same schema as the Signals API — the same event, organization, parties, register_entry, source, and details blocks. So you can process the payload directly, with no second API call.
What your plan covers
Every monitor checks the same seven topics and removes duplicates. Your plan decides what actually gets delivered, and it decides that immediately before sending. There is no per-monitor topic filter.
| Topic code | Topic | Available from |
|---|---|---|
| NEW_REGISTRATIONS | New registrations | Every account, no paid plan required |
| MASTER_DATA_CHANGES | Master data changes | Every account, no paid plan required |
| CLOSURES | Closures & dissolutions | Every account, no paid plan required |
| ROLE_HOLDER_CHANGES | Management changes | Every account, no paid plan required |
| CAPITAL_CHANGES | Capital changes | Every account, no paid plan required |
| INSOLVENCIES | Insolvencies | Pro and Max |
| TRANSFORMATIONS | Transformations & M&A | Max only |
Moving to a smaller plan keeps the monitor running. Topics your plan still covers are delivered as usual; the rest are dropped and are not sent later either. Messages already delivered stay unchanged and can still be retried. After an upgrade the wider access applies to whatever is found from that point on — we do not go back and fill in what happened before.
Setting monitoring up
- Request the pricing preview for the interval you want and check that entitled is true, so you know what a cycle will cost before you create anything.
- Create a webhook endpoint; this needs an admin token with the account:keys ability. The response contains the signing secret (whsec_…) exactly once, so store it right away.
- Implement signature verification and the challenge echo in your receiver. Only then call the endpoint verify action.
- Create the monitor: entity_id, an interval between 1 and 30 days, and the id of the verified endpoint.
- You get HTTP 202 straight away while the free baseline runs in the background. Poll the monitor and wait for status active — nothing is delivered before that.
Authentication and abilities
On all the paths below, credentials belong in headers only, and responses are private, no-store. Abilities are checked literally: an old wildcard token does not count as an admin credential.
| Operation | Required credential |
|---|---|
| Read pricing, monitors, endpoints, deliveries, and events | x-api-key, or a Bearer token with account:read. |
| Create, update, pause, resume, and archive monitors; verify and test endpoints; retry deliveries | x-api-key, or a Bearer token with account:read plus the monitoring:manage ability. |
| Create endpoints, rotate secrets, enable, disable, and archive | A Bearer admin token with account:read plus the account:keys ability. A plain API key is not enough for these operations. |
The ?api_key= query parameter is rejected on every /v1/account path. Otherwise your credentials would end up in URLs and in every access log.
Send an Idempotency-Key with every write
Send your own Idempotency-Key with every POST, PATCH, and DELETE. It makes sure that a request you repeat after a timeout or a network error does not take effect twice: no duplicate credit charge, no second secret, no repeated state change.
Idempotency-Key: monitor-create-01J7MZ1VN8N6FZQ2
- A key is 1 to 128 ASCII characters — letters, digits, dot, underscore, colon, and dash. The first character must be a letter or a digit.
- Repeat the same request with the same key and the same parameters within 24 hours and you get the stored response back exactly: same body, same HTTP status. The Idempotency-Status header tells you whether the operation just ran (created) or whether you are seeing a repeat (replayed).
- Reusing a key with different parameters returns 409 idempotency_conflict.
- Every monitor operation, creating, rotating, enabling, disabling, and archiving endpoints, and retrying a delivery can all be safely repeated with the same key. Never switch to a new one — the old key is exactly what prevents the duplicate effect.
- Verify and test talk to your server. If either returns 409 idempotency_in_progress (with Idempotency-Status: processing), there is no way to tell whether your receiver already saw the request. Stop automatic retries and contact support: do not try again later, and do not use a new key. A 503, by contrast, means nothing ran at all and the key stays unused.
- Once a key is older than the replay window, the API returns 409 idempotency_key_expired instead of running an operation involving secrets or credits a second time. Talk to support before repeating it.
Every endpoint at a glance
All 18 monitoring routes live below /api/v1/account. GET requests are free. Every write needs an Idempotency-Key.
| Method | Path | Description |
|---|---|---|
| GET | /v1/account/monitoring/pricing | Shows the current price for a poll_interval_days (default 30) and whether your account can use monitoring. |
| GET | /v1/account/monitors | Lists up to 100 non-archived monitors, newest first. |
| POST | /v1/account/monitors | Starts the free baseline in the background and returns the monitor with HTTP 202. |
| GET | /v1/account/monitors/{publicId} | Returns one monitor with its running billing cycle and its last 20 checks. For a successful check, events_new counts the new notifications it released for your plan — several endpoints do not multiply it. A continued check may show partial progress, and an unfunded failed check may show candidates that were never delivered. |
| PATCH | /v1/account/monitors/{publicId} | Changes poll_interval_days at the current price — going forward only, with no refund and no catching up on missed checks. |
| POST | /v1/account/monitors/{publicId}/pause | Pauses future checks on an active monitor. History is kept and a cycle you already paid for is not refunded. In any other state, including initializing, the call returns HTTP 200 and changes nothing; archive instead to stop before activation. |
| POST | /v1/account/monitors/{publicId}/resume | Rechecks plan, destination, policy, and balance, and picks the monitor back up. An unfinished baseline is restarted. |
| DELETE | /v1/account/monitors/{publicId} | Permanently archives the monitor and its endpoint subscriptions. This is also how you stop a monitor before it activates and its cadence price is charged. |
| GET | /v1/account/webhook-endpoints | Lists all non-archived webhook endpoints. URLs and the values of your custom headers are masked. |
| POST | /v1/account/webhook-endpoints | Creates an endpoint and returns its signing secret exactly once. |
| POST | /v1/account/webhook-endpoints/{publicId}/verify | Sends a signed challenge and activates the endpoint once it is echoed correctly — which only happens in pending_verification. If the endpoint is already verified or disabled, you get its existing state back and no request goes out. That does not enable it; call enable for that. |
| POST | /v1/account/webhook-endpoints/{publicId}/rotate-secret | Issues a new signing secret. For seven days, normal deliveries, tests, and samples also carry a signature from the old secret; verification challenges only use the new one. |
| POST | /v1/account/webhook-endpoints/{publicId}/test | Sends a signed endpoint.test event immediately and returns the real delivery outcome. |
| POST | /v1/account/webhook-endpoints/{publicId}/{state} | Sets the endpoint to enable or disable. Enabling only works if it was verified at some point. |
| DELETE | /v1/account/webhook-endpoints/{publicId} | Archives the endpoint. Monitors left without a verified destination are paused. |
| GET | /v1/account/webhook-deliveries | Lists the 50 most recent deliveries, optionally filtered by the public id of one of your endpoints. |
| POST | /v1/account/webhook-deliveries/{publicId}/retry | Sends a delivery in state exhausted or blocked_endpoint again — with exactly the same stored message. |
| GET | /v1/account/webhook-events | Lists the 50 most recent events with their metadata and shows whether the stored data is still available. |
These lists are deliberately bounded and not paginated: 100 monitors, 50 deliveries, 50 events. All account routes share a limit of 30 requests per minute; writes are additionally capped at 10 per minute, and verify, rotate, and test at 6 per minute.
1. Create and verify a receiver
A new endpoint starts in pending_verification, and you get the signing secret in plaintext. Store it immediately — you will not see it a second time. When you read the endpoint later we show only the host; an existing path is shortened to …, and of your custom headers you only see the names.
curl -X POST 'https://handelsregister.ai/api/v1/account/webhook-endpoints' \
-H 'Authorization: Bearer YOUR_ADMIN_TOKEN' \
-H 'Content-Type: application/json' \
-H 'Idempotency-Key: endpoint-create-01J7MZ1VN8N6FZQ2' \
--data '{
"name": "Production receiver",
"url": "https://hooks.example.com/handelsregister",
"headers": {"x-tenant": "customer-42"}
}'
{
"endpoint": {
"id": "wep_01j7n0...",
"name": "Production receiver",
"url": "hooks.example.com/…",
"status": "pending_verification",
"verified_at": null,
"header_names": ["x-tenant"],
"consecutive_failures": 0,
"created_at": "2026-08-02T12:00:00+00:00"
},
"signing_secret": "whsec_STORE_THIS_VALUE_NOW",
"meta": {"request_credit_cost": 0, "credits_remaining": 1250}
}
Production destinations must be publicly reachable HTTPS URLs on port 443. IP addresses instead of hostnames, credentials in the URL, private or reserved destinations, and redirects are rejected. You can set up 10 endpoints per account and 10 custom headers per endpoint. Those headers are only ever sent, never returned, and you cannot override transport headers or anything starting with webhook-.
Verification handshake
A challenge only goes to an endpoint in pending_verification: it receives a signed endpoint.verification body. Check the Standard Webhooks signature, return any 2xx status, and echo data.challenge unchanged in the webhook-verification response header. Only if all of that holds is verified=true set. For an endpoint that is already verified or disabled, verify just returns the existing state — no request to your server, no status change. It does not enable the endpoint either; call enable separately for that. Verify is limited to six calls per minute.
POST /handelsregister HTTP/1.1
content-type: application/json
webhook-id: msg_01j7n1...
webhook-timestamp: 1785667200
webhook-signature: v1,BASE64_HMAC
{
"id": "msg_01j7n1...",
"type": "endpoint.verification",
"timestamp": "2026-08-02T12:00:00.000Z",
"schema_version": 1,
"data": {"challenge": "40f2..."}
}
HTTP/1.1 204 No Content
webhook-verification: 40f2...
curl -X POST \
'https://handelsregister.ai/api/v1/account/webhook-endpoints/wep_01j7n0.../verify' \
-H 'x-api-key: YOUR_API_KEY' \
-H 'Idempotency-Key: endpoint-verify-01J7MZ1VN8N6FZQ2'
2. Create a monitor
You need a canonical entity_id, at least one active endpoint, and an integer interval between 1 and 30 days. The cycle is priced at the rate in force when it opens. One monitor runs per company, and at most 50 per account at a time. On top of that, you can create 50 per 24 hours — monitors you have since archived count towards it.
# Fetch the current price and policy version first
curl 'https://handelsregister.ai/api/v1/account/monitoring/pricing?poll_interval_days=7' \
-H 'x-api-key: YOUR_API_KEY'
# Queue a free baseline; this request itself charges zero credits
curl -X POST 'https://handelsregister.ai/api/v1/account/monitors' \
-H 'x-api-key: YOUR_API_KEY' \
-H 'Content-Type: application/json' \
-H 'Idempotency-Key: monitor-create-01J7MZ1VN8N6FZQ2' \
--data '{
"entity_id": "cc78cf0b230aeae35c6df7ba31989bb9",
"label": "BMW AG",
"poll_interval_days": 7,
"endpoint_ids": ["wep_01j7n0..."]
}'
HTTP/1.1 202 Accepted
Idempotency-Status: created
{
"monitor": {
"id": "mon_01j7n2...",
"type": "organization.signals",
"entity_id": "cc78cf0b230aeae35c6df7ba31989bb9",
"label": "BMW AG",
"poll_interval_days": 7,
"status": "initializing",
"status_reason": null,
"pricing_policy_version": "2026-08.v2",
"baseline_completed_at": null,
"activated_at": null,
"last_succeeded_at": null,
"next_poll_at": null,
"created_at": "2026-08-02T12:01:00+00:00"
},
"meta": {
"baseline_queued": true,
"billing": {
"charged_credits": 0,
"new_cycle": false,
"floor_transaction_id": null
},
"request_credit_cost": 0,
"credits_remaining": 1250
}
}
POST /monitors does not run a check yet and charges no credits: it stores the monitor, queues the baseline, and answers with HTTP 202, baseline_queued=true, and charged_credits=0. The baseline covers all seven topics and records what is already known, including topics your plan cannot deliver. Nothing that happened before the start is ever delivered. The monitor only goes active once the baseline has finished completely. While it is initializing, DELETE is the only way to stop it; pause does nothing until it is active.
Billing
- The free baseline runs first. Then we recheck plan, destination, policy, and balance in one go, charge the price for your cadence, open a rolling 30-day cycle, and set the monitor to active.
- A cycle is a set price per cadence: daily 50, every 3 days 40, weekly 20, monthly 10 credits. An interval with no listed price is billed at the next more frequent cadence — every 4 days therefore costs the same as every 3 days, because it polls more often than weekly. The price covers the runs that cadence forecasts (ceil(30 / interval)); any run beyond them, such as a catch-up after an outage, costs 2 credits. Plan, topic mix, and a result with zero eligible/new signals do not change that. Failed or superseded checks add no run charge.
- The pricing preview assumes ceil(30 / poll_interval_days) checks. We spread the actual runs across a window, so do not rely on a particular wall-clock time.
- At renewal an active monitor opens a fresh 30-day cycle and charges its cadence price exactly once. Missed slots are neither billed individually nor backdated. After an outage a single combined catch-up check may run (outage_catchup); it counts as an ordinary check of the cycle and only costs the 2 credits fixed for that cycle if the allowance is already used up.
- Pause only affects an active monitor, which then sits in paused_user. In any other state the API returns HTTP 200 and does nothing — the baseline keeps running, and the monitor is activated and charged later as usual. To prevent that, archive it with DELETE. Neither pausing nor archiving refunds a cycle you have already paid for.
- Resuming inside a cycle you have already paid for costs nothing extra. Once that cycle has expired, a new one starts and is charged.
- We check your consent to the current pricing policy at activation, on resume, when the interval changes, and whenever renewal opens a new cycle. Inside a paid cycle the price and terms stay frozen, so a later policy change on its own will not pause your monitor mid-cycle.
- We recheck your current plan before every publication. After a downgrade, newly found signals your plan does not cover are dropped: no delivery, no entry in events_new. Topics your plan still covers continue as normal, and messages already sent stay unchanged. If the verified destination or the credits for a due extra check are missing, the monitor pauses before anything goes out. And a due extra charge always settles before that run releases its webhooks.
Monitor lifecycle
Always read status_reason together with status. Pause only affects active monitors — in any other state the call returns HTTP 200 and changes nothing. Resume rechecks every activation condition. DELETE archives permanently and is the only way to stop a monitor in initializing before it activates.
| Status | Description |
|---|---|
| initializing | The baseline is still running and changes cannot be delivered yet. Pause does nothing in this state; DELETE prevents the later activation. |
| active | The cycle is paid for and the monitor is checked on schedule. |
| paused_user | You paused the monitor yourself. |
| paused_configuration | No active, verified destination is left. |
| paused_entitlement | The account has access to no monitoring topic at all. A normal downgrade does not lead here — the monitor stays active. |
| paused_billing | Credits are missing, or consent to the current pricing policy is. |
| error | The source or the baseline failed several times in a row. Resume rechecks everything. |
| archived | Permanently archived and no longer scheduled. |
Webhook envelope
Live changes always carry type organization.signal.detected and schema_version 1. data.signal holds the complete normalized signal, so you do not need another API call. Fields without a value are simply omitted, and if the message would otherwise exceed 20 KiB we drop the optional details and register_entry and set data.truncated=true. data.links.signal is only a convenience link to the Signals detail API: delivery neither calls nor charges it. Following it yourself costs 20 credits on success, and it can also return 401, 402, 403 PLAN_REQUIRED, 404, or an upstream error.
{
"id": "msg_01j7n3...",
"event_id": "evt_01j7n3...",
"type": "organization.signal.detected",
"timestamp": "2026-08-02T12:05:00.000Z",
"schema_version": 1,
"data": {
"monitor": {
"id": "mon_01j7n2...",
"type": "organization.signals"
},
"signal": {
"event": {
"id": "SOURCE_EVENT_ID",
"topic": "CAPITAL_CHANGES",
"topic_name": {
"de": "Kapitaländerungen",
"en": "Capital changes"
},
"occurred_on": "2026-08-02",
"announced_on": "2026-08-02",
"date_basis": "REGISTER_ENTRY"
},
"organization": {
"type": "ORGANIZATION",
"entity_id": "cc78cf0b230aeae35c6df7ba31989bb9",
"current_profile": {
"name": "Example GmbH",
"status": {"code": "ACTIVE"}
}
},
"source": {"kind": "COMMERCIAL_REGISTER"}
},
"links": {
"signal": "https://handelsregister.ai/api/v1/signals/SOURCE_EVENT_ID"
}
}
}
Verify the Standard Webhooks headers
The signature covers the raw bytes of the request. So do not parse the JSON body before you have verified it: once parsed and re-serialized, the signature no longer matches. Header names are case-insensitive.
signed_content = webhook-id + "." + webhook-timestamp + "." + exact_raw_body
expected = base64(HMAC-SHA256(base64_decode(secret after "whsec_"), signed_content))
webhook-signature = "v1," + expected
- Strip the whsec_ prefix, strictly base64-decode the rest, and compute HMAC-SHA256 over webhook-id.webhook-timestamp.raw_body. Compare the base64 result in constant time against every v1 value in webhook-signature.
- Discard messages whose timestamp falls outside your tolerance, then deduplicate on webhook-id. On a retry the message id and body stay the same; timestamp and signature change with every attempt.
- For seven days after a rotation, normal deliveries, endpoint.test requests, and signed samples carry two signatures: one from the new secret and one from the old. Accept either during that period. An endpoint.verification challenge is always signed with the current secret only.
- Return 2xx quickly and do the real work afterwards in your own durable queue. Delivery is at-least-once: the same message can arrive more than once, and that must not trigger anything twice on your side.
Sample events from the UI
In the monitoring area you can send yourself up to 10 real signals as samples, matching your plan and drawn from a bounded slice of the last 48 months. The list can have gaps and is not a complete history. Sending is free and rechecks your current plan. Samples have the same shape as real events — data.monitor, data.signal, and data.links — but carry type organization.signal.sample and data.sample=true. A real event never contains data.sample.
Delivery, retries, and automatic disabling
For every delivery you see its state, how many attempts the current pass has made, the last HTTP or error result, the next scheduled attempt, and the final timestamps. A successful check is charged before its withheld notifications turn pending.
| Status | Description |
|---|---|
| withheld | Produced by a billable check, but not deliverable until that same check has settled. |
| pending | Ready for its first or a manually triggered attempt. |
| in_flight | Currently being delivered. |
| retry_wait | An attempt failed; the next one is already scheduled. |
| succeeded | Your receiver returned 2xx. |
| blocked_endpoint | The endpoint is unreachable or violates the rules for outbound requests. |
| exhausted | Every automatic attempt has been used up, or your receiver returned 410. |
| cancelled | The originating check was discarded or never funded, or the stored body is gone. |
- Every 2xx response counts as success. We never follow redirects; they count as a failure we retry. An HTTP 410 ends the delivery immediately and disables the endpoint.
- Automatic retries are spread randomly across up to 10 attempts and roughly three days. We honour a valid Retry-After on 429, 502, 503, and 504 up to a maximum of one day. Exact times are not a guarantee.
- Five exhausted deliveries in a row that are down to your receiver disable the endpoint. A successful delivery resets the counter; failures on our side do not count towards it.
- A manual retry needs an active, verified endpoint and the stored body. webhook-id and body stay identical. The public attempts counter restarts at 0 for the new pass, while attempt_number keeps counting up across the individual attempt records.
- The test action creates an endpoint.test event and delivers it right away. Its response therefore contains the real delivery outcome, not just an acknowledgement that something was queued.
Privacy, secrets, and retention
All account responses are private and no-store. Before a stored response is replayed we check who the data belongs to: a public id from another account returns the same 404 as an id that does not exist.
- Endpoint URLs, the values of your custom headers, signing secrets, event data, and stored message bodies are encrypted at rest. Responses mask URL paths and never return your header values.
- A signing secret appears in plaintext only when you create or rotate an endpoint. That exact response can be replayed through the Idempotency-Key for 24 hours; after that the body is deleted.
- Event data and the exact message bodies are kept for 30 days. After that GET /webhook-events keeps only the metadata and reports data_available=false.
- Delivery-attempt metadata is kept for 90 days. An expired Idempotency-Key also stays traceable for 90 days, without its response.
- For outbound requests: 5 seconds to connect, 15 seconds in total, no proxies, no redirects, and we read at most 64 KiB of the response.
Errors and what they cost
- 400/422: invalid input, a URL or header configuration we do not allow, a failed verification, or a state change that does not exist.
- 403: your account has access to no monitoring topic at all, or the token is missing a required ability. 404: the id is unknown or belongs to another account.
- 409: this company already has a monitor, the pricing policy is stale, the Idempotency-Key conflicts or has expired, or an operation is still running. After pricing_policy_stale, just fetch the current prices again.
- 428: a write is missing its Idempotency-Key. 429: you exceeded a rate limit.
- 503 temporarily_unavailable: monitoring is not currently running operations. On monitor create and resume, endpoint verify and test, and delivery retry this applies before anything happens: nothing ran, your Idempotency-Key stays unused, and you can use it again later.
- Failures in validation, authorization, the source, or delivery cost no credits themselves. Billing happens only through funded monitor cycles and successfully settled extra checks.