Validar assinatura (HMAC)
Qualquer pessoa que descubra a URL do seu webhook pode enviar requisições para ela. Para ter certeza de que uma notificação veio da marx e não foi alterada no caminho, ative a assinatura HMAC e valide cada requisição.
Ativando
No painel, em Integração → Webhooks, ligue Assinar com HMAC e salve (a senha é pedida). Depois, em Segredo de assinatura → Ver, copie o segredo e guarde no seu servidor.
A partir daí, todo webhook chega com dois cabeçalhos extras:
| Cabeçalho | Exemplo | Significado |
|---|---|---|
X-Marx-Timestamp |
1759708800 |
Momento do envio (Unix, segundos). |
X-Marx-Signature |
t=1759708800,v1=5f2b…c9 |
Timestamp + assinatura HMAC-SHA256 em hexadecimal. |
Como a assinatura é calculada
assinatura = HMAC_SHA256(segredo, "<timestamp>.<corpo bruto da requisição>")
timestampé o valor det=(o mesmo deX-Marx-Timestamp);- o corpo bruto é exatamente o texto recebido, byte a byte. Valide antes de fazer o parse do JSON: reformatar o JSON muda a assinatura.
Passo a passo da validação
- Leia
X-Marx-Signaturee separetev1. - Recuse se
testiver a mais de 5 minutos do seu relógio (evita reenvio de requisições antigas). - Calcule o HMAC-SHA256 de
"<t>.<corpo bruto>"com o seu segredo. - Compare com
v1usando comparação em tempo constante. - Só então processe o evento e responda
2xx.
Node.js (Express)
import crypto from 'node:crypto'
import express from 'express'
const app = express()
const SECRET = process.env.MARX_WEBHOOK_SECRET
app.post('/webhooks/marx', express.raw({ type: '*/*' }), (req, res) => {
const header = req.get('X-Marx-Signature') || ''
const parts = Object.fromEntries(header.split(',').map((kv) => kv.split('=')))
const age = Math.abs(Date.now() / 1000 - Number(parts.t))
const expected = crypto.createHmac('sha256', SECRET).update(`${parts.t}.${req.body}`).digest('hex')
const valid = parts.v1?.length === expected.length && age < 300 &&
crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(parts.v1))
if (!valid) return res.status(401).end()
const event = JSON.parse(req.body)
// ... processe o evento (de forma idempotente) ...
res.status(204).end()
})
Python (Flask)
import hmac, hashlib, os, time
from flask import Flask, request, abort
app = Flask(__name__)
SECRET = os.environ["MARX_WEBHOOK_SECRET"].encode()
@app.post("/webhooks/marx")
def marx_webhook():
raw = request.get_data() # corpo bruto
parts = dict(p.split("=", 1) for p in request.headers.get("X-Marx-Signature", "").split(",") if "=" in p)
expected = hmac.new(SECRET, f"{parts.get('t')}.".encode() + raw, hashlib.sha256).hexdigest()
if abs(time.time() - int(parts.get("t", 0))) > 300 or not hmac.compare_digest(expected, parts.get("v1", "")):
abort(401)
event = request.get_json()
# ... processe o evento ...
return "", 204
Ruby (Rails)
class MarxWebhooksController < ActionController::API
def create
raw = request.raw_post
parts = request.headers['X-Marx-Signature'].to_s.split(',').to_h { |kv| kv.split('=', 2) }
expected = OpenSSL::HMAC.hexdigest('SHA256', ENV.fetch('MARX_WEBHOOK_SECRET'), "#{parts['t']}.#{raw}")
fresh = (Time.now.to_i - parts['t'].to_i).abs < 300
return head :unauthorized unless fresh && ActiveSupport::SecurityUtils.secure_compare(expected, parts['v1'].to_s)
event = JSON.parse(raw)
# ... processe o evento ...
head :no_content
end
end
PHP
<?php
$raw = file_get_contents('php://input');
parse_str(str_replace(',', '&', $_SERVER['HTTP_X_MARX_SIGNATURE'] ?? ''), $parts);
$expected = hash_hmac('sha256', ($parts['t'] ?? '') . '.' . $raw, getenv('MARX_WEBHOOK_SECRET'));
if (abs(time() - (int)($parts['t'] ?? 0)) > 300 || !hash_equals($expected, $parts['v1'] ?? '')) {
http_response_code(401);
exit;
}
$event = json_decode($raw, true);
// ... processe o evento ...
http_response_code(204);
Trocando o segredo
Em Segredo de assinatura → Gerar novo (avisos, frase GERAR NOVO SEGREDO e senha). Os webhooks seguintes já saem assinados com o novo segredo. Para trocar sem perder notificações, aceite temporariamente os dois segredos no seu servidor e remova o antigo depois da troca.
Dica
Use Integração → Enviar teste: a marx envia um evento ping assinado para a sua URL e mostra o código HTTP e o tempo de resposta. É a forma mais rápida de testar a sua validação.