HTML to PDF in PHP
PHP's PDF story is mostly DOMPDF and mPDF, and both are impressive pieces of work:
they parse HTML and CSS in pure PHP, with no external process and no binary to
install. That is exactly why they hit a ceiling. DOMPDF supports most of CSS 2.1
and very little after it — no flexbox, no grid, patchy float interactions. mPDF is
better on some properties and slower on large documents.
So the usual next step is Browsershot, which drives headless Chrome through Node — and now a PHP application has a Node runtime, a Chromium install and a Puppeteer dependency to keep current.
This is one cURL call and none of that.
One-off render
No Composer package. curl is in every PHP build.
<?php
$html = <<<'HTML'
<!doctype html>
<html><head><style>
@page { size: A4; margin: 20mm; }
body { font: 14px/1.6 system-ui, sans-serif; color: #16181d; }
h1 { font-size: 28px; margin: 0 0 4px; }
</style></head>
<body>
<h1>Invoice 2026-0142</h1>
<p>Issued 2026-08-08 · Due 2026-09-07</p>
</body></html>
HTML;
$ch = curl_init('https://renderpaper.com/v1/render');
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
'X-API-Key: ' . getenv('RENDER_API_KEY'),
'Content-Type: application/json',
],
CURLOPT_POSTFIELDS => json_encode(['template' => $html]),
CURLOPT_TIMEOUT => 60,
]);
$pdf = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($status !== 200) {
throw new RuntimeException("render failed: HTTP $status: $pdf");
}
file_put_contents('invoice.pdf', $pdf);
echo "wrote invoice.pdf\n";
RENDER_API_KEY=rs_live_… php invoice.php
Stored templates: send data, not markup
With the document inside your PHP, changing a margin is a deploy. Store it once and post data instead.
<?php
$templateId = 'your-template-id';
$payload = [
'data' => [
'InvoiceNumber' => '2026-0142',
'Customer' => ['Name' => 'Northwind Trading AB'],
'LineItems' => [
['Description' => 'Consulting', 'Quantity' => 12, 'Amount' => 14400],
['Description' => 'Hosting', 'Quantity' => 1, 'Amount' => 4350],
],
'Total' => 18750,
],
];
$ch = curl_init("https://renderpaper.com/v1/templates/{$templateId}/render");
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
'X-API-Key: ' . getenv('RENDER_API_KEY'),
'Content-Type: application/json',
],
CURLOPT_POSTFIELDS => json_encode($payload),
CURLOPT_TIMEOUT => 60,
]);
$pdf = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($status !== 200) {
throw new RuntimeException("render failed: HTTP $status: $pdf");
}
file_put_contents('invoice.pdf', $pdf);
The template is a Go html/template:
<h1>Invoice {{.InvoiceNumber}}</h1>
<p>{{.Customer.Name}}</p>
<table>
{{range .LineItems}}
<tr><td>{{.Description}}</td><td>{{.Quantity}}</td><td>{{.Amount}}</td></tr>
{{end}}
</table>
Sending it straight to the browser
The download-endpoint shape, without writing a temp file:
<?php
function streamInvoice(string $templateId, array $data): void
{
$ch = curl_init("https://renderpaper.com/v1/templates/{$templateId}/render");
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
'X-API-Key: ' . getenv('RENDER_API_KEY'),
'Content-Type: application/json',
],
CURLOPT_POSTFIELDS => json_encode(['data' => $data]),
CURLOPT_TIMEOUT => 60,
]);
$pdf = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($status !== 200) {
http_response_code(502);
echo 'could not render the document';
return;
}
header('Content-Type: application/pdf');
header('Content-Disposition: attachment; filename="invoice.pdf"');
header('Content-Length: ' . strlen($pdf));
echo $pdf;
}
Why not DOMPDF
DOMPDF's appeal is real: pure PHP, no external process, no binary. Its limit is CSS.
It targets most of CSS 2.1, so display: flex and display: grid do nothing,
modern layout collapses to stacked blocks, and web fonts need explicit registration.
It remains the right answer when the document is simple, generated entirely by your code, and never handed to a designer.
Why not Browsershot
Browsershot is the honest answer to DOMPDF's ceiling: it drives real Chrome, so the CSS is real too. The cost is the dependency chain — your PHP application now needs Node, Puppeteer and Chromium present in every environment, including CI and each developer's laptop, and the failure mode is a stack trace from a subprocess two languages away.
Run Browsershot when documents must never leave your infrastructure. Otherwise you are operating a browser to produce an invoice.
Try it without a key
https://renderpaper.com/sample.pdf is a real render, no account. The free tier is
50 documents a month with no card.