Verificar a assinatura
Como confirmar que uma chamada de webhook veio mesmo da Venddor e não foi alterada, com o algoritmo, os cabeçalhos e código pronto.
A URL do seu receptor é pública. Qualquer pessoa que a descubra pode enviar um POST fingindo ser a Venddor. Por isso toda chamada de saída é assinada: a plataforma calcula um HMAC-SHA256 do corpo com o secret do webhook e manda o resultado no cabeçalho X-Venddor-Signature. O seu receptor refaz a conta e só aceita se o resultado for igual.
O secret é entregue uma única vez, na resposta de Cadastrar webhook. Guarde-o em um cofre de segredos do seu lado, nunca no código nem em páginas de navegador.
Cabeçalhos de cada chamada
Toda entrega de saída (exceto os eventos de incentivo, descritos em Eventos) é um POST com estes cabeçalhos:
| Cabeçalho | Valor | Para que serve |
|---|---|---|
Content-Type | application/json | O corpo é sempre JSON. |
X-Venddor-Signature | sha256=<64 caracteres hexadecimais> | A assinatura do corpo. |
X-Venddor-Event | Nome do evento, por exemplo order.created. test no envio de teste | Roteamento sem precisar abrir o JSON. |
User-Agent | Venddor-Webhook/1.0 | Identifica a origem nos seus logs. |
O corpo é o envelope {event, payload, timestamp, webhook_id}. Em um reenvio manual (rota retry) ele ganha também "retry": true.
O algoritmo
- Leia o corpo bruto
Pegue os bytes exatos que chegaram, antes de qualquer parser de JSON. Se o framework já converteu o corpo em objeto, configure-o para guardar o texto original.
- Calcule o HMAC
HMAC-SHA256(chave, corpo), onde a chave é o texto dosecrettal como recebido (os 64 caracteres, em UTF-8). Não decodifique osecretde hexadecimal para bytes: ele é usado como texto. - Monte o valor esperado
Escreva o resultado em hexadecimal minúsculo e coloque o prefixo
sha256=. - Compare em tempo constante
Compare com o cabeçalho usando uma função de comparação de tempo constante (
crypto.timingSafeEqual,hmac.compare_digest,hash_equals). Uma comparação comum (==) vaza informação pelo tempo de resposta. - Cheque a idade da chamada
O campo
timestampdo envelope está dentro do corpo assinado e é recalculado a cada tentativa. Recuse chamadas com mais de 5 minutos de diferença do seu relógio para barrar a repetição de uma captura antiga.
Exemplo verificável
Use este par para testar a sua implementação. Se o seu código não chegar ao mesmo resultado, o problema está no tratamento do corpo.
secret : 9f2c4e6a8b1d3f507a9c2e4b6d8f1a3c5e7b9d0f2a4c6e8b1d3f5a7c9e0b2d4f
corpo : {"event":"order.created","payload":{"type":"order.created","order_id":"3f73310d-bfdd-4713-a664-305c08eda58b","total":22500},"timestamp":"2026-10-11T20:15:03Z","webhook_id":"0b9c2f6e-3a5d-4c0e-8a52-6a1f2f0d8c11"}
header : X-Venddor-Signature: sha256=20bf56ce5ff122aa96a7ba52b2282406fd37429c811e28a3ec2eae6cbab354d7Código de exemplo
import crypto from "node:crypto";
import express from "express";
const app = express();
const SECRET = process.env.VENDDOR_WEBHOOK_SECRET;
// express.raw guarda o corpo como Buffer, sem reformatar.
app.post("/webhooks/venddor", express.raw({ type: "application/json" }), (req, res) => {
const received = req.get("X-Venddor-Signature") || "";
const expected =
"sha256=" + crypto.createHmac("sha256", SECRET).update(req.body).digest("hex");
const a = Buffer.from(received);
const b = Buffer.from(expected);
if (a.length !== b.length || !crypto.timingSafeEqual(a, b)) {
return res.status(401).send("invalid signature");
}
const envelope = JSON.parse(req.body.toString("utf8"));
const ageMs = Math.abs(Date.now() - Date.parse(envelope.timestamp));
if (ageMs > 5 * 60 * 1000) return res.status(401).send("stale");
res.sendStatus(200); // responda logo
queue.push(envelope); // e processe em segundo plano
});Assinatura nas rotas de entrada
Os parceiros que chamam a loja usam o próprio esquema: HMAC no corpo, segredo compartilhado em cabeçalho ou token de verificação. Cada um está descrito em Visão geral da entrada.