Use case
Government Forms PDF API
Local governments and GovTech platforms issue thousands of permits, licenses, and certificates every day. Moving from manual form printing to programmatic PDF generation reduces processing time, eliminates formatting errors, and makes every document instantly available for download or email delivery.
Building permit (Node.js, TypeScript)
A Letter-format permit covering every required field: permit number, property address, parcel number, applicant and contractor names, project description, approving official, issue and expiration dates, a city seal, a legal disclaimer footer, and a QR code linking to the public verification URL. The CSS uses @page { size: Letter; margin: 1in } with a running page-number counter and official serif typography. Call with store: true so the returned URL is stable enough to embed in emails and inspection records.
// handlers/building-permit.ts
import { readFileSync } from "fs";
// templates/building-permit.html — Letter format, official serif typography,
// city seal, QR code linking to a verification URL, page numbers.
const PERMIT_HTML = `<!DOCTYPE html>
<html lang="en">
<head>
<title>Building Permit</title>
<style>
@page {
size: Letter;
margin: 1in;
@bottom-right { content: "Page " counter(page) " of " counter(pages); font-family: Georgia, serif; font-size: 10px; color: #555; }
}
body { font-family: Georgia, serif; color: #1d1812; margin: 0; }
.header { display: flex; justify-content: space-between; align-items: flex-start; border-bottom: 2px solid #1d1812; padding-bottom: 20px; margin-bottom: 28px; }
.seal img { height: 72px; }
.agency { text-align: right; }
.agency h1 { font-size: 18px; font-weight: 700; margin: 0 0 4px; }
.agency p { font-size: 12px; color: #555; margin: 0; }
.permit-title { text-align: center; font-size: 22px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.08em; margin-bottom: 8px; }
.permit-number { text-align: center; font-size: 14px; color: #555; margin-bottom: 32px; }
.fields-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 0; border: 1px solid #bbb; margin-bottom: 24px; }
.field { padding: 10px 14px; border-bottom: 1px solid #ddd; }
.field:nth-child(odd) { border-right: 1px solid #ddd; }
.field label { display: block; font-size: 10px; text-transform: uppercase; letter-spacing: 0.07em; color: #888; margin-bottom: 4px; }
.field span { font-size: 13px; font-weight: 600; }
.description-block { border: 1px solid #bbb; padding: 14px; margin-bottom: 24px; }
.description-block label { font-size: 10px; text-transform: uppercase; letter-spacing: 0.07em; color: #888; margin-bottom: 6px; display: block; }
.description-block p { font-size: 13px; line-height: 1.6; margin: 0; }
.disclaimer { background: #f9f5ef; border-left: 3px solid #d23a1d; padding: 12px 16px; font-size: 11px; line-height: 1.6; color: #555; margin-bottom: 24px; }
.approval-row { display: flex; justify-content: space-between; align-items: flex-end; margin-top: 40px; }
.approval-col { text-align: center; }
.approval-col .sig-line { border-bottom: 1px solid #1d1812; width: 220px; height: 36px; }
.approval-col .sig-label { font-size: 11px; color: #888; margin-top: 6px; }
.approval-col .sig-name { font-size: 13px; font-weight: 600; margin-top: 2px; }
.qr-block { text-align: center; }
.qr-block img { height: 80px; width: 80px; }
.qr-block p { font-size: 10px; color: #888; margin-top: 4px; }
</style>
</head>
<body>
<div class="header">
<div class="seal"><img src="{{sealUrl}}" alt="{{cityName}} Official Seal" /></div>
<div class="agency">
<h1>{{cityName}} Department of Building and Safety</h1>
<p>{{agencyAddress}}</p>
<p>{{agencyPhone}} | {{agencyEmail}}</p>
</div>
</div>
<div class="permit-title">Building Permit</div>
<div class="permit-number">Permit No. {{permitNumber}}</div>
<div class="fields-grid">
<div class="field"><label>Property Address</label><span>{{propertyAddress}}</span></div>
<div class="field"><label>Parcel Number (APN)</label><span>{{parcelNumber}}</span></div>
<div class="field"><label>Owner / Applicant</label><span>{{applicantName}}</span></div>
<div class="field"><label>Contractor</label><span>{{contractorName}}</span></div>
<div class="field"><label>Issue Date</label><span>{{issueDate}}</span></div>
<div class="field"><label>Expiration Date</label><span>{{expirationDate}}</span></div>
<div class="field"><label>Permit Type</label><span>{{permitType}}</span></div>
<div class="field"><label>Valuation</label><span>{{projectValuation}}</span></div>
</div>
<div class="description-block">
<label>Project Description</label>
<p>{{projectDescription}}</p>
</div>
<div class="disclaimer">
This permit is issued subject to the provisions of the municipal code and all applicable
state and federal regulations. Work must conform to approved plans on file with this office.
This permit expires if work does not commence within 180 days of issuance or if work is
suspended for more than 180 consecutive days. Verify permit validity at {{verificationUrl}}.
</div>
<div class="approval-row">
<div class="approval-col">
<div class="sig-line"></div>
<div class="sig-label">Approving Official</div>
<div class="sig-name">{{approvingOfficial}}</div>
<div class="sig-label">{{approvingTitle}}</div>
</div>
<div class="qr-block">
<img src="{{qrCodeUrl}}" alt="Permit verification QR code" />
<p>Scan to verify this permit</p>
</div>
</div>
</body>
</html>`;
type PermitPayload = {
permitNumber: string;
propertyAddress: string;
parcelNumber: string;
applicantName: string;
contractorName: string;
issueDate: string;
expirationDate: string;
permitType: string;
projectValuation: string;
projectDescription: string;
approvingOfficial: string;
approvingTitle: string;
sealUrl: string;
qrCodeUrl: string;
};
export async function generateBuildingPermit(permit: PermitPayload): Promise<string> {
let html = PERMIT_HTML;
const vars: Record<string, string> = {
cityName: "City of Riverside",
agencyAddress: "3900 Main Street, Riverside, CA 92522",
agencyPhone: "(951) 826-5341",
agencyEmail: "[email protected]",
verificationUrl: `https://permits.riversideca.gov/verify/${permit.permitNumber}`,
...permit,
};
for (const [key, value] of Object.entries(vars)) {
html = html.replaceAll(`{{${key}}}`, value);
}
const res = await fetch("https://api.pdfpipe.xyz/v1/pdf", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.PDFPIPE_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
html,
filename: `permit-${permit.permitNumber}.pdf`,
store: true,
options: { format: "Letter" },
}),
});
const { document_url } = await res.json();
return document_url;
}Business license (Python, webhook handler)
A Flask or FastAPI webhook handler that listens for the license.approved event from a permit management system. It fills the license HTML template, calls the PDF API with store: True, writes the document URL back to the permit record, and emails the applicant a direct download link.
# webhooks/license_approved.py
# Triggered when a permit management system fires a "license.approved" event.
import os
import httpx
from datetime import date, timedelta
PDFPIPE_KEY = os.environ["PDFPIPE_KEY"]
PDFPIPE_URL = "https://api.pdfpipe.xyz/v1/pdf"
LICENSE_TEMPLATE = """<!DOCTYPE html>
<html lang="en">
<head>
<title>Business License</title>
<style>
@page {{ size: Letter; margin: 1in; }}
body {{ font-family: Georgia, serif; color: #1d1812; margin: 0; }}
.border-frame {{ border: 6px double #1d1812; padding: 32px; min-height: calc(100vh - 2in - 64px); display: flex; flex-direction: column; }}
.header {{ text-align: center; border-bottom: 2px solid #1d1812; padding-bottom: 20px; margin-bottom: 24px; }}
.seal {{ height: 80px; margin-bottom: 12px; }}
h1 {{ font-size: 28px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.1em; margin: 0 0 6px; }}
.subtitle {{ font-size: 15px; color: #555; }}
.body-text {{ text-align: center; font-size: 14px; line-height: 1.8; margin-bottom: 24px; }}
.license-details {{ border: 1px solid #bbb; margin: 0 auto 28px; width: 80%; }}
.license-details tr td {{ padding: 10px 16px; font-size: 13px; border-bottom: 1px solid #ddd; }}
.license-details tr td:first-child {{ font-weight: 600; width: 40%; background: #f9f5ef; }}
.footer {{ margin-top: auto; display: flex; justify-content: space-between; align-items: flex-end; }}
.sig-col {{ text-align: center; }}
.sig-line {{ border-bottom: 1px solid #1d1812; width: 200px; height: 36px; }}
.sig-label {{ font-size: 11px; color: #888; margin-top: 6px; }}
.sig-name {{ font-size: 13px; font-weight: 600; margin-top: 2px; }}
.license-number {{ font-family: 'Courier New', monospace; font-size: 18px; font-weight: 700; text-align: center; letter-spacing: 0.15em; color: #d23a1d; margin-bottom: 20px; }}
</style>
</head>
<body>
<div class="border-frame">
<div class="header">
<img class="seal" src="{seal_url}" alt="{city_name} Official Seal" />
<h1>{city_name}</h1>
<div class="subtitle">Office of the City Clerk, Business Licensing Division</div>
</div>
<div class="license-number">License No. {license_number}</div>
<div class="body-text">
<p>This certifies that</p>
<p><strong style="font-size:18px">{business_name}</strong></p>
<p>located at {business_address}</p>
<p>is hereby authorized to conduct the business of</p>
<p><strong>{business_type}</strong></p>
<p>within the jurisdictional limits of {city_name}.</p>
</div>
<table class="license-details">
<tr><td>Owner / Registrant</td><td>{owner_name}</td></tr>
<tr><td>Issue Date</td><td>{issue_date}</td></tr>
<tr><td>Expiration Date</td><td>{expiration_date}</td></tr>
<tr><td>Tax Registration</td><td>{tax_registration}</td></tr>
<tr><td>State License</td><td>{state_license}</td></tr>
</table>
<div class="footer">
<div class="sig-col">
<div class="sig-line"></div>
<div class="sig-label">City Clerk</div>
<div class="sig-name">{city_clerk_name}</div>
</div>
<div class="sig-col">
<div class="sig-line"></div>
<div class="sig-label">Mayor</div>
<div class="sig-name">{mayor_name}</div>
</div>
</div>
</div>
</body>
</html>"""
def handle_license_approved(event: dict) -> None:
"""
Webhook handler for the 'license.approved' event from a permit management system.
Generates the business license PDF, stores it, and emails the applicant a download link.
"""
data = event["data"]
today = date.today()
expiration = today.replace(year=today.year + 1)
html = LICENSE_TEMPLATE.format(
city_name="City of Riverside",
seal_url="https://assets.riversideca.gov/seal.png",
license_number=data["license_number"],
business_name=data["business_name"],
business_address=data["business_address"],
business_type=data["business_type"],
owner_name=data["owner_name"],
issue_date=today.strftime("%B %d, %Y"),
expiration_date=expiration.strftime("%B %d, %Y"),
tax_registration=data.get("tax_registration", "Pending"),
state_license=data.get("state_license", "N/A"),
city_clerk_name="Maria Estrada",
mayor_name="Scott Bates",
)
response = httpx.post(
PDFPIPE_URL,
headers={
"Authorization": f"Bearer {PDFPIPE_KEY}",
"Content-Type": "application/json",
},
json={
"html": html,
"filename": f"business-license-{data['license_number']}.pdf",
"store": True,
"options": {"format": "Letter"},
},
timeout=30,
)
response.raise_for_status()
result = response.json()
# Persist the document URL in the permit management system
db.licenses.update(
data["license_id"],
{
"pdf_url": result["document_url"],
"pdf_id": result["document_id"],
"issued_at": today.isoformat(),
"expires_at": expiration.isoformat(),
},
)
# Email the applicant a download link
send_email(
to=data["applicant_email"],
subject=f"Your business license is ready: {data['business_name']}",
body=(
f"Dear {data['owner_name']},\n\n"
f"Your business license (No. {data['license_number']}) has been approved "
f"and is ready to download.\n\n"
f"Download: {result['document_url']}\n\n"
f"This license is valid until {expiration.strftime('%B %d, %Y')}. "
f"Please display it prominently at your place of business."
),
)Inspection certificate batch (Python asyncio)
After a daily inspection run, generate certificates for all passed properties in a single asynchronous operation using the batch endpoint. The records are chunked at 50 per request (the batch limit), dispatched concurrently with httpx.AsyncClient, and the returned document URLs are stored directly in the inspection records. No per-certificate HTTP loops needed.
# scripts/generate_inspection_certificates.py
# Run after the daily inspection sweep to issue certificates for all passed properties.
import asyncio
import os
import httpx
from datetime import date
PDFPIPE_KEY = os.environ["PDFPIPE_KEY"]
BATCH_URL = "https://api.pdfpipe.xyz/v1/pdf/batch"
CERT_TEMPLATE = """<!DOCTYPE html>
<html lang="en">
<head>
<title>Inspection Certificate</title>
<style>
@page {{ size: Letter; margin: 1in; }}
body {{ font-family: Georgia, serif; color: #1d1812; margin: 0; }}
.border-frame {{ border: 4px solid #1d1812; padding: 28px; }}
.header {{ text-align: center; margin-bottom: 24px; }}
h1 {{ font-size: 24px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.08em; margin: 0 0 6px; }}
.cert-type {{ font-size: 14px; color: #555; margin-bottom: 20px; }}
.pass-badge {{ display: inline-block; background: #1d5c2e; color: #fff; font-size: 13px;
font-weight: 700; text-transform: uppercase; letter-spacing: 0.1em; padding: 6px 18px;
border-radius: 3px; margin-bottom: 24px; }}
table {{ width: 100%; border-collapse: collapse; margin-bottom: 24px; }}
td {{ padding: 9px 12px; border-bottom: 1px solid #ddd; font-size: 13px; }}
td:first-child {{ font-weight: 600; width: 40%; background: #f9f5ef; }}
.inspector-block {{ display: flex; justify-content: space-between; margin-top: 36px; }}
.inspector-col {{ text-align: center; }}
.sig-line {{ border-bottom: 1px solid #1d1812; width: 200px; height: 36px; }}
.sig-label {{ font-size: 11px; color: #888; margin-top: 6px; }}
.sig-name {{ font-size: 13px; font-weight: 600; margin-top: 2px; }}
</style>
</head>
<body>
<div class="border-frame">
<div class="header">
<h1>City of Riverside</h1>
<div class="cert-type">Department of Building and Safety, Inspection Division</div>
<div style="font-size:20px;font-weight:700;margin:12px 0 4px">Certificate of Inspection</div>
<div class="pass-badge">PASSED</div>
</div>
<table>
<tr><td>Certificate No.</td><td>{certificate_number}</td></tr>
<tr><td>Property Address</td><td>{property_address}</td></tr>
<tr><td>Inspection Type</td><td>{inspection_type}</td></tr>
<tr><td>Inspection Date</td><td>{inspection_date}</td></tr>
<tr><td>Inspector</td><td>{inspector_name}, License #{inspector_license}</td></tr>
<tr><td>Related Permit</td><td>{permit_number}</td></tr>
<tr><td>Certificate Issued</td><td>{issue_date}</td></tr>
</table>
<p style="font-size:12px;color:#555;line-height:1.6">
This certificate confirms that the above-referenced property passed inspection on the
date noted. It does not constitute approval for occupancy unless combined with a valid
Certificate of Occupancy. Retain this document for your records.
</p>
<div class="inspector-block">
<div class="inspector-col">
<div class="sig-line"></div>
<div class="sig-label">Inspector Signature</div>
<div class="sig-name">{inspector_name}</div>
</div>
<div class="inspector-col">
<div class="sig-line"></div>
<div class="sig-label">Division Chief</div>
<div class="sig-name">{division_chief}</div>
</div>
</div>
</div>
</body>
</html>"""
def build_cert_html(record: dict) -> str:
today = date.today().strftime("%B %d, %Y")
return CERT_TEMPLATE.format(
certificate_number=record["certificate_number"],
property_address=record["property_address"],
inspection_type=record["inspection_type"],
inspection_date=record["inspection_date"],
inspector_name=record["inspector_name"],
inspector_license=record["inspector_license"],
permit_number=record["permit_number"],
issue_date=today,
division_chief="Robert Nakamura",
)
async def generate_certificates(passed_inspections: list[dict]) -> None:
"""
Generate inspection certificates for all properties that passed today's inspection run.
Uses the batch endpoint — one HTTP call per group of up to 50 records.
"""
async with httpx.AsyncClient(timeout=60) as client:
# Chunk into groups of 50 (batch endpoint limit)
for i in range(0, len(passed_inspections), 50):
chunk = passed_inspections[i : i + 50]
documents = [
{
"html": build_cert_html(r),
"filename": f"cert-{r['certificate_number']}.pdf",
"store": True,
"options": {"format": "Letter"},
}
for r in chunk
]
response = await client.post(
BATCH_URL,
headers={
"Authorization": f"Bearer {PDFPIPE_KEY}",
"Content-Type": "application/json",
},
json={"documents": documents},
)
response.raise_for_status()
results = response.json()["results"]
# Align results with input records and persist the document URLs
for record, result in zip(chunk, results):
db.inspections.update(
record["inspection_id"],
{
"certificate_pdf_url": result["document_url"],
"certificate_pdf_id": result["document_id"],
"certificate_issued_at": date.today().isoformat(),
},
)
# Entry point: called from the daily inspection cron after the run completes
if __name__ == "__main__":
today_passed = db.inspections.find_passed_today()
asyncio.run(generate_certificates(today_passed))
print(f"Certificates generated for {len(today_passed)} properties.")Accessibility of generated documents
The PDF output is rendered from HTML. Use semantic markup in your templates so the underlying document is accessible: use <h1>, <section>, proper <th> headers in tables, and alt text on seal and QR code images. Add a <title> element and lang="en" to the <html> tag. Screen readers that support tagged PDFs will surface the document structure correctly when the source HTML is structured this way.
Plans
| Plan | Documents/mo | Storage | Price |
|---|---|---|---|
| Hobby | 500 | 1 day | Free |
| Starter | 3,000 | 30 days | $19/mo |
| Growth | 15,000 | 365 days | $49/mo |
| Scale | 50,000 | 2 years | $149/mo |
| Business | 100,000 | 2 years | $499/mo |
Need more than 100,000 documents per month? Contact us for enterprise pricing.