Pular para o conteúdo principal

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

ArquivoTipoDescricao
app/hooks/useKycFlow.tsHookOrquestra start, iframe/SDK, polling de status
app/store/kyc.tsStore (Zustand)Estado completo do fluxo KYC
app/components/kyc/KycModal.tsxComponentModal com iframe do operador
app/components/kyc/LazyKycModal.tsxComponentLazy wrapper para o modal
app/components/kyc/KycSdkOrchestrator.tsxComponentOrquestra os operadores de SDK
app/components/kyc/LazyKycSdkOrchestrator.tsxComponentLazy wrapper do orquestrador
app/components/kyc/operators/ComponentsKycSdkWrapper, UnicoSdkPanel, SumsubSdkPanel
app/services/kyc.server.tsService (server)createKycService() — chama o SDK KYC no server
app/routes/api/kyc/start.tsAPI RouteInicia KYC via SDK
app/routes/api/kyc/status.tsAPI RouteConsulta 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:

KycIntegrationTypeQuandoRenderizacao
"iframe"O BFF devolveu url (pay2free, serasa, legitimuz, shieldid…)<iframe> no KycModal
"sdk"O BFF devolveu token (+ process_id opcional) — unico, sumsubPainel de SDK por operador
nullEstado inicial, ou start() falhou antes de classificar

Status

KycStatus e um enum numerico com 7 valores, espelhados em KycStatusString:

ValorNomeSignificado
0PROCESSINGEm processamento
1APPROVEDAprovado
2REPROVEDReprovado
3MANUAL_APPROVEDAprovado manualmente
4MANUAL_APPROVE_PENDINGAprovacao manual pendente
5MANUAL_REPROVEDReprovado manualmente
6LIVENESS_PENDINGLiveness 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):

StepDescricao
initialEstado inicial, aguardando usuario clicar
startedKYC iniciado, iframe/SDK carregando
checkingPolling de status ativo
finishedOperador finalizou (aguardando resultado)
validatedKYC aprovado
failedKYC reprovado
expiredSessao expirada
manual_refreshAprovacao manual pendente
internal_start_errorErro ao iniciar
internal_status_errorErro 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:

RotaFaseDescricao
api/auth/documents/validatepre-authPasso 1 — valida o RUT. Devolve name do titular no sucesso (mascarar na exibicao)
api/auth/documents/confirm-birthdatepre-authPasso 2 — confirma a data de nascimento contra o bureau
api/user/documents/verify-identityautenticadoPasso 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();
CampoSemantica
enabledcountry === "CHL" && clientEnv.RUT_VALIDATION === true
validatedGate. 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

SuperficieComponente / arquivo
Cadastroapp/components/auth/RegisterModal.tsx (+ RutValidationField, BirthDateValidationField)
Pagina de contaapp/components/user/account/ChileIdentityValidationBlock.tsx
Depositoapp/components/payments/DepositModal.tsx
Saqueapp/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).