How to Use Html2Pdf.app in a Laravel App to Create Website PDFs
Build a Laravel endpoint that turns a public webpage or HTML into a PDF with Html2Pdf.app, handles errors, and returns the file safely.
Use Laravel’s HTTP client to send the webpage URL (or HTML) to Html2Pdf.app’s POST https://api.html2pdf.app/v1/generate endpoint with your API key in the X-API-Key header. A successful synchronous response contains PDF bytes, so check the HTTP status before returning the body as a PDF download.
This guide uses a Laravel controller and server-side configuration. Keep your API key on the server, test that the page and its assets are publicly reachable, and choose synchronous generation or a callback based on how long the conversion takes.
1. Configure your API key
Put the key in the server environment, not in a Blade template, browser request, or committed source file. Add this to config/services.php:
'html2pdf' => [
'key' => env('HTML2PDF_API_KEY'),
],
Set HTML2PDF_API_KEY in your deployment environment or local .env file:
HTML2PDF_API_KEY=your_private_api_key
After changing environment configuration in a deployment that caches config, refresh Laravel’s configuration cache as part of your normal deployment process. Never expose the key in client-side JavaScript or a public repository. Html2Pdf.app’s documentation also emphasizes that the key is private.
2. Generate a PDF synchronously
Create a controller that posts a public page URL to the service, checks for success, and returns the bytes as a download. This example targets Laravel 12 and uses Laravel’s HTTP client and streamed download response:
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Str;
use RuntimeException;
class WebsitePdfController extends Controller
{
public function download(Request $request)
{
$validated = $request->validate([
'url' => ['required', 'url', 'max:2048'],
]);
$apiKey = config('services.html2pdf.key');
if (! is_string($apiKey) || $apiKey === '') {
throw new RuntimeException('The HTML2PDF_API_KEY server setting is missing.');
}
$response = Http::withHeaders([
'X-API-Key' => $apiKey,
'Accept' => 'application/pdf',
])
->timeout(90)
->post('https://api.html2pdf.app/v1/generate', [
'html' => $validated['url'],
]);
if (! $response->successful()) {
// Log only safe diagnostic details. Do not return the upstream body or key.
logger()->warning('Html2Pdf.app conversion failed', [
'status' => $response->status(),
]);
return response('PDF generation failed.', 502);
}
$filename = 'website-' . Str::uuid() . '.pdf';
$pdfBytes = $response->body();
return response()->streamDownload(
static function () use ($pdfBytes): void {
echo $pdfBytes;
},
$filename,
['Content-Type' => 'application/pdf']
);
}
}
Register a route, for example in routes/web.php:
use App\Http\Controllers\WebsitePdfController;
use Illuminate\Support\Facades\Route;
Route::get('/website-pdf', [WebsitePdfController::class, 'download']);
Then request /website-pdf?url=https%3A%2F%2Fexample.com. The URL validation here is basic input validation, not a complete defense against server-side request forgery. If users can submit arbitrary URLs, apply an allowlist or other SSRF protections appropriate to your application, and ensure the conversion service can only be asked to fetch destinations you intend to support.
The example keeps the API key in configuration, checks the upstream status, and does not mistake an error response for a PDF. Laravel documents download and streamDownload responses and its HTTP client exposes status and body access through the response API. The integration pattern combines those Laravel facilities with Html2Pdf.app’s documented API contract; it is not a vendor-published Laravel code sample.
3. Send inline HTML instead of a URL
The required request field is html. It can contain a publicly reachable URL or raw HTML markup. For inline content, render a trusted Blade view on the server and send that HTML string:
$html = view('pdf.report', ['report' => $report])->render();
$response = Http::withHeaders([
'X-API-Key' => config('services.html2pdf.key'),
])->timeout(90)->post('https://api.html2pdf.app/v1/generate', [
'html' => $html,
]);
if (! $response->successful()) {
return response('PDF generation failed.', 502);
}
return response()->streamDownload(
fn () => print($response->body()),
'report.pdf',
['Content-Type' => 'application/pdf']
);
For URL conversion, the submitted page must be publicly reachable by the service. For inline markup, verify that linked stylesheets, fonts, images, and other resources are also accessible to the renderer. Avoid placing credentials or secrets in source URLs or markup unless your application’s data handling review permits it.
4. Choose the conversion flow
| Flow | Use it when | Laravel work |
|---|---|---|
| Synchronous | The conversion finishes within the request and the user needs the file immediately. | Post the request, check success, and return the PDF bytes. |
| Callback | Conversion may outlast a web request or should run in the background. | Submit callBackUrl and a correlation state, then handle the callback as a job result. |
A synchronous request is straightforward, but the Laravel request remains open while conversion runs. Set a timeout that fits your application and proxy limits. If conversions are slow or traffic is bursty, a queue plus callback avoids holding a user-facing request open.
Asynchronous generation with a callback
Send callBackUrl and an application-generated state value with the conversion request. An accepted asynchronous request returns HTTP 202. The service later posts JSON containing a base64-encoded document. Decode that field and associate the PDF with the job identified by state.
$state = (string) \Illuminate\Support\Str::uuid();
$response = Http::withHeaders([
'X-API-Key' => config('services.html2pdf.key'),
])->timeout(30)->post('https://api.html2pdf.app/v1/generate', [
'html' => 'https://example.com/report',
'callBackUrl' => route('html2pdf.callback'),
'state' => $state,
]);
if ($response->status() !== 202 && ! $response->successful()) {
return response('Could not queue PDF generation.', 502);
}
// Persist $state and your application's report/job identifier before relying on
// the callback to deliver the completed document.
return response()->json(['state' => $state], 202);
Make the callback endpoint reachable by the service. Validate the callback according to the vendor’s current documentation and your application’s security requirements. Treat callback delivery as repeatable: the vendor says failed delivery can be retried up to three times. Persist the state-to-job mapping and make processing idempotent so a repeated callback does not create duplicate work or corrupt a completed result.
Route::post('/integrations/html2pdf/callback', [Html2PdfCallbackController::class, 'handle'])
->name('html2pdf.callback');
public function handle(Request $request)
{
$payload = $request->validate([
'state' => ['required', 'string', 'max:200'],
'document' => ['required', 'string'],
]);
$pdfBytes = base64_decode($payload['document'], true);
if ($pdfBytes === false) {
return response('Invalid document payload.', 400);
}
// Look up the persisted job by state. Store or process the PDF once.
// Make this operation idempotent because callback delivery can be retried.
return response()->noContent();
}
The callback example shows the payload handling pattern, not a complete callback authentication scheme. Follow the service’s current callback verification instructions before accepting documents in production.
5. Set rendering options deliberately
The API supports options for page format and rendering, including format, landscape, width, height, margins, and media. The media value can be screen or print. Include only the options your report needs and confirm accepted values and exact parameter formats in the current API documentation.
$response = Http::withHeaders([
'X-API-Key' => config('services.html2pdf.key'),
])->timeout(90)->post('https://api.html2pdf.app/v1/generate', [
'html' => 'https://example.com/report',
'format' => 'A4',
'landscape' => false,
'media' => 'print',
]);
Choose print media when the site has print-specific styles and screen when the screen layout is the desired result. Check the output for page breaks, clipped content, and missing resources. The vendor notes that media selection, available fonts and resources, and JavaScript load timing can affect rendering. A page that appears correct in a browser is not guaranteed to produce the same PDF without testing its actual render.
6. Return, store, or queue the PDF
For a small document and an immediate download, returning the binary body is convenient. The example buffers the response body in PHP memory before streaming it to the client. For large PDFs or high-volume workloads, consider storing the result in managed storage or processing it in a queue instead of holding both the upstream response and client download in a web request.
- Check the status before setting
Content-Type: application/pdf. - Do not send JSON or an upstream error page with a
.pdffilename. - Use a safe filename generated by your application; do not directly use untrusted input.
- For queued jobs, persist job state and define how long generated files should be retained.
- Log status and correlation identifiers, but avoid logging API keys, sensitive HTML, or confidential source URLs.
7. Errors and troubleshooting
| Symptom or status | Likely cause | What to do |
|---|---|---|
| 400 Bad Request | Invalid input or inaccessible source URL. | Check the required html field, URL spelling, and public reachability. Do not retry unchanged input. |
| 401 Unauthorized | Missing or invalid API key. | Check the server environment value, configuration cache, and X-API-Key header. Never put the key in the browser. |
| 403 Forbidden | A plan limit was reached or the request is not permitted by the plan. | Review current plan limits and usage before retrying. |
| 500 Internal Server Error | An unhandled error at the service. | Retry after a short delay with increasing delays. If it persists, contact the vendor. |
| Laravel timeout or gateway timeout | The conversion or download takes longer than your application or proxy allows. | Adjust compatible timeouts or move the operation to a queue with callback handling. |
| Blank or incomplete PDF | The page or its assets were unavailable, JavaScript had not finished, or rendering settings differ from the expected layout. | Confirm public access to the page, CSS, fonts, and images. Try the appropriate media setting and test representative pages. |
| PDF file contains an error message | The application saved a non-success response as if it were PDF bytes. | Check status before writing or serving the body and return an error response on failure. |
| Callback appears to process twice | A failed delivery may be retried. | Use the correlation state and make callback processing idempotent. |
The vendor documents 400, 401, 403, and 500 status cases and recommends retrying a persistent 500 with increasing delays. Avoid retrying input, authentication, or plan errors until their cause is corrected.
8. Performance, reliability, and cost
Rendering time depends on the page and the resources it loads. Large documents, slow remote assets, and client-side JavaScript can increase latency. For user-facing downloads, keep an eye on Laravel, web-server, and proxy timeouts together. Use asynchronous callbacks when work cannot reliably complete inside those limits, and make callback handling safe to repeat.
Html2Pdf.app’s published plan page, accessed October 3, 2026, lists these terms. Pricing and quotas can change, so confirm the current pricing page before choosing a plan. The vendor says each 5 MB chunk of generated PDF consumes one credit and credits reset on the first day of the month.
| Plan | Published price | Credits/month | Parallel conversions | PDF size |
|---|---|---|---|---|
| Free | $0 | 100 | 1 | Up to 1 MB |
| Startup | $9/month | 1,000 | 3 | Unlimited |
| Standard | $25/month | 5,000 | 10 | Unlimited |
| Scale | $39/month | 10,000 | 20 | Unlimited |
These are vendor-published limits, not an independent performance or reliability assessment. The vendor states that generated PDFs are processed temporarily and not permanently stored on its servers, and that raw HTML or text is not stored in conversion logs; it says selected request metadata and a source URL may be retained in logs. This is the vendor’s description, not an independent audit. Review its documentation, Privacy Policy, and Data Processing Agreement for your use case. Avoid putting secrets in source URLs or markup unless your own review allows it.
9. Or skip the browser setup
If the result you need is a screenshot or PDF capture of a webpage, ScreenshotNeo offers a one-call API. Its PDF output can be configured with paper size, margins, landscape orientation, and page ranges. It also removes cookie banners, newsletter popups, and chat widgets before capture; bot checks, blank pages, and failed loads are never billed; and its MCP server lets AI agents take screenshots.
See the ScreenshotNeo API documentation for current request options.
curl -G "https://api.screenshotneo.com/v1/shot" \
-d access_key=YOUR_API_KEY \
--data-urlencode url=https://stripe.com \
-o shot.webp
import requests
r = requests.get(
"https://api.screenshotneo.com/v1/shot",
params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"},
timeout=90,
)
r.raise_for_status()
open("shot.webp", "wb").write(r.content)
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
if (!res.ok) throw new Error(`ScreenshotNeo returned ${res.status}`);
const bytes = new Uint8Array(await res.arrayBuffer());
await import('node:fs/promises').then(fs => fs.writeFile('shot.webp', bytes));
ScreenshotNeo includes 1,000 screenshots a month free with no card; paid plans start at $5 for 3,000. Sign up free for ScreenshotNeo.
10. Frequently asked questions
Does the synchronous endpoint return JSON?
No. On success it returns the PDF bytes. Check the HTTP status before treating the body as a file.
Can I convert a page on my local machine?
The service needs to reach a URL submitted for URL-based conversion. A localhost address on your computer is not generally publicly reachable by an external service; use a reachable environment or send HTML directly where appropriate.
Should I use a queue for every conversion?
No. Use synchronous generation when it reliably fits the request window and the user needs an immediate download. Use callback-based processing when render duration or traffic makes an open web request unsuitable.
Can I trust the vendor’s data-handling description as an audit?
No. It is the vendor’s stated practice. Review its current policy and agreement against your application’s data requirements.
Sources
- Html2Pdf.app documentation — request contract, authentication, callback flow, status codes, and vendor data-handling statements.
- Html2Pdf.app PHP examples — PHP requirements and troubleshooting notes.
- Html2Pdf.app pricing — published plan terms and credit rules.
- Laravel HTTP responses — download and streamed download responses.
- Laravel HTTP client response API — response status and body access.


