Docs
Webhooks

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
ParteSignificado
tMomento em que o webhook foi gerado, em segundos desde 1970 (Unix).
v1HMAC-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 v1 bater 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

verify-signature.js
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():

server.js
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.

EntradaValor
Segredowhsec_test_secret_0123456789
t1718480000
Corpo{"eventType":"payment_intent.succeeded","data":{"merchantId":"ace56969-32ab-402c-8394-d5a7c9d242de"}}
v1 esperadob4bee7de0edd1a4b7fe81014f62493868bac279ba7a86fb80d4e5c3661ac2908
Conferência com openssl
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 com GET /api/v1/paymentintents/{id} antes de alterar o pedido, e peça o segredo à equipe de integração.
  • refund_failed sem 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.

Nesta página