Pular para o conteúdo principal

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").

Não achou o cartão Webhooks?

Webhooks são do plano Pro. No plano Básico o cartão não aparece. Veja Seu plano para mudar de plano.

Tela Webhooks com o cartão Planilha de vendas (Zapier), os eventos Lead criado e Mudou de etapa no funil, a chavinha e a linha Última entrega ok

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

  1. Clique em Novo webhook.
  2. Dê um Nome que você reconheça depois, por exemplo "Planilha de vendas".
  3. 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, como localhost ou 192.168..., são recusados).
  4. 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).
  5. 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.
  6. 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.
  7. Abra Entregas e confira: ali aparece cada tentativa, o código HTTP, o tempo de resposta e o que o outro sistema devolveu.
Teste antes de confiar

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.

Trate o segredo como uma senha

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

Webhook não manda mensagem para cliente

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çalhoConteúdo
Content-Typeapplication/json
User-AgentSafeMarketing-Webhooks/1.0
X-SafeMarketing-EventO tipo do evento, por exemplo lead.stage_changed
X-SafeMarketing-DeliveryUm UUID novo a cada tentativa (útil para log)
X-SafeMarketing-Signaturet=<unix>,v1=<hex>, veja Conferir a assinatura
O seu cabeçalho de autenticaçãoSe 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çalho X-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. actorType diz quem causou: system, user, agent (a IA), form ou integration. data.event.data muda conforme o tipo (no exemplo, a etapa de origem e a de destino; em eventos de formulário, formId, formName e, 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) e scoreBand (cold, warm ou hot). Campos sem valor vêm como null.

Formatos diferentes:

  • campaign.finished não é de um lead: data traz { "campaign": { "campaignId", "total", "sent", "failed", "results": [ ... ] } }, com o resultado por contato (customerFullName, phoneNumber, externalCustomerId, status sent/error, errorMessage).
  • O botão Testar envia type = webhook.test, com data contendo message, webhookId e webhookName. Responda 2xx a ele também.

Os eventos

EventoNome na telaQuando acontece
lead.createdLead criadoUm lead novo entrou (formulário, WhatsApp, anúncio, cadastro manual, planilha...).
lead.updatedDados do lead alteradosAlguém mudou nome, telefone, e-mail ou outro dado do lead.
lead.stage_changedMudou de etapa no funilO lead foi movido de coluna (ou de funil).
lead.deletedLead removidoO lead foi apagado.
lead.convertedVirou clienteO lead foi marcado como cliente.
form.step_viewedViu uma etapa do formulárioO visitante abriu uma etapa do formulário do site.
form.step_completedConcluiu uma etapa do formulárioO visitante passou de uma etapa para a seguinte.
form.field_filledPreencheu um campo do formulárioUm campo foi preenchido (uma vez por campo).
form.submittedEnviou o formulárioO visitante terminou e enviou.
form.abandonedAbandonou o formulárioO visitante saiu da página no meio do formulário.
form.errorTeve um erro no formulárioAlgo deu errado no formulário do visitante.
form.customAção no formulárioUm passo próprio de um formulário (por exemplo, escolheu um plano).
meeting.scheduledAgendou reuniãoUma reunião foi marcada.
meeting.rescheduledRemarcou reuniãoA reunião mudou de data ou horário.
meeting.canceledCancelou reuniãoA reunião foi cancelada.
meeting.heldReunião realizadaA reunião aconteceu.
meeting.no_showNão compareceuO lead faltou à reunião.
whatsapp.message_receivedMandou mensagem no WhatsAppO lead escreveu para a sua empresa.
whatsapp.message_sentRecebeu mensagem no WhatsAppA sua empresa mandou mensagem para o lead.
email.sentRecebeu e-mailUm e-mail foi enviado ao lead.
note.addedAnotaçãoAlguém escreveu uma anotação no lead.
campaign.finishedCampanha de envio finalizadaTerminou 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 com whsec_) sobre a string <t>.<corpo>: o valor de t, um ponto e o corpo exatamente como chegou (os bytes crus, antes de qualquer JSON.parse).

Para conferir:

  1. Separe t e v1 do cabeçalho.
  2. Recuse se t estiver a mais de 5 minutos do seu relógio. Isso impede que alguém capture uma entrega e a reenvie depois.
  3. Calcule HMAC-SHA256(segredo, t + "." + corpo_cru) em hexadecimal.
  4. Compare com v1 usando uma comparação de tempo constante. Se não bater, responda 401 e descarte.
Use o corpo cru

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, 4xx e 5xx) é 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:

TentativaQuando
Na hora (em segundos depois do evento)
1 minuto depois da falha
5 minutos depois
30 minutos depois
2 horas depois
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 os id já tratados e ignore os repetidos.
  • A ordem não é garantida. Uma nova tentativa pode chegar depois de um aviso mais recente. Use data.event.occurredAt para ordenar e, se precisar do estado atual, confie no retrato mais novo de data.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

Para ir além