Three ways to generate a PDF from a Blade view in Laravel, all run on Laravel 13.38 and PHP 8.5 with the same invoice: barryvdh/laravel-dompdf (pure PHP), spatie/laravel-pdf (headless Chrome through Browsershot, or any driver you plug in) and the plain Http client calling an API. We downloaded each PDF and checked what came out, so you can choose before you write the template.
| Option | Pick it when |
|---|---|
| laravel-dompdf Pure PHP wrapper around dompdf | Simple templates (blocks, tables, floats) and hosting where you can only install Composer packages. |
| spatie/laravel-pdf Fluent builder, swappable drivers | Your Blade view uses flexbox, grid or web fonts. The default Browsershot driver needs Node, Puppeteer and Chrome on the server; a remote driver needs none of them. |
| Http client + API Chrome on someone else's server | Chrome output with no extra package: one Http::post() call. |
resources/views/invoice.blade.php. The header is a flexbox row, as in most templates today:
<!doctype html>
<html><head><meta charset="utf-8"><style>
body { font-family: sans-serif; }
.top { display: flex; justify-content: space-between; align-items: center;
background: #eef2ff; padding: 12px; }
table { width: 100%; border-collapse: collapse; }
td, th { border-bottom: 1px solid #ddd; padding: 6px; text-align: left; }
tr { break-inside: avoid; }
</style></head><body>
<div class="top"><h1>Invoice #{{ $number }}</h1><p>Due: {{ $due }}</p></div>
<table><tr><th>Item</th><th>Price</th></tr>
@foreach ($items as $item)
<tr><td>{{ $item['name'] }}</td><td>€{{ number_format($item['price'], 2) }}</td></tr>
@endforeach
</table>
</body></html>
composer require barryvdh/laravel-dompdf (tested with 3.1.2, which pulls dompdf 3.1.6). The facade is auto-registered:
<?php
// routes/web.php
use Illuminate\Support\Facades\Route;
$invoice = [
'number' => 1042,
'due' => '2026-11-01',
'items' => [
['name' => 'Consulting (3h)', 'price' => 450],
['name' => 'Hosting, Oct.', 'price' => 19],
],
];
Route::get('/invoice/dompdf', function () use ($invoice) {
return \Barryvdh\DomPDF\Facade\Pdf::loadView('invoice', $invoice)
->setPaper('a4')
->download('invoice-1042.pdf');
});
It returns a 1-page A4 PDF in about 0.3 s with no browser anywhere. But dompdf implements CSS 2.1, so flexbox is ignored: "Invoice #1042" and "Due: 2026-11-01" come out stacked on two lines instead of one row (the text layout of the PDF shows it plainly). Rebuild such rows as a <table> or with floats; grid doesn't work either. Its built-in fonts also turn ✓ and CJK text into ?, and remote images need isRemoteEnabled; our HTML to PDF in PHP guide measures each of these and shows the fixes.
composer require spatie/laravel-pdf (tested with 2.14.0). Its builder reads nicely: Pdf::view('invoice', $data)->format('a4')->download('invoice.pdf'), plus headerHtml(), footerHtml(), margins(), landscape() and queued saving. The default driver is Browsershot, so you also need composer require spatie/browsershot, Node, npm install puppeteer and Chrome's system libraries on every server that renders. On Docker or Ubuntu 23.10+ set LARAVEL_PDF_NO_SANDBOX=true (Chrome otherwise exits with "No usable sandbox!"); a custom binary goes in LARAVEL_PDF_CHROME_PATH.
Version 2 also ships drivers for Cloudflare Browser Rendering, Gotenberg, WeasyPrint, dompdf and a PHP-only Chrome client, set with LARAVEL_PDF_DRIVER. And since a driver is a small interface (generatePdf() and savePdf()), you can add your own.
Keep the builder, drop Node and Chrome from your servers: this driver sends the rendered Blade HTML to the Shotmint API, which prints it in Chrome and returns the PDF. app/Pdf/ShotmintDriver.php:
<?php
namespace App\Pdf;
use Illuminate\Support\Facades\Http;
use RuntimeException;
use Spatie\LaravelPdf\Drivers\PdfDriver;
use Spatie\LaravelPdf\PdfOptions;
class ShotmintDriver implements PdfDriver
{
public function __construct(private string $key, private string $endpoint) {}
public function generatePdf(string $html, ?string $headerHtml, ?string $footerHtml, PdfOptions $options): string
{
$body = array_filter([
'html' => $html,
'format' => $options->format, // a3, a4, a5, letter, legal, tabloid
'landscape' => $options->orientation === 'landscape',
'scale' => $options->scale,
'header_template' => $headerHtml,
'footer_template' => $footerHtml,
], fn ($v) => $v !== null && $v !== false);
if ($options->margins) {
// The API takes one margin for all four sides: use the top one.
$body['margin'] = $options->margins['top'].$options->margins['unit'];
}
$response = Http::withHeaders(['X-API-Key' => $this->key])
->timeout(90)
->post($this->endpoint, $body);
if (! $response->successful()) {
throw new RuntimeException('Shotmint render failed ('.$response->status().'): '.$response->body());
}
return $response->body();
}
public function savePdf(string $html, ?string $headerHtml, ?string $footerHtml, PdfOptions $options, string $path): void
{
file_put_contents($path, $this->generatePdf($html, $headerHtml, $footerHtml, $options));
}
}
Register it in app/Providers/AppServiceProvider.php. The package looks drivers up as laravel-pdf.driver.NAME in the container, so that's the only wiring needed:
<?php
namespace App\Providers;
use App\Pdf\ShotmintDriver;
use Illuminate\Support\ServiceProvider;
use Spatie\LaravelPdf\Drivers\PdfDriver;
class AppServiceProvider extends ServiceProvider
{
public function register(): void
{
// Makes ->driver('shotmint') available on every Pdf builder.
$this->app->singleton('laravel-pdf.driver.shotmint', fn () => new ShotmintDriver(
config('services.shotmint.key'),
'https://shotmint.creasework.com/v1/pdf',
));
// Optional: make it the default, so Pdf::view() needs no ->driver() call.
$this->app->singleton(PdfDriver::class, fn ($app) => $app->make('laravel-pdf.driver.shotmint'));
}
public function boot(): void
{
//
}
}
Add the key to config/services.php and put SHOTMINT_KEY=... in .env (a free key with 25 renders takes one click, no signup):
// config/services.php
'shotmint' => [
'key' => env('SHOTMINT_KEY'),
],
Then use the builder as usual, with ->driver('shotmint') (or nothing, if you kept the default binding):
use Spatie\LaravelPdf\Facades\Pdf;
Route::get('/invoice/laravel-pdf', function () use ($invoice) {
return Pdf::view('invoice', $invoice)
->driver('shotmint')
->format('a4')
->margins(20, 15, 20, 15)
->footerHtml('<div style="width:100%;text-align:center;font:9px sans-serif">Page <span class="pageNumber"></span> of <span class="totalPages"></span></div>')
->download('invoice-1042.pdf');
});
Result: a 1-page A4 PDF with the header on one line, the shaded background kept and "Page 1 of 1" in the footer. Chrome fills the pageNumber and totalPages spans, and the footer is drawn inside the bottom margin, so keep a margin of at least ~15mm. Two limits of this driver: the API takes one margin for all four sides (it uses the top value), and custom paper sizes aren't passed on (use a named format(): a3, a4, a5, letter, legal or tabloid). Footer templates can't load external images or fonts; inline them.
If you only need a download button, Laravel's Http client is enough. page_numbers adds a centered "1 / 3" footer without writing a template:
use Illuminate\Support\Facades\Http;
Route::get('/invoice/http', function () use ($invoice) {
$pdf = Http::withHeaders(['X-API-Key' => config('services.shotmint.key')])
->timeout(90)
->post('https://shotmint.creasework.com/v1/pdf', [
'html' => view('invoice', $invoice)->render(),
'format' => 'A4',
'margin' => '20mm',
'page_numbers' => true,
])
->throw()
->body();
return response($pdf, 200, [
'Content-Type' => 'application/pdf',
'Content-Disposition' => 'attachment; filename="invoice-1042.pdf"',
]);
});
->throw() turns a 4xx into a RequestException; the body is always JSON (for example {"error":"Missing or invalid API key..."} with a 401), so log it. To render a live page instead of a view, send 'url' => 'https://...' instead of 'html'. All parameters: API docs.
| Feature | Support |
|---|---|
| CSS | Chrome (Browsershot, API): everything a browser supports dompdf: CSS 2.1, no flexbox, grid or JavaScript |
| Page numbers | spatie/laravel-pdf: footerHtml(); API: page_numbersdompdf: page_text() on the canvas in PHP |
| Server installs | dompdf and API: Composer only Browsershot: Node, Puppeteer and a ~400 MB Chrome |
| Private assets | dompdf and Browsershot can reach localhost API: inline images/CSS or use public URLs |
Simple table-based templates on cheap hosting: laravel-dompdf. Modern CSS and you're happy to run Chrome next to PHP: spatie/laravel-pdf with Browsershot. Modern CSS without Chrome on your servers (Forge boxes, Vapor, slim containers): the same builder with the Shotmint driver, or the Http client for one-off downloads. One PDF is one credit whatever its page count; €5 buys 2,000 and credits never expire. Paste your HTML into the free HTML to PDF converter to see the Chrome output first.
Related: page numbers and footers, CSS page breaks, Tailwind print classes, invoice PDFs.
No signup, no card. Get a key in one click, then buy credits only if you need more (from €5 for 2,000, never expire). From a script, an AI agent, Make or Zapier, POST /v1/keys/free (no body) returns a key as JSON (openapi.json, no-code recipes).
All guides: guides · integrations · comparisons