Bannerify
All posts

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.

· Duc An

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. 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 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

$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' => 'Pro 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.

Where the data comes from

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

$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:

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:

{ "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. PRO is $29 a month for 10,000 generations. Details on pricing.

If you want to see the endpoint parameters, they are in the create PDF reference. For the general shape of the modifications object, including images, QR codes, barcodes, and chart rows, see elements. The same pattern works for receipts, shipping labels, and certificates, which is what the PDF generation API is built for.

Your first 100 images are on us.

Make a template, grab an API key, and render your first image in a few minutes.