/v1/account/webhook-endpoints
account/webhook-endpoints
Webhooks : où le Monitoring envoie les nouveaux Signals et comment les vérifier.
Un endpoint webhook est l’adresse HTTPS de ton serveur à laquelle nous envoyons les nouveaux signals de tes moniteurs. Chaque message est signé, tu peux donc vérifier qu’il vient de nous.
Coûts et limites
| Quoi | Crédits |
|---|---|
| Gérer les endpoints webhook et les livraisons | gratuit |
Limite : 30 requêtes par minute. Créer, modifier et supprimer : 10 par minute. Vérifier, tester et nouveau secret : 6 par minute.
Chaque compte peut avoir jusqu’à 10 endpoints webhook ; les archivés ne comptent pas. Chaque endpoint webhook peut porter jusqu’à 10 headers personnalisés.
Token admin ou clé API
Il te faut le token admin pour créer un secret de signature et pour activer, désactiver ou archiver des endpoints webhook. Tu le crées sous Compte › Clés API dans la section « Pour les développeurs : gérer les clés via l’API ». Il est valable un an, et un nouveau remplace l’ancien.
Envoie le token admin sous la forme Authorization: Bearer TON_TOKEN_ADMIN. Tous les autres appels fonctionnent aussi avec la clé API.
Comme pour account/monitors, tous les appels d’écriture nécessitent le header Idempotency-Key.
Endpoints
| Méthode | Chemin | Usage | Accès |
|---|---|---|---|
| GET |
/v1/account/webhook-endpoints |
Tous les endpoints webhook, sans les archivés. | Clé API ou token admin |
| POST |
/v1/account/webhook-endpoints |
Créer un endpoint webhook. | Token admin |
| POST |
/v1/account/webhook-endpoints/{id}/verify |
Vérifier. | Clé API ou token admin |
| POST |
/v1/account/webhook-endpoints/{id}/test |
Envoyer un message de test. | Clé API ou token admin |
| POST |
/v1/account/webhook-endpoints/{id}/rotate-secret |
Nouveau secret de signature. | Token admin |
| POST |
/v1/account/webhook-endpoints/{id}/enable |
Activer. | Token admin |
| POST |
/v1/account/webhook-endpoints/{id}/disable |
Désactiver. | Token admin |
| DELETE |
/v1/account/webhook-endpoints/{id} |
Archiver. | Token admin |
| GET |
/v1/account/webhook-deliveries |
Les 50 dernières livraisons. | Clé API ou token admin |
| POST |
/v1/account/webhook-deliveries/{id}/retry |
Renvoyer une livraison échouée. | Clé API ou token admin |
| GET |
/v1/account/webhook-events |
Les 50 derniers événements. | Clé API ou token admin |
Créer un endpoint webhook
curl -X POST "https://handelsregister.ai/api/v1/account/webhook-endpoints" \ -H "Authorization: Bearer TON_TOKEN_ADMIN" \ -H "Idempotency-Key: $(uuidgen)" \ -H "Content-Type: application/json" \ -d '{ "name": "CRM-Sync", "url": "https://hooks.example.com/handelsregister", "headers": { "X-Receiver-Token": "TON_TOKEN_RECEPTEUR" } }'
import uuid import requests response = requests.post( "https://handelsregister.ai/api/v1/account/webhook-endpoints", headers={ "Authorization": "Bearer TON_TOKEN_ADMIN", "Idempotency-Key": str(uuid.uuid4()), }, json={ "name": "CRM-Sync", "url": "https://hooks.example.com/handelsregister", "headers": { "X-Receiver-Token": "TON_TOKEN_RECEPTEUR" } }, ) data = response.json()
const response = await fetch("https://handelsregister.ai/api/v1/account/webhook-endpoints", { method: "POST", headers: { "Authorization": "Bearer TON_TOKEN_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": "TON_TOKEN_RECEPTEUR" } }), }); const data = await response.json();
| Nom | Type | Description | Exemple |
|---|---|---|---|
| Idempotency-KeyHeader | chaîneobligatoire | Une valeur unique par opération, idéalement un UUID. Voir Idempotency-Key.Limites: 1 à 128 caractères |
req-20260802-01 |
| nameBody | chaîneobligatoire | Un nom pour ta propre référence.Limites: 1 à 120 caractères |
CRM-Sync |
| urlBody | chaîne (uri)obligatoire |
L’adresse de ton serveur : accessible publiquement, https sur le port 443, avec un nom d’hôte au lieu d’une adresse IP.Limites: 1 à 2 048 caractères |
https://hooks.example.com/handelsregister |
| headersBody | objetfacultatif | Headers personnalisés envoyés avec chaque message, comme un token pour ton serveur.Limites: au plus 10 entrées |
{"X-Receiver-Token":"TON_TOKEN_RECEPTEUR"} |
Les noms de headers ne doivent pas commencer par webhook- ni remplacer des headers standard comme Content-Type ou Host. Les valeurs sont en écriture seule ; par la suite, l’API n’affiche que les noms.
La réponse contient le secret de signature whsec_…. Nous ne l’affichons que cette seule fois. Enregistre-le immédiatement.
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 nouvel endpoint webhook a le statut pending_verification et ne reçoit des messages qu’après vérification. Le nom, l’URL et les headers ne peuvent pas être modifiés ensuite. Pour une nouvelle adresse, crée un nouvel endpoint webhook.
Dans les réponses, url n’affiche que l’hôte ; nous raccourcissons le chemin en /….
Vérifier
Avec POST …/{id}/verify, nous envoyons un message signé de type endpoint.verification à ton URL. data.challenge contient une valeur aléatoire.
Ton serveur répond dans les 15 secondes avec un statut 2xx et renvoie la valeur dans le header de réponse webhook-verification. L’endpoint webhook devient alors 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: TA_CLE_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": "TA_CLE_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": "TA_CLE_API", "Idempotency-Key": crypto.randomUUID(), }, }); const data = await response.json();
Si cela fonctionne, l’API répond avec "verified": true, sinon avec 422 et "verified": false. Pour une nouvelle tentative, il te faut une nouvelle Idempotency-Key.
Chaque endpoint webhook autorise 6 tentatives par heure.
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}
Ensuite, POST …/{id}/test envoie quand tu veux un message de test signé de type endpoint.test. La réponse montre le résultat de la livraison.
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}
Vérifier la signature
Chaque message est signé selon le standard ouvert Standard Webhooks. Vérifie la signature avant de traiter un message.
| Header | Signification |
|---|---|
| webhook-id |
Identifiant du message (msg_…). Il reste le même lors des nouvelles tentatives. Utilise-le pour repérer les doublons.
|
| webhook-timestamp | Heure de cette tentative de livraison en secondes Unix. |
| webhook-signature |
Une ou plusieurs signatures de la forme v1,<Base64>, séparées par des espaces.
|
Comment vérifier un message :
- Prends la partie du secret après
whsec_et décode-la depuis Base64. C’est la clé. - Construis le texte
webhook-id.webhook-timestamp.Bodyà partir des deux headers et du corps inchangé. - Calcule le HMAC-SHA256 de ce texte avec la clé et encode le résultat en Base64.
- Compare le résultat en temps constant avec chaque signature
v1du header. Si l’une correspond, le message est authentique.
Utilise le corps exactement tel qu’il arrive, pas un JSON re-sérialisé. Rejette les messages dont l’horodatage s’écarte de plus de cinq minutes.
import base64, hashlib, hmac, time def verify_webhook(secret: str, headers: dict, body: bytes) -> bool: msg_id = headers["webhook-id"] timestamp = headers["webhook-timestamp"] # Rejeter les messages de plus de cinq minutes 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() # Pendant une rotation de secret, l’en-tête porte deux signatures 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"]; // Rejeter les messages de plus de cinq minutes 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(); // Pendant une rotation de secret, l’en-tête porte deux signatures 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); }); }
Au lieu d’écrire ton propre code, tu peux utiliser les bibliothèques officielles Standard Webhooks, comme le paquet standardwebhooks pour Python et JavaScript.
Avec POST …/{id}/rotate-secret, tu obtiens un nouveau secret, une seule fois, dans la réponse. Pendant 7 jours, les messages portent deux signatures, une avec le nouveau secret et une avec l’ancien. Tu bascules ainsi sans interruption.
Payload
Chaque message est un objet JSON avec la même enveloppe. Ce qui constitue l’événement figure dans data.
| Champ | Signification |
|---|---|
| id |
Identifiant du message, égal au header webhook-id.
|
| event_id |
Identifiant de l’événement (evt_…).
|
| type | Type de l’événement, voir ci-dessous. |
| timestamp | Moment où nous avons détecté l’événement, en UTC. |
| schema_version |
Version de la structure, actuellement 1.
|
| data | Les données de l’événement. |
| Type d’événement | Signification |
|---|---|
| organization.signal.detected |
Un moniteur a trouvé un nouveau signal. data.signal a la même structure qu’un signal dans signals, plus data.monitor et, sous data.links.signal, le lien vers le signal dans l’API.
|
| organization.signal.sample |
Un exemple tiré de l’historique réel de l’entreprise, envoyé sous Compte › Monitoring. Même structure que ci-dessus, plus data.sample: true.
|
| endpoint.test |
Message de test via /test.
|
| endpoint.verification | Challenge lors de la vérification. |
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 message fait au plus 20 Ko. Si un signal devient plus gros, details et register_entry sont omis et data.truncated vaut true. Tu récupères alors le signal complet via le lien.
Ignore les types d’événements que tu ne connais pas et réponds quand même avec 2xx. Ton serveur reste ainsi stable quand de nouveaux types s’ajoutent.
Livraisons et nouvelles tentatives
Une livraison est réussie quand ton serveur répond dans les 15 secondes avec un statut 2xx. Nous ne suivons pas les redirections.
Sinon, nous réessayons : jusqu’à 10 tentatives, à intervalles croissants sur environ trois jours. Si ton serveur envoie Retry-After, nous le respectons.
Si ton serveur répond 410, nous arrêtons les livraisons et désactivons l’endpoint webhook.
Si 5 livraisons consécutives échouent définitivement, l’endpoint webhook est désactivé avec disabled_failures. Avec POST …/{id}/enable, tu le réactives.
webhook-id. Réponds rapidement et traite le message seulement ensuite.
GET /v1/account/webhook-deliveries montre les 50 dernières livraisons, avec ?endpoint=wep_… pour un endpoint webhook. GET /v1/account/webhook-events montre les 50 derniers événements.
Tu renvoies une livraison définitivement échouée avec POST /v1/account/webhook-deliveries/{id}/retry, avec le même webhook-id et le même corps. C’est possible pendant 30 jours ; c’est la durée de conservation des messages.
| Statut | Signification |
|---|---|
| withheld | Attend que la vérification soit facturée. |
| pending | Attend d’être envoyée. |
| in_flight | En cours d’envoi. |
| retry_wait | Attend la prochaine tentative. |
| succeeded | Livrée. |
| exhausted | Toutes les tentatives ont échoué. |
| blocked_endpoint | L’endpoint webhook ne peut rien recevoir pour le moment. |
| cancelled | Annulée, par exemple parce que le message a expiré. |
Activer, désactiver, archiver
POST …/{id}/disable suspend la livraison. Les moniteurs sans autre endpoint webhook se mettent alors en pause avec paused_configuration.
POST …/{id}/enable réactive un endpoint webhook vérifié. Reprends ensuite les moniteurs en pause avec resume.
DELETE …/{id} archive définitivement l’endpoint webhook. Pour l’arrêter temporairement, désactive-le plutôt.
curl -X POST "https://handelsregister.ai/api/v1/account/webhook-endpoints/wep_01m6b3j1g0cdz98hw8zdefbf52/disable" \ -H "Authorization: Bearer TON_TOKEN_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 TON_TOKEN_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 TON_TOKEN_ADMIN", "Idempotency-Key": crypto.randomUUID(), }, }); const data = await response.json();
Erreurs
Les erreurs ont la même structure que pour account/monitors : un code dans error, l’explication dans meta.message. Les erreurs ne coûtent rien.
| Statut | Cause | Que faire |
|---|---|---|
| 400 |
api_key figure dans l’URL. |
Envoie la clé dans le header x-api-key. |
| 400 |
L’URL n’est pas autorisée comme destination, un header n’est pas permis, le nombre maximal est atteint, ou l’endpoint webhook n’est pas actif. meta.message indique la raison. |
Corrige la valeur ou vérifie d’abord. |
| 400 |
L’Idempotency-Key a un format invalide. |
Corrige le format, par exemple en utilisant un UUID. |
| 401 | Aucune clé API ni aucun token, ou une valeur invalide. |
Envoie la clé dans le header x-api-key. |
| 403 |
Il manque au bearer token le droit account:read. |
Utilise la clé API ou un token avec account:read. |
| 403 | L’adresse e-mail de ton compte n’est pas encore confirmée, ou le compte est bloqué. | Ouvre le lien de l’e-mail de confirmation. Si le compte est bloqué, le support peut t’aider. |
| 403 | Cet appel nécessite le token admin. Une clé API ne suffit pas, pas plus qu’un autre token. |
Envoie-le avec Authorization: Bearer et le token admin. |
| 403 |
Il manque au bearer token le droit monitoring:manage. |
Utilise la clé API ou le token admin. |
| 404 | Aucun endpoint webhook ni aucune livraison avec cet identifiant. | Vérifie l’identifiant. |
| 409 |
L’Idempotency-Key a déjà été utilisée avec d’autres données, date de plus de 24 heures ou est encore en cours de traitement. |
Utilise une nouvelle clé ou attends brièvement. |
| 422 |
Une valeur manque ou est invalide, comme name ou url. errors nomme le champ. |
Corrige la valeur et réessaie avec une nouvelle clé. |
| 422 | La vérification a échoué. | Vérifie le header de challenge et le statut 2xx, puis réessaie avec une nouvelle clé. |
| 428 |
Le header Idempotency-Key manque. |
Envoie le header. |
| 429 | Trop de requêtes. |
Réessaie après Retry-After. |
| 503 | La vérification, le test et le renvoi sont temporairement indisponibles. | Réessaie un peu plus tard. |
1{2 "error": "invalid_request",3 "meta": {4 "message": "The endpoint is not active.",5 "request_credit_cost": 0,6 "credits_remaining": 1457 }8}