Leaper Desenvolvedores
Central de Ajuda Entrar
Webhooks

Webhooks

Um webhook é um endereço do seu sistema que a Leaper chama sempre que algo acontece com um lead. Em vez de o seu sistema perguntar "tem novidade?" a cada poucos minutos, nós avisamos. Se o que você precisa é consultar ou alterar dados quando quiser, veja a API de Integração. As duas coisas se completam: o webhook avisa, a API responde.

Abrir no ChatGPT ↗ Abrir no Claude ↗ Ver .md

Visão geral

Você cadastra um endereço de destino, escolhe quais eventos quer receber e usa a chave de assinatura para confirmar que a chamada veio mesmo da Leaper. Daí em diante o seu sistema é avisado sozinho — a cada lead novo, a cada mudança de etapa.

Toda entrega é um POST com corpo JSON e a mesma estrutura de blocos, qualquer que seja o evento. O que muda é o valor de event e quais blocos vêm preenchidos.

Quando a entrega acontece

O evento entra numa fila e a entrega acontece logo em seguida, não no exato instante do acontecimento. Na prática isso significa duas coisas:

  • O corpo é montado na hora do envio. Se o lead foi enriquecido nesse intervalo — o nome chegou depois, os dados de campanha foram resolvidos — você já recebe a versão atual.
  • O bloco status descreve a transição que gerou o evento, mesmo que o lead tenha mudado de etapa de novo nesse meio tempo. Ele continua contando o que aconteceu.

Ativar seu webhook

A configuração fica no painel, em Configurações → Webhooks. São três coisas: o endereço, os eventos e a chave de assinatura que geramos para você.

O endereço de destino

Precisa aceitar POST com corpo JSON, estar acessível pela internet e usar HTTPS. Para os primeiros testes, serviços gratuitos como o Webhook.site já mostram os dados chegando.

Os eventos

Marque só o que o seu sistema vai tratar. Evento que você não usa gera chamada desnecessária e deixa o histórico mais difícil de ler quando precisar investigar algo. Os quatro disponíveis estão na referência.

A chave de assinatura

Cada webhook tem uma chave própria, gerada automaticamente. É com ela que o seu sistema confirma que a chamada veio da Leaper. Veja em Ver secret, copie e guarde num cofre de segredos ou numa variável de ambiente — evite deixá-la escrita no código. Ela continua disponível no painel, não é exibida uma única vez.

Cabeçalhos extras, se precisar

Se o seu endpoint exige autenticação própria, dá para cadastrar cabeçalhos que acompanham toda chamada. Os três que a Leaper define — Content-Type, X-Webhook-Event e X-Webhook-Signature — não podem ser sobrescritos.

São até 3 webhooks por empresa, cada um com seu endereço e sua lista de eventos. Dá para separar produção de testes, ou mandar cada tipo de evento para um sistema diferente. Um webhook pode ser pausado a qualquer momento sem perder a configuração; enquanto estiver pausado, nada é entregue.

Eventos que se sobrepõem

Alguns eventos descrevem o mesmo acontecimento sob ângulos diferentes, e assinar os dois faz o seu endpoint receber duas chamadas:

  • Conversão. Um lead convertido também mudou de status. Com status_change e lead_converted marcados, chegam duas chamadas — uma de cada evento.
  • Formulário de contato novo. Um envio feito por quem ainda não estava na base também cria o lead, então dispara form_submission e new_lead.

A sobreposição é intencional: ela deixa você tratar só a venda ganha sem olhar código de etapa. Se os dois eventos levam ao mesmo processamento no seu lado, assine apenas um.

Verificar a assinatura

Toda chamada vai assinada. Verificar a assinatura é o que garante que a requisição veio da Leaper e que ninguém alterou o conteúdo no caminho — sem isso, qualquer pessoa que descubra a URL do seu endpoint pode enviar dados falsos para ele.

CabeçalhoConteúdo
Content-Typeapplication/json
X-Webhook-EventNome do evento. Permite rotear sem abrir o corpo
X-Webhook-Signaturesha256= seguido do HMAC-SHA256 do corpo, em hexadecimal, usando o secret do webhook

Calcule o HMAC sobre os bytes exatos que chegaram, antes de qualquer conversão para objeto. Se você fizer JSON.parse e depois JSON.stringify de novo, a ordem das chaves e os espaços podem mudar e a assinatura nunca vai bater. Compare sempre com uma função de tempo constante, nunca com ==.

Node.js — Express
const crypto = require("crypto");

app.post(
  "/integracoes/leaper",
  express.raw({ type: "application/json" }),   // mantém o corpo cru
  (req, res) => {
    const recebida = req.header("X-Webhook-Signature") || "";
    const esperada =
      "sha256=" +
      crypto.createHmac("sha256", process.env.LEAPER_WEBHOOK_SECRET)
        .update(req.body)
        .digest("hex");

    const a = Buffer.from(recebida);
    const b = Buffer.from(esperada);
    if (a.length !== b.length || !crypto.timingSafeEqual(a, b)) {
      return res.sendStatus(401);
    }

    const evento = JSON.parse(req.body.toString("utf8"));
    fila.enfileirar(evento);        // processe fora do ciclo da requisição
    res.sendStatus(200);            // responda rápido
  }
);
PHP
$corpo    = file_get_contents("php://input");
$recebida = $_SERVER["HTTP_X_WEBHOOK_SIGNATURE"] ?? "";
$esperada = "sha256=" . hash_hmac("sha256", $corpo, getenv("LEAPER_WEBHOOK_SECRET"));

if (!hash_equals($esperada, $recebida)) {
    http_response_code(401);
    exit;
}

$evento = json_decode($corpo, true);
enfileirar($evento);
http_response_code(200);
Python — Flask
import hmac, hashlib, os
from flask import request, abort

@app.post("/integracoes/leaper")
def leaper_webhook():
    corpo    = request.get_data()          # bytes crus
    recebida = request.headers.get("X-Webhook-Signature", "")
    esperada = "sha256=" + hmac.new(
        os.environ["LEAPER_WEBHOOK_SECRET"].encode(),
        corpo,
        hashlib.sha256,
    ).hexdigest()

    if not hmac.compare_digest(esperada, recebida):
        abort(401)

    enfileirar(request.get_json())
    return "", 200

Entrega e tentativas

RegraComportamento
MétodoPOST com corpo JSON
SucessoQualquer resposta 2xx. O corpo da sua resposta é ignorado
FalhaQualquer outro código, erro de conexão ou estouro de tempo
Tempo limite10 segundos para a resposta completa, 5 para estabelecer a conexão
TentativasAté 3 no total. Esgotadas, o evento é marcado como falho e não é reenviado
RedirecionamentosSeguidos, até 3 saltos, só em http e https
Webhook pausadoEnquanto estiver pausado, nada é entregue nem guardado para depois

O tempo limite de 10 segundos vale para a resposta inteira. Se o seu endpoint grava em CRM, dispara e-mail e chama outras APIs antes de responder, ele vai estourar o limite e receber a mesma entrega de novo. Valide a assinatura, guarde o evento numa fila sua, responda 200 e processe depois.

Descartar entrega repetida

Uma entrega pode chegar mais de uma vez: a tentativa que estourou o seu tempo limite pode ter sido processada do seu lado mesmo assim. O campo event_id identifica a entrega e repete nas retentativas da mesma entrega — guarde os que você já processou e descarte o que voltar.

lead_converted · corpo
{
  "event": "lead_converted",
  "event_id": "9b35e0c7-4a1d-4f82-b6e9-3c08d7152fa4",
  "timestamp": "2026-09-03T10:08:33Z",
  "company": {
    "id": "6201138c-fc0a-4478-820f-8907907e7f00",
    "whatsapp_phone": null
  },
  "lead": {
    "id": "b7d4e1a2-8f3c-4a19-9c62-1e5f0a7d3b84",
    "name": "Marina Alves",
    "phone": "5511987654321",
    "email": "marina.alves@exemplo.com.br",
    "source": "METAADS_MSG",
    "tags": "prioridade-alta",
    "created_at": "2026-09-02 09:14:50",
    "created_from": null,
    "lead_value": "3200"
  },
  "status": {
    "current": {
      "code": "END_WON",
      "name": "Finalizado - Convertido",
      "changed_at": "2026-09-03 10:08:31"
    },
    "previous": {
      "code": "NEGOCIACAO",
      "name": "Negociação",
      "changed_at": "2026-09-02 15:41:05"
    }
  },
  "tracking": {
    "source": "METAADS_MSG",
    "sourceApp": "instagram",
    "sourceUrl": "https://www.instagram.com/",
    "sourceType": "ad",
    "utmSource": null,
    "utmMedium": null,
    "utmCampaign": null,
    "utmContent": null,
    "utmTerm": null,
    "referer": null,
    "pageUrl": null,
    "device": null,
    "browser": null,
    "os": null,
    "ipAddress": null,
    "userAgent": null,
    "country": null,
    "clickId": "ARAaZv8x1QpKm3nLd0TbYq",
    "clickIdType": "ctwa_clid",
    "clickDate": null,
    "campaignData": null,
    "formData": null,
    "journey": null
  },
  "formSubmissions": []
}

Eventos diferentes que descrevem o mesmo fato têm event_id diferentes, porque são duas entregas. É o caso de uma conversão para quem assina status_change e lead_converted.

Boas práticas

  • Use o code, não o name. Os nomes das etapas podem ser renomeados a qualquer momento no painel, e isso quebraria qualquer regra escrita em cima deles.
  • Descarte repetição pelo event_id, e deixe o processamento idempotente.
  • Aceite campos novos e valores nulos. Podemos acrescentar campos ao corpo sem aviso; ignore o que o seu sistema não conhece em vez de rejeitar a entrega. Qualquer campo pode vir null quando o dado não existe.
  • Assine sempre. Um endpoint que aceita qualquer requisição é um endpoint que qualquer pessoa pode alimentar. A verificação são cinco linhas de código.
  • Prefira um webhook por finalidade. Separar por destino é mais simples de operar do que um endereço único que recebe tudo e distribui internamente, e permite pausar uma integração sem afetar as outras.

Acompanhar entregas

O painel guarda o histórico do que foi enviado, com filtros por webhook, por lead, por evento, por situação e por período. É por ali que se descobre, por exemplo, que as entregas pararam porque o certificado do servidor venceu.

SituaçãoO que significa
Na filaO evento foi registrado e aguarda o envio ou uma nova tentativa
EntregueO seu sistema recebeu e confirmou. Nada a fazer
FalhouAs tentativas se esgotaram sem confirmação, ou o webhook estava pausado na hora do envio

Ao lado de cada tentativa fica o registro do que aconteceu — o código que o seu servidor devolveu ou o erro de conexão. Um 401 costuma ser cabeçalho de autenticação faltando, um 404 é endereço errado, e tempo esgotado quase sempre significa que o endpoint está processando antes de responder.