PHPTutorial
HTML to PDF in Laravel
Generate PDFs from your Laravel app with one HTTP call.
01. Install
# composer require guzzlehttp/guzzle (or use Laravel's built-in Http facade)
# Add to .env:
# PDFMYHTML_API_KEY=pmh_live_xxxxxxxxxxxxxxxxxxxx02. Basic call
// app/Http/Controllers/InvoiceController.php
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\View;
public function pdf(Invoice $invoice)
{
$html = View::make('invoices.template', compact('invoice'))->render();
$response = Http::withHeaders([
'X-API-Key' => env('PDFMYHTML_API_KEY'),
])->post('https://api.pdfmyhtml.com/v1/html-to-pdf', [
'html' => $html,
'wait' => true,
]);
$pdf = Http::get($response->json('download_url'))->body();
return response($pdf, 200, [
'Content-Type' => 'application/pdf',
'Content-Disposition' => 'inline; filename="invoice-' . $invoice->id . '.pdf"',
]);
}03. Error handling
// Robust version with error handling + logging
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Log;
public function pdf(Invoice $invoice)
{
try {
$response = Http::timeout(30)
->withHeaders(['X-API-Key' => env('PDFMYHTML_API_KEY')])
->post('https://api.pdfmyhtml.com/v1/html-to-pdf', [
'html' => View::make('invoices.template', compact('invoice'))->render(),
'wait' => true,
]);
if ($response->failed()) {
Log::error('pdfmyhtml HTTP error', ['status' => $response->status(), 'body' => $response->body()]);
return response('Could not generate PDF', 500);
}
$data = $response->json();
if ($data['status'] !== 'COMPLETED') {
return response('PDF still processing — try async polling', 503);
}
$pdf = Http::timeout(30)->get($data['download_url'])->body();
return response($pdf, 200, ['Content-Type' => 'application/pdf']);
} catch (\Illuminate\Http\Client\ConnectionException $e) {
Log::error('pdfmyhtml timeout', ['exception' => $e]);
return response('PDF service unavailable', 503);
}
}04. Async pattern (high volume)
// Async pattern: dispatch a queued job, poll later
// Avoids holding the request open for 25s.
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Support\Facades\Http;
class GenerateInvoicePdf implements ShouldQueue
{
use Queueable;
public function __construct(public Invoice $invoice) {}
public function handle()
{
$response = Http::withHeaders(['X-API-Key' => env('PDFMYHTML_API_KEY')])
->post('https://api.pdfmyhtml.com/v1/html-to-pdf', [
'html' => $this->invoice->renderHtml(),
'wait' => false, // async
]);
$jobId = $response->json('job_id');
// Poll until COMPLETED (or schedule a follow-up job in 30s)
for ($i = 0; $i < 60; $i++) {
sleep(2);
$status = Http::withHeaders(['X-API-Key' => env('PDFMYHTML_API_KEY')])
->get("https://api.pdfmyhtml.com/v1/jobs/{$jobId}")->json();
if ($status['status'] === 'COMPLETED') {
$pdfBytes = Http::get($status['download_url'])->body();
Storage::put("invoices/{$this->invoice->id}.pdf", $pdfBytes);
return;
}
}
throw new \Exception('PDF generation timed out');
}
}05. Real-world example
// Real-world: Stripe checkout.session.completed → invoice PDF → email
// In your StripeWebhookController:
use Illuminate\Support\Facades\Mail;
use Illuminate\Support\Facades\Http;
use App\Mail\InvoiceMail;
public function handleCheckoutCompleted($session)
{
$invoice = Invoice::create([
'stripe_session_id' => $session->id,
'amount_cents' => $session->amount_total,
'customer_email' => $session->customer_details->email,
]);
$html = View::make('invoices.template', compact('invoice'))->render();
$response = Http::withHeaders(['X-API-Key' => env('PDFMYHTML_API_KEY')])
->post('https://api.pdfmyhtml.com/v1/html-to-pdf', ['html' => $html, 'wait' => true]);
$pdf = Http::get($response->json('download_url'))->body();
Storage::put("invoices/{$invoice->id}.pdf", $pdf);
Mail::to($invoice->customer_email)->send(new InvoiceMail($invoice, $pdf));
}Common gotchas
- 01.Laravel's `Http` facade defaults to a 30-second timeout; you must set `Http::timeout(30)` explicitly when using `wait: true` to avoid surprise truncation.
- 02.Blade templates render with relative URLs by default. If your invoice template references CSS or images, use `asset()` or `URL::to()` to produce absolute URLs that Playwright can fetch.
- 03.When dispatching the PDF generation as a queued job, make sure your queue worker has internet egress to `api.pdfmyhtml.com` — some PaaS providers firewall outbound by default.
- 04.Don't store the API key in `config/services.php` and commit it — use `env('PDFMYHTML_API_KEY')` and keep secrets in `.env` (or a vault).
Going deeper? Read the full guide on cost trade-offs vs self-hosted Headless Chrome.