/v1/account/webhook-endpoints
account/webhook-endpoints
Webhooky: kam Monitoring posílá nové Signals a jak je ověříš.
Webhook endpoint je HTTPS adresa tvého serveru, na kterou posíláme nové signály tvých monitorů. Každá zpráva je podepsaná, takže si můžeš ověřit, že pochází od nás.
Náklady a limity
| Co | Kredity |
|---|---|
| Správa webhook endpointů a doručení | zdarma |
Limit: 30 požadavků za minutu. Vytvoření, úprava a smazání: 10 za minutu. Kontrola, test a nový secret: 6 za minutu.
Každý účet může mít až 10 webhook endpointů; archivované se nepočítají. Každý webhook endpoint může nést až 10 vlastních headerů.
Admin token nebo API klíč
Admin token potřebuješ k vytvoření podpisového secretu a k zapnutí, vypnutí nebo archivaci webhook endpointů. Vytvoříš ho v Účet › API klíče v části „Pro vývojáře: správa klíčů přes API“. Platí jeden rok a nový nahradí starý.
Posílej admin token jako Authorization: Bearer TVUJ_ADMIN_TOKEN. Všechna ostatní volání fungují i s API klíčem.
Stejně jako u account/monitors potřebují všechna zápisová volání header Idempotency-Key.
Endpointy
| Metoda | Cesta | Účel | Přístup |
|---|---|---|---|
| GET |
/v1/account/webhook-endpoints |
Všechny webhook endpointy, bez archivovaných. | API klíč nebo admin token |
| POST |
/v1/account/webhook-endpoints |
Vytvořit webhook endpoint. | Admin token |
| POST |
/v1/account/webhook-endpoints/{id}/verify |
Ověřit. | API klíč nebo admin token |
| POST |
/v1/account/webhook-endpoints/{id}/test |
Poslat testovací zprávu. | API klíč nebo admin token |
| POST |
/v1/account/webhook-endpoints/{id}/rotate-secret |
Nový podpisový secret. | Admin token |
| POST |
/v1/account/webhook-endpoints/{id}/enable |
Zapnout. | Admin token |
| POST |
/v1/account/webhook-endpoints/{id}/disable |
Vypnout. | Admin token |
| DELETE |
/v1/account/webhook-endpoints/{id} |
Archivovat. | Admin token |
| GET |
/v1/account/webhook-deliveries |
Posledních 50 doručení. | API klíč nebo admin token |
| POST |
/v1/account/webhook-deliveries/{id}/retry |
Znovu poslat neúspěšné doručení. | API klíč nebo admin token |
| GET |
/v1/account/webhook-events |
Posledních 50 událostí. | API klíč nebo admin token |
Vytvořit webhook endpoint
curl -X POST "https://handelsregister.ai/api/v1/account/webhook-endpoints" \ -H "Authorization: Bearer TVUJ_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": "TVUJ_RECEIVER_TOKEN" } }'
import uuid import requests response = requests.post( "https://handelsregister.ai/api/v1/account/webhook-endpoints", headers={ "Authorization": "Bearer TVUJ_ADMIN_TOKEN", "Idempotency-Key": str(uuid.uuid4()), }, json={ "name": "CRM-Sync", "url": "https://hooks.example.com/handelsregister", "headers": { "X-Receiver-Token": "TVUJ_RECEIVER_TOKEN" } }, ) data = response.json()
const response = await fetch("https://handelsregister.ai/api/v1/account/webhook-endpoints", { method: "POST", headers: { "Authorization": "Bearer TVUJ_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": "TVUJ_RECEIVER_TOKEN" } }), }); const data = await response.json();
| Název | Typ | Popis | Příklad |
|---|---|---|---|
| Idempotency-KeyHeader | řetězecpovinný | Jednoznačná hodnota pro každou operaci, ideálně UUID. Viz Idempotency-Key.Limity: 1 až 128 znaků |
req-20260802-01 |
| nameBody | řetězecpovinný | Název pro tvou vlastní orientaci.Limity: 1 až 120 znaků |
CRM-Sync |
| urlBody | řetězec (uri)povinný |
Adresa tvého serveru: veřejně dostupná, https na portu 443, s názvem hostitele místo IP adresy.Limity: 1 až 2 048 znaků |
https://hooks.example.com/handelsregister |
| headersBody | objektvolitelný | Vlastní headery posílané s každou zprávou, například token pro tvůj server.Limity: nejvýše 10 položek |
{"X-Receiver-Token":"TVUJ_RECEIVER_TOKEN"} |
Názvy headerů nesmějí začínat na webhook- a nesmějí přepisovat standardní headery jako Content-Type nebo Host. Hodnoty jsou jen pro zápis; později API ukazuje jen názvy.
Odpověď obsahuje podpisový secret whsec_…. Ukážeme ho jen tentokrát. Hned si ho ulož.
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}
Nový webhook endpoint má stav pending_verification a zprávy přijímá až po ověření. Název, URL a headery nelze později změnit. Pro novou adresu vytvoř nový webhook endpoint.
V odpovědích ukazuje url jen host; cestu zkracujeme na /….
Ověřit
Pomocí POST …/{id}/verify pošleme na tvou URL podepsanou zprávu typu endpoint.verification. data.challenge obsahuje náhodnou hodnotu.
Tvůj server odpoví do 15 sekund se stavem 2xx a vrátí hodnotu v headeru odpovědi webhook-verification. Webhook endpoint pak přejde do 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: TVUJ_API_KLIC" \ -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": "TVUJ_API_KLIC", "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": "TVUJ_API_KLIC", "Idempotency-Key": crypto.randomUUID(), }, }); const data = await response.json();
Pokud to funguje, API odpoví s "verified": true, jinak s 422 a "verified": false. Pro nový pokus potřebuješ nový Idempotency-Key.
Každý webhook endpoint povoluje 6 pokusů za hodinu.
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}
Potom POST …/{id}/test pošle podepsanou testovací zprávu typu endpoint.test, kdykoli chceš. Odpověď ukazuje výsledek doručení.
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}
Ověřit podpis
Každá zpráva je podepsaná podle otevřeného standardu Standard Webhooks. Ověř podpis dřív, než zprávu zpracuješ.
| Header | Význam |
|---|---|
| webhook-id |
ID zprávy (msg_…). Při opakováních zůstává stejné. Podle něj poznáš duplicity.
|
| webhook-timestamp | Čas tohoto pokusu o doručení v unixových sekundách. |
| webhook-signature |
Jeden nebo více podpisů ve tvaru v1,<Base64>, oddělených mezerami.
|
Jak zprávu ověříš:
- Vezmi část secretu za
whsec_a dekóduj ji z Base64. To je klíč. - Sestav text
webhook-id.webhook-timestamp.Bodyze dvou headerů a nezměněného těla. - Spočítej z něj HMAC-SHA256 s klíčem a výsledek zakóduj do Base64.
- Porovnej výsledek v konstantním čase s každým podpisem
v1v headeru. Pokud jeden odpovídá, zpráva je pravá.
Použij tělo přesně tak, jak dorazí, ne znovu serializovaný JSON. Odmítej zprávy, jejichž časové razítko se liší o víc než pět 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"] # Odmítnout zprávy starší než pět 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() # Během rotace secretu nese header dva 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"]; // Odmítnout zprávy starší než pět 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(); // Během rotace secretu nese header dva 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); }); }
Místo psaní vlastního kódu můžeš použít oficiální knihovny Standard Webhooks, například balíček standardwebhooks pro Python a JavaScript.
Pomocí POST …/{id}/rotate-secret dostaneš nový secret, jednou, v odpovědi. Po 7 dní nesou zprávy dva podpisy, jeden s novým a jeden se starým secretem. Přejdeš tak bez výpadku.
Payload
Každá zpráva je objekt JSON se stejnou obálkou. To, co tvoří událost, je v data.
| Pole | Význam |
|---|---|
| id |
ID zprávy, shodné s headerem webhook-id.
|
| event_id |
ID události (evt_…).
|
| type | Typ události, viz níže. |
| timestamp | Kdy jsme událost zjistili, v UTC. |
| schema_version |
Verze struktury, aktuálně 1.
|
| data | Data události. |
| Typ události | Význam |
|---|---|
| organization.signal.detected |
Monitor našel nový signál. data.signal má stejnou strukturu jako signál v signals, plus data.monitor a v data.links.signal odkaz na signál v API.
|
| organization.signal.sample |
Příklad ze skutečné historie firmy, odeslaný z Účet › Monitoring. Stejná struktura jako výše, plus data.sample: true.
|
| endpoint.test |
Testovací zpráva přes /test.
|
| endpoint.verification | Challenge při ověření. |
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}
Zpráva má nejvýše 20 KB. Pokud je signál větší, details a register_entry se vynechají a data.truncated je true. Celý signál pak načteš přes odkaz.
Typy událostí, které neznáš, ignoruj a přesto odpověz 2xx. Tvůj server tak zůstane stabilní, když přibudou nové typy.
Doručení a opakování
Doručení se počítá jako úspěšné, když tvůj server odpoví do 15 sekund se stavem 2xx. Přesměrování nenásledujeme.
Jinak to zkoušíme znovu: až 10 pokusů, s rostoucími odstupy po dobu zhruba tří dnů. Pokud tvůj server pošle Retry-After, řídíme se jím.
Pokud tvůj server odpoví 410, přestaneme doručovat a webhook endpoint vypneme.
Pokud 5 doručení po sobě definitivně selže, webhook endpoint se vypne s disabled_failures. Pomocí POST …/{id}/enable ho znovu zapneš.
webhook-id. Odpověz rychle a zprávu zpracuj až potom.
GET /v1/account/webhook-deliveries ukazuje posledních 50 doručení, s ?endpoint=wep_… pro jeden webhook endpoint. GET /v1/account/webhook-events ukazuje posledních 50 událostí.
Definitivně neúspěšné doručení pošleš znovu pomocí POST /v1/account/webhook-deliveries/{id}/retry, se stejným webhook-id a stejným tělem. Funguje to 30 dní; tak dlouho zprávy uchováváme.
| Stav | Význam |
|---|---|
| withheld | Čeká, až bude kontrola zaúčtována. |
| pending | Čeká na odeslání. |
| in_flight | Právě se odesílá. |
| retry_wait | Čeká na další pokus. |
| succeeded | Doručeno. |
| exhausted | Všechny pokusy selhaly. |
| blocked_endpoint | Webhook endpoint teď nemůže nic přijímat. |
| cancelled | Zrušeno, například protože zpráva vypršela. |
Zapnout, vypnout, archivovat
POST …/{id}/disable prozatím zastaví doručování. Monitory bez dalšího webhook endpointu se pak pozastaví s paused_configuration.
POST …/{id}/enable znovu zapne ověřený webhook endpoint. Pozastavené monitory pak obnov pomocí resume.
DELETE …/{id} webhook endpoint natrvalo archivuje. Chceš-li ho zastavit jen na čas, raději ho vypni.
curl -X POST "https://handelsregister.ai/api/v1/account/webhook-endpoints/wep_01m6b3j1g0cdz98hw8zdefbf52/disable" \ -H "Authorization: Bearer TVUJ_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 TVUJ_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 TVUJ_ADMIN_TOKEN", "Idempotency-Key": crypto.randomUUID(), }, }); const data = await response.json();
Chyby
Chyby mají stejnou strukturu jako u account/monitors: kód v error, vysvětlení v meta.message. Chyby nic nestojí.
| Stav | Příčina | Co můžeš udělat |
|---|---|---|
| 400 |
api_key je v URL. |
Posílej klíč v headeru x-api-key. |
| 400 |
URL není povolena jako cíl, header není přípustný, bylo dosaženo maximálního počtu, nebo webhook endpoint není aktivní. meta.message uvádí důvod. |
Oprav hodnotu nebo nejdřív ověř. |
| 400 |
Idempotency-Key má neplatný formát. |
Oprav formát, například použij UUID. |
| 401 | Žádný nebo neplatný API klíč či token. |
Posílej klíč v headeru x-api-key. |
| 403 |
Bearer tokenu chybí oprávnění account:read. |
Použij API klíč nebo token s account:read. |
| 403 | E-mailová adresa tvého účtu ještě není potvrzená, nebo je účet zablokovaný. | Otevři odkaz z potvrzovacího e-mailu. Pokud je účet zablokovaný, pomůže ti podpora. |
| 403 | Toto volání potřebuje admin token. API klíč nestačí a jiný token také ne. |
Pošli ho s Authorization: Bearer a admin tokenem. |
| 403 |
Bearer tokenu chybí oprávnění monitoring:manage. |
Použij API klíč nebo admin token. |
| 404 | Žádný webhook endpoint ani doručení s tímto ID. | Zkontroluj ID. |
| 409 |
Idempotency-Key už byl použit s jinými daty, je starší než 24 hodin, nebo se ještě zpracovává. |
Použij nový klíč nebo chvíli počkej. |
| 422 |
Hodnota chybí nebo je neplatná, například name nebo url. errors jmenuje pole. |
Oprav hodnotu a zkus to znovu s novým klíčem. |
| 422 | Ověření selhalo. | Zkontroluj challenge header a stav 2xx, pak to zkus znovu s novým klíčem. |
| 428 |
Chybí header Idempotency-Key. |
Pošli header. |
| 429 | Příliš mnoho požadavků. |
Zkus to znovu po Retry-After. |
| 503 | Ověřování, testování a opětovné odesílání jsou dočasně nedostupné. | Zkus to o něco později. |
1{2 "error": "invalid_request",3 "meta": {4 "message": "The endpoint is not active.",5 "request_credit_cost": 0,6 "credits_remaining": 1457 }8}