Paperbolt is now live on the Shopify App Store Browse the listing

Templates API

Templates API

Templates allow you to create reusable PDF designs with variables that can be injected at generation time. Use block-based templates for complex layouts.

Endpoints

Method Endpoint Description
POST /v1/templates Create a new template
GET /v1/templates List all templates
GET /v1/templates/:id Get template details
PUT /v1/templates/:id Update a template
DELETE /v1/templates/:id Delete a template

Create Template

POST /v1/templates

Request Body

{
  "name": "Invoice Template",
  "description": "Standard invoice layout",
  "html": "<h1>Invoice for {{.customer_name}}</h1><p>Total: ${{.total}}</p>",
  "is_public": false
}

Use {{.variable}} syntax (Go-template style, with a leading dot). See Template Variables below for the full substitution rules.

Response

{
  "success": true,
  "data": {
    "template_id": "tpl_abc123",
    "name": "Invoice Template",
    "created_at": "2024-01-15T10:30:00Z"
  }
}

Template Variables

Templates use Go-style placeholder syntax ({{.variableName}} — note the leading dot) powered by Go's html/template package with a curated set of formatting helpers from Sprig. You get contextual auto-escaping (HTML, JS, CSS, URL), loops, conditionals, nested access, and helpers like default, printf, upper, date — safe by default against injection.

Basic substitution

<h1>Hello {{.name}}!</h1>
<p>Email: {{.email}}</p>
{
  "template_id": "tpl_abc123",
  "variables": {
    "name": "John Doe",
    "email": "john@example.com"
  }
}

Loops — {{range}}

Iterate over arrays. Essential for invoices and reports.

<h1>Invoice {{.number}}</h1>
<table>
  {{range .items}}
  <tr><td>{{.desc}}</td><td>{{.qty}}</td><td>${{printf "%.2f" .total}}</td></tr>
  {{end}}
</table>
{
  "template_id": "...",
  "variables": {
    "number": "INV-42",
    "items": [
      {"desc": "Widget", "qty": 2, "total": 20.00},
      {"desc": "Gadget", "qty": 1, "total": 50.00}
    ]
  }
}

Conditionals — {{if}} / {{else}}

<h1>Certificate for {{.name}}{{if .honors}} — {{.honors}}{{end}}</h1>
<p>{{if eq .status "paid"}}✓ PAID{{else}}UNPAID{{end}}</p>

Nested property access

<p>To: {{.recipient.name}}</p>
<p>{{.recipient.address.street}}<br>{{.recipient.address.city}}, {{.recipient.address.country}}</p>

Scoped blocks — {{with}}

{{with .customer}}
  <strong>{{.name}}</strong><br>{{.email}}
{{end}}

Defaults for optional fields

Use the default helper inside the template for per-field fallbacks:

<p>Tax ID: {{default "N/A" .tax_id}}</p>

Or declare a default_value on the variable itself (see json_schema below) — the server injects the default when the field is absent from the request.

Formatting helpers

Common ones (full list: Sprig's string, date, math docs):

Helper Example Output
printf {{printf "$%.2f" 3.5}} $3.50
upper / lower / title {{upper "hi"}} HI
trim {{trim " x "}} x
replace {{replace "-" "_" .sku}} dashes → underscores
date {{date "Jan 2, 2006" .signed_at}} Apr 14, 2026
default / coalesce {{default "n/a" .x}} fallback
len / first / last {{len .items}} 3
add / sub / mul / div {{mul .qty .price}} number

Schema enforcement

Templates can declare a schema of expected variables. Required fields missing from a request return 400 VARIABLE_VALIDATION_FAILED; optional fields with a default_value are auto-filled.

Store the schema as a JSON-encoded string on the json_schema field when you create or update a template:

{
  "name": "Invoice",
  "html": "<h1>{{.invoice_number}}</h1>",
  "json_schema": "[{\"name\":\"invoice_number\",\"type\":\"text\",\"required\":true,\"default_value\":\"\"},{\"name\":\"notes\",\"type\":\"text\",\"required\":false,\"default_value\":\"No notes.\"}]"
}

Schema fields:

  • name — variable identifier used in the template ({{.name}})
  • type — one of text, number, date, boolean, image_url, array (advisory; used by the designer UI)
  • requiredtrue rejects requests that omit this field unless a default_value is set
  • default_value — string used when the field is missing from the request

Substitution rules

  • Auto-escaped by default — value contents are HTML-escaped contextually (so <script>alert(1)</script> renders as visible text, not live script). URL, JS, and CSS contexts each get the right escape.
  • SSTI-safe — a variable value containing {{.admin_secret}} renders as literal text, never re-parsed as a template.
  • Missing optional variables render as empty strings. Missing required variables return 400 VARIABLE_VALIDATION_FAILED with details.field identifying the missing name.
  • Extra variables are ignored silently.
  • template_id takes precedence over inline html. If both are supplied, the template wins.
  • Deleted or non-existent template_id404 TEMPLATE_NOT_FOUND.

Complete example — Invoice with line items

<!DOCTYPE html>
<html>
<head><title>Invoice {{.invoice_number}}</title></head>
<body>
  <h1>Invoice {{.invoice_number}}</h1>
  <p>Bill to: {{.customer.name}}<br>{{.customer.email}}</p>
  <p>Issued: {{date "Jan 2, 2006" .issued_at}}</p>

  <table>
    <thead><tr><th>Item</th><th>Qty</th><th>Price</th><th>Total</th></tr></thead>
    <tbody>
      {{range .items}}
      <tr>
        <td>{{.description}}</td>
        <td>{{.quantity}}</td>
        <td>${{printf "%.2f" .unit_price}}</td>
        <td>${{printf "%.2f" .line_total}}</td>
      </tr>
      {{end}}
    </tbody>
  </table>

  <p>Subtotal: ${{printf "%.2f" .subtotal}}</p>
  {{if .tax}}<p>Tax: ${{printf "%.2f" .tax}}</p>{{end}}
  <p><strong>Total: ${{printf "%.2f" .total}}</strong></p>

  <p>Notes: {{default "Thank you for your business." .notes}}</p>
</body>
</html>
{
  "template_id": "tpl_abc123",
  "variables": {
    "invoice_number": "INV-42",
    "customer": {"name": "Acme Corp", "email": "ap@acme.example"},
    "issued_at": "2026-04-14T09:00:00Z",
    "items": [
      {"description": "Consulting",  "quantity": 10, "unit_price": 150, "line_total": 1500},
      {"description": "Integration", "quantity": 1,  "unit_price": 500, "line_total": 500}
    ],
    "subtotal": 2000,
    "tax": 320,
    "total": 2320
  }
}

List Templates

GET /v1/templates

Query Parameters

Parameter Type Description
page integer Page number (default: 1)
limit integer Results per page (default: 20, max: 100)
search string Search by name or description

Example

curl https://lightningpdf.dev/api/v1/templates?page=1&limit=10 \
  -H "Authorization: Bearer lpdf_your_api_key"

Response

{
  "success": true,
  "data": {
    "templates": [
      {
        "template_id": "tpl_abc123",
        "name": "Invoice Template",
        "description": "Standard invoice layout",
        "created_at": "2024-01-15T10:30:00Z",
        "updated_at": "2024-01-15T10:30:00Z"
      }
    ],
    "pagination": {
      "page": 1,
      "limit": 10,
      "total": 1,
      "total_pages": 1
    }
  }
}

Get Template

GET /v1/templates/:id
curl https://lightningpdf.dev/api/v1/templates/tpl_abc123 \
  -H "Authorization: Bearer lpdf_your_api_key"

Response

{
  "success": true,
  "data": {
    "template_id": "tpl_abc123",
    "name": "Invoice Template",
    "description": "Standard invoice layout",
    "html": "<h1>Invoice for {{customer_name}}</h1>...",
    "variables": {
      "customer_name": "string",
      "items": "array",
      "total": "number"
    },
    "options": {
      "format": "A4",
      "print_background": true
    },
    "created_at": "2024-01-15T10:30:00Z",
    "updated_at": "2024-01-15T10:30:00Z"
  }
}

Update Template

PUT /v1/templates/:id

Request Body

{
  "name": "Updated Invoice Template",
  "html": "<h1>INVOICE</h1><p>Customer: {{customer_name}}</p>",
  "variables": {
    "customer_name": "string"
  }
}

Response

{
  "success": true,
  "data": {
    "template_id": "tpl_abc123",
    "updated_at": "2024-01-15T14:30:00Z"
  }
}

Delete Template

DELETE /v1/templates/:id
curl -X DELETE https://lightningpdf.dev/api/v1/templates/tpl_abc123 \
  -H "Authorization: Bearer lpdf_your_api_key"

Response

{
  "success": true,
  "message": "Template deleted successfully"
}

Complete Example: Invoice System

1. Create Template

import requests

API_KEY = "lpdf_your_api_key"
BASE_URL = "https://lightningpdf.dev/api/v1"

def create_invoice_template():
    html = """
    <!DOCTYPE html>
    <html>
    <head>
      <style>
        body { font-family: Arial; margin: 2cm; }
        .header { text-align: center; margin-bottom: 2cm; }
        .invoice-table { width: 100%; border-collapse: collapse; }
        .invoice-table th, .invoice-table td {
          border: 1px solid #ddd;
          padding: 8px;
        }
        .total { text-align: right; font-size: 18px; margin-top: 1cm; }
      </style>
    </head>
    <body>
      <div class="header">
        <h1>INVOICE #{{invoice_number}}</h1>
        <p>Date: {{date}}</p>
        <p>Customer: {{customer_name}}</p>
      </div>
      <table class="invoice-table">
        <thead>
          <tr>
            <th>Item</th>
            <th>Quantity</th>
            <th>Price</th>
            <th>Total</th>
          </tr>
        </thead>
        <tbody>
          {{#each items}}
          <tr>
            <td>{{description}}</td>
            <td>{{quantity}}</td>
            <td>${{price}}</td>
            <td>${{total}}</td>
          </tr>
          {{/each}}
        </tbody>
      </table>
      <div class="total">
        <p>Subtotal: ${{subtotal}}</p>
        <p>Tax ({{tax_rate}}%): ${{tax_amount}}</p>
        <p><strong>Total: ${{total}}</strong></p>
      </div>
      {{#if notes}}
      <div style="margin-top: 2cm;">
        <p><strong>Notes:</strong></p>
        <p>{{notes}}</p>
      </div>
      {{/if}}
    </body>
    </html>
    """

    response = requests.post(
        f"{BASE_URL}/templates",
        headers={
            "Authorization": f"Bearer {API_KEY}",
            "Content-Type": "application/json"
        },
        json={
            "name": "Invoice Template v1",
            "description": "Standard invoice with line items and tax",
            "html": html,
            "variables": {
                "invoice_number": "string",
                "date": "string",
                "customer_name": "string",
                "items": "array",
                "subtotal": "number",
                "tax_rate": "number",
                "tax_amount": "number",
                "total": "number",
                "notes": "string"
            },
            "options": {
                "format": "Letter",
                "print_background": True
            }
        }
    )

    return response.json()["data"]["template_id"]

2. Generate Invoice

def generate_invoice(template_id, invoice_data):
    response = requests.post(
        f"{BASE_URL}/pdf/generate",
        headers={
            "Authorization": f"Bearer {API_KEY}",
            "Content-Type": "application/json"
        },
        json={
            "template_id": template_id,
            "variables": invoice_data
        }
    )

    result = response.json()
    if result["success"]:
        import base64
        pdf_bytes = base64.b64decode(result["data"]["pdf"])
        return pdf_bytes
    else:
        raise Exception(result["error"]["message"])

# Usage
template_id = create_invoice_template()

invoice_data = {
    "invoice_number": "INV-2024-001",
    "date": "January 15, 2024",
    "customer_name": "Acme Corp",
    "items": [
        {"description": "Widget", "quantity": 10, "price": 25.00, "total": 250.00},
        {"description": "Gadget", "quantity": 5, "price": 50.00, "total": 250.00}
    ],
    "subtotal": 500.00,
    "tax_rate": 8.5,
    "tax_amount": 42.50,
    "total": 542.50,
    "notes": "Payment due within 30 days"
}

pdf = generate_invoice(template_id, invoice_data)
with open("invoice.pdf", "wb") as f:
    f.write(pdf)

Error Codes

Code Description
template_not_found Template ID does not exist
invalid_template Template HTML or structure is invalid
missing_variables Required variables not provided
invalid_variables Variable types don't match schema

Best Practices

  1. Version your templates: Use descriptive names like "Invoice v2" when updating
  2. Define variable schemas: Always specify variable types for validation
  3. Test with sample data: Use the preview feature before production
  4. Cache template IDs: Store template IDs in your database for faster lookups
  5. Use blocks for complex layouts: Block-based templates are easier to maintain