# Webhooks

Da documentação da API do Lero: https://lero.studio/docs#webhooks. Tudo num arquivo só: https://lero.studio/docs/llms-full.txt.

Webhooks são o Lero chamando o **seu** servidor quando algo acontece, em vez de você perguntar de tempos em tempos. Exemplo: a conversa do Conversational Agent terminou, e o seu sistema recebe na hora o transcript, a duração e o motivo do fim, para salvar no CRM, avaliar uma entrevista ou abrir um ticket.

1. Em [app.lero.studio/desenvolvedores](https://app.lero.studio/desenvolvedores?aba=webhooks), aba Webhooks, adicione o endereço `https` do seu servidor e escolha os eventos (até 5 webhooks por conta).
2. Copie o segredo do webhook (`whsec_...`) para o seu servidor.
3. Clique em **Enviar teste**: chega o evento `webhook.teste`.
4. No seu servidor, confira a assinatura e responda `2xx` em até 10 s.

| evento | quando |
|---|---|
| `agente.conversa.encerrada` | Uma conversa do Conversational Agent da sua conta terminou. Traz versão, LLM, voz, início, fim, duração, motivo e o transcript. |
| `webhook.teste` | Você clicou em Enviar teste. |

O Speech-to-Text responde na mesma chamada, sem webhook. Eventos de Voice Clone entram quando o produto sair.

Cada envio é um `POST` com `Content-Type: application/json` e os headers `Lero-Evento`, `Lero-Evento-Id` e `Lero-Assinatura: t=<unix>,v1=<hex>`. O `v1` é o HMAC-SHA256 de `"<t>.<corpo cru>"` com o segredo do webhook. Recuse quando a assinatura não bate ou quando `t` tem mais de 5 minutos.

```json
{
  "id": "6f1c2a9e-3b7d-4e58-9a0c-2d4f6b8e1a37",
  "evento": "agente.conversa.encerrada",
  "criado": "2026-11-11T15:42:10.512Z",
  "dados": {
    "conversa_id": "2eecec700b63",
    "versao": "v0.4",
    "llm": "oss120b",
    "voz": "rio",
    "lingua": "pt",
    "inicio": "2026-11-11T15:31:07Z",
    "fim": "2026-11-11T15:42:09Z",
    "duracao_s": 662.4,
    "motivo": "a pessoa desligou",
    "transcript": [
      { "papel": "agente", "texto": "Oi! Aqui é a Bia, da Acme.", "t": 1.2 },
      { "papel": "pessoa", "texto": "Oi, tudo bem?", "t": 4.8 }
    ]
  }
}
```

Conferir a assinatura (Node):

```javascript
import crypto from "node:crypto";

// Next.js route: POST /api/lero-webhook. LERO_WEBHOOK_SECRET = o segredo do webhook (whsec_...)
export async function POST(req) {
  const corpo = await req.text();                                  // o corpo cru, antes do JSON.parse
  const [t, v1] = (req.headers.get("lero-assinatura") || "").split(",").map((p) => p.split("=")[1]);
  const esperado = crypto.createHmac("sha256", process.env.LERO_WEBHOOK_SECRET).update(`${t}.${corpo}`).digest("hex");
  const ok = v1 && v1.length === esperado.length && crypto.timingSafeEqual(Buffer.from(v1), Buffer.from(esperado));
  if (!ok || Math.abs(Date.now() / 1000 - Number(t)) > 300) return new Response("assinatura inválida", { status: 401 });

  const evento = JSON.parse(corpo);
  if (evento.evento === "agente.conversa.encerrada") {
    // salve evento.dados.transcript, avalie a entrevista, abra o ticket...
  }
  return new Response("ok");                                       // responda 2xx rápido; o trabalho pesado vai para uma fila
}
```

Conferir a assinatura (Python):

```python
import hashlib, hmac, json, os, time

from flask import Flask, request

app = Flask(__name__)


@app.post("/lero-webhook")
def lero_webhook():
    corpo = request.get_data(as_text=True)                         # o corpo cru
    partes = dict(p.split("=", 1) for p in request.headers.get("Lero-Assinatura", "").split(",") if "=" in p)
    esperado = hmac.new(os.environ["LERO_WEBHOOK_SECRET"].encode(), f"{partes.get('t')}.{corpo}".encode(), hashlib.sha256).hexdigest()
    if not hmac.compare_digest(partes.get("v1", ""), esperado) or abs(time.time() - int(partes.get("t", 0))) > 300:
        return "assinatura inválida", 401
    evento = json.loads(corpo)
    if evento["evento"] == "agente.conversa.encerrada":
        pass  # salve evento["dados"]["transcript"]...
    return "ok"

```

- Sem `2xx`, o Lero tenta de novo depois de 2 s e de 8 s (3 tentativas). O `id` do evento é o mesmo nas tentativas: use para não processar 2 vezes.
- Redirect não é seguido: use o endereço final.
- As últimas entregas, com o status e o tempo, aparecem na aba Webhooks.
