Webhooks
Como receber e validar webhooks do Hub — eventos suportados, payload canônico, assinatura HMAC SHA-256, rate limit e idempotência.
Webhooks
O Hub recebe eventos de provedores externos (CRM, formulários, integrações) em endpoints públicos versionados sob /api/v1/webhooks/{orgId}. Esta página descreve os tipos de eventos suportados, o formato dos payloads, a validação de assinatura HMAC SHA-256, política de rate limit e idempotência.
Base URL: https://hub.simplafy.com.br/api/v1
Os endpoints de webhook são públicos (sem autenticação de usuário). A proteção é feita por validação de assinatura HMAC quando o segredo está configurado para a organização, somada a rate limit por orgId e por provedor.
Tipos de eventos
O Hub expõe um endpoint por provedor. Cada provedor traz seu próprio conjunto de eventos, normalizado internamente em uma estrutura comum.
| Provedor | Endpoint | Eventos suportados |
|---|---|---|
| Pipefy CRM | POST /api/v1/webhooks/{orgId}/crm/pipefy | card.create, card.move, card.field_update, card.delete |
| Followize | POST /api/v1/webhooks/{orgId}/followize | Novo lead (template-driven, sem tipo discreto no payload do provedor) |
O parâmetro {orgId} é o identificador da organização Simplafy que recebe o evento. Webhooks endereçados a um orgId inexistente respondem 404 Not Found.
Eventos de tipo desconhecido (provedor Pipefy) são respondidos com 200 OK e o campo skipped: "unrecognized event". Eventos sem template ativo na organização (Followize) respondem 200 OK com message: "No active template". Em ambos os casos o provedor NÃO deve reenviar.
Payload canônico
Cada provedor entrega seu payload nativo no corpo application/json da requisição. O Hub interpreta o payload e o transforma internamente em um evento canônico antes de processar — o provedor não precisa adaptar nada.
Pipefy
Payloads de webhook Pipefy seguem o formato padrão da plataforma (data.action, data.card.id, data.card.pipe.id). O Hub usa o action para roteamento e o card.id como identificador externo do lead.
{
"data": {
"action": "card.move",
"card": {
"id": "12345678",
"title": "Nova Cotação",
"pipe": { "id": "302040506" }
},
"from": { "id": "p1", "name": "Inbox" },
"to": { "id": "p2", "name": "Em qualificação" }
}
}Webhooks de card.create do Pipefy não trazem valores de campos. Quando essa ação é recebida, o Hub faz uma chamada subsequente card(id) na API GraphQL do Pipefy para hidratar os dados — por isso o pipeline precisa ter credenciais do Pipefy configuradas para a organização.
Followize
O payload é o JSON nativo do Followize com contact, attendant, team, interests e tracking. O Hub achata a estrutura (contact.phone, attendant.name, interests.brand, etc.) e a usa para resolver o template configurado para a organização.
{
"id": "lead-abc-123",
"contact": {
"name": "Maria Silva",
"phone": "5511999998888",
"email": "[email protected]"
},
"attendant": { "name": "João" },
"team": { "name": "DMA" },
"interests": { "brand": "Toyota", "product": "Corolla" },
"tracking": { "source": "google", "medium": "cpc" }
}O telefone é obrigatório e pode vir em contact.phone, contact.cellphone ou no nível raiz phone. Requisições sem telefone respondem 400 Bad Request.
Assinatura HMAC (X-Signature)
Quando o segredo de webhook da organização está configurado, o Hub valida a assinatura HMAC SHA-256 do corpo bruto da requisição. Se o segredo não estiver configurado, o webhook é aceito sem validação (modo de compatibilidade).
Headers reconhecidos
| Provedor | Header |
|---|---|
| Pipefy | X-Pipefy-Signature |
| Followize | X-Webhook-Signature ou X-Hub-Signature-256 |
Ambos os formatos abaixo são aceitos para o valor do header:
sha256=3c2f8e7a91d4b5...
3c2f8e7a91d4b5...Algoritmo
A assinatura é o HMAC SHA-256 do corpo bruto da requisição (UTF-8), codificado em hexadecimal:
signature = hex( hmac_sha256( secret, raw_body ) )A comparação no servidor é feita com crypto.timingSafeEqual para evitar timing attacks. Buffers de tamanhos diferentes são rejeitados imediatamente como inválidos.
Gerar a assinatura
import crypto from "crypto";
function sign(rawBody: string, secret: string): string {
return crypto.createHmac("sha256", secret).update(rawBody, "utf8").digest("hex");
}
const body = JSON.stringify(payload);
const signature = sign(body, process.env.WEBHOOK_SECRET!);
await fetch("https://hub.simplafy.com.br/api/v1/webhooks/ORG_ID/followize", {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-Webhook-Signature": `sha256=${signature}`,
},
body,
});import hmac
import hashlib
import json
import requests
def sign(raw_body: str, secret: str) -> str:
return hmac.new(
secret.encode("utf-8"),
raw_body.encode("utf-8"),
hashlib.sha256,
).hexdigest()
body = json.dumps(payload)
signature = sign(body, WEBHOOK_SECRET)
requests.post(
"https://hub.simplafy.com.br/api/v1/webhooks/ORG_ID/followize",
headers={
"Content-Type": "application/json",
"X-Webhook-Signature": f"sha256={signature}",
},
data=body,
)BODY='{"id":"lead-abc","contact":{"phone":"5511999998888"}}'
SIG=$(printf '%s' "$BODY" | openssl dgst -sha256 -hmac "$WEBHOOK_SECRET" -hex | sed 's/^.* //')
curl -X POST https://hub.simplafy.com.br/api/v1/webhooks/ORG_ID/followize \
-H "Content-Type: application/json" \
-H "X-Webhook-Signature: sha256=$SIG" \
-d "$BODY"Respostas de falha
| Cenário | Status | Body |
|---|---|---|
| Header ausente (Followize, com segredo configurado) | 401 | { "error": "Missing webhook signature" } |
| Assinatura inválida ou em formato não reconhecido | 401 | { "error": "Invalid webhook signature" } |
| Assinatura inválida no endpoint Pipefy | 401 | { "error": "Invalid signature" } |
orgId não encontrado | 404 | { "error": "Not found" } |
O corpo da requisição precisa ser assinado exatamente como será enviado (mesma codificação, mesmos espaços, mesma ordem de chaves). Re-serializar o JSON entre a assinatura e o envio quebra a validação.
Retry e idempotência
Rate limit
Cada endpoint aplica rate limit por chave webhook:{provider}:{orgId}. Requisições acima do limite recebem:
HTTP/1.1 429 Too Many Requests
Content-Type: application/json
{ "error": "Rate limit exceeded" }Os provedores devem implementar retry com backoff exponencial ao receber 429. Recomenda-se intervalos crescentes de 1s, 5s, 30s, 2min, 10min.
Códigos para retry
| Status | O que fazer |
|---|---|
2xx | Sucesso — não reenviar. |
400 | Payload inválido (ex.: telefone faltando). Não reenviar sem ajuste. |
401 | Assinatura ausente ou inválida. Revisar segredo antes de reenviar. |
404 | orgId desconhecido. Não reenviar. |
429 | Rate limit. Reenviar com backoff. |
5xx | Erro transitório. Reenviar com backoff. |
Idempotência
O Hub deduplica eventos pelo identificador externo nativo de cada provedor:
- Pipefy:
card.idé a chave do lead na tabelaCrmLead(pipelineId + externalId). Eventoscard.move,card.field_updateecard.deletesão aplicados ao lead existente;card.createfaz upsert. - Followize: usa
payload.idquando presente, caindo para o telefone normalizado como fallback. O upsert é feito por(pipelineId, externalId).
Reentregas do mesmo evento são seguras — o estado final converge para o mesmo lead. Webhooks recebidos durante o horário comercial em organizações com time de atendimento humano (ex.: Brasal) podem responder 200 OK com skipped: "business_hours" sem efeito colateral; o provedor não deve reenviar.
Receber webhook (exemplo)
Exemplo mínimo de um receiver que valida assinatura HMAC, retorna 200 imediatamente e processa o evento em background.
import express from "express";
import crypto from "crypto";
const app = express();
const SECRET = process.env.WEBHOOK_SECRET!;
// IMPORTANTE: usar raw body, não express.json()
app.post(
"/webhooks/hub",
express.raw({ type: "application/json" }),
(req, res) => {
const rawBody = req.body.toString("utf8");
const header = req.header("x-webhook-signature") ?? "";
const received = header.replace(/^sha256=/i, "").toLowerCase();
const expected = crypto
.createHmac("sha256", SECRET)
.update(rawBody, "utf8")
.digest("hex");
const a = Buffer.from(expected, "hex");
const b = Buffer.from(received, "hex");
if (a.length !== b.length || !crypto.timingSafeEqual(a, b)) {
return res.status(401).json({ error: "Invalid signature" });
}
const event = JSON.parse(rawBody);
// Processar em background, responder rápido
queueMicrotask(() => handleEvent(event));
return res.status(200).json({ received: true });
},
);import hmac
import hashlib
import os
from fastapi import FastAPI, Header, HTTPException, Request, BackgroundTasks
app = FastAPI()
SECRET = os.environ["WEBHOOK_SECRET"]
@app.post("/webhooks/hub")
async def receive(
request: Request,
background_tasks: BackgroundTasks,
x_webhook_signature: str | None = Header(default=None),
):
raw_body = await request.body()
header = (x_webhook_signature or "").lower()
received = header[len("sha256="):] if header.startswith("sha256=") else header
expected = hmac.new(
SECRET.encode("utf-8"), raw_body, hashlib.sha256,
).hexdigest()
if not hmac.compare_digest(expected, received):
raise HTTPException(status_code=401, detail="Invalid signature")
event = await request.json()
background_tasks.add_task(handle_event, event)
return {"received": True}Sempre use o corpo bruto da requisição para validar a assinatura. Frameworks que parseiam o JSON antes (como express.json()) reescrevem a representação e invalidam a comparação.
Erros
Formato canônico de error response da Hub API, códigos HTTP utilizados, semântica de cada status e boas práticas para implementar handlers de erro resilientes em clientes.
Changelog
Registro de mudanças notáveis da Hub API — versões, correções e novidades publicadas em https://hub.simplafy.com.br/api/v1.