---
title: "Invoice PDF API: Generate Invoice and Receipt PDFs from HTML"
description: "Invoice PDF API guide: generate invoice and receipt PDFs from HTML or a JSON template with one request. Real A4 output, selectable text, embedded fonts."
url: "https://html2img.com/articles/invoice-pdf-api/"
section: "Tutorials"
published: "2026-09-16T12:45:21.949Z"
updated: "2026-09-16T12:47:34.551Z"
---

# Invoice PDF API: Generate Invoice and Receipt PDFs from HTML

By Mike Griffiths. https://html2img.com/articles/invoice-pdf-api/

![Invoice PDF API: Generate Invoice and Receipt PDFs from HTML](https://a.storyblok.com/f/320619/1200x630/acef79229e/og.png)

Your billing flow already knows everything an invoice needs: the line items, the totals, who owes what and when. The part that keeps going wrong is turning that data into a file a customer can open, print and forward to their accounts team. A PNG in an email looks fine but cannot be copied from or searched. A PDF from a library like dompdf loses your fonts and mangles your layout. Running headless Chrome yourself works until the box runs out of memory at month end, when every invoice in the system is generated at once.

The HTML to Image API treats a PDF as an output format, not a separate product. Any request that would return a PNG returns a real A4 document instead when you add `"format": "pdf"`, and that includes the ready-made invoice and receipt templates. This article walks through both routes, shows what actually comes back, and covers the two or three things you need to get right before wiring it into a billing system.

## Two routes to an invoice PDF

There are two ways in, and which one you pick depends on whether you already have invoice markup.

If you do not, the [invoice image template](https://html2img.com/templates/invoice-image) and the [receipt image template](https://html2img.com/templates/receipt-image) take JSON and return a finished document. You never write HTML. The design is versioned on the API side, the output is tested, and the integration is one POST.

If you do, or you need a layout the templates do not cover, you send your own HTML and CSS to `/api/html` with the same `format` parameter and get the same kind of PDF back. This is the route the [HTML to PDF API](https://html2img.com/html-to-pdf) page describes, and it is covered in the second half of this article.

Both routes produce the same class of output: a vector PDF laid out on A4 portrait pages, with selectable text, embedded webfonts, and automatic pagination. The [format parameter reference](https://html2img.com/docs/parameters/format) has the full behaviour, but the short version is that a PDF costs the same single credit as an image and comes back as a CDN URL in the same response envelope.

## Route 1: the invoice template with one extra key

Here is the complete request for a four-line invoice. Every field is a string, including the money, because the template renders what you send and does not do arithmetic or currency formatting for you. That is deliberate: your billing system already knows how to format `$10,344.00` for the customer's locale, and the API should not second-guess it.

```
curl -X POST https://app.html2img.com/api/v1/templates/invoice-image \
  -H "X-API-Key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "invoice_number": "INV-2026-0912",
    "issue_date": "16 Sep 2026",
    "due_date": "16 Oct 2026",
    "business_name": "Fieldgate Studio Ltd",
    "business_address": "4 Watergate Row\nChester CH1 2LE\nUnited Kingdom",
    "business_email": "billing@fieldgate.studio",
    "client_name": "Northgate Coffee Ltd",
    "client_address": "18 Bold Street\nLiverpool L1 4DS\nUnited Kingdom",
    "client_email": "accounts@northgatecoffee.co.uk",
    "items": [
      {"description": "Online ordering system, phase 2", "quantity": "1", "unit_price": "$6,400.00", "amount": "$6,400.00"},
      {"description": "Menu photography (half day)", "quantity": "2", "unit_price": "$550.00", "amount": "$1,100.00"},
      {"description": "Hosting and monitoring, Q4", "quantity": "3", "unit_price": "$120.00", "amount": "$360.00"},
      {"description": "Support retainer (hours)", "quantity": "8", "unit_price": "$95.00", "amount": "$760.00"}
    ],
    "subtotal": "$8,620.00",
    "tax_label": "VAT (20%)",
    "tax_amount": "$1,724.00",
    "total": "$10,344.00",
    "notes": "Payment due within 30 days by bank transfer. Please quote the invoice number as the payment reference.",
    "accent_color": "#2563EB",
    "format": "pdf"
  }'
```

The last key is the whole difference between an image and a document. Everything above it is the template's normal input set, documented on the [invoice template reference](https://html2img.com/docs/templates/invoice-image). Only `invoice_number`, `business_name`, `client_name`, `items` and `total` are required; the rest are optional, and the layout collapses cleanly when they are missing.

The response is the standard envelope, with the `url` now pointing at a `.pdf`:

```
{
  "success": true,
  "id": "968de823-85ed-4e79-904d-96168ab85240",
  "template": "invoice-image",
  "expires_at": null,
  "credits_remaining": 1234,
  "url": "https://i.html2img.com/image-1789562462319-296970.pdf"
}
```

This is what that exact payload produced. The image below is the PNG render of the same request without the `format` key, so you can see the layout; [the PDF itself is here](https://i.html2img.com/image-1789562462319-296970.pdf) if you want to open it and select the text.

![The rendered invoice: Fieldgate Studio Ltd billing Northgate Coffee Ltd, invoice INV-2026-0912, four line items, a VAT row and a total of $10,344.00, with a blue accent bar and a notes block at the foot](https://i.html2img.com/image-1789562458135-180027.png)

Running `pdfinfo` and `pdffonts` against the downloaded file confirms what kind of document it is. One A4 page, produced by Chromium's Skia backend, with the template's Open Sans weights embedded as subsetted TrueType fonts. `pdftotext` pulls out every line item, the VAT row and the total in order. Nothing in it is a screenshot in a wrapper.

## Receipts follow the same pattern

Receipts are the same idea with a shorter input set: an order number, a customer, a compact item list, and shipping and tax lines. In practice most teams render the receipt twice from the same data, a PNG for the confirmation email body and a PDF the customer can file for expenses. Here it is in Node, with the output format switched by a single argument:

```
const ENDPOINT = 'https://app.html2img.com/api/v1/templates/receipt-image';

async function renderReceipt(order, format = 'png') {
  const response = await fetch(ENDPOINT, {
    method: 'POST',
    headers: {
      'X-API-Key': process.env.HTML2IMG_KEY,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({
      business_name: 'Northgate Coffee',
      order_number: order.number,
      order_date: order.placedAt,
      customer_name: order.customer.name,
      customer_email: order.customer.email,
      items: order.lines.map((line) => ({
        name: line.name,
        qty: String(line.qty),
        amount: line.amountFormatted,
      })),
      subtotal: order.subtotalFormatted,
      shipping: order.shippingFormatted,
      tax_amount: order.taxFormatted,
      total: order.totalFormatted,
      thank_you_message: 'Thanks for your order. Keep this receipt for your records.',
      accent_color: '#2563EB',
      format,
    }),
  });

  if (!response.ok) {
    throw new Error(`Receipt render failed: ${response.status} ${await response.text()}`);
  }

  const { url } = await response.json();
  return url;
}

const pngUrl = await renderReceipt(order);          // for the email body
const pdfUrl = await renderReceipt(order, 'pdf');   // for the attachment
```

![The rendered receipt for order NG-20481 from Northgate Coffee: three items, subtotal, shipping and tax rows, a blue total of $26.40 and a thank-you line](https://i.html2img.com/image-1789562463980-633834.png)

Two renders means two credits, which is still cheaper than one browser instance idling on a server for the rest of the month. If you only want one, render the PDF and link to it from the email rather than embedding a PNG; the [receipt template reference](https://html2img.com/docs/templates/receipt-image) has the complete input list either way.

## Route 2: your own invoice HTML as a PDF

When the template does not match your brand, or you need multi-currency layouts, per-line tax rates, or a second page of terms, send your own markup to the HTML endpoint. The request shape is the one every [HTML to Image integration](https://html2img.com/integrations) already uses, plus the format key. In Laravel that is a Blade view and one `Http` call:

```
use Illuminate\Support\Facades\Http;

$html = view('invoices.pdf', ['invoice' => $invoice])->render();

$response = Http::withHeaders(['X-API-Key' => config('services.html2img.key')])
    ->timeout(60)
    ->post('https://app.html2img.com/api/html', [
        'html'   => $html,
        'format' => 'pdf',
    ])
    ->throw();

$pdfUrl = $response->json('url');
```

That is the whole integration. If you do not have invoice markup yet, the [HTML invoice example in the docs](https://html2img.com/docs/examples/invoice-receipt) is a sensible starting point: a header, a two-column parties block, an items table and a totals footer, in plain HTML and CSS. The interesting part is the stylesheet, because a PDF paginates and an image does not. Three rules matter.

First, the PDF is rendered with your **screen** CSS, not `@media print`. Whatever the invoice looks like in a browser tab is what the PDF looks like, page after page. Put every rule the document needs in your normal styles and do not rely on a print stylesheet being picked up.

Second, the page is A4 portrait and content reflows to its width. `width` and `height` in the request set the rendering viewport but do not change the page size. Design the invoice to flow like a document, with a fluid container rather than a fixed 1240px canvas. If you already have a fixed-width design and cannot change it, the `scale_to_fit` parameter lays the page out at your width and scales it down onto A4.

Third, page breaks are a CSS problem, and the rules that control them work. For an invoice the stylesheet you want is small:

```
tr            { break-inside: avoid; }   /* never split a line item across pages */
.totals,
.notes        { break-inside: avoid; }   /* keep the totals block together */
.terms        { break-before: page; }    /* start terms on a fresh page */
```

To check this behaves under load rather than on a tidy four-line example, I sent an invoice with 34 line items through the endpoint above. It came back as [two A4 pages](https://i.html2img.com/image-1789562522446-692802.pdf): the table breaks cleanly between rows, the subtotal, VAT and total block stays together at the top of page two, and the notes panel sits under it. Inter, loaded from Google Fonts, is embedded and every row is in the text layer. The one thing Chromium does not do is repeat `<thead>` on the second page, so if your invoices regularly run long, repeat the header row yourself or keep line items compact. The [page break guide](https://html2img.com/articles/html-to-pdf-page-breaks/) covers that pattern and the rest of the pagination rules in detail, and the [complete guide to generating PDFs from HTML](https://html2img.com/articles/generate-pdf-from-html/) compares this route against the libraries and self-hosted browsers it replaces.

If you are on Laravel and currently generating invoice images rather than PDFs, the [HTML invoice to image in Laravel](https://html2img.com/articles/html-invoice-to-image-laravel/) article covers the same Blade setup with a PNG output, and the [Laravel integration](https://html2img.com/integrations/laravel) page has the package that wraps all of this in a facade.

## Where the call sits in a billing system

The mistake to avoid is calling the API from the request that finalises the invoice and returning the CDN URL to the customer. Two reasons. A render takes a couple of seconds, which is fine in a queue and not fine in a controller. And the URL is a render artefact, not your system of record: on the free plan CDN files expire after seven days, and on any plan you want the invoice bytes in storage you control, keyed to your invoice ID, so they are still there in seven years when an auditor asks.

![Four-step flow: the invoice is finalised in your app, one POST with format pdf goes to the template endpoint, a vector PDF comes back as a CDN URL, and the file is copied into your own storage before it is attached to an email or shown in a customer portal](https://i.html2img.com/image-1789562585068-432257.png)

The pattern that holds up is a queued job triggered when the invoice is finalised (the point after which the numbers cannot change), which renders the PDF, copies the bytes into your own bucket, and only then sends the email. In Laravel:

```
class RenderInvoicePdf implements ShouldQueue
{
    public int $tries = 3;
    public int $backoff = 30;

    public function __construct(public Invoice $invoice) {}

    public function handle(): void
    {
        $response = Http::withHeaders(['X-API-Key' => config('services.html2img.key')])
            ->timeout(60)
            ->post('https://app.html2img.com/api/v1/templates/invoice-image', [
                ...$this->invoice->toTemplatePayload(),
                'format' => 'pdf',
            ])
            ->throw();

        $path = "invoices/{$this->invoice->year}/{$this->invoice->number}.pdf";

        Storage::disk('s3')->put($path, Http::get($response->json('url'))->body());

        $this->invoice->update(['pdf_path' => $path]);

        Mail::to($this->invoice->client_email)
            ->send(new InvoiceIssued($this->invoice));
    }
}
```

`toTemplatePayload()` is where your money formatting lives, so every string the template receives is already in the customer's currency and locale. The mailable then attaches from `$invoice->pdf_path` rather than from the CDN URL, and a re-send a year later attaches the identical file.

If you would rather not poll or block a worker on the render at all, pass a `webhook_url` in the request and the API calls back with the same response envelope when the PDF is ready. That suits high-volume month-end runs, where you fire a few thousand requests and let the webhooks drive the storage step.

## Prove it is a real PDF before you ship it

It takes thirty seconds to confirm you are getting a document rather than a picture, and it is worth doing once against your own template before the first customer sees one. Download the file and run:

```
pdffonts invoice.pdf     # every font should show emb = yes
pdftotext invoice.pdf -  # the line items and total should print in order
pdfinfo invoice.pdf      # Page size: 595.92 x 841.92 pts (A4)
```

If `pdffonts` lists nothing and `pdftotext` prints nothing, you have a raster PDF: a screenshot wrapped in a page. Accounts teams notice, because they cannot copy the invoice number into their ledger. Every PDF from this API passes all three checks, and the [best HTML to PDF API](https://html2img.com/articles/best-html-to-pdf-api/) comparison runs the same tests against the other services if you want the numbers side by side.

## The scope, stated plainly

There is no per-page pricing tier. A PDF is one credit whether it is one page or twenty, and it is the same credit a PNG costs. Pages are A4 portrait; there is no landscape or Letter option today. `dpi`, `fullpage` and `selector` are ignored in PDF mode because vector output has no resolution to raise and no region to crop. And the `@media print` rules in your stylesheet are not applied, so put everything the document needs in the screen styles. If those constraints fit, which for invoices and receipts they almost always do, the integration really is the one request shown at the top of this article.

---

Need invoices, receipts or certificates rendered as PDFs without running a browser yourself? [Browse the templates gallery](https://html2img.com/templates) or [read the docs](https://html2img.com/docs) to get started.
