---
name: htpbe-pdf-tamper-detection
description: Integrate the HTPBE API to detect whether a PDF was modified after it was created. Use when a workflow receives a PDF (bank statement, payslip, invoice, contract, certificate) and needs to verify it was not edited after issue. HTPBE analyzes the PDF file structure, not the visual content — it does not do OCR, KYC, or identity verification.
---

# HTPBE — PDF Tamper Detection API

HTPBE (htpbe.tech) detects post-creation modification of PDF files. It returns a
three-state verdict — `intact` / `modified` / `inconclusive` — plus named forensic
markers explaining the reasoning. It reads the file structure (metadata timestamps,
xref/incremental-update chains, digital signatures, font and page-assembly
consistency), not the rendered page. It does NOT do OCR, pixel comparison, content
forensics, KYC, or signature-validity cryptography.

## Get a key

Sign up at https://htpbe.tech/auth/signin — the API key is provisioned instantly, no
manual approval. Each account gets a **live** key (`htpbe_live_...`) and a **test** key
(`htpbe_test_...`). A live key works without a subscription; calls draw from the same
credit pool as web uploads. Keep the key in an env var (`HTPBE_API_KEY`), never in a URL.

**Integrate against the test key first — it is free and consumes no credits.** Test keys
only accept the fixed mock URLs (see "Test URLs" below) and return deterministic results,
so you can build and verify the whole request/response contract end to end before spending
anything.

## Base URL

```
https://api.htpbe.tech/v1
```

All endpoints require `Authorization: Bearer <key>`.

## The flow — two calls

`POST /analyze` runs the analysis synchronously but returns only a check `id`. Fetch the
full verdict from `GET /result/{id}` immediately after.

The input is a **publicly downloadable HTTP/HTTPS URL** to the PDF (max 10 MB, no auth,
no password). HTPBE downloads it server-side. There is no multipart file upload on the
API — host the file at a reachable URL (e.g. a signed S3/R2/GCS link) and pass that.

### 1. Submit

```bash
curl -sS -X POST https://api.htpbe.tech/v1/analyze \
  -H "Authorization: Bearer $HTPBE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"url": "https://example.com/documents/statement.pdf"}'
# -> HTTP 201 Created, Location: .../v1/result/<id>
# -> {"id": "3f9c8b7a-2e1d-4c5f-9b8e-7a6d5c4b3a21"}
```

The submit returns **201 Created** with only the `id` (also in the `Location` header).
Optional body field `original_filename` overrides the name stored with the result.

### 2. Retrieve the verdict

```bash
curl -sS https://api.htpbe.tech/v1/result/3f9c8b7a-2e1d-4c5f-9b8e-7a6d5c4b3a21 \
  -H "Authorization: Bearer $HTPBE_API_KEY"
```

```json
{
  "status": "modified",
  "modification_confidence": "high",
  "modification_markers": ["HTPBE_MULTIPLE_REVISION_LAYERS", "HTPBE_DATES_DISAGREE"],
  "creator": "Microsoft Excel",
  "producer": "Microsoft: Print To PDF",
  "usage_caution": {
    "safe_for_automated_adverse_decision": false,
    "recommended_action": "route_to_human_review",
    "message": "Route this document to a human reviewer before any decision."
  }
}
```

There is no nested `origin` object — the software is exposed as flat `creator` / `producer`
strings, and the reason for an `inconclusive` verdict is in `status_reason` (below).
The response also carries `filename`, `file_size`, `algorithm_version`, `creation_date` /
`modification_date`, and structure/signature counters (`xref_count`,
`has_incremental_updates`, `has_digital_signature`, etc.) — see the full docs.

### End to end (Python)

```python
import os, requests

API = "https://api.htpbe.tech/v1"
H = {"Authorization": f"Bearer {os.environ['HTPBE_API_KEY']}"}

def check_pdf(url: str) -> dict:
    submit = requests.post(f"{API}/analyze", headers={**H, "Content-Type": "application/json"},
                           json={"url": url})
    submit.raise_for_status()
    check_id = submit.json()["id"]
    result = requests.get(f"{API}/result/{check_id}", headers=H)
    result.raise_for_status()
    return result.json()

verdict = check_pdf("https://example.com/documents/statement.pdf")
print(verdict["status"], verdict.get("modification_markers"))
```

An official Python client also exists: `pip install htpbe` (https://pypi.org/project/htpbe/).

## Interpreting the verdict

`status` is one of:

- **`intact`** — no modification detected; document originates from an institutional
  source (bank, government, enterprise software).
- **`modified`** — structural evidence of post-creation edit. `modification_markers[]`
  names what was found.
- **`inconclusive`** — the integrity check does not apply. A `status_reason` field says
  why, one of: `consumer_software_origin`, `online_editor_origin` (iLovePDF / Smallpdf /
  PDF24), `scanned_document`, `unverifiable_metadata`, `filled_form_origin`, or
  `fill_sign_origin`. Default-branch any value you don't recognize. Treat `inconclusive`
  as "cannot certify", not as "clean".

**Branch your logic on the marker `id`, not on the label.** Markers are stable
machine-readable ids (e.g. `HTPBE_SIGNATURE_REMOVED`, `HTPBE_DATES_DISAGREE`,
`HTPBE_POST_SIGNATURE_EDIT`, `HTPBE_MULTIPLE_REVISION_LAYERS`). The full id -> human-label
dictionary is published at https://htpbe.tech/how — render the label from there, key your
code off the id. HTPBE returns named markers instead of a numeric risk score by design.

## Responsible use — do not auto-reject on the verdict alone

Every result carries a `usage_caution` object. `safe_for_automated_adverse_decision` is
always `false`: a structural verdict describes the file, not the person who sent it, and
must never be the sole basis for an automatic adverse decision (rejecting an applicant,
claimant, or customer). Branch on `recommended_action`:

- `route_to_human_review` (on `modified`) — send the case to a human before deciding.
- `request_from_issuer` (on `inconclusive`) — confirm the document with the issuing
  organization; inconclusive is not proof of fraud.
- `no_action` (on `intact`) — no adverse action, but still not a standalone authenticity
  guarantee.

Wire the verdict as a signal into a review workflow, not as an automatic decision.

## Errors to handle

Errors are JSON: `{"error": "...", "code": "...", "details": "..."}`.

- **402 `payment_required`** — no credits left. Add a credit pack or subscribe; then retry.
- **429 `server_at_capacity`** — honor the `Retry-After` header (seconds), then retry the
  same request. Use exponential backoff on repeats.
- **400** (`invalid_url_format`, `download_failed`) / **413 `file_too_large`** /
  **422 `invalid_pdf`** — client-side input problems. Do NOT retry unchanged; fix the URL,
  the size (>10 MB), or the file.
- **401 / 403** — bad key, or a test key used against a non-test URL.
- **500 `internal_error`** — transient; retry once.

Retry only on `429` and `500`. A `4xx` will not succeed on retry.

## Test URLs

With a test key (`htpbe_test_...`), only these fixed URLs work — each returns a
deterministic result and spends no credits. Pattern: `https://api.htpbe.tech/v1/test/{name}.pdf`.

- `.../v1/test/clean.pdf` — `intact`
- `.../v1/test/modified-high.pdf` — `modified` (high confidence)
- `.../v1/test/modified-after-sign.pdf` — `modified` (post-signature edit)
- `.../v1/test/signature-removed.pdf` — `modified` (signature removed)
- `.../v1/test/inconclusive.pdf` — `inconclusive`
- `.../v1/test/scanned-document.pdf` — `inconclusive` (scanned)
- `.../v1/test/trigger-402.pdf` — forces a `402 payment_required` so you can test that path

Full list and the synthetic id table: https://github.com/htpbe/docs (testing.md). Point a
test key at any real URL and it returns `403 test_url_required`.

## Constraints

- PDF only, max 10 MB, must be a public HTTP/HTTPS URL (no auth, no password).
- Structural forensics only — cannot flag a PDF recreated from scratch with clean
  metadata, and does not read page content.
- Not for court-admissible authentication or full KYC/identity verification.

## Reference

- API docs: https://htpbe.tech/api
- Forensic markers (id -> label): https://htpbe.tech/how
- OpenAPI 3.1 spec: https://htpbe.tech/openapi.json
- Full endpoint docs (incl. `GET /checks` list + filters): https://github.com/htpbe/docs
- Machine overview: https://htpbe.tech/llms.txt
- Pricing: https://htpbe.tech/pricing
