Autenticação
No formato v2 a Rapid assina cada webhook com HMAC-SHA256 usando a sua chave de assinatura (o secret). A Rapid gera essa chave quando você cadastra o webhook e a mostra uma única vez no painel, em Configurações > Webhook; guarde na hora. Não há como recuperar a chave depois: se perder, peça uma nova ao suporte da Rapid (a anterior deixa de valer no mesmo instante). Sua aplicação deve validar essa assinatura antes de processar o payload, é assim que você confirma que o POST veio da Rapid.
Contas no formato
legacy(migradas do sistema anterior) não recebem assinatura: a autenticação é feita pelos headersclientid/clientkey, como antes. Veja qual é o seu formato em Payload.
Header de assinatura
Seção intitulada “Header de assinatura”X-Webhook-Signature: sha256=<hmac_hex>Onde <hmac_hex> é o HMAC-SHA256, em hexadecimal, da string ${timestamp}.${corpo_bruto}, ou seja o valor do header X-Webhook-Timestamp (segundos desde a epoch), um ponto, e o corpo bruto da requisição (o JSON recebido, byte a byte), usando a sua chave de assinatura.
O timestamp entra na assinatura para dar proteção contra replay: sem ele, um POST válido capturado poderia ser reenviado indefinidamente com assinatura correta. Rejeite requisições cujo X-Webhook-Timestamp esteja fora de uma janela de tolerância (recomendamos 5 minutos).
Como validar
Seção intitulada “Como validar”- Leia o corpo bruto da requisição. Não parse como JSON antes de validar: a assinatura é calculada sobre os bytes exatos recebidos.
- Monte
signed = timestamp + "." + raw_body, calculeHMAC-SHA256(secret, signed)e codifique em hexadecimal - Compare com o valor em
X-Webhook-Signature(após o prefixosha256=) - Use comparação timing-safe para evitar timing attacks
Exemplos
Seção intitulada “Exemplos”Um endpoint completo em cada linguagem: lê o corpo bruto, valida a assinatura e a janela de tempo, e só então lê o JSON. Responde 401 para qualquer entrega que não passe, inclusive sem os headers. A chave vem da variável de ambiente RAPID_WEBHOOK_SECRET.
import crypto from 'node:crypto'import express from 'express'
const TOLERANCIA_SEGUNDOS = 5 * 60
function validaAssinatura(corpoBruto, assinatura, timestamp, secret) { // 1. Janela de tolerância (anti-replay). Sem isto, um POST antigo capturado // continua passando pra sempre. const idade = Math.abs(Math.floor(Date.now() / 1000) - Number(timestamp)) if (!Number.isFinite(idade) || idade > TOLERANCIA_SEGUNDOS) return false
// 2. Assina `timestamp.corpo_bruto` (o corpo recebido, sem reserializar). const esperada = 'sha256=' + crypto.createHmac('sha256', secret).update(`${timestamp}.${corpoBruto}`).digest('hex')
const a = Buffer.from(esperada) const b = Buffer.from(assinatura ?? '') return a.length === b.length && crypto.timingSafeEqual(a, b)}
const app = express()
// express.raw, e não express.json: a assinatura é calculada sobre o corpo bruto.app.post('/webhooks/rapid', express.raw({ type: 'application/json' }), (req, res) => { const corpoBruto = req.body.toString('utf8') const ok = validaAssinatura( corpoBruto, req.get('X-Webhook-Signature'), req.get('X-Webhook-Timestamp'), process.env.RAPID_WEBHOOK_SECRET, ) if (!ok) return res.status(401).end()
const evento = JSON.parse(corpoBruto) // Processe o `evento` (de preferência numa fila) e responda rápido. res.status(200).end()})
app.listen(process.env.PORT ?? 3000)import hashlibimport hmacimport osimport time
from flask import Flask, request
TOLERANCIA_SEGUNDOS = 5 * 60
def valida_assinatura(corpo_bruto: bytes, assinatura: str, timestamp: str, secret: str) -> bool: # 1. Janela de tolerância (anti-replay). try: idade = abs(int(time.time()) - int(timestamp)) except (TypeError, ValueError): return False if idade > TOLERANCIA_SEGUNDOS or not assinatura: return False
# 2. Assina `timestamp.corpo_bruto` (os bytes recebidos, sem reserializar). assinado = timestamp.encode() + b"." + corpo_bruto esperada = "sha256=" + hmac.new(secret.encode(), assinado, hashlib.sha256).hexdigest() return hmac.compare_digest(esperada.encode(), assinatura.encode())
app = Flask(__name__)
@app.post("/webhooks/rapid")def webhook_rapid(): # get_data(), e não get_json(): a assinatura é calculada sobre o corpo bruto. corpo_bruto = request.get_data() ok = valida_assinatura( corpo_bruto, request.headers.get("X-Webhook-Signature", ""), request.headers.get("X-Webhook-Timestamp", ""), os.environ["RAPID_WEBHOOK_SECRET"], ) if not ok: return "", 401
evento = request.get_json() # Processe o `evento` (de preferência numa fila) e responda rápido. return "", 200<?phpconst TOLERANCIA_SEGUNDOS = 5 * 60;
function validaAssinatura(string $corpoBruto, string $assinatura, string $timestamp, string $secret): bool{ // 1. Janela de tolerância (anti-replay). if (!ctype_digit($timestamp) || abs(time() - (int) $timestamp) > TOLERANCIA_SEGUNDOS) { return false; }
// 2. Assina `timestamp.corpo_bruto` (os bytes recebidos, sem reserializar). $esperada = 'sha256=' . hash_hmac('sha256', $timestamp . '.' . $corpoBruto, $secret); return hash_equals($esperada, $assinatura);}
// php://input, e não json_decode antes: a assinatura é calculada sobre o corpo bruto.$corpoBruto = file_get_contents('php://input');$ok = validaAssinatura( $corpoBruto, $_SERVER['HTTP_X_WEBHOOK_SIGNATURE'] ?? '', $_SERVER['HTTP_X_WEBHOOK_TIMESTAMP'] ?? '', getenv('RAPID_WEBHOOK_SECRET'));if (!$ok) { http_response_code(401); exit;}
$evento = json_decode($corpoBruto, true);// Processe o $evento (de preferência numa fila) e responda rápido.http_response_code(200);Rotação de secret
Seção intitulada “Rotação de secret”A rotação é feita pelo suporte da Rapid, a pedido. A partir dela os webhooks novos já saem assinados com a chave nova, sem período de overlap: quem validar com a anterior passa a recusar. Combine a troca nos seus servidores e a rotação em sequência, de preferência em janela de baixo tráfego.
Salvar uma URL nova não mexe na chave.
Headers legados
Seção intitulada “Headers legados”No formato legacy, a Rapid envia os headers clientid e clientkey e não envia assinatura, exatamente como o sistema anterior.
clientid: <client_id>clientkey: <client_secret>No formato v2 esses headers não existem: validar por clientid/clientkey significaria receber a credencial de acesso à API em cada request (menos seguro que assinar o payload), então a autenticação da entrega é só X-Webhook-Signature. Quem está em legacy continua recebendo os headers por tempo indeterminado, para não quebrar integrações existentes.