HTML to PDF Reports
Generate pixel-perfect PDF contracts, proposals, and reports from HTML — personalized per client with variable substitution.
The problem
Generating PDFs server-side usually means wrestling with PDF libraries — escaping special characters, fighting layout engines, or paying for heavy SaaS tools. If you already know HTML and CSS, you already know how to design the document. StarkRender renders it with a real browser, so what you see in the preview is exactly what lands in the PDF.
Step 1 — Design the document in HTML
Write the document as standard HTML. Use {{variable}} placeholders for anything that changes per client or per render. Print-specific CSS like @page, page-break-before, and page-break-after is fully supported.
<!DOCTYPE html>
<html>
<head>
<style>
@page { margin: 0; } /* margin is controlled by the API param */
body {
font-family: Inter, sans-serif;
color: #1e293b;
line-height: 1.6;
}
.header {
background: #0f172a;
color: #fff;
padding: 40px 60px;
display: flex;
justify-content: space-between;
align-items: center;
}
.logo { font-size: 24px; font-weight: 800; color: #C9952A; }
.content { padding: 48px 60px; }
h1 { font-size: 28px; margin: 0 0 8px; }
.meta { color: #64748b; font-size: 14px; margin: 0 0 40px; }
table { width: 100%; border-collapse: collapse; margin: 24px 0; }
th { background: #f1f5f9; text-align: left; padding: 12px 16px; font-size: 13px; }
td { padding: 12px 16px; border-bottom: 1px solid #e2e8f0; font-size: 14px; }
.total { font-weight: 700; font-size: 16px; }
.footer { padding: 32px 60px; border-top: 1px solid #e2e8f0; color: #64748b; font-size: 13px; }
.signature-block { margin-top: 60px; display: flex; gap: 80px; }
.sig-line { border-top: 1px solid #334155; padding-top: 8px; width: 240px; font-size: 13px; color: #64748b; }
</style>
</head>
<body>
<div class="header">
<div class="logo">YourBrand</div>
<div style="font-size:13px;color:#94a3b8">Contract #{{contract_number}}</div>
</div>
<div class="content">
<h1>Service Agreement</h1>
<p class="meta">Issued: {{issue_date}} · Valid until: {{expiry_date}}</p>
<p>This agreement is entered into between <strong>YourBrand</strong>
and <strong>{{client_name}}</strong> (<em>{{client_company}}</em>).</p>
<table>
<thead>
<tr><th>Service</th><th>Qty</th><th>Unit price</th><th>Total</th></tr>
</thead>
<tbody>
<tr>
<td>{{service_description}}</td>
<td>{{qty}}</td>
<td>{{unit_price}}</td>
<td class="total">{{total_value}}</td>
</tr>
</tbody>
</table>
<div class="signature-block">
<div>
<div class="sig-line">YourBrand representative</div>
</div>
<div>
<div class="sig-line">{{client_name}} · {{client_company}}</div>
</div>
</div>
</div>
<div class="footer">
YourBrand · hello@yourbrand.com · This document is confidential.
</div>
</body>
</html>
Step 2 — Call the API
Pass the HTML with variables filled in per client. The PDF is multi-page automatically if the content overflows. Background colors and images are always preserved.
Node.js
const CONTRACT_HTML = `<!-- your template HTML -->`;
async function generateContract(client) {
const res = await fetch('https://api.starkrender.com/v1/pdf', {
method: 'POST',
headers: {
'x-api-key': process.env.STARKRENDER_API_KEY,
'Content-Type': 'application/json',
},
body: JSON.stringify({
html: CONTRACT_HTML,
format: 'A4',
margin: '0', // controlled by @page in the HTML
variables: {
contract_number: client.contractNumber,
issue_date: client.issueDate,
expiry_date: client.expiryDate,
client_name: client.name,
client_company: client.company,
service_description: client.service,
qty: client.qty,
unit_price: client.unitPrice,
total_value: client.totalValue,
},
}),
});
const { url } = await res.json();
return url; // permanent PDF URL — send to client or store in DB
}
Python
import os, requests CONTRACT_HTML = """<!-- your template HTML -->""" def generate_contract(client: dict) -> str: resp = requests.post( "https://api.starkrender.com/v1/pdf", headers={ "x-api-key": os.environ["STARKRENDER_API_KEY"], "Content-Type": "application/json", }, json={ "html": CONTRACT_HTML, "format": "A4", "margin": "0", "variables": { "contract_number": client["contract_number"], "issue_date": client["issue_date"], "expiry_date": client["expiry_date"], "client_name": client["name"], "client_company": client["company"], "service_description": client["service"], "qty": client["qty"], "unit_price": client["unit_price"], "total_value": client["total_value"], }, }, ) resp.raise_for_status() return resp.json()["url"] # permanent PDF URL
Tips
Page breaks
Control pagination with standard CSS. Force a new page before a section, or prevent a table row from splitting across pages:
.new-section { page-break-before: always; }
tr { page-break-inside: avoid; }
.keep-together { break-inside: avoid; }
Landscape for wide tables
Pass "landscape": true for reports with many columns. Works with all format values.
{
"html": "<table>...</table>",
"format": "A4",
"landscape": true,
"margin": "30px"
}
Fonts
Any Google Font referenced in your CSS is automatically detected and loaded before rendering — no setup needed. Use font-family: Inter, sans-serif in your CSS and it just works.
Store the URL, not the file
The returned URL is permanent. Store it in your database and send it directly to the client (e.g., in an email) or embed it in a download link. You don't need to stream or proxy the file through your server.
<a href="https://api.starkrender.com/v1/image/uuid" download="contract.pdf"> Download contract </a>
Letter and Legal formats
Pass "format": "Letter" or "format": "Legal" for US paper sizes. The default is "A4".
StarkRender