| Stack | Next.js 15 (App Router) · React 19 |
| Você terá | a rota que cria a verificação, a página que abre o fluxo e o handler do desfecho |
Confira o exemplo
Os arquivos desta POC, com o destino de cada um.
app/
├─ api/
│ ├─ verifications/route.ts # cria a verificação
│ └─ webhooks/legitimuz/route.ts # recebe o desfecho
└─ registration/page.tsx # abre o fluxo
Rota que cria
app/api/verifications/route.ts
import { NextResponse } from "next/server";
export async function POST() {
// 1. Quem está pedindo? Sem isto, qualquer um cria verificação na sua conta.
const user = await authenticate();
if (!user) return NextResponse.json({ error: "not_authenticated" }, { status: 401 });
// 2. O CPF vem do SEU cadastro, nunca do corpo da requisição.
const registration = await db.registration.byUser(user.id);
const response = await fetch("https://api-core.legitimuz.com/public/verifications", {
method: "POST",
headers: {
"X-API-Key": process.env.LEGITIMUZ_API_KEY!,
"Content-Type": "application/json",
},
body: JSON.stringify({
schema_version: "1.0",
ref_id: registration.id,
document: { type: "cpf", number: registration.cpf },
flow_public_id: process.env.LEGITIMUZ_FLOW_ID!,
}),
});
if (!response.ok) {
const error = await response.json();
console.error("legitimuz", response.status, error.message, response.headers.get("X-Request-Id"));
return NextResponse.json({ error: "create_failed" }, { status: 502 });
}
const { verification, entry } = await response.json();
// 3. Grave o vínculo ANTES de responder. Sem ele, o webhook chega órfão.
await db.registration.link(registration.id, verification.public_id);
// 4. Devolva só a entry.
return NextResponse.json({ entry });
}
Página que abre
app/registration/page.tsx
"use client";
import { useEffect, useRef, useState } from "react";
export default function Registration() {
const container = useRef<HTMLDivElement>(null);
const [state, setState] = useState<"idle" | "open" | "waiting">("idle");
useEffect(() => {
if (state !== "open" || !container.current) return;
let handle: { destroy: () => void } | undefined;
let cancelled = false;
fetch("/api/verifications", { method: "POST" })
.then((r) => r.json())
.then(({ entry }) => {
if (cancelled || !container.current) return;
handle = window.Legitimuz.mount({
sdkUrl: entry.url,
target: container.current,
onComplete: () => setState("waiting"),
});
});
return () => {
cancelled = true;
handle?.destroy();
};
}, [state]);
if (state === "waiting") {
// O desfecho vem do webhook. Aqui você só espera.
return <p>Estamos analisando seus dados. Avisamos assim que terminar.</p>;
}
return state === "idle" ? (
<button onClick={() => setState("open")}>Verificar identidade</button>
) : (
<div ref={container} style={{ minHeight: 600 }} />
);
}
A tela nunca libera o cadastro. Ela só muda para
waiting, quem libera é o handler de
webhook, no servidor.Antes de rodar
.env
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. Aparece uma vez |
LEGITIMUZ_WEBHOOK_SECRET | Integrações → Segurança → Webhooks, na criação do endpoint |
LEGITIMUZ_FLOW_ID | Solução KYC → Fluxos, no menu da linha, em Copiar ID do fluxo |
O que esta POC não faz
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.- Migrations e schema do banco.
- Observabilidade e alerta.
- Tela de retomada quando o titular abandona e volta. Veja a POC de retomada.
Próximo passo
Boas práticas
O que separa uma integração que funciona de uma que gera chamado.
Ir para produção
O checklist antes do primeiro titular real.