PaperPony

Recipe

Certificate template

A landscape certificate template for the PaperPony PDF API: centred serif typography, a decorative border and a verification code.

Worth knowing

  • Landscape comes from options.orientation, not from the template. Rotating with CSS produces a page that is the right shape and the wrong size.
  • The border is drawn with CSS rather than an image, so the certificate has no subresources and renders in one pass.
  • A verification code that resolves to a page you control is what makes a certificate checkable. Print it. A certificate nobody can verify is a picture.

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.

certificate.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; }
    /* html too: a percentage height on body resolves against html, and with
       html unsized both fall back to auto and the frame stops short. */
    html { height: 100%; }
    body {
      font-family: 'Noto Serif', serif;
      height: 100%;
      display: flex;
      align-items: center;
      justify-content: center;
      text-align: center;
    }
    .frame {
      width: 100%;
      height: 100%;
      padding: 14mm;
      border: 2.5px solid {{accent}};
      outline: 1px solid {{accent}};
      outline-offset: 4mm;
      display: flex;
      flex-direction: column;
      justify-content: center;
    }
    .kicker {
      font-family: 'Noto Sans', sans-serif;
      font-size: 9pt;
      letter-spacing: 0.32em;
      text-transform: uppercase;
      color: {{accent}};
    }
    h1 { margin: 6mm 0 0; font-size: 30pt; font-weight: 400; letter-spacing: 0.06em; }
    .recipient { margin-top: 10mm; font-size: 24pt; font-weight: 700; }
    .reason { margin: 6mm auto 0; max-width: 150mm; font-size: 12pt; color: #374151; }
    .signatures { display: flex; justify-content: center; gap: 30mm; margin-top: 16mm; }
    .signature { min-width: 55mm; }
    .signature .line { border-top: 1px solid #111827; padding-top: 2mm; }
    .signature .role {
      font-family: 'Noto Sans', sans-serif;
      font-size: 8pt;
      color: #6b7280;
    }
    .verify {
      font-family: 'Noto Sans Mono', monospace;
      margin-top: 12mm;
      font-size: 7.5pt;
      color: #9ca3af;
    }
  </style>
</head>
<body>
  <div class="frame">
    <div class="kicker">{{kicker}}</div>
    <h1>{{title}}</h1>

    <div class="recipient">{{recipient}}</div>
    <p class="reason">{{reason}}</p>

    <div class="signatures">
      {{#each signatories}}
        <div class="signature">
          <div class="line">{{this.name}}</div>
          <div class="role">{{this.role}}</div>
        </div>
      {{/each}}
    </div>

    <div class="verify">
      Issued {{formatDate issued_on dateStyle="long"}} · Verify at {{verify_url}} · {{code}}
    </div>
  </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": {
    "kicker": "Certificate of completion",
    "title": "Advanced Typesetting",
    "recipient": "Amelia Okonkwo",
    "reason": "has completed the forty-hour Advanced Typesetting programme, covering pagination, hyphenation and the preparation of print-ready artwork.",
    "accent": "#7c5c1e",
    "issued_on": "2026-07-26",
    "signatories": [
      {
        "name": "Dr Helen Marsh",
        "role": "Programme director"
      },
      {
        "name": "Tomas Klein",
        "role": "Head of studies"
      }
    ],
    "verify_url": "example.edu/verify",
    "code": "CT-2026-3F91-AA7C"
  },
  "options": {
    "format": "A4",
    "orientation": "landscape",
    "margin": {
      "top": "10mm",
      "right": "10mm",
      "bottom": "10mm",
      "left": "10mm"
    }
  }
}'
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": {
    "kicker": "Certificate of completion",
    "title": "Advanced Typesetting",
    "recipient": "Amelia Okonkwo",
    "reason": "has completed the forty-hour Advanced Typesetting programme, covering pagination, hyphenation and the preparation of print-ready artwork.",
    "accent": "#7c5c1e",
    "issued_on": "2026-07-26",
    "signatories": [
      {
        "name": "Dr Helen Marsh",
        "role": "Programme director"
      },
      {
        "name": "Tomas Klein",
        "role": "Head of studies"
      }
    ],
    "verify_url": "example.edu/verify",
    "code": "CT-2026-3F91-AA7C"
  },
  "options": {
    "format": "A4",
    "orientation": "landscape",
    "margin": {
      "top": "10mm",
      "right": "10mm",
      "bottom": "10mm",
      "left": "10mm"
    }
  }
});

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": {
        "kicker": "Certificate of completion",
        "title": "Advanced Typesetting",
        "recipient": "Amelia Okonkwo",
        "reason": "has completed the forty-hour Advanced Typesetting programme, covering pagination, hyphenation and the preparation of print-ready artwork.",
        "accent": "#7c5c1e",
        "issued_on": "2026-07-26",
        "signatories": [
            {
                "name": "Dr Helen Marsh",
                "role": "Programme director"
            },
            {
                "name": "Tomas Klein",
                "role": "Head of studies"
            }
        ],
        "verify_url": "example.edu/verify",
        "code": "CT-2026-3F91-AA7C"
    },
    "options": {
        "format": "A4",
        "orientation": "landscape",
        "margin": {
            "top": "10mm",
            "right": "10mm",
            "bottom": "10mm",
            "left": "10mm"
        }
    }
},
)
response.raise_for_status()
print(response.json()["output_url"])