PaperPony

Recipe

Receipt template

A narrow thermal-style receipt template for the PaperPony PDF API, laid out as a single monospaced column on an A6 page with no logo fetch.

Worth knowing

  • The page size comes from options.format, and A6 is the narrowest named format the API has. That is the one to use here: a receipt printer feeds a roll, and forcing A4 wastes most of the paper. There is no way to ask for a growing height.
  • Monospaced figures matter more here than anywhere: a receipt is read as a column of numbers, and proportional digits break the alignment.
  • There is no logo image. A receipt that waits on a network fetch is a receipt that prints late.

The template

Store it once with POST /v1/templates. It is compiled when you store it, so a mistake surfaces now rather than on the first render.

receipt.hbs
<!doctype html>
<html>
<head>
  <meta charset="utf-8">
  <style>
    *, *::before, *::after { box-sizing: border-box; }
    body {
      margin: 0;
      font-family: 'Noto Sans', sans-serif;
      font-size: 10pt;
      line-height: 1.5;
      color: #111827;
      -webkit-print-color-adjust: exact;
      print-color-adjust: exact;
    }
    table { border-collapse: collapse; width: 100%; }
    thead { display: table-header-group; }
    tr { break-inside: avoid; }
    .num { text-align: right; font-variant-numeric: tabular-nums; white-space: nowrap; }
    body { font-family: 'Noto Sans Mono', monospace; font-size: 8.5pt; padding: 6mm 5mm; }
    h1 { margin: 0 0 1mm; font-size: 12pt; text-align: center; letter-spacing: 0.1em; }
    .centre { text-align: center; color: #4b5563; }
    .rule { border-top: 1px dashed #9ca3af; margin: 4mm 0; }
    .line { display: flex; justify-content: space-between; gap: 3mm; }
    .line .label { overflow-wrap: anywhere; }
    .total { font-weight: 700; font-size: 10pt; }
    .thanks { margin-top: 6mm; text-align: center; color: #4b5563; }
  </style>
</head>
<body>
  <h1>{{merchant.name}}</h1>
  <div class="centre">{{merchant.address}}</div>
  {{#if merchant.tax_id}}<div class="centre">VAT {{merchant.tax_id}}</div>{{/if}}

  <div class="rule"></div>

  <div class="line"><span>Receipt</span><span>{{number}}</span></div>
  <div class="line"><span>Date</span><span>{{formatDate issued_on locale=locale}}</span></div>
  {{#if payment_method}}
    <div class="line"><span>Paid by</span><span>{{payment_method}}</span></div>
  {{/if}}

  <div class="rule"></div>

  {{#each items}}
    <div class="line">
      <span class="label">{{this.description}}</span>
      <span>{{formatCurrency this.amount currency=../currency locale=../locale}}</span>
    </div>
    {{#if this.note}}<div class="centre" style="text-align:left">  {{this.note}}</div>{{/if}}
  {{/each}}

  <div class="rule"></div>

  <div class="line"><span>Subtotal</span><span>{{formatCurrency subtotal currency=currency locale=locale}}</span></div>
  {{#each tax_rows}}
    <div class="line"><span>{{this.label}}</span><span>{{formatCurrency this.amount currency=../currency locale=../locale}}</span></div>
  {{/each}}
  <div class="line total"><span>Total</span><span>{{formatCurrency total currency=currency locale=locale}}</span></div>

  <div class="thanks">{{footer_note}}</div>
</body>
</html>

Rendering it

Substitute the template id you got back, and send this payload.

curl https://api.paperpony.dev/v1/pdf/render \
  -H "Authorization: Bearer $PAPERPONY_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "template_id": "tpl_YOUR_TEMPLATE_ID",
  "data": {
    "number": "R-104829",
    "issued_on": "2026-07-26",
    "currency": "EUR",
    "locale": "de-DE",
    "merchant": {
      "name": "CAFE ATLAS",
      "address": "Torstrasse 118, 10119 Berlin",
      "tax_id": "DE812345678"
    },
    "payment_method": "Card ****4242",
    "items": [
      {
        "description": "Flat white",
        "amount": 4.2
      },
      {
        "description": "Sourdough toast",
        "amount": 6.5,
        "note": "no butter"
      },
      {
        "description": "Orange juice",
        "amount": 3.8
      }
    ],
    "subtotal": 12.18,
    "tax_rows": [
      {
        "label": "MwSt 19%",
        "amount": 2.32
      }
    ],
    "total": 14.5,
    "footer_note": "Vielen Dank!"
  },
  "options": {
    "format": "A6",
    "margin": {
      "top": "0",
      "right": "0",
      "bottom": "0",
      "left": "0"
    },
    "print_background": true
  }
}'
import { PaperPony } from 'paperpony';

const pony = new PaperPony({ apiKey: process.env.PAPERPONY_API_KEY });

const job = await pony.pdf.render({
  "template_id": "tpl_YOUR_TEMPLATE_ID",
  "data": {
    "number": "R-104829",
    "issued_on": "2026-07-26",
    "currency": "EUR",
    "locale": "de-DE",
    "merchant": {
      "name": "CAFE ATLAS",
      "address": "Torstrasse 118, 10119 Berlin",
      "tax_id": "DE812345678"
    },
    "payment_method": "Card ****4242",
    "items": [
      {
        "description": "Flat white",
        "amount": 4.2
      },
      {
        "description": "Sourdough toast",
        "amount": 6.5,
        "note": "no butter"
      },
      {
        "description": "Orange juice",
        "amount": 3.8
      }
    ],
    "subtotal": 12.18,
    "tax_rows": [
      {
        "label": "MwSt 19%",
        "amount": 2.32
      }
    ],
    "total": 14.5,
    "footer_note": "Vielen Dank!"
  },
  "options": {
    "format": "A6",
    "margin": {
      "top": "0",
      "right": "0",
      "bottom": "0",
      "left": "0"
    },
    "print_background": true
  }
});

console.log(job.output_url);
import os
import requests

response = requests.post(
    "https://api.paperpony.dev/v1/pdf/render",
    headers={"Authorization": f"Bearer {os.environ['PAPERPONY_API_KEY']}"},
    json={
    "template_id": "tpl_YOUR_TEMPLATE_ID",
    "data": {
        "number": "R-104829",
        "issued_on": "2026-07-26",
        "currency": "EUR",
        "locale": "de-DE",
        "merchant": {
            "name": "CAFE ATLAS",
            "address": "Torstrasse 118, 10119 Berlin",
            "tax_id": "DE812345678"
        },
        "payment_method": "Card ****4242",
        "items": [
            {
                "description": "Flat white",
                "amount": 4.2
            },
            {
                "description": "Sourdough toast",
                "amount": 6.5,
                "note": "no butter"
            },
            {
                "description": "Orange juice",
                "amount": 3.8
            }
        ],
        "subtotal": 12.18,
        "tax_rows": [
            {
                "label": "MwSt 19%",
                "amount": 2.32
            }
        ],
        "total": 14.5,
        "footer_note": "Vielen Dank!"
    },
    "options": {
        "format": "A6",
        "margin": {
            "top": "0",
            "right": "0",
            "bottom": "0",
            "left": "0"
        },
        "print_background": true
    }
},
)
response.raise_for_status()
print(response.json()["output_url"])