/v1/account/webhook-endpoints
account/webhook-endpoints
Webhooks: para onde o Monitoramento envia novos Signals e como você os verifica.
Um endpoint de webhook é o endereço HTTPS do seu servidor para o qual enviamos os novos sinais dos seus monitores. Toda mensagem é assinada, para você poder verificar que vem de nós.
Custos e limites
| O quê | Créditos |
|---|---|
| Gerenciar endpoints e entregas de webhook | grátis |
Limite: 30 requisições por minuto. Criar, alterar e excluir: 10 por minuto. Verificar, testar e novo secret: 6 por minuto.
Cada conta pode ter até 10 endpoints de webhook; os arquivados não contam. Cada endpoint de webhook pode levar até 10 headers personalizados.
Admin token ou chave de API
Você precisa do admin token para criar um secret de assinatura e para ativar, desativar ou arquivar endpoints de webhook. Você o cria em Conta › Chaves de API, na seção “Para desenvolvedores: gerencie chaves pela API”. Ele vale por um ano, e um novo substitui o antigo.
Envie o admin token como Authorization: Bearer SEU_ADMIN_TOKEN. Todas as outras chamadas também funcionam com a chave de API.
Como em account/monitors, todas as chamadas de escrita precisam do header Idempotency-Key.
Endpoints
| Método | Caminho | Finalidade | Acesso |
|---|---|---|---|
| GET |
/v1/account/webhook-endpoints |
Todos os endpoints de webhook, sem os arquivados. | Chave de API ou admin token |
| POST |
/v1/account/webhook-endpoints |
Criar um endpoint de webhook. | Admin token |
| POST |
/v1/account/webhook-endpoints/{id}/verify |
Verificar. | Chave de API ou admin token |
| POST |
/v1/account/webhook-endpoints/{id}/test |
Enviar uma mensagem de teste. | Chave de API ou admin token |
| POST |
/v1/account/webhook-endpoints/{id}/rotate-secret |
Novo secret de assinatura. | Admin token |
| POST |
/v1/account/webhook-endpoints/{id}/enable |
Ativar. | Admin token |
| POST |
/v1/account/webhook-endpoints/{id}/disable |
Desativar. | Admin token |
| DELETE |
/v1/account/webhook-endpoints/{id} |
Arquivar. | Admin token |
| GET |
/v1/account/webhook-deliveries |
As últimas 50 entregas. | Chave de API ou admin token |
| POST |
/v1/account/webhook-deliveries/{id}/retry |
Reenviar uma entrega que falhou. | Chave de API ou admin token |
| GET |
/v1/account/webhook-events |
Os últimos 50 eventos. | Chave de API ou admin token |
Criar um endpoint de webhook
curl -X POST "https://handelsregister.ai/api/v1/account/webhook-endpoints" \ -H "Authorization: Bearer SEU_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": "SEU_TOKEN_RECEPTOR" } }'
import uuid import requests response = requests.post( "https://handelsregister.ai/api/v1/account/webhook-endpoints", headers={ "Authorization": "Bearer SEU_ADMIN_TOKEN", "Idempotency-Key": str(uuid.uuid4()), }, json={ "name": "CRM-Sync", "url": "https://hooks.example.com/handelsregister", "headers": { "X-Receiver-Token": "SEU_TOKEN_RECEPTOR" } }, ) data = response.json()
const response = await fetch("https://handelsregister.ai/api/v1/account/webhook-endpoints", { method: "POST", headers: { "Authorization": "Bearer SEU_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": "SEU_TOKEN_RECEPTOR" } }), }); const data = await response.json();
| Nome | Tipo | Descrição | Exemplo |
|---|---|---|---|
| Idempotency-KeyHeader | stringobrigatório | Um valor único por operação, de preferência um UUID. Veja Idempotency-Key.Limites: 1 a 128 caracteres |
req-20260802-01 |
| nameBody | stringobrigatório | Um nome para a sua própria referência.Limites: 1 a 120 caracteres |
CRM-Sync |
| urlBody | string (uri)obrigatório |
O endereço do seu servidor: acessível publicamente, https na porta 443, com nome de host em vez de endereço IP.Limites: 1 a 2.048 caracteres |
https://hooks.example.com/handelsregister |
| headersBody | objetoopcional | Headers personalizados enviados com cada mensagem, como um token para o seu servidor.Limites: no máximo 10 entradas |
{"X-Receiver-Token":"SEU_TOKEN_RECEPTOR"} |
Nomes de headers não podem começar com webhook- nem sobrescrever headers padrão como Content-Type ou Host. Os valores são só de escrita; depois, a API mostra apenas os nomes.
A resposta contém o secret de assinatura whsec_…. Nós o mostramos só esta vez. Guarde-o na hora.
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}
Um novo endpoint de webhook tem o status pending_verification e só recebe mensagens depois de verificado. Nome, URL e headers não podem ser alterados depois. Para um novo endereço, crie um novo endpoint de webhook.
Nas respostas, url mostra só o host; encurtamos o caminho para /….
Verificar
Com POST …/{id}/verify, enviamos uma mensagem assinada do tipo endpoint.verification para a sua URL. data.challenge contém um valor aleatório.
O seu servidor responde em até 15 segundos com um status 2xx e devolve o valor no header de resposta webhook-verification. O endpoint de webhook então fica 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: SUA_CHAVE_DE_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": "SUA_CHAVE_DE_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": "SUA_CHAVE_DE_API", "Idempotency-Key": crypto.randomUUID(), }, }); const data = await response.json();
Se funcionar, a API responde com "verified": true; caso contrário, com 422 e "verified": false. Para uma nova tentativa, você precisa de um novo Idempotency-Key.
Cada endpoint de webhook permite 6 tentativas por hora.
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}
Depois disso, POST …/{id}/test envia uma mensagem de teste assinada do tipo endpoint.test quando você quiser. A resposta mostra o resultado da entrega.
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}
Verificar a assinatura
Toda mensagem é assinada segundo o padrão aberto Standard Webhooks. Verifique a assinatura antes de processar uma mensagem.
| Header | Significado |
|---|---|
| webhook-id |
ID da mensagem (msg_…). Ele permanece igual nas novas tentativas. Use-o para detectar duplicatas.
|
| webhook-timestamp | Momento desta tentativa de entrega em segundos Unix. |
| webhook-signature |
Uma ou mais assinaturas no formato v1,<Base64>, separadas por espaços.
|
Como verificar uma mensagem:
- Pegue a parte do secret depois de
whsec_e decodifique-a de Base64. Essa é a chave. - Monte o texto
webhook-id.webhook-timestamp.Bodya partir dos dois headers e do body inalterado. - Calcule o HMAC-SHA256 dele com a chave e codifique o resultado em Base64.
- Compare o resultado em tempo constante com cada assinatura
v1do header. Se uma corresponder, a mensagem é autêntica.
Use o body exatamente como chega, não JSON serializado de novo. Rejeite mensagens cujo timestamp divirja em mais de cinco minutos.
import base64, hashlib, hmac, time def verify_webhook(secret: str, headers: dict, body: bytes) -> bool: msg_id = headers["webhook-id"] timestamp = headers["webhook-timestamp"] # Rejeitar mensagens com mais de cinco minutos 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 uma rotação de secret, o header traz duas assinaturas 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"]; // Rejeitar mensagens com mais de cinco minutos 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 uma rotação de secret, o header traz duas assinaturas 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); }); }
Em vez de escrever seu próprio código, você pode usar as bibliotecas oficiais do Standard Webhooks, como o pacote standardwebhooks para Python e JavaScript.
Com POST …/{id}/rotate-secret, você recebe um novo secret, uma única vez, na resposta. Por 7 dias, as mensagens trazem duas assinaturas, uma com o secret novo e outra com o antigo. Assim você faz a troca sem interrupção.
Payload
Toda mensagem é um objeto JSON com o mesmo envelope. O que constitui o evento está em data.
| Campo | Significado |
|---|---|
| id |
ID da mensagem, igual ao header webhook-id.
|
| event_id |
ID do evento (evt_…).
|
| type | Tipo do evento, veja abaixo. |
| timestamp | Quando detectamos o evento, em UTC. |
| schema_version |
Versão da estrutura, atualmente 1.
|
| data | Os dados do evento. |
| Tipo de evento | Significado |
|---|---|
| organization.signal.detected |
Um monitor encontrou um novo sinal. data.signal tem a mesma estrutura de um sinal em signals, mais data.monitor e, em data.links.signal, o link para o sinal na API.
|
| organization.signal.sample |
Um exemplo do histórico real da empresa, enviado em Conta › Monitoramento. Mesma estrutura de acima, mais data.sample: true.
|
| endpoint.test |
Mensagem de teste via /test.
|
| endpoint.verification | Desafio na verificação. |
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}
Uma mensagem tem no máximo 20 KB. Se um sinal ficar maior, details e register_entry são omitidos e data.truncated é true. Você então obtém o sinal inteiro pelo link.
Ignore tipos de evento que você não conhece e responda mesmo assim com 2xx. Assim o seu servidor continua estável quando novos tipos forem acrescentados.
Entregas e novas tentativas
Uma entrega conta como bem-sucedida quando o seu servidor responde em até 15 segundos com um status 2xx. Não seguimos redirecionamentos.
Caso contrário, tentamos de novo: até 10 tentativas, com intervalos crescentes ao longo de cerca de três dias. Se o seu servidor enviar Retry-After, nós o seguimos.
Se o seu servidor responder com 410, paramos de entregar e desativamos o endpoint de webhook.
Se 5 entregas seguidas falharem definitivamente, o endpoint de webhook é desativado com disabled_failures. Com POST …/{id}/enable você o reativa.
webhook-id. Responda rápido e só depois processe a mensagem.
GET /v1/account/webhook-deliveries mostra as últimas 50 entregas, com ?endpoint=wep_… para um endpoint de webhook. GET /v1/account/webhook-events mostra os últimos 50 eventos.
Você reenvia uma entrega que falhou definitivamente com POST /v1/account/webhook-deliveries/{id}/retry, com o mesmo webhook-id e o mesmo body. Isso funciona por 30 dias; é o tempo que guardamos as mensagens.
| Status | Significado |
|---|---|
| withheld | Aguarda até a verificação ser cobrada. |
| pending | Aguarda o envio. |
| in_flight | Sendo enviada agora. |
| retry_wait | Aguarda a próxima tentativa. |
| succeeded | Entregue. |
| exhausted | Todas as tentativas falharam. |
| blocked_endpoint | O endpoint de webhook não pode receber nada no momento. |
| cancelled | Cancelada, por exemplo porque a mensagem expirou. |
Ativar, desativar, arquivar
POST …/{id}/disable interrompe a entrega por enquanto. Monitores sem outro endpoint de webhook pausam então com paused_configuration.
POST …/{id}/enable reativa um endpoint de webhook verificado. Depois, retome os monitores pausados com resume.
DELETE …/{id} arquiva o endpoint de webhook definitivamente. Para pará-lo por um tempo, desative-o.
curl -X POST "https://handelsregister.ai/api/v1/account/webhook-endpoints/wep_01m6b3j1g0cdz98hw8zdefbf52/disable" \ -H "Authorization: Bearer SEU_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 SEU_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 SEU_ADMIN_TOKEN", "Idempotency-Key": crypto.randomUUID(), }, }); const data = await response.json();
Erros
Erros têm a mesma estrutura de account/monitors: um código em error, a explicação em meta.message. Erros não custam nada.
| Status | Causa | O que você pode fazer |
|---|---|---|
| 400 |
api_key está na URL. |
Envie a chave no header x-api-key. |
| 400 |
A URL não é permitida como destino, um header não é permitido, o número máximo foi atingido ou o endpoint de webhook não está ativo. meta.message informa o motivo. |
Corrija o valor ou verifique primeiro. |
| 400 |
O Idempotency-Key tem formato inválido. |
Corrija o formato, por exemplo use um UUID. |
| 401 | Nenhuma chave de API ou token, ou um inválido. |
Envie a chave no header x-api-key. |
| 403 |
Falta ao bearer token a permissão account:read. |
Use a chave de API ou um token com account:read. |
| 403 | O endereço de e-mail da sua conta ainda não foi confirmado, ou a conta está bloqueada. | Abra o link do e-mail de confirmação. Se a conta estiver bloqueada, o suporte pode ajudar. |
| 403 | Esta chamada precisa do admin token. Uma chave de API não basta, nem outro token. |
Envie-o com Authorization: Bearer e o admin token. |
| 403 |
Falta ao bearer token a permissão monitoring:manage. |
Use a chave de API ou o admin token. |
| 404 | Nenhum endpoint de webhook ou entrega com este ID. | Confira o ID. |
| 409 |
O Idempotency-Key já foi usado com outros dados, tem mais de 24 horas ou ainda está sendo processado. |
Use uma chave nova ou espere um pouco. |
| 422 |
Um valor está faltando ou é inválido, como name ou url. errors indica o campo. |
Corrija o valor e tente de novo com uma chave nova. |
| 422 | A verificação falhou. | Confira o header do desafio e o status 2xx e tente de novo com uma chave nova. |
| 428 |
O header Idempotency-Key está faltando. |
Envie o header. |
| 429 | Requisições demais. |
Tente de novo depois de Retry-After. |
| 503 | Verificar, testar e reenviar estão temporariamente indisponíveis. | Tente de novo um pouco mais tarde. |
1{2 "error": "invalid_request",3 "meta": {4 "message": "The endpoint is not active.",5 "request_credit_cost": 0,6 "credits_remaining": 1457 }8}