# Risco de IP e de e-mail

<https://unifokal.com/docs/modulos/risco>

## Risco de IP e de e-mail

Dois módulos avaliam o **contexto** da verificação, e não a identidade: **Verificação de IP** (`ip_risk`) e **Risco de e-mail** (`email_risk`). Nenhum dos dois pede passo novo ao usuário, e nenhum dos dois reprova sozinho: são **portões suaves**, como os módulos de canal. Um sinal aqui leva a verificação para `review` com a evidência no webhook, nunca a `denied` automático.

! **Limites honestos.** VPN corporativa, rede de hotel, CGNAT de operadora móvel, domínio próprio recém registrado e caixa de função de um MEI são o cotidiano de gente legítima: trate estes dois como **agravantes em conjunto**, nunca como veredito. `ip_risk` não diz quem está por trás do IP, e `email_risk` não prova que a caixa é da pessoa (quem prova posse é a Validação de e-mail).

Dois campos do payload merecem leitura antes de você programar contra ele. `provider_verdict` diz se a camada de reputação de rede/endereço estava disponível para aquela consulta: quando vem `not_cached`, os campos que dependem dela chegam `null` e isso significa **não medido**, jamais "limpo". No `ip_risk` existe ainda o `network_measured`: quando ele vem `false`, nenhuma camada de rede respondeu e o módulo sai `pending` em vez de dizer que está limpo (e nesse caso ele **não é cobrado**). E `scope`, no `email_risk`, diz o que foi de fato analisado: `mailbox` quando o flow também tem `email_otp` (é dele que o endereço vem, digitado pelo titular no widget) e `domain` quando só o domínio existia. No escopo `domain`, `role_based` e `plus_alias` vêm `null`: não havia caixa para olhar. **Combine sempre `email_risk` com `email_otp` no mesmo flow**: sem ele o endereço nunca chega ao produto (você não envia contato na criação), o módulo sai `pending` com `reason: "email_not_provided"`, a verificação vai para revisão e o módulo **não é cobrado**.

O `ip_risk` devolve **derivados** do endereço, e não o endereço: país, ASN, organização do ASN e a classe da conexão. O IP, a cidade e a coordenada **não trafegam**. No `email_risk` vale a mesma regra dos módulos de canal: só o domínio, a máscara e o `email_fp` (o mesmo HMAC que a Validação de e-mail publica para o mesmo endereço, para você correlacionar sem receber o dado em claro).

No `ip_risk`, **proxy e VPN saem separados** porque a pergunta que você faz sobre cada um é diferente, e `anonymizer` é apenas o "ou" dos dois. Ao lado deles viaja o **comportamento recente** do endereço, como o fornecedor o declara: `recent_abuse` (houve abuso conhecido a partir dali), `bot` (tráfego automatizado) e `abuse_velocity`, um ordinal fechado em `none`, `low`, `medium` e `high`. Valor fora desse vocabulário vira `null`, nunca um veredito por acidente. E vale para todos eles a regra do `provider_verdict`: `null` significa que o fornecedor não respondeu sobre aquele ponto, jamais que respondeu que está limpo.

No `email_risk`, `disposable_listed` e `forwarder_listed` declaram **de onde veio a afirmação**: eles são a nossa lista curada de domínios descartáveis e de encaminhadores mascarados, que **soma** ao veredito do fornecedor em vez de substituí-lo. Assim `disposable: true` com `disposable_listed: false` quer dizer "quem afirmou foi o fornecedor", e a nossa lista, que é curta por desenho, não transforma o próprio `false` em "medimos e está limpo". Outros dois campos olham o **formato** do endereço, e só existem no escopo `mailbox`: `digits_heavy` marca o local-part dominado por dígitos (uma corrida de cinco ou mais, ou mais da metade dos caracteres), o padrão do endereço fabricado em massa, calibrado de propósito para **não** pegar o apelido com ano de nascimento; e `name_email_score` mede, de 0 a 1, quanto do local-part é coberto pelo nome lido no documento. Este último é **informativo e não pontua**: apelido legítimo é comum demais para virar agravante, e ele vem `null` quando não há caixa, não há OCR na verificação, ou o local-part não tem nenhum token com cara de nome.

```
// ip_risk: rede residencial, nada a apontar
{ "module": "ip_risk", "passed": true, "outcome": "approved", "score": 100,
  "data": { "risk_score": 0, "level": "low", "provider_verdict": "clean", "network_measured": true,
            "country": "BR", "asn": 28573, "asn_org": "OPERADORA X S.A.",
            "connection": "residential", "tor": false, "datacenter": false, "anonymizer": false,
            // proxy e vpn SEPARADOS: o anonymizer acima é só o "ou" dos dois.
            "proxy": false, "vpn": false,
            // comportamento RECENTE do endereço, como o fornecedor o declara
            "recent_abuse": false, "bot": false, "abuse_velocity": "none",
            "geo_mismatch": false, "timezone_mismatch": false, "asn_incoherent": false,
            "ip_reuse_count": 1, "reasons": [] } }

// ip_risk: saída Tor, fora do país, fuso do navegador incoerente (review, NUNCA decline)
{ "module": "ip_risk", "passed": false, "outcome": "failed", "score": 40,
  "data": { "risk_score": 60, "level": "high", "provider_verdict": "dirty", "network_measured": true,
            "country": "NL", "asn": 60068, "asn_org": "DATACAMP LIMITED",
            "connection": "anonymizer", "tor": true, "datacenter": false, "anonymizer": true,
            "proxy": false, "vpn": true,
            "recent_abuse": true, "bot": false, "abuse_velocity": "medium",
            "geo_mismatch": true, "timezone_mismatch": true, "asn_incoherent": false,
            "ip_reuse_count": 4,
            "reasons": ["ip_tor", "ip_geo_mismatch", "ip_timezone_mismatch"] } }

// ip_risk: nenhuma camada de rede respondeu -> pending, e o módulo NÃO é cobrado
{ "module": "ip_risk", "passed": null, "outcome": "pending", "score": 0,
  "data": { "provider_verdict": "not_cached", "network_measured": false,
            "reason": "ip_intel_unavailable", "country": null, "asn": null,
            "tor": null, "datacenter": null, "anonymizer": null,
            // null nos SEIS: "o fornecedor não disse", jamais "disse que está limpo".
            "proxy": null, "vpn": null,
            "recent_abuse": null, "bot": null, "abuse_velocity": null } }

// email_risk: caixa analisada (o flow tem email_otp), endereço descartável
{ "module": "email_risk", "passed": false, "outcome": "failed", "score": 38,
  "data": { "scope": "mailbox", "risk_score": 62, "level": "high",
            "provider_verdict": "disposable", "email_domain": "tempmail.xyz",
            "email_masked": "u***r@tempmail.xyz", "email_fp": "7c14ab…",
            "disposable": true, "undeliverable": false, "role_based": false, "plus_alias": false,
            "typosquat": false, "homoglyph": false, "masked_forwarder": false,
            "suspicious_lexicon": true, "domain_age_days": 12,
            // DE ONDE veio a afirmação: true = a NOSSA lista curada também listou o domínio
            "disposable_listed": true, "forwarder_listed": false,
            // padrão do endereço e coerência com o nome do documento (0..1, INFORMATIVA)
            "digits_heavy": true, "name_email_score": 0.12,
            "reasons": ["email_disposable", "email_suspicious_lexicon", "email_domain_recent"] } }

// email_risk: só o domínio existia (flow sem email_otp) -> o que NÃO foi olhado vem null
{ "module": "email_risk", "passed": null, "outcome": "pending", "score": 0,
  "data": { "scope": "domain", "provider_verdict": "not_cached",
            "email_domain": "empresa.com.br", "email_masked": null, "email_fp": null,
            "role_based": null, "plus_alias": null, "disposable": null,
            // as listas NOSSAS respondem sobre o DOMÍNIO, então continuam medindo aqui
            "disposable_listed": false, "forwarder_listed": false,
            // estes DOIS dependem da caixa: sem local-part não há o que medir
            "digits_heavy": null, "name_email_score": null,
            "reason": "email_not_provided" } }
```

Em sandbox nada é consultado: o desfecho vem do **sufixo do documento**, como no resto do ambiente. Para o `ip_risk`: `33` datacenter, `44` saída Tor fora do país e `55` reputação indisponível (`not_cached`). Para o `email_risk`: `33` domínio descartável, `44` caixa de função com alias e `55` escopo de domínio.

A mesma dupla tem uma versão **avançada**, com um terceiro módulo de telefone ao lado: **Verificação avançada de IP** (`ip_risk_plus`), **Risco avançado de e-mail** (`email_risk_plus`) e **Risco de telefone** (`telefone_risco`). Os dois primeiros devolvem os MESMOS campos dos módulos de origem acima, com uma diferença de **entrega**: aqui o veredito nunca chega como não medido. Sem resposta da consulta o módulo **defere**, e o deferimento tem forma própria no payload: o item de `check_details` continua vindo **com** `data`, e é dentro dele que o deferimento se declara, com `provider_verdict: "unavailable"`, `reason: "provider_unavailable"` e `null` em tudo que não foi medido. Nessa passagem **o módulo não é cobrado**: sai da conta só ele, e o resto do flow continua sendo cobrado normalmente. Nenhum campo é completado com "limpo".

O `telefone_risco` exige `sms_otp` no mesmo flow: o número analisado é o que o titular já provou controlar pelo código por SMS, nunca um número solto. Ele confirma a **linha**, não a pessoa, e não substitui a prova de posse por SMS nem a biometria. O número completo não trafega: só a máscara e o `phone_fp`, o mesmo pseudônimo que o `sms_otp` publica para o mesmo número. `line_class` é vocabulário fechado nosso (`mobile`, `landline`, `voip`, `toll_free`, `premium`, `satellite`, `pager`, `unknown`), nunca a string crua do fornecedor, e `high_risk` é derivado por nós: não há score cru de fornecedor no payload. Os três são **portões suaves**: um sinal alto leva a verificação para `review` com a evidência no webhook, nunca a `denied` automático.

```
// ip_risk_plus: mesma rede do ip_risk, agora com o veredito garantido (nunca not_cached)
{ "module": "ip_risk_plus", "passed": true, "outcome": "approved", "score": 100,
  "data": { "risk_score": 0, "level": "low", "provider_verdict": "clean", "network_measured": true,
            "country": "BR", "asn": 28573, "asn_org": "OPERADORA X S.A.",
            "connection": "residential", "tor": false, "datacenter": false, "anonymizer": false,
            "proxy": false, "vpn": false,
            "recent_abuse": false, "bot": false, "abuse_velocity": "none",
            "geo_mismatch": false, "timezone_mismatch": false, "asn_incoherent": false,
            "ip_reuse_count": 1, "reasons": [], "reason": null } }

// ip_risk_plus: saída Tor, fora do país, abuso recente (review, NUNCA decline)
{ "module": "ip_risk_plus", "passed": false, "outcome": "failed", "score": 20,
  "data": { "risk_score": 80, "level": "high", "provider_verdict": "dirty", "network_measured": true,
            "country": "NL", "asn": 60068, "asn_org": "DATACAMP LIMITED",
            "connection": "anonymizer", "tor": true, "datacenter": false, "anonymizer": true,
            "proxy": false, "vpn": true,
            "recent_abuse": true, "bot": false, "abuse_velocity": "high",
            "geo_mismatch": true, "timezone_mismatch": true, "asn_incoherent": false,
            "ip_reuse_count": 1,
            // toda razão que pontuou aparece aqui, e "score" é sempre 100 menos "risk_score"
            "reasons": ["ip_tor", "ip_geo_mismatch", "ip_timezone_mismatch",
                        "ip_recent_abuse", "ip_abuse_velocity_high"], "reason": null } }

// ip_risk_plus: sem veredito do fornecedor -> DEFERE. O "data" VEM, com null no que ninguém mediu,
// e só o MÓDULO sai da cobrança (o resto do flow segue cobrando)
{ "module": "ip_risk_plus", "passed": null, "outcome": "pending", "score": 0,
  "data": { "risk_score": null, "level": null, "provider_verdict": "unavailable",
            "network_measured": false,
            "country": null, "asn": null, "asn_org": null, "connection": null,
            "tor": null, "datacenter": null, "anonymizer": null, "proxy": null, "vpn": null,
            "recent_abuse": null, "bot": null, "abuse_velocity": null,
            "geo_mismatch": null, "timezone_mismatch": null, "asn_incoherent": null,
            "ip_reuse_count": null, "reasons": [], "reason": "provider_unavailable" } }

// email_risk_plus: caixa analisada, entregabilidade real e idade do domínio garantidas
{ "module": "email_risk_plus", "passed": true, "outcome": "approved", "score": 100,
  "data": { "scope": "mailbox", "risk_score": 0, "level": "low", "provider_verdict": "deliverable",
            "email_domain": "gmail.com", "email_masked": "j***o@gmail.com", "email_fp": "b3f1c9…",
            "disposable": false, "disposable_listed": false, "undeliverable": false,
            "role_based": false, "plus_alias": false, "typosquat": false, "homoglyph": false,
            "masked_forwarder": false, "forwarder_listed": false, "suspicious_lexicon": false,
            "domain_age_days": 8200, "digits_heavy": false, "name_email_score": 0.82,
            "reasons": [], "reason": null } }

// email_risk_plus: caixa NÃO entrega, domínio descartável e recente (review, NUNCA decline)
{ "module": "email_risk_plus", "passed": false, "outcome": "failed", "score": 8,
  "data": { "scope": "mailbox", "risk_score": 92, "level": "high", "provider_verdict": "disposable",
            "email_domain": "tempmail.xyz", "email_masked": "u***r@tempmail.xyz", "email_fp": "7c14ab…",
            "disposable": true, "disposable_listed": true, "undeliverable": true,
            "role_based": false, "plus_alias": false, "typosquat": false, "homoglyph": false,
            "masked_forwarder": false, "forwarder_listed": false, "suspicious_lexicon": true,
            "domain_age_days": 9, "digits_heavy": false, "name_email_score": 0.05,
            "reasons": ["email_disposable", "email_undeliverable", "email_suspicious_lexicon",
                        "email_domain_recent"], "reason": null } }

// email_risk_plus: sem veredito do fornecedor -> DEFERE. O "data" VEM com null no que ninguém mediu:
// sobra o "scope" (o que teria sido analisado, caixa ou domínio) e nada do endereço, porque o
// deferimento não publica máscara, domínio nem pseudônimo. Só o MÓDULO sai da cobrança
{ "module": "email_risk_plus", "passed": null, "outcome": "pending", "score": 0,
  "data": { "scope": "mailbox", "risk_score": null, "level": null,
            "provider_verdict": "unavailable",
            "email_domain": null, "email_masked": null, "email_fp": null,
            "disposable": null, "disposable_listed": null, "undeliverable": null,
            "role_based": null, "plus_alias": null, "typosquat": null, "homoglyph": null,
            "masked_forwarder": null, "forwarder_listed": null, "suspicious_lexicon": null,
            "domain_age_days": null, "digits_heavy": null, "name_email_score": null,
            "reasons": [], "reason": "provider_unavailable" } }

// telefone_risco: linha móvel ativa, sem indício de abuso
{ "module": "telefone_risco", "passed": true, "outcome": "approved", "score": 100,
  "data": { "risk_score": 0, "level": "low", "provider_verdict": "phone_clean",
            "phone_masked": "+55 11 9****-**99", "phone_fp": "9f2c1a…", "country_code": "+55",
            "valid": true, "active": true, "line_class": "mobile", "carrier": "OPERADORA X S.A.",
            "voip": false, "prepaid": false, "risky": false, "recent_abuse": false,
            "high_risk": false, "reasons": [], "reason": null } }

// telefone_risco: linha VOIP com abuso recente e reputação de risco (review, NUNCA decline)
{ "module": "telefone_risco", "passed": false, "outcome": "failed", "score": 35,
  "data": { "risk_score": 65, "level": "high", "provider_verdict": "phone_risky",
            "phone_masked": "+55 11 9****-**21", "phone_fp": "4ab8e2…", "country_code": "+55",
            "valid": true, "active": true, "line_class": "voip", "carrier": "OPERADORA VIRTUAL LTDA",
            "voip": true, "prepaid": false, "risky": true, "recent_abuse": true,
            "high_risk": false,
            "reasons": ["phone_risky", "phone_recent_abuse", "phone_voip"], "reason": null } }

// telefone_risco: sem veredito do fornecedor -> DEFERE. O "data" VEM: máscara, pseudônimo e código
// do país continuam (eles são nossos, não do fornecedor), o resto sai null, e só o MÓDULO sai da
// cobrança
{ "module": "telefone_risco", "passed": null, "outcome": "pending", "score": 0,
  "data": { "risk_score": null, "level": null, "provider_verdict": "unavailable",
            "phone_masked": "+55 11 9****-**99", "phone_fp": "9f2c1a…", "country_code": "+55",
            "valid": null, "active": null, "line_class": null, "carrier": null,
            "voip": null, "prepaid": null, "risky": null, "recent_abuse": null,
            "high_risk": null, "reasons": [], "reason": "provider_unavailable" } }
```

Em sandbox, o mesmo sufixo do documento decide o caminho, e não há veredito ausente: o ambiente de testes não encena queda do fornecedor, do mesmo jeito que não encena queda da nossa própria infraestrutura. Aqui o sufixo troca o **dado devolvido**, e não o desfecho: `passed`, `outcome` e `score` continuam saindo da [tabela de sufixos do sandbox](https://unifokal.com/docs/ambientes#sandbox) (`00`, `01` e `02`), e qualquer outro sufixo aprova. Para o `ip_risk_plus`: `33` responde com rede de datacenter e `44` com saída Tor fora do país. Para o `email_risk_plus`: `33` domínio descartável, `44` caixa de função com alias e `55` escopo de domínio, com o veredito medido. Para o `telefone_risco`: `33` abuso recente, `44` linha VOIP e `55` número fora da faixa atribuída pela base. Ou seja: em sandbox dá para ver um payload de risco alto junto de `outcome: "approved"`, o que não acontece em produção. Programe contra os campos, nunca contra o par que o sandbox mostra.
