Skip to content

Invoice

A multi-page invoice template, with logo and colour assets,

Download Full PDF

invoice-thumbs.png

Key Features

This template uses JSON data to populate the invoice, and <assets> to set the logo and brand colour. It uses conditions within attributes to override defaults if the user provides custom values.

Template Source

Copy and paste this source into the Papermill editor to preview the template.

Details
xml
<press>

  <document format="A4" page-margin-left="1.4cm" page-margin-right="1.4cm" page-margin-bottom="2cm">

    <page flow="invoice">

      <frame width="100%" direction="row">

        <frame width="50%">
          <frame height="5cm" width="3.2cm" padding="12pt" v-align="bottom" h-align="center" background-color="primary"
            text-align="center" font-color="white">
            <img max-width="1cm" src="{{ data.logo }}" space-after-desired="8pt"
              font-size="9pt" background-color="primary" />
            <frame width="2cm">
              {{ data.companyName }}
            </frame>
          </frame>
        </frame>

        <frame width="fill" padding-bottom="20pt" font="Roboto" padding-left="0.5cm">
          <frame font-size="42pt" font-weight="black" margin-top="1.9cm" font="Plus Jakarta Sans" tracking="0.02">
            INVOICE</frame>
          <frame font-size="13pt" padding-left="2pt">Invoice No: {{ data.info.invoiceNumber }}</frame>
        </frame>

      </frame>

      <frame width="100%" direction="row" padding-top="0.62cm" space-after-desired="0.8cm">

        <frame width="50%" padding-left="0.1cm">
          <frame font-size="10pt">Invoice To:</frame>
          <frame style="@text-big">{{ data.to.name }}</frame>
          <frame font-size="10pt" show-if="data.to.about">{{ data.to.about }}</frame>
          <hr width="4.8cm" />
          <frame direction="row" v-align="center" space-after-desired="2pt">
            <img width="8pt" height="8pt" src="https://examples.papermill.io/invoice/phone.png"
              space-after-desired="4pt" />
            <frame>{{ data.info.phone }}</frame>
          </frame>
          <frame direction="row" v-align="center" space-after-desired="2pt">
            <img width="8pt" height="8pt" src="https://examples.papermill.io/invoice/mail.png"
              space-after-desired="4pt" />
            <frame>{{ data.info.email }}</frame>
          </frame>
          <frame direction="row" v-align="center">
            <img width="8pt" height="8pt" src="https://examples.papermill.io/invoice/pin.png"
              space-after-desired="4pt" />
            <frame>{{ data.info.address }}</frame>
          </frame>
        </frame>

        <frame width="50%" padding-left="16pt">
          <frame font-size="10pt">Invoice From:</frame>
          <frame style="@text-big">{{ data.from.name }}</frame>
          <frame font-size="10pt" show-if="data.from.about">{{ data.from.about }}</frame>
          <hr width="5cm" />
          <labelled label-style="regular" gap="2pt" label="Invoice Date:">{{ data.info.invoiceDate }}</labelled>
          <labelled label-style="regular" gap="2pt" label="Issue Date:">{{ data.info.issueDate }}</labelled>
          <labelled label-style="regular" gap="2pt" label="Account No:">{{ data.payment.accountNumber }}</labelled>
        </frame>

      </frame>

      <frame height="fill">
        <flow name="invoice" />
      </frame>
      <footer />

    </page>

    <page repeat="true" flow="invoice" page-margin-top="2cm">
      <frame height="fill">
        <flow name="invoice" />
      </frame>
      <footer />

    </page>
  </document>

  <flows>

    <invoice>

      <table>
        <tr font-size="9pt">
          <th width="45%">DESCRIPTION</th>
          <th text-align="center">PRICE</th>
          <th text-align="center">QTY</th>
          <th text-align="right">TOTAL</th>
        </tr>
        <repeat data="data.items">
          <tr font-size="8pt">
            <td>
              <frame font-size="9pt">{{ item.name }}</frame>
              <frame show-if="item.description" margin-top="4pt">{{ item.description }}</frame>
            </td>
            <td h-align="center">
              <amount value="{{ item.price }}" />
            </td>
            <td h-align="center" text-align="center">{{ item.quantity }}</td>
            <td h-align="right">
              <amount value="{{ item.total }}" />
            </td>
          </tr>
        </repeat>
      </table>

      <table break="never">
        <tr style="@summary-row" v-align="center" line-height="14pt" font-size="9pt">
          <td width="65%" font-style="bold" text-align="right" colspan="2">
            SUB TOTAL:<br />
            VAT ({{ data.taxPercent }}%):
          </td>
          <td h-align="right">
            <amount value="{{ data.subTotal }}" />
            <amount value="{{ data.tax ? data.tax : 0 }}" />
          </td>
        </tr>
        <tr style="@summary-row" background-color="primary" font-size="10pt" v-align="center" font-color="white">
          <td width="40%"><b>CARD PAYMENT:</b> {{ data.payment.type }}</td>
          <td font-style="bold" text-align="right">
            GRAND TOTAL:</td>
          <td h-align="right">
            <frame h-align="right">
              <amount value="{{ data.total }}" />
            </frame>
          </td>
        </tr>
      </table>

      <frame break="never" font-size="8pt" line-height="1.2">
        <h2>TERMS &amp; CONDITIONS</h2>
        <p><b>Payment Terms</b>: Payment is due within 30 days of the invoice date. Accepted payment methods include
          bank transfer and credit card.

          <b>Late Payment</b>: A late fee of 1.5% per month will be applied to overdue balances. <b>Refund Policy</b>: Refunds
          are available within 14 days for defective products only. No refunds for services once delivered.

          <b>Delivery</b>: Products will be shipped within 7 business days. Risk transfers to the client upon delivery.

          <b>Confidentiality</b>: Both parties agree to keep all business and personal data confidential.

          <b>Liability</b>: Our liability is limited to the amount paid for the services.

          <b>Governing Law</b>: This invoice is governed by the laws of {{ data.country }}.

          <b>Contact</b>: {{ data.companyName }}, {{ data.info.address }}, {{ data.info.phone }}, {{ data.info.email }}.
        </p>
      </frame>

      <frame break="never">
        <h2 space-after-desired="12pt">PAYMENT METHOD</h2>
        <frame break="never" direction="row" v-align="top" width="fill">
          <frame line-height="1.68">
            Account No: <span>{{ data.payment.accountNumber }}</span><br />
            Account Name: <span>{{ data.payment.accountName }}</span><br />
            Sort Code: <span>{{ data.payment.sortCode }}</span><br />
          </frame>
          <frame width="fill" text-align="right" h-align="right">
            <span font="Alex Brush" font-size="fit" font-size-max="24pt" max-height="32pt">{{ data.from.name }}</span><br />
            <span font-style="bold" font-size="11pt">{{ data.from.name }}</span><br />
            <span font-size="8pt">{{ data.from.about }}</span>
          </frame>
        </frame>
      </frame>
    </invoice>
  </flows>

  <components>

    <amount style="@mono">
      {{ data.currency }}<value dp="2">{{ attrs.value }}</value>
    </amount>

    <labelled label-style="bold" gap="8pt" font-size="8pt" space-after-desired="4pt">
      <frame direction="row">
        <frame font-style="{{ attrs.label-style }}" space-after-desired="{{ attrs.gap }}">{{ attrs.label }}</frame>
        <frame>
          <slot />
        </frame>
      </frame>
    </labelled>

    <footer show-page-num="true" padding-top="16pt" font-size="8pt">
      <frame width="100%" direction="row" v-align="center" padding-top="12pt" border-weight-top="0.5pt"
        border-color="black">
        <frame width="fill">{{ data.info.website }}</frame>
        <frame width="fill" text-align="center" h-align="center">
          <frame show-if="{{ attrs.show-page-num }}"><page-number /> / <total-pages /></frame>
        </frame>
        <frame width="fill" text-align="right" h-align="right">{{ data.info.phone }}</frame>
      </frame>
    </footer>

  </components>

  <assets>
    <colors>
      <primary>#4376ba</primary>
    </colors>
  </assets>

  <styles>

    <document>
      <font>Roboto</font>
      <font-size>8pt</font-size>
    </document>

    <hr>
      <space-before-desired>11pt</space-before-desired>
      <space-after-desired>10pt</space-after-desired>
      <color>black</color>
      <height>0.5pt</height>
    </hr>

    <h2>
      <font-style>bold</font-style>
      <font-size>10pt</font-size>
      <padding-top>32pt</padding-top>
    </h2>

    <table>
      <width>fill</width>
      <child name="td">
        <border-weight>0</border-weight>
        <border-weight-bottom>0.5pt</border-weight-bottom>
        <padding-left>16pt</padding-left>
        <padding-right>16pt</padding-right>
        <padding-top>8pt</padding-top>
        <padding-bottom>8pt</padding-bottom>
        <v-align>center</v-align>
      </child>
      <child name="th">
        <padding-left>16pt</padding-left>
        <padding-right>16pt</padding-right>
        <padding-top>8pt</padding-top>
        <padding-bottom>8pt</padding-bottom>
        <background-color>primary</background-color>
        <font-color>white</font-color>
        <font-style>bold</font-style>
        <border-weight>0</border-weight>
      </child>
    </table>

    <alias name="summary-row">
      <child name="td">
        <border-weight>0</border-weight>
        <padding-top>16pt</padding-top>
        <padding-bottom>16pt</padding-bottom>
      </child>
    </alias>

    <alias name="text-big">
      <font-size>14pt</font-size>
      <font-style>bold</font-style>
      <space-before-desired>2pt</space-before-desired>
      <space-after-desired>2pt</space-after-desired>
    </alias>

    <alias name="mono">
      <font>UbuntuMono</font>
    </alias>
  </styles>

  <data type="json">
    {
      "currency": "£",
      "country": "UK",
      "companyName": "ASHGROVE PARTNERS",
      "logo": "https://examples.papermill.io/invoice/logo.png",
      "info": {
        "phone": "+44 (0)20 7946 0234",
        "email": "billing@ashgrovepartners.co.uk",
        "website": "www.ashgrovepartners.co.uk",
        "address": "12 Carter Lane, London, EC4V 5EQ",
        "invoiceDate": "30 Apr 2026",
        "issueDate": "30 Apr 2026",
        "invoiceNumber": "INV-2026-0184"
      },
      "from": {
        "name": "Catherine Ashgrove",
        "about": "Managing Partner"
      },
      "to": {
        "name": "Daniel Whitfield",
        "about": "Director of Operations"
      },
      "payment": {
        "type": "Bank Transfer",
        "accountNumber": "40928173",
        "accountName": "Ashgrove Partners LLP",
        "sortCode": "20-45-78"
      },
      "items": [
        {
          "name": "Discovery &amp; Assessment",
          "description": "Stakeholder interviews, site visits across three regions, and current-state process mapping",
          "price": 1450,
          "quantity": 4,
          "total": 5800
        },
        {
          "name": "Sector Benchmarking Pack",
          "description": "Comparative analysis against four sector peers across six operational KPI dimensions",
          "price": 2200,
          "quantity": 1,
          "total": 2200
        }
      ],
      "subTotal": 8000,
      "tax": 1600,
      "taxPercent": 20,
      "total": 9600
    }
  </data>
</press>

Example Payload

This template requires only JSON. We can either wrap the data in an XML payload, or set the content type to JSON and send directly to the PDF endpoint. In the example, we use the JSON-only method.

Details
json
{
  "info": {
    "phone": "+447522254001",
    "email": "tom@papermill.io",
    "website": "papermill.io",
    "address": "Renold Building, 81 Sackville St, Manchester",
    "invoiceDate": "15 Jan 2026",
    "issueDate": "15 Jan 2026",
    "invoiceNumber": "#946546"
  },
  "from": {
    "name": "Thomas White",
    "about": "CTO"
  },
  "to": {
    "name": "Mr Customer",
    "about": "PDF Needer"
  },
  "payment": {
    "type": "Visa Debit",
    "accountNumber": "64499877",
    "accountName": "Thomas White",
    "sortCode": "11-44-77"
  },
  "items": [
    {
      "name": "New Template",
      "description": "Letter, Invoice and Report templates",
      "price": 450,
      "quantity": 2,
      "total": 900
    },
    {
      "name": "Google Cloud Assistance",
      "description": "1 hour of video call assistance with Google Cloud",
      "price": 150,
      "quantity": 5,
      "total": 750
    }
  ],
  "subTotal": 1650,
  "tax": 330,
  "taxPercent": 20,
  "total": 1980,
  "currency": "$"
}

Generating PDFs

shell
curl -X POST https://api.papermill.io/v2/pdf?template_id=papermill-invoice \
  -H "Authorization: Bearer $PAPERMILL_API_KEY" \
  -H "Content-Type: application/json" \
  -o invoice.pdf \
  --data-binary @- <<EOF
  <insert JSON here>
EOF
python
import os
import requests

API_KEY = os.environ["PAPERMILL_API_KEY"]

payload = """
    <payload here>
"""

response = requests.post(
    "https://api.papermill.io/v2/pdf?template_id=papermill-invoice",
    headers={
        "Authorization": f"Bearer {API_KEY}",
        "Content-Type": "application/json",
    },
    data=payload.encode("utf-8"),
)
response.raise_for_status()

with open("letter.pdf", "wb") as f:
    f.write(response.content)
javascript
import fs from 'node:fs/promises'

const API_KEY = process.env.PAPERMILL_API_KEY

const payload = `
  <insert payload here>
`

const response = await fetch('https://api.papermill.io/v2/pdf?template_id=papermill-invoice', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${API_KEY}`,
    'Content-Type': 'application/json',
  },
  body: payload,
})

if (!response.ok) {
  throw new Error(`${response.status} ${response.statusText}`)
}

const buffer = Buffer.from(await response.arrayBuffer())
await fs.writeFile('quickstart.pdf', buffer)

Adapt this Template

  • Invoice items are in data.items
  • You'll need to ensure tax, subtotals, and totals are correctly calculated before sending them to Papermill.
  • Defaults are set for the logo and branding colour from the template assets. You can override these assets, and a logo URL can optionally be provided in data.logo