Get started

AI invoice generator: build one with Claude and PDF4.dev in 10 minutes

Generate professional PDF invoices with AI using Claude and PDF4.dev. Full tutorial with Node.js code, Handlebars templates, and MCP integration examples.

benoitdedMarch 23, 202611 min read

AI invoice generators have gone from novelty to production pattern in 18 months. The combination of an LLM (for understanding and structuring unstructured input) and a PDF rendering API (for pixel-perfect output) produces invoices in under 5 seconds from a plain-text prompt. This guide builds one end-to-end.

What is an AI invoice generator?

An AI invoice generator is a pipeline that combines three components: a large language model that extracts and structures invoice data from free-text or semi-structured input, an HTML template that defines the visual layout, and a PDF rendering API that merges the two and returns a binary PDF. Each component does what it does best: the LLM handles language, the template handles design, and the API handles rendering.

The alternative, form-based invoice generators, require the user to fill every field manually. An AI generator accepts natural language like "invoice Acme Corp $2400 for 4 days of API consulting, net 30" and populates all fields automatically.

Architecture overview

ComponentRoleExample
LLM APIParse input, return structured JSONClaude 3.5 Haiku, GPT-4o-mini
HTML templateVisual layout with variablesHandlebars, stored in PDF4.dev
PDF rendering APIMerge template + data, return PDFPDF4.dev /api/v1/render
DeliveryStore or send the PDFS3/R2, Resend, file download

This separation of concerns is the core design principle. Changing the template doesn't require touching the LLM prompt. Adding a new data field means updating the JSON schema and the template, not the rendering layer.

Step 1: design the invoice HTML template

A good invoice template is standard HTML and CSS. PDF4.dev renders it through Chromium, so any CSS that works in Chrome works here. Handlebars syntax ({{variable}}) marks dynamic fields.

<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8" />
  <style>
    * { box-sizing: border-box; margin: 0; padding: 0; }
    body {
      font-family: 'Inter', sans-serif;
      font-size: 14px;
      color: #111;
      padding: 48px;
    }
    .header { display: flex; justify-content: space-between; margin-bottom: 40px; }
    .company-name { font-size: 24px; font-weight: 700; }
    .invoice-meta { text-align: right; }
    .invoice-number { font-size: 20px; font-weight: 600; color: #2563eb; }
    table { width: 100%; border-collapse: collapse; margin-top: 32px; }
    th { background: #f1f5f9; text-align: left; padding: 10px 12px; font-size: 12px; text-transform: uppercase; letter-spacing: 0.05em; }
    td { padding: 10px 12px; border-bottom: 1px solid #e2e8f0; }
    .total-row td { font-weight: 700; font-size: 16px; border-bottom: none; padding-top: 16px; }
    .due-date { color: #dc2626; font-weight: 600; }
  </style>
</head>
<body>
  <div class="header">
    <div>
      <div class="company-name">{{from.name}}</div>
      <div>{{from.address}}</div>
      <div>{{from.email}}</div>
    </div>
    <div class="invoice-meta">
      <div class="invoice-number">Invoice {{invoiceNumber}}</div>
      <div>Issued: {{issuedAt}}</div>
      <div class="due-date">Due: {{dueDate}}</div>
    </div>
  </div>
 
  <div>
    <strong>Bill to:</strong>
    <div>{{to.name}}</div>
    <div>{{to.email}}</div>
  </div>
 
  <table>
    <thead>
      <tr>
        <th>Description</th>
        <th>Qty</th>
        <th>Unit price</th>
        <th>Total</th>
      </tr>
    </thead>
    <tbody>
      {{#each items}}
      <tr>
        <td>{{description}}</td>
        <td>{{quantity}}</td>
        <td>${{unitPrice}}</td>
        <td>${{total}}</td>
      </tr>
      {{/each}}
    </tbody>
    <tfoot>
      <tr class="total-row">
        <td colspan="3">Total due</td>
        <td>${{totalAmount}}</td>
      </tr>
    </tfoot>
  </table>
 
  {{#if notes}}
  <p style="margin-top: 32px; color: #64748b; font-size: 12px;">{{notes}}</p>
  {{/if}}
</body>
</html>

Save this template in PDF4.dev's template manager. Note the variable names: from.name, to.name, items[].description, totalAmount. These must match exactly what the LLM returns.

Step 2: define the JSON schema for the LLM

The JSON schema drives Claude's structured output. Every field in the schema maps to a Handlebars variable in the template.

const INVOICE_SCHEMA = {
  type: "object",
  properties: {
    invoiceNumber: { type: "string", description: "Invoice number, e.g. INV-2026-001" },
    issuedAt: { type: "string", description: "Issue date in DD MMM YYYY format" },
    dueDate: { type: "string", description: "Due date in DD MMM YYYY format" },
    from: {
      type: "object",
      properties: {
        name: { type: "string" },
        address: { type: "string" },
        email: { type: "string" }
      },
      required: ["name", "email"]
    },
    to: {
      type: "object",
      properties: {
        name: { type: "string" },
        email: { type: "string" }
      },
      required: ["name", "email"]
    },
    items: {
      type: "array",
      items: {
        type: "object",
        properties: {
          description: { type: "string" },
          quantity: { type: "number" },
          unitPrice: { type: "number" },
          total: { type: "number" }
        },
        required: ["description", "quantity", "unitPrice", "total"]
      }
    },
    totalAmount: { type: "number" },
    notes: { type: "string" }
  },
  required: ["invoiceNumber", "issuedAt", "dueDate", "from", "to", "items", "totalAmount"]
};

Step 3: extract invoice data with Claude

Send the user's input to Claude with the schema. Claude's tool_use mode guarantees the response matches the expected structure.

import Anthropic from "@anthropic-ai/sdk";
 
const client = new Anthropic();
 
async function extractInvoiceData(userInput: string): Promise<InvoiceData> {
  const response = await client.messages.create({
    model: "claude-haiku-4-5",
    max_tokens: 1024,
    tools: [
      {
        name: "create_invoice",
        description: "Extract invoice data from the user's input and return structured JSON",
        input_schema: INVOICE_SCHEMA,
      },
    ],
    tool_choice: { type: "tool", name: "create_invoice" },
    messages: [
      {
        role: "user",
        content: `Today is ${new Date().toLocaleDateString("en-GB", { day: "2-digit", month: "short", year: "numeric" })}. My company is Acme Dev, [email protected], 12 Rue de la Paix, Paris.
 
Extract the invoice data from this input: ${userInput}`,
      },
    ],
  });
 
  const toolUse = response.content.find((block) => block.type === "tool_use");
  if (!toolUse || toolUse.type !== "tool_use") {
    throw new Error("Claude did not return structured invoice data");
  }
 
  return toolUse.input as InvoiceData;
}

Claude Haiku processes a typical invoice prompt in under 1 second and costs roughly $0.0003 per call. For high-volume use, that's less than $0.30 per 1000 invoices.

Step 4: render the invoice with PDF4.dev

Pass the structured JSON to PDF4.dev's API with your stored template ID.

async function renderInvoicePDF(data: InvoiceData): Promise<Buffer> {
  const response = await fetch("https://pdf4.dev/api/v1/render", {
    method: "POST",
    headers: {
      Authorization: `Bearer ${process.env.PDF4DEV_API_KEY}`,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      template_id: process.env.PDF4DEV_TEMPLATE_ID,
      data,
      options: {
        format: "A4",
        margin: { top: "0", right: "0", bottom: "0", left: "0" },
      },
    }),
  });
 
  if (!response.ok) {
    throw new Error(`PDF rendering failed: ${response.statusText}`);
  }
 
  const buffer = await response.arrayBuffer();
  return Buffer.from(buffer);
}

PDF4.dev renders through a warm Chromium pool, so the typical render time for a one-page invoice is 200-400ms. Cold starts are avoided by the pool, which keeps browser instances alive between requests.

Putting it together: a complete Express endpoint

import express from "express";
import { extractInvoiceData } from "./claude";
import { renderInvoicePDF } from "./pdf4dev";
 
const app = express();
app.use(express.json());
 
app.post("/api/generate-invoice", async (req, res) => {
  const { prompt } = req.body;
 
  if (!prompt || typeof prompt !== "string") {
    return res.status(400).json({ error: "prompt is required" });
  }
 
  try {
    // 1. Extract structured data with Claude (~500ms)
    const invoiceData = await extractInvoiceData(prompt);
 
    // 2. Render PDF with PDF4.dev (~300ms)
    const pdfBuffer = await renderInvoicePDF(invoiceData);
 
    // 3. Return as download
    res.setHeader("Content-Type", "application/pdf");
    res.setHeader(
      "Content-Disposition",
      `attachment; filename="invoice-${invoiceData.invoiceNumber}.pdf"`
    );
    res.send(pdfBuffer);
  } catch (err) {
    console.error(err);
    res.status(500).json({ error: "Invoice generation failed" });
  }
});
 
app.listen(3000, () => console.log("Server running on port 3000"));

Test it with a plain-text prompt:

curl -X POST http://localhost:3000/api/generate-invoice \
  -H "Content-Type: application/json" \
  -d '{"prompt": "Invoice Brandlink Agency ([email protected]) for 3 days of backend consulting at $1800/day, payment due in 30 days"}' \
  --output invoice.pdf

Claude parses the client name, email, service description, quantity, unit price, and calculates the total. The resulting PDF downloads immediately.

Automating invoices from Stripe webhooks

The same pattern works for automated post-payment invoices. When a Stripe checkout.session.completed event fires, pass the session data to the LLM to format it, then render and email the PDF.

app.post("/webhooks/stripe", express.raw({ type: "application/json" }), async (req, res) => {
  const event = stripe.webhooks.constructEvent(
    req.body,
    req.headers["stripe-signature"]!,
    process.env.STRIPE_WEBHOOK_SECRET!
  );
 
  if (event.type === "checkout.session.completed") {
    const session = event.data.object;
 
    // Format Stripe data into invoice prompt
    const prompt = `
      Customer: ${session.customer_details?.name} (${session.customer_details?.email})
      Amount paid: $${(session.amount_total! / 100).toFixed(2)}
      Product: ${session.metadata?.product_name ?? "Software license"}
      Payment date: ${new Date(session.created * 1000).toLocaleDateString()}
    `;
 
    const invoiceData = await extractInvoiceData(prompt);
    const pdfBuffer = await renderInvoicePDF(invoiceData);
 
    // Email via Resend
    await resend.emails.send({
      from: "[email protected]",
      to: session.customer_details!.email!,
      subject: `Invoice ${invoiceData.invoiceNumber}`,
      html: "<p>Thank you for your purchase. Your invoice is attached.</p>",
      attachments: [{ filename: "invoice.pdf", content: pdfBuffer }],
    });
  }
 
  res.json({ received: true });
});

This pattern eliminates manual invoice creation after every sale. The LLM handles the formatting (net 30 language, date formatting, description writing), while PDF4.dev handles the rendering.

Using Claude Desktop with the PDF4.dev MCP server

For non-developer workflows, the PDF4.dev MCP server provides a zero-code path to invoice generation. Once connected to Claude Desktop, you can type invoice instructions in chat and receive a PDF file directly.

Setup takes under 5 minutes:

  1. Install the PDF4.dev MCP server from pdf4.dev/mcp
  2. Add your API key to the MCP config
  3. Connect to Claude Desktop in Settings > Developer > MCP servers

Then from Claude Desktop:

Generate an invoice for Virtuel Studio ([email protected]) for UX design work, 
8 hours at €150/hour, issued today, due April 15. Include a 10% discount.
Save it as virtuel-invoice.pdf.

Claude calls the PDF4.dev MCP tool, renders the invoice using your account's default template, and downloads the file to your machine. No server, no code.

Comparison: manual vs AI invoice generation

ApproachTime per invoiceError rateScales to 1000/monthSetup
Manual (form fill)3-5 minHuman typosNoNone
Template only (fixed form)30 secLowYesMedium
AI + PDF API (this guide)< 5 secVery lowYesMedium
MCP (Claude Desktop)10 secVery lowNoLow

The AI approach is best when the input source is unstructured: emails, chat messages, free-text descriptions. For structured data (Stripe, database), you can skip the LLM entirely and pass data directly to the PDF API.

Error handling and production considerations

A few things to harden before going to production:

LLM validation: always validate the LLM output against your schema before sending it to the PDF API. Claude is reliable with tool use, but malformed JSON (missing required fields, wrong types) will cause template rendering errors. Use Zod or a JSON Schema validator.

import { z } from "zod";
 
const InvoiceDataSchema = z.object({
  invoiceNumber: z.string(),
  issuedAt: z.string(),
  dueDate: z.string(),
  from: z.object({ name: z.string(), email: z.string() }),
  to: z.object({ name: z.string(), email: z.string() }),
  items: z.array(z.object({
    description: z.string(),
    quantity: z.number(),
    unitPrice: z.number(),
    total: z.number(),
  })),
  totalAmount: z.number(),
  notes: z.string().optional(),
});
 
const validated = InvoiceDataSchema.parse(rawLLMOutput);

PDF API timeouts: set a 30-second timeout on your PDF4.dev requests and retry once on timeout. Slow renders are rare but happen under load.

Data privacy: for invoices with customer PII, use Claude via Anthropic's API (data is not used for training) and avoid logging full invoice data to persistent storage unless required for audit.

Invoice numbering: generate invoice numbers server-side (e.g. with a database sequence), not from the LLM. LLMs will invent plausible-looking numbers but cannot guarantee uniqueness or sequential ordering.

What the HTML to PDF tool covers for one-off conversions

For ad-hoc conversions where you already have an HTML invoice and just need the PDF, the HTML to PDF tool at PDF4.dev handles it without any API setup. Paste or upload the HTML, download the PDF. No account required for small documents.

The API approach in this guide is for automated, recurring generation at scale.

Next steps

The full example code from this article is about 80 lines of TypeScript. Adding proper error handling, Zod validation, and email delivery brings it to roughly 150 lines — a complete invoicing backend from scratch.

Free tools mentioned:

Html To PdfTry it freeMerge PdfTry it free

Start generating PDFs

Build PDF templates with a visual editor. Render them via API from any language in ~300ms.