How to Convert an HTML Invoice to PDF with APITemplate.io in Laravel
Generate an invoice PDF from raw HTML in Laravel with APITemplate.io, or use a reusable template when the layout repeats. Includes request handling and troubleshooting.
To convert an HTML invoice to PDF in Laravel with APITemplate.io, send the invoice HTML in a JSON request to POST https://rest.apitemplate.io/v2/create-pdf-from-html. Authenticate with the X-API-KEY header, then read and validate the returned download_url. If the same invoice layout is reused, a saved APITemplate.io template with invoice data sent as JSON may be easier to maintain.
This guide covers both routes. The raw HTML endpoint is the most direct fit when Laravel already builds the invoice markup. The template recommendation is based on the provider’s description of reusable templates; the available sources do not establish a speed or cost advantage for either method.
1. Configure the API key in Laravel
Keep the API key on the server. Do not place it in browser JavaScript, HTML, or a URL. Add it to the environment configuration:
APITEMPLATE_API_KEY=your_api_key_here
Expose it through config/services.php:
'apitemplate' => [
'key' => env('APITEMPLATE_API_KEY'),
],
After changing configuration in a deployment that caches Laravel config, refresh that cache as part of the normal deployment process. The request below reads the key from config(), rather than accessing the environment directly in application code.
2. Build invoice HTML safely
Start with complete HTML. Escape values that came from users or external systems before inserting them into markup. Otherwise, invoice fields can break the document or inject unintended HTML. Format monetary values and dates deliberately in your application, and use stable invoice data rather than trusting a client-submitted total.
$invoice = [
'number' => 'INV-2026-0042',
'customer' => 'Acme Ltd',
'issued_at' => '2026-10-04',
'currency' => 'USD',
'items' => [
['description' => 'Consulting', 'quantity' => 2, 'unit_price' => 125.00],
['description' => 'Hosting', 'quantity' => 1, 'unit_price' => 30.00],
],
];
$escape = static fn ($value) => e((string) $value);
$rows = '';
$subtotal = 0.0;
foreach ($invoice['items'] as $item) {
$lineTotal = $item['quantity'] * $item['unit_price'];
$subtotal += $lineTotal;
$rows .= '<tr>'
. '<td>' . $escape($item['description']) . '</td>'
. '<td class="number">' . $escape($item['quantity']) . '</td>'
. '<td class="number">' . number_format($item['unit_price'], 2) . '</td>'
. '<td class="number">' . number_format($lineTotal, 2) . '</td>'
. '</tr>';
}
$invoiceHtml = '<!doctype html>
<html>
<head><meta charset="utf-8"><title>Invoice '" . $escape($invoice['number']) . '</title></head>
<body>
<h1>Invoice '" . $escape($invoice['number']) . '</h1>
<p>Bill to: '" . $escape($invoice['customer']) . '</p>
<p>Issued: '" . $escape($invoice['issued_at']) . '</p>
<table>
<thead><tr><th>Description</th><th>Qty</th><th>Unit price</th><th>Total</th></tr></thead>
<tbody>' . $rows . '</tbody>
</table>
<p class="total">Subtotal: ' . $escape($invoice['currency']) . ' ' . number_format($subtotal, 2) . '</p>
</body>
</html>';
$invoiceCss = '
body { font-family: Arial, sans-serif; color: #222; font-size: 12px; }
h1 { font-size: 24px; }
table { border-collapse: collapse; width: 100%; margin-top: 24px; }
th, td { border-bottom: 1px solid #ddd; padding: 8px; text-align: left; }
.number { text-align: right; }
.total { text-align: right; font-weight: bold; margin-top: 20px; }
';
This example keeps the HTML generation visible so it can be adapted to a Blade view or an existing invoice renderer. If using Blade, render a dedicated invoice view to a string and ensure untrusted values remain escaped. Avoid inserting user-provided HTML as raw markup unless it has been deliberately sanitized.
3. Send HTML to APITemplate.io from Laravel
Laravel’s HTTP client can send the JSON payload and set a request timeout. The API documents body as the required HTML field and supports optional css, data for Jinja2 placeholders, and settings for page configuration.
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Log;
use RuntimeException;
$response = Http::withHeaders([
'X-API-KEY' => config('services.apitemplate.key'),
'Accept' => 'application/json',
])->timeout(100)->post(
'https://rest.apitemplate.io/v2/create-pdf-from-html',
[
'body' => $invoiceHtml,
'css' => $invoiceCss,
'settings' => [
'paper_size' => 'A4',
'orientation' => '1',
'print_background' => '1',
],
]
);
// Laravel does not throw automatically for HTTP 4xx/5xx responses.
$response->throw();
$pdfUrl = $response->json('download_url');
if (! is_string($pdfUrl) || filter_var($pdfUrl, FILTER_VALIDATE_URL) === false) {
throw new RuntimeException('APITemplate.io did not return a valid download_url.');
}
$transactionRef = $response->json('transaction_ref');
$status = $response->json('status');
The settings shown follow the provider’s documented example: A4 paper, orientation, and background printing. Verify accepted names and values against the current [APITemplate.io raw HTML endpoint documentation](https://apitemplate.io/apiv2/) before relying on them, because endpoint configuration can change. The endpoint documentation describes Chromium-based rendering, including CSS and JavaScript, and supports page configuration such as headers, footers, page numbers, paper size, and margins.
The 100-second timeout is an illustrative setting, not a universal production value. The vendor’s PHP client lists limits that vary by region and API mode; your Laravel web request, queue worker, reverse proxy, and hosting platform may impose lower limits. Set the timeout within the request budget of your application and the currently applicable provider limit.
4. Return or download the PDF
The generation response includes a download_url. One option is to redirect the user to it:
return redirect()->away($pdfUrl);
Alternatively, fetch the PDF server-side and stream it from your application. If you do that, check the download response status and content type before returning the bytes. Do not assume a download URL’s lifetime, access controls, or retention period: those details are not established by the API references used here. If invoice access is sensitive, apply your application’s authorization checks before providing the URL or proxying the file.
For a user-facing controller, convert HTTP errors and connection timeouts into controlled application responses. Avoid returning raw provider responses or logging invoice HTML, customer details, API keys, or full download URLs if those could grant access.
5. Use a reusable template for recurring invoice layouts
If every invoice shares a layout, create a PDF template in the APITemplate.io console. Use the HTML editor for direct control of HTML, CSS, and JavaScript, and put dynamic values in Jinja2 placeholders such as {{ invoice_number }}. Jinja2 loops and conditionals can represent line items and optional invoice sections. The provider also offers a visual editor for simpler layouts where its reduced HTML/CSS control is sufficient. Preview the template with sample JSON before wiring it into Laravel.
Send the template ID and invoice data as JSON to POST /v2/create-pdf?template_id=YOUR_TEMPLATE_ID, using the same API key header. The provider documents a successful response with a PDF download_url.
$response = Http::withHeaders([
'X-API-KEY' => config('services.apitemplate.key'),
'Accept' => 'application/json',
])->timeout(100)->post(
'https://rest.apitemplate.io/v2/create-pdf?template_id=YOUR_TEMPLATE_ID',
[
'invoice_number' => $invoice['number'],
'customer' => $invoice['customer'],
'currency' => $invoice['currency'],
'items' => $invoice['items'],
]
)->throw();
$pdfUrl = $response->json('download_url');
if (! is_string($pdfUrl) || filter_var($pdfUrl, FILTER_VALIDATE_URL) === false) {
throw new RuntimeException('APITemplate.io did not return a valid download_url.');
}
Choose the raw HTML endpoint when Laravel assembles the full document or the invoice is a one-off. Choose a reusable template when the same layout is generated repeatedly and separating layout from per-invoice data makes maintenance clearer. This is a workflow choice, not a claim that one route is faster or cheaper.
6. Laravel HTTP and PDF configuration details
- JSON request: Laravel’s
post()method accepts an array payload for a JSON request. Include the HTML as a string inbody; do not send the invoice as a local file path. - Authentication: Send the documented
X-API-KEYrequest header. Keep the key server-side. - Optional raw HTML fields:
csssupplies styles;datasupplies values for Jinja2 placeholders in the body;settingsconfigures PDF output. - Page setup: The provider’s example includes paper size, orientation, margins, and background printing. Confirm current accepted setting names and values in the endpoint docs.
- Renderer behavior: The provider describes a Chromium-based renderer. Layouts that depend on external resources or browser execution should be checked with representative invoice data and the current endpoint behavior.
- Failure handling: Use
throw()or explicitly inspect the response status. Laravel does not automatically throw just because the server returned a 4xx or 5xx response. - Timeouts: Set an intentional client timeout that fits your app and provider limits. Laravel documents that a timeout raises a connection exception.
7. Reliability, performance, and cost considerations
PDF generation is an external network operation, so the request can fail due to a connection problem, a timeout, provider-side validation, or rendering failure. Handle these cases explicitly. If the user is waiting in a web request, keep the timeout within the application’s own request budget. For slow or bulk generation, Laravel queues are an implementation option: enqueue a job, generate the document in a worker, and let the user retrieve it when ready. APITemplate.io describes synchronous and asynchronous generation and webhook notifications in its methods overview; verify the precise endpoint and callback behavior before building a webhook flow.
Do not retry every failure blindly. A timeout can leave uncertainty about whether the provider completed the generation. Before adding automatic retries, check the provider’s current guidance on idempotency and transaction references so retries do not create confusing duplicate work. The references in this guide do not establish retry semantics, a rendering benchmark, or a price comparison between raw HTML and templates.
No APITemplate.io rate, subscription price, or retention period is asserted here. Check current account pricing and terms for your expected volume, and review the provider’s privacy and retention terms before sending sensitive invoice data. Keep logs useful but minimal: record an internal invoice identifier, outcome, and appropriate transaction reference without logging secrets or full customer documents.
8. Troubleshooting common problems
| Symptom | Likely cause | What to check or change |
|---|---|---|
| 401 or 403 response | Missing, invalid, or incorrectly configured API key. | Confirm the server environment value is present, config is refreshed if cached, and the exact X-API-KEY header is sent. |
| 4xx validation error | Malformed JSON payload, missing body, or unsupported setting name/value. |
Confirm body is a string containing HTML and compare settings with the current endpoint documentation. Surface a sanitized error to the caller. |
| Laravel appears successful but no PDF URL exists | HTTP errors do not throw automatically, or response JSON differs from the expected success shape. | Call throw() or check successful(); then verify the response has a non-empty download_url before redirecting. |
| Connection exception or timeout | Rendering/network time exceeded the client or hosting request limit. | Review Laravel timeout, PHP and web server limits, proxy limits, and the provider’s current regional/API-mode limit. Move long-running work to a queue where appropriate. |
| PDF is missing styles or background colors | CSS was omitted, invalid, or background printing was not enabled as expected. | Send CSS in the documented field or inline it for diagnosis; confirm the current background-print setting and use supported print CSS. |
| Invoice content is blank or incomplete | HTML construction, dynamic values, external assets, or JavaScript-dependent rendering did not produce the expected document. | Inspect the final HTML string, test with a small static document, and verify all needed assets are available to the renderer. Avoid relying on undocumented resource timing behavior. |
| Characters display incorrectly | Document encoding is missing or source text is malformed. | Include a UTF-8 charset declaration, preserve UTF-8 data through JSON encoding, and inspect the source string before sending. |
| Unexpected markup in an invoice field | Untrusted data was inserted without escaping. | Escape text values with Laravel’s HTML escaping or use an escaped Blade view. Sanitize deliberately if rich HTML is a requirement. |
| Download URL fails later | The URL may have access or lifetime constraints not covered by the sources here. | Consult current provider documentation for retention and access behavior; download or proxy the file in a controlled flow if it fits your requirements. |
9. Or skip the browser setup
If your goal is to capture a rendered invoice page as an image or PDF, ScreenshotNeo provides a website screenshot API. A single GET request can return PNG, JPEG, WebP, or PDF; it is a screenshot service, so use APITemplate.io when you need the HTML-to-invoice PDF workflow described above.
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
See the ScreenshotNeo API documentation. ScreenshotNeo accepts cookie/consent banners and removes 60+ known consent platforms, newsletter popups, and chat widgets before capture; each step can be turned off. Bot checks/CAPTCHAs, blank pages, timeouts, failed loads, and cache hits cost nothing, and response headers identify the page verdict and billing status. Its MCP server lets Claude, Cursor, and other MCP clients use take_screenshot, get_page_info, and capture_pdf. The free plan includes 1,000 screenshots per month with no card; paid plans start at $5 for 3,000 screenshots. Learn about ScreenshotNeo or sign up free for 1,000 screenshots a month with no card.
10. Frequently asked questions
Can I use a Blade view as the invoice HTML?
Yes. Render a dedicated Blade view to a string and send that string as the raw endpoint’s body. Keep ordinary Blade escaping enabled for values that are not trusted markup.
Should invoice rows be created in Laravel or Jinja2?
Either can fit the workflow. Build the complete HTML in Laravel for the raw HTML route, or pass structured fields and line items to a saved template when that better separates a recurring layout from invoice data.
Does the API response contain the PDF bytes?
The documented successful response includes a download_url, along with transaction_ref and status. Handle the returned URL according to the provider’s current access and retention rules.
Can this run outside a web controller?
Yes. The Laravel HTTP client can be used from a queued job or command as well. Queues are useful when generation time does not fit a user’s synchronous request budget.
Sources
- APITemplate.io v2 API documentation (generation methods, raw HTML request, response, and settings).
- APITemplate.io documentation (templates and editor workflows).
- Laravel HTTP Client documentation (requests, timeouts, and response error handling).
- APITemplate.io PHP client repository (regional and API-mode timeout context).


