HTML Form to PDF: Convert Web Forms to PDF with an API
HTML forms are everywhere — application forms, intake questionnaires, government permits, insurance underwriting, client contracts. Once a user submits a form, the next step is almost always producing a signed, archivable PDF. The naive approach (browser print dialog) produces inconsistent results; the heavy approach (Puppeteer spinning up Chromium) adds 300MB to your deployment. A PDF form generator API sits cleanly in between: send HTML, receive a pixel-perfect PDF.
This guide shows how to convert HTML forms to PDF using the FUNBREW PDF API, covers the most common form types (application forms, contracts, survey responses), and provides working code in Node.js, Python, and PHP.
Try a sample form conversion right now in the Playground — no account required.
Why dedicated form-to-PDF conversion beats browser print
| Method | Layout consistency | Server-side | CJK fonts | Production-ready |
|---|---|---|---|---|
| Browser print dialog | Varies per browser | No | Varies | No |
| Puppeteer (self-hosted) | Excellent | Yes | Manual setup | Complex |
| wkhtmltopdf | Moderate | Yes | Limited | Aging |
| FUNBREW PDF API | Excellent | Yes | Built-in | Yes |
The key advantage of a pdf form generator api is that your server controls every pixel: fonts, colors, page breaks, and margins are identical on every generation, regardless of which browser or OS the end-user happens to be on.
For a full comparison of approaches see the HTML to PDF complete guide.
Core concept: render the filled form as HTML, then convert
The workflow is always the same regardless of form type:
- User fills the form (your frontend, a CMS, a CLI tool — anything)
- On submit, your backend assembles an HTML string with the submitted values baked in
- POST the HTML to the FUNBREW PDF API
- Stream or store the returned PDF bytes
User → Form submission → Backend → FUNBREW PDF API → PDF bytes → Storage or response
The HTML you send can be as simple as a <table> of key-value pairs, or as elaborate as a multi-page document with custom fonts and a company header. The API renders it the same way a Chromium browser would.
Quick start: convert your first form to PDF
# One-liner to convert an HTML form to PDF
curl -X POST https://api.pdf.funbrew.cloud/v1/pdf/from-html \
-H "Authorization: Bearer $FUNBREW_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"html": "<h1>Application Form</h1><p><strong>Name:</strong> Jane Smith</p><p><strong>Email:</strong> jane@example.com</p>",
"engine": "quality",
"format": "A4",
"margin": { "top": "20mm", "bottom": "20mm", "left": "20mm", "right": "20mm" }
}' \
--output application.pdf
Get your API key from the dashboard or test with the Playground first.
Form type 1: Application and registration forms
Application forms — job applications, school enrollment, permit requests — need a consistent, professional appearance and must be archivable.
Node.js example
// generate-application-pdf.js
import { htmlToPdf } from './pdf-client.js'; // see pdf-generation-nodejs-guide
function buildApplicationHtml(data) {
const {
applicantName,
email,
phone,
dateOfBirth,
address,
notes = '',
submittedAt = new Date().toLocaleString('en-US'),
} = data;
return `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<style>
@page { size: A4; margin: 20mm 15mm; }
* { box-sizing: border-box; }
body {
font-family: -apple-system, 'Segoe UI', Arial, sans-serif;
font-size: 12px;
color: #1e293b;
-webkit-print-color-adjust: exact;
print-color-adjust: exact;
}
.header {
border-bottom: 3px solid #2563eb;
padding-bottom: 12px;
margin-bottom: 24px;
}
h1 { font-size: 22px; color: #2563eb; margin: 0 0 4px; }
.meta { color: #64748b; font-size: 11px; }
.section { margin-bottom: 20px; }
.section-title {
font-size: 13px;
font-weight: 700;
color: #475569;
text-transform: uppercase;
letter-spacing: 0.05em;
border-bottom: 1px solid #e2e8f0;
padding-bottom: 4px;
margin-bottom: 10px;
}
.field-row {
display: grid;
grid-template-columns: 160px 1fr;
gap: 4px;
padding: 6px 0;
border-bottom: 1px solid #f1f5f9;
}
.field-label { font-weight: 600; color: #64748b; }
.notes-box {
background: #f8fafc;
border: 1px solid #e2e8f0;
border-radius: 4px;
padding: 10px;
min-height: 60px;
white-space: pre-wrap;
}
.footer {
margin-top: 32px;
font-size: 10px;
color: #94a3b8;
text-align: center;
}
</style>
</head>
<body>
<div class="header">
<h1>Application Form</h1>
<div class="meta">Reference: APP-${Date.now()} | Submitted: ${submittedAt}</div>
</div>
<div class="section">
<div class="section-title">Applicant Information</div>
<div class="field-row"><span class="field-label">Full Name</span><span>${applicantName}</span></div>
<div class="field-row"><span class="field-label">Email</span><span>${email}</span></div>
<div class="field-row"><span class="field-label">Phone</span><span>${phone}</span></div>
<div class="field-row"><span class="field-label">Date of Birth</span><span>${dateOfBirth}</span></div>
<div class="field-row"><span class="field-label">Address</span><span>${address}</span></div>
</div>
<div class="section">
<div class="section-title">Notes</div>
<div class="notes-box">${notes || '—'}</div>
</div>
<div class="footer">
This document was generated automatically. For inquiries, contact support@example.com
</div>
</body>
</html>`;
}
// Express route example
app.post('/api/applications/submit', async (req, res) => {
const formData = req.body; // validated upstream
try {
const html = buildApplicationHtml(formData);
const pdf = await htmlToPdf(html, {
engine: 'quality',
format: 'A4',
margin: { top: '20mm', bottom: '20mm', left: '15mm', right: '15mm' },
});
// Option A: stream PDF directly back to the client
res.set({
'Content-Type': 'application/pdf',
'Content-Disposition': `attachment; filename="application-${Date.now()}.pdf"`,
'Content-Length': pdf.length,
});
res.send(pdf);
// Option B: save to S3 and return a URL (see pdf-generation-nodejs-guide)
} catch (err) {
res.status(500).json({ error: 'PDF generation failed', detail: err.message });
}
});
Python example
# generate_application_pdf.py
import os
import requests
from datetime import datetime
FUNBREW_API_KEY = os.environ["FUNBREW_API_KEY"]
FUNBREW_API_URL = os.environ.get("FUNBREW_API_URL", "https://api.pdf.funbrew.cloud/v1")
def build_application_html(data: dict) -> str:
submitted_at = datetime.now().strftime("%Y-%m-%d %H:%M")
return f"""<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<style>
body {{ font-family: sans-serif; font-size: 12px; color: #1e293b; padding: 30px; }}
h1 {{ color: #2563eb; border-bottom: 2px solid #2563eb; padding-bottom: 8px; }}
.row {{ display: grid; grid-template-columns: 160px 1fr; padding: 6px 0;
border-bottom: 1px solid #f1f5f9; }}
.lbl {{ font-weight: 600; color: #64748b; }}
</style>
</head>
<body>
<h1>Application Form</h1>
<p style="color:#64748b;font-size:11px">Submitted: {submitted_at}</p>
<div class="row"><span class="lbl">Full Name</span><span>{data['applicantName']}</span></div>
<div class="row"><span class="lbl">Email</span><span>{data['email']}</span></div>
<div class="row"><span class="lbl">Phone</span><span>{data['phone']}</span></div>
<div class="row"><span class="lbl">Address</span><span>{data['address']}</span></div>
<div style="margin-top:16px">
<strong>Notes:</strong>
<div style="background:#f8fafc;border:1px solid #e2e8f0;padding:10px;margin-top:6px">
{data.get('notes', '—')}
</div>
</div>
</body>
</html>"""
def html_form_to_pdf(form_data: dict) -> bytes:
html = build_application_html(form_data)
resp = requests.post(
f"{FUNBREW_API_URL}/pdf/from-html",
headers={"Authorization": f"Bearer {FUNBREW_API_KEY}"},
json={
"html": html,
"engine": "quality",
"format": "A4",
"margin": {"top": "20mm", "bottom": "20mm", "left": "15mm", "right": "15mm"},
},
timeout=30,
)
resp.raise_for_status()
return resp.content
# Usage
if __name__ == "__main__":
form = {
"applicantName": "Jane Smith",
"email": "jane@example.com",
"phone": "+1-555-0100",
"address": "123 Main St, Springfield, IL 62701",
"notes": "Referred by the community outreach program.",
}
pdf_bytes = html_form_to_pdf(form)
with open("application.pdf", "wb") as f:
f.write(pdf_bytes)
print(f"PDF saved: {len(pdf_bytes):,} bytes")
Form type 2: Contracts and agreements
Contracts need a signature block and sometimes a watermark for draft status. The HTML approach makes both trivial.
PHP example with signature block
<?php
// generate_contract_pdf.php
function buildContractHtml(array $data): string
{
$clientName = htmlspecialchars($data['clientName']);
$providerName = htmlspecialchars($data['providerName']);
$serviceDesc = htmlspecialchars($data['serviceDescription']);
$startDate = htmlspecialchars($data['startDate']);
$value = number_format($data['contractValue'], 2);
$isDraft = $data['isDraft'] ?? false;
$generatedAt = date('Y-m-d H:i');
$watermark = $isDraft
? '<div style="position:fixed;top:40%;left:50%;transform:translate(-50%,-50%) rotate(-30deg);
font-size:100px;color:rgba(239,68,68,0.12);font-weight:900;
white-space:nowrap;pointer-events:none;z-index:9999;">DRAFT</div>'
: '';
return <<<HTML
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<style>
@page { size: A4; margin: 25mm 20mm; }
body {
font-family: 'Times New Roman', Times, serif;
font-size: 11px;
color: #1e293b;
line-height: 1.6;
-webkit-print-color-adjust: exact;
print-color-adjust: exact;
}
h1 { font-size: 18px; text-align: center; margin-bottom: 4px; }
.subtitle { text-align: center; font-size: 11px; color: #64748b; margin-bottom: 32px; }
.clause { margin-bottom: 16px; }
.clause-title { font-weight: 700; margin-bottom: 4px; }
.sig-section {
margin-top: 48px;
display: grid;
grid-template-columns: 1fr 1fr;
gap: 40px;
}
.sig-block { border-top: 1px solid #1e293b; padding-top: 8px; }
.sig-label { font-size: 10px; color: #64748b; }
</style>
</head>
<body>
{$watermark}
<h1>Service Agreement</h1>
<div class="subtitle">Generated: {$generatedAt}</div>
<div class="clause">
<div class="clause-title">1. Parties</div>
<p>This Agreement is entered into between <strong>{$providerName}</strong> ("Provider")
and <strong>{$clientName}</strong> ("Client").</p>
</div>
<div class="clause">
<div class="clause-title">2. Services</div>
<p>{$serviceDesc}</p>
</div>
<div class="clause">
<div class="clause-title">3. Term and Value</div>
<p>Services commence on <strong>{$startDate}</strong>.
Total contract value: <strong>\${$value}</strong>.</p>
</div>
<div class="clause">
<div class="clause-title">4. Governing Law</div>
<p>This Agreement shall be governed by applicable law.</p>
</div>
<div class="sig-section">
<div class="sig-block">
<p> </p>
<div class="sig-label">Signature — Provider: {$providerName}</div>
<div class="sig-label">Date: ___________________</div>
</div>
<div class="sig-block">
<p> </p>
<div class="sig-label">Signature — Client: {$clientName}</div>
<div class="sig-label">Date: ___________________</div>
</div>
</div>
</body>
</html>
HTML;
}
function htmlFormToPdf(array $formData): string
{
$html = buildContractHtml($formData);
$ch = curl_init('https://api.pdf.funbrew.cloud/v1/pdf/from-html');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . $_ENV['FUNBREW_API_KEY'],
'Content-Type: application/json',
],
CURLOPT_POSTFIELDS => json_encode([
'html' => $html,
'engine' => 'quality',
'format' => 'A4',
'margin' => ['top' => '25mm', 'bottom' => '25mm', 'left' => '20mm', 'right' => '20mm'],
]),
]);
$pdf = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($status !== 200) {
throw new RuntimeException("PDF API returned HTTP $status");
}
return $pdf;
}
// Usage
$formData = [
'clientName' => 'Acme Corp',
'providerName' => 'My Agency Ltd',
'serviceDescription' => 'Website redesign and ongoing maintenance.',
'startDate' => '2026-06-01',
'contractValue' => 12500,
'isDraft' => true,
];
$pdfBytes = htmlFormToPdf($formData);
file_put_contents('contract-draft.pdf', $pdfBytes);
echo "Saved: " . strlen($pdfBytes) . " bytes\n";
Form type 3: Survey and questionnaire responses
Survey-to-PDF conversion is common in HR, research, and compliance workflows. The challenge is rendering dynamic question-answer pairs gracefully.
// generate-survey-pdf.js
function buildSurveyHtml(survey) {
const { title, respondentName, submittedAt, answers = [] } = survey;
const answerRows = answers.map((qa, i) => `
<div class="qa-block">
<div class="question">Q${i + 1}. ${qa.question}</div>
<div class="answer">${Array.isArray(qa.answer)
? '<ul>' + qa.answer.map(a => `<li>${a}</li>`).join('') + '</ul>'
: qa.answer || '<em>No response</em>'
}</div>
</div>
`).join('');
return `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<style>
@page { size: A4; margin: 20mm 15mm; }
body { font-family: sans-serif; font-size: 12px; color: #1e293b;
-webkit-print-color-adjust: exact; print-color-adjust: exact; }
h1 { color: #2563eb; font-size: 20px; margin-bottom: 4px; }
.meta { font-size: 11px; color: #64748b; margin-bottom: 24px; }
.qa-block { margin-bottom: 16px; break-inside: avoid; }
.question { font-weight: 700; background: #f1f5f9; padding: 6px 10px;
border-left: 3px solid #2563eb; margin-bottom: 4px; }
.answer { padding: 6px 10px 6px 14px; }
ul { margin: 0; padding-left: 20px; }
</style>
</head>
<body>
<h1>${title}</h1>
<div class="meta">
Respondent: <strong>${respondentName}</strong>
| Submitted: ${submittedAt}
</div>
${answerRows}
</body>
</html>`;
}
// Usage
const survey = {
title: 'Employee Satisfaction Survey Q2 2026',
respondentName: 'Alex Johnson',
submittedAt: new Date().toLocaleString('en-US'),
answers: [
{ question: 'How satisfied are you with your work environment?', answer: '4 / 5' },
{ question: 'Which benefits do you use most?', answer: ['Health insurance', 'Remote work', 'Learning budget'] },
{ question: 'What can we improve?', answer: 'Better async communication norms would help distributed teams.' },
],
};
const html = buildSurveyHtml(survey);
const pdf = await htmlToPdf(html, { engine: 'quality', format: 'A4' });
For rendering many responses at once, see the batch processing guide.
Styling tips for form PDFs
Consistent page breaks
The most common layout bug in form PDFs is a question-answer pair split across pages. Use CSS to prevent it:
.qa-block, .field-row, .clause {
break-inside: avoid; /* prevent splitting within a block */
page-break-inside: avoid; /* older engines */
}
See the HTML to PDF CSS tips guide for a complete reference on @page, headers, footers, and page numbering.
Print-accurate colors
Browsers apply color adjustments when printing. To make sure background colors and images render correctly in the PDF:
* {
-webkit-print-color-adjust: exact;
print-color-adjust: exact;
}
Multi-column layouts
For forms with short fields (name, date, phone), a two-column layout saves space:
.field-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 12px;
}
CSS Grid works in FUNBREW PDF's Chromium engine (engine: "quality"). For the fast engine (engine: "fast") stick to table-based layouts. See wkhtmltopdf CSS guide for fast-engine-specific tips.
Error handling
async function safeFormToPdf(html, options = {}) {
const maxRetries = 3;
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
return await htmlToPdf(html, options);
} catch (err) {
if (attempt === maxRetries || err.response?.status < 500) throw err;
const delay = 1000 * 2 ** (attempt - 1); // 1s, 2s, 4s
console.warn(`[PDF] Retry ${attempt}/${maxRetries} in ${delay}ms — ${err.message}`);
await new Promise(r => setTimeout(r, delay));
}
}
}
For comprehensive error handling and retry patterns, see the PDF API error handling guide.
Storing and serving form PDFs
After generating the PDF, you typically need to store it:
import { S3Client, PutObjectCommand } from '@aws-sdk/client-s3';
const s3 = new S3Client({ region: 'us-east-1' });
async function storeFormPdf(pdfBuffer, formId) {
const key = `forms/${new Date().toISOString().slice(0, 10)}/${formId}.pdf`;
await s3.send(new PutObjectCommand({
Bucket: process.env.S3_BUCKET,
Key: key,
Body: pdfBuffer,
ContentType: 'application/pdf',
ServerSideEncryption: 'AES256',
Metadata: { formId },
}));
return `https://${process.env.S3_BUCKET}.s3.amazonaws.com/${key}`;
}
For full storage and retrieval patterns including pre-signed URLs, see the PDF generation Node.js guide.
Frequently asked questions
How do I convert an HTML form to PDF without a server?
You need a server-side step for consistent results. Client-side approaches (browser print, jsPDF) produce different output per browser and cannot match CSS precisely. A one-line call to the FUNBREW PDF API from any backend language is the fastest server-side path.
Can I convert a live web form (by URL) rather than HTML?
Yes. The FUNBREW PDF API accepts a URL instead of an HTML string. The renderer loads the page as Chromium would, so JavaScript-rendered forms work too. Use the url field instead of html in the request body.
Does the PDF form generator API support fillable PDF forms?
The API produces static (non-interactive) PDFs from HTML. If you need AcroForm-style fillable fields, render the HTML with the field values already filled in — the resulting PDF is then a record of what the user submitted, which is the more common archival use case.
What is the maximum HTML size I can send?
The API accepts HTML up to several MB. For very large forms with embedded images, encode images as base64 data URIs or host them at a public URL and reference them with <img src="https://...">.
How do I add a signature image to the contract PDF?
Collect the signature as a base64-encoded PNG from a canvas element on the frontend, then embed it in the HTML as <img src="data:image/png;base64,<...>">. The API renders it at full resolution.
Summary
| Form type | Key HTML pattern | Notable CSS |
|---|---|---|
| Application form | display:grid field rows |
break-inside:avoid |
| Contract / agreement | Clause divs + signature grid | position:fixed watermark |
| Survey / questionnaire | Dynamic QA blocks from array | break-inside:avoid per block |
The convert-form-to-pdf pattern reduces to a single API call: send HTML with values baked in, receive a PDF. The FUNBREW PDF API handles fonts, CJK characters, and layout precision without any local browser or binary.
- Get your API key: dashboard
- Try it live: Playground
- Full endpoint reference: API Docs
Related
- HTML to PDF Complete Guide — all conversion methods compared
- HTML to PDF CSS Tips — page breaks, headers, footers, and print CSS
- PDF Generation Node.js Guide — Express, Lambda, and Edge patterns
- PDF Invoice Template Guide — billing document best practices
- PDF API Batch Processing — generate hundreds of form PDFs efficiently
- PDF API Error Handling — retry, timeout, and fallback patterns
- Use Cases — real-world PDF generation scenarios