KYC
Integracao de verificacao de identidade (Know Your Customer) no template. Suporta multiplos operadores via o pacote @cactus-agents/kyc.
Cuidado com o escopo: KYC nao e a validacao de identidade do Chile. O fluxo RUT + DIDIT e um subsistema separado, documentado no fim desta pagina.
Visao geral
Usuario clica "Iniciar verificacao"
│
▼
useKycFlow.start() → POST /api/kyc/start
│
▼
BFF devolve { operator, correlation_id, url? , token?, process_id? }
│
▼
inferKycIntegrationType(resposta) → "iframe" | "sdk" | "unknown"
│
├── iframe → KycModal renderiza <iframe src={url}>
└── sdk → KycSdkOrchestrator monta o painel do operador (Unico, Sumsub)
│
▼
Polling: POST /api/kyc/status (delay de 2s entre tentativas, max 20)
│
├── PROCESSING / LIVENESS_PENDING → continua polling
├── APPROVED / MANUAL_APPROVED → step "validated" → onValidated()
├── REPROVED / MANUAL_REPROVED → step "failed"
└── MANUAL_APPROVE_PENDING → step "manual_refresh"
A classificacao de status nao e feita a mao — usa os predicados do SDK: isKycApprovedStatus, isKycReprovedStatus, isKycManualApprovePendingStatus.
Arquivos principais
| Arquivo | Tipo | Descricao |
|---|---|---|
app/hooks/useKycFlow.ts | Hook | Orquestra start, iframe/SDK, polling de status |
app/store/kyc.ts | Store (Zustand) | Estado completo do fluxo KYC |
app/components/kyc/KycModal.tsx | Component | Modal com iframe do operador |
app/components/kyc/LazyKycModal.tsx | Component | Lazy wrapper para o modal |
app/components/kyc/KycSdkOrchestrator.tsx | Component | Orquestra os operadores de SDK |
app/components/kyc/LazyKycSdkOrchestrator.tsx | Component | Lazy wrapper do orquestrador |
app/components/kyc/operators/ | Components | KycSdkWrapper, UnicoSdkPanel, SumsubSdkPanel |
app/services/kyc.server.ts | Service (server) | createKycService() — chama o SDK KYC no server |
app/routes/api/kyc/start.ts | API Route | Inicia KYC via SDK |
app/routes/api/kyc/status.ts | API Route | Consulta status via SDK |
:::note Nao existe um kyc.client.ts
O useKycFlow chama /api/kyc/start e /api/kyc/status com fetch direto. Nao ha service singleton no client.
:::
Operadores
O type KycOperatorName (@cactus-agents/kyc) enumera 7 operadores conhecidos, e aceita string pra tolerar um operador novo do BFF sem quebrar o build:
export type KycOperatorName =
| "caf"
| "legitimuz"
| "pay2free"
| "serasa"
| "sumsub"
| "unico"
| "shielid" // grafia legada do BFF, preservada
| "shieldid"
| string;
O template nao escolhe operador — quem resolve e o BFF. O que o template faz e classificar a forma de integracao:
KycIntegrationType | Quando | Renderizacao |
|---|---|---|
"iframe" | O BFF devolveu url (pay2free, serasa, legitimuz, shieldid…) | <iframe> no KycModal |
"sdk" | O BFF devolveu token (+ process_id opcional) — unico, sumsub | Painel de SDK por operador |
null | Estado inicial, ou start() falhou antes de classificar | — |
Status
KycStatus e um enum numerico com 7 valores, espelhados em KycStatusString:
| Valor | Nome | Significado |
|---|---|---|
0 | PROCESSING | Em processamento |
1 | APPROVED | Aprovado |
2 | REPROVED | Reprovado |
3 | MANUAL_APPROVED | Aprovado manualmente |
4 | MANUAL_APPROVE_PENDING | Aprovacao manual pendente |
5 | MANUAL_REPROVED | Reprovado manualmente |
6 | LIVENESS_PENDING | Liveness pendente |
KycStatusLike = KycStatus | KycStatusString | number | string — o BFF pode devolver numero ou string, entao sempre use os predicados do SDK em vez de comparar direto.
Store — useKycStore
// app/store/kyc.ts
export type KycFlowType = "default" | "mobile" | "manual_approve_pending";
export type KycIntegrationType = "iframe" | "sdk" | null;
interface KycState {
open: boolean;
source: string; // contexto que disparou (global, deposit, …)
recoveryToken: string | null; // token de link de recuperacao
autoStart: boolean;
kycId: number | null;
operator: KycOperatorName | null;
iframeUrl: string;
integrationType: KycIntegrationType;
sdkToken: string | null;
processId: string | null;
flow: KycFlowType;
step: KycVerificationStep;
isKycLoading: boolean;
isCheckStatusLoading: boolean;
isPollingRunning: boolean;
error: string | null;
sdkVisible: boolean;
loadingMessageIndex: number;
reloadOnValidated: boolean;
onValidated: ((payload: ValidationSuccessPayload) => void | Promise<void>) | null;
openKycModal(options?: OpenKycOptions): void;
closeKycModal(): void;
// ... setters
}
Steps do fluxo
KycVerificationStep (definido no store, nao no SDK):
| Step | Descricao |
|---|---|
initial | Estado inicial, aguardando usuario clicar |
started | KYC iniciado, iframe/SDK carregando |
checking | Polling de status ativo |
finished | Operador finalizou (aguardando resultado) |
validated | KYC aprovado |
failed | KYC reprovado |
expired | Sessao expirada |
manual_refresh | Aprovacao manual pendente |
internal_start_error | Erro ao iniciar |
internal_status_error | Erro ao consultar status |
Abrindo o KYC programaticamente
import { useKycStore } from "~/store/kyc";
const openKycModal = useKycStore((s) => s.openKycModal);
openKycModal({
source: "deposit",
autoStart: true,
reloadOnValidated: false,
onValidated: async (payload) => {
// payload da validacao disponivel
},
});
Dentro do sistema de validacoes
KycStep (app/components/validation/steps/KycStep.tsx) abre o modal com autoStart: true e reloadOnValidated: false, usando o callback onValidated para notificar a resolucao ao sistema de steps. Ver Validacoes.
Rota de recuperacao
user.validate (/user/validate/:type/:token) permite completar o KYC via link enviado por e-mail/SMS:
/user/validate/kyc/abc123
O type identifica o tipo de validacao e o token e o token de recuperacao; a rota abre o modal KYC com recoveryToken preenchido. O arquivo e app/routes/user/validate.$type.$token.tsx.
Erros
As rotas api/kyc/* seguem o padrao de proxy do template: proxyErrorResponse() + unauthorizedNoToken() de ~/utils/proxy-error.server. No client, o useKycFlow usa extractApiError() de @cactus-agents/api-client — nunca String(err), que renderiza [object Object]. Ver Services.
Validacao de identidade — Chile (RUT + DIDIT)
Fluxo separado do KYC por operador. Vale so no Chile e e gateado pela env RUT_VALIDATION (kill switch por brand/ambiente; togglar em prod exige redeploy porque o valor e lido do env).
Os tres passos
app/services/identity.client.ts expoe as tres chamadas encadeadas:
validateRut(number) // passo 1
confirmBirthDate(...) // passo 2
verifyIdentity(...) // passo 3
O client nunca fala com o BFF direto. As rotas same-origin sao:
| Rota | Fase | Descricao |
|---|---|---|
api/auth/documents/validate | pre-auth | Passo 1 — valida o RUT. Devolve name do titular no sucesso (mascarar na exibicao) |
api/auth/documents/confirm-birthdate | pre-auth | Passo 2 — confirma a data de nascimento contra o bureau |
api/user/documents/verify-identity | autenticado | Passo 3 — conclui a verificacao |
Os passos 1 e 2 sao compartilhados com o cadastro; o passo 3 exige sessao. O cadastro usa 1-2; conta, deposito e saque usam 1-2-3.
:::info Espacamento entre chamadas
Os tres passos disparam em sequencia no mesmo clique. Sem folga eles chegam no BFF em rajada e batem no rate limit (TOO_MANY_ATTEMPTS), entao o client espaca as chamadas dos passos 2 e 3 em 500ms (0 em teste). O delay vive no client, nao nas rotas, pra valer pra todos os fluxos.
:::
Chaveie por code, nunca por message
O contrato devolve um code estavel (IdentityCode em app/services/identity.types.ts) e uma message ja traduzida pelo tenant. Toda decisao de comportamento le o code; message serve apenas como fallback de exibicao.
Codigos declarados hoje (consulte o type pra lista atual): IDENTITY_BUREAU_UNAVAILABLE, IDENTITY_BIRTHDATE_MISMATCH, IDENTITY_VERIFICATION_PENDING, CL00404, BDT00008, DOCUMENT_ALREADY_REGISTERED, IDENTITY_MODULE_DISABLED, INVALID_DOCUMENT, RESTRICTED, DOCUMENT_RESTRICTED, TOO_MANY_ATTEMPTS, REQUIRED_DOCUMENT, REQUIRED_BIRTH_DATE, BAD_REQUEST, UNKNOWN_ERROR.
Ramo A = RUT e data confirmados no bureau. Ramo B = bureau indisponivel (IDENTITY_BUREAU_UNAVAILABLE, HTTP 200 com pending: true): o fluxo segue com pendencia, guardando o RUT cru e a data digitada.
Estado derivado — useChileIdentityStatus()
import { useChileIdentityStatus } from "~/hooks/useChileIdentityStatus";
const { enabled, validated, missing } = useChileIdentityStatus();
| Campo | Semantica |
|---|---|
enabled | country === "CHL" && clientEnv.RUT_VALIDATION === true |
validated | Gate. enabled && !!userInfo.kyc_validated_at — fonte da verdade e o backend |
missing | { rut, birthDate, name } — presenca dos campos, so pra display |
:::danger missing nao e gate
Nao infira validacao por presenca de campos. Um usuario legado pode ter document e birth_date preenchidos e ainda nao estar validado (ex.: o documento e um CPF migrado, nao um RUT). O unico gate e userInfo.kyc_validated_at.
Detalhe do backend: "0000-00-00" e "1901-01-01" sao sentinelas de "sem data de nascimento" e nao contam como preenchido — um /profile de conta nova traz birth_date: "1901-01-01".
:::
Superficies
| Superficie | Componente / arquivo |
|---|---|
| Cadastro | app/components/auth/RegisterModal.tsx (+ RutValidationField, BirthDateValidationField) |
| Pagina de conta | app/components/user/account/ChileIdentityValidationBlock.tsx |
| Deposito | app/components/payments/DepositModal.tsx |
| Saque | app/components/payments/WithdrawModal.tsx (gate adicional: metodo ProntoPaga) |
O bloco da conta reusa os campos do cadastro sem tocar neles, e opera em dois modos: com knownBirthDate (a conta ja tem nascimento — sem campo de data) e sem (espelha o cadastro, orquestrando RUT → data num unico clique).