PDF Verification API
Integrate PDF authenticity checking into your application with our simple REST API. Detect modifications, analyze metadata, and protect your document workflow.
Quick Start Example
# Basic usage - analyze any publicly accessible PDF
curl -X POST https://htpbe.tech/api/v1/analyze \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"url": "https://example.com/document.pdf"}'Free testing via browser: For individual PDF checks, visit our homepage. API access is for commercial integration only.
Simple, Transparent Pricing
Choose the plan that fits your needs. All plans include full API access and comprehensive documentation.
Free PDF Verification
For individual PDF checks without API access, use our web interface for free with unlimited checks. API access is for commercial integration only.
Included in All Plans
Starter
For small projects and testing
Cancel anytime
Growth
For growing businesses
Cancel anytime
Pro
For high-volume applications
Cancel anytime
API Documentation
Simple REST API with comprehensive PDF analysis
Complete API Reference on GitHub
For detailed field-by-field documentation including all possible values, error codes, and comprehensive examples, visit our GitHub documentation:
Quick Reference
| Method | Endpoint | Description |
|---|---|---|
POST | /api/v1/analyze | Analyze PDF from URL for modifications |
GET | /api/v1/result/{id} | Retrieve previously completed check by ID |
GET | /api/v1/checks | List all checks with filtering and pagination |
Base URL: https://htpbe.tech/api/v1
Authentication: All endpoints require Authorization: Bearer YOUR_API_KEY
Monthly Quota: Depends on your plan (Starter: 30/month, Growth: 350/month, Pro: 1,500/month, Enterprise: unlimited)
Analyze PDF Document
POST https://htpbe.tech/api/v1/analyzeRequest Headers
Authorization: Bearer YOUR_API_KEY
Content-Type: application/jsonGet your API key from the dashboard after signing up. Supports both htpbe_live_... (production) and htpbe_test_... (testing) keys.
Request Body
{
"url": "https://example.com/documents/contract.pdf",
"original_filename": "contract.pdf"
}url (required): Public URL to your PDF file. Must be accessible via HTTP/HTTPS.
original_filename (optional): Original filename of the document. Useful when the URL contains a generated or hashed filename (e.g. from Vercel Blob or S3). When provided, this name is stored and returned in results instead of the filename extracted from the URL.
Supported sources: AWS S3 (presigned URLs), Google Cloud Storage, Azure Blob, Dropbox shared links, your own CDN, or any publicly accessible URL.
Limitations: Max 10 MB file size. URL must be publicly accessible without authentication.
Response (201 Created)
{
"id": "3f9c8b7a-2e1d-4c5f-9b8e-7a6d5c4b3a21"
}Analysis is performed synchronously. The response contains only the check id — call GET /api/v1/result/{id} immediately after to retrieve the full analysis.
With test keys the ID is a deterministic UUID v4 like 00000000-0000-4000-8000-000000000001 — passes UUID format validation but is obviously synthetic.
Two-Step Usage
# Step 1: Submit for analysis
curl -s -X POST https://htpbe.tech/api/v1/analyze \
-H "Authorization: Bearer htpbe_live_..." \
-H "Content-Type: application/json" \
-d '{"url": "https://example.com/contract.pdf"}'
# → { "id": "3f9c8b7a-2e1d-4c5f-9b8e-7a6d5c4b3a21" }
# Step 2: Retrieve full result
curl https://htpbe.tech/api/v1/result/3f9c8b7a-2e1d-4c5f-9b8e-7a6d5c4b3a21 \
-H "Authorization: Bearer htpbe_live_..."
# → { "status": "modified", "origin": { ... }, ... }Retrieve Check Result
GET https://htpbe.tech/api/v1/result/{id}Description
Retrieve a previously completed PDF analysis by its unique check ID. Returns the full analysis data including metadata, structure, signatures, and findings. Only returns checks that belong to your API client.
Request Headers
Authorization: Bearer YOUR_API_KEYPath Parameters
id (required): Check ID returned from POST /api/v1/analyze (full UUID v4)
Response (200 OK)
{
"id": "506a6b1b-1360-48a2-b389-abb346f85d04",
"filename": "contract.pdf",
"check_date": 1736542583,
"file_size": 245632,
"algorithm_version": "2.2.1",
"current_algorithm_version": "2.2.1",
"status": "modified",
"origin": { "type": "institutional", "software": null },
"creation_date": 1704110400,
"modification_date": 1707840000,
"creator": "Adobe Acrobat Pro DC",
"producer": "Adobe PDF Library 15.0",
"modification_confidence": "certain",
"date_sequence_valid": true,
"metadata_completeness_score": 90,
"xref_count": 2,
"has_incremental_updates": true,
"update_chain_length": 3,
"pdf_version": "1.7",
"has_digital_signature": false,
"signature_count": 0,
"signature_removed": true,
"modifications_after_signature": false,
"page_count": 12,
"object_count": 487,
"has_javascript": false,
"has_embedded_files": false,
"modification_markers": [
"Digital signature was removed",
"Different creation and modification dates"
]
}All date fields (check_date, creation_date, modification_date) are Unix timestamps (seconds since epoch).
modification_markers: All modification signals detected, ordered strongest-first
algorithm_version: Version numbers reflect the algorithm in use at the time of analysis. The current version may differ.
Error Responses
// 404 Not Found - Check doesn't exist or belongs to another client
{
"error": "Check not found or access denied",
"code": "not_found"
}
// 401 Unauthorized - Invalid API key
{
"error": "Invalid API key. Please check your credentials.",
"code": "invalid_api_key"
}List All Checks
GET https://htpbe.tech/api/v1/checksDescription
Retrieve a paginated list of all your PDF check results with flexible filtering options. This endpoint provides raw data access for custom analytics, exports, and advanced reporting. Use it to build dashboards, export data, or perform custom analysis on your PDF checks.
Request Headers
Authorization: Bearer YOUR_API_KEYQuery Parameters (All Optional)
limit (1-500, default: 100): Number of results per page
offset (default: 0): Number of results to skip for pagination
tool: Filter by tool name (matches Creator OR Producer)
creator: Filter by Creator tool only
producer: Filter by Producer tool only
status (intact/modified/inconclusive): Filter by verdict
from_date / to_date (Unix timestamp): Filter by check date (when analysis was performed)
Response (200 OK)
{
"data": [
{
"id": "a3f5c9d2-1360-48a2-b389-abb346f85d04",
"filename": "invoice-2024-01.pdf",
"check_date": 1738368000,
"status": "modified",
"metadata_completeness_score": 85,
"creator": "Microsoft Word for Microsoft 365",
"producer": "Adobe PDF Library 15.0",
"file_size": 524288,
"page_count": 5,
"pdf_version": "1.7",
"creation_date": 1735689600,
"modification_date": 1738281600,
"has_javascript": false,
"has_digital_signature": true,
"has_embedded_files": false,
"has_incremental_updates": true,
"update_chain_length": 3,
"object_count": 234
}
],
"total": 1250,
"limit": 100,
"offset": 0,
"has_more": true
}Use cases: Export all data, build custom analytics, discover all tools, filter modified PDFs
Pagination: Usehas_moreto know when to stop
Example:/api/v1/checks?status=modified&limit=200
Error Responses
All errors include an error string and a machine-readable code. Some errors also include a details string with additional context. Requests beyond your monthly quota are charged at overage rates — there is no 429 cutoff.
| Code | Description |
|---|---|
400 | Bad Request — Invalid URL, malformed body, download failed |
401 | Unauthorized — Missing or invalid API key |
402 | Payment Required — No active subscription |
403 | Forbidden — Deactivated key, or test key used with non-test URL |
404 | Not Found — Check ID not found or belongs to a different API key |
413 | Payload Too Large — File exceeds 10 MB |
422 | Unprocessable Entity — Invalid or corrupted PDF |
500 | Internal Server Error — Processing failed |
Integration Examples
Get started quickly with these code examples
cURL
# Step 1: Submit PDF for analysis
curl -X POST https://htpbe.tech/api/v1/analyze \
-H "Authorization: Bearer htpbe_live_..." \
-H "Content-Type: application/json" \
-d '{"url": "https://example.com/document.pdf"}'
# Returns: {"id":"3f9c8b7a-2e1d-4c5f-9b8e-7a6d5c4b3a21"}
# Step 2: Retrieve full results
ID="3f9c8b7a-2e1d-4c5f-9b8e-7a6d5c4b3a21"
curl -s "https://htpbe.tech/api/v1/result/$ID" \
-H "Authorization: Bearer htpbe_live_..." \
| jq '.status'JavaScript/TypeScript (Node.js)
// Step 1: Submit PDF for analysis — returns only { id }
async function analyzePDF(fileUrl: string): Promise<string> {
const response = await fetch('https://htpbe.tech/api/v1/analyze', {
method: 'POST',
headers: {
'Authorization': `Bearer ${process.env.HTPBE_API_KEY}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({ url: fileUrl })
});
if (!response.ok) {
const error = await response.json();
throw new Error(`API error: ${error.error}`);
}
const { id } = await response.json();
return id;
}
// Step 2: Retrieve full results by ID
async function getResult(id: string) {
const response = await fetch(`https://htpbe.tech/api/v1/result/${id}`, {
headers: { 'Authorization': `Bearer ${process.env.HTPBE_API_KEY}` }
});
return response.json();
}
// Usage
const id = await analyzePDF('https://example.com/contract.pdf');
const result = await getResult(id);
switch (result.status) {
case 'modified':
console.log('Document has been modified!');
console.log(`Markers: ${result.modification_markers.join(', ')}`);
break;
case 'inconclusive':
if (result.status_reason === 'online_editor_origin') {
console.log('Cannot verify — processed through online editor');
} else if (result.status_reason === 'scanned_document') {
console.log('Cannot verify — scanned document (no text layer)');
} else {
console.log('Cannot verify — created with consumer software');
}
console.log(`Origin: ${result.origin.type}, software: ${result.origin.software}`);
break;
case 'intact':
console.log('Document is intact');
break;
}Python
import requests
import os
from typing import Dict, Any
class HTPBEClient:
def __init__(self, api_key: str):
self.api_key = api_key
self.base_url = 'https://htpbe.tech/api/v1'
def analyze(self, url: str) -> str:
"""Submit PDF for analysis, returns check ID"""
response = requests.post(
f'{self.base_url}/analyze',
headers={
'Authorization': f'Bearer {self.api_key}',
'Content-Type': 'application/json'
},
json={'url': url}
)
response.raise_for_status()
return response.json()['id']
def get_result(self, check_id: str) -> Dict[str, Any]:
"""Retrieve full analysis result"""
response = requests.get(
f'{self.base_url}/result/{check_id}',
headers={'Authorization': f'Bearer {self.api_key}'}
)
response.raise_for_status()
return response.json()
# Initialize client
client = HTPBEClient(os.getenv('HTPBE_API_KEY'))
# Two-step analysis
check_id = client.analyze('https://example.com/contract.pdf')
result = client.get_result(check_id)
# Check results
if result['status'] == 'modified':
print("Document has been modified!")
print("\nMarkers:")
for marker in result['modification_markers']:
print(f" • {marker}")
elif result['status'] == 'inconclusive':
reason = result.get('status_reason', '')
if reason == 'online_editor_origin':
print("Cannot verify — processed through online editor")
elif reason == 'scanned_document':
print("Cannot verify — scanned document (no text layer)")
else:
print("Cannot verify — created with consumer software")
else:
print("Document appears to be original")PHP
<?php
function analyzePDF($fileUrl, $apiKey) {
// Step 1: Submit for analysis
$ch = curl_init('https://htpbe.tech/api/v1/analyze');
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . $apiKey,
'Content-Type: application/json'
],
CURLOPT_POSTFIELDS => json_encode(['url' => $fileUrl])
]);
$response = curl_exec($ch);
curl_close($ch);
$data = json_decode($response, true);
$checkId = $data['id'];
// Step 2: Retrieve full result
$ch = curl_init("https://htpbe.tech/api/v1/result/{$checkId}");
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $apiKey]
]);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($httpCode !== 200) {
throw new Exception("API request failed: " . $response);
}
return json_decode($response, true);
}
// Usage
$result = analyzePDF(
'https://example.com/contract.pdf',
getenv('HTPBE_API_KEY')
);
if ($result['status'] === 'modified') {
echo "Document modified!\n";
echo "Markers: " . implode(", ", $result['modification_markers']) . "\n";
} elseif ($result['status'] === 'inconclusive') {
$reason = $result['status_reason'];
if ($reason === 'online_editor_origin') {
echo "Cannot verify — processed through online editor\n";
} elseif ($reason === 'scanned_document') {
echo "Cannot verify — scanned document (no text layer)\n";
} else {
echo "Cannot verify — created with consumer software\n";
}
} else {
echo "Document is original\n";
}Go
package main
import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
"os"
)
type AnalyzeRequest struct {
FileURL string `json:"url"`
}
type AnalyzeResponse struct {
ID string `json:"id"`
}
type AnalysisResult struct {
Status string `json:"status"`
ModificationMarkers []string `json:"modification_markers"`
}
func submitAnalysis(fileURL, apiKey string) (string, error) {
reqBody, _ := json.Marshal(AnalyzeRequest{FileURL: fileURL})
req, _ := http.NewRequest("POST",
"https://htpbe.tech/api/v1/analyze",
bytes.NewBuffer(reqBody))
req.Header.Set("Authorization", "Bearer "+apiKey)
req.Header.Set("Content-Type", "application/json")
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
return "", err
}
defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)
var data AnalyzeResponse
json.Unmarshal(body, &data)
return data.ID, nil
}
func getResult(id, apiKey string) (*AnalysisResult, error) {
req, _ := http.NewRequest("GET",
"https://htpbe.tech/api/v1/result/"+id, nil)
req.Header.Set("Authorization", "Bearer "+apiKey)
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)
var result AnalysisResult
json.Unmarshal(body, &result)
return &result, nil
}
func main() {
apiKey := os.Getenv("HTPBE_API_KEY")
id, err := submitAnalysis("https://example.com/contract.pdf", apiKey)
if err != nil {
panic(err)
}
result, err := getResult(id, apiKey)
if err != nil {
panic(err)
}
switch result.Status {
case "modified":
fmt.Println("Modified!")
for _, m := range result.ModificationMarkers {
fmt.Println(" •", m)
}
case "inconclusive":
fmt.Println("Cannot verify — consumer software, online editor, or scanned origin")
default:
fmt.Println("Original document")
}
}Ruby
require 'net/http'
require 'json'
require 'uri'
class HTPBEClient
def initialize(api_key)
@api_key = api_key
@base_url = 'https://htpbe.tech/api/v1'
end
def analyze(url)
uri = URI("#{@base_url}/analyze")
http = Net::HTTP.new(uri.host, uri.port)
http.use_ssl = true
request = Net::HTTP::Post.new(uri.path)
request['Authorization'] = "Bearer #{@api_key}"
request['Content-Type'] = 'application/json'
request.body = { url: url }.to_json
response = http.request(request)
JSON.parse(response.body)['id']
end
def get_result(check_id)
uri = URI("#{@base_url}/result/#{check_id}")
http = Net::HTTP.new(uri.host, uri.port)
http.use_ssl = true
request = Net::HTTP::Get.new(uri.path)
request['Authorization'] = "Bearer #{@api_key}"
response = http.request(request)
JSON.parse(response.body)
end
end
# Usage
client = HTPBEClient.new(ENV['HTPBE_API_KEY'])
check_id = client.analyze('https://example.com/contract.pdf')
result = client.get_result(check_id)
if result['status'] == 'modified'
puts "Document modified!"
puts "Markers: #{result['modification_markers'].join(', ')}"
elsif result['status'] == 'inconclusive'
puts "Cannot verify — consumer software, online editor, or scanned origin"
else
puts "Document is original"
endRetrieving Results and Check History
Examples of using the GET endpoints to retrieve check results and check history
Get Check Result by ID
// Retrieve a specific check result
const checkId = '506a6b1b-1360-48a2-b389-abb346f85d04';
const response = await fetch(
`https://htpbe.tech/api/v1/result/${checkId}`,
{
headers: {
'Authorization': `Bearer ${API_KEY}`
}
}
);
const result = await response.json();
console.log(`File: ${result.filename}`);
console.log(`Status: ${result.status}`);
console.log(`Markers: ${result.modification_markers.join(', ')}`);Analyze Specific Tool Usage
import requests
from urllib.parse import quote
# Get all modified PDFs for manual review
response = requests.get(
'https://htpbe.tech/api/v1/checks',
params={
'status': 'modified',
'limit': 100
},
headers={'Authorization': f'Bearer {API_KEY}'}
)
data = response.json()
print(f"Found {data['total']} modified PDFs")
print(f"\nShowing first {len(data['data'])} results:")
for check in data['data'][:5]:
print(f"\n{check['filename']}")
print(f" Tool: {check['creator']} → {check['producer']}")
print(f" Review: https://htpbe.tech/result/{check['id']}")Building a Dashboard
// Build a dashboard from /checks — no extra endpoints needed
async function fetchDashboardData(apiKey: string) {
const headers = { Authorization: `Bearer ${apiKey}` };
// Fetch all checks (paginate if needed)
const checksRes = await fetch(
'https://htpbe.tech/api/v1/checks?limit=500',
{ headers }
);
const { data: checks, total } = await checksRes.json();
// Calculate metrics from raw data
const modified = checks.filter((c) => c.status === 'modified').length;
const toolStats = new Map<string, { count: number; modified: number }>();
checks.forEach((check) => {
const tool = check.producer || 'Unknown';
const current = toolStats.get(tool) || { count: 0, modified: 0 };
toolStats.set(tool, {
count: current.count + 1,
modified: current.modified + (check.status === 'modified' ? 1 : 0)
});
});
return {
overview: {
total,
modified,
modificationRate: total > 0 ? ((modified / total) * 100).toFixed(1) : '0.0'
},
recentModified: checks
.filter((c) => c.status === 'modified')
.slice(0, 5)
.map((c) => ({ filename: c.filename, tool: c.producer })),
toolBreakdown: Array.from(toolStats.entries())
.map(([name, data]) => ({
name,
count: data.count,
modificationRate: ((data.modified / data.count) * 100).toFixed(1)
}))
.sort((a, b) => b.count - a.count)
};
}
const dashboardData = await fetchDashboardData(API_KEY);
console.log('Dashboard Data:', JSON.stringify(dashboardData, null, 2));Real-World Use Cases
1. Contract Verification Before Signing
// Verify contract hasn't been tampered with
async function verifyContract(contractUrl) {
// Step 1: Submit for analysis
const { id } = await fetch('https://htpbe.tech/api/v1/analyze', {
method: 'POST',
headers: {
'Authorization': 'Bearer ' + process.env.HTPBE_API_KEY,
'Content-Type': 'application/json'
},
body: JSON.stringify({ url: contractUrl })
}).then(r => r.json());
// Step 2: Get full result
const result = await fetch(`https://htpbe.tech/api/v1/result/${id}`, {
headers: { 'Authorization': 'Bearer ' + process.env.HTPBE_API_KEY }
}).then(r => r.json());
if (result.status === 'modified') {
return {
safe: false,
message: 'Contract has been modified. Do not sign!',
warnings: result.modification_markers
};
}
if (result.status === 'inconclusive') {
const reasons = {
online_editor_origin: 'Processed through online editor — original metadata stripped. Manual review required.',
scanned_document: 'Scanned document — no text layer to verify. Manual review required.',
consumer_software_origin: 'Origin unclear — created with consumer software. Manual review required.'
};
return {
safe: false,
message: reasons[result.status_reason] || 'Origin unclear. Manual review required.',
warnings: []
};
}
return { safe: true, message: 'Contract is intact and safe to review' };
}2. Bulk Document Verification
import asyncio
import aiohttp
async def analyze_bulk(urls: list[str], api_key: str):
"""Analyze multiple PDFs concurrently"""
headers = {'Authorization': f'Bearer {api_key}'}
async with aiohttp.ClientSession() as session:
# Step 1: Submit all PDFs for analysis
submit_tasks = [
session.post(
'https://htpbe.tech/api/v1/analyze',
headers={**headers, 'Content-Type': 'application/json'},
json={'url': url}
)
for url in urls
]
submit_responses = await asyncio.gather(*submit_tasks)
ids = [(await r.json())['id'] for r in submit_responses]
# Step 2: Retrieve all results
result_tasks = [
session.get(
f'https://htpbe.tech/api/v1/result/{id}',
headers=headers
)
for id in ids
]
result_responses = await asyncio.gather(*result_tasks)
results = [await r.json() for r in result_responses]
modified_count = sum(1 for r in results if r['status'] == 'modified')
inconclusive_count = sum(1 for r in results if r['status'] == 'inconclusive')
return {
'total': len(results),
'modified': modified_count,
'inconclusive': inconclusive_count,
'intact': len(results) - modified_count - inconclusive_count,
'details': results
}
# Process 100 documents in parallel
urls = [f'https://storage.example.com/doc_{i}.pdf' for i in range(100)]
summary = await analyze_bulk(urls, os.getenv('HTPBE_API_KEY'))
print(f"Scanned {summary['total']} docs: {summary['modified']} modified, {summary['inconclusive']} inconclusive, {summary['intact']} intact")3. Document Management System Integration
// Automatic verification on upload
async function handleDocumentUpload(file: File) {
// 1. Upload to your storage
const fileUrl = await uploadToS3(file);
// 2. Submit for analysis
const { id } = await fetch('https://htpbe.tech/api/v1/analyze', {
method: 'POST',
headers: {
'Authorization': `Bearer ${HTPBE_API_KEY}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({ url: fileUrl })
}).then(r => r.json());
// 3. Retrieve full result
const result = await fetch(`https://htpbe.tech/api/v1/result/${id}`, {
headers: { 'Authorization': `Bearer ${HTPBE_API_KEY}` }
}).then(r => r.json());
// 4. Store in database with verification status
await db.documents.create({
filename: file.name,
url: fileUrl,
verified: result.status === 'intact',
uploaded_at: new Date()
});
// 5. Alert if modified
if (result.status === 'modified') {
await notifySecurityTeam({
document: file.name,
findings: result.modification_markers
});
}
return result;
}LLM-Friendly Documentation
For AI assistants and LLM integration, our API documentation is available in a machine-readable format optimized for language models.
View llms.txtDon't Trust Blindly — Check Your Document
Our free tool analyzes PDFs to detect modifications.
No registration required. Instant results.