Motor Antifraude
Motor Antifraude
O módulo fraud_ai não olha o documento nem o rosto: ele olha a sessão. Rede e IP, aparelho, velocidade de tentativas, o e-mail informado, a sua lista de bloqueio e o reuso na sua própria base entram num motor de regras ponderadas que devolve um risco de 0 a 100 e o motivo de cada ponto. Não há modelo treinado no caminho: são regras determinísticas de peso fixo, e o model_version que chega no seu webhook diz isso na cara (fraud-rules-1, fraud-rules-2). Ele não pede passo novo ao titular, mas faz consulta a fornecedor externo pago: a reputação do IP, do e-mail e do telefone vem de uma consulta que nós fazemos por você, com cache e com teto de chamadas por verificação. O resto dos sinais já existe na verificação e no seu histórico. Quando esse fornecedor não está configurado, o motor continua rodando com os sinais próprios e os que dependem dele saem como não medidos, nunca como se fossem limpos.
data. No check_details ele vem em fraud_assessment. Um parser que procura data em todo item lê undefined exatamente neste, e é o erro de integração mais caro da página, porque ele passa despercebido até o dia em que você for auditar por que o score não bate.Atenção à direção da escala, que é invertida em relação ao resto do payload. fraud_score é risco: 0 é ótimo e 100 é péssimo. O score do próprio check, ao lado dele, segue a convenção do resto da API (alto é bom). Em produção os dois são exatamente complementares, score = 100 menos fraud_score, então ver "score": 92 junto de "fraud_score": 8 não é contradição, é a mesma medida em dois sentidos. Ramifique por level ou por decision, que não têm essa ambiguidade.
level tem três valores, low, medium e high, e é ele que determina o desfecho do check: low aprova (passed: true), medium fica indeterminado (passed: null, outcome: "pending") e high reprova o módulo (passed: false). O campo decision é a mesma coisa na língua da decisão: approve, review e decline. Reprovar o módulo não é reprovar a verificação: o fraud_ai é um portão suave, então decision: "decline" aqui dentro não recusa nada sozinho, entra ponderado com os outros módulos. Quem decide a verificação é o status no topo do webhook, nunca este campo.
// fraud_ai no check_details: repare no "fraud_assessment" no lugar do "data"
{ "module": "fraud_ai", "passed": true, "outcome": "approved", "score": 92,
"fraud_assessment": {
"fraud_score": 8, // RISCO 0..100 (baixo = bom). É o complemento do "score" acima.
"level": "low", // low | medium | high
"decision": "approve", // approve | review | decline (o módulo, não a verificação)
"signals": {
// TODA categoria abaixo pode sair null, e null significa NÃO MEDIDA: o vocabulário de
// cada uma tem três valores, nunca dois. Um flow sem telefone não é um telefone limpo.
"device": "trusted", // trusted | flagged | null (aparelho reusado ou na sua blocklist)
"device_reuse_count": 0, // a CONTAGEM crua que o motor usou (null = sem vetor de features)
"velocity": "normal", // normal | high | null (tentativas na janela)
"ip_risk": "normal", // normal | elevated | null
"ip_reuse_count": 1,
"geo": "ok", // ok | mismatch | null (inclui viagem impossível)
"email": "ok", // ok | disposable | null
"phone": null, // ok | flagged | null
"company": null, // ok | flagged | null (domínio declarado pela empresa)
"identity_coherence": null, // ok | flagged | null (nome, nascimento, filiação, óbito)
"device_intel": { "automation": null, "tampered": null, "incoherent": null },
"recurrence": { "passages": 0, "prior_declines": 0, "prior_approvals": 1 },
// Com quanto do vetor esta decisão foi tomada. "sufficient": false explica um
// "decision": "review" com "fraud_score" baixo.
"coverage": { "measured": 41, "total": 55, "pct": 75, "sufficient": true },
"reasons": [],
"reasons_text": [],
// Explicabilidade POR REGRA: a família e o peso que cada razão contribuiu, e a soma já
// cortada no teto por família (a soma delas é o "fraud_score"). Enquanto o motor v1 decide,
// "applied_rules" e "families" saem null: o v1 é soma plana e não conhece família.
"applied_rules": null, // [{ code, family, weight, texto }] | null
"families": null, // { email: 10, rede: 15, ... } | null
"engine_version": "fraud-rules-1" } } }
// fraud_ai com sinal: o mesmo aparelho em várias identidades, e o e-mail descartável
{ "module": "fraud_ai", "passed": null, "outcome": "pending", "score": 55,
"fraud_assessment": {
"fraud_score": 45, "level": "medium", "decision": "review",
"signals": {
"device": "flagged", "device_reuse_count": 4,
"velocity": "high", "ip_risk": "elevated", "ip_reuse_count": 7,
"geo": "ok", "email": "disposable",
"phone": "flagged", "company": null, "identity_coherence": null,
"device_intel": { "automation": null, "tampered": null, "incoherent": null },
"recurrence": { "passages": 3, "prior_declines": 1, "prior_approvals": 0 },
"coverage": { "measured": 26, "total": 55, "pct": 47, "sufficient": true },
// códigos ESTÁVEIS de regra: ramifique por eles, nunca pelo texto do painel
"reasons": ["device_reuse", "velocity", "disposable_email", "phone_voip"],
// os MESMOS códigos com texto pronto para tela. O código é o contrato; o texto, não.
"reasons_text": [
{ "code": "device_reuse", "texto": "O mesmo aparelho foi visto com várias identidades diferentes" },
{ "code": "phone_voip", "texto": "O telefone é de voz sobre IP, não de operadora móvel" } ],
"applied_rules": null, "families": null, "engine_version": "fraud-rules-1" } } }
// cota do fornecedor estourada: o motor mediu pouco e NÃO afirma que passou
{ "module": "fraud_ai", "passed": null, "outcome": "approved", "score": 100,
"fraud_assessment": {
"fraud_score": 0, "level": "low", "decision": "review",
"signals": {
"coverage": { "measured": 6, "total": 55, "pct": 11, "sufficient": false },
"reasons": [] } } }Há uma diferença entre "olhamos e está limpo" e "não conseguimos olhar", e o bloco diz qual dos dois foi. Cada categoria (device, velocity, geo, email, ip_risk, phone, company, identity_coherence, device_intel) sai null quando ninguém a mediu naquela verificação, e nunca "ok" por omissão: um fluxo sem telefone não é um telefone verificado. O campo coverage fecha a conta, dizendo quantos dos 55 sinais foram medidos. Quando "sufficient" vem false, o motor mediu pouco demais para afirmar que está limpo: nesse caso passed sai null e decision sai review mesmo com fraud_score baixo, e é isso que o terceiro exemplo acima mostra. Isso não reprova ninguém e não muda a decisão da verificação: é o módulo se recusando a assinar embaixo de um vetor que ele não conseguiu levantar.
reasons_text traz as mesmas razões com texto pronto para tela. Ramifique sempre por reasons, nunca pelo texto: o código é o contrato e é estável, o texto pode mudar de redação.
applied_rules abre o fraud_score por regra: cada razão com a família a que ela pertence e o peso que ela contribuiu, e families traz a soma de cada família já cortada no teto dela (a soma das famílias é o fraud_score). engine_version diz qual motor produziu aquelas razões, para você conseguir comparar duas verificações separadas no tempo. Os dois primeiros saem null enquanto o motor de soma plana estiver decidindo, porque ele não conhece família nem peso por regra. As chaves já saem hoje para que a troca de motor não mude a forma da resposta.
O bloco signals traz duas coisas de propósito: o estado por categoria, que é legível e curto, e a contagem crua que o motor usou (device_reuse_count, ip_reuse_count). Sem a contagem você leria "device": "flagged" sem saber se foram dois ou quarenta cadastros no mesmo aparelho, que é a diferença entre um celular de família e uma fazenda de contas. null na contagem significa não medido naquela verificação, nunca zero. Já reasons é a lista de códigos de regra que dispararam, um vocabulário estável (device_reuse, velocity, impossible_travel, blocklist_hit, disposable_email e outros) do qual as categorias acima são derivadas. Ele pode ganhar códigos novos: trate como lista aberta.
O que não está aqui, e é deliberado. Detecção de mídia sintética não vive neste módulo nem em nenhum outro hoje: não existe modelo de deepfake com licença comercial de ponta a ponta, e não publicamos campo sem medição por trás. E nenhum valor de identificador viaja no bloco: nem IP, nem e-mail, nem o fingerprint do aparelho, nem o hash de nenhum deles. Sai a categoria e sai a contagem, que é o que sustenta a sua decisão sem transformar o webhook num oráculo sobre terceiros.
Em sandbox este módulo é o menos realista da API, e é melhor você saber por quê. Os sinais de risco vêm do seu histórico real (aparelhos, IPs e tentativas da sua organização), e no ambiente de testes esse histórico não existe. Então o bloco fraud_assessment sai sempre no caminho limpo, com level: "low", fraud_score baixo e reasons: [], enquanto passed, outcome e score continuam seguindo o sufixo do documento, como no resto do sandbox. Ou seja: lá, e só lá, dá para ver decision: "decline" ao lado de level: "low", e a relação score = 100 menos fraud_score não vale. Não programe a sua conciliação contra o par que o sandbox mostra; programe contra os campos, que são os mesmos nos dois ambientes.
Pronto para integrar? A chave de sandbox sai no painel, logo depois do cadastro. Criar conta grátis