Skip to content
PPDFInvoiceAPI
All posts
invoiceshtml-to-pdftutorialapi

How to generate a PDF invoice from HTML — one API call

Turn an HTML invoice into a pixel-perfect PDF with a single POST request. Real curl and Node.js examples using the PDFInvoiceAPI /v1/render endpoint.

The PDFInvoiceAPI team3 min read

Generating a PDF invoice should not require a headless-Chrome cluster, a print stylesheet you maintain forever, or a weekend wrestling with a PDF library’s box model. If you can write HTML, you already have the layout engine — the browser. PDFInvoiceAPI gives you that engine behind one HTTP endpoint: POST your HTML, get application/pdf back.

This post walks through the core use case end to end: an HTML invoice in, a branded PDF out, in a single API call.

The one endpoint

Everything happens at POST https://api.pdfinvoiceapi.com/v1/render. You authenticate with a Bearer API key, send JSON, and the response body is the PDF — raw bytes, content-type: application/pdf. There’s no job to poll and no file URL to fetch; you stream the result straight to a file, a browser download, or object storage.

You’ll need an API key first. Sign up free (25 render credits, no card), then create a key in Dashboard → API keys — it’s shown once, so copy it somewhere safe.

The simplest possible render: raw HTML

Send your invoice markup in the html field. That’s the whole request:

curl https://api.pdfinvoiceapi.com/v1/render \
  -H "Authorization: Bearer sk_live_..." \
  -H "Content-Type: application/json" \
  -d '{
    "html": "<h1>Invoice #1024</h1><p>Bill to: Globex LLC</p><p>Total: €1,240.00</p>",
    "pdf": { "format": "A4", "printBackground": true }
  }' \
  -o invoice.pdf

You now have invoice.pdf on disk. The pdf object controls print options — format ("A4", "Letter", …), landscape, printBackground, margin, and scale. It defaults to A4 with backgrounds on, which is what you want for an invoice with a coloured header.

The same thing in Node.js

Because the response is binary, write the arrayBuffer() straight to a file:

import { writeFile } from "node:fs/promises";

const invoiceHtml = `
  <style>
    body { font-family: system-ui, sans-serif; color: #1a1a23; }
    .total { font-size: 1.4rem; font-weight: 700; }
  </style>
  <h1>Invoice #1024</h1>
  <p>Bill to: Globex LLC</p>
  <p class="total">Total: €1,240.00</p>
`;

const res = await fetch("https://api.pdfinvoiceapi.com/v1/render", {
  method: "POST",
  headers: {
    Authorization: "Bearer sk_live_...",
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    html: invoiceHtml,
    pdf: { format: "A4", printBackground: true },
  }),
});

if (!res.ok) throw new Error(await res.text());
await writeFile("invoice.pdf", Buffer.from(await res.arrayBuffer()));

Style it with ordinary CSS — @page rules, web fonts, flexbox, a coloured header bar. If it renders in Chrome, it renders in your PDF.

Don’t concatenate strings: use a template

Building HTML by string concatenation is how you ship an invoice with an unescaped customer name. Instead, send a template plus a data object and let the API merge them. Placeholders use {{ }} (HTML-escaped, the safe default), {{{ }}} (raw, for HTML you trust), and dotted paths for nested values:

curl https://api.pdfinvoiceapi.com/v1/render \
  -H "Authorization: Bearer sk_live_..." \
  -H "Content-Type: application/json" \
  -d '{
    "template": "<h1>Invoice {{number}}</h1><p>Bill to: {{billTo.name}}</p><p>Total: {{total}}</p>",
    "data": {
      "number": "INV-1024",
      "billTo": { "name": "Globex LLC" },
      "total": "€1,240.00"
    },
    "pdf": { "format": "A4", "printBackground": true }
  }' \
  -o invoice.pdf

The merge engine is deliberately simple — {{ }}, {{{ }}}, and dotted paths, no loops or conditionals. For a line-item table, render the rows in your own code (you already have the data in a list) and pass the finished HTML to template, or build the whole document and send it as html. Keeping the engine loop-free keeps renders fast and predictable.

If you reuse the same layout for every invoice, save it once in the dashboard and render it by its stored id (tpl_…) instead of resending the markup each time — pass that id as template and only send data.

What you get back

A 200 with content-type: application/pdf and the PDF bytes as the body. Each successful render costs one credit. On a failure you get a JSON error body instead of a PDF, so check res.ok before writing the file.

That’s the whole loop: HTML in, PDF out, one call. From here:

Related posts