/v1/account/webhook-endpoints
account/webhook-endpoints
Webhook: dove Monitoring invia i nuovi Signals e come li verifichi.
Un endpoint webhook è l’indirizzo HTTPS del tuo server a cui inviamo i nuovi segnali dei tuoi monitor. Ogni messaggio è firmato, così puoi verificare che venga da noi.
Costi e limiti
| Cosa | Crediti |
|---|---|
| Gestire endpoint webhook e consegne | gratis |
Limite: 30 richieste al minuto. Creare, modificare ed eliminare: 10 al minuto. Controllo, test e nuovo secret: 6 al minuto.
Ogni account può avere fino a 10 endpoint webhook; quelli archiviati non contano. Ogni endpoint webhook può portare fino a 10 header personalizzati.
Admin token o chiave API
Ti serve l’admin token per creare un secret di firma e per attivare, disattivare o archiviare endpoint webhook. Lo crei in Account › Chiavi API nella sezione “Per sviluppatori: gestire le chiavi tramite API”. Vale un anno e uno nuovo sostituisce quello vecchio.
Invia l’admin token come Authorization: Bearer IL_TUO_ADMIN_TOKEN. Tutte le altre chiamate funzionano anche con la chiave API.
Come per account/monitors, tutte le chiamate di scrittura richiedono l’header Idempotency-Key.
Endpoint
| Metodo | Percorso | Scopo | Accesso |
|---|---|---|---|
| GET |
/v1/account/webhook-endpoints |
Tutti gli endpoint webhook, senza quelli archiviati. | Chiave API o admin token |
| POST |
/v1/account/webhook-endpoints |
Creare un endpoint webhook. | Admin token |
| POST |
/v1/account/webhook-endpoints/{id}/verify |
Verificare. | Chiave API o admin token |
| POST |
/v1/account/webhook-endpoints/{id}/test |
Inviare un messaggio di prova. | Chiave API o admin token |
| POST |
/v1/account/webhook-endpoints/{id}/rotate-secret |
Nuovo secret di firma. | Admin token |
| POST |
/v1/account/webhook-endpoints/{id}/enable |
Attivare. | Admin token |
| POST |
/v1/account/webhook-endpoints/{id}/disable |
Disattivare. | Admin token |
| DELETE |
/v1/account/webhook-endpoints/{id} |
Archiviare. | Admin token |
| GET |
/v1/account/webhook-deliveries |
Le ultime 50 consegne. | Chiave API o admin token |
| POST |
/v1/account/webhook-deliveries/{id}/retry |
Reinviare una consegna non riuscita. | Chiave API o admin token |
| GET |
/v1/account/webhook-events |
Gli ultimi 50 eventi. | Chiave API o admin token |
Creare un endpoint webhook
curl -X POST "https://handelsregister.ai/api/v1/account/webhook-endpoints" \ -H "Authorization: Bearer IL_TUO_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": "IL_TUO_RECEIVER_TOKEN" } }'
import uuid import requests response = requests.post( "https://handelsregister.ai/api/v1/account/webhook-endpoints", headers={ "Authorization": "Bearer IL_TUO_ADMIN_TOKEN", "Idempotency-Key": str(uuid.uuid4()), }, json={ "name": "CRM-Sync", "url": "https://hooks.example.com/handelsregister", "headers": { "X-Receiver-Token": "IL_TUO_RECEIVER_TOKEN" } }, ) data = response.json()
const response = await fetch("https://handelsregister.ai/api/v1/account/webhook-endpoints", { method: "POST", headers: { "Authorization": "Bearer IL_TUO_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": "IL_TUO_RECEIVER_TOKEN" } }), }); const data = await response.json();
| Nome | Tipo | Descrizione | Esempio |
|---|---|---|---|
| Idempotency-KeyHeader | stringaobbligatorio | Un valore univoco per operazione, idealmente un UUID. Vedi Idempotency-Key.Limiti: da 1 a 128 caratteri |
req-20260802-01 |
| nameBody | stringaobbligatorio | Un nome come tuo riferimento.Limiti: da 1 a 120 caratteri |
CRM-Sync |
| urlBody | stringa (uri)obbligatorio |
L’indirizzo del tuo server: raggiungibile pubblicamente, https sulla porta 443, con un nome host invece di un indirizzo IP.Limiti: da 1 a 2.048 caratteri |
https://hooks.example.com/handelsregister |
| headersBody | oggettofacoltativo | Header personalizzati inviati con ogni messaggio, come un token per il tuo server.Limiti: al massimo 10 voci |
{"X-Receiver-Token":"IL_TUO_RECEIVER_TOKEN"} |
I nomi degli header non devono iniziare con webhook- e non devono sovrascrivere header standard come Content-Type o Host. I valori sono di sola scrittura; in seguito l’API mostra solo i nomi.
La risposta contiene il secret di firma whsec_…. Lo mostriamo solo questa volta. Salvalo subito.
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 nuovo endpoint webhook ha lo stato pending_verification e riceve messaggi solo dopo essere stato verificato. Nome, URL e header non si possono cambiare in seguito. Per un nuovo indirizzo, crea un nuovo endpoint webhook.
Nelle risposte url mostra solo l’host; abbreviamo il percorso in /….
Verificare
Con POST …/{id}/verify inviamo al tuo URL un messaggio firmato di tipo endpoint.verification. data.challenge contiene un valore casuale.
Il tuo server risponde entro 15 secondi con uno stato 2xx e restituisce il valore nell’header di risposta webhook-verification. L’endpoint webhook diventa allora 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: LA_TUA_CHIAVE_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": "LA_TUA_CHIAVE_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": "LA_TUA_CHIAVE_API", "Idempotency-Key": crypto.randomUUID(), }, }); const data = await response.json();
Se funziona, l’API risponde con "verified": true, altrimenti con 422 e "verified": false. Per un nuovo tentativo ti serve un nuovo Idempotency-Key.
Ogni endpoint webhook consente 6 tentativi all’ora.
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}
Dopo, POST …/{id}/test invia quando vuoi un messaggio di prova firmato di tipo endpoint.test. La risposta mostra l’esito della consegna.
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}
Verificare la firma
Ogni messaggio è firmato secondo lo standard aperto Standard Webhooks. Verifica la firma prima di elaborare un messaggio.
| Header | Significato |
|---|---|
| webhook-id |
ID del messaggio (msg_…). Resta uguale nei nuovi tentativi. Usalo per riconoscere i duplicati.
|
| webhook-timestamp | Momento di questo tentativo di consegna in secondi Unix. |
| webhook-signature |
Una o più firme nella forma v1,<Base64>, separate da spazi.
|
Come verificare un messaggio:
- Prendi la parte del secret dopo
whsec_e decodificala da Base64. Quella è la chiave. - Componi il testo
webhook-id.webhook-timestamp.Bodydai due header e dal corpo invariato. - Calcola su di esso l’HMAC-SHA256 con la chiave e codifica il risultato in Base64.
- Confronta il risultato in tempo costante con ogni firma
v1nell’header. Se una corrisponde, il messaggio è autentico.
Usa il corpo esattamente come arriva, non un JSON riserializzato. Rifiuta i messaggi il cui timestamp si discosta di più di cinque minuti.
import base64, hashlib, hmac, time def verify_webhook(secret: str, headers: dict, body: bytes) -> bool: msg_id = headers["webhook-id"] timestamp = headers["webhook-timestamp"] # Rifiuta i messaggi più vecchi di cinque minuti 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() # Durante una rotazione del secret, l’header contiene due firme 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"]; // Rifiuta i messaggi più vecchi di cinque minuti 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(); // Durante una rotazione del secret, l’header contiene due firme 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); }); }
Invece di scrivere codice tuo, puoi usare le librerie ufficiali Standard Webhooks, come il pacchetto standardwebhooks per Python e JavaScript.
Con POST …/{id}/rotate-secret ricevi un nuovo secret, una sola volta, nella risposta. Per 7 giorni i messaggi portano due firme, una con il nuovo e una con il vecchio secret. Così passi al nuovo senza interruzioni.
Payload
Ogni messaggio è un oggetto JSON con la stessa busta. Ciò che costituisce l’evento è in data.
| Campo | Significato |
|---|---|
| id |
ID del messaggio, uguale all’header webhook-id.
|
| event_id |
ID dell’evento (evt_…).
|
| type | Tipo dell’evento, vedi sotto. |
| timestamp | Quando abbiamo rilevato l’evento, in UTC. |
| schema_version |
Versione della struttura, attualmente 1.
|
| data | I dati dell’evento. |
| Tipo di evento | Significato |
|---|---|
| organization.signal.detected |
Un monitor ha trovato un nuovo segnale. data.signal ha la stessa struttura di un segnale in signals, più data.monitor e, in data.links.signal, il link al segnale nell’API.
|
| organization.signal.sample |
Un esempio dallo storico reale dell’azienda, inviato da Account › Monitoring. Stessa struttura di sopra, più data.sample: true.
|
| endpoint.test |
Messaggio di prova tramite /test.
|
| endpoint.verification | Challenge durante la verifica. |
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 messaggio è al massimo di 20 KB. Se un segnale diventa più grande, details e register_entry vengono omessi e data.truncated è true. Recuperi allora l’intero segnale tramite il link.
Ignora i tipi di evento che non conosci e rispondi comunque con 2xx. Così il tuo server resta stabile quando si aggiungono nuovi tipi.
Consegne e nuovi tentativi
Una consegna è considerata riuscita quando il tuo server risponde entro 15 secondi con uno stato 2xx. Non seguiamo i redirect.
Altrimenti riproviamo: fino a 10 tentativi, con intervalli crescenti per circa tre giorni. Se il tuo server invia Retry-After, lo rispettiamo.
Se il tuo server risponde con 410, smettiamo di consegnare e disattiviamo l’endpoint webhook.
Se 5 consegne di fila falliscono definitivamente, l’endpoint webhook viene disattivato con disabled_failures. Con POST …/{id}/enable lo riattivi.
webhook-id. Rispondi rapidamente ed elabora il messaggio solo dopo.
GET /v1/account/webhook-deliveries mostra le ultime 50 consegne, con ?endpoint=wep_… per un endpoint webhook. GET /v1/account/webhook-events mostra gli ultimi 50 eventi.
Una consegna fallita definitivamente la reinvii con POST /v1/account/webhook-deliveries/{id}/retry, con lo stesso webhook-id e lo stesso corpo. Funziona per 30 giorni; per tanto tempo conserviamo i messaggi.
| Stato | Significato |
|---|---|
| withheld | Attende che il controllo sia stato addebitato. |
| pending | Attende di essere inviata. |
| in_flight | In fase di invio. |
| retry_wait | Attende il prossimo tentativo. |
| succeeded | Consegnata. |
| exhausted | Tutti i tentativi sono falliti. |
| blocked_endpoint | L’endpoint webhook al momento non può ricevere nulla. |
| cancelled | Annullata, ad esempio perché il messaggio è scaduto. |
Attivare, disattivare, archiviare
POST …/{id}/disable sospende la consegna per il momento. I monitor senza un altro endpoint webhook vanno allora in pausa con paused_configuration.
POST …/{id}/enable riattiva un endpoint webhook verificato. Poi riprendi i monitor in pausa con resume.
DELETE …/{id} archivia l’endpoint webhook definitivamente. Per fermarlo temporaneamente, disattivalo invece.
curl -X POST "https://handelsregister.ai/api/v1/account/webhook-endpoints/wep_01m6b3j1g0cdz98hw8zdefbf52/disable" \ -H "Authorization: Bearer IL_TUO_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 IL_TUO_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 IL_TUO_ADMIN_TOKEN", "Idempotency-Key": crypto.randomUUID(), }, }); const data = await response.json();
Errori
Gli errori hanno la stessa struttura di account/monitors: un codice in error, la spiegazione in meta.message. Gli errori non costano nulla.
| Stato | Causa | Cosa puoi fare |
|---|---|---|
| 400 |
api_key è nell’URL. |
Invia la chiave nell’header x-api-key. |
| 400 |
L’URL non è consentito come destinazione, un header non è ammesso, è stato raggiunto il numero massimo oppure l’endpoint webhook non è attivo. meta.message indica il motivo. |
Correggi il valore o verifica prima. |
| 400 |
L’Idempotency-Key ha un formato non valido. |
Correggi il formato, ad esempio usa un UUID. |
| 401 | Nessuna chiave API o token, o uno non valido. |
Invia la chiave nell’header x-api-key. |
| 403 |
Al bearer token manca l’abilitazione account:read. |
Usa la chiave API o un token con account:read. |
| 403 | L’indirizzo email del tuo account non è ancora confermato, oppure l’account è bloccato. | Apri il link dall’email di conferma. Se l’account è bloccato, il supporto può aiutarti. |
| 403 | Questa chiamata richiede l’admin token. Una chiave API non basta, e nemmeno un altro token. |
Invialo con Authorization: Bearer e l’admin token. |
| 403 |
Al bearer token manca l’abilitazione monitoring:manage. |
Usa la chiave API o l’admin token. |
| 404 | Nessun endpoint webhook o consegna con questo ID. | Controlla l’ID. |
| 409 |
L’Idempotency-Key è già stato usato con dati diversi, è più vecchio di 24 ore oppure è ancora in elaborazione. |
Usa una nuova chiave o aspetta un momento. |
| 422 |
Un valore manca o non è valido, come name o url. errors indica il campo. |
Correggi il valore e riprova con una nuova chiave. |
| 422 | Verifica non riuscita. | Controlla l’header della challenge e lo stato 2xx, poi riprova con una nuova chiave. |
| 428 |
Manca l’header Idempotency-Key. |
Invia l’header. |
| 429 | Troppe richieste. |
Riprova dopo Retry-After. |
| 503 | Verifica, test e reinvio sono temporaneamente non disponibili. | Riprova poco più tardi. |
1{2 "error": "invalid_request",3 "meta": {4 "message": "The endpoint is not active.",5 "request_credit_cost": 0,6 "credits_remaining": 1457 }8}