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 resolve →
401na 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,Keywordse 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/baselineslista os seus perfis e os incluídos, combuiltInseparando uns dos outros e um índice deconsistencypor dimensão. -
DELETE /api/v1/baselines/:issuerremove um dos seus. Um perfil incluído é compartilhado e responde404. - 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
policyfica 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[].ideseverity— 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ê querincremental-updatescom peso menor esignature-coveragecom 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 dodetail, mas como umcodeestável mais osparamsinterpolados 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. Odetailé renderizado a partir disso e não mudou, então nada que lêdetailhoje precisa ser alterado. Um sinal também pode trazernotes[]— frases complementares anexadas depois da principal, no mesmo formato; odetailé 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 sinalstructure-warningscomevidence.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 busye um headerRetry-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
- Referência OpenAPI Especificação legível por máquina e console de teste para cada endpoint.
- Guia de campo As onze famílias de sinais, chaves de evidência, causas legítimas, lógica de severidade.
- Triagem de extratos bancários Como ligar os sinais a uma decisão de crédito ou de onboarding.
- Forense de metadados de PDF O que o motor realmente lê, estrutura por estrutura.