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 da conta dona da chave (todas as chaves da conta dividem um único teto; rotacionar a chave não o zera), 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.
Para agentes: cheque o documento antes que o seu agente o leia
Um agente que abre um anexo já leu tudo o que o anexo diz. Um PDF ou um
.docx é um lugar cômodo para escrever uma frase endereçada ao
modelo, e não à pessoa, e um pipeline de extração entrega justamente
os campos que nenhum leitor humano olha. A Tamperlens classifica esses
portadores na entrada: valores do dicionário Info, inclusive as chaves
personalizadas que cada pipeline inventa, o pacote XMP, o conteúdo de
anotações, nomes e descrições de arquivos embutidos e, no lado Office,
docProps, propriedades personalizadas, texto de comentários e
trechos marcados como ocultos. A mesma leitura reporta ao lado a superfície
executável (JavaScript embutido, ações /Launch e arquivos anexos
), e a Tamperlens nunca executa, abre ou segue nenhuma delas.
A ordem útil, então, é checar e só depois ler: uma chamada
antes de os bytes chegarem ao contexto do seu modelo, ramificando em
document-injection-markers e
office-injection-markers como em qualquer outra família. O
payload não precisa viajar junto com o achado, ?redact=payload devolve o mesmo relatório
com toda string escrita pelo atacante elidida.
O que isto não lê, dito para você não supor. O classificador roda nos portadores acima, não no conteúdo de página de um PDF: texto escondido dentro do content stream não é checado em busca de instruções, porque separar uma mensagem oculta da camada de OCR invisível sob uma digitalização é trabalho especificado e não construído. Um relatório limpo aqui quer dizer que aqueles campos estavam limpos, não que o documento é seguro.
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.
Está no npm. A configuração acima continua valendo para
quem prefere fixar um build: clone o repositório, rode
npm run build e aponte o seu host MCP para
dist/mcp/index.js. Para todo o resto:
npx tamperlens-mcp
O motivo de ter demorado a ser publicável é que este repositório carrega o
servidor da API, então instalá-lo compilaria um driver de banco nativo para
um cliente que só fala HTTP. Isso está resolvido:
packages/mcp gera o tamperlens-mcp com
duas dependências de execução, o SDK do MCP e o zod, e
instala num projeto limpo sem nenhum build nativo.
A versão dele é a versão do motor, de propósito: o
[email protected] diz qual motor ele conversa, em vez de
contar as próprias publicações. Ele é um cliente HTTP, não carrega nenhuma
lógica de sinal, e uma versão nova do motor chega até você quando esta API é
publicada, não quando você atualiza o pacote.
3. Baselines de emissor
As dezenove 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.37.0",
"summary": {
"riskScore": 10, // 0-100, agregação ponderada — não é probabilidade
"riskBand": "low", // "low" <30 · "elevated" 30-69 · "high" >=70
"signalCount": 1, // 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 ids estáveis de família
"severity": "low", // "info" | "low" | "medium" | "high"
"title": "PDF 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.rewritten",
"params": { "tools": "iLovePDF" }
},
"evidence": { // formato varia por família; sempre legível por máquina
"tools": ["iLovePDF"],
"editorTools": ["iLovePDF"],
"origin": "pdf-editor", // um editor reescreve um PDF; ferramentas "authoring" o criam
"producer": "iLovePDF",
"creator": "iText 7.2.5",
"revisions": 1, // revisão única, então "low" — "medium" exige re-salvamento
"fullPageImagePages": []
}
},
{
"id": "id-inconsistency",
"severity": "info", // /ID divergente em arquivo de revisão única pontua zero
"title": "Trailer /ID elements differ on a file written once",
"detail": "The trailer /ID array holds two different identifiers. …",
"evidence": {
"idOriginal": "8f2c...a91b",
"idCurrent": "41de...77c0",
"trailerCount": 1,
"revisions": 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, // escrito uma única vez; o /ID divergente sozinho não prova nada
"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. -
evidence.payloadIsUntrusted: presente etrueemdocument-injection-markerseoffice-injection-markers, e desde o motor 1.30.0 também emoffice-hidden-contenteoffice-tracked-changes. Significa que as strings emevidence.fields[].payload,evidence.fields[].name,evidence.recoveredeevidence.authorsforam escritas por quem montou o documento, e nas famílias de injeção para serem lidas por um modelo de linguagem. Todo o resto daquele sinal (as categorias de indício, as contagens, que tipo de campo carregava, quais técnicas de ofuscação foram usadas) é nosso e é seguro. Não coloque a carga num prompt de modelo sem neutralizá-la. Nada escrito pelo autor do documento aparece emtitle,detailoumessage.params: o texto nomeia o tipo de campo a partir de uma lista fixa, porque uma chave personalizada do Info também é uma string escolhida por ele. Veja §5.1. -
document.encrypted, quando true, seis 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.
5.1 ?redact=payload
POST /api/v1/inspect?redact=payload devolve o mesmo relatório com
toda string escrita pelo autor do documento suprimida: payload,
name, recovered e authors viram
"[redacted]" e o sinal ganha
evidence.payloadRedacted: true, para você distinguir “nada foi
encontrado” de “isto não foi entregue a você”. Nada mais muda: a
pontuação, a faixa, a severidade, as categorias de indício e todas as contagens
são idênticas, e uma política de veredicto é avaliada sobre
o relatório sem supressão, então seu veredicto não muda com esse parâmetro.
Existe para um formato de integração, e ele é comum: você entrega documentos a um
modelo e quer o achado sem a frase que foi escrita para atacá-lo. Qualquer outro
valor de redact é ignorado em vez de rejeitado. Há exatamente uma
coisa a suprimir, e nada que um erro de digitação aqui pudesse desligar em
silêncio.
O servidor MCP faz isso sempre, e esse é o único lugar em que nosso padrão seguro difere do desta API. O resultado de uma ferramenta MCP é o contexto de um modelo, então devolver a carga ali entregaria a injeção pela nossa própria ferramenta, com um rótulo confiável. O resumo do MCP traz categorias de indício e contagens; busque o relatório por esta API quando uma pessoa precisar ler as palavras.
curl -X POST 'https://tamperlens.com/api/v1/inspect?redact=payload' \
-H 'Authorization: Bearer tl_...' \
--data-binary @curriculo.pdf
5.2 receipt: provar depois o que devolvemos
Nada é armazenado, então não conseguimos reproduzir um relatório para resolver
uma disputa. Por isso toda resposta do /api/v1/inspect termina num
receipt: uma assinatura HMAC sobre o corpo exato que devolvemos, com
a versão do motor e a hora em que foi emitida. Ela cobre tudo o que vem acima
dela na resposta, issuerBaseline e policy inclusive, e
depois de qualquer ?redact=payload; nunca cobre
a si mesma. Guarde o relatório e o recibo juntos. O recibo cobre o relatório, não
o arquivo: prova o que dissemos, nunca o que o documento era.
"receipt": {
"version": "v1",
"issuedAt": "2026-08-15T19:32:25.147Z",
"engineVersion": "1.37.0",
"reportHash": "7543fa3f…76ef",
"redacted": false,
"signature": "…"
}
Para conferir o par depois, POST /api/v1/receipt/verify com o
relatório exatamente como você o recebeu e o recibo ao lado. A chamada exige
chave (um recibo é evidência sobre o relatório de um cliente, e o oráculo fica
fora da internet anônima), tem rate limit e nunca conta na sua
cota: ela não lê documento nenhum.
curl -X POST https://tamperlens.com/api/v1/receipt/verify \
-H 'Authorization: Bearer tl_...' \
-H 'Content-Type: application/json' \
-d '{"report": {…o relatório, com ou sem a chave receipt…}, "receipt": {…}}'
# -> {"valid": true}
# -> {"valid": false, "reason": "bad_signature"} nós nunca emitimos este recibo
# -> {"valid": false, "reason": "report_mismatch"} recibo genuíno, relatório diferente
A assinatura é um HMAC, então só nós conseguimos verificá-la; uma assinatura de
chave publicada, que qualquer um confere offline, é a v2 pretendida, e
version está dentro dos bytes assinados para que um recibo v1
guardado continue verificando quando ela chegar. O argumento mais longo está em
/pt/seguranca.
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 e acima da margem
gratuita que a segue nos planos pagos. A chamada recusada não é cobrada.
Acompanhe o x-quota-state para ver isso chegando: ele diz
within, depois soft-ceiling quando você entra na
margem, e então exhausted
|
| 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.
Cabeçalhos de cota. Toda resposta com chave carrega
x-quota-limit, x-quota-used,
x-quota-remaining, x-quota-ceiling e
x-quota-state, em toda requisição, não só quando algo dá errado,
então não há condicional para escrever. O x-quota-ceiling é quantos
documentos são servidos de graça depois da cota antes de a API começar a
recusar; é 0 no plano Grátis, onde os créditos pré-pagos fazem esse
papel. Estão na lista de CORS, então código no navegador consegue lê-los entre
origens.
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 conta, definida pelo plano e dividida por todas as chaves dela. 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 dezenove 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.