A Tamperlens tem uma superfície deliberadamente pequena:
POST /api/v1/inspect recebe um PDF e devolve sinais estruturais de risco
de fraude em JSON. Todo o resto desta página é autenticação, limites e tratamento de
erro. A especificação legível por máquina, com console de teste, está em
/docs (em inglês).
1. Pegue uma chave
Crie uma conta — e-mail e senha, sem cartão. O cadastro emite uma chave do plano Free na hora, com cota de 25 documentos por mês.
ou pelo terminal
curl -s -c /tmp/tl.jar https://tamperlens.com/api/v1/auth/signup \
-H 'Content-Type: application/json' \
-d '{"email":"[email protected]","password":"uma-senha-longa-o-suficiente"}'
# -> {"user":{…},"apiKey":{"plaintext":"tl_…","plan":"free","quota":25}}
A chave é exibida exatamente uma vez. Guardamos apenas um hash
SHA-256, então ela não pode ser exibida de novo — o painel mostra um preview não
reversível que não revela nada sobre o segredo. Se você perdê-la, rotacione a
chave no painel ou com
POST /api/v1/keys/rotate; a rotação revoga a chave antiga e emite
uma nova.
Você também pode chamar o endpoint sem chave nenhuma. Chamadas anônimas funcionam e devolvem o relatório completo — elas são limitadas por IP do cliente em vez de medidas contra uma cota. É esse o modo que o verificador web gratuito usa. Serve para experimentar e é errado para produção.
2. 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.
3. Fazendo uma requisição
POST /api/v1/inspect aceita multipart/form-data com um
campo chamado file, ou um corpo bruto application/pdf. As
duas formas são equivalentes; escolha a que for mais barata no seu cliente HTTP. O
limite é 20 MB e o corpo precisa começar com os bytes mágicos
%PDF-.
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"]])
Health check, sem autenticação: GET /api/v1/health.
4. 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.2.0",
"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. …",
"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— as dez 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. -
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 (em inglês).
5. 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 |
| 413 | {"error":"file_too_large","maxMb":20} |
corpo ou arquivo multipart acima do limite |
| 422 | {"error":"not_a_pdf"} |
o corpo não começa com %PDF- |
| 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 |
| 429 | {"error":"quota_exceeded","quota":25,"used":25} |
chamador com chave acima da cota mensal. A chamada recusada não é cobrada |
| 429 | corpo do rate limit | chamador anônimo acima do limite por hora por IP |
| 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.
6. 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: 20 MB por documento.
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.
7. Preços
| Plano | Preço | Documentos / mês | Observações |
|---|---|---|---|
| Free | US$ 0 | 25 | Relatório completo, sem marca d'água, sem cartão. Mais o verificador web. |
| Starter | US$ 49/mês | 1.000 | Cartão, autoatendimento. Sem reunião, sem contrato. |
| Growth | US$ 199/mês | 10.000 | Inclui suporte prioritário por e-mail. |
Todo plano recebe o mesmo motor, os mesmos sinais e a mesma latência. Não existe um tier enterprise que libere detecção melhor, porque não há detecção melhor para liberar.
Upgrades e downgrades são autoatendimento pelo painel (Stripe Checkout e o portal de cobrança da Stripe). Cancelar devolve a chave ao plano Free e à cota de 25 documentos, em vez de desativá-la. Acima de 10.000 documentos por mês, mande um e-mail — a resposta honesta é que preferimos conversar sobre o seu volume do que chutar um preço para ele.
25 documentos por mês, grátis, agora
Crie uma conta e a chave aparece na tela em uns quinze segundos. Ou teste sem conta alguma: o verificador gratuito roda o mesmo motor num arquivo arrastado para a página.
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.
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 campo de veredicto. Não existe
"fraud": true, não existe booleano"authentic", e não vai existir. A resposta reporta o que é verdade sobre os bytes. - 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 (em inglês) As dez famílias de sinais, chaves de evidência, causas legítimas, lógica de severidade.
- Triagem de extratos bancários (em inglês) Como ligar os sinais a uma decisão de crédito ou de onboarding.
- Forense de metadados de PDF (em inglês) O que o motor realmente lê, estrutura por estrutura.