Verificar assinaturas de webhook
Toda entrega traz o cabeçalho FeitPay-Signature:
FeitPay-Signature: t=1760961600,v1=186aa47526353adc39a808f8e0bd84f501a5a6a1f483572b1f8a3eab3ed44cc8O algoritmo
Seção intitulada “O algoritmo”- 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.
- Interprete o cabeçalho:
té um timestamp Unix; cadav1é uma assinatura em hexadecimal. Ignore esquemas desconhecidos. - Monte a mensagem assinada:
<t>+"."+<corpo bruto>. - Calcule
HMAC-SHA256(chave = o segredo completo "whsec_…" em UTF-8, mensagem)e codifique em hexadecimal. - Compare com cada valor
v1usando comparação em tempo constante. Qualquer correspondência é válida (várias aparecem durante a rotação). - Rejeite se
|agora − t|for maior que a sua tolerância (padrão 300 segundos) — isso bloqueia replays. - 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.
Verificadores prontos
Seção intitulada “Verificadores prontos”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).
import "github.com/feitpay/feitpay-go/webhook"
func handle(w http.ResponseWriter, r *http.Request) { body, err := io.ReadAll(http.MaxBytesReader(w, r.Body, 1<<20)) if err != nil { http.Error(w, "payload too large", http.StatusRequestEntityTooLarge) return } secrets := []string{os.Getenv("FEITPAY_WEBHOOK_SECRET")} event, err := webhook.Verify(body, r.Header.Get(webhook.SignatureHeader), secrets, nil) if err != nil { http.Error(w, string(webhook.ReasonOf(err)), http.StatusBadRequest) return } // descarte duplicatas pelo event.EventID e processe w.WriteHeader(http.StatusOK)}import osfrom feitpay import webhooksfrom feitpay.webhooks import WebhookVerificationError
SECRETS = [os.environ["FEITPAY_WEBHOOK_SECRET"]]
def handle(raw_body: bytes, signature_header: str) -> tuple[int, dict]: try: event = webhooks.verify(raw_body, signature_header, SECRETS) except WebhookVerificationError as exc: return 400, {"error": exc.reason} # descarte duplicatas pelo event["event_id"] e processe return 200, {"received": True}signed = timestamp + "." + raw_bodyexpected = hex( HMAC_SHA256( key = "whsec_…", data = signed ) )valid = any( constant_time_equal(expected, v1) for v1 in header.v1 ) and abs(now - timestamp) <= 300Vetores de teste
Seção intitulada “Vetores de teste”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.
Envie a si mesmo um teste assinado
Seção intitulada “Envie a si mesmo um teste assinado”- Painel: Integração → Webhooks → Enviar evento de teste, ou
POST /v1/webhook-endpoints/{id}/test, que coloca na fila uma entrega assinada dewebhook.test.
Para testes unitários, gere cabeçalhos válidos localmente com generateTestHeader (TypeScript),
generate_test_header (Python) ou webhook.GenerateTestHeader (Go).