/v1/account/webhook-endpoints
account/webhook-endpoints
Webhooks: dit Bevakning skickar nya Signals och hur du verifierar dem.
En webhook-endpoint är den HTTPS-adress på din server som vi skickar dina bevakningars nya signaler till. Varje meddelande är signerat, så att du kan kontrollera att det kommer från oss.
Kostnader och gränser
| Vad | Krediter |
|---|---|
| Hantera webhook-endpoints och leveranser | gratis |
Gräns: 30 förfrågningar per minut. Skapa, ändra och radera: 10 per minut. Kontroll, test och ny secret: 6 per minut.
Varje konto kan ha upp till 10 webhook-endpoints; arkiverade räknas inte. Varje webhook-endpoint kan bära upp till 10 egna headers.
Admin-token eller API-nyckel
Du behöver admin-token för att skapa en signeringshemlighet (secret) och för att aktivera, inaktivera eller arkivera webhook-endpoints. Du skapar den under Konto › API-nycklar i avsnittet ”För utvecklare: hantera nycklar via API:et”. Den gäller i ett år, och en ny ersätter den gamla.
Skicka admin-token som Authorization: Bearer YOUR_ADMIN_TOKEN. Alla andra anrop fungerar även med API-nyckeln.
Som för account/monitors kräver alla skrivande anrop headern Idempotency-Key.
Endpoints
| Metod | Sökväg | Syfte | Åtkomst |
|---|---|---|---|
| GET |
/v1/account/webhook-endpoints |
Alla webhook-endpoints, utan arkiverade. | API-nyckel eller admin-token |
| POST |
/v1/account/webhook-endpoints |
Skapa en webhook-endpoint. | Admin-token |
| POST |
/v1/account/webhook-endpoints/{id}/verify |
Verifiera. | API-nyckel eller admin-token |
| POST |
/v1/account/webhook-endpoints/{id}/test |
Skicka ett testmeddelande. | API-nyckel eller admin-token |
| POST |
/v1/account/webhook-endpoints/{id}/rotate-secret |
Ny signeringshemlighet. | Admin-token |
| POST |
/v1/account/webhook-endpoints/{id}/enable |
Aktivera. | Admin-token |
| POST |
/v1/account/webhook-endpoints/{id}/disable |
Inaktivera. | Admin-token |
| DELETE |
/v1/account/webhook-endpoints/{id} |
Arkivera. | Admin-token |
| GET |
/v1/account/webhook-deliveries |
De senaste 50 leveranserna. | API-nyckel eller admin-token |
| POST |
/v1/account/webhook-deliveries/{id}/retry |
Skicka en misslyckad leverans igen. | API-nyckel eller admin-token |
| GET |
/v1/account/webhook-events |
De senaste 50 händelserna. | API-nyckel eller admin-token |
Skapa en webhook-endpoint
curl -X POST "https://handelsregister.ai/api/v1/account/webhook-endpoints" \ -H "Authorization: Bearer DIN_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": "DIN_MOTTAGAR_TOKEN" } }'
import uuid import requests response = requests.post( "https://handelsregister.ai/api/v1/account/webhook-endpoints", headers={ "Authorization": "Bearer DIN_ADMIN_TOKEN", "Idempotency-Key": str(uuid.uuid4()), }, json={ "name": "CRM-Sync", "url": "https://hooks.example.com/handelsregister", "headers": { "X-Receiver-Token": "DIN_MOTTAGAR_TOKEN" } }, ) data = response.json()
const response = await fetch("https://handelsregister.ai/api/v1/account/webhook-endpoints", { method: "POST", headers: { "Authorization": "Bearer DIN_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": "DIN_MOTTAGAR_TOKEN" } }), }); const data = await response.json();
| Namn | Typ | Beskrivning | Exempel |
|---|---|---|---|
| Idempotency-KeyHeader | strängobligatorisk | Ett unikt värde per operation, helst ett UUID. Se Idempotency-Key.Gränser: 1 till 128 tecken |
req-20260802-01 |
| nameBody | strängobligatorisk | Ett namn för din egen referens.Gränser: 1 till 120 tecken |
CRM-Sync |
| urlBody | sträng (uri)obligatorisk |
Din servers adress: offentligt nåbar, https på port 443, med ett värdnamn i stället för en IP-adress.Gränser: 1 till 2 048 tecken |
https://hooks.example.com/handelsregister |
| headersBody | objektvalfri | Egna headers som skickas med varje meddelande, till exempel en token för din server.Gränser: högst 10 poster |
{"X-Receiver-Token":"DIN_MOTTAGAR_TOKEN"} |
Headernamn får inte börja med webhook- och får inte skriva över standardheaders som Content-Type eller Host. Värdena kan bara skrivas; senare visar API:et bara namnen.
Svaret innehåller signeringshemligheten whsec_…. Vi visar den bara denna enda gång. Spara den direkt.
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}
En ny webhook-endpoint har statusen pending_verification och tar emot meddelanden först efter verifieringen. Namn, URL och headers kan inte ändras i efterhand. För en ny adress skapar du en ny webhook-endpoint.
I svar visar url bara värden; sökvägen förkortar vi till /….
Verifiera
Med POST …/{id}/verify skickar vi ett signerat meddelande av typen endpoint.verification till din URL. data.challenge innehåller ett slumpmässigt värde.
Din server svarar inom 15 sekunder med en 2xx-status och returnerar värdet i svarsheadern webhook-verification. Webhook-endpointen blir då 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: DIN_API_NYCKEL" \ -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": "DIN_API_NYCKEL", "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": "DIN_API_NYCKEL", "Idempotency-Key": crypto.randomUUID(), }, }); const data = await response.json();
Om det fungerar svarar API:et med "verified": true, annars med 422 och "verified": false. För ett nytt försök behöver du en ny Idempotency-Key.
Varje webhook-endpoint tillåter 6 försök per timme.
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}
Därefter skickar POST …/{id}/test ett signerat testmeddelande av typen endpoint.test när du vill. Svaret visar leveransens resultat.
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}
Verifiera signaturen
Varje meddelande är signerat enligt den öppna standarden Standard Webhooks. Verifiera signaturen innan du bearbetar ett meddelande.
| Header | Betydelse |
|---|---|
| webhook-id |
Meddelandets ID (msg_…). Det förblir detsamma vid nya försök. Använd det för att upptäcka dubbletter.
|
| webhook-timestamp | Tidpunkt för detta leveransförsök i Unix-sekunder. |
| webhook-signature |
En eller flera signaturer av formen v1,<Base64>, separerade med mellanslag.
|
Så verifierar du ett meddelande:
- Ta den del av secret som står efter
whsec_och avkoda den från Base64. Det är nyckeln. - Bygg texten
webhook-id.webhook-timestamp.Bodyav de två headerna och den oförändrade bodyn. - Beräkna HMAC-SHA256 av den med nyckeln och koda resultatet i Base64.
- Jämför resultatet i konstant tid med varje
v1-signatur i headern. Om en matchar är meddelandet äkta.
Använd bodyn exakt som den kommer, inte omserialiserad JSON. Avvisa meddelanden vars tidsstämpel avviker med mer än fem minuter.
import base64, hashlib, hmac, time def verify_webhook(secret: str, headers: dict, body: bytes) -> bool: msg_id = headers["webhook-id"] timestamp = headers["webhook-timestamp"] # Avvisa meddelanden som är äldre än fem minuter 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-rotation innehåller headern två 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"]; // Avvisa meddelanden som är äldre än fem minuter 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-rotation innehåller headern två 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 stället för egen kod kan du använda de officiella Standard Webhooks-biblioteken, till exempel paketet standardwebhooks för Python och JavaScript.
Med POST …/{id}/rotate-secret får du en ny secret, en gång, i svaret. I 7 dagar har meddelanden två signaturer, en med den nya och en med den gamla secret. Så byter du utan avbrott.
Payload
Varje meddelande är ett JSON-objekt med samma kuvert. Det som utgör händelsen står i data.
| Fält | Betydelse |
|---|---|
| id |
Meddelandets ID, lika med headern webhook-id.
|
| event_id |
Händelsens ID (evt_…).
|
| type | Händelsens typ, se nedan. |
| timestamp | När vi upptäckte händelsen, i UTC. |
| schema_version |
Strukturens version, för närvarande 1.
|
| data | Händelsens data. |
| Händelsetyp | Betydelse |
|---|---|
| organization.signal.detected |
En bevakning har hittat en ny signal. data.signal har samma uppbyggnad som en signal i signals, plus data.monitor och, under data.links.signal, länken till signalen i API:et.
|
| organization.signal.sample |
Ett exempel ur företagets verkliga historik, skickat under Konto › Bevakning. Samma uppbyggnad som ovan, plus data.sample: true.
|
| endpoint.test |
Testmeddelande via /test.
|
| endpoint.verification | Challenge vid verifieringen. |
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}
Ett meddelande är högst 20 KB. Om en signal blir större utelämnas details och register_entry och data.truncated är true. Hela signalen hämtar du då via länken.
Ignorera händelsetyper som du inte känner till och svara ändå med 2xx. Då förblir din server stabil när nya typer tillkommer.
Leveranser och nya försök
En leverans räknas som lyckad när din server svarar inom 15 sekunder med en 2xx-status. Vi följer inte omdirigeringar.
Annars försöker vi igen: upp till 10 försök, med växande mellanrum under cirka tre dagar. Om din server skickar Retry-After följer vi det.
Om din server svarar med 410 slutar vi leverera och inaktiverar webhook-endpointen.
Om 5 leveranser i rad misslyckas slutgiltigt inaktiveras webhook-endpointen med disabled_failures. Med POST …/{id}/enable slår du på den igen.
webhook-id. Svara snabbt och bearbeta meddelandet först därefter.
GET /v1/account/webhook-deliveries visar de senaste 50 leveranserna, med ?endpoint=wep_… för en webhook-endpoint. GET /v1/account/webhook-events visar de senaste 50 händelserna.
En slutgiltigt misslyckad leverans skickar du igen med POST /v1/account/webhook-deliveries/{id}/retry, med samma webhook-id och samma body. Det fungerar i 30 dagar; så länge sparar vi meddelandena.
| Status | Betydelse |
|---|---|
| withheld | Väntar tills kontrollen har debiterats. |
| pending | Väntar på att skickas. |
| in_flight | Skickas just nu. |
| retry_wait | Väntar på nästa försök. |
| succeeded | Levererad. |
| exhausted | Alla försök misslyckades. |
| blocked_endpoint | Webhook-endpointen kan inte ta emot något just nu. |
| cancelled | Avbruten, till exempel för att meddelandet har gått ut. |
Aktivera, inaktivera, arkivera
POST …/{id}/disable stoppar leveransen tills vidare. Bevakningar utan annan webhook-endpoint pausas då med paused_configuration.
POST …/{id}/enable slår på en verifierad webhook-endpoint igen. Återuppta sedan pausade bevakningar med resume.
DELETE …/{id} arkiverar webhook-endpointen för gott. För att stoppa den en tid inaktiverar du den i stället.
curl -X POST "https://handelsregister.ai/api/v1/account/webhook-endpoints/wep_01m6b3j1g0cdz98hw8zdefbf52/disable" \ -H "Authorization: Bearer DIN_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 DIN_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 DIN_ADMIN_TOKEN", "Idempotency-Key": crypto.randomUUID(), }, }); const data = await response.json();
Fel
Fel har samma uppbyggnad som för account/monitors: en kod i error, förklaringen i meta.message. Fel kostar ingenting.
| Status | Orsak | Vad du kan göra |
|---|---|---|
| 400 |
api_key står i URL:en. |
Skicka nyckeln i headern x-api-key. |
| 400 |
URL:en är inte tillåten som mål, en header är inte tillåten, maxantalet är nått eller webhook-endpointen är inte aktiv. meta.message anger orsaken. |
Rätta värdet eller verifiera först. |
| 400 |
Idempotency-Key har ett ogiltigt format. |
Rätta formatet, använd till exempel ett UUID. |
| 401 | Ingen API-nyckel eller token, eller en ogiltig. |
Skicka nyckeln i headern x-api-key. |
| 403 |
Bearer-token saknar behörigheten account:read. |
Använd API-nyckeln eller en token med account:read. |
| 403 | Kontots e-postadress är ännu inte bekräftad, eller kontot är spärrat. | Öppna länken i bekräftelsemejlet. Om kontot är spärrat kan supporten hjälpa till. |
| 403 | Detta anrop kräver admin-token. En API-nyckel räcker inte, och inte heller en annan token. |
Skicka den med Authorization: Bearer och admin-token. |
| 403 |
Bearer-token saknar behörigheten monitoring:manage. |
Använd API-nyckeln eller admin-token. |
| 404 | Ingen webhook-endpoint eller leverans med detta ID. | Kontrollera ID:t. |
| 409 |
Idempotency-Key har redan använts med andra data, är äldre än 24 timmar eller bearbetas fortfarande. |
Använd en ny nyckel eller vänta en kort stund. |
| 422 |
Ett värde saknas eller är ogiltigt, till exempel name eller url. errors nämner fältet. |
Rätta värdet och försök igen med en ny nyckel. |
| 422 | Verifieringen misslyckades. | Kontrollera challenge-headern och 2xx-statusen och försök sedan igen med en ny nyckel. |
| 428 |
Headern Idempotency-Key saknas. |
Skicka headern. |
| 429 | För många förfrågningar. |
Försök igen efter Retry-After. |
| 503 | Verifiering, test och omsändning är tillfälligt otillgängliga. | Försök igen lite senare. |
1{2 "error": "invalid_request",3 "meta": {4 "message": "The endpoint is not active.",5 "request_credit_cost": 0,6 "credits_remaining": 1457 }8}