Guide
How to generate a PDF from HTML in Laravel
Laravel apps frequently need to turn a Blade view into a downloadable PDF: invoices, receipts, reports, tickets. You can render the PDF inside your own process with a library like dompdf or snappy, or you can POST the HTML to a rendering API and stream the result back to the browser. This guide shows the API approach with a clean controller, then explains exactly when a local library is the better call.
The quickest path: render a Blade view, POST the HTML
The pattern is simple. Render any Blade template to an HTML string with view()->render(), send that string to the PDFPipe endpoint using Laravel HTTP client, and return the raw bytes as a download response. The endpoint is POST https://api.pdfpipe.xyz/v1/pdf, it accepts a JSON body and returns application/pdf.
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Http;
class InvoicePdfController extends Controller
{
public function download(Request $request, Invoice $invoice)
{
// 1. Render a Blade view to an HTML string.
$html = view('invoices.pdf', ['invoice' => $invoice])->render();
// 2. POST the HTML to the PDFPipe rendering API.
$response = Http::withToken(config('services.pdfpipe.key'))
->withOptions(['stream' => false])
->post('https://api.pdfpipe.xyz/v1/pdf', [
'html' => $html,
'options' => ['format' => 'A4'],
]);
// 3. Fail loudly if the API returned an error status.
$response->throw();
// 4. Return the raw PDF bytes as a download.
return response($response->body(), 200, [
'Content-Type' => 'application/pdf',
'Content-Disposition' => 'attachment; filename="invoice-' . $invoice->id . '.pdf"',
]);
}
}The Authorization: Bearer pp_live_your_key header is added for you by Http::withToken(). Keep the key in config/services.php so it stays out of version control:
// config/services.php
return [
// ...
'pdfpipe' => [
'key' => env('PDFPIPE_KEY'),
],
];
// .env
PDFPIPE_KEY=pp_live_your_keyWire up the route and you are done. No browser binary to install, no PHP extension to compile, no font packages to copy into the container.
// routes/web.php
use App\Http\Controllers\InvoicePdfController;
Route::get('/invoices/{invoice}/pdf', [InvoicePdfController::class, 'download'])
->name('invoices.pdf');Streaming the PDF instead of buffering it
For large reports you may not want to hold the entire file in memory. The HTTP client can hand you a PSR stream, which you can pass straight to a StreamedResponse:
use Symfony\Component\HttpFoundation\StreamedResponse;
$response = Http::withToken(config('services.pdfpipe.key'))
->withOptions(['stream' => true])
->post('https://api.pdfpipe.xyz/v1/pdf', [
'html' => $html,
'options' => ['format' => 'A4'],
]);
return new StreamedResponse(function () use ($response) {
$body = $response->toPsrResponse()->getBody();
while (!$body->eof()) {
echo $body->read(8192);
}
}, 200, [
'Content-Type' => 'application/pdf',
'Content-Disposition' => 'attachment; filename="report.pdf"',
]);When dompdf or snappy is the better choice
Local libraries are genuinely good tools and the right answer in plenty of cases. barryvdh/laravel-dompdf is pure PHP, so it has zero external dependencies and works offline, which is ideal for simple, text heavy documents. barryvdh/laravel-snappy wraps wkhtmltopdf and handles richer CSS, but it needs that binary present on every machine that runs your app. Here is how the options compare honestly:
| Concern | dompdf | snappy (wkhtmltopdf) | PDFPipe API |
|---|---|---|---|
| Install | composer only | composer plus wkhtmltopdf binary | composer only (HTTP client) |
| Modern CSS, flexbox, grid | Limited | Partial (old WebKit) | Full (current browser engine) |
| Web fonts and emoji | Manual font setup | Manual font setup | Built in |
| CPU and memory on your server | Yours | Yours (heavy) | Offloaded |
| Works fully offline | Yes | Yes | No (network call) |
| Per document cost | Free | Free | Metered |
For a quick comparison, the equivalent dompdf controller looks like this:
use Barryvdh\DomPDF\Facade\Pdf;
public function download(Invoice $invoice)
{
$pdf = Pdf::loadView('invoices.pdf', ['invoice' => $invoice])
->setPaper('a4');
return $pdf->download('invoice-' . $invoice->id . '.pdf');
}That is less code and has no network dependency, so reach for it when your documents are straightforward and you want everything to stay inside your own process. Choose the API when your invoices use a real design system, when wkhtmltopdf is fighting your CSS, or when you would rather not run a heavy rendering engine inside your web workers or queue.
Rendering in a queued job
Generating PDFs during a web request blocks the response. For batch exports, move the call into a queued job and store the result. The HTTP call itself is unchanged, you simply return the bytes to disk instead of the browser:
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Storage;
public function handle(): void
{
$html = view('invoices.pdf', ['invoice' => $this->invoice])->render();
$pdf = Http::withToken(config('services.pdfpipe.key'))
->post('https://api.pdfpipe.xyz/v1/pdf', [
'html' => $html,
'options' => ['format' => 'A4'],
])
->throw()
->body();
Storage::disk('s3')->put("invoices/{$this->invoice->id}.pdf", $pdf);
}Paste a Blade rendered HTML string into the live playground on the home page to preview output instantly, then read the full request and option reference in the docs.
See pricing →