Tutorial

Invoice Walkthrough - Template, JSON Payload, and API Render Request

Build a realistic invoice flow in HookPDF: create template variables, prepare JSON payload, send render request, and read job status.

This is a practical, end-to-end example based on the real HookPDF flow:

  1. You create a template in HTML + CSS.
  2. You place Handlebars variables in the template.
  3. You send JSON payload with template_id to /render.
  4. You receive a queued job and track status.

No abstract theory in this post. We will build one invoice example from start to finish.

Step 1 - Create the invoice template

Below is a minimal invoice body with dynamic fields.

HTML
<div class="invoice-wrap">
  <header class="invoice-header">
    <h1>Invoice</h1>
    <p>Invoice No: {{invoice_number}}</p>
    <p>Issue Date: {{issue_date}}</p>
    <p>Due Date: {{due_date}}</p>
  </header>

  <section class="bill-to">
    <h2>Bill To</h2>
    <p>{{customer.name}}</p>
    <p>{{customer.email}}</p>
    <p>{{customer.address}}</p>
  </section>

  <table class="invoice-table">
    <thead>
      <tr>
        <th>Description</th>
        <th>Qty</th>
        <th>Unit Price</th>
        <th>Line Total</th>
      </tr>
    </thead>
    <tbody>
      {{#each items}}
      <tr>
        <td>{{description}}</td>
        <td>{{quantity}}</td>
        <td>{{unit_price}}</td>
        <td>{{line_total}}</td>
      </tr>
      {{/each}}
    </tbody>
  </table>

  <section class="totals">
    <p>Subtotal: {{subtotal}}</p>
    <p>Tax: {{tax}}</p>
    <p class="grand-total">Total: {{total}}</p>
  </section>
</div>

Step 2 - Add print-safe CSS

CSS
@page {
  size: A4;
  margin: 20mm;
}

body {
  font-family: Arial, sans-serif;
  font-size: 12px;
  color: #111827;
}

.invoice-table {
  width: 100%;
  border-collapse: collapse;
  margin-top: 16px;
}

.invoice-table th,
.invoice-table td {
  border-bottom: 1px solid #d1d5db;
  padding: 8px;
  text-align: left;
}

.grand-total {
  font-weight: 700;
  font-size: 14px;
}

Keep CSS simple and explicit. The goal is consistency across documents, not visual experimentation.

Step 3 - Build JSON payload that matches your variables

Every variable in the template should be satisfiable by one payload path.

JSON
{
  "invoice_number": "INV-2026-0042",
  "issue_date": "2026-03-19",
  "due_date": "2026-04-02",
  "customer": {
    "name": "Northwind Labs",
    "email": "billing@northwind.example",
    "address": "42 Market Street, San Francisco"
  },
  "items": [
    {
      "description": "Pro Plan - March",
      "quantity": 1,
      "unit_price": "$99.00",
      "line_total": "$99.00"
    },
    {
      "description": "Onboarding support",
      "quantity": 2,
      "unit_price": "$75.00",
      "line_total": "$150.00"
    }
  ],
  "subtotal": "$249.00",
  "tax": "$24.90",
  "total": "$273.90"
}

Step 4 - Send render request

Now call the API with your saved template id.

BASH
curl -X POST "https://api.hookpdf.com/v1/render" \
  -H "Authorization: Bearer <api_key>" \
  -H "Content-Type: application/json" \
  -d '{
    "template_id": "1c24f959-8e1a-4a14-acc4-cf57b4e60e99",
    "payload": {
      "invoice_number": "INV-2026-0042",
      "issue_date": "2026-03-19",
      "due_date": "2026-04-02",
      "customer": {
        "name": "Northwind Labs",
        "email": "billing@northwind.example",
        "address": "42 Market Street, San Francisco"
      },
      "items": [
        {
          "description": "Pro Plan - March",
          "quantity": 1,
          "unit_price": "$99.00",
          "line_total": "$99.00"
        }
      ],
      "subtotal": "$99.00",
      "tax": "$9.90",
      "total": "$108.90"
    }
  }'

Typical response:

JSON
{
  "job_id": "7f2a6c3f-4e83-4da4-aac8-5c0b7d7adf5b",
  "status": "queued",
  "is_preview": false
}

Step 5 - Check job status

Use job_id to check render status.

BASH
curl -X GET "https://api.hookpdf.com/v1/reports/7f2a6c3f-4e83-4da4-aac8-5c0b7d7adf5b" \
  -H "Authorization: Bearer <api_key>"

When status becomes completed, the response includes output_url.

Common mistakes to avoid

  1. Variable mismatch: template expects {{customer.name}} but payload only has name.
  2. Array mismatch: template loops over items but payload sends lines.
  3. Formatting mismatch: payload sends raw number but template expects formatted currency text.
  4. Huge template edits without test payload: always validate with one known-good JSON.

Use this template for free now

Start with this template in HookPDF and render your first PDF for free.

Use this template free

Continue with stronger variable patterns

If your team has frequent mapping mistakes, read this next:

Ready to move from prototype to production?

Create your account and generate your first API-driven PDF with HookPDF.

Get your API key