> ## Documentation Index
> Fetch the complete documentation index at: https://documentacao.legitimuz.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Receptor de webhook

> O endpoint que recebe o desfecho: confere a assinatura, descarta repetição e enfileira o processamento.

O endpoint que recebe o desfecho. É a peça que mais dá errado, e a que mais importa acertar.

```ts title="app/api/webhooks/legitimuz/route.ts" theme={null}
import { createHmac, timingSafeEqual } from "node:crypto";

const TOLERANCIA_SEGUNDOS = 300;

export async function POST(request: Request) {
  // 1. CORPO CRU, antes de qualquer parse. Reserializar quebra a assinatura.
  const corpoCru = await request.text();
  const assinatura = request.headers.get("x-legitimuz-signature") ?? "";
  const entregaId = request.headers.get("x-legitimuz-delivery") ?? "";

  if (!assinaturaValida(corpoCru, assinatura, process.env.LEGITIMUZ_WEBHOOK_SECRET!)) {
    return new Response(null, { status: 401 });
  }

  // 2. Descarte repetição. INSERT com chave única, não SELECT seguido de INSERT.
  const inedita = await db.entregas.registrarSeInedita(entregaId);
  if (!inedita) return new Response(null, { status: 200 });

  // 3. Enfileire e responda. Não processe dentro do request.
  await fila.publicar(JSON.parse(corpoCru));
  return new Response(null, { status: 200 });
}

function assinaturaValida(corpoCru: string, header: string, segredo: string): boolean {
  const partes = Object.fromEntries(
    header.split(",").map((p) => p.split("=") as [string, string])
  );

  const timestamp = Number(partes.t);
  const recebida = partes.v1;
  if (!timestamp || !recebida) return false;

  if (Math.abs(Math.floor(Date.now() / 1000) - timestamp) > TOLERANCIA_SEGUNDOS) return false;

  const esperada = createHmac("sha256", segredo)
    .update(`${timestamp}.${corpoCru}`)
    .digest("hex");

  const a = Buffer.from(esperada, "utf8");
  const b = Buffer.from(recebida, "utf8");
  return a.length === b.length && timingSafeEqual(a, b);
}
```

### Worker que processa

```ts title="worker.ts" theme={null}
export async function processar(evento: EnvelopeLegitimuz) {
  // Evento desconhecido não derruba nada: registre e siga.
  if (evento.event !== "verification.decided") return;

  const { status, ref_id } = evento.verification;
  if (!ref_id) return;

  switch (status) {
    case "approved":
      await db.cadastro.liberar(ref_id);
      break;
    case "reproved":
      await db.cadastro.recusar(ref_id);
      break;
    case "review":
      // Não é recusa. Este evento dispara DE NOVO quando um analista decidir.
      await db.cadastro.marcarEmAnalise(ref_id);
      break;
  }
}
```

### Testar localmente

```bash theme={null}
# 1. Exponha a porta local
npx untun tunnel http://localhost:3000

# 2. Cadastre a URL do túnel no dashboard, em Integrações → Segurança → Webhooks
# 3. Dispare a entrega de teste pelo próprio dashboard
```

<Card title="Verificar a assinatura" icon="shield-check" href="/webhooks/security" horizontal>
  O mesmo código em Python, PHP e Go.
</Card>

## Antes de rodar

As três POCs usam as mesmas variáveis:

```bash title=".env" theme={null}
LEGITIMUZ_API_KEY=<SUA_CHAVE>
LEGITIMUZ_ORIGIN_ID=<ORIGIN_PUBLIC_ID>
LEGITIMUZ_WEBHOOK_SECRET=<SEGREDO_DO_ENDPOINT>
```

Os três valores saem do dashboard, em
[Integrações → Segurança](/platform/integrations). Use uma integração **sandbox**.

## O que esta POC não faz

Ela mostra o caminho feliz e os erros que quebram integração. Ficou de fora de propósito:

* Autenticação de verdade. `autenticar()` é um stub.
* Migrations e schema do banco.
* Observabilidade e alerta.
* Tela de retomada quando o titular abandona e volta.

## Próximo passo

<Columns cols={2}>
  <Card title="Boas práticas" icon="bulb" href="/help/best-practices">
    O que separa uma integração que funciona de uma que gera chamado.
  </Card>

  <Card title="Ir para produção" icon="rocket" href="/start/sandbox">
    O checklist antes do primeiro titular real.
  </Card>
</Columns>
