# 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](https://leaper.com.br/desenvolvedores/guia/). As duas coisas se completam: o webhook avisa, a API responde.

## 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](https://leaper.com.br/desenvolvedores/webhooks-referencia/#eventos).

### 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çalho | Conteúdo |
|---|---|
| `Content-Type` | `application/json` |
| `X-Webhook-Event` | Nome do evento. Permite rotear sem abrir o corpo |
| `X-Webhook-Signature` | `sha256=` 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 `==`.

```javascript title="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 title="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 title="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

| Regra | Comportamento |
|---|---|
| Método | `POST` com corpo JSON |
| Sucesso | Qualquer resposta `2xx`. O corpo da sua resposta é ignorado |
| Falha | Qualquer outro código, erro de conexão ou estouro de tempo |
| Tempo limite | 10 segundos para a resposta completa, 5 para estabelecer a conexão |
| Tentativas | Até 3 no total. Esgotadas, o evento é marcado como falho e não é reenviado |
| Redirecionamentos | Seguidos, até 3 saltos, só em `http` e `https` |
| Webhook pausado | Enquanto 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.

```json title="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ção | O que significa |
|---|---|
| Na fila | O evento foi registrado e aguarda o envio ou uma nova tentativa |
| Entregue | O seu sistema recebeu e confirmou. Nada a fazer |
| Falhou | As 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.
