/v1/account/webhook-endpoints

account/webhook-endpoints

Webhooks: where Monitoring sends new Signals and how you verify them.

Costs and limits

Costs and limits
What Credits
Manage webhook endpoints and deliveries free

Limit: 30 requests per minute. Create, update and delete: 10 per minute. Check, test and new secret: 6 per minute.

Admin token or API key

Endpoints

Method
Method Path Purpose Access
GET /v1/account/webhook-endpoints All webhook endpoints, without archived ones. API key or admin token
POST /v1/account/webhook-endpoints Create a webhook endpoint. Admin token
POST /v1/account/webhook-endpoints/{id}/verify Verify. API key or admin token
POST /v1/account/webhook-endpoints/{id}/test Send a test message. API key or admin token
POST /v1/account/webhook-endpoints/{id}/rotate-secret New signing secret. Admin token
POST /v1/account/webhook-endpoints/{id}/enable Enable. Admin token
POST /v1/account/webhook-endpoints/{id}/disable Disable. Admin token
DELETE /v1/account/webhook-endpoints/{id} Archive. Admin token
GET /v1/account/webhook-deliveries The last 50 deliveries. API key or admin token
POST /v1/account/webhook-deliveries/{id}/retry Send a failed delivery again. API key or admin token
GET /v1/account/webhook-events The last 50 events. API key or admin token

Create a webhook endpoint

curl -X POST "https://handelsregister.ai/api/v1/account/webhook-endpoints" \
  -H "Authorization: Bearer YOUR_ADMIN_TOKEN" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "CRM-Sync",
    "url": "https://hooks.example.com/handelsregister",
    "headers": {
        "X-Receiver-Token": "YOUR_RECEIVER_TOKEN"
    }
}'
Parameters
Name Type Description Example
Idempotency-KeyHeader stringrequired A unique value per operation, ideally a UUID. See Idempotency-Key.Limits: 1 to 128 characters req-20260802-01
nameBody stringrequired A name for your own reference.Limits: 1 to 120 characters CRM-Sync
urlBody string (uri)required Your server's address: publicly reachable, https on port 443, with a host name instead of an IP address.Limits: 1 to 2,048 characters https://hooks.example.com/handelsregister
headersBody objectoptional Custom headers sent with every message, such as a token for your server.Limits: at most 10 entries {"X-Receiver-Token":"YOUR_RECEIVER_TOKEN"}
1{2  "endpoint": {3    "id": "wep_01m6b3j1g0cdz98hw8zdefbf52",4    "name": "CRM-Sync",5    "url": "hooks.example.com/…",6    "status": "pending_verification",7    "verified_at": null,8    "header_names": [9      "X-Receiver-Token"10    ],11    "consecutive_failures": 0,12    "created_at": "2026-11-01T08:28:01+00:00"13  },14  "signing_secret": "whsec_…",15  "meta": {16    "message": "Endpoint created. Store the signing secret now — it is shown only once.",17    "request_credit_cost": 0,18    "credits_remaining": 14519  }20}
201 · Example response: a new webhook endpoint, the secret shortened here

Verify

1{2  "id": "msg_01m6b3jsqq7ntwzkdj099ayraz",3  "type": "endpoint.verification",4  "timestamp": "2026-11-01T08:28:26.743Z",5  "schema_version": 1,6  "data": {7    "challenge": "189fb34b2af819bb13cb47d9806dbf30"8  }9}
The message your server receives during verification
curl -X POST "https://handelsregister.ai/api/v1/account/webhook-endpoints/wep_01m6b3j1g0cdz98hw8zdefbf52/verify" \
  -H "x-api-key: YOUR_API_KEY" \
  -H "Idempotency-Key: $(uuidgen)"
1{2  "verified": true,3  "endpoint": {4    "id": "wep_01m6b3j1g0cdz98hw8zdefbf52",5    "name": "CRM-Sync",6    "url": "hooks.example.com/…",7    "status": "active",8    "verified_at": "2026-11-01T08:28:26+00:00",9    "header_names": [10      "X-Receiver-Token"11    ],12    "consecutive_failures": 0,13    "created_at": "2026-11-01T08:28:01+00:00"14  },15  "meta": {16    "request_credit_cost": 0,17    "credits_remaining": 14518  }19}
200 · Example response to a successful verification
1{2  "delivery": {3    "id": "del_01m6b3jsyrnc3sz1mt31dq6107",4    "message_id": "msg_01m6b3jsynxzhvagm7n2nq7mpv",5    "event_id": "evt_01m6b3jsynxzhvagm7n2nq7mpt",6    "event_type": "endpoint.test",7    "endpoint_id": "wep_01m6b3j1g0cdz98hw8zdefbf52",8    "status": "succeeded",9    "attempts": 1,10    "last_status_code": 200,11    "last_error_category": null,12    "next_attempt_at": "2026-11-01T08:28:26+00:00",13    "succeeded_at": "2026-11-01T08:28:26+00:00",14    "created_at": "2026-11-01T08:28:26+00:00"15  },16  "meta": {17    "request_credit_cost": 0,18    "credits_remaining": 14519  }20}
200 · Example response to a test message

Verify the signature

Header
Header Meaning
webhook-id ID of the message (msg_…). It stays the same on retries. Use it to detect duplicates.
webhook-timestamp Time of this delivery attempt in Unix seconds.
webhook-signature One or more signatures of the form v1,<Base64>, separated by spaces.
import base64, hashlib, hmac, time

def verify_webhook(secret: str, headers: dict, body: bytes) -> bool:
    msg_id = headers["webhook-id"]
    timestamp = headers["webhook-timestamp"]
    # Reject messages older than five minutes
    if abs(time.time() - int(timestamp)) > 300:
        return False
    key = base64.b64decode(secret.removeprefix("whsec_"))
    signed = f"{msg_id}.{timestamp}.".encode() + body
    expected = base64.b64encode(hmac.new(key, signed, hashlib.sha256).digest()).decode()
    # During a secret rotation, the header carries two signatures
    for part in headers["webhook-signature"].split(" "):
        version, _, signature = part.partition(",")
        if version == "v1" and hmac.compare_digest(signature, expected):
            return True
    return False

Payload

Field
Field Meaning
id ID of the message, equal to the header webhook-id.
event_id ID of the event (evt_…).
type Type of the event, see below.
timestamp When we detected the event, in UTC.
schema_version Version of the structure, currently 1.
data The event data.
Event types
Event type Meaning
organization.signal.detected A monitor has found a new signal. data.signal has the same structure as a signal in signals, plus data.monitor and, under data.links.signal, the link to the signal in the API.
organization.signal.sample An example from the company's real history, sent under Account › Monitoring. Same structure as above, plus data.sample: true.
endpoint.test Test message via /test.
endpoint.verification Challenge during verification.
1{2  "id": "msg_01m65pqg9x0176cteqvtxw68sa",3  "event_id": "evt_01m65pqg9sx7mwmw0edc1mfbbe",4  "type": "organization.signal.sample",5  "timestamp": "2026-10-30T06:07:37.000Z",6  "schema_version": 1,7  "data": {8    "monitor": {9      "id": "mon_01m65pn3vm11sjjp1j076cbvec",10      "type": "organization.signals"11    },12    "sample": true,13    "signal": {14      "event": {15        "id": "36c7c8fddf99bb0299d6815bcaa01013",16        "topic": "TRANSFORMATIONS",17        "topic_name": {18          "de": "Umwandlungen & M&A",19          "en": "Transformations & M&A"20        },21        "occurred_on": "2022-12-22",22        "announced_on": "2023-08-10",23        "date_basis": "SOURCE_EVENT"24      },25      "organization": {26        "type": "ORGANIZATION",27        "entity_id": "94eb910fef13f94585c48c263fa446d4",28        "current_profile": {29          "name": "Hetzner Online GmbH",30          "registration": {31            "register_type": "HRB",32            "register_number": "6089",33            "court": "Ansbach",34            "register_locality": "Ansbach",35            "registration_authority_id": "D3201",36            "register_entity_name": "Ansbach HRB 6089"37          },38          "legal_form": {39            "name": {40              "de": "Gesellschaft mit beschränkter Haftung"41            },42            "short": {43              "de": "GmbH"44            }45          },46          "seat": {47            "city": "Gunzenhausen"48          },49          "status": {50            "code": "ACTIVE"51          }52        }53      },54      "parties": {55        "counterparty": {56          "entity": {57            "type": "ORGANIZATION",58            "entity_id": "dd4d2d6854b2dbd1ffce74e669f3852e",59            "current_profile": {60              "name": "Headtrip GmbH",61              "registration": {62                "register_type": "HRB",63                "register_number": "33432",64                "court": "Nürnberg",65                "register_locality": "Nuremberg",66                "registration_authority_id": "D3310",67                "register_entity_name": "Nürnberg HRB 33432"68              },69              "legal_form": {70                "name": {71                  "de": "Gesellschaft mit beschränkter Haftung"72                },73                "short": {74                  "de": "GmbH"75                }76              },77              "seat": {78                "city": "Nürnberg"79              },80              "status": {81                "code": "INACTIVE"82              }83            }84          },85          "relationship": "TRANSFERRING_COMPANY"86        }87      },88      "register_entry": {89        "entry_number": "3",90        "entry_date": "2023-08-10",91        "description_text": "Verschmelzung zur Aufnahme der Headtrip GmbH",92        "context": "PRIMARY",93        "flags": []94      },95      "source": {96        "kind": "COMMERCIAL_REGISTER"97      },98      "details": {99        "schema": "TRANSFORMATION",100        "kind": "MERGER",101        "specifics": {102          "merger_target": "ABSORPTION"103        },104        "direction": "INCOMING",105        "is_cross_border": false,106        "resolution_date": "2022-12-22",107        "agreement_date": "2022-12-22"108      }109    },110    "links": {111      "signal": "https://handelsregister.ai/api/v1/signals/36c7c8fddf99bb0299d6815bcaa01013"112    }113  }114}
Example message for Hetzner Online GmbH, Ansbach HRB 6089: the merger of Headtrip GmbH, Nürnberg HRB 33432, sent as an example from the history

Deliveries and retries

We deliver every message at least once, but not in a fixed order. Detect duplicates by webhook-id. Answer quickly and process the message only after that.
Status of a delivery
Status Meaning
withheld Waits until the check has been billed.
pending Waits to be sent.
in_flight Being sent right now.
retry_wait Waits for the next attempt.
succeeded Delivered.
exhausted All attempts failed.
blocked_endpoint The webhook endpoint cannot receive anything right now.
cancelled Cancelled, for example because the message expired.

Enable, disable, archive

curl -X POST "https://handelsregister.ai/api/v1/account/webhook-endpoints/wep_01m6b3j1g0cdz98hw8zdefbf52/disable" \
  -H "Authorization: Bearer YOUR_ADMIN_TOKEN" \
  -H "Idempotency-Key: $(uuidgen)"

Errors

Errors
Status Cause What you can do
400 api_key is in the URL. Send the key in the header x-api-key.
400 The URL is not allowed as a destination, a header is not permitted, the maximum number is reached, or the webhook endpoint is not active. meta.message gives the reason. Correct the value or verify first.
400 The Idempotency-Key has an invalid format. Correct the format, for example use a UUID.
401 No API key or token, or an invalid one. Send the key in the header x-api-key.
403 The bearer token lacks the ability account:read. Use the API key or a token with account:read.
403 Your account's email address is not confirmed yet, or the account is locked. Open the link from the confirmation email. If the account is locked, support can help.
403 This call needs the admin token. An API key is not enough, and neither is another token. Send it with Authorization: Bearer and the admin token.
403 The bearer token lacks the ability monitoring:manage. Use the API key or the admin token.
404 No webhook endpoint or delivery with this ID. Check the ID.
409 The Idempotency-Key was already used with different data, is older than 24 hours, or is still being processed. Use a new key or wait briefly.
422 A value is missing or invalid, such as name or url. errors names the field. Correct the value and retry with a new key.
422 Verification failed. Check the challenge header and the 2xx status, then retry with a new key.
428 The header Idempotency-Key is missing. Send the header.
429 Too many requests. Retry after Retry-After.
503 Verifying, testing and resending are temporarily unavailable. Retry a little later.
1{2  "error": "invalid_request",3  "meta": {4    "message": "The endpoint is not active.",5    "request_credit_cost": 0,6    "credits_remaining": 1457  }8}
400 · A webhook endpoint that is not verified yet cannot receive a test message