Verificação e Segurança

Como verificar a assinatura HMAC de um webhook da ValidaPay antes de confiar no payload.

Todo webhook chega com o header X-Webhook-Signature, que não é um HMAC simples do corpo. Ele é composto:

Texto
X-Webhook-Signature: t=1757083200000,v1=9f2b8c...

A string assinada é o timestamp, um ponto, e o corpo:

Texto
v1 = HMAC_SHA256(secret, `${t}.${corpo_bruto}`)
Use o corpo bruto, byte a byte. Se você fizer JSON.parse e reserializar, o espaçamento e a ordem das chaves mudam e o HMAC deixa de bater.

Em Express, isso significa capturar o raw body antes do express.json():

JavaScript
import crypto from 'crypto';
import express from 'express';

const app = express();

app.post('/webhooks/validapay',
  express.raw({ type: 'application/json' }),
  (req, res) => {
    const header = req.get('X-Webhook-Signature') ?? '';
    const { t, v1 } = Object.fromEntries(
      header.split(',').map((part) => part.split('='))
    );

    const esperado = crypto
      .createHmac('sha256', process.env.VALIDAPAY_WEBHOOK_SECRET)
      .update(`${t}.${req.body.toString('utf8')}`)
      .digest('hex');

    const confere =
      v1?.length === esperado.length &&
      crypto.timingSafeEqual(Buffer.from(v1), Buffer.from(esperado));

    if (!confere) return res.sendStatus(401);

    const idadeMs = Date.now() - Number(t);
    if (idadeMs > 5 * 60 * 1000) return res.sendStatus(401);

    res.sendStatus(200);
    processarDepois(JSON.parse(req.body.toString('utf8')));
  }
);
Compare com timingSafeEqual, não com ===. E rejeite assinaturas antigas. Sem janela de tolerância, uma assinatura capturada vale para sempre.

Headers

X-Webhook-Signature
t=<timestamp>,v1=<hmac hex>
x-access-token
Token estático opcional, se configurado no cadastro
Essa página foi útil?