To generate a PDF in Laravel with a laravel html to pdf api, render a Blade view to an HTML string and send that string to RelayPDF. Do not install Chromium or wkhtmltopdf on the app host. The official PHP client is composer require relaypdf/relaypdf (PHP 8.1+, ext-curl, ext-json, ext-hash). Call pdf.fromHtml. Product: https://relaypdf.com/html-to-pdf. PHP SDK: https://relaypdf.com/docs/sdks/php. REST hub: https://relaypdf.com/docs. Sibling PHP recipe: https://relaypdf.com/blog/html-to-pdf-php.
Laravel already owns layout. Blade compiles to HTML. RelayPDF owns Chromium print. The hop is view()->render(), then POST /v1/pdf. A successful HTML job is $0.015 on the live rate card. Failed jobs are not billed.
Blade to HTML to RelayPDF
Keep invoice markup in resources/views/pdf. Use a dedicated layout that does not pull the app chrome (nav, Vite HMR, session flashes). Absolute URLs for images and fonts if Chromium must fetch them. Private, loopback, and metadata hosts return url_not_allowed. Inlining CSS and base64 images avoids that fetch.
Load RELAYPDF_API_KEY from the environment. The PHP docs say the key is required and that relaypdf setup writes it to an env file. In Laravel that is .env plus config/services.php if you want a named key. Do not commit the key. Do not put it on a query string. Authorization is Bearer only.
<?php
use RelayPDF\RelayPDF;
use RelayPDF\RelayPDFError;
$html = view('pdf.invoice', ['invoice' => $invoice])->render();
$client = new RelayPDF(env('RELAYPDF_API_KEY'));
try {
$pdf = $client->pdf->fromHtml($html, [
'filename' => 'invoice-'.$invoice->number.'.pdf',
'options' => [
'format' => 'A4',
'printBackground' => true,
'margin' => [
'top' => '20mm',
'bottom' => '20mm',
'left' => '15mm',
'right' => '15mm',
],
'footerTemplate' =>
'<div style="font-size:9px;width:100%;text-align:center;">'
.'Page <span class="pageNumber"></span> of ' .'<span class="totalPages"></span></div>',
],
]);
$path = storage_path('app/invoice-'.$invoice->number.'.pdf');
$pdf->save($path);
return response()->download($path);
} catch (RelayPDFError $err) {
report($err->getMessage().' '.$err->code);
abort(502);
}
Request JSON field names match REST: html, filename, options.printBackground, options.waitUntil, callbackUrl. Methods are camelCase. Default response is binary; the PHP docs say save() writes the file. response=url returns a 24-hour public link at GET /v1/files/:id (no API key on that GET). response=async returns HTTP 202 with id and pollUrl.
The PHP package line is 0.1.x. The install page states that until Packagist publish you require the path in the repo. Use composer require relaypdf/relaypdf when the package is listed; otherwise follow that path note. The Composer README is the full method list.
laravel-dompdf and laravel-snappy
barryvdh/laravel-dompdf wraps Dompdf. It runs inside PHP. CSS support is a subset. Flex, grid, webfonts, and canvas charts often fail or look wrong. There is no JavaScript. You stay on the Laravel process and you own memory spikes.
barryvdh/laravel-snappy wraps wkhtmltopdf. You install a binary on every app server or worker. Qt WebKit is not Chromium. Deploy images must carry the binary. Workers that fork wkhtml fight PHP-FPM and queue concurrency.
RelayPDF is a remote Chromium print. You do not ship a browser. CSS that works in current Chrome is the target. waitUntil defaults to networkidle0. options.timeout is the Chromium render budget, max 60000 ms, not an HTTP client timeout. Charts that paint after load need waitForSelector or waitForTimeout (max 30000 ms). wkhtmltopdf still exists on RelayPDF as POST /v1/convert with engine wkhtmltopdf at $0.025 if you have old HTML that only printed under Snappy. New Laravel work should use POST /v1/pdf.
| Piece | Where it runs | Engine | Typical Laravel cost |
|---|---|---|---|
| barryvdh/laravel-dompdf | App PHP process | Dompdf | CPU and memory on the box |
| barryvdh/laravel-snappy | App host binary | wkhtmltopdf | Binary + worker slots |
| RelayPDF pdf.fromHtml | api.relaypdf.com | Chromium | $0.015 on success |
| RelayPDF convert wkhtml | document worker | wkhtmltopdf | $0.025 on success |
Queue: Laravel jobs versus response async
Two queues get mixed up. A Laravel job (database, Redis, SQS) is your worker. A RelayPDF async job is the API holding Chromium or a convert. You can use either or both.
Synchronous fromHtml is fine for a one-page invoice in a controller if the user can wait. Hold the HTTP request only if the render finishes in a few seconds. Office convert, large HTML, and agent loops should not sit in php-fpm. Set response to async. The 202 body is id, status processing, and pollUrl. Poll GET /v1/jobs/:id with the same API key until status is completed or failed. The PHP client exposes jobs.wait and files.download.
$job = $client->pdf->fromHtml($html, [
'filename' => 'report.pdf',
'response' => 'async',
'callbackUrl' => 'https://app.example.com/webhooks/relaypdf',
]);
// $job->id, pollUrl. HTTP 202.
$done = $client->jobs->wait($job->id);
$client->files->download($done['id'])->save(storage_path('app/report.pdf'));
callbackUrl is optional and must be https. Non-https callbackUrl is url_not_allowed. The callback is POSTed when that job finishes. Completed files stay public at GET /v1/files/:id for 24 hours. A Laravel queued job that only calls jobs.wait is valid if you prefer poll over webhook. Do not invent a second async path such as POST /v1/pdf/create-async. The documented switch is the response field on the same generating endpoints.
Webhooks and HMAC (documented names only)
RelayPDF documents two delivery paths. callbackUrl on a single generating request receives that job. Dashboard endpoints at /dashboard/webhooks receive account-level events. Both POSTs are HMAC-signed. Verify the exact raw body. Do not json_decode and re-encode before the check.
The signature header is RelayPDF-Signature. Dashboard deliveries also include RelayPDF-Event. The header format is t=,v1=. The MAC is HMAC-SHA256 of {t}.{raw_body}. The secret is the dashboard webhook signing secret, not the API key. Default clock skew in the Node/Python verifiers is 300 seconds. The PHP table lists Webhooks::verify against RelayPDF-Signature.
Documented event names: job.completed, job.failed, wallet.topup, wallet.auto_reload, wallet.auto_reload_failed, wallet.payment_required. Do not subscribe to names that are not on that list. Failed jobs are never billed. payment_required means the wallet is empty.
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use RelayPDF\Webhooks;
class RelayPdfWebhookController
{
public function __invoke(Request $request)
{
$raw = $request->getContent();
$header = $request->header('RelayPDF-Signature') ?? '';
$ok = Webhooks::verify(env('RELAYPDF_WEBHOOK_SECRET'), $raw, $header);
if (!$ok) {
abort(400);
}
$event = $request->header('RelayPDF-Event');
// job.completed | job.failed | wallet.*
return response()->noContent();
}
}
Exclude this route from CSRF. Read the body once. Store RELAYPDF_WEBHOOK_SECRET next to the API key. Rotate the dashboard secret if it leaks. The PHP docs do not publish extra header names beyond RelayPDF-Signature and, on the webhooks page, RelayPDF-Event. Do not add BladePDF-Signature, X-RelayPDF-Signature, or sha256= prefixes. Those are other products.
Errors, limits, price
Failures are { error: { code, message } }. The SDK throws RelayPDFError with status, code, message, and optional retryAfter. Branch on code. invalid_request is a bad body (exactly one of html, url, markdown, templateId). unauthorized is a missing or unknown key. rate_limited is 429 plus Retry-After; not billed. render_failed is Chromium print failure; not billed. payload_too_large is 413; no published megabyte figure. Trial wallets: 20 requests/minute. Funded or auto-reload: 60/minute. Burst: 5 / 10 seconds. New accounts: $5 trial credit.
If you need a 24-hour link for a download button, use response=url instead of streaming binary through Laravel. If you need Handlebars stored in the dashboard, use fromTemplate with a published id or slug plus an array. Markdown is fromMarkdown. A public page is fromUrl. Same POST /v1/pdf.
What not to do
- Do not run Browsershot or a local Chrome on Forge, Vapor, or a 512 MB worker to print invoices.
- Do not mix laravel-dompdf CSS assumptions with Chromium. Print the Blade you would open in Chrome.
- Do not send the API key to the browser. Blade renders on the server; the API call stays on the server.
- Do not invent webhook header names. Use RelayPDF-Signature and RelayPDF-Event as documented.
- Do not treat GET /v1/files/:id as permanent storage. It is a 24-hour download.
Start at https://relaypdf.com/html-to-pdf. Install notes: https://relaypdf.com/docs/sdks/php. Async: https://relaypdf.com/docs/async. Webhooks: https://relaypdf.com/docs/webhooks. Options: https://relaypdf.com/docs/options. Errors: https://relaypdf.com/docs/errors. Pricing: https://relaypdf.com/pricing.