Webhooks
Um webhook é a forma como Eventos entrega a um servidor que você opera: todo evento que corresponde a uma assinatura chega ao seu endpoint HTTPS como um POST assinado. Este guia cobre a criação de uma assinatura de webhook, a requisição que o seu endpoint recebe, a verificação da assinatura, como funcionam as novas tentativas e as mensagens mortas, e a rotação do segredo de assinatura sem perder nenhuma entrega.
O que é uma assinatura de webhook
Uma assinatura diz quais eventos um receptor quer e como eles chegam até ele. Uma assinatura de webhook tem uma lista de padrões de tipo e um bloco de entrega com "mode": "webhook" e a sua url. A partir daí, cada evento publicado no espaço de trabalho cujo type corresponda a um dos padrões é enviado por POST a essa URL, uma vez por assinatura.
A entrega é de pelo menos uma vez. Em operação normal cada evento chega uma vez; uma nova tentativa, uma resposta lenta ou uma conexão perdida pode fazê-lo chegar de novo. O seu receptor deve, portanto, ser construído para aceitar uma duplicata sem consequências, usando o id do evento como chave.
A URL deve ser https, na porta 443, sem nome de usuário ou senha, sem fragmento e com um host público: nomes privados, de loopback, de link local e de uso local são recusados quando a assinatura é criada e verificados de novo a cada entrega. Redirecionamentos nunca são seguidos.
Crie uma
Chame POST https://events.inovacc.dev/v1/workspaces/{workspace}/subscriptions com uma chave secreta que tenha a permissão de escrita de assinaturas:
Linguagem
⋮
JSON
{"types": ["order.*"], "delivery": {"mode": "webhook", "url": "https://hooks.example.com/inovacc"}}{"types": ["order.*"], "delivery": {"mode": "webhook", "url": "https://hooks.example.com/inovacc"}}
A resposta é 201:
Linguagem
⋮
JSON
{"subscription_id": "sub_...", "types": ["order.*"],
"delivery": {"mode": "webhook", "url": "https://hooks.example.com/inovacc", "secret_ref": "managed"},
"created_at": "2026-10-06T12:00:00.000Z",
"signing_secret": "whsec_..."}{"subscription_id": "sub_...", "types": ["order.*"],
"delivery": {"mode": "webhook", "url": "https://hooks.example.com/inovacc", "secret_ref": "managed"},
"created_at": "2026-10-06T12:00:00.000Z",
"signing_secret": "whsec_..."}
signing_secret é a chave que o seu endpoint usa para verificar as entregas. Ele é mostrado uma única vez, nesta resposta, e nunca mais: as listagens mostram "secret_ref": "managed" e nada mais. Guarde-o imediatamente no seu gerenciador de segredos. Se for perdido, rotacione-o (abaixo). Um espaço de trabalho comporta no máximo 50 assinaturas, cada uma com 1 a 20 padrões.
A requisição de entrega
Cada entrega é:
POST https://hooks.example.com/inovacc
content-type: application/json
x-event-id: evt_01K...
x-event-type: order.paid
x-event-timestamp: 1791201600
x-event-signature: v1=<64 lowercase hex characters>
{"event_id":"evt_01K...","type":"order.paid", ... }
x-event-timestamp é o horário Unix, em segundos, em que a entrega foi assinada. Durante uma rotação de segredo, x-event-signature traz uma entrada por segredo ativo, a mais nova primeiro, separadas por vírgulas: v1=<new>,v1=<old>.
Responda com qualquer status 2xx para confirmar a entrega. Qualquer outra coisa (um 4xx, um 5xx, nenhuma resposta em 10 segundos, um erro de rede) é uma tentativa que falhou e será repetida.
O envelope do evento
O corpo é o envelope do evento, exatamente como os assinantes de todos os modos o recebem:
| Campo | Significado |
|---|---|
event_id | evt_ seguido de 26 caracteres, atribuído pela Inovacc quando o evento foi publicado. Único por evento. |
type | O tipo do evento, como order.paid: palavras minúsculas separadas por pontos, até 128 bytes. |
source | A aplicação que publicou o evento, obtida da sua credencial. |
time | Quando foi publicado, RFC 3339 UTC com milissegundos. |
tenant | A organização, a conta, o espaço de trabalho e, quando conhecida, a aplicação a que o evento pertence. |
idempotency_key | A chave do publicador; quando ele não enviou nenhuma, o event_id. |
data | A carga JSON do publicador: o que aconteceu, normalmente ids em vez de objetos inteiros. |
Leia apenas os campos de que precisa e ignore os campos que você não conhece.
Verifique a assinatura
Verifique cada entrega antes de interpretá-la ou agir sobre ela:
- Leia
x-event-timestampe os bytes brutos do corpo, exatamente como recebidos, antes de qualquer interpretação de JSON. - Calcule
HMAC-SHA256(secret, timestamp + "." + body), em quesecreté a stringwhsec_...inteira em UTF-8, e escreva o resultado em hexadecimal minúsculo. - Divida
x-event-signaturepelas vírgulas e aceite a entrega quando qualquer entradav1=for igual ao seu valor, comparando em tempo constante. - Rejeite um carimbo de data e hora a mais de cinco minutos do seu relógio, para que uma entrega antiga não possa ser reenviada contra você.
Exemplo prático. Com o segredo example-key-0001, o carimbo 1791201600 e o corpo {"hello":"world"}, o texto assinado é 1791201600.{"hello":"world"} e a assinatura são os 64 caracteres hexadecimais abaixo, impressos aqui em duas metades que você junta sem espaço:
05080ab29a6abfb44033966c7837c2a1
889fbf1a909f9cbd12b9dd5d12bb2bf5
de modo que o cabeçalho fica v1= seguido das duas metades. Execute o seu verificador neste exemplo antes de implantá-lo.
TypeScript (Node.js 18 ou posterior):
Linguagem
⋮
TypeScript
import { createHmac, timingSafeEqual } from "node:crypto";
export function verifyInovaccWebhook(
secret: string,
timestamp: string,
signatureHeader: string,
rawBody: Buffer,
nowSeconds: number = Math.floor(Date.now() / 1000),
): boolean {
const ts = Number(timestamp);
if (!Number.isInteger(ts) || Math.abs(nowSeconds - ts) > 300) return false;
const expected = createHmac("sha256", secret)
.update(`${timestamp}.`)
.update(rawBody)
.digest();
return signatureHeader.split(",").some((entry) => {
const part = entry.trim();
if (!part.startsWith("v1=")) return false;
const given = Buffer.from(part.slice(3), "hex");
return given.length === expected.length && timingSafeEqual(given, expected);
});
}import { createHmac, timingSafeEqual } from "node:crypto";
export function verifyInovaccWebhook(
secret: string,
timestamp: string,
signatureHeader: string,
rawBody: Buffer,
nowSeconds: number = Math.floor(Date.now() / 1000),
): boolean {
const ts = Number(timestamp);
if (!Number.isInteger(ts) || Math.abs(nowSeconds - ts) > 300) return false;
const expected = createHmac("sha256", secret)
.update(`${timestamp}.`)
.update(rawBody)
.digest();
return signatureHeader.split(",").some((entry) => {
const part = entry.trim();
if (!part.startsWith("v1=")) return false;
const given = Buffer.from(part.slice(3), "hex");
return given.length === expected.length && timingSafeEqual(given, expected);
});
}
Python (3.10 ou posterior):
Linguagem
⋮
Python
import hashlib
import hmac
import time
def verify_inovacc_webhook(secret: str, timestamp: str, signature_header: str,
raw_body: bytes, now: int | None = None) -> bool:
try:
ts = int(timestamp)
except ValueError:
return False
now = int(time.time()) if now is None else now
if abs(now - ts) > 300:
return False
expected = hmac.new(secret.encode(), timestamp.encode() + b"." + raw_body,
hashlib.sha256).hexdigest()
return any(
part.strip().startswith("v1=")
and hmac.compare_digest(part.strip()[3:], expected)
for part in signature_header.split(",")
)import hashlib
import hmac
import time
def verify_inovacc_webhook(secret: str, timestamp: str, signature_header: str,
raw_body: bytes, now: int | None = None) -> bool:
try:
ts = int(timestamp)
except ValueError:
return False
now = int(time.time()) if now is None else now
if abs(now - ts) > 300:
return False
expected = hmac.new(secret.encode(), timestamp.encode() + b"." + raw_body,
hashlib.sha256).hexdigest()
return any(
part.strip().startswith("v1=")
and hmac.compare_digest(part.strip()[3:], expected)
for part in signature_header.split(",")
)
Ambas as funções recebem o corpo bruto: configure o seu framework web para entregar os bytes, e não um objeto interpretado, nesta rota.
Novas tentativas e tempos limite
O seu endpoint tem 10 segundos para responder. Uma tentativa que falhou é repetida após 10 segundos, e o atraso dobra a cada nova falha até 10 minutos. Quando o limite de 5 tentativas é atingido, o evento vai para as mensagens mortas do seu espaço de trabalho. Uma falha afeta apenas a entrega que falhou: os outros eventos, e as outras assinaturas do mesmo evento, continuam fluindo.
Como uma entrega pode ser repetida, um 2xx que chega depois de o seu endpoint já ter processado o evento é inofensivo, desde que você deduplique por x-event-id.
Rotacione o segredo
POST /v1/workspaces/{workspace}/subscriptions/{id}/rotate-secret devolve um novo segredo, uma única vez:
Linguagem
⋮
JSON
{"subscription_id": "sub_...", "signing_secret": "whsec_...", "previous_valid_until": "2026-10-07T12:00:00.000Z"}{"subscription_id": "sub_...", "signing_secret": "whsec_...", "previous_valid_until": "2026-10-07T12:00:00.000Z"}
Até previous_valid_until (24 horas por padrão), ambos os segredos assinam: cada entrega traz v1=<new>,v1=<old>. Implante o novo segredo no seu receptor dentro dessa janela; como o verificador aceita qualquer entrada correspondente, nada é perdido durante a troca. Rotacionar de novo dentro da janela mantém todos os segredos não expirados. A rotação se aplica apenas a assinaturas de webhook (409 wrong_delivery_mode caso contrário).
Mensagens mortas e reenvio
GET /v1/workspaces/{workspace}/dead?limit=50 lista as mensagens mortas, da mais nova para a mais antiga, com o evento, attempts, dead_at, replay_count e last_error:
last_error | Significado |
|---|---|
webhook_status_<code> | O seu endpoint respondeu esse status, por exemplo webhook_status_500. |
webhook_timeout | Nenhuma resposta em 10 segundos. |
webhook_network | A conexão falhou. |
url_destination_blocked | A URL agora aponta para um lugar aonde as entregas não podem ir. |
secret_missing | O segredo de assinatura da assinatura não pôde ser usado. |
Corrija a causa e depois faça POST /v1/workspaces/{workspace}/dead/{event_id}/replay. A resposta é 202, e o mesmo envelope, com o mesmo event_id, é entregue de novo às assinaturas que nunca o receberam. As mensagens mortas são mantidas por 30 dias.
Boas práticas
- Verifique antes de interpretar: calcule a assinatura sobre os bytes brutos e só depois interprete.
- Torne o receptor idempotente: guarde cada
x-event-idprocessado e responda2xximediatamente para um que você já viu. - Responda rápido: confirme com um
2xxe faça o trabalho lento depois, a partir de uma tarefa sua. - Rejeite carimbos de data e hora obsoletos (a mais de cinco minutos de diferença) e mantenha o relógio do seu servidor sincronizado.
- Mantenha o segredo em um gerenciador de segredos e rotacione-o quando alguém que o conhecia sair, ou em um calendário.
- Monitore as mensagens mortas e
dlq_inflowemGET /stats; reenvie quando o endpoint estiver saudável. - Devolva
2xxapenas quando tiver o evento em segurança: um2xxencerra as novas tentativas.
Veja também
- Eventos: publicação, assinaturas, pull e transmissão
- Erros, Autenticação, Limites