Referência da API

Todo o comportamento da API, e por que cada decisão é o que é. O guia rápido é a versão de uma tela.

O contrato inteiro: autenticação, as duas formas de requisição, os endpoints vizinhos, o formato do relatório campo a campo, todos os códigos de erro e os limites em volta deles. Se você ainda não fez nenhuma chamada, o guia rápido cabe em uma tela e chega lá mais rápido. A especificação legível por máquina, com console de teste, está em /docs (em inglês).

1. Autenticação

Um bearer token no header Authorization. As chaves têm o prefixo tl_.

Authorization: Bearer tl_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

Três comportamentos que vale conhecer, porque são deliberados:

  • Sem header → anônimo. Permitido, com rate limit por IP.
  • Um header que não resolve401 na hora. Uma chave apresentada e inválida nunca degrada silenciosamente para anônimo, porque é essa degradação silenciosa que transforma uma chave rotacionada em produção num mistério em vez de num erro.
  • Uma chave válida → medida contra a cota mensal daquela chave, e totalmente isenta do rate limit por IP dos anônimos.

O endpoint de análise autentica só por header, nunca por cookie, então não há nada a configurar em CORS com credenciais e nada com cara de CSRF para se preocupar. Os endpoints de conta e cobrança são o oposto — cookie de sessão, corpo JSON obrigatório — e estão documentados em /docs.

2. Fazendo uma requisição

POST /api/v1/inspect aceita multipart/form-data com um campo chamado file, ou um corpo bruto. As duas formas são equivalentes; escolha a que for mais barata no seu cliente HTTP. O limite é 10 MB.

Três mídias, um endpoint e um formato de relatório. PDFs; documentos do Word, Excel e PowerPoint (.docx, .xlsx, .pptx e os equivalentes com macro); e imagens JPEG, PNG, WebP, HEIC/HEIF e AVIF. TIFF, GIF e BMP são reconhecidos e reportados, mas seus contêineres não são percorridos — e o relatório diz isso num sinal, porque um resultado vazio nunca pode ser confundido com um resultado limpo.

O formato é decidido farejando os bytes, nunca pelo cabeçalho Content-Type nem pelo nome do arquivo. Os relatórios trazem um discriminador mediaType"pdf", "office" ou "image" — mais um bloco aditivo office ou image; document continua preenchido nos três casos, então uma integração existente continua funcionando.

multipart/form-data

curl -s https://tamperlens.com/api/v1/inspect \
  -H "Authorization: Bearer tl_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" \
  -F [email protected]

corpo bruto application/pdf

curl -s https://tamperlens.com/api/v1/inspect \
  -H "Authorization: Bearer tl_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" \
  -H "Content-Type: application/pdf" \
  --data-binary @extrato.pdf

Node — corpo bruto, sem encoder de multipart

const pdf = await readFile("extrato.pdf");

const res = await fetch("https://tamperlens.com/api/v1/inspect", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.TAMPERLENS_KEY}`,
    "Content-Type": "application/pdf",
  },
  body: pdf,
});

if (!res.ok) throw new Error(`inspect falhou: ${res.status}`);
const report = await res.json();

if (report.summary.riskBand !== "low") {
  await enviarParaAnaliseManual(applicationId, report.signals);
}

Python

import os, requests

with open("extrato.pdf", "rb") as fh:
    res = requests.post(
        "https://tamperlens.com/api/v1/inspect",
        headers={
            "Authorization": f"Bearer {os.environ['TAMPERLENS_KEY']}",
            "Content-Type": "application/pdf",
        },
        data=fh.read(),
        timeout=30,
    )

res.raise_for_status()
report = res.json()
print(report["summary"]["riskBand"], [s["id"] for s in report["signals"]])

Verificando um arquivo contra o original: /compare

Quando você tem o original em mãos — você gerou o documento, ou guardou a cópia que o emissor enviou — POST /api/v1/compare responde a uma pergunta mais forte do que a análise isolada consegue: este arquivo é aquele original, sem alterações? Envie dois arquivos e receba os fatos em três camadas, da mais forte para a mais fraca.

Comparar um arquivo reenviado com o que está em registro

curl -s https://tamperlens.com/api/v1/compare \
  -H "Authorization: Bearer tl_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" \
  -F [email protected] \
  -F [email protected]
  • relationship: "identical" — os arquivos têm o mesmo hash. A única resposta que é uma prova, e não um sinal.
  • "candidate-extends-original" — o candidato contém o original byte a byte, mais atualizações incrementais anexadas. O relatório classifica o que as revisões anexadas tocam; ancestry.contentTouched é o fato principal — o conteúdo da página foi substituído depois que o original estava completo. "candidate-truncates-original" é o inverso: o candidato é uma revisão anterior do original.
  • "rewritten" — nenhum contém o outro, e a comparação recua com honestidade para a estrutura interpretada: metadados campo a campo, o par /ID, fontes, número e geometria das páginas. Nenhuma página é renderizada, então nenhum resultado afirma que a aparência visual é idêntica.

O /compare exige uma chave (não há acesso anônimo) e interpreta dois documentos, então é medido como 2 documentos na cota mensal. Apenas pares de PDF. O schema completo está em /docs, e o guia explica o que cada relação prova — e o que não prova.

Lendo os metadados de um arquivo: /metadata

O POST /api/v1/metadata responde a uma pergunta diferente da inspeção: não isto foi editado?, mas o que este arquivo diz sobre si mesmo? Todas as chaves, agrupadas pelo contêiner de onde vieram, com a forma armazenada ao lado da decodificada.

Liste tudo o que um documento carrega

curl -s https://tamperlens.com/api/v1/metadata \
  -F [email protected]
  • PDF — o dicionário Info completo, não só os cinco campos que as famílias de sinais leem. Author, Subject, Keywords e qualquer chave personalizada inventada por um sistema aparecem todas, além de todas as propriedades XMP e dos fatos estruturais (páginas, revisões, criptografia, assinaturas, fontes).
  • Imagens — EXIF separado por diretório, para que os fatos da miniatura continuem distinguíveis dos da fotografia; coordenadas de GPS decodificadas; blocos de texto de PNG; XMP. Contêineres presentes mas não enumerados (IPTC, ICC, MakerNote, C2PA) são nomeados em vez de omitidos em silêncio.
  • Office — propriedades core, de aplicação e personalizadas, e o EXIF de cada imagem embutida no pacote. Um .docx é um ZIP: uma foto colada nele mantém as coordenadas que a câmera escreveu, e isso costuma ser o mais surpreendente do relatório.

O campo personal reúne as entradas que identificam uma pessoa, um aparelho ou um lugar — nomes, números de série, programas, localizações — como cópia, para que a lista agrupada continue completa. Não há sinais, nota de risco nem veredicto; isso é trabalho do /inspect.

Somente leitura, por construção. A resposta é JSON e nada além de JSON. A Tamperlens não tem nenhum endpoint que devolva um arquivo modificado — nem cópia limpa, nem download sem metadados — porque produzir um significaria escrever um documento, e todas as promessas da página de segurança dependem de nunca fazermos isso. Se você precisa de um arquivo limpo, limpe localmente; nós dizemos exatamente o que remover.

Um arquivo, uma leitura, medido como 1 documento. Funciona anonimamente como o verificador gratuito, então a chave é opcional. A versão no navegador está em /pt/metadados.

Health check, sem autenticação: GET /api/v1/health.

A partir de um agente (MCP)

Se quem chama é um agente de LLM e não o seu próprio código, existe um servidor MCP que expõe os mesmos endpoints como três ferramentas — inspect_document, check_redaction e compare_documents. Ele é um invólucro fino sobre a API HTTP desta página; não há um segundo motor atrás dele.

{
  "mcpServers": {
    "tamperlens": {
      "command": "node",
      "args": ["/caminho/absoluto/para/tamperlens/dist/mcp/index.js"],
      "env": { "TAMPERLENS_API_KEY": "tl_..." }
    }
  }
}

As ferramentas recebem um caminho absoluto de arquivo, nunca base64 embutido: os bytes precisam chegar à Tamperlens, não ao modelo, e passar um documento pelo contexto de um agente custaria megabytes à toa.

Ainda não está no npm. O servidor roda a partir de um build local — clone o repositório, rode npm run build e aponte o seu host MCP para dist/mcp/index.js, como acima.

3. Baselines de emissor

As onze famílias de sinais acima respondem a uma pergunta: este arquivo foi alterado depois de gerado? Elas nada dizem sobre um documento fabricado do zero — gerado uma única vez a partir de um modelo e nunca mais tocado. Um arquivo assim é estruturalmente impecável, então nada dispara.

Um baseline de emissor responde à outra pergunta: este arquivo se parece com o que a instituição citada realmente emite? Toda instituição gera seus PDFs com uma cadeia de ferramentas fixa, e essa cadeia deixa uma impressão digital estrutural consistente — o mesmo produtor, a mesma versão de PDF, o mesmo conjunto de fontes incorporadas, a mesma geometria de página, o mesmo estilo de tabela de referências cruzadas, uma única revisão. Um extrato falso gerado com wkhtmltopdf é internamente coerente, mas errado para o banco cujo nome está nele — e isso é visível sem ler um único número da página.

O que um baseline guarda

Um perfil é apenas estrutura derivada: ele registra que os documentos de um emissor trazem o produtor X e a versão de PDF 1.4. Ele nunca contém os documentos, o texto deles, números de conta, nomes ou saldos — e ocupa algumas centenas de bytes. Os documentos de treino são lidos em memória e descartados exatamente como em uma análise, de modo que “documentos enviados nunca são armazenados” continua literalmente verdadeiro com este recurso ligado.

Informando o emissor em uma análise

Opcional e aditivo. Envie o slug do emissor como um campo multipart issuer (antes da parte do arquivo) ou como o cabeçalho X-Tamperlens-Issuer em um corpo cru. Os slugs seguem ^[a-z0-9][a-z0-9-]{0,63}$.

multipart

curl -s https://tamperlens.com/api/v1/inspect \
  -H "Authorization: Bearer tl_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" \
  -F issuer=chase \
  -F [email protected]

corpo cru

curl -s https://tamperlens.com/api/v1/inspect \
  -H "Authorization: Bearer tl_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" \
  -H "Content-Type: application/pdf" \
  -H "X-Tamperlens-Issuer: chase" \
  --data-binary @extrato.pdf

A resposta é o relatório de sempre mais um bloco no nível raiz. Uma divergência confiável também levanta um sinal issuer-mismatch em signals.

{
  "issuerBaseline": {
    "requested": "chase",
    "resolved": true,
    "source": "customer",
    "sampleCount": 18,
    "matchingActive": true,
    "divergence": 0.42,
    "mismatchedDimensions": ["producers", "fontSets"]
  }
}

Um slug desconhecido não é erro. Você recebe o relatório normal e {"requested": "chase", "resolved": false, "reason": "no_profile"}, para que “combinou” e “não havia com o que comparar” nunca se confundam — uma distinção que a ausência de um sinal não consegue expressar. Um slug malformado é 422 invalid_issuer, porque só pode ser um bug de quem chama. Omita issuer e a resposta é byte a byte a mesma de antes deste recurso existir: a chave nem aparece.

Treinando os seus

Os baselines que você treina com seus próprios documentos comprovadamente legítimos são privados da sua conta e sempre vencem os que já vêm incluídos. Um pequeno conjunto de perfis públicos de bancos acompanha o produto como alternativa. Perfis nunca são combinados entre contas.

curl -s https://tamperlens.com/api/v1/baselines \
  -H "Cookie: tl_session=$SESSION" \
  -F issuer=chase \
  -F label="JPMorgan Chase" \
  -F [email protected] -F [email protected] -F [email protected] \
  -F [email protected] -F [email protected] -F [email protected]
  • GET /api/v1/baselines lista os seus perfis e os incluídos, com builtIn separando uns dos outros e um índice de consistency por dimensão.
  • DELETE /api/v1/baselines/:issuer remove um dos seus. Um perfil incluído é compartilhado e responde 404.
  • Cinco documentos legítimos é o piso para que um perfil consiga apontar uma divergência, e uma dimensão em que os seus documentos discordam simplesmente não gera expectativa. Treinar o mesmo emissor de novo funde com o perfil existente, então um baseline pode crescer em lotes.
  • Treinar não consome cota de análise. O limite é outro: 25 documentos por requisição, 25 perfis por conta, 5 treinos por hora.

Estes três endpoints autenticam por sessão do painel, e não por chave de API — o jeito mais simples de usá-los é pelo painel.

4. Sua política de veredicto

A Tamperlens não decide se um documento é fraudulento, e a seção 9 explica por que isso é uma posição, e não uma esquiva. Mas você pode ter decidido — e, antes disto existir, precisava reimplementar essa decisão do seu lado: cada integrador escrevendo o mesmo if (riskScore >= 70) de um jeito ligeiramente diferente, nada disso visível para nós quando você perguntava por que um documento tirou a nota que tirou.

Então mande a regra junto com a requisição. As mesmas duas grafias do emissor: um campo policy no multipart, antes da parte do arquivo, ou um cabeçalho X-Tamperlens-Policy. Os dois carregam um objeto JSON.

curl -X POST https://tamperlens.com/api/v1/inspect \
  -H "Authorization: Bearer $TL_KEY" \
  -H 'X-Tamperlens-Policy: {"review":30,"reject":70,"rejectOn":["redaction-exposure"]}' \
  -H "Content-Type: application/pdf" \
  --data-binary @extrato.pdf

A resposta ganha um bloco, e nada mais muda:

"policy": {
  "verdict": "reject",          // accept | review | reject
  "riskScore": 45,              // a nota do próprio relatório, nunca recalculada
  "thresholds": { "review": 30, "reject": 70 },
  "reason": "signal",           // "score" quando um corte foi cruzado
  "triggeredBy": ["redaction-exposure"]
}
Campo Padrão O que faz
review 30 Nota a partir da qual o veredicto é review
reject 70 Nota a partir da qual o veredicto é reject
rejectOn [] Famílias de sinais que forçam reject, qualquer que seja a nota
reviewOn [] Famílias de sinais que forçam pelo menos review

Os cortes padrão são as próprias faixas do motor, então uma política sem configuração concorda com summary.riskBand em vez de discordar dele em silêncio. Uma família nomeada vence um corte, e rejectOn vence reviewOn — quem nomeia uma família está dizendo que ela importa independentemente do que a aritmética deu, e um corte capaz de vetar isso tornaria a regra apenas consultiva. Só sinais acima da severidade info podem disparar uma regra.

  • Ids de sinal desconhecidos são aceitos e simplesmente nunca casam. Renomear uma família não pode derrubar o seu pipeline.
  • Uma política malformada é 422 invalid_policy, nunca um retorno silencioso aos nossos padrões — um erro de digitação que deixasse você acreditando que uma regra está valendo quando não está é a única falha que isto não pode ter.
  • Omita e nada muda. A chave policy fica ausente da resposta, não nula, e o relatório é idêntico byte a byte ao que era antes deste recurso existir.

5. O relatório, anotado

Uma resposta 200 é um InspectionReport. O formato é um contrato estável; novas famílias de sinais são aditivas. Os campos de texto (title, detail, disclaimer) são gerados pelo motor em inglês — o verificador web traduz os títulos por família, mas a API devolve o texto original.

{
  // Identificador da chamada. O ÚNICO campo não determinístico da resposta.
  "id": "insp_2f1e9c04-7b3a-4d51-9f0e-6c2a18d4bb71",

  // Se isto mudar, os pesos do score podem ter mudado. Fixe nos seus testes.
  "engineVersion": "1.13.3",

  "summary": {
    "riskScore": 95,          // 0-100, agregação ponderada — não é probabilidade
    "riskBand": "high",       // "low" <30 · "elevated" 30-69 · "high" >=70
    "signalCount": 2,         // sinais com severidade acima de "info"
    "revisions": 1            // 1 = nunca sofreu atualização incremental
  },

  // Mais grave primeiro, depois em ordem alfabética por id. No máximo um por família.
  "signals": [
    {
      "id": "producer-fingerprint",     // uma das 10 ids estáveis de família
      "severity": "high",               // "info" | "low" | "medium" | "high"
      "title": "Consumer editing tool in the production chain (iLovePDF)",
      "detail": "The document's metadata names iLovePDF in its production chain. …",
      "message": {                      // motor 1.8.0 — o `detail` como código mais valores
        "code": "producer-fingerprint.base",
        "params": { "tools": "iLovePDF" }
      },
      "evidence": {                     // formato varia por família; sempre legível por máquina
        "tools": ["iLovePDF"],
        "producer": "iLovePDF",
        "creator": "iText 7.2.5",
        "revisions": 1,
        "fullPageImagePages": [1]       // corroboração: é por isso que é "high"
      }
    },
    {
      "id": "id-inconsistency",
      "severity": "medium",
      "title": "Trailer /ID marks the file as changed since creation",
      "detail": "The trailer /ID array holds two different identifiers. …",
      "evidence": {
        "idOriginal": "8f2c...a91b",
        "idCurrent": "41de...77c0",
        "trailerCount": 1
      }
    }
  ],

  // Fatos, não achados. Qualquer campo de texto pode vir null.
  "document": {
    "pages": 2,
    "producer": "iLovePDF",
    "creator": "iText 7.2.5",
    "creationDate": "2026-01-04T10:02:00.000Z",   // ISO-8601, ou null
    "modDate": "2026-01-06T18:41:00.000Z",
    "encrypted": false,
    "signed": false,
    "revisions": 1,     // uma revisão, mas /ID divergente: reescrito por inteiro
    "sizeBytes": 184320
  },

  "disclaimer": "Tamperlens reports risk signals, not authenticity verdicts. …"
}

Campos em que você realmente deve ramificar

  • summary.riskBand — a decisão grosseira de roteamento. O contrato das faixas é estreito de propósito: high é reservado para relatórios com pelo menos um sinal de severidade alta, então um acúmulo de achados fracos é limitado em 69 e nunca chega lá.
  • signals[].id e severity — os ids de família são strings estáveis. Ramifique nelas quando quiser peso por sinal em vez de um único limiar, e você vai querer: se o seu fluxo envolve legitimamente clientes assinando documentos, você quer incremental-updates com peso menor e signature-coverage com peso maior.
  • signals[].evidence — guarde. É pequeno, não contém conteúdo do documento e é exatamente o que um analista humano precisa. Trate o formato como específico por família e aditivo; não assuma que as chaves estão presentes.
  • signals[].message (motor 1.8.0) — o mesmo parágrafo do detail, mas como um code estável mais os params interpolados nele. Use se você apresenta os achados em outro idioma, ou com as suas próprias palavras: os params são valores (deslocamentos, contagens, números de objeto), então a sua frase continua exatamente tão precisa quanto a nossa. O detail é renderizado a partir disso e não mudou, então nada que lê detail hoje precisa ser alterado. Um sinal também pode trazer notes[] — frases complementares anexadas depois da principal, no mesmo formato; o detail é a concatenação.
  • document.encrypted — quando true, cinco famílias de sinais dependentes de conteúdo não rodaram, e o relatório diz isso por meio de um sinal structure-warnings com evidence.code: "encrypted-content-not-analysed". A ausência de um sinal de conteúdo nesse relatório não significa nada. Detalhes.

Cada família, suas chaves de evidência, suas causas legítimas e as regras exatas de severidade: o guia de campo.

6. Erros

Todos os erros são JSON com uma string error. Nada sobre o documento vaza no corpo de um erro.

Status Corpo Quando
400 {"error":"no_file"} requisição multipart sem a parte file
401 {"error":"invalid_api_key"} uma chave foi apresentada e não resolveu
403 {"error":"origin_not_allowed"} um navegador chamou a partir de uma origem que não é a nossa. Só navegador manda Origin, então um chamador no servidor nunca vê isso
413 {"error":"file_too_large","maxMb":10} corpo ou arquivo multipart acima do limite
413 {"error":"body_too_large","maxBytes":65536} o OUTRO 413: o limite pequeno de corpo que protege toda rota que não recebe arquivo. Você enviou algo que nunca foi um arquivo — mande o documento como upload. maxBytes é o limite da própria rota que recusou
422 {"error":"not_a_pdf"} os bytes não são um formato que a Tamperlens leia. O código é anterior ao suporte a Office e imagens e foi mantido para as integrações que dependem dele
422 {"error":"analysis_timeout","timeoutMs":15000} o documento esgotou o orçamento de CPU e a worker dele foi morta. Não faz sentido repetir — o problema é a entrada
422 {"error":"invalid_issuer"} o issuer enviado não é um slug (letras minúsculas, dígitos, hifens). Um slug desconhecido não é erro — você recebe o relatório normal, sem comparação com linha de base
422 {"error":"invalid_policy","message":"…"} não foi possível ler a política de veredicto, então nada foi decidido com ela. Recusada em vez de cair silenciosamente nos nossos padrões
422 {"error":"profile_rejected"} só no treino de linha de base. O perfil que o motor montou não passou nas nossas próprias verificações de privacidade e foi descartado em vez de guardado. Repetir não resolve; o motivo fica registrado do nosso lado
429 {"error":"quota_exceeded","quota":25,"used":25} chamador com chave acima da cota mensal. A chamada recusada não é cobrada
429 {"error":"rate_limited","retryAfterSeconds":60} chamadas rápidas demais — o limite anônimo por rede por hora, ou qualquer outro limitador do produto. Todos respondem com este código. Distinto de quota_exceeded de propósito: cota é o plano que você contratou, rate limit é o ritmo com que você chamou
503 {"error":"busy","retryAfterSeconds":5} todas as workers ocupadas e a fila cheia; Retry-After: 5 é enviado. Contrapressão honesta — repita
500 {"error":"internal_error"} culpa nossa. Genérico de propósito; o interno nunca vaza

Repita o 503 com backoff. Não repita o 422. No 429 quota_exceeded, enfileire em vez de descartar — o contador zera todo mês.

Note o que não é erro: um arquivo que o parser não consegue entender. Entrada ilegível ou hostil degrada para um 200 carregando um sinal structure-warnings, em vez de lançar exceção. Um PDF quebrado é um achado, não uma falha.

7. Rate limits e cotas

  • Anônimo: 10 documentos por hora por IP do cliente. Sem cota, sem chave, relatório completo. Pensado para o verificador web e para experimentar a API.
  • Com chave: sem limite por hora; uma cota mensal de documentos por chave, definida pelo plano. O uso é contado por análise bem-sucedida; uma chamada recusada por cota não conta.
  • Concorrência: as análises rodam num pool de worker threads com fila limitada e prazo rígido de 15 segundos por documento. Quando a fila enche, a API descarta carga com 503 busy e um header Retry-After, em vez de deixar a latência crescer sem limite.
  • Tamanho: 10 MB por documento.

Endpoints de sessão do painel. GET /api/v1/me e GET /api/v1/usage autenticam com o cookie de sessão do painel, não com um bearer token — uma requisição que leve apenas Authorization: Bearer … recebe 401 not_authenticated. Eles servem o painel; a superfície de API com chave é /inspect, /compare e /baselines.

Uso do mês corrente: GET /api/v1/me. Detalhamento por dia do mês atual: GET /api/v1/usage. Os dois também aparecem no painel.

Planos, cotas, pacotes de créditos e o que conta como um documento: a página de preços. Todo plano roda o mesmo motor — você paga por volume, nunca por detecção melhor.

8. Privacidade e determinismo

  • Nada é armazenado. Os bytes enviados são lidos em memória e descartados quando a resposta é escrita. Nenhum documento vai para disco, nenhum conteúdo de documento é logado e não existe etapa de revisão humana. O que fica é uma linha no registro de uso: qual chave, quando, quantos bytes, qual faixa de risco.
  • Nenhum terceiro no caminho da análise. Só CPU, sem modelos de ML, sem fornecedores externos de dados, sem chamadas de saída durante a análise.
  • Determinístico. Bytes idênticos produzem relatório idêntico, fora o id. Fixe fixtures, faça snapshot dos relatórios e teste regressão contra nós — isso é um uso previsto do plano gratuito, e se um bump de versão mudar um score, preferimos que você perceba antes de nós.
  • Retenção. As linhas de registro de uso de chamadas anônimas (que carregam um endereço IP) são apagadas depois de 90 dias; as com chave, depois de 400 dias.

A versão longa — como os uploads são tratados, o que fica retido, a lista de subprocessadores — está na página de segurança, e o compromisso em si está na política de privacidade.

9. Nossa postura quanto ao disclaimer

Todo relatório carrega esta frase, e ela é a posição real do produto, não enfeite jurídico (o motor a devolve em inglês; a tradução vem depois):

Tamperlens reports risk signals, not authenticity verdicts. Signals can have benign causes; combine them with your own decision logic.

A Tamperlens reporta sinais de risco, não veredictos de autenticidade. Sinais podem ter causas legítimas; combine-os com a sua própria lógica de decisão.

Concretamente, com o que isso nos compromete:

  • Nenhum veredicto NOSSO. Não existe "fraud": true, não existe booleano "authentic", e não vai existir. O relatório descreve o que é verdade sobre os bytes e para por aí.
    A política de veredicto não é exceção, e vale ser exato sobre o porquê: aquele bloco só aparece quando você manda uma regra, é calculado a partir de cortes que você escolheu, e não muda nenhuma nota, nenhuma faixa e nenhum sinal. É o seu julgamento devolvido com as entradas à mostra — o oposto de um fornecedor decidindo por você, e a razão de o código que o avalia estar deliberadamente fora do motor.
  • Causas legítimas são documentadas por sinal, no guia de campo, no mesmo nível de detalhe das maliciosas. Um PDF legitimamente salvo de novo vai disparar sinais. Isso não é bug; é o que "este arquivo foi modificado após a criação" significa.
  • Roteie, não recuse automaticamente. Mapeie faixas para filas de análise. Qualquer desenho que recuse uma pessoa por um único sinal estrutural vai recusar clientes reais que abriram o extrato no Preview.
  • Limites declarados. Sem OCR, sem parsing de transações, sem aritmética de saldo, sem checagem de identidade ou de titularidade da conta, sem validação criptográfica de assinatura e sem capacidade de detectar uma falsificação limpa gerada de uma vez a partir de um template falso. Um adversário determinado que conheça histórico de revisões consegue achatá-lo.

Próximos passos