Todo webhook enviado pela Movvia inclui o header x-movvia-signature com a assinatura HMAC-SHA256 do payload. Sempre valide a assinatura antes de processar.
HMAC-SHA256(secret, payload_utf8) → hex stringOnde:
secreté owebhook_secretfornecido no onboarding.payload_utf8é o body JSON bruto (bytes), não o objeto deserializado.
Use o body bruto
Deserializar e re-serializar o JSON pode alterar a ordem das chaves e quebrar a verificação. Leia o body como bytes antes de qualquer parse.
import crypto from 'crypto';
import express from 'express';
const app = express();
// Capturar body como Buffer antes do parse
app.use(express.json({
verify: (req: any, _res, buf) => { req.rawBody = buf; }
}));
function verificarAssinatura(
rawBody: Buffer,
signature: string,
secret: string
): boolean {
const expected = crypto
.createHmac('sha256', secret)
.update(rawBody)
.digest('hex');
return crypto.timingSafeEqual(
Buffer.from(signature, 'hex'),
Buffer.from(expected, 'hex')
);
}
app.post('/webhook', (req: any, res) => {
const sig = req.headers['x-movvia-signature'] as string;
if (!sig || !verificarAssinatura(req.rawBody, sig, process.env.MV_WEBHOOK_SECRET!)) {
return res.status(401).json({ erro: 'Assinatura inválida' });
}
// processar req.body com segurança
console.log('Evento válido:', req.body.evento);
res.status(200).send('OK');
});import hashlib
import hmac
import os
from flask import Flask, request, abort
app = Flask(__name__)
SECRET = os.environ['MV_WEBHOOK_SECRET'].encode()
def verificar_assinatura(payload: bytes, signature: str) -> bool:
expected = hmac.new(SECRET, payload, hashlib.sha256).hexdigest()
return hmac.compare_digest(signature, expected)
@app.route('/webhook', methods=['POST'])
def webhook():
sig = request.headers.get('x-movvia-signature', '')
payload = request.get_data() # body bruto
if not verificar_assinatura(payload, sig):
abort(401)
evento = request.json
print(f"Evento válido: {evento['evento']}")
return 'OK', 200package main
import (
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"io"
"net/http"
"os"
)
func verificarAssinatura(payload []byte, signature, secret string) bool {
mac := hmac.New(sha256.New, []byte(secret))
mac.Write(payload)
expected := hex.EncodeToString(mac.Sum(nil))
return hmac.Equal([]byte(signature), []byte(expected))
}
func webhookHandler(w http.ResponseWriter, r *http.Request) {
sig := r.Header.Get("x-movvia-signature")
body, _ := io.ReadAll(r.Body)
if !verificarAssinatura(body, sig, os.Getenv("MV_WEBHOOK_SECRET")) {
http.Error(w, "Assinatura inválida", http.StatusUnauthorized)
return
}
w.WriteHeader(http.StatusOK)
}| Problema | Causa provável | Solução |
|---|---|---|
| Assinatura sempre inválida | Body deserializado antes da verificação | Usar body bruto (bytes) |
| Comparação insegura | == em vez de timingSafeEqual/compare_digest | Sempre usar comparação em tempo constante |
| Secret incorreto | webhook_secret diferente do client_secret | São credenciais distintas — verificar o correto no onboarding |
Testar localmente
Use o endpoint de simulação do sandbox para gerar eventos reais com assinatura válida. Veja o Quickstart.