Docs
Acessar painel

Webhooks

Verificar assinatura

Como confirmar que um webhook veio da Koku e não foi alterado, com exemplos em Node.js, Python e PHP.

Toda entrega traz o cabeçalho x-koku-signature:

x-koku-signature: t=<unix>,v1=<hex>
  • t é o momento da assinatura em segundos (Unix).
  • v1 é o HMAC-SHA256, em hexadecimal, calculado com o segredo do seu endpoint sobre o texto <t>.<corpo>: o valor de t, um ponto e o corpo exatamente como chegou.

Como verificar

  1. Leia o corpo bruto da requisição, antes de qualquer conversão de JSON. Reformatar o JSON (espaços, ordem das chaves) muda a assinatura.
  2. Extraia t e v1 do cabeçalho.
  3. Recuse se t estiver a mais de 5 minutos do seu relógio (proteção contra reenvio por terceiros).
  4. Calcule HMAC-SHA256(segredo, t + "." + corpo) em hexadecimal.
  5. Compare com v1 usando comparação em tempo constante. Diferente: responda 400 e não processe.
Verificar a assinatura
import { createHmac, timingSafeEqual } from 'node:crypto';

/**
 * Confere a assinatura de um webhook da Koku.
 * rawBody: o corpo EXATO recebido (string), antes de qualquer JSON.parse.
 * signatureHeader: valor do cabeçalho x-koku-signature ("t=<unix>,v1=<hex>").
 */
export function verifyKokuSignature({ rawBody, signatureHeader, secret, toleranceSeconds = 300, nowSeconds = Math.floor(Date.now() / 1000) }) {
  const header = String(signatureHeader ?? '');
  const t = /(?:^|,)\s*t=(\d+)/.exec(header);
  const v1 = /(?:^|,)\s*v1=([0-9a-f]{64})/.exec(header);
  if (!t || !v1) return false;

  const timestamp = Number(t[1]);
  if (Math.abs(nowSeconds - timestamp) > toleranceSeconds) return false;

  const expected = createHmac('sha256', secret).update(`${timestamp}.${rawBody}`).digest();
  const received = Buffer.from(v1[1], 'hex');
  return received.length === expected.length && timingSafeEqual(received, expected);
}

// Exemplo com Express: use o corpo bruto (express.raw), nunca o JSON já convertido.
//
// app.post('/webhooks/koku', express.raw({ type: 'application/json' }), (req, res) => {
//   const rawBody = req.body.toString('utf8');
//   const ok = verifyKokuSignature({
//     rawBody,
//     signatureHeader: req.get('x-koku-signature'),
//     secret: process.env.KOKU_WEBHOOK_SECRET,
//   });
//   if (!ok) return res.status(400).send('assinatura inválida');
//   res.status(200).send('ok'); // responda rápido; processe o evento em segundo plano
//   enfileirar(JSON.parse(rawBody));
// });

Estes trechos são executados pelos testes automáticos da documentação contra entregas reais da API.

Exemplo para conferir a sua implementação

Com o segredo whsec_Zq4m8Tn2Vb7Lk1Xc9Rd5Hs3Wf6Jp0Ga2Ye8Ui4Ko7 (fictício), a entrega abaixo tem assinatura válida no instante t mostrado. Use-a num teste unitário passando o "agora" igual a t:

Entrega recebida pelo seu endpoint
POST /webhooks/koku HTTP/1.1
content-type: application/json
user-agent: Koku-Webhooks/1
x-koku-signature: t=1791473531,v1=a735379a545698a02021820ddff8136ca623148267e128cf0ab6107e94dc3d96
x-koku-event-id: evt_2f6b1c9e8a4d4e7f9b3c5a1d0e6f8b27
x-koku-event-type: charge.paid
x-koku-event-version: v1

{"id":"evt_2f6b1c9e8a4d4e7f9b3c5a1d0e6f8b27","type":"charge.paid","version":"v1","created_at":"2026-10-07T15:32:11.000Z","environment":"production","data":{"charge_id":"8d3f6a12-4b9e-4c71-a2d5-6e0f1b3c9a47","order_reference":"PEDIDO-1001","amount_cents":"15990","fee_cents":"320","net_cents":"15670","reserve_cents":"0","paid_at":"2026-10-07T15:32:10.000Z","end_to_end_id":"E12345678202610071532a1B2c3D4e5F","late_payment":false,"case_id":null}}

Cuidados

  • Corpo bruto: em Express use express.raw({ type: 'application/json' }) no endpoint do webhook; em Flask, request.get_data(); em PHP, file_get_contents('php://input').
  • Segredo: guarde em variável de ambiente ou cofre. Se vazar, cadastre um endpoint novo (com segredo novo) e desabilite o antigo.
  • Tolerância de tempo: mantenha o relógio do servidor sincronizado (NTP).
  • Mais de um endpoint: cada endpoint tem o seu segredo; use o segredo do endpoint que recebeu a chamada.