/v1/account/webhook-endpoints
account/webhook-endpoints
Webhooki: dokąd Monitoring wysyła nowe Signals i jak je weryfikujesz.
Endpoint webhooka to adres HTTPS twojego serwera, na który wysyłamy nowe sygnały twoich monitorów. Każda wiadomość jest podpisana, więc możesz sprawdzić, że pochodzi od nas.
Koszty i limity
| Co | Kredyty |
|---|---|
| Zarządzanie endpointami webhooków i dostarczeniami | bezpłatnie |
Limit: 30 zapytań na minutę. Tworzenie, zmiana i usuwanie: 10 na minutę. Sprawdzenie, test i nowy secret: 6 na minutę.
Każde konto może mieć do 10 endpointów webhooków; zarchiwizowane się nie liczą. Każdy endpoint webhooka może mieć do 10 własnych nagłówków.
Token admina lub klucz API
Token admina jest potrzebny do utworzenia secretu do podpisów oraz do włączania, wyłączania i archiwizowania endpointów webhooków. Tworzysz go w Konto › Klucze API w sekcji „Dla deweloperów: zarządzaj kluczami przez API”. Jest ważny przez rok, a nowy zastępuje stary.
Wysyłaj token admina jako Authorization: Bearer TWOJ_TOKEN_ADMINA. Wszystkie inne wywołania działają też z kluczem API.
Tak jak w account/monitors wszystkie wywołania zapisujące wymagają nagłówka Idempotency-Key.
Endpointy
| Metoda | Ścieżka | Cel | Dostęp |
|---|---|---|---|
| GET |
/v1/account/webhook-endpoints |
Wszystkie endpointy webhooków, bez zarchiwizowanych. | Klucz API lub token admina |
| POST |
/v1/account/webhook-endpoints |
Utwórz endpoint webhooka. | Token admina |
| POST |
/v1/account/webhook-endpoints/{id}/verify |
Zweryfikuj. | Klucz API lub token admina |
| POST |
/v1/account/webhook-endpoints/{id}/test |
Wyślij wiadomość testową. | Klucz API lub token admina |
| POST |
/v1/account/webhook-endpoints/{id}/rotate-secret |
Nowy secret do podpisów. | Token admina |
| POST |
/v1/account/webhook-endpoints/{id}/enable |
Włącz. | Token admina |
| POST |
/v1/account/webhook-endpoints/{id}/disable |
Wyłącz. | Token admina |
| DELETE |
/v1/account/webhook-endpoints/{id} |
Zarchiwizuj. | Token admina |
| GET |
/v1/account/webhook-deliveries |
Ostatnie 50 dostarczeń. | Klucz API lub token admina |
| POST |
/v1/account/webhook-deliveries/{id}/retry |
Wyślij ponownie nieudane dostarczenie. | Klucz API lub token admina |
| GET |
/v1/account/webhook-events |
Ostatnie 50 zdarzeń. | Klucz API lub token admina |
Utwórz endpoint webhooka
curl -X POST "https://handelsregister.ai/api/v1/account/webhook-endpoints" \ -H "Authorization: Bearer TWOJ_TOKEN_ADMINA" \ -H "Idempotency-Key: $(uuidgen)" \ -H "Content-Type: application/json" \ -d '{ "name": "CRM-Sync", "url": "https://hooks.example.com/handelsregister", "headers": { "X-Receiver-Token": "TWOJ_TOKEN_ODBIORCY" } }'
import uuid import requests response = requests.post( "https://handelsregister.ai/api/v1/account/webhook-endpoints", headers={ "Authorization": "Bearer TWOJ_TOKEN_ADMINA", "Idempotency-Key": str(uuid.uuid4()), }, json={ "name": "CRM-Sync", "url": "https://hooks.example.com/handelsregister", "headers": { "X-Receiver-Token": "TWOJ_TOKEN_ODBIORCY" } }, ) data = response.json()
const response = await fetch("https://handelsregister.ai/api/v1/account/webhook-endpoints", { method: "POST", headers: { "Authorization": "Bearer TWOJ_TOKEN_ADMINA", "Idempotency-Key": crypto.randomUUID(), "Content-Type": "application/json", }, body: JSON.stringify({ "name": "CRM-Sync", "url": "https://hooks.example.com/handelsregister", "headers": { "X-Receiver-Token": "TWOJ_TOKEN_ODBIORCY" } }), }); const data = await response.json();
| Nazwa | Typ | Opis | Przykład |
|---|---|---|---|
| Idempotency-KeyHeader | stringwymagany | Unikalna wartość na operację, najlepiej UUID. Zobacz Idempotency-Key.Granice: od 1 do 128 znaków |
req-20260802-01 |
| nameBody | stringwymagany | Nazwa na twój własny użytek.Granice: od 1 do 120 znaków |
CRM-Sync |
| urlBody | string (uri)wymagany |
Adres twojego serwera: publicznie dostępny, https na porcie 443, z nazwą hosta zamiast adresu IP.Granice: od 1 do 2 048 znaków |
https://hooks.example.com/handelsregister |
| headersBody | obiektopcjonalny | Własne nagłówki wysyłane z każdą wiadomością, np. token dla twojego serwera.Granice: najwyżej 10 pozycji |
{"X-Receiver-Token":"TWOJ_TOKEN_ODBIORCY"} |
Nazwy nagłówków nie mogą zaczynać się od webhook- ani nadpisywać standardowych nagłówków, takich jak Content-Type lub Host. Wartości są tylko do zapisu; później API pokazuje tylko nazwy.
Odpowiedź zawiera secret do podpisów whsec_…. Pokazujemy go tylko ten jeden raz. Zapisz go od razu.
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}
Nowy endpoint webhooka ma status pending_verification i otrzymuje wiadomości dopiero po weryfikacji. Nazwy, URL i nagłówków nie da się później zmienić. Dla nowego adresu utwórz nowy endpoint webhooka.
W odpowiedziach url pokazuje tylko hosta; ścieżkę skracamy do /….
Weryfikacja
Przez POST …/{id}/verify wysyłamy podpisaną wiadomość typu endpoint.verification na twój URL. data.challenge zawiera losową wartość.
Twój serwer odpowiada w ciągu 15 sekund statusem 2xx i zwraca wartość w nagłówku odpowiedzi webhook-verification. Endpoint webhooka staje się wtedy 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: TWOJ_KLUCZ_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": "TWOJ_KLUCZ_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": "TWOJ_KLUCZ_API", "Idempotency-Key": crypto.randomUUID(), }, }); const data = await response.json();
Jeśli się uda, API odpowiada "verified": true, w przeciwnym razie 422 i "verified": false. Do nowej próby potrzebujesz nowego Idempotency-Key.
Każdy endpoint webhooka pozwala na 6 prób na godzinę.
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}
Potem POST …/{id}/test wysyła w dowolnej chwili podpisaną wiadomość testową typu endpoint.test. Odpowiedź pokazuje wynik dostarczenia.
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}
Weryfikacja podpisu
Każda wiadomość jest podpisana zgodnie z otwartym standardem Standard Webhooks. Sprawdź podpis, zanim przetworzysz wiadomość.
| Nagłówek | Znaczenie |
|---|---|
| webhook-id |
ID wiadomości (msg_…). Pozostaje takie samo przy ponowieniach. Użyj go do wykrywania duplikatów.
|
| webhook-timestamp | Czas tej próby dostarczenia w sekundach Unix. |
| webhook-signature |
Jeden lub więcej podpisów w formie v1,<Base64>, rozdzielonych spacjami.
|
Jak zweryfikować wiadomość:
- Weź część secretu po
whsec_i zdekoduj ją z Base64. To jest klucz. - Zbuduj tekst
webhook-id.webhook-timestamp.Bodyz dwóch nagłówków i niezmienionego body. - Oblicz z niego HMAC-SHA256 kluczem i zakoduj wynik w Base64.
- Porównaj wynik w stałym czasie z każdym podpisem
v1w nagłówku. Jeśli jeden pasuje, wiadomość jest prawdziwa.
Użyj body dokładnie tak, jak przychodzi, nie ponownie zserializowanego JSON. Odrzucaj wiadomości, których znacznik czasu odbiega o więcej niż pięć minut.
import base64, hashlib, hmac, time def verify_webhook(secret: str, headers: dict, body: bytes) -> bool: msg_id = headers["webhook-id"] timestamp = headers["webhook-timestamp"] # Odrzucaj wiadomości starsze niż pięć minut 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() # Podczas rotacji secretu w nagłówku są dwa podpisy 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"]; // Odrzucaj wiadomości starsze niż pięć minut 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(); // Podczas rotacji secretu w nagłówku są dwa podpisy 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); }); }
Zamiast pisać własny kod, możesz użyć oficjalnych bibliotek Standard Webhooks, np. pakietu standardwebhooks dla Pythona i JavaScriptu.
Przez POST …/{id}/rotate-secret dostajesz nowy secret, jednorazowo, w odpowiedzi. Przez 7 dni wiadomości mają dwa podpisy, jeden nowym, a jeden starym secretem. Tak przełączasz się bez przestoju.
Payload
Każda wiadomość to obiekt JSON z tą samą kopertą. Co stanowi zdarzenie, jest w data.
| Pole | Znaczenie |
|---|---|
| id |
ID wiadomości, równe nagłówkowi webhook-id.
|
| event_id |
ID zdarzenia (evt_…).
|
| type | Typ zdarzenia, zobacz niżej. |
| timestamp | Kiedy wykryliśmy zdarzenie, w UTC. |
| schema_version |
Wersja struktury, obecnie 1.
|
| data | Dane zdarzenia. |
| Typ zdarzenia | Znaczenie |
|---|---|
| organization.signal.detected |
Monitor znalazł nowy sygnał. data.signal ma tę samą strukturę co sygnał w signals, plus data.monitor i, pod data.links.signal, link do sygnału w API.
|
| organization.signal.sample |
Przykład z prawdziwej historii firmy, wysłany w Konto › Monitoring. Ta sama struktura co wyżej, plus data.sample: true.
|
| endpoint.test |
Wiadomość testowa przez /test.
|
| endpoint.verification | Challenge podczas weryfikacji. |
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}
Wiadomość ma najwyżej 20 KB. Jeśli sygnał jest większy, details i register_entry są pomijane, a data.truncated ma wartość true. Cały sygnał pobierasz wtedy przez link.
Ignoruj typy zdarzeń, których nie znasz, i mimo to odpowiadaj 2xx. Dzięki temu twój serwer pozostaje stabilny, gdy dojdą nowe typy.
Dostarczenia i ponowienia
Dostarczenie jest udane, gdy twój serwer odpowie w ciągu 15 sekund statusem 2xx. Nie podążamy za przekierowaniami.
W przeciwnym razie próbujemy ponownie: do 10 prób, w rosnących odstępach przez około trzy dni. Jeśli twój serwer wyśle Retry-After, stosujemy się do niego.
Jeśli twój serwer odpowie 410, przestajemy dostarczać i wyłączamy endpoint webhooka.
Jeśli 5 dostarczeń z rzędu nie uda się ostatecznie, endpoint webhooka jest wyłączany z disabled_failures. Przez POST …/{id}/enable włączasz go ponownie.
webhook-id. Odpowiadaj szybko i przetwarzaj wiadomość dopiero potem.
GET /v1/account/webhook-deliveries pokazuje ostatnie 50 dostarczeń, z ?endpoint=wep_… dla jednego endpointu webhooka. GET /v1/account/webhook-events pokazuje ostatnie 50 zdarzeń.
Dostarczenie, które ostatecznie się nie udało, wysyłasz ponownie przez POST /v1/account/webhook-deliveries/{id}/retry, z tym samym webhook-id i tym samym body. Działa to przez 30 dni; tak długo przechowujemy wiadomości.
| Status | Znaczenie |
|---|---|
| withheld | Czeka, aż sprawdzenie zostanie naliczone. |
| pending | Czeka na wysłanie. |
| in_flight | Właśnie wysyłane. |
| retry_wait | Czeka na następną próbę. |
| succeeded | Dostarczone. |
| exhausted | Wszystkie próby się nie udały. |
| blocked_endpoint | Endpoint webhooka nie może w tej chwili niczego odebrać. |
| cancelled | Anulowane, np. bo wiadomość wygasła. |
Włącz, wyłącz, zarchiwizuj
POST …/{id}/disable tymczasowo zatrzymuje dostarczanie. Monitory bez innego endpointu webhooka wstrzymują się wtedy z paused_configuration.
POST …/{id}/enable włącza ponownie zweryfikowany endpoint webhooka. Potem wznów wstrzymane monitory przez resume.
DELETE …/{id} archiwizuje endpoint webhooka na stałe. Aby zatrzymać go na jakiś czas, wyłącz go zamiast tego.
curl -X POST "https://handelsregister.ai/api/v1/account/webhook-endpoints/wep_01m6b3j1g0cdz98hw8zdefbf52/disable" \ -H "Authorization: Bearer TWOJ_TOKEN_ADMINA" \ -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 TWOJ_TOKEN_ADMINA", "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 TWOJ_TOKEN_ADMINA", "Idempotency-Key": crypto.randomUUID(), }, }); const data = await response.json();
Błędy
Błędy mają tę samą strukturę co w account/monitors: kod w error, wyjaśnienie w meta.message. Błędy nic nie kosztują.
| Status | Przyczyna | Co możesz zrobić |
|---|---|---|
| 400 |
api_key jest w URL. |
Wysyłaj klucz w nagłówku x-api-key. |
| 400 |
URL nie jest dozwolony jako cel, nagłówek jest niedozwolony, osiągnięto maksymalną liczbę albo endpoint webhooka nie jest aktywny. meta.message podaje powód. |
Popraw wartość lub najpierw zweryfikuj. |
| 400 |
Idempotency-Key ma nieprawidłowy format. |
Popraw format, np. użyj UUID. |
| 401 | Brak klucza API lub tokenu albo są nieprawidłowe. |
Wysyłaj klucz w nagłówku x-api-key. |
| 403 |
Bearer tokenowi brakuje uprawnienia account:read. |
Użyj klucza API lub tokenu z account:read. |
| 403 | Adres e-mail twojego konta nie jest jeszcze potwierdzony albo konto jest zablokowane. | Otwórz link z e-maila potwierdzającego. Jeśli konto jest zablokowane, pomoże support. |
| 403 | To wywołanie wymaga tokenu admina. Klucz API nie wystarcza, inny token też nie. |
Wyślij go z Authorization: Bearer i tokenem admina. |
| 403 |
Bearer tokenowi brakuje uprawnienia monitoring:manage. |
Użyj klucza API lub tokenu admina. |
| 404 | Brak endpointu webhooka lub dostarczenia o tym ID. | Sprawdź ID. |
| 409 |
Idempotency-Key był już użyty z innymi danymi, jest starszy niż 24 godziny lub jest jeszcze przetwarzany. |
Użyj nowego klucza lub odczekaj chwilę. |
| 422 |
Brakuje wartości lub jest nieprawidłowa, np. name lub url. errors wskazuje pole. |
Popraw wartość i spróbuj ponownie z nowym kluczem. |
| 422 | Weryfikacja się nie udała. | Sprawdź nagłówek challenge i status 2xx, potem spróbuj ponownie z nowym kluczem. |
| 428 |
Brakuje nagłówka Idempotency-Key. |
Wyślij nagłówek. |
| 429 | Za dużo zapytań. |
Spróbuj ponownie po Retry-After. |
| 503 | Weryfikacja, testy i ponowne wysyłanie są chwilowo niedostępne. | Spróbuj ponownie nieco później. |
1{2 "error": "invalid_request",3 "meta": {4 "message": "The endpoint is not active.",5 "request_credit_cost": 0,6 "credits_remaining": 1457 }8}