ScreenshotNeo

BlogHow-to

How to Use PDFShift API in a Laravel App to Generate Invoices

Render invoice HTML in Laravel, convert it with PDFShift, and store the PDF safely. Includes server-side code, delivery options, and troubleshooting.

By the ScreenshotNeo team4 October 202610 min read

To generate an invoice PDF with PDFShift in Laravel, render the invoice as HTML on your server, send that HTML in a JSON POST request to https://api.pdfshift.io/v3/convert/pdf with your API key in the X-API-Key header, then save the returned PDF bytes in storage you control. Keep the API key out of browser code. PDFShift can also return a temporary download URL when you provide a filename; that URL is documented as available for two days, so copy the PDF into your own storage if it must remain available.

The example below is an implementation pattern based on PDFShift’s documented endpoint and authentication. It is not a tested, vendor-supplied Laravel integration. Confirm current request options and response behavior against the PDFShift API information and authentication guidance before deploying.

1. Choose where the invoice PDF comes from

This workflow fits an invoice whose content your Laravel application owns: render a Blade template from trusted invoice data, then convert the resulting HTML. If the document is an invoice generated through Stripe billing in Laravel Cashier, first check whether Cashier’s documented invoice PDF feature already fits that billing flow. PDFShift converts HTML or a URL supplied by your application; Cashier’s feature belongs to the Stripe billing context. See the Laravel Cashier billing documentation.

Question Use this pattern when
Who owns the invoice content? Your application renders its own HTML invoice.
Is it a Stripe/Cashier invoice? Check Cashier’s invoice PDF support before building a separate conversion step.
Must the PDF persist? Save the bytes in application-controlled storage and associate the path with the invoice.

2. Configure the API key on the server

Never put the PDFShift key in a Blade page, JavaScript bundle, mobile app, or request sent directly from a browser. Users can inspect frontend code and recover a key. Store it in the deployment environment and expose it through Laravel configuration.

PDFSHIFT_API_KEY=your_pdfshift_api_key

In config/services.php, add:

'pdfshift' => [
    'key' => env('PDFSHIFT_API_KEY'),
],

After changing environment configuration in a deployed Laravel app, follow your usual configuration-cache process so the running app sees the new value. Do not commit a real key to source control.

3. Render invoice HTML with Blade

Pass a persisted invoice and its related line items to a dedicated view. Escape customer-controlled values through Blade’s normal escaped output syntax ({{ ... }}); avoid raw HTML output for untrusted names, addresses, or descriptions.

<!-- resources/views/invoices/pdf.blade.php -->
<!doctype html>
<html>
<head>
    <meta charset="utf-8">
    <style>
        body { font-family: sans-serif; color: #222; }
        table { width: 100%; border-collapse: collapse; }
        th, td { padding: 8px; border-bottom: 1px solid #ddd; text-align: left; }
        .total { text-align: right; margin-top: 24px; }
    </style>
</head>
<body>
    <h1>Invoice {{ $invoice->number }}</h1>
    <p>{{ $invoice->customer_name }}</p>
    <p>Issued: {{ $invoice->issued_at->toDateString() }}</p>
    <table>
        <thead><tr><th>Description</th><th>Amount</th></tr></thead>
        <tbody>
        @foreach ($invoice->items as $item)
            <tr>
                <td>{{ $item->description }}</td>
                <td>{{ number_format($item->amount_minor / 100, 2) }}</td>
            </tr>
        @endforeach
        </tbody>
    </table>
    <p class="total">Total: {{ number_format($invoice->total_minor / 100, 2) }}</p>
</body>
</html>

Adapt currency formatting and minor-unit handling to your invoice model and currency. This template is illustrative: tax presentation, currency rules, required invoice fields, and retention obligations depend on your application and jurisdiction. The research sources do not establish legal invoice requirements.

4. Convert the HTML and save the PDF bytes

PDFShift’s request pattern uses a JSON source field containing HTML or a URL and authenticates with X-API-Key. With no filename, the documented behavior is to return the PDF itself. The Laravel code below assumes that response is raw PDF bytes, checks for a successful HTTP status, and stores those bytes using Laravel’s configured storage disk.

<?php

namespace App\Services;

use App\Models\Invoice;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Storage;
use RuntimeException;

class InvoicePdfGenerator
{
    public function generate(Invoice $invoice): string
    {
        $html = view('invoices.pdf', ['invoice' => $invoice])->render();
        $apiKey = config('services.pdfshift.key');

        if (! is_string($apiKey) || $apiKey === '') {
            throw new RuntimeException('PDFShift API key is not configured.');
        }

        $response = Http::withHeaders([
                'X-API-Key' => $apiKey,
                'Accept' => 'application/pdf',
            ])
            ->timeout(90)
            ->post('https://api.pdfshift.io/v3/convert/pdf', [
                'source' => $html,
            ]);

        if (! $response->successful()) {
            // Log a request identifier and status for diagnosis. Avoid logging the API key
            // or full invoice HTML, which may contain personal or financial information.
            throw new RuntimeException(
                'PDFShift conversion failed with HTTP status '.$response->status()
            );
        }

        $pdfBytes = $response->body();
        if ($pdfBytes === '') {
            throw new RuntimeException('PDFShift returned an empty response body.');
        }

        $path = 'invoices/'.$invoice->getKey().'.pdf';
        Storage::disk('local')->put($path, $pdfBytes);

        return $path;
    }
}

Set the HTTP timeout to suit your queue and request limits; the value shown is an application-side example, not a PDFShift guarantee. If conversion may outlast a web request, dispatch it to a queue and show the invoice as processing until the job finishes. Save the resulting storage path to your invoice record after confirming the storage write succeeded.

5. Use a temporary URL only when it suits delivery

PDFShift documents a different response mode when you send filename: the service temporarily stores the generated document and returns JSON containing a URL available for two days. That can be useful for a short-lived handoff. It is not durable archival storage. Download the document while the URL is valid and save it in your application’s storage if users must access it later.

The retrieved material does not establish the exact JSON schema or error response fields for the filename mode. Check the current API reference before adding code that parses the returned URL. Treat the URL as temporary access, do not persist it as the invoice’s permanent location, and handle expiration or download failure by generating the document again or reporting a recoverable failure.

6. Decide whether to send HTML or a URL

  • Send HTML when Laravel can render the invoice directly and the markup is the authoritative document.
  • Send a URL when the page is already hosted and reachable by PDFShift. Ensure it does not expose a customer invoice publicly without appropriate access controls.
  • Store the result in your own configured disk when retention, repeat access, or a stable download link matters.

The sources establish that source accepts HTML or a URL. They do not establish other conversion options, their names, or defaults. Verify layout, page sizing, fonts, image loading, and any additional API parameters in the current PDFShift reference instead of assuming option names.

7. Expose invoice downloads through your application

Keep the stored file behind your application’s authorization check when an invoice is private. A download controller should load the invoice for the authenticated user, retrieve its saved path, and return a download response from the chosen storage disk. Avoid exposing predictable public file paths for documents containing customer or billing information.

For large or slow conversions, queue the conversion job and make it idempotent: a retry should not create duplicate invoice records or overwrite a different document. Record useful state such as pending, ready, or failed, plus a storage path and generation timestamp. Do not log the key or the full invoice HTML.

8. cURL, Python, and Node.js request equivalents

These examples show the same documented endpoint, header, and HTML source shape for debugging or for services outside Laravel. Replace the sample markup with server-rendered invoice HTML and keep the key in a server-side secret store.

cURL

curl -X POST 'https://api.pdfshift.io/v3/convert/pdf' \
  -H 'X-API-Key: YOUR_PDFSHIFT_API_KEY' \
  -H 'Content-Type: application/json' \
  -H 'Accept: application/pdf' \
  --data '{"source":"<html><body><h1>Invoice INV-1001</h1></body></html>"}' \
  --output invoice.pdf

Python

import os
import requests

api_key = os.environ["PDFSHIFT_API_KEY"]
html = "<html><body><h1>Invoice INV-1001</h1></body></html>"

response = requests.post(
    "https://api.pdfshift.io/v3/convert/pdf",
    headers={"X-API-Key": api_key, "Accept": "application/pdf"},
    json={"source": html},
    timeout=90,
)
response.raise_for_status()
with open("invoice.pdf", "wb") as pdf_file:
    pdf_file.write(response.content)

Node.js

const apiKey = process.env.PDFSHIFT_API_KEY;
if (!apiKey) throw new Error('PDFSHIFT_API_KEY is not configured');

const html = '<html><body><h1>Invoice INV-1001</h1></body></html>';
const response = await fetch('https://api.pdfshift.io/v3/convert/pdf', {
  method: 'POST',
  headers: {
    'X-API-Key': apiKey,
    'Content-Type': 'application/json',
    'Accept': 'application/pdf',
  },
  body: JSON.stringify({ source: html }),
  signal: AbortSignal.timeout(90000),
});
if (!response.ok) {
  throw new Error(`PDFShift returned HTTP ${response.status}`);
}
const bytes = new Uint8Array(await response.arrayBuffer());
await import('node:fs/promises').then(fs => fs.writeFile('invoice.pdf', bytes));

9. Or skip the browser setup

For screenshots of invoice previews or other web pages, ScreenshotNeo is a website screenshot API and MCP server for developers. It is separate from PDFShift’s HTML-to-PDF workflow. One GET request returns a PNG, JPEG, WebP, or PDF; the code below uses the documented API endpoint. See the ScreenshotNeo API documentation for available 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)
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 banners are accepted and removed before the shot; more than 60 known consent platforms, newsletter popups, and chat widgets can be removed, and each cleanup step can be turned off.
  • Bot checks, blank pages, timeouts, failed loads, and cache hits are not billed; response headers report the page verdict and billing status.
  • An MCP server provides take_screenshot, get_page_info, and capture_pdf for AI agents and MCP clients.
  • The free plan includes 1,000 screenshots a month with no card; paid plans start at $5 for 3,000 screenshots. Every feature is available on every plan.

Sign up for ScreenshotNeo’s free plan to get 1,000 screenshots a month with no card.

10. Troubleshooting

Symptom Likely cause What to check
Unauthorized or rejected authentication Missing, incorrect, or stale API key; using an outdated authentication pattern. Send the key in X-API-Key, confirm the server environment value, and check PDFShift’s current authentication help.
Laravel reports the key is missing Environment/config cache does not contain the deployed value. Check the deployment secret and refresh Laravel’s configuration cache using your normal release process.
Non-success HTTP response Authentication, request shape, source content, or a service-side error. Record the HTTP status and a safe request correlation ID if available; inspect the current PDFShift response details without logging the secret or invoice contents.
Empty or non-PDF response where PDF bytes were expected The selected response mode or API behavior differs from the assumption. Confirm whether filename was included. Without it, the documented mode returns the PDF; with it, expect a JSON URL response.
Temporary URL no longer works The documented availability window elapsed. Download and store the PDF before the two-day window ends; regenerate if necessary.
Conversion times out in a web request Conversion took longer than the application request budget. Move the work to a queue, set compatible worker and HTTP timeouts, and provide a pending state. The retrieved sources do not specify PDFShift timeout or retry guarantees.
Images or styles are missing HTML references assets that are inaccessible to the conversion service or not included in the rendered source. Check how assets are referenced and whether the source is reachable. Consult current PDFShift documentation for supported resource and rendering options.
PDF exists but is not retained The file was only held at a temporary service URL or the app never stored the response bytes. Write the bytes to application-controlled storage and persist the resulting path after a successful write.

11. Performance, reliability, and cost

  • Latency: A remote conversion adds a network request. Queue invoice generation when it should not hold up a customer-facing request. No verified conversion-time benchmark is available in the research dossier.
  • Reliability: Treat conversion as a fallible external dependency. Check status, handle empty responses, persist failures for retry, and avoid duplicate work with an idempotent job design. Exact retry behavior and service guarantees were not established by the retrieved sources.
  • Retention: A filename response URL is temporary, documented for two days. Application storage is the durable copy in this workflow.
  • Cost: PDFShift’s FAQ states that an account receives 50 credits per month at no charge and without requiring a credit card. This is a vendor-stated allowance; re-check current account terms before relying on it. Current paid pricing was not verified in the research.

12. Frequently asked questions

Can Laravel send a Blade view directly to PDFShift?

Render the view to an HTML string with Laravel’s view renderer, then use that string as the documented JSON source value in a server-side request.

Can I make the generated file permanent with PDFShift’s returned URL?

The documented URL is temporary for two days. Download the PDF and store it on a disk your application controls for durable access.

Is PDFShift required to generate invoice PDFs in Laravel?

No. The fit depends on where invoice content lives and how it is generated. Applications using Stripe billing should also consider the invoice PDF functionality documented by Laravel Cashier.

Does this example establish tax or invoice compliance?

No. It demonstrates rendering and delivery only. Determine required invoice fields and retention rules for your business and jurisdiction independently.