How to Call the Html2Pdf.app API from PHP
Send HTML or a public URL to Html2Pdf.app from PHP, then save or stream the returned PDF. Includes synchronous and callback examples, options, and fixes.
Call https://api.html2pdf.app/v1/generate with a JSON POST, put your API key in the X-API-Key header, and pass either raw HTML or a publicly reachable URL in the required html field. For a synchronous request, a successful response body is the PDF’s binary data: check the HTTP status before saving it or returning it to a browser.
The examples below use PHP 8.1 or newer and the PHP cURL extension. Keep the API key in an environment variable or your framework’s secret store. Do not put it in browser JavaScript, a public repository, or a client-rendered template.
1. Make a synchronous PDF request in PHP
Set the key in your server environment as HTML2PDF_API_KEY, then save this as generate.php and run it from the command line. Replace the example URL or HTML with the document you want to render.
<?php
declare(strict_types=1);
$apiKey = getenv('HTML2PDF_API_KEY');
if ($apiKey === false || $apiKey === '') {
throw new RuntimeException('Set the HTML2PDF_API_KEY environment variable.');
}
$payload = [
'html' => 'https://www.example.com',
];
$json = json_encode($payload, JSON_THROW_ON_ERROR);
$ch = curl_init('https://api.html2pdf.app/v1/generate');
if ($ch === false) {
throw new RuntimeException('Could not initialize cURL.');
}
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POSTFIELDS => $json,
CURLOPT_HTTPHEADER => [
'Content-Type: application/json',
'X-API-Key: ' . $apiKey,
],
]);
$pdf = curl_exec($ch);
$statusCode = (int) curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
$error = curl_error($ch);
curl_close($ch);
if ($pdf === false) {
throw new RuntimeException('Request failed: ' . $error);
}
if ($statusCode < 200 || $statusCode >= 300) {
throw new RuntimeException('PDF generation returned HTTP ' . $statusCode . ': ' . $pdf);
}
$outputPath = __DIR__ . '/document.pdf';
if (file_put_contents($outputPath, $pdf) === false) {
throw new RuntimeException('Could not write the PDF to ' . $outputPath);
}
echo 'Saved PDF to ' . $outputPath . PHP_EOL;
The provider’s PHP guide lists PHP 8.1+ and cURL as requirements. The same request pattern works in a plain PHP script or in a server-side job. In Laravel or Symfony, read the API key from the framework’s secret configuration and keep the HTTP call in server-side application code.
Send raw HTML instead of a URL
The required html value can contain markup directly. Encode the document as JSON instead of manually concatenating a JSON string; this safely handles quotes, newlines, and non-ASCII characters.
$payload = [
'html' => '<!doctype html>
<html>
<head>
<meta charset="utf-8">
<style>body { font-family: sans-serif; margin: 32px; }</style>
</head>
<body>
<h1>Monthly report</h1>
<p>Generated by a PHP application.</p>
</body>
</html>',
];
Use the same cURL setup and response handling as the first example. For markup that references images, stylesheets, scripts, or fonts by URL, make sure those resources are reachable by the rendering service. Do not assume that resources available only on your local machine or behind a private network can be fetched remotely.
2. Return the PDF from a PHP controller
To let a user download the result, send the binary response with Content-Type: application/pdf and an attachment disposition. Do not return an upstream error page or JSON error body with PDF headers.
<?php
$apiKey = getenv('HTML2PDF_API_KEY');
if ($apiKey === false || $apiKey === '') {
http_response_code(500);
exit('PDF service is not configured.');
}
$payload = [
'html' => '<h1>Receipt</h1><p>Order #1234</p>',
'filename' => 'receipt.pdf',
'format' => 'A4',
'marginTop' => '12mm',
'marginRight' => '12mm',
'marginBottom' => '12mm',
'marginLeft' => '12mm',
];
$ch = curl_init('https://api.html2pdf.app/v1/generate');
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POSTFIELDS => json_encode($payload, JSON_THROW_ON_ERROR),
CURLOPT_HTTPHEADER => [
'Content-Type: application/json',
'X-API-Key: ' . $apiKey,
],
]);
$pdf = curl_exec($ch);
$statusCode = (int) curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
$error = curl_error($ch);
curl_close($ch);
if ($pdf === false || $statusCode < 200 || $statusCode >= 300) {
error_log('PDF conversion failed. HTTP ' . $statusCode . '; ' . $error);
http_response_code(502);
exit('The PDF could not be generated. Please try again later.');
}
header('Content-Type: application/pdf');
header('Content-Disposition: attachment; filename="receipt.pdf"');
header('Content-Length: ' . strlen($pdf));
echo $pdf;
In production, avoid exposing upstream response details to visitors. Log useful diagnostics on the server and return an appropriate application error. If your framework has an HTTP client, you can use it instead of cURL, but preserve the same JSON request, API key header, status check, and binary-response handling.
3. Use asynchronous conversion with a callback
Use a callback when the PHP request should not stay open while a document renders. Include callBackUrl in the request. A successful enqueue returns 202 Accepted; it means the job is queued, not that the response contains a PDF.
<?php
$apiKey = getenv('HTML2PDF_API_KEY');
if ($apiKey === false || $apiKey === '') {
throw new RuntimeException('Missing HTML2PDF_API_KEY.');
}
$payload = [
'html' => 'https://www.example.com/monthly-report',
'callBackUrl' => 'https://app.example.com/webhooks/html2pdf',
'state' => 'report-1234',
];
$ch = curl_init('https://api.html2pdf.app/v1/generate');
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POSTFIELDS => json_encode($payload, JSON_THROW_ON_ERROR),
CURLOPT_HTTPHEADER => [
'Content-Type: application/json',
'X-API-Key: ' . $apiKey,
],
]);
$response = curl_exec($ch);
$statusCode = (int) curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
$error = curl_error($ch);
curl_close($ch);
if ($response === false) {
throw new RuntimeException('Could not submit PDF job: ' . $error);
}
if ($statusCode !== 202) {
throw new RuntimeException('Expected HTTP 202 from queued job; got ' . $statusCode . ': ' . $response);
}
echo "PDF job queued.\n";
Make the callback endpoint publicly reachable over HTTPS and able to accept a POST. The callback JSON contains document, a base64-encoded PDF, and may include the unchanged state value. Decode document before saving or serving it.
<?php
$raw = file_get_contents('php://input');
if ($raw === false) {
http_response_code(400);
exit;
}
try {
$callback = json_decode($raw, true, 512, JSON_THROW_ON_ERROR);
} catch (JsonException $e) {
http_response_code(400);
exit;
}
$encodedDocument = $callback['document'] ?? null;
if (!is_string($encodedDocument)) {
http_response_code(400);
exit;
}
$pdf = base64_decode($encodedDocument, true);
if ($pdf === false) {
http_response_code(400);
exit;
}
$state = $callback['state'] ?? null;
// Look up the pending job using your application's state/job mapping.
// Make this operation idempotent: repeated callbacks must not duplicate work.
$path = __DIR__ . '/completed-report.pdf';
if (file_put_contents($path, $pdf) === false) {
http_response_code(500);
exit;
}
http_response_code(204);
The provider documents that callback delivery may be attempted more than once, with up to three retries before delivery is marked failed. Design the handler to be idempotent: identify the job, detect whether it has already been completed, and avoid creating duplicate records or side effects. A callback endpoint should validate requests according to the provider’s current webhook guidance before trusting the payload; do not treat an arbitrary public POST as an authorized completion.
4. Configure the PDF output
Options are JSON fields in the same request as html. The documented settings include:
| Option | Purpose and notes |
|---|---|
format |
Choose a paper format. Documented choices include Letter, Legal, Tabloid, Ledger, and A0 through A6. |
landscape |
Set page orientation to landscape when the document is wider than it is tall. |
width, height |
Set custom page dimensions instead of relying only on a named paper format. |
marginTop, marginRight, marginBottom, marginLeft |
Set each page margin. The example uses CSS-style millimeter values. |
media |
Choose screen or print CSS media behavior. The choice can change colors, visibility, and layout. |
filename |
Set a filename for the generated document or download behavior. |
waitFor |
Wait for JavaScript-driven content, from 0 to 10 seconds as documented. This is a timing allowance, not a guarantee that every application has finished loading. |
scale |
Adjust rendered scale from 0.1 to 2 as documented. Check readability after changing it. |
| Header and footer templates | Add repeating page header or footer content. Check page layout and margins so the templates do not overlap the body. |
| Password and permission fields | Configure encryption and PDF permissions when required by the document workflow. |
For example, a landscape report with print styles and a short wait can be requested like this:
$payload = [
'html' => 'https://www.example.com/report',
'format' => 'A4',
'landscape' => true,
'media' => 'print',
'marginTop' => '15mm',
'marginRight' => '12mm',
'marginBottom' => '15mm',
'marginLeft' => '12mm',
'waitFor' => 2,
'scale' => 1,
];
Use values accepted by the current API schema. The documented waitFor and scale ranges are bounded; invalid parameter values can produce a 400 response. For encrypted PDFs or header/footer templates, consult the provider’s current API documentation for the exact field names and accepted formats before adding them.
5. cURL, Python, and Node.js equivalents
These examples use the same endpoint and API key header. They illustrate the basic synchronous request; check for a successful status before treating response bytes as a PDF.
cURL
curl --fail-with-body \
-X POST 'https://api.html2pdf.app/v1/generate' \
-H 'Content-Type: application/json' \
-H "X-API-Key: $HTML2PDF_API_KEY" \
--data '{"html":"https://www.example.com","format":"A4"}' \
--output document.pdf
Python
import os
import requests
api_key = os.environ['HTML2PDF_API_KEY']
response = requests.post(
'https://api.html2pdf.app/v1/generate',
headers={
'Content-Type': 'application/json',
'X-API-Key': api_key,
},
json={
'html': 'https://www.example.com',
'format': 'A4',
},
timeout=90,
)
response.raise_for_status()
with open('document.pdf', 'wb') as output:
output.write(response.content)
Node.js
import { writeFile } from 'node:fs/promises';
const apiKey = process.env.HTML2PDF_API_KEY;
if (!apiKey) throw new Error('Set HTML2PDF_API_KEY');
const response = await fetch('https://api.html2pdf.app/v1/generate', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-API-Key': apiKey,
},
body: JSON.stringify({
html: 'https://www.example.com',
format: 'A4',
}),
});
if (!response.ok) {
throw new Error(`PDF generation failed: HTTP ${response.status} ${await response.text()}`);
}
await writeFile('document.pdf', Buffer.from(await response.arrayBuffer()));
6. Troubleshoot common failures
| Symptom or status | Likely cause | What to do |
|---|---|---|
| 400 Bad Request | The source URL cannot be reached, or a request parameter is invalid. | Check that the URL is public and correctly formed. Validate option names and values, including documented ranges for waitFor and scale. |
| 401 Unauthorized | The API key is missing or invalid. | Confirm the server has the correct key and that the request sends it as X-API-Key. Keep it out of client-side code. |
| 403 Forbidden | The account has reached a plan limit. | Check the account and current plan limits before retrying. Repeating the same request will not resolve an account limit. |
| 500 Internal Server Error | An unhandled error occurred on the service. | Retry after a short delay. If retrying repeatedly, increase the delay between attempts and avoid launching a large burst of duplicate conversions. |
| PHP reports cURL is undefined | The PHP cURL extension is not installed or enabled for the PHP runtime handling the script. | Enable or install cURL for that runtime, then confirm the web server and command-line PHP use the expected configuration. |
| The saved file is not a PDF | An error response was saved as if it were a successful binary PDF. | Check the HTTP status before writing or streaming. Log the error response on the server and do not send it with PDF headers. |
| PDF is blank or content is missing | The source URL may not be publicly accessible, or its CSS, fonts, images, or JavaScript content may not be ready or reachable. | Test the URL from outside your network, check resource URLs, select the correct media mode, and use waitFor for content that appears after JavaScript runs. |
| Styles or page breaks differ from the browser | Print and screen styles can produce different layouts; Chromium rendering may also expose timing or resource differences. | Compare screen and print media settings, inspect print-specific CSS, and test representative pages before deploying. |
| Callback job appears stuck | The callback URL may not be publicly reachable over HTTPS, may reject POST requests, or may fail while processing the payload. | Check endpoint reachability and server logs. Return a success status after durable processing, and make repeated delivery safe through idempotency. |
Do not automatically retry 400, 401, or 403 responses before fixing the request, credentials, or account limit. For retryable server or network failures, use a bounded retry policy with increasing delays and ensure duplicate requests will not cause unwanted side effects.
7. Rendering, reliability, and cost considerations
Rendering behavior
Html2Pdf.app documents that rendering runs in headless Chromium and supports modern HTML, CSS, and JavaScript. Output still depends on what the renderer can reach and when the page is ready. Publicly hosted fonts and images, JavaScript timing, and the selected CSS media mode can all affect the PDF. Test documents that represent your real layouts, especially long pages, tables, and pages with client-rendered content.
Choose synchronous or asynchronous processing
| Approach | Use it when | Application work required |
|---|---|---|
| Synchronous | The caller can wait for the conversion and needs the PDF in the same request. | Handle the binary response, enforce an appropriate application timeout, and return it only after a successful status. |
| Asynchronous callback | The conversion should continue after the initiating request ends. | Host a public HTTPS callback endpoint, map the returned state to a job, decode the base64 document, and handle duplicate delivery idempotently. |
Plan capacity and spending
The provider’s pricing page, checked on 2026-10-03, listed Free at 100 credits per month, Startup at $9 for 1,000 credits, Standard at $25 for 5,000 credits, and Scale at $39 for 10,000 credits. The page says each 5 MB chunk of generated PDF costs one credit, credits reset on the first day of each month, and the Free plan limits PDFs to 1 MB; paid plans list unlimited PDF size. Pricing and limits can change, so check the current pricing page and account limits before estimating production volume.
Estimate usage from generated PDF size and expected monthly volume, then account for larger documents and retries. The Free plan’s 1 MB cap matters if your output contains high-resolution images or many pages. Avoid blind retries for account-limit responses, and use asynchronous conversion when holding an application request open would be a poor fit.
Or skip the browser setup
If your goal is a website screenshot or a PDF capture of a page, ScreenshotNeo provides a website screenshot API and MCP server. Its API accepts a URL in one GET request and can return PNG, JPEG, WebP, or PDF. See the ScreenshotNeo API documentation for the available parameters.
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)
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}`);
- Cookie and consent banners, newsletter popups, and chat widgets are removed before the shot; each cleanup step can be turned off.
- Bot checks, blank pages, timeouts, failed loads, and cache hits are not billed. Responses include
X-Page-VerdictandX-Billedheaders. - An MCP server gives AI agents tools named
take_screenshot,get_page_info, andcapture_pdf. - 1,000 screenshots per month are free with no card; paid plans start at $5 for 3,000 screenshots.
Sign up for ScreenshotNeo’s free plan to get 1,000 screenshots a month with no card.
FAQ
Can I call the API from browser JavaScript?
Keep the API key on your server. Send the request from PHP or another trusted backend, then return the generated PDF through your application.
Does a 202 response contain the finished PDF?
No. It confirms that an asynchronous job was accepted. The PDF arrives later at the callback URL as base64 data in the document field.
Can the API convert HTML that is not public?
You can send raw HTML in the request. If that HTML references external resources, those resources still need to be reachable by the renderer for them to appear.
Should I use print or screen media?
Choose the mode that matches the CSS you want rendered. If the output is missing styling or differs from your page, test both modes and review the corresponding CSS rules.


