Webhooks
Webhooks são notificações HTTP enviadas pela API do Dashboard Mágico sempre que um evento relevante ocorre na sua Organização — token de API criado, desativado, rotacionado ou revogado etc.
Em vez de fazer polling, você cadastra um endpoint HTTP público e recebe um POST com o payload do evento assim que ele acontece.
Cadastro
Gerencie endpoints em Dashboard → Configurações → API → Webhooks. Cada endpoint tem:
- URL — destino HTTPS público do seu servidor.
- Eventos assinados — lista dos
event typesque deseja receber. Se vazia, recebe todos. - Status —
activeouinactive. - Secret — string secreta usada para assinar cada entrega (HMAC-SHA256). Gerada automaticamente; pode ser rotacionada.
- Auth headers — opcionalmente, cabeçalhos extras (Basic, Bearer, custom) injetados em cada requisição.
Entrega
Cada evento dispara uma requisição POST para a URL cadastrada:
POST /seu-endpoint HTTP/1.1
Content-Type: application/json
User-Agent: Kobana-Webhooks/1.0
X-Kobana-App: dashboards
X-Kobana-Environment: production
X-Kobana-Webhook-Id: 7c1e9d2a-3b4f-4c5d-8e6f-0a1b2c3d4e5f
X-Kobana-Event: api_key.status_changed
X-Kobana-Event-Id: 3d1f0c9a-5b2e-4c7d-9e1f-8a6b4c2d0e9f
X-Kobana-Delivery-Id: 9b2e4a10-7c3f-4d8e-a1b2-c3d4e5f60718
X-Kobana-Timestamp: 1753272000
X-Kobana-Signature: v1,MEUCIQD3…
webhook-id: 9b2e4a10-7c3f-4d8e-a1b2-c3d4e5f60718
webhook-timestamp: 1753272000
webhook-signature: v1,MEUCIQD3…
{...payload JSON, em snake_case...}
Os cabeçalhos estão descritos em Headers e assinatura, abaixo.
Sucesso
Qualquer resposta 2xx em até 30 segundos marca a entrega como bem-sucedida. Qualquer outra coisa (timeout, 4xx, 5xx) é tratada como falha.
Retry
Falhas são reentregues com backoff exponencial:
| Tentativa | Atraso |
|---|---|
| 1 | imediato |
| 2 | +1 min |
| 3 | +5 min |
| 4 | +30 min |
| 5 | +2 h |
| 6 | +6 h |
| 7 | +24 h |
Após 7 tentativas, a entrega é marcada como failed permanentemente. Eventos órfãos ficam disponíveis no dashboard para reenvio manual.
Headers e assinatura (padrão Kobana)
Toda entrega é um POST com Content-Type: application/json e os headers abaixo — os mesmos em todos os produtos Kobana, então um único receptor atende vários produtos e ambientes e roteia com segurança:
| Header | Valor |
|---|---|
X-Kobana-App | dashboards — o produto Kobana que enviou a entrega. |
X-Kobana-Environment | development, sandbox ou production. Nunca deixe uma entrega sandbox tocar dados de produção. |
X-Kobana-Webhook-Id | Id do endpoint cadastrado a que a entrega pertence. |
X-Kobana-Event | Slug do evento (ex.: api_key.status_changed). |
X-Kobana-Event-Id | Id do evento. Retries do mesmo evento repetem este valor — use-o para deduplicar. |
X-Kobana-Delivery-Id | Id único desta tentativa de entrega. Faz parte do conteúdo assinado. |
X-Kobana-Timestamp | Unix time (segundos) em que a entrega foi assinada. Faz parte do conteúdo assinado. |
X-Kobana-Signature | v1,<base64 do HMAC> — um ou mais tokens separados por espaço. |
webhook-id, webhook-timestamp, webhook-signature | Os mesmos três valores acima, no layout Standard Webhooks — verificáveis com uma biblioteca de prateleira. |
Os headers legados X-Signature (t=<unix>,v1=<hex> sobre "{t}.{body}"), X-Delivery-Id, X-Event-Id, X-Event-Type, X-Timestamp, X-App e X-Environment continuam sendo enviados, deprecados, para integrações existentes. Migre para os X-Kobana-*: eles serão removidos numa próxima versão do contrato.
Como a assinatura é calculada
chave = base64_decode(secret sem o prefixo "whsec_")
conteúdo = delivery_id + "." + timestamp + "." + body_cru
assinatura = "v1," + base64(HMAC_SHA256(chave, conteúdo))
body_crué o corpo da requisição exatamente como recebido, byte a byte. Não parseie e re-serialize: ordem de chaves, espaços ou escapes mudariam e a assinatura deixaria de bater.- Amarrar o
delivery_idao conteúdo assinado impede que uma requisição capturada seja reapresentada com outro id para furar a sua deduplicação; o timestamp limita o replay no tempo. - O secret tem a forma
whsec_+ base64 de 32 bytes aleatórios e é exibido uma única vez. Secrets antigos (sem o prefixo, ou com hexadecimal após ele) continuam válidos: a chave é o base64 decodificado após o prefixo, ou os bytes UTF-8 do secret quando não há prefixo. - Durante uma rotação de secret o header carrega um token por secret ainda válido (
v1,AAA… v1,BBB…). A entrega é válida se qualquer token casar com qualquer secret que você tenha.
Verificando uma entrega
- Leia
webhook-id,webhook-timestampewebhook-signature(ou os equivalentesX-Kobana-*). Rejeite a requisição se algum faltar. - Rejeite se
|agora − timestamp| > 300segundos: cinco minutos absorvem diferença de relógio e limitam replays. - Monte o conteúdo assinado com o body cru e calcule o HMAC esperado para cada secret que você confia.
- Compare com cada token
v1,em tempo constante — nunca com==, que devolve no primeiro byte diferente e vaza o digest por timing. - Deduplique por
X-Kobana-Event-Idpara processar cada evento uma vez, ou porwebhook-idpara registrar cada tentativa. - Responda
2xxrápido e faça o trabalho pesado de forma assíncrona. Qualquer outra resposta entra no ciclo de retry.
const crypto = require('node:crypto');
/**
* @param headers headers da requisição, com nomes em minúsculas
* @param rawBody body cru (string ou Buffer), ANTES de qualquer JSON.parse
* @param secrets secret atual (e o anterior, durante uma rotação)
*/
function verifyKobanaWebhook(headers, rawBody, secrets) {
const id = headers['webhook-id'];
const timestamp = headers['webhook-timestamp'];
const signature = headers['webhook-signature'];
if (!id || !timestamp || !signature) throw new Error('missing signature headers');
if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) throw new Error('timestamp out of tolerance');
const content = `${id}.${timestamp}.${rawBody}`;
const received = signature.split(' ').filter((t) => t.startsWith('v1,')).map((t) => Buffer.from(t.slice(3), 'base64'));
for (const secret of [].concat(secrets)) {
const key = secret.startsWith('whsec_') ? Buffer.from(secret.slice(6), 'base64') : Buffer.from(secret, 'utf8');
const expected = crypto.createHmac('sha256', key).update(content).digest();
if (received.some((r) => r.length === expected.length && crypto.timingSafeEqual(r, expected))) return true;
}
throw new Error('no matching signature');
}
import base64, hashlib, hmac, time
def verify_kobana_webhook(headers, raw_body: bytes, secrets) -> bool:
msg_id = headers.get("webhook-id")
timestamp = headers.get("webhook-timestamp")
signature = headers.get("webhook-signature")
if not (msg_id and timestamp and signature):
raise ValueError("missing signature headers")
if abs(time.time() - int(timestamp)) > 300:
raise ValueError("timestamp out of tolerance")
content = f"{msg_id}.{timestamp}.".encode() + raw_body
received = [base64.b64decode(t[3:]) for t in signature.split(" ") if t.startswith("v1,")]
for secret in ([secrets] if isinstance(secrets, str) else secrets):
key = base64.b64decode(secret[6:]) if secret.startswith("whsec_") else secret.encode()
expected = hmac.new(key, content, hashlib.sha256).digest()
if any(hmac.compare_digest(r, expected) for r in received):
return True
raise ValueError("no matching signature")
Com Express, mantenha o body cru montando express.raw({ type: 'application/json' }) na rota do webhook (não express.json()), verifique e só então faça JSON.parse. Com Django/Flask, use request.body/request.get_data() — nunca o objeto já parseado.
Estrutura do payload
Todos os eventos compartilham o mesmo envelope:
{
"id": "evt_01HXYZABC123",
"type": "api_key.status_changed",
"created_at": "2026-05-20T12:00:00.000Z",
"environment": "production",
"version": "2026-03-01",
"data": { /* recurso completo */ },
"changes": { /* opcional, mudanças desde o estado anterior */ }
}
| Campo | Descrição |
|---|---|
id | ID único do evento. |
type | Tipo do evento (ex.: api_key.status_changed). |
created_at | Timestamp ISO 8601 (UTC) em que o evento foi gerado. |
environment | production ou sandbox. |
version | Versão do schema do payload (string YYYY-MM-DD). |
data | O recurso completo no estado atual. |
changes | Para eventos *.updated e *.status_changed, mapa campo: {from, to}. Pode estar ausente em eventos de criação/deleção. |
Eventos disponíveis
| Recurso | Eventos |
|---|---|
| Token de API | api_key.db.created, api_key.db.updated, api_key.db.deleted, api_key.status_changed, api_key.rotated, api_key.revoked |
Boas práticas
- Idempotência: use
X-Kobana-Event-Idpara evitar processar duplicatas em caso de retry. - Resposta rápida: responda
2xxem até 30s. Coloque o trabalho pesado em fila assíncrona. - Versionamento: o campo
versionindica o schema. Trate campos novos como aditivos. - Validação obrigatória: sempre valide a assinatura antes de confiar no payload.
- HTTPS: use TLS no endpoint. Endpoints HTTP são rejeitados em produção.
- Lista de eventos: prefira assinar apenas os eventos que você processa, não
*.