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