Pular para o conteúdo principal

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 types que deseja receber. Se vazia, recebe todos.
  • Status — active ou inactive.
  • 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:

TentativaAtraso
1imediato
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:

HeaderValor
X-Kobana-Appdashboards — o produto Kobana que enviou a entrega.
X-Kobana-Environmentdevelopment, sandbox ou production. Nunca deixe uma entrega sandbox tocar dados de produção.
X-Kobana-Webhook-IdId do endpoint cadastrado a que a entrega pertence.
X-Kobana-EventSlug do evento (ex.: api_key.status_changed).
X-Kobana-Event-IdId do evento. Retries do mesmo evento repetem este valor — use-o para deduplicar.
X-Kobana-Delivery-IdId único desta tentativa de entrega. Faz parte do conteúdo assinado.
X-Kobana-TimestampUnix time (segundos) em que a entrega foi assinada. Faz parte do conteúdo assinado.
X-Kobana-Signaturev1,<base64 do HMAC> — um ou mais tokens separados por espaço.
webhook-id, webhook-timestamp, webhook-signatureOs 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_id ao 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​

  1. Leia webhook-id, webhook-timestamp e webhook-signature (ou os equivalentes X-Kobana-*). Rejeite a requisição se algum faltar.
  2. Rejeite se |agora − timestamp| > 300 segundos: cinco minutos absorvem diferença de relógio e limitam replays.
  3. Monte o conteúdo assinado com o body cru e calcule o HMAC esperado para cada secret que você confia.
  4. Compare com cada token v1, em tempo constante — nunca com ==, que devolve no primeiro byte diferente e vaza o digest por timing.
  5. Deduplique por X-Kobana-Event-Id para processar cada evento uma vez, ou por webhook-id para registrar cada tentativa.
  6. Responda 2xx rá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 */ }
}
CampoDescrição
idID único do evento.
typeTipo do evento (ex.: api_key.status_changed).
created_atTimestamp ISO 8601 (UTC) em que o evento foi gerado.
environmentproduction ou sandbox.
versionVersão do schema do payload (string YYYY-MM-DD).
dataO recurso completo no estado atual.
changesPara eventos *.updated e *.status_changed, mapa campo: {from, to}. Pode estar ausente em eventos de criação/deleção.

Eventos disponíveis​

RecursoEventos
Token de APIapi_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-Id para evitar processar duplicatas em caso de retry.
  • Resposta rápida: responda 2xx em até 30s. Coloque o trabalho pesado em fila assíncrona.
  • Versionamento: o campo version indica 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 *.