/v1/account/webhook-endpoints
account/webhook-endpoints
Webhookuri: unde trimite Monitoring noile Signals și cum le verifici.
Un endpoint webhook este adresa HTTPS a serverului tău la care trimitem semnalele noi ale monitoarelor tale. Fiecare mesaj este semnat, ca să poți verifica faptul că vine de la noi.
Costuri și limite
| Ce | Credite |
|---|---|
| Gestionarea endpointurilor webhook și a livrărilor | gratuit |
Limită: 30 cereri pe minut. Creare, modificare și ștergere: 10 pe minut. Verificare, test și secret nou: 6 pe minut.
Fiecare cont poate avea până la 10 endpointuri webhook; cele arhivate nu se socotesc. Fiecare endpoint webhook poate purta până la 10 headere personalizate.
Token admin sau cheie API
Ai nevoie de tokenul admin ca să creezi un secret de semnare și ca să activezi, dezactivezi sau arhivezi endpointuri webhook. Îl creezi la Cont › Chei API, în secțiunea „Pentru dezvoltatori: gestionează cheile prin API”. Este valabil un an, iar unul nou îl înlocuiește pe cel vechi.
Trimite tokenul admin ca Authorization: Bearer YOUR_ADMIN_TOKEN. Toate celelalte apeluri funcționează și cu cheia API.
Ca la account/monitors, toate apelurile de scriere au nevoie de headerul Idempotency-Key.
Endpointuri
| Metodă | Cale | Scop | Acces |
|---|---|---|---|
| GET |
/v1/account/webhook-endpoints |
Toate endpointurile webhook, fără cele arhivate. | Cheie API sau token admin |
| POST |
/v1/account/webhook-endpoints |
Creează un endpoint webhook. | Token admin |
| POST |
/v1/account/webhook-endpoints/{id}/verify |
Verificare. | Cheie API sau token admin |
| POST |
/v1/account/webhook-endpoints/{id}/test |
Trimite un mesaj de test. | Cheie API sau token admin |
| POST |
/v1/account/webhook-endpoints/{id}/rotate-secret |
Secret de semnare nou. | Token admin |
| POST |
/v1/account/webhook-endpoints/{id}/enable |
Activare. | Token admin |
| POST |
/v1/account/webhook-endpoints/{id}/disable |
Dezactivare. | Token admin |
| DELETE |
/v1/account/webhook-endpoints/{id} |
Arhivare. | Token admin |
| GET |
/v1/account/webhook-deliveries |
Ultimele 50 livrări. | Cheie API sau token admin |
| POST |
/v1/account/webhook-deliveries/{id}/retry |
Retrimite o livrare eșuată. | Cheie API sau token admin |
| GET |
/v1/account/webhook-events |
Ultimele 50 evenimente. | Cheie API sau token admin |
Creează un endpoint webhook
curl -X POST "https://handelsregister.ai/api/v1/account/webhook-endpoints" \ -H "Authorization: Bearer TOKENUL_TAU_ADMIN" \ -H "Idempotency-Key: $(uuidgen)" \ -H "Content-Type: application/json" \ -d '{ "name": "CRM-Sync", "url": "https://hooks.example.com/handelsregister", "headers": { "X-Receiver-Token": "TOKENUL_TAU_RECEPTOR" } }'
import uuid import requests response = requests.post( "https://handelsregister.ai/api/v1/account/webhook-endpoints", headers={ "Authorization": "Bearer TOKENUL_TAU_ADMIN", "Idempotency-Key": str(uuid.uuid4()), }, json={ "name": "CRM-Sync", "url": "https://hooks.example.com/handelsregister", "headers": { "X-Receiver-Token": "TOKENUL_TAU_RECEPTOR" } }, ) data = response.json()
const response = await fetch("https://handelsregister.ai/api/v1/account/webhook-endpoints", { method: "POST", headers: { "Authorization": "Bearer TOKENUL_TAU_ADMIN", "Idempotency-Key": crypto.randomUUID(), "Content-Type": "application/json", }, body: JSON.stringify({ "name": "CRM-Sync", "url": "https://hooks.example.com/handelsregister", "headers": { "X-Receiver-Token": "TOKENUL_TAU_RECEPTOR" } }), }); const data = await response.json();
| Nume | Tip | Descriere | Exemplu |
|---|---|---|---|
| Idempotency-KeyHeader | șir de caractereobligatoriu | O valoare unică per operație, ideal un UUID. Vezi Idempotency-Key.Limite: 1 până la 128 caractere |
req-20260802-01 |
| nameBody | șir de caractereobligatoriu | Un nume pentru propria ta referință.Limite: 1 până la 120 caractere |
CRM-Sync |
| urlBody | șir de caractere (uri)obligatoriu |
Adresa serverului tău: accesibilă public, https pe portul 443, cu nume de host în loc de adresă IP.Limite: 1 până la 2.048 caractere |
https://hooks.example.com/handelsregister |
| headersBody | obiectopțional | Headere personalizate trimise cu fiecare mesaj, de ex. un token pentru serverul tău.Limite: cel mult 10 intrări |
{"X-Receiver-Token":"TOKENUL_TAU_RECEPTOR"} |
Numele headerelor nu pot începe cu webhook- și nu pot suprascrie headere standard precum Content-Type sau Host. Valorile sunt doar pentru scriere; ulterior API-ul arată doar numele.
Răspunsul conține secretul de semnare whsec_…. Îl afișăm doar această dată. Salvează-l imediat.
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}
Un endpoint webhook nou are statusul pending_verification și primește mesaje doar după ce e verificat. Numele, URL-ul și headerele nu pot fi modificate ulterior. Pentru o adresă nouă, creează un endpoint webhook nou.
În răspunsuri, url arată doar hostul; scurtăm calea la /….
Verificare
Cu POST …/{id}/verify trimitem un mesaj semnat de tipul endpoint.verification la URL-ul tău. data.challenge conține o valoare aleatorie.
Serverul tău răspunde în 15 secunde cu un status 2xx și returnează valoarea în headerul de răspuns webhook-verification. Endpointul webhook devine atunci 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: CHEIA_TA_API" \ -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": "CHEIA_TA_API", "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": "CHEIA_TA_API", "Idempotency-Key": crypto.randomUUID(), }, }); const data = await response.json();
Dacă funcționează, API-ul răspunde cu "verified": true, altfel cu 422 și "verified": false. Pentru o încercare nouă ai nevoie de un Idempotency-Key nou.
Fiecare endpoint webhook permite 6 încercări pe oră.
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}
După aceea, POST …/{id}/test trimite oricând vrei un mesaj de test semnat de tipul endpoint.test. Răspunsul arată rezultatul livrării.
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}
Verifică semnătura
Fiecare mesaj este semnat conform standardului deschis Standard Webhooks. Verifică semnătura înainte să procesezi un mesaj.
| Header | Semnificație |
|---|---|
| webhook-id |
ID-ul mesajului (msg_…). Rămâne același la reîncercări. Folosește-l ca să recunoști duplicatele.
|
| webhook-timestamp | Momentul acestei încercări de livrare în secunde Unix. |
| webhook-signature |
Una sau mai multe semnături de forma v1,<Base64>, separate prin spații.
|
Cum verifici un mesaj:
- Ia partea secretului de după
whsec_și decodeaz-o din Base64. Aceasta este cheia. - Construiește textul
webhook-id.webhook-timestamp.Bodydin cele două headere și corpul nemodificat. - Calculează HMAC-SHA256 din el cu cheia și codifică rezultatul în Base64.
- Compară rezultatul în timp constant cu fiecare semnătură
v1din header. Dacă una se potrivește, mesajul este autentic.
Folosește corpul exact cum sosește, nu JSON reserializat. Respinge mesajele al căror timestamp diferă cu mai mult de cinci minute.
import base64, hashlib, hmac, time def verify_webhook(secret: str, headers: dict, body: bytes) -> bool: msg_id = headers["webhook-id"] timestamp = headers["webhook-timestamp"] # Respinge mesajele mai vechi de cinci minute 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() # În timpul unei rotații a secretului, headerul conține două semnături 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"]; // Respinge mesajele mai vechi de cinci minute 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(); // În timpul unei rotații a secretului, headerul conține două semnături 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); }); }
În loc să scrii propriul cod, poți folosi bibliotecile oficiale Standard Webhooks, de ex. pachetul standardwebhooks pentru Python și JavaScript.
Cu POST …/{id}/rotate-secret primești un secret nou, o singură dată, în răspuns. Timp de 7 zile, mesajele poartă două semnături, una cu secretul nou și una cu cel vechi. Astfel treci la noul secret fără întrerupere.
Payload
Fiecare mesaj este un obiect JSON cu același înveliș. Ce constituie evenimentul se află în data.
| Câmp | Semnificație |
|---|---|
| id |
ID-ul mesajului, egal cu headerul webhook-id.
|
| event_id |
ID-ul evenimentului (evt_…).
|
| type | Tipul evenimentului, vezi mai jos. |
| timestamp | Când am detectat evenimentul, în UTC. |
| schema_version |
Versiunea structurii, momentan 1.
|
| data | Datele evenimentului. |
| Tip de eveniment | Semnificație |
|---|---|
| organization.signal.detected |
Un monitor a găsit un semnal nou. data.signal are aceeași structură ca un semnal în signals, plus data.monitor și, la data.links.signal, linkul către semnal în API.
|
| organization.signal.sample |
Un exemplu din istoricul real al companiei, trimis din Cont › Monitoring. Aceeași structură ca mai sus, plus data.sample: true.
|
| endpoint.test |
Mesaj de test prin /test.
|
| endpoint.verification | Challenge la verificare. |
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}
Un mesaj are cel mult 20 KB. Dacă un semnal devine mai mare, details și register_entry sunt omise, iar data.truncated este true. Preiei atunci semnalul întreg prin link.
Ignoră tipurile de evenimente pe care nu le cunoști și răspunde totuși cu 2xx. Astfel serverul tău rămâne stabil când se adaugă tipuri noi.
Livrări și reîncercări
O livrare este reușită când serverul tău răspunde în 15 secunde cu un status 2xx. Nu urmăm redirecționări.
Altfel încercăm din nou: până la 10 încercări, la intervale tot mai mari, pe parcursul a aproximativ trei zile. Dacă serverul tău trimite Retry-After, îl respectăm.
Dacă serverul tău răspunde cu 410, oprim livrarea și dezactivăm endpointul webhook.
Dacă 5 livrări la rând eșuează definitiv, endpointul webhook este dezactivat cu disabled_failures. Cu POST …/{id}/enable îl reactivezi.
webhook-id. Răspunde repede și procesează mesajul abia după aceea.
GET /v1/account/webhook-deliveries arată ultimele 50 livrări, cu ?endpoint=wep_… pentru un endpoint webhook. GET /v1/account/webhook-events arată ultimele 50 evenimente.
O livrare eșuată definitiv o retrimiți cu POST /v1/account/webhook-deliveries/{id}/retry, cu același webhook-id și același corp. Asta funcționează 30 zile; atât păstrăm mesajele.
| Status | Semnificație |
|---|---|
| withheld | Așteaptă până se facturează verificarea. |
| pending | Așteaptă să fie trimisă. |
| in_flight | Se trimite chiar acum. |
| retry_wait | Așteaptă următoarea încercare. |
| succeeded | Livrată. |
| exhausted | Toate încercările au eșuat. |
| blocked_endpoint | Endpointul webhook nu poate primi nimic momentan. |
| cancelled | Anulată, de exemplu pentru că mesajul a expirat. |
Activare, dezactivare, arhivare
POST …/{id}/disable oprește deocamdată livrarea. Monitoarele fără alt endpoint webhook se pun atunci pe pauză cu paused_configuration.
POST …/{id}/enable reactivează un endpoint webhook verificat. Reia apoi monitoarele puse pe pauză cu resume.
DELETE …/{id} arhivează endpointul webhook definitiv. Ca să-l oprești temporar, dezactivează-l în schimb.
curl -X POST "https://handelsregister.ai/api/v1/account/webhook-endpoints/wep_01m6b3j1g0cdz98hw8zdefbf52/disable" \ -H "Authorization: Bearer TOKENUL_TAU_ADMIN" \ -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 TOKENUL_TAU_ADMIN", "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 TOKENUL_TAU_ADMIN", "Idempotency-Key": crypto.randomUUID(), }, }); const data = await response.json();
Erori
Erorile au aceeași structură ca la account/monitors: un cod în error, explicația în meta.message. Erorile nu costă nimic.
| Status | Cauză | Ce poți face |
|---|---|---|
| 400 |
api_key se află în URL. |
Trimite cheia în headerul x-api-key. |
| 400 |
URL-ul nu este permis ca destinație, un header nu este permis, s-a atins numărul maxim sau endpointul webhook nu este activ. meta.message indică motivul. |
Corectează valoarea sau verifică mai întâi. |
| 400 |
Idempotency-Key are un format invalid. |
Corectează formatul, de exemplu folosește un UUID. |
| 401 | Nicio cheie API sau token ori unul invalid. |
Trimite cheia în headerul x-api-key. |
| 403 |
Bearer tokenului îi lipsește dreptul account:read. |
Folosește cheia API sau un token cu account:read. |
| 403 | Adresa de e-mail a contului tău nu este încă confirmată sau contul este blocat. | Deschide linkul din e-mailul de confirmare. Dacă contul este blocat, te poate ajuta suportul. |
| 403 | Acest apel are nevoie de tokenul admin. O cheie API nu ajunge, nici alt token. |
Trimite-l cu Authorization: Bearer și tokenul admin. |
| 403 |
Bearer tokenului îi lipsește dreptul monitoring:manage. |
Folosește cheia API sau tokenul admin. |
| 404 | Niciun endpoint webhook sau livrare cu acest ID. | Verifică ID-ul. |
| 409 |
Idempotency-Key a fost deja folosit cu alte date, este mai vechi de 24 de ore sau încă se procesează. |
Folosește o cheie nouă sau așteaptă puțin. |
| 422 |
O valoare lipsește sau este invalidă, de ex. name sau url. errors numește câmpul. |
Corectează valoarea și reîncearcă cu o cheie nouă. |
| 422 | Verificarea a eșuat. | Verifică headerul challenge și statusul 2xx, apoi reîncearcă cu o cheie nouă. |
| 428 |
Lipsește headerul Idempotency-Key. |
Trimite headerul. |
| 429 | Prea multe cereri. |
Reîncearcă după Retry-After. |
| 503 | Verificarea, testarea și retrimiterea sunt temporar indisponibile. | Reîncearcă puțin mai târziu. |
1{2 "error": "invalid_request",3 "meta": {4 "message": "The endpoint is not active.",5 "request_credit_cost": 0,6 "credits_remaining": 1457 }8}