Webhooks
Resumo
Webhook é para quem tem outro sistema e quer que ele saiba o que acontece com os leads: uma planilha, um ERP ou uma ferramenta de automação (Zapier, Make, n8n). Se você não tem outro sistema, pode ignorar esta tela.
A primeira metade deste artigo é para quem cadastra o webhook no SafeMarketing. A segunda, Para desenvolvedores, é para quem vai programar o sistema que recebe os avisos.
Onde fica
No menu lateral, grupo Integrações, clique em Integrações e depois no cartão Webhooks ("Avisar outro sistema quando algo acontecer aqui").
Webhooks são do plano Pro. No plano Básico o cartão não aparece. Veja Seu plano para mudar de plano.
No alto, a tela explica o que ela faz: envia os eventos dos leads para outro sistema, cada entrega vai assinada (cabeçalho X-SafeMarketing-Signature) e, se falhar, é repetida automaticamente.
O que aparece em cada cartão
- O nome que você deu e o endereço que vai receber os avisos.
- As fichinhas dos eventos escolhidos, por exemplo "Lead criado" e "Mudou de etapa no funil".
- A chavinha de ligar e pausar (Ativo / Pausado).
- A linha de situação, por exemplo "Última entrega ok · há 3 h", "Última entrega falhou" ou "Nenhuma entrega ainda.".
- As ações Entregas, Testar, Editar e a lixeira (Remover). Ao remover, o endereço deixa de receber os eventos na hora.
Criar um webhook
- Clique em Novo webhook.
- Dê um Nome que você reconheça depois, por exemplo "Planilha de vendas".
- Em URL que recebe os eventos, cole o endereço que o outro sistema forneceu. Ele precisa começar com
https://e apontar para um endereço público (endereços locais, comolocalhostou192.168..., são recusados). - Em Eventos, marque os que interessam, por exemplo Lead criado, Mudou de etapa no funil, Agendou reunião, Virou cliente ou Campanha de envio finalizada (o fim de um envio em lote feito pela tela Clientes).
- Se o outro sistema pedir uma senha, preencha Autenticação (opcional): o Cabeçalho (por exemplo,
Authorization) e o Valor (por exemplo,Bearer ...). Os dois juntos, ou nenhum. - Clique em Criar webhook e, no cartão, em Testar. O sistema manda uma entrega de exemplo para o endereço e diz se ela chegou ("Teste entregue", com o código HTTP e o tempo, ou "O teste falhou", com o motivo). O teste funciona até com o webhook pausado.
- Abra Entregas e confira: ali aparece cada tentativa, o código HTTP, o tempo de resposta e o que o outro sistema devolveu.
Testar existe justamente para isso: você vê a entrega chegar do outro lado sem precisar esperar um lead de verdade.
O segredo da assinatura
O Segredo da assinatura aparece quando você abre Editar num webhook já criado. Copie (ícone Copiar) e passe para quem cuida do outro sistema: é com ele que o sistema confere que o aviso veio mesmo da SafeMarketing.
O segredo é gerado pelo SafeMarketing (você não escolhe). Se ele vazar, clique em Gerar novo: o segredo antigo para de valer na hora, e o outro sistema precisa ser atualizado com o novo, senão vai recusar as entregas.
Não cole o segredo em e-mail aberto, grupo de WhatsApp ou planilha compartilhada. Passe só para quem programa o sistema que recebe.
Quando a entrega falha
Se o outro sistema estiver fora do ar ou responder com erro, o SafeMarketing tenta de novo sozinho, com intervalos cada vez maiores. Se todas as tentativas falharem, ele desiste daquele aviso, e a falha fica registrada em Entregas, com o motivo. Os horários exatos estão em Novas tentativas.
O que webhook não faz
Nenhum cliente recebe nada por causa de um webhook. Ele só avisa outro sistema. Para mandar mensagem para pessoas, use Automações ou os envios em lote.
Webhook também é só de saída: ele avisa o seu sistema, mas não recebe dados dele. Para leads entrarem no SafeMarketing a partir do seu site, use o formulário no seu site.
Para desenvolvedores
Esta parte é para quem vai programar o endpoint que recebe os avisos.
A requisição
Cada entrega é um POST com corpo JSON para a URL cadastrada:
| Cabeçalho | Conteúdo |
|---|---|
Content-Type | application/json |
User-Agent | SafeMarketing-Webhooks/1.0 |
X-SafeMarketing-Event | O tipo do evento, por exemplo lead.stage_changed |
X-SafeMarketing-Delivery | Um UUID novo a cada tentativa (útil para log) |
X-SafeMarketing-Signature | t=<unix>,v1=<hex>, veja Conferir a assinatura |
| O seu cabeçalho de autenticação | Se você preencheu Autenticação (opcional), ele vai em toda entrega |
Nomes de cabeçalho começando com X-SafeMarketing- são reservados e não podem ser usados na autenticação.
O corpo
Os eventos de lead chegam neste formato:
{
"id": "0b8f6a3e-6c1d-4e0a-9d7b-2f1c5a9e8b41",
"type": "lead.stage_changed",
"createdAt": "2026-09-17T10:00:00.000Z",
"storeCode": "1785271125408",
"data": {
"event": {
"id": "5d2e9c10-3b7a-4f6e-8a21-9c4d7e0f1b32",
"type": "lead.stage_changed",
"occurredAt": "2026-09-17T10:00:00.000Z",
"actorType": "user",
"actorId": null,
"data": {
"fromStageId": "a1f0c2d4-...",
"fromStageName": "Novo",
"fromStageKind": "open",
"toStageId": "b7e3d9a1-...",
"toStageName": "Reunião agendada",
"toStageKind": "open",
"fromPipelineId": "c4d8e2f6-...",
"toPipelineId": "c4d8e2f6-..."
}
},
"lead": {
"id": "9e4b1c7a-2d3f-4a8b-b6c5-1f0e9d8c7b6a",
"contactName": "Mariana Souza",
"email": "mariana@exemplo.com.br",
"contactPhone": "5511987654321",
"source": "form",
"pipelineId": "c4d8e2f6-...",
"stageId": "b7e3d9a1-...",
"score": 7,
"scoreBand": "warm"
}
}
}
id: o identificador do aviso. É o mesmo em todas as tentativas do mesmo aviso. Use-o para ignorar duplicados.type: o evento (igual ao cabeçalhoX-SafeMarketing-Event).createdAt: quando o aviso foi gerado (ISO 8601, UTC).storeCode: o código da sua empresa no SafeMarketing.data.event: o evento da linha do tempo do lead.actorTypediz quem causou:system,user,agent(a IA),formouintegration.data.event.datamuda conforme o tipo (no exemplo, a etapa de origem e a de destino; em eventos de formulário,formId,formNamee, quando houver, a etapa e o campo).data.lead: o retrato do lead no momento do evento, para você não precisar consultar nada:id,contactName,email,contactPhone,source,pipelineId,stageId,score(a temperatura em pontos) escoreBand(cold,warmouhot). Campos sem valor vêm comonull.
Formatos diferentes:
campaign.finishednão é de um lead:datatraz{ "campaign": { "campaignId", "total", "sent", "failed", "results": [ ... ] } }, com o resultado por contato (customerFullName,phoneNumber,externalCustomerId,statussent/error,errorMessage).- O botão Testar envia
type=webhook.test, comdatacontendomessage,webhookIdewebhookName. Responda 2xx a ele também.
Os eventos
| Evento | Nome na tela | Quando acontece |
|---|---|---|
lead.created | Lead criado | Um lead novo entrou (formulário, WhatsApp, anúncio, cadastro manual, planilha...). |
lead.updated | Dados do lead alterados | Alguém mudou nome, telefone, e-mail ou outro dado do lead. |
lead.stage_changed | Mudou de etapa no funil | O lead foi movido de coluna (ou de funil). |
lead.deleted | Lead removido | O lead foi apagado. |
lead.converted | Virou cliente | O lead foi marcado como cliente. |
form.step_viewed | Viu uma etapa do formulário | O visitante abriu uma etapa do formulário do site. |
form.step_completed | Concluiu uma etapa do formulário | O visitante passou de uma etapa para a seguinte. |
form.field_filled | Preencheu um campo do formulário | Um campo foi preenchido (uma vez por campo). |
form.submitted | Enviou o formulário | O visitante terminou e enviou. |
form.abandoned | Abandonou o formulário | O visitante saiu da página no meio do formulário. |
form.error | Teve um erro no formulário | Algo deu errado no formulário do visitante. |
form.custom | Ação no formulário | Um passo próprio de um formulário (por exemplo, escolheu um plano). |
meeting.scheduled | Agendou reunião | Uma reunião foi marcada. |
meeting.rescheduled | Remarcou reunião | A reunião mudou de data ou horário. |
meeting.canceled | Cancelou reunião | A reunião foi cancelada. |
meeting.held | Reunião realizada | A reunião aconteceu. |
meeting.no_show | Não compareceu | O lead faltou à reunião. |
whatsapp.message_received | Mandou mensagem no WhatsApp | O lead escreveu para a sua empresa. |
whatsapp.message_sent | Recebeu mensagem no WhatsApp | A sua empresa mandou mensagem para o lead. |
email.sent | Recebeu e-mail | Um e-mail foi enviado ao lead. |
note.added | Anotação | Alguém escreveu uma anotação no lead. |
campaign.finished | Campanha de envio finalizada | Terminou um envio em lote feito pela tela Clientes. |
Os nomes dos eventos são estáveis: podem surgir eventos novos, mas os existentes não mudam de nome.
Conferir a assinatura
O cabeçalho X-SafeMarketing-Signature tem duas partes:
X-SafeMarketing-Signature: t=1789639200,v1=5f2b1c...e9a0
t: o momento da entrega, em segundos Unix.v1: o HMAC-SHA256, em hexadecimal, calculado com o Segredo da assinatura (o texto inteiro, começando comwhsec_) sobre a string<t>.<corpo>: o valor det, um ponto e o corpo exatamente como chegou (os bytes crus, antes de qualquerJSON.parse).
Para conferir:
- Separe
tev1do cabeçalho. - Recuse se
testiver a mais de 5 minutos do seu relógio. Isso impede que alguém capture uma entrega e a reenvie depois. - Calcule
HMAC-SHA256(segredo, t + "." + corpo_cru)em hexadecimal. - Compare com
v1usando uma comparação de tempo constante. Se não bater, responda401e descarte.
Se o seu framework já transformou o corpo em objeto e você fizer JSON.stringify de novo, a assinatura não bate (espaços, ordem e acentos podem mudar). Leia o corpo como texto ou bytes antes de interpretar.
Node.js (Express):
const express = require('express');
const crypto = require('crypto');
const SECRET = process.env.SAFEMARKETING_WEBHOOK_SECRET; // whsec_...
const TOLERANCE_SECONDS = 5 * 60;
function verifySignature(header, rawBody, secret) {
if (!header) return false;
const parts = Object.fromEntries(
header.split(',').map((kv) => {
const i = kv.indexOf('=');
return [kv.slice(0, i).trim(), kv.slice(i + 1).trim()];
}),
);
const t = Number(parts.t);
const v1 = parts.v1;
if (!Number.isInteger(t) || !v1) return false;
// Entrega antiga demais (ou do futuro): recusa.
const now = Math.floor(Date.now() / 1000);
if (Math.abs(now - t) > TOLERANCE_SECONDS) return false;
const expected = crypto
.createHmac('sha256', secret)
.update(`${t}.${rawBody}`)
.digest('hex');
const a = Buffer.from(expected, 'hex');
const b = Buffer.from(v1, 'hex');
return a.length === b.length && crypto.timingSafeEqual(a, b);
}
const app = express();
// express.raw: o corpo chega como Buffer, sem ser interpretado.
app.post('/webhooks/safemarketing', express.raw({ type: 'application/json' }), (req, res) => {
const rawBody = req.body.toString('utf8');
if (!verifySignature(req.get('X-SafeMarketing-Signature'), rawBody, SECRET)) {
return res.status(401).send('assinatura inválida');
}
const payload = JSON.parse(rawBody);
// Responda rápido; o trabalho pesado vai para uma fila sua.
res.status(200).send('ok');
// Ex.: ignore payload.id se já processou, depois trate payload.type.
});
app.listen(3000);
Python (Flask):
import hashlib, hmac, os, time
from flask import Flask, request, abort
SECRET = os.environ["SAFEMARKETING_WEBHOOK_SECRET"].encode() # whsec_...
TOLERANCE_SECONDS = 5 * 60
app = Flask(__name__)
def verify(header: str, raw_body: bytes) -> bool:
try:
parts = dict(p.strip().split("=", 1) for p in header.split(","))
t, v1 = int(parts["t"]), parts["v1"]
except (AttributeError, KeyError, ValueError):
return False
if abs(time.time() - t) > TOLERANCE_SECONDS:
return False
expected = hmac.new(SECRET, f"{t}.".encode() + raw_body, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, v1)
@app.post("/webhooks/safemarketing")
def safemarketing():
raw = request.get_data() # bytes crus
if not verify(request.headers.get("X-SafeMarketing-Signature", ""), raw):
abort(401)
event = request.get_json()
# ignore event["id"] se já processou; depois trate event["type"]
return "ok", 200
O que conta como entregue
- Só uma resposta 2xx conta como sucesso. Qualquer outro código (inclusive
3xx,4xxe5xx) é falha. - Redirecionamento não é seguido. Um
301/302é tratado como falha: cadastre a URL final. - O limite de espera é de 10 segundos. Sem resposta nesse tempo, a tentativa falha ("Sem resposta em 10s"). Responda logo e processe depois, numa fila sua.
- O que você devolver no corpo da resposta aparece (os primeiros 500 caracteres) em Entregas, o que ajuda a depurar.
Novas tentativas
Se a entrega falhar, o mesmo aviso é reenviado com esta espera entre as tentativas:
| Tentativa | Quando |
|---|---|
| 1ª | Na hora (em segundos depois do evento) |
| 2ª | 1 minuto depois da falha |
| 3ª | 5 minutos depois |
| 4ª | 30 minutos depois |
| 5ª | 2 horas depois |
| 6ª | 6 horas depois |
Se a última também falhar, o aviso fica como falhou e não é reenviado. Cada tentativa tem um X-SafeMarketing-Delivery e um t novos, mas o id do corpo é o mesmo. Se você tem mais de um webhook, só o que falhou recebe de novo.
Duplicados e ordem
- Idempotência pelo
id. A sua resposta pode se perder depois de você já ter processado o aviso, e aí ele chega de novo. Guarde osidjá tratados e ignore os repetidos. - A ordem não é garantida. Uma nova tentativa pode chegar depois de um aviso mais recente. Use
data.event.occurredAtpara ordenar e, se precisar do estado atual, confie no retrato mais novo dedata.lead.
Não existe API de entrada
Webhooks são só de saída. Não há API pública com chave para criar ou alterar leads a partir de outro sistema. Para leads entrarem, use o formulário no seu site. Se você precisa de outra integração, escreva para contato@safemarketing.com.br.
E depois
- Automações: quando o que você quer é mandar mensagem, não avisar sistema.
- Auditoria dos envios: o histórico do que saiu para clientes.
Para ir além
- Montar o funil de vendas: as etapas que geram o evento "Mudou de etapa no funil".
- Captar leads sozinho: as portas que geram o evento "Lead criado".
- Formulário no seu site (código): a porta de entrada para quem tem site.