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 id da 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.