/v1/account/webhook-endpoints
account/webhook-endpoints
Webhooks: hvor Monitoring sender nye Signals, og hvordan du verifiserer dem.
Et webhook-endepunkt er HTTPS-adressen til serveren din som vi sender monitorenes nye signaler til. Hver melding er signert, så du kan sjekke at den kommer fra oss.
Kostnader og grenser
| Hva | Kreditter |
|---|---|
| Administrer webhook-endepunkter og leveranser | gratis |
Grense: 30 forespørsler per minutt. Opprette, endre og slette: 10 per minutt. Sjekk, test og ny secret: 6 per minutt.
Hver konto kan ha opptil 10 webhook-endepunkter; arkiverte teller ikke. Hvert webhook-endepunkt kan ha opptil 10 egne headere.
Admin-token eller API-nøkkel
Du trenger admin-tokenet for å opprette en signeringssecret og for å aktivere, deaktivere eller arkivere webhook-endepunkter. Du oppretter det under Konto › API-nøkler i avsnittet «For utviklere: administrer nøkler via API-et». Det gjelder i ett år, og et nytt erstatter det gamle.
Send admin-tokenet som Authorization: Bearer YOUR_ADMIN_TOKEN. Alle andre kall fungerer også med API-nøkkelen.
Som for account/monitors trenger alle skrivekall headeren Idempotency-Key.
Endepunkter
| Metode | Sti | Formål | Tilgang |
|---|---|---|---|
| GET |
/v1/account/webhook-endpoints |
Alle webhook-endepunkter, uten arkiverte. | API-nøkkel eller admin-token |
| POST |
/v1/account/webhook-endpoints |
Opprett et webhook-endepunkt. | Admin-token |
| POST |
/v1/account/webhook-endpoints/{id}/verify |
Verifiser. | API-nøkkel eller admin-token |
| POST |
/v1/account/webhook-endpoints/{id}/test |
Send en testmelding. | API-nøkkel eller admin-token |
| POST |
/v1/account/webhook-endpoints/{id}/rotate-secret |
Ny signeringssecret. | Admin-token |
| POST |
/v1/account/webhook-endpoints/{id}/enable |
Aktiver. | Admin-token |
| POST |
/v1/account/webhook-endpoints/{id}/disable |
Deaktiver. | Admin-token |
| DELETE |
/v1/account/webhook-endpoints/{id} |
Arkiver. | Admin-token |
| GET |
/v1/account/webhook-deliveries |
De siste 50 leveransene. | API-nøkkel eller admin-token |
| POST |
/v1/account/webhook-deliveries/{id}/retry |
Send en mislykket leveranse på nytt. | API-nøkkel eller admin-token |
| GET |
/v1/account/webhook-events |
De siste 50 hendelsene. | API-nøkkel eller admin-token |
Opprett et webhook-endepunkt
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" } }'
import uuid import requests response = requests.post( "https://handelsregister.ai/api/v1/account/webhook-endpoints", headers={ "Authorization": "Bearer YOUR_ADMIN_TOKEN", "Idempotency-Key": str(uuid.uuid4()), }, json={ "name": "CRM-Sync", "url": "https://hooks.example.com/handelsregister", "headers": { "X-Receiver-Token": "YOUR_RECEIVER_TOKEN" } }, ) data = response.json()
const response = await fetch("https://handelsregister.ai/api/v1/account/webhook-endpoints", { method: "POST", headers: { "Authorization": "Bearer YOUR_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": "YOUR_RECEIVER_TOKEN" } }), }); const data = await response.json();
| Navn | Type | Beskrivelse | Eksempel |
|---|---|---|---|
| Idempotency-KeyHeader | strengpåkrevd | En unik verdi per operasjon, helst en UUID. Se Idempotency-Key.Grenser: 1 til 128 tegn |
req-20260802-01 |
| nameBody | strengpåkrevd | Et navn til eget bruk.Grenser: 1 til 120 tegn |
CRM-Sync |
| urlBody | streng (uri)påkrevd |
Adressen til serveren din: offentlig tilgjengelig, https på port 443, med et vertsnavn i stedet for en IP-adresse.Grenser: 1 til 2 048 tegn |
https://hooks.example.com/handelsregister |
| headersBody | objektvalgfri | Egne headere som sendes med hver melding, for eksempel et token for serveren din.Grenser: høyst 10 oppføringer |
{"X-Receiver-Token":"YOUR_RECEIVER_TOKEN"} |
Headernavn kan ikke begynne med webhook- og kan ikke overstyre standardheadere som Content-Type eller Host. Verdiene kan bare skrives; senere viser API-et bare navnene.
Svaret inneholder signeringssecreten whsec_…. Vi viser den bare denne ene gangen. Lagre den med en gang.
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}
Et nytt webhook-endepunkt har statusen pending_verification og mottar meldinger først etter at det er verifisert. Navn, URL og headere kan ikke endres senere. For en ny adresse oppretter du et nytt webhook-endepunkt.
I svar viser url bare verten; stien forkorter vi til /….
Verifiser
Med POST …/{id}/verify sender vi en signert melding av typen endpoint.verification til URL-en din. data.challenge inneholder en tilfeldig verdi.
Serveren din svarer innen 15 sekunder med en 2xx-status og returnerer verdien i svarheaderen webhook-verification. Webhook-endepunktet blir da 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: YOUR_API_KEY" \ -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": "YOUR_API_KEY", "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": "YOUR_API_KEY", "Idempotency-Key": crypto.randomUUID(), }, }); const data = await response.json();
Fungerer det, svarer API-et med "verified": true, ellers med 422 og "verified": false. For et nytt forsøk trenger du en ny Idempotency-Key.
Hvert webhook-endepunkt tillater 6 forsøk per time.
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}
Deretter sender POST …/{id}/test en signert testmelding av typen endpoint.test når du vil. Svaret viser resultatet av leveransen.
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}
Verifiser signaturen
Hver melding er signert etter den åpne standarden Standard Webhooks. Verifiser signaturen før du behandler en melding.
| Header | Betydning |
|---|---|
| webhook-id |
Meldingens ID (msg_…). Den forblir den samme ved nye forsøk. Bruk den til å oppdage duplikater.
|
| webhook-timestamp | Tidspunktet for dette leveringsforsøket i Unix-sekunder. |
| webhook-signature |
Én eller flere signaturer av formen v1,<Base64>, adskilt med mellomrom.
|
Slik verifiserer du en melding:
- Ta delen av secreten etter
whsec_og dekod den fra Base64. Det er nøkkelen. - Bygg teksten
webhook-id.webhook-timestamp.Bodyav de to headerne og den uendrede bodyen. - Beregn HMAC-SHA256 av den med nøkkelen og koder resultatet i Base64.
- Sammenlign resultatet i konstant tid med hver
v1-signatur i headeren. Passer én, er meldingen ekte.
Bruk bodyen nøyaktig slik den kommer, ikke JSON som er serialisert på nytt. Avvis meldinger der tidsstempelet avviker med mer enn fem minutter.
import base64, hashlib, hmac, time def verify_webhook(secret: str, headers: dict, body: bytes) -> bool: msg_id = headers["webhook-id"] timestamp = headers["webhook-timestamp"] # Avvis meldinger som er eldre enn fem minutter 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() # Under en secret-rotasjon har headeren to signaturer 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"]; // Avvis meldinger som er eldre enn fem minutter 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(); // Under en secret-rotasjon har headeren to signaturer 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); }); }
I stedet for å skrive egen kode kan du bruke de offisielle Standard Webhooks-bibliotekene, for eksempel pakken standardwebhooks for Python og JavaScript.
Med POST …/{id}/rotate-secret får du en ny secret, én gang, i svaret. I 7 dager har meldingene to signaturer, én med den nye og én med den gamle secreten. Slik bytter du uten nedetid.
Nyttelast
Hver melding er et JSON-objekt med den samme konvolutten. Det som utgjør hendelsen, står i data.
| Felt | Betydning |
|---|---|
| id |
Meldingens ID, lik headeren webhook-id.
|
| event_id |
Hendelsens ID (evt_…).
|
| type | Hendelsestypen, se nedenfor. |
| timestamp | Når vi oppdaget hendelsen, i UTC. |
| schema_version |
Versjonen av strukturen, for tiden 1.
|
| data | Hendelsesdataene. |
| Hendelsestype | Betydning |
|---|---|
| organization.signal.detected |
En monitor har funnet et nytt signal. data.signal har samme struktur som et signal i signals, pluss data.monitor og, under data.links.signal, lenken til signalet i API-et.
|
| organization.signal.sample |
Et eksempel fra firmaets ekte historikk, sendt under Konto › Monitoring. Samme struktur som ovenfor, pluss data.sample: true.
|
| endpoint.test |
Testmelding via /test.
|
| endpoint.verification | Challenge under verifiseringen. |
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}
En melding er høyst 20 KB. Blir et signal større, utelates details og register_entry, og data.truncated er true. Da henter du hele signalet via lenken.
Ignorer hendelsestyper du ikke kjenner, og svar likevel med 2xx. Slik forblir serveren din stabil når nye typer kommer til.
Leveranser og nye forsøk
En leveranse regnes som vellykket når serveren din svarer innen 15 sekunder med en 2xx-status. Vi følger ikke omdirigeringer.
Ellers prøver vi igjen: opptil 10 forsøk, med økende mellomrom over ca. tre dager. Sender serveren din Retry-After, følger vi den.
Svarer serveren din med 410, slutter vi å levere og deaktiverer webhook-endepunktet.
Mislykkes 5 leveranser på rad for godt, deaktiveres webhook-endepunktet med disabled_failures. Med POST …/{id}/enable slår du det på igjen.
webhook-id. Svar raskt og behandle meldingen først etterpå.
GET /v1/account/webhook-deliveries viser de siste 50 leveransene, med ?endpoint=wep_… for ett webhook-endepunkt. GET /v1/account/webhook-events viser de siste 50 hendelsene.
En leveranse som har mislyktes for godt, sender du på nytt med POST /v1/account/webhook-deliveries/{id}/retry, med samme webhook-id og samme body. Det fungerer i 30 dager; så lenge oppbevarer vi meldingene.
| Status | Betydning |
|---|---|
| withheld | Venter til sjekken er avregnet. |
| pending | Venter på å bli sendt. |
| in_flight | Sendes akkurat nå. |
| retry_wait | Venter på neste forsøk. |
| succeeded | Levert. |
| exhausted | Alle forsøk mislyktes. |
| blocked_endpoint | Webhook-endepunktet kan ikke motta noe akkurat nå. |
| cancelled | Avbrutt, for eksempel fordi meldingen utløp. |
Aktiver, deaktiver, arkiver
POST …/{id}/disable stopper leveransen inntil videre. Monitorer uten et annet webhook-endepunkt settes da på pause med paused_configuration.
POST …/{id}/enable slår et verifisert webhook-endepunkt på igjen. Gjenoppta deretter monitorer på pause med resume.
DELETE …/{id} arkiverer webhook-endepunktet for godt. For å stoppe det en stund deaktiverer du det i stedet.
curl -X POST "https://handelsregister.ai/api/v1/account/webhook-endpoints/wep_01m6b3j1g0cdz98hw8zdefbf52/disable" \ -H "Authorization: Bearer YOUR_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 YOUR_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 YOUR_ADMIN_TOKEN", "Idempotency-Key": crypto.randomUUID(), }, }); const data = await response.json();
Feil
Feil har samme struktur som for account/monitors: en kode i error, forklaringen i meta.message. Feil koster ingenting.
| Status | Årsak | Hva du kan gjøre |
|---|---|---|
| 400 |
api_key står i URL-en. |
Send nøkkelen i headeren x-api-key. |
| 400 |
URL-en er ikke tillatt som mål, en header er ikke tillatt, maksimalt antall er nådd, eller webhook-endepunktet er ikke aktivt. meta.message oppgir årsaken. |
Rett verdien eller verifiser først. |
| 400 |
Idempotency-Key har et ugyldig format. |
Rett formatet, bruk for eksempel en UUID. |
| 401 | Ingen eller ugyldig API-nøkkel eller token. |
Send nøkkelen i headeren x-api-key. |
| 403 |
Bearer-tokenet mangler rettigheten account:read. |
Bruk API-nøkkelen eller et token med account:read. |
| 403 | Kontoens e-postadresse er ikke bekreftet ennå, eller kontoen er sperret. | Åpne lenken fra bekreftelses-e-posten. Er kontoen sperret, kan support hjelpe. |
| 403 | Dette kallet trenger admin-tokenet. En API-nøkkel er ikke nok, og heller ikke et annet token. |
Send det med Authorization: Bearer og admin-tokenet. |
| 403 |
Bearer-tokenet mangler rettigheten monitoring:manage. |
Bruk API-nøkkelen eller admin-tokenet. |
| 404 | Ikke noe webhook-endepunkt eller noen leveranse med denne ID-en. | Sjekk ID-en. |
| 409 |
Idempotency-Key ble allerede brukt med andre data, er eldre enn 24 timer eller behandles fortsatt. |
Bruk en ny nøkkel eller vent litt. |
| 422 |
En verdi mangler eller er ugyldig, for eksempel name eller url. errors nevner feltet. |
Rett verdien og prøv igjen med en ny nøkkel. |
| 422 | Verifiseringen mislyktes. | Sjekk challenge-headeren og 2xx-statusen, og prøv deretter igjen med en ny nøkkel. |
| 428 |
Headeren Idempotency-Key mangler. |
Send headeren. |
| 429 | For mange forespørsler. |
Prøv igjen etter Retry-After. |
| 503 | Verifisering, testing og ny sending er midlertidig utilgjengelig. | Prøv igjen litt senere. |
1{2 "error": "invalid_request",3 "meta": {4 "message": "The endpoint is not active.",5 "request_credit_cost": 0,6 "credits_remaining": 1457 }8}