Developer-first SaaS API for EU VAT validation via VIES with Redis caching, change monitoring, and webhook notifications.
SaaS platform for PDF authenticity verification with a public REST API. Detects edits through 5-layer analysis of metadata, xref structure, digital signatures, and content.
Libraries
Databases
Services
Key Results

When a bank receives a signed loan contract, or a law firm gets a countersigned agreement, or an HR team processes an employment certificate — how do they verify the document has not been edited after signing? Most organisations have no reliable answer. They look at the signature icon in Acrobat and assume that means something. Often it does not.
Enterprise PDF verification tools charge $1–$1.50 per document and require sales calls, NDAs, and annual contracts before you can run a single test. There was no self-service option for teams that need to verify dozens or hundreds of documents a month without going through an enterprise procurement process. The gap was obvious: a product that works via API, explains what it found, and costs less than a coffee per day.
I built a self-service SaaS that answers one question fast and clearly: has this signed PDF been edited since it was signed? Upload a document — or send it through the API — and in under nine seconds you get a 0–100 risk score with the specific evidence behind it, not a black-box verdict you have to trust blindly.
The verdict is reliable because it is deterministic. A legitimate PDF has one internal revision table; every later edit adds another. That single, checkable fact — combined with metadata analysis, digital-signature validation, and fingerprinting of known editing tools — is more dependable than any heuristic guess, and it is reproducible: the same document always yields the same explained result.
Around that engine I built the full commercial product: sign-up, separate live and test API environments, tiered billing (a self-built billing layer handling mandates, proration, dunning, and EU VAT), and algorithm versioning so that when the detection logic improves, an older stored result is automatically flagged rather than silently trusted. For banks, law firms, and other regulated teams whose documents cannot leave their own infrastructure, an Enterprise Docker option runs the whole thing on-premise, addressing GDPR, HIPAA, and PCI DSS requirements.
What previously required an enterprise contract and per-document billing is now available via API for $15/month. Teams at banks, law firms, and HR departments can integrate PDF verification into their own workflows without a procurement process.
| Metric | Value |
|---|---|
| Analysis time | ≤9 seconds (within Vercel timeout) |
| Max file size | 10 MB (2.2× Vercel's 4.5 MB limit) |
| Detection markers | 7+ deterministic (iLovePDF, PDF24, QPDF, etc.) |
| Algorithm versions | 7 in 5 days (v2.0.0 → v2.1.3) |
| Database schema | 6 tables with dynamic quota calculation |
| Pricing tiers | $15–$499/month + Enterprise on-premise |
The deterministic approach means results are reproducible, explainable, and fast. Enterprise option (Docker/Kubernetes) addresses GDPR, HIPAA, and PCI DSS requirements for fintech and legal clients.
The category — "PDF authenticity verification API" — is small enough that organic discovery from developers searching for the exact problem is the primary acquisition channel. That makes technical SEO load-bearing, not optional:
SoftwareApplication, FAQPage, BreadcrumbList, and Organization — surfaces rich results for the long-tail queries this niche actually generates ("detect if PDF was edited", "verify digital signature PDF API")llms.txt describing the API, pricing, and the technical articles — the developer audience increasingly evaluates tools through ChatGPT and Perplexity before they ever click throughFor the technically curious, here is how the core pieces are built. One real constraint shaped the upload path: the platform's serverless functions cap request bodies at 4.5 MB, and real-world contracts and scans regularly exceed that.
A two-stage upload pattern: the browser uploads directly to Vercel Blob via a presigned URL (bypassing the serverless limit), then the server downloads and analyses it:
// components/Hero/index.tsx — Step 1: browser → Vercel Blob
const blob = await upload(filename, file, {
access: "public",
handleUploadUrl: "/api/blob-token",
clientPayload: JSON.stringify({ size: file.size }),
});
// Step 2: server downloads from Blob URL and analyzes
await analyzePdf(blob.url, file.name); // Server Action
// app/api/blob-token/route.ts — origin-based security
export async function POST(req: Request): Promise<Response> {
const origin = req.headers.get("origin") ?? "";
const isAllowed = ALLOWED_ORIGINS.some((o) => origin === o);
if (!isAllowed) return new Response("Forbidden", { status: 403 });
const body = await req.json();
const jsonResponse = await handleUpload({
body,
request: req,
onBeforeGenerateToken: async () => ({
allowedContentTypes: ["application/pdf"],
maximumSizeInBytes: 10 * 1024 * 1024,
}),
});
return Response.json(jsonResponse);
}This allows processing files up to 10 MB without infrastructure changes.
Analysis is split into independent layers. The critical marker is xref table counting: a legitimate PDF has one, each incremental update adds another. PDF 1.5+ complicates this with xref streams (different syntax):
// lib/services/pdf-structure.service.ts
private countXrefTables(pdfBuffer: Buffer): number {
let count = 0;
let pos = 0;
while (pos < pdfBuffer.length) {
// Classic xref tables: "xref\n" at start of line
const xrefIdx = pdfBuffer.indexOf(Buffer.from('xref\n'), pos);
// xref streams (PDF 1.5+): "/Type /XRef" in stream dictionary
const xrefStreamIdx = pdfBuffer.indexOf(Buffer.from('/Type /XRef'), pos);
const nextIdx = Math.min(
xrefIdx === -1 ? Infinity : xrefIdx,
xrefStreamIdx === -1 ? Infinity : xrefStreamIdx
);
if (nextIdx === Infinity) break;
count++;
pos = nextIdx + 5;
}
return count;
}The algorithm is versioned (v2.1.3), and the version is saved with each result. When requesting /api/v1/result/{uid}, outdated results are automatically flagged with algorithmOutdated: true.
Instead of storing a usage counter in the user row (denormalization → race conditions), quota is calculated dynamically from the checks table:
// lib/services/quota.service.ts
async checkQuota(userId: string): Promise<QuotaStatus> {
const user = await db.select().from(users).where(eq(users.id, userId)).get();
const startOfMonth = new Date();
startOfMonth.setDate(1);
startOfMonth.setHours(0, 0, 0, 0);
// Count from checks table — no stale counters, no race conditions
const result = await db
.select({ count: sql<number>`cast(count(*) as integer)` })
.from(checks)
.innerJoin(apiKeys, eq(checks.apiKeyId, apiKeys.id))
.where(
and(
eq(apiKeys.userId, userId),
gte(checks.checkDate, Math.floor(startOfMonth.getTime() / 1000))
)
);
const used = result[0]?.count ?? 0;
const limit = user.requestsPerMonth; // null = unlimited (Enterprise)
return { used, limit, remaining: limit === null ? null : limit - used };
}Keys follow format htpbe_{live|test}_{43-random-chars}. Test keys only accept mock URLs from whitelist — developers can test integration without consuming quota:
// app/api/v1/analyze/route.ts
const keyEnv = getApiKeyEnvironment(apiKey); // 'live' | 'test'
if (keyEnv === 'test') {
const TEST_URLS = ['https://htpbe.tech/samples/modified-high.pdf', ...];
if (!TEST_URLS.includes(file_url)) {
return Response.json({
error: 'Test keys only work with official HTPBE? sample URLs',
test_urls: TEST_URLS,
}, { status: 422 });
}
}
// Live keys: fetch any public PDF URL, analyze, bill quotaLong-Term Validation (LTV) adds timestamps and certificates to signed PDFs — this is legitimate modification that shouldn't trigger alerts:
// lib/services/pdf-ltv.service.ts
export function analyzeLTV(bytes: Uint8Array): LTVAnalysis {
const text = new TextDecoder("latin1").decode(bytes);
// Detect Document Security Store (DSS)
const hasDSS = /\/Type\s*\/DSS\b/.test(text);
// Detect Document Timestamp
const hasDTS =
/\/Type\s*\/DocTimeStamp\b/.test(text) || /\/SubFilter\s*\/ETSI\.RFC3161\b/.test(text);
// ETSI PAdES-LTV compliance markers
const isETSICompliant = hasDSS || hasDTS || /\/SubFilter\s*\/ETSI\.CAdES\.detached\b/.test(text);
return {
hasLTV: hasDSS || hasDTS,
hasDSS,
hasDTS,
isETSICompliant,
ltvDetails: isETSICompliant ? extractLTVDetails(bytes) : null,
};
}
AvailableNeed something similar?
I build custom solutions — from APIs to full products. Let's talk about your project.
Developer-first SaaS API for EU VAT validation via VIES with Redis caching, change monitoring, and webhook notifications.
International e-commerce platform with 30 locales, product configurators, AI chatbot, and fully automated order flow: Stripe → Zoho CRM → Airtable → Mailgun →
40+ point production SaaS checklist: auth, Stripe billing, PostgreSQL, rate limiting, email, monitoring, and security — with honest 8-week build estimates.
Stack
Databases
Topics
API key management for a public SaaS: hashing keys at rest, prefix + last-4 display, fail-closed validation, and revocation — plus the scoping, rotation, and