> ## 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.

# Fila e worker

> Responda 2xx em milissegundos e processe o desfecho fora do request, com retentativa própria.

O endpoint de webhook tem um trabalho só: conferir a assinatura e aceitar. Tudo o que demora —
liberar cadastro, chamar antifraude interno, mandar e-mail — acontece depois, num worker.

|               |                                                                               |
| ------------- | ----------------------------------------------------------------------------- |
| **Stack**     | Node 20 · BullMQ · Redis                                                      |
| **Você terá** | um endpoint que responde em milissegundos e um worker com retentativa própria |

<Card title="Confira o exemplo" icon="brand-github" href="https://github.com/Legitimuz-Tech/legitimuz-examples/tree/master/pocs/queue-worker" horizontal>
  Os arquivos desta POC, com o destino de cada um.
</Card>

## Por que separar

A Legitimuz reentrega o que não responde `2xx`, em intervalos crescentes. Se o seu handler
processa dentro do request, dois problemas aparecem juntos: uma tarefa lenta vira timeout e a
entrega é reenviada; e o seu retry passa a ser o retry da Legitimuz, que não conhece o seu banco.

Separar deixa cada lado com a retentativa que ele entende.

## O endpoint

```ts title="app/api/webhooks/legitimuz/route.ts" theme={null}
import { outcomeQueue } from "@/lib/queues";

export async function POST(request: Request) {
  const rawBody = await request.text();
  const deliveryId = request.headers.get("x-legitimuz-delivery") ?? "";

  if (!isSignatureValid(rawBody, request.headers.get("x-legitimuz-signature") ?? "")) {
    return new Response(null, { status: 401 });
  }

  // `jobId` é a chave de idempotência: a fila recusa o mesmo id e a reentrega vira no-op.
  await outcomeQueue.add("outcome", JSON.parse(rawBody), {
    jobId: deliveryId,
    attempts: 5,
    backoff: { type: "exponential", delay: 1_000 },
    removeOnComplete: 1_000,
  });

  return new Response(null, { status: 200 });
}
```

<Info>
  O `jobId` substitui a tabela de deduplicação das outras POCs. Um id só existe uma vez na fila,
  então a segunda entrega do mesmo evento não cria um segundo job.
</Info>

## O worker

```ts title="workers/outcome.ts" theme={null}
import { Worker } from "bullmq";

new Worker(
  "outcome",
  async (job) => {
    const event = job.data;

    if (event.event !== "verification.decided") return;

    const registration = await db.registrations.byRefId(event.verification.ref_id);
    if (!registration) throw new Error(`no registration for ref_id: ${event.verification.ref_id}`);

    // `review` não é recusa: a verificação volta a decidir depois, e este mesmo evento chega de novo.
    if (event.verification.status === "review") {
      await db.registrations.markUnderReview(registration.id);
      return;
    }

    await db.registrations.applyOutcome(registration.id, event.verification.status);
  },
  { connection: { host: process.env.REDIS_HOST, port: 6379 }, concurrency: 5 },
);
```

<Warning>
  Lançar dentro do worker é o caminho certo de erro: o BullMQ conta a tentativa e reprograma. Um
  `try/catch` que engole a exceção marca o job como concluído e o desfecho se perde em silêncio.
</Warning>

## Ordem não é garantida

Dois eventos da mesma verificação podem chegar fora de ordem. Se a sua lógica depende disso, use o
`occurred_at` do corpo e descarte o que for mais antigo que o estado já gravado:

```ts title="Ignorar evento atrasado" theme={null}
const atual = await db.registrations.byRefId(event.verification.ref_id);

if (atual.outcome_at && new Date(event.occurred_at) < atual.outcome_at) return;
```

## Antes de rodar

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

| Variável                   | Onde achar                                                                       |
| -------------------------- | -------------------------------------------------------------------------------- |
| `LEGITIMUZ_API_KEY`        | Integrações → Segurança → [Chaves de API](/platform/tokens). Aparece uma vez     |
| `LEGITIMUZ_WEBHOOK_SECRET` | Integrações → Segurança → [Webhooks](/platform/webhooks), na criação do endpoint |
| `LEGITIMUZ_FLOW_ID`        | Solução KYC → Fluxos, no menu da linha, em **Copiar ID do fluxo**                |

Use uma integração **sandbox**. Nenhum dos três valores vai para o browser ou para o app.

Esta POC precisa também de um Redis:

```bash title=".env" theme={null}
REDIS_HOST=localhost
```

## O que esta POC não faz

<Warning>
  POC é código para entender o fluxo, não para copiar em produção. Em todas elas, `authenticate()` é
  um stub, `db` é um objeto de mentira e não há migration, observabilidade nem retentativa própria.
</Warning>

* Sem dead-letter queue. Depois de 5 tentativas o job fica em `failed` e ninguém é avisado.
* Sem métrica de profundidade da fila.
* Sem lock por `ref_id`: dois eventos da mesma verificação podem rodar em paralelo.

## Próximo passo

<Columns cols={2}>
  <Card title="Receptor de webhook" icon="webhook" href="/guides/pocs/webhook-receiver">
    A conferência da assinatura, linha a linha.
  </Card>

  <Card title="Retomada e expiração" icon="clock" href="/guides/pocs/resume-expiry">
    O que fazer quando o titular some no meio.
  </Card>
</Columns>
