# Dynamic invoice PDFs from JSON in PHP

> Design the invoice once, then generate every copy from your billing data. PHP examples for tables from JSON, QR payment codes, and hosted PDF links.

Canonical: https://bannerify.co/blog/dynamic-invoice-pdf-php
Last updated: 2026-10-08

By Duc An, 2026-10-08.

Invoices are the one document every product eventually has to generate, and they are the least fun to build. Dompdf and TCPDF make you express a layout in HTML and CSS that half-works, FPDF means positioning text with coordinates, and a headless Chrome worker is a lot of infrastructure for a page with a table on it.

The layout is not the hard part. Invoices look the same every time. What changes is the data: who it is for, which line items, which totals, which tax rate.

So the approach here is: design the invoice once, keep it as a template, and send the billing data as JSON to a [PDF generation API](https://bannerify.co/product/pdf-generation-api). The API returns the finished PDF.

## What you need

- A template with your logo, address block, a line-items table, a totals block, and a footer. Build it in the [editor](https://bannerify.co/product/modern-editor) and give each layer a name, because names are how you target them.
- An API key from **Dashboard → API Keys**, stored server-side.
- The template ID, which looks like `tpl_kd93mz`.

## The call

The endpoint is `createPdf`. It takes your data and returns PDF bytes.

```php
<?php

$payload = [
    'apiKey' => getenv('BANNERIFY_API_KEY'),
    'templateId' => 'tpl_invoice',
    'modifications' => [
        ['name' => 'invoice-number', 'text' => 'INV-2026-0042'],
        ['name' => 'invoice-date', 'text' => '8 October 2026'],
        ['name' => 'customer-name', 'text' => 'Acme Studio'],
        ['name' => 'customer-address', 'text' => "12 Rue de Rivoli\n75001 Paris"],
        [
            'name' => 'line-items',
            'columns' => [
                ['key' => 'item', 'header' => 'Item'],
                ['key' => 'qty', 'header' => 'Qty', 'align' => 'right'],
                ['key' => 'amount', 'header' => 'Amount', 'align' => 'right'],
            ],
            'rows' => [
                ['item' => 'Automate plan, October', 'qty' => 1, 'amount' => '$29.00'],
                ['item' => 'Extra renders', 'qty' => 5, 'amount' => '$15.00'],
            ],
        ],
        ['name' => 'subtotal', 'text' => '$44.00'],
        ['name' => 'tax', 'text' => '$8.80'],
        ['name' => 'total', 'text' => '$52.80'],
        ['name' => 'pay-qr', 'qrcode' => 'https://pay.example.com/inv/2026-0042'],
    ],
];

$ch = curl_init('https://api.bannerify.co/v1/templates/createPdf');

curl_setopt_array($ch, [
    CURLOPT_POST => true,
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER => ['Content-Type: application/json'],
    CURLOPT_POSTFIELDS => json_encode($payload),
    CURLOPT_TIMEOUT => 30,
]);

$pdf = curl_exec($ch);
$status = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);

if ($pdf === false || $status !== 200) {
    throw new RuntimeException("Invoice render failed with status {$status}");
}

file_put_contents("/invoices/INV-2026-0042.pdf", $pdf);
```

The table layer is the interesting part. You send `columns` and `rows`, and the styling you set in the editor (stripes, row height, alignment, header style) is applied to whatever data arrives. Add a new line item and you do not touch the layout. Table layers work in images too, for price lists and reports: see the [table generation API](https://bannerify.co/product/table-generation-api).

## Where the data comes from

The shape above is deliberately boring, because that is the point: it maps directly onto rows from your database.

```php
$lineItems = [];

foreach ($order['items'] as $item) {
    $lineItems[] = [
        'item' => $item['description'],
        'qty' => $item['quantity'],
        'amount' => '$' . number_format($item['subtotal'], 2),
    ];
}

$modifications[] = ['name' => 'line-items', 'rows' => $lineItems];
```

Format money and dates on your side before sending them. The template does not guess currency or locale, and you probably already have that logic in your billing code.

## Stream it or store it

Two ways to hand the file over, depending on whether you want to keep it:

**Stream it to the browser.** The response is the PDF itself, so you can pass it straight through with the right headers:

```php
header('Content-Type: application/pdf');
header('Content-Disposition: inline; filename="INV-2026-0042.pdf"');
echo $pdf;
```

**Store it and get a URL.** `createStoredPdf` uploads the render and returns a link you can put in an email:

```json
{ "url": "https://cdn.bannerify.co/invoices/..." }
```

That is the path to use when the invoice goes out by email and you do not want to manage file storage, expiring links, or S3 permissions yourself.

## A few things to get right

- **Layer names are exact.** `line-items` and `line_items` are different layers, and a mismatch means that part of the invoice keeps its template value.
- **Test with your longest real invoice.** Every billing system eventually produces a customer with forty line items, so check that document before it finds you.
- **Make generation idempotent.** Use your invoice number as the file name and skip the work if the file already exists. Re-rendering the same invoice should be a no-op, not a second charge against your quota.
- **Keep the API key server-side.** Invoice generation happens in your billing code, never in the browser.

## Why not a PDF library

You can absolutely build this with a library, and plenty of teams do. The tradeoff is where the layout lives. With a library it lives in PHP, so a designer cannot touch it and every branding change is a deploy plus a test pass. With a template, the layout lives in the editor and PHP only moves data around.

It is also faster to iterate: open the template, move the total block two centimeters up, save. That is a two-minute change instead of a pull request.

## Next

The free plan includes 100 generations a month, one template, and 1GB of stored images or files, which is enough to ship this and test it with real invoices. Automate starts at $29 a month for 3,000 generations. Details on [pricing](https://bannerify.co/pricing).

If you want to see the endpoint parameters, they are in the [create PDF reference](https://bannerify.co/docs/api-reference/endpoint/create-pdf). For the general shape of the modifications object, including images, QR codes, barcodes, and chart rows, see [elements](https://bannerify.co/docs/essentials/elements). The same pattern works for receipts, shipping labels, and [certificates](https://bannerify.co/blog/certificate-of-completion), which is what the [PDF generation API](https://bannerify.co/product/pdf-generation-api) is built for.

## Related

- [Open Graph tags: og:type, og:title, og:url, and the rest](https://bannerify.co/blog/open-graph-tags)
- [What is an AVIF file? How to open and convert it](https://bannerify.co/blog/avif-file)
- [Canva Bulk Create: how to use it, limits, and fixes](https://bannerify.co/blog/canva-bulk-create)
- [All posts](https://bannerify.co/blog)
