Building a PDF feature?
Save this to your Work Desktop.

All Tutorials
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_xxxxxxxxxxxxxxxxxxxx

02. 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.