/v1/account/webhook-endpoints
account/webhook-endpoints
Webhooks: waar Monitoring nieuwe Signals naartoe stuurt en hoe je ze verifieert.
Een webhook-endpoint is het HTTPS-adres van je server waar we de nieuwe signalen van je monitors naartoe sturen. Elk bericht is ondertekend, zodat je kunt controleren dat het van ons komt.
Kosten en limieten
| Wat | Credits |
|---|---|
| Webhook-endpoints en leveringen beheren | gratis |
Limiet: 30 aanvragen per minuut. Aanmaken, wijzigen en verwijderen: 10 per minuut. Controleren, testen en nieuw secret: 6 per minuut.
Elk account kan tot 10 webhook-endpoints hebben; gearchiveerde tellen niet mee. Elk webhook-endpoint kan tot 10 eigen headers meesturen.
Admin-token of API-sleutel
Het admin-token heb je nodig om een ondertekeningssecret aan te maken en webhook-endpoints in te schakelen, uit te schakelen of te archiveren. Je maakt het aan onder Account › API-sleutels in het onderdeel „Voor ontwikkelaars: sleutels beheren via de API”. Het is één jaar geldig, en een nieuw token vervangt het oude.
Stuur het admin-token als Authorization: Bearer JOUW_ADMIN_TOKEN. Alle andere aanroepen werken ook met de API-sleutel.
Net als bij account/monitors vragen alle schrijfaanroepen de header Idempotency-Key.
Endpoints
| Methode | Pad | Doel | Toegang |
|---|---|---|---|
| GET |
/v1/account/webhook-endpoints |
Alle webhook-endpoints, zonder gearchiveerde. | API-sleutel of admin-token |
| POST |
/v1/account/webhook-endpoints |
Een webhook-endpoint aanmaken. | Admin-token |
| POST |
/v1/account/webhook-endpoints/{id}/verify |
Verifiëren. | API-sleutel of admin-token |
| POST |
/v1/account/webhook-endpoints/{id}/test |
Een testbericht sturen. | API-sleutel of admin-token |
| POST |
/v1/account/webhook-endpoints/{id}/rotate-secret |
Nieuw ondertekeningssecret. | Admin-token |
| POST |
/v1/account/webhook-endpoints/{id}/enable |
Inschakelen. | Admin-token |
| POST |
/v1/account/webhook-endpoints/{id}/disable |
Uitschakelen. | Admin-token |
| DELETE |
/v1/account/webhook-endpoints/{id} |
Archiveren. | Admin-token |
| GET |
/v1/account/webhook-deliveries |
De laatste 50 leveringen. | API-sleutel of admin-token |
| POST |
/v1/account/webhook-deliveries/{id}/retry |
Een mislukte levering opnieuw versturen. | API-sleutel of admin-token |
| GET |
/v1/account/webhook-events |
De laatste 50 events. | API-sleutel of admin-token |
Een webhook-endpoint aanmaken
curl -X POST "https://handelsregister.ai/api/v1/account/webhook-endpoints" \ -H "Authorization: Bearer JOUW_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": "JOUW_ONTVANGER_TOKEN" } }'
import uuid import requests response = requests.post( "https://handelsregister.ai/api/v1/account/webhook-endpoints", headers={ "Authorization": "Bearer JOUW_ADMIN_TOKEN", "Idempotency-Key": str(uuid.uuid4()), }, json={ "name": "CRM-Sync", "url": "https://hooks.example.com/handelsregister", "headers": { "X-Receiver-Token": "JOUW_ONTVANGER_TOKEN" } }, ) data = response.json()
const response = await fetch("https://handelsregister.ai/api/v1/account/webhook-endpoints", { method: "POST", headers: { "Authorization": "Bearer JOUW_ADMIN_TOKEN", "Idempotency-Key": crypto.randomUUID(), "Content-Type": "application/json", }, body: JSON.stringify({ "name": "CRM-Sync", "url": "https://hooks.example.com/handelsregister", "headers": { "X-Receiver-Token": "JOUW_ONTVANGER_TOKEN" } }), }); const data = await response.json();
| Naam | Type | Beschrijving | Voorbeeld |
|---|---|---|---|
| Idempotency-KeyHeader | stringverplicht | Een unieke waarde per bewerking, bij voorkeur een UUID. Zie Idempotency-Key.Limieten: 1 tot 128 tekens |
req-20260802-01 |
| nameBody | stringverplicht | Een naam voor je eigen overzicht.Limieten: 1 tot 120 tekens |
CRM-Sync |
| urlBody | string (uri)verplicht |
Het adres van je server: openbaar bereikbaar, https op poort 443, met een hostnaam in plaats van een IP-adres.Limieten: 1 tot 2.048 tekens |
https://hooks.example.com/handelsregister |
| headersBody | objectoptioneel | Eigen headers die bij elk bericht worden meegestuurd, zoals een token voor je server.Limieten: hoogstens 10 items |
{"X-Receiver-Token":"JOUW_ONTVANGER_TOKEN"} |
Headernamen mogen niet met webhook- beginnen en geen standaardheaders zoals Content-Type of Host overschrijven. De waarden zijn alleen schrijfbaar; later toont de API alleen de namen.
Het antwoord bevat het ondertekeningssecret whsec_…. We tonen het alleen deze ene keer. Sla het meteen op.
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}
Een nieuw webhook-endpoint heeft de status pending_verification en ontvangt pas berichten nadat het is geverifieerd. Naam, URL en headers kun je later niet wijzigen. Maak voor een nieuw adres een nieuw webhook-endpoint aan.
In antwoorden toont url alleen de host; het pad korten we in tot /….
Verifiëren
Met POST …/{id}/verify sturen we een ondertekend bericht van het type endpoint.verification naar je URL. data.challenge bevat een willekeurige waarde.
Je server antwoordt binnen 15 seconden met een 2xx-status en geeft de waarde terug in de antwoordheader webhook-verification. Het webhook-endpoint wordt dan active.
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}
curl -X POST "https://handelsregister.ai/api/v1/account/webhook-endpoints/wep_01m6b3j1g0cdz98hw8zdefbf52/verify" \ -H "x-api-key: JOUW_API_SLEUTEL" \ -H "Idempotency-Key: $(uuidgen)"
import uuid import requests response = requests.post( "https://handelsregister.ai/api/v1/account/webhook-endpoints/wep_01m6b3j1g0cdz98hw8zdefbf52/verify", headers={ "x-api-key": "JOUW_API_SLEUTEL", "Idempotency-Key": str(uuid.uuid4()), }, ) data = response.json()
const response = await fetch("https://handelsregister.ai/api/v1/account/webhook-endpoints/wep_01m6b3j1g0cdz98hw8zdefbf52/verify", { method: "POST", headers: { "x-api-key": "JOUW_API_SLEUTEL", "Idempotency-Key": crypto.randomUUID(), }, }); const data = await response.json();
Lukt het, dan antwoordt de API met "verified": true, anders met 422 en "verified": false. Voor een nieuwe poging heb je een nieuwe Idempotency-Key nodig.
Elk webhook-endpoint staat 6 pogingen per uur toe.
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}
Daarna stuurt POST …/{id}/test wanneer je wilt een ondertekend testbericht van het type endpoint.test. Het antwoord toont het resultaat van de levering.
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}
De handtekening verifiëren
Elk bericht is ondertekend volgens de open standaard Standard Webhooks. Verifieer de handtekening voordat je een bericht verwerkt.
| Header | Betekenis |
|---|---|
| webhook-id |
ID van het bericht (msg_…). Blijft gelijk bij herhalingen. Gebruik het om duplicaten te herkennen.
|
| webhook-timestamp | Tijdstip van deze leveringspoging in Unix-seconden. |
| webhook-signature |
Eén of meer handtekeningen van de vorm v1,<Base64>, gescheiden door spaties.
|
Zo verifieer je een bericht:
- Neem het deel van het secret na
whsec_en decodeer het uit Base64. Dat is de sleutel. - Vorm de tekst
webhook-id.webhook-timestamp.Bodyuit de twee headers en de ongewijzigde body. - Bereken daarover HMAC-SHA256 met de sleutel en codeer het resultaat in Base64.
- Vergelijk het resultaat in constante tijd met elke
v1-handtekening in de header. Komt er een overeen, dan is het bericht echt.
Gebruik de body precies zoals hij binnenkomt, geen opnieuw geserialiseerde JSON. Weiger berichten waarvan de timestamp meer dan vijf minuten afwijkt.
import base64, hashlib, hmac, time def verify_webhook(secret: str, headers: dict, body: bytes) -> bool: msg_id = headers["webhook-id"] timestamp = headers["webhook-timestamp"] # Berichten weigeren die ouder zijn dan vijf minuten 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() # Tijdens een secret-rotatie staan er twee handtekeningen in de header for part in headers["webhook-signature"].split(" "): version, _, signature = part.partition(",") if version == "v1" and hmac.compare_digest(signature, expected): return True return False
import crypto from "node:crypto"; export function verifyWebhook(secret, headers, rawBody) { const msgId = headers["webhook-id"]; const timestamp = headers["webhook-timestamp"]; // Berichten weigeren die ouder zijn dan vijf minuten if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) return false; const key = Buffer.from(secret.replace(/^whsec_/, ""), "base64"); const expected = crypto .createHmac("sha256", key) .update(`${msgId}.${timestamp}.${rawBody}`) .digest(); // Tijdens een secret-rotatie staan er twee handtekeningen in de header return headers["webhook-signature"].split(" ").some((part) => { const [version, signature] = part.split(","); const given = Buffer.from(signature ?? "", "base64"); return version === "v1" && given.length === expected.length && crypto.timingSafeEqual(given, expected); }); }
In plaats van eigen code kun je de officiële Standard Webhooks-bibliotheken gebruiken, zoals het pakket standardwebhooks voor Python en JavaScript.
Met POST …/{id}/rotate-secret krijg je eenmalig een nieuw secret in het antwoord. 7 dagen lang dragen berichten twee handtekeningen, één met het nieuwe en één met het oude secret. Zo stap je zonder onderbreking over.
Payload
Elk bericht is een JSON-object met dezelfde envelop. Wat het event inhoudt, staat in data.
| Veld | Betekenis |
|---|---|
| id |
ID van het bericht, gelijk aan de header webhook-id.
|
| event_id |
ID van het event (evt_…).
|
| type | Type van het event, zie hieronder. |
| timestamp | Wanneer we het event hebben gedetecteerd, in UTC. |
| schema_version |
Versie van de opbouw, op dit moment 1.
|
| data | De gegevens van het event. |
| Eventtype | Betekenis |
|---|---|
| organization.signal.detected |
Een monitor heeft een nieuw signaal gevonden. data.signal heeft dezelfde opbouw als een signaal in signals, plus data.monitor en, onder data.links.signal, de link naar het signaal in de API.
|
| organization.signal.sample |
Een voorbeeld uit de echte historie van het bedrijf, verstuurd onder Account › Monitoring. Dezelfde opbouw als hierboven, plus data.sample: true.
|
| endpoint.test |
Testbericht via /test.
|
| endpoint.verification | Challenge bij de verificatie. |
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}
Een bericht is hoogstens 20 KB. Wordt een signaal groter, dan worden details en register_entry weggelaten en is data.truncated gelijk aan true. Het volledige signaal haal je dan op via de link.
Negeer eventtypen die je niet kent en antwoord toch met 2xx. Zo blijft je server stabiel als er nieuwe typen bijkomen.
Leveringen en herhalingen
Een levering geldt als geslaagd als je server binnen 15 seconden met een 2xx-status antwoordt. Redirects volgen we niet.
Anders proberen we het opnieuw: tot 10 pogingen, met groeiende tussenpozen over ongeveer drie dagen. Stuurt je server Retry-After, dan houden we ons daaraan.
Antwoordt je server met 410, dan stoppen we met leveren en schakelen we het webhook-endpoint uit.
Mislukken 5 leveringen op rij definitief, dan wordt het webhook-endpoint uitgeschakeld met disabled_failures. Met POST …/{id}/enable zet je het weer aan.
webhook-id. Antwoord snel en verwerk het bericht pas daarna.
GET /v1/account/webhook-deliveries toont de laatste 50 leveringen, met ?endpoint=wep_… voor één webhook-endpoint. GET /v1/account/webhook-events toont de laatste 50 events.
Een definitief mislukte levering verstuur je opnieuw met POST /v1/account/webhook-deliveries/{id}/retry, met dezelfde webhook-id en dezelfde body. Dat kan 30 dagen lang; zo lang bewaren we de berichten.
| Status | Betekenis |
|---|---|
| withheld | Wacht tot de controle is afgerekend. |
| pending | Wacht op verzending. |
| in_flight | Wordt nu verzonden. |
| retry_wait | Wacht op de volgende poging. |
| succeeded | Geleverd. |
| exhausted | Alle pogingen mislukt. |
| blocked_endpoint | Het webhook-endpoint kan op dit moment niets ontvangen. |
| cancelled | Geannuleerd, bijvoorbeeld omdat het bericht is verlopen. |
Inschakelen, uitschakelen, archiveren
POST …/{id}/disable stopt de levering voorlopig. Monitors zonder ander webhook-endpoint pauzeren dan met paused_configuration.
POST …/{id}/enable zet een geverifieerd webhook-endpoint weer aan. Hervat daarna gepauzeerde monitors met resume.
DELETE …/{id} archiveert het webhook-endpoint definitief. Wil je het tijdelijk stoppen, schakel het dan uit.
curl -X POST "https://handelsregister.ai/api/v1/account/webhook-endpoints/wep_01m6b3j1g0cdz98hw8zdefbf52/disable" \ -H "Authorization: Bearer JOUW_ADMIN_TOKEN" \ -H "Idempotency-Key: $(uuidgen)"
import uuid import requests response = requests.post( "https://handelsregister.ai/api/v1/account/webhook-endpoints/wep_01m6b3j1g0cdz98hw8zdefbf52/disable", headers={ "Authorization": "Bearer JOUW_ADMIN_TOKEN", "Idempotency-Key": str(uuid.uuid4()), }, ) data = response.json()
const response = await fetch("https://handelsregister.ai/api/v1/account/webhook-endpoints/wep_01m6b3j1g0cdz98hw8zdefbf52/disable", { method: "POST", headers: { "Authorization": "Bearer JOUW_ADMIN_TOKEN", "Idempotency-Key": crypto.randomUUID(), }, }); const data = await response.json();
Fouten
Fouten hebben dezelfde opbouw als bij account/monitors: een code in error, de toelichting in meta.message. Fouten kosten niets.
| Status | Oorzaak | Wat je kunt doen |
|---|---|---|
| 400 |
api_key staat in de URL. |
Stuur de sleutel in de header x-api-key. |
| 400 |
De URL is niet toegestaan als bestemming, een header is niet toegestaan, het maximale aantal is bereikt, of het webhook-endpoint is niet actief. meta.message geeft de reden. |
Corrigeer de waarde of verifieer eerst. |
| 400 |
De Idempotency-Key heeft een ongeldig formaat. |
Corrigeer het formaat, gebruik bijvoorbeeld een UUID. |
| 401 | Geen API-sleutel of token, of een ongeldige. |
Stuur de sleutel in de header x-api-key. |
| 403 |
Het bearer-token mist het recht account:read. |
Gebruik de API-sleutel of een token met account:read. |
| 403 | Het e-mailadres van je account is nog niet bevestigd, of het account is geblokkeerd. | Open de link uit de bevestigingsmail. Is het account geblokkeerd, dan helpt support. |
| 403 | Deze aanroep vraagt het admin-token. Een API-sleutel is niet genoeg, en een ander token ook niet. |
Stuur het mee met Authorization: Bearer en het admin-token. |
| 403 |
Het bearer-token mist het recht monitoring:manage. |
Gebruik de API-sleutel of het admin-token. |
| 404 | Geen webhook-endpoint of levering met deze ID. | Controleer de ID. |
| 409 |
De Idempotency-Key is al met andere gegevens gebruikt, is ouder dan 24 uur, of wordt nog verwerkt. |
Gebruik een nieuwe sleutel of wacht even. |
| 422 |
Een waarde ontbreekt of is ongeldig, zoals name of url. errors noemt het veld. |
Corrigeer de waarde en probeer het opnieuw met een nieuwe sleutel. |
| 422 | De verificatie is mislukt. | Controleer de challenge-header en de 2xx-status en probeer het opnieuw met een nieuwe sleutel. |
| 428 |
De header Idempotency-Key ontbreekt. |
Stuur de header mee. |
| 429 | Te veel aanvragen. |
Probeer het opnieuw na Retry-After. |
| 503 | Verifiëren, testen en opnieuw versturen zijn tijdelijk niet beschikbaar. | Probeer het wat later opnieuw. |
1{2 "error": "invalid_request",3 "meta": {4 "message": "The endpoint is not active.",5 "request_credit_cost": 0,6 "credits_remaining": 1457 }8}