Início rápido da API

Um endpoint, duas formas de requisição, um relatório JSON. Sem SDK — e não pretendemos oferecer um.

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 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.

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[].id e severity — 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ê 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.
  • 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 (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 busy e um header Retry-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