Pular para o conteúdo

Verificar assinaturas de webhook

Toda entrega traz o cabeçalho FeitPay-Signature:

FeitPay-Signature: t=1760961600,v1=186aa47526353adc39a808f8e0bd84f501a5a6a1f483572b1f8a3eab3ed44cc8
  1. Leia o corpo bruto da requisição exatamente como recebido (bytes). Não interprete e re-serialize o JSON antes — qualquer mudança de espaços ou ordem de chaves quebra a assinatura.
  2. Interprete o cabeçalho: t é um timestamp Unix; cada v1 é uma assinatura em hexadecimal. Ignore esquemas desconhecidos.
  3. Monte a mensagem assinada: <t> + "." + <corpo bruto>.
  4. Calcule HMAC-SHA256(chave = o segredo completo "whsec_…" em UTF-8, mensagem) e codifique em hexadecimal.
  5. Compare com cada valor v1 usando comparação em tempo constante. Qualquer correspondência é válida (várias aparecem durante a rotação).
  6. Rejeite se |agora − t| for maior que a sua tolerância (padrão 300 segundos) — isso bloqueia replays.
  7. Só então interprete o JSON e processe o evento, descartando duplicatas pelo event_id.

Motivos de falha usados pelas bibliotecas oficiais: malformed_header, no_signatures, signature_mismatch, timestamp_outside_tolerance, invalid_payload. Responda 400 a qualquer um deles.

import { createServer } from 'node:http';
import { verifyWebhook, WebhookVerificationError } from '@feitpay/webhooks';
const secrets = [process.env.FEITPAY_WEBHOOK_SECRET!]; // adicione o segredo anterior durante a rotação
createServer(async (req, res) => {
const chunks: Buffer[] = [];
for await (const chunk of req) chunks.push(chunk as Buffer);
const raw = Buffer.concat(chunks); // o corpo BRUTO
try {
const event = await verifyWebhook({ payload: raw, header: req.headers['feitpay-signature'] as string, secrets });
// descarte duplicatas pelo event.event_id e processe
res.writeHead(200).end();
} catch (err) {
const reason = err instanceof WebhookVerificationError ? err.reason : 'error';
res.writeHead(400, { 'Content-Type': 'application/json' }).end(JSON.stringify({ error: reason }));
}
}).listen(4400);

Em route handlers do Next.js, use await request.text() (não request.json()) para obter o corpo bruto. Uma variante síncrona para Node está disponível em @feitpay/webhooks/node (verifyWebhookSync).

O repositório inclui vetores para todas as linguagens em packages/test-fixtures/src/webhook-vectors.json — assinaturas válidas, rotação com duas assinaturas, corpo adulterado, segredo errado, timestamps expirados e futuros, cabeçalhos ausentes e malformados. As bibliotecas TypeScript, Python e Go executam todos os casos na CI. Se você escrever o seu próprio verificador, rode-o contra o mesmo arquivo.

  • Painel: Integração → Webhooks → Enviar evento de teste, ou
  • POST /v1/webhook-endpoints/{id}/test, que coloca na fila uma entrega assinada de webhook.test.

Para testes unitários, gere cabeçalhos válidos localmente com generateTestHeader (TypeScript), generate_test_header (Python) ou webhook.GenerateTestHeader (Go).