Verifique a assinatura
Valide o HMAC-SHA256 do cabeçalho X-DM-Signature para ter certeza de que o webhook veio da Accelerazione e não foi alterado.
Qualquer pessoa que descubra a sua postbackUrl pode enviar um POST para ela. A assinatura HMAC prova que o webhook foi gerado pela Accelerazione com o segredo da sua conta e que o corpo não mudou no caminho.
Como a assinatura é montada
X-DM-Signature: t=1788876000,v1=5f0c8e...e91a
X-DM-Webhook-Id: 3f1c2b9e-7a64-8d2f-b1e0-6c5a4d3e2f10| Parte | Significado |
|---|---|
t | Momento em que o webhook foi gerado, em segundos desde 1970 (Unix). |
v1 | HMAC-SHA256(segredo, t + "." + corpo), em hexadecimal minúsculo. Pode haver mais de um v1: durante a troca de segredo, a mensagem é assinada com o segredo antigo e com o novo. |
- O segredo começa com
whsec_e é usado inteiro, prefixo incluído, como texto UTF-8. Não decodifique o restante como base64. - O corpo são os bytes exatos recebidos. Qualquer conversão (parse e serialização de novo, troca de espaços, normalização de acentos) muda o resultado.
- A assinatura é aceita se qualquer
v1bater com qualquer segredo ativo seu.
O segredo é entregue pela equipe de integração por canal seguro, um por ambiente. Guarde-o como a chave de API: no cofre de segredos do servidor, nunca em código ou log.
Verificação
const { createHmac, timingSafeEqual } = require('node:crypto');
/**
* @param {Buffer} rawBody corpo cru, antes de qualquer parse
* @param {string} header valor de X-DM-Signature
* @param {string[]} secrets segredos ativos (whsec_...)
*/
function verifySignature(rawBody, header, secrets) {
if (!Buffer.isBuffer(rawBody) || typeof header !== 'string') return false;
const fields = header.split(',').map((x) => x.trim());
const ts = fields.filter((x) => x.startsWith('t='));
if (ts.length !== 1) return false;
const t = ts[0].slice(2);
if (!/^\d+$/.test(t)) return false;
const signatures = fields
.filter((x) => x.startsWith('v1='))
.map((x) => x.slice(3))
.filter((x) => /^[0-9a-f]{64}$/i.test(x));
if (signatures.length === 0) return false;
return secrets.some((secret) => {
const expected = createHmac('sha256', secret).update(`${t}.`, 'utf8').update(rawBody).digest();
return signatures.some((hex) => timingSafeEqual(expected, Buffer.from(hex, 'hex')));
});
}
module.exports = { verifySignature };Com Express, capture o corpo cru antes do express.json():
const express = require('express');
const { verifySignature } = require('./verify-signature');
const app = express();
const secrets = [process.env.DM_WEBHOOK_SECRET];
app.post('/webhooks/dm', express.raw({ type: 'application/json' }), async (req, res) => {
const header = req.get('X-DM-Signature') ?? req.get('X-Kirvano-Signature');
if (!verifySignature(req.body, header, secrets)) return res.status(400).end();
const event = JSON.parse(req.body.toString('utf8'));
const deliveryId = req.get('X-DM-Webhook-Id') ?? req.get('X-Kirvano-Webhook-Id');
await saveEventIfNew(deliveryId, event); // grava de forma durável e deduplica
res.status(200).end();
});Teste a sua implementação
Use este vetor para conferir o código antes de receber o primeiro webhook real. Com os valores abaixo, a função deve retornar verdadeiro.
| Entrada | Valor |
|---|---|
| Segredo | whsec_test_secret_0123456789 |
t | 1718480000 |
| Corpo | {"eventType":"payment_intent.succeeded","data":{"merchantId":"ace56969-32ab-402c-8394-d5a7c9d242de"}} |
v1 esperado | b4bee7de0edd1a4b7fe81014f62493868bac279ba7a86fb80d4e5c3661ac2908 |
printf '%s' '1718480000.{"eventType":"payment_intent.succeeded","data":{"merchantId":"ace56969-32ab-402c-8394-d5a7c9d242de"}}' \
| openssl dgst -sha256 -hmac 'whsec_test_secret_0123456789'Janela de tempo
Reenvios carregam o mesmo t e a mesma assinatura da primeira tentativa, e uma reentrega feita pela equipe de suporte pode trazer um t de dias atrás. Por isso não rejeite um webhook só porque t é antigo. A proteção contra repetição vem da deduplicação persistente por X-DM-Webhook-Id e da consulta do estado na API.
Quando a assinatura falha ou está ausente
- Assinatura inválida. Não aplique o evento. Responda
400, registre o ocorrido sem gravar o corpo inteiro e confira se o segredo e o ambiente estão certos. - Assinatura ausente. A assinatura depende de a conta ter um segredo ativo. Se você ainda não recebeu o seu, os webhooks chegam sem
X-DM-Signature. Nesse caso, trate o webhook como um aviso: confirme o estado comGET /api/v1/paymentintents/{id}antes de alterar o pedido, e peça o segredo à equipe de integração. refund_failedsem assinatura. O motivo da falha não aparece na consulta. Mantenha o estorno como pendente de verificação até confirmar com a equipe de integração.
Cabeçalhos legados
Integrações anteriores recebem a mesma assinatura também em X-Kirvano-Signature e X-Kirvano-Webhook-Id. Os valores são idênticos. Leia X-DM-* primeiro e use os legados só como alternativa, como nos exemplos acima.
Mesmo com a assinatura válida, aplique os controles de Receba webhooks: pedido conhecido, deduplicação, comparação de valor e moeda e tratamento da ordem dos eventos. Uma assinatura válida não significa que um evento repetido deve ser processado de novo.