Se você processa o webhook sem checar a assinatura, qualquer pessoa com a sua URL pode marcar um pedido como pago. Este artigo mostra como verificar o header X-Signature da Vanilla Pag em três linguagens.
O contrato
Toda vez que enviamos um webhook, calculamos:
signature = "sha256=" + HEX(HMAC_SHA256(webhook_secret, raw_request_body))
E enviamos no header X-Signature. O webhook_secret é um campo da sua credencial (página Credenciais no painel admin). Use o raw body recebido — não a versão JSON parseada — porque qualquer normalização vai quebrar o hash.
Node.js (Express)
import crypto from "crypto";
import express from "express";
const app = express();
// IMPORTANTE: precisa do raw body para o HMAC bater
app.use("/webhook", express.raw({ type: "application/json" }));
app.post("/webhook", (req, res) => {
const sent = (req.get("X-Signature") || "").replace(/^sha256=/, "");
const expected = crypto
.createHmac("sha256", process.env.VANILLA_WEBHOOK_SECRET)
.update(req.body)
.digest("hex");
if (!sent || !crypto.timingSafeEqual(Buffer.from(sent), Buffer.from(expected))) {
return res.status(401).send("invalid signature");
}
const event = JSON.parse(req.body.toString());
if (event.status === "PAID") {
// libera o pedido
}
res.status(200).send("ok");
});
PHP (raw)
<?php
$raw = file_get_contents("php://input");
$sent = preg_replace('/^sha256=/', '', $_SERVER['HTTP_X_SIGNATURE'] ?? '');
$expected = hash_hmac('sha256', $raw, getenv('VANILLA_WEBHOOK_SECRET'));
if (!$sent || !hash_equals($expected, $sent)) {
http_response_code(401);
exit('invalid signature');
}
$event = json_decode($raw, true);
if (($event['status'] ?? '') === 'PAID') {
// libera o pedido
}
http_response_code(200);
echo 'ok';
Python (Flask)
import hmac, hashlib, os
from flask import Flask, request, abort
app = Flask(__name__)
SECRET = os.environ["VANILLA_WEBHOOK_SECRET"].encode()
@app.post("/webhook")
def webhook():
raw = request.get_data() # bytes, antes de qualquer parse
sent = request.headers.get("X-Signature", "").removeprefix("sha256=")
expected = hmac.new(SECRET, raw, hashlib.sha256).hexdigest()
if not sent or not hmac.compare_digest(sent, expected):
abort(401)
event = request.get_json()
if event["status"] == "PAID":
... # libera
return "ok", 200
Cuidados extras
- Sempre use timingSafeEqual / hash_equals. Comparar com
==abre a porta para timing attacks. - Trate idempotência. O mesmo evento pode chegar duas vezes (retry). Use o
idda transação como chave única para não processar duas vezes. - Responda em até 10 segundos. Se demorar, tentamos de novo com backoff e seu sistema acumula trabalho.
Veja a documentação completa do webhook para mais detalhes.