API v1 · Guia
Webhooks
Receba os eventos do Rumo CRM e confirme a assinatura Rumo-Signature em Node, Python ou PHP.
Quando algo acontece no Rumo CRM, um POST com o evento em JSON chega ao endereço que você
cadastrou em Configurações → Integrações → Técnico → Webhooks.
Como confirmar que o aviso veio do Rumo CRM
Todo webhook com chave secreta leva o cabeçalho Rumo-Signature:
Rumo-Signature: t=1791547200,v1=9f2c5b0e7d1a4c3b8e6f0a2d4c6b8e0f1a3c5e7d9b1f3a5c7e9d1b3f5a7c9e1dté a hora do envio, em segundos desde 1970 (UTC).v1é o HMAC-SHA256, em hexadecimal, do texto<t>.<corpo>: o valor det, um ponto e o corpo da requisição exatamente como chegou.
Para conferir:
- Leia o corpo cru, antes de converter para JSON. Qualquer espaço ou ordem de campo diferente muda a assinatura.
- Separe o cabeçalho pelas vírgulas e cada parte no primeiro
=. Guarde ote todos osv1. Ignore partes que você não conhece: no futuro pode haver mais de umv1(por exemplo, uma assinatura por chave) e outros esquemas. - Recuse se
testiver a mais de 5 minutos (300 segundos) do seu relógio, para trás ou para frente. É isso que impede alguém de capturar um aviso e reenviar depois. - Calcule o HMAC-SHA256 de
<t>.<corpo>com a sua chave. A chave é o texto de 64 caracteres que apareceu ao criar o webhook: use como texto, sem decodificar o hexadecimal. - Compare com cada
v1usando comparação de tempo constante. Basta um bater. - Responda
2xxe processe. Se oiddo evento já foi processado, responda2xxe ignore: a mesma entrega pode chegar mais de uma vez.
Mantenha o relógio do servidor sincronizado (NTP). Um relógio adiantado ou atrasado em mais de 5 minutos recusa avisos verdadeiros.
Node.js
const crypto = require("node:crypto");
const TOLERANCIA_SEGUNDOS = 5 * 60;
// corpo: o corpo cru (Buffer ou string). cabecalho: o valor de Rumo-Signature.
function verificarAssinaturaRumo(corpo, cabecalho, segredo, agora = Math.floor(Date.now() / 1000)) {
let timestamp = NaN;
const assinaturas = [];
for (const parte of String(cabecalho || "").split(",")) {
const separador = parte.indexOf("=");
const chave = parte.slice(0, separador).trim();
const valor = parte.slice(separador + 1).trim();
if (chave === "t" && /^\d+$/.test(valor)) timestamp = Number(valor);
if (chave === "v1" && valor) assinaturas.push(valor);
}
if (!Number.isInteger(timestamp) || assinaturas.length === 0) return false;
if (Math.abs(agora - timestamp) > TOLERANCIA_SEGUNDOS) return false;
const esperada = crypto.createHmac("sha256", segredo).update(`${timestamp}.`).update(corpo).digest();
return assinaturas.some((hex) => {
const recebida = Buffer.from(hex, "hex");
return recebida.length === esperada.length && crypto.timingSafeEqual(recebida, esperada);
});
}Com Express, receba o corpo cru só nesta rota:
app.post("/webhooks/rumo", express.raw({ type: "application/json" }), (req, res) => {
if (!verificarAssinaturaRumo(req.body, req.get("Rumo-Signature"), process.env.RUMO_WEBHOOK_SECRET)) {
return res.status(400).send("assinatura inválida");
}
const evento = JSON.parse(req.body);
// Guarde evento.id e ignore repetidos.
res.sendStatus(200);
});Python
import hashlib
import hmac
import time
TOLERANCIA_SEGUNDOS = 5 * 60
def verificar_assinatura_rumo(corpo: bytes, cabecalho: str, segredo: str, agora: int | None = None) -> bool:
timestamp, assinaturas = None, []
for parte in (cabecalho or "").split(","):
chave, _, valor = parte.strip().partition("=")
if chave == "t" and valor.isdigit():
timestamp = int(valor)
elif chave == "v1" and valor:
assinaturas.append(valor)
if timestamp is None or not assinaturas:
return False
agora = int(time.time()) if agora is None else agora
if abs(agora - timestamp) > TOLERANCIA_SEGUNDOS:
return False
esperada = hmac.new(segredo.encode(), f"{timestamp}.".encode() + corpo, hashlib.sha256).hexdigest()
return any(hmac.compare_digest(esperada, recebida) for recebida in assinaturas)Com Flask, o corpo cru é request.get_data() e o cabeçalho, request.headers.get("Rumo-Signature").
PHP
function verificarAssinaturaRumo(string $corpo, string $cabecalho, string $segredo, ?int $agora = null): bool
{
$timestamp = null;
$assinaturas = [];
foreach (explode(',', $cabecalho) as $parte) {
[$chave, $valor] = array_pad(explode('=', trim($parte), 2), 2, '');
if ($chave === 't' && ctype_digit($valor)) {
$timestamp = (int) $valor;
} elseif ($chave === 'v1' && $valor !== '') {
$assinaturas[] = $valor;
}
}
if ($timestamp === null || $assinaturas === []) {
return false;
}
if (abs(($agora ?? time()) - $timestamp) > 300) {
return false;
}
$esperada = hash_hmac('sha256', $timestamp . '.' . $corpo, $segredo);
foreach ($assinaturas as $recebida) {
if (hash_equals($esperada, $recebida)) {
return true;
}
}
return false;
}
$corpo = file_get_contents('php://input');
$cabecalho = $_SERVER['HTTP_RUMO_SIGNATURE'] ?? '';
if (!verificarAssinaturaRumo($corpo, $cabecalho, getenv('RUMO_WEBHOOK_SECRET'))) {
http_response_code(400);
exit;
}Cabeçalho antigo
X-Webhook-Signature: sha256=<hex> continua saindo igual: HMAC-SHA256 só do corpo, com a mesma
chave. Ele não leva hora, então não protege contra reenvio. Integrações novas devem usar
Rumo-Signature.
Entrega e novas tentativas
- O Rumo CRM espera a resposta por até 10 segundos. Qualquer status fora de
2xx, erro de conexão ou demora maior conta como falha. - Redirecionamento (
3xx) não é seguido: conta como falha e entra nas novas tentativas. Cadastre a URL final. Endereço de rede interna é recusado, inclusive quando o domínio passa a apontar para um. - São até 8 tentativas. Depois de cada falha, a próxima sai em cerca de 1 min, 5 min, 30 min, 2 h, 4 h, 7 h e 10 h (variação de até 10% para cima ou para baixo). A última tentativa sai cerca de 24 horas depois da primeira. Esgotadas as 8, a entrega fica como Falhou.
- Cada tentativa é assinada na hora:
tev1mudam, o corpo e oidnão. - A ordem de chegada não é garantida. Use
timestampdo corpo para ordenar. - Em Histórico de entregas, Tentar agora reenvia uma entrega que não foi confirmada, com assinatura nova. A contagem de tentativas recomeça.
- Um endereço sem nenhuma entrega confirmada por 5 dias é desativado automaticamente e os gerentes da empresa recebem um aviso no sino. Enquanto estiver desativado, os eventos não são guardados. Corrija o endereço e ative de novo em Configurações.
Troca de chave
Gerar nova chave troca a chave na hora: a anterior para de valer no mesmo instante, sem período em que as duas valem. Os avisos que o seu sistema recusar enquanto você atualiza a chave voltam nas próximas tentativas, já assinados com a chave nova. Atualize dentro do período de novas tentativas (cerca de 24 horas) e nada se perde.
Teste
Testar webhook envia {"event": "test", "timestamp": ..., "data": {...}} com os mesmos
cabeçalhos de assinatura. Esse aviso não tem id, não leva X-Webhook-Id nem X-Webhook-Attempt
e não entra no histórico.
Eventos
O nome do evento vem no campo event do corpo e no cabeçalho X-Webhook-Event. Escolha quais receber ao cadastrar o endereço.
campaign.startedcampaign.completedcampaign.pausedmessage.sentmessage.failedreply.receivedform.submittedform.qualifiedform.disqualifiedform.abandonedbooking.confirmedbooking.canceledcontact.createddeal.createddeal.stage_changeddeal.wondeal.lost