Integrate the HTPBE API to detect whether a PDF was modified after it was created...
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.
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.
https://api.htpbe.tech/v1
All endpoints require Authorization: Bearer <key>.
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.
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.
curl -sS https://api.htpbe.tech/v1/result/3f9c8b7a-2e1d-4c5f-9b8e-7a6d5c4b3a21 \
-H "Authorization: Bearer $HTPBE_API_KEY"
{
"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.
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/).
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.
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 are JSON: {"error": "...", "code": "...", "details": "..."}.
payment_required — no credits left. Add a credit pack or subscribe; then retry.server_at_capacity — honor the Retry-After header (seconds), then retry the
same request. Use exponential backoff on repeats.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.internal_error — transient; retry once.Retry only on 429 and 500. A 4xx will not succeed on retry.
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 pathFull 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.
GET /checks list + filters): https://github.com/htpbe/docs