Assinatura

Como confirmar que a entrega veio mesmo da plataforma.

O header

Se a cobrança foi criada com webhook_secret, ou o endpoint cadastrado tem secret, toda entrega chega com X-Webhook-Signature: o HMAC SHA-256 do corpo exato da requisição, em hexadecimal, prefixado por sha256=.

Header
X-Webhook-Signature: sha256=6f1a...c93b

Verificando

Calcule o HMAC sobre o corpo cru, antes de qualquer parse. Serializar o JSON de novo muda espaços e ordem de chaves, e a assinatura deixa de bater.

Node.js (Express)
import { createHmac, timingSafeEqual } from 'crypto'

app.post(
  '/hooks/pagamentos',
  express.raw({ type: 'application/json' }),
  (req, res) => {
    const recebida = req.header('X-Webhook-Signature') ?? ''
    const esperada =
      'sha256=' +
      createHmac('sha256', process.env.WEBHOOK_SECRET)
        .update(req.body)
        .digest('hex')

    const a = Buffer.from(recebida)
    const b = Buffer.from(esperada)

    if (a.length !== b.length || !timingSafeEqual(a, b)) {
      return res.sendStatus(401)
    }

    res.sendStatus(200)
    processarDepois(JSON.parse(req.body.toString('utf8')))
  },
)

Compare em tempo constante, com timingSafeEqual ou equivalente. Comparação com === vaza, pela diferença de tempo, quantos caracteres iniciais estavam certos.

Boas práticas

Recuse a requisição quando a assinatura não bater, com 401, e registre o caso: assinatura inválida é tentativa de forjar evento, não erro de rede.

Sem secret cadastrado não há header de assinatura, e qualquer um que descubra a URL consegue simular um pagamento confirmado. Configure o segredo antes de colocar a integração no ar.

Para trocar o segredo sem perder entregas, cadastre um segundo endpoint com o segredo novo, valide que ele está recebendo e só então remova o antigo.