Criar conta grátis

Documentação
Ver em Markdown

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.

!Este é o único módulo cujo bloco não se chama 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