Templates

Templates let you create reusable PDF layouts with dynamic content. Store your HTML once, then render it with different data each time.

Why Use Templates?

  • Consistency: Same layout every time
  • Performance: HTML stored server-side, only send variables
  • Validation: Define a schema to catch errors before rendering
  • Maintenance: Update the template once, all future renders use the new version

Creating Templates

  1. Go to your Dashboard
  2. Click Create Template
  3. Enter a name and optional description
  4. Paste your HTML with Handlebars placeholders
  5. Define your variable schema (optional but recommended)
  6. Save the template

API Usage

To render a template, send a POST request with templateId and variables:

{
  "templateId": "550e8400-e29b-41d4-a716-446655440000",
  "variables": {
    "customerName": "Acme Corp",
    "invoiceNumber": "INV-001",
    "date": "2025-01-15",
    "items": [
      { "name": "Widget Pro", "qty": 2, "price": 50.00 },
      { "name": "Service Fee", "qty": 1, "price": 25.00 }
    ],
    "total": 125.00
  },
  "options": {
    "format": "A4"
  }
}
const response = await fetch('https://convert.pdfshot.com/convert', {
  method: 'POST',
  headers: {
    'Authorization': `Bearer ${process.env.PDFSHOT_API_KEY}`,
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({
    templateId: 'your-template-id',
    variables: {
      customerName: 'Acme Corp',
      invoiceNumber: 'INV-001',
      items: [{ name: 'Widget', qty: 2, price: 50 }],
      total: 100
    }
  })
})

const { url } = await response.json()

Handlebars Syntax

Templates use Handlebars for variable substitution.

Simple Variables

Use double curly braces to insert a value:

<h1>Invoice {{invoiceNumber}}</h1>
<p>Customer: {{customerName}}</p>

Iteration

Use {{#each}} to loop over arrays:

<table>
  <thead>
    <tr>
      <th>Item</th>
      <th>Qty</th>
      <th>Price</th>
    </tr>
  </thead>
  <tbody>
    {{#each items}}
    <tr>
      <td>{{this.name}}</td>
      <td>{{this.qty}}</td>
      <td>${{this.price}}</td>
    </tr>
    {{/each}}
  </tbody>
</table>

Conditionals

Use {{#if}} for conditional content:

{{#if discount}}
  <p>Discount: -${{discount}}</p>
{{/if}}

{{#if isPaid}}
  <span class="badge paid">PAID</span>
{{else}}
  <span class="badge unpaid">UNPAID</span>
{{/if}}

Nested Objects

Access nested properties with dot notation:

<p>{{customer.name}}</p>
<p>{{customer.address.city}}, {{customer.address.state}}</p>

Variable Schema

Define a JSON Schema to validate variables before rendering. This catches errors early and provides better error messages.

Example schema for an invoice template:

{
  "type": "object",
  "required": ["customerName", "invoiceNumber", "items"],
  "properties": {
    "customerName": {
      "type": "string",
      "description": "Customer's full name"
    },
    "invoiceNumber": {
      "type": "string",
      "pattern": "^INV-[0-9]+$"
    },
    "items": {
      "type": "array",
      "minItems": 1,
      "items": {
        "type": "object",
        "required": ["name", "qty", "price"],
        "properties": {
          "name": { "type": "string" },
          "qty": { "type": "integer", "minimum": 1 },
          "price": { "type": "number", "minimum": 0 }
        }
      }
    },
    "discount": {
      "type": "number",
      "minimum": 0
    },
    "total": {
      "type": "number"
    }
  }
}

If validation fails, you'll receive a clear error message:

{
  "success": false,
  "error": {
    "code": "VALIDATION_FAILED",
    "message": "Variable 'invoiceNumber' must match pattern ^INV-[0-9]+$"
  }
}

Example: Invoice Template

Here's a complete invoice template:

<!DOCTYPE html>
<html>
<head>
  <style>
    body { font-family: system-ui, sans-serif; padding: 40px; }
    .header { display: flex; justify-content: space-between; margin-bottom: 40px; }
    .logo { font-size: 24px; font-weight: bold; }
    .invoice-meta { text-align: right; color: #666; }
    table { width: 100%; border-collapse: collapse; margin: 20px 0; }
    th, td { padding: 12px; text-align: left; border-bottom: 1px solid #eee; }
    th { background: #f9f9f9; }
    .total { font-size: 18px; font-weight: bold; text-align: right; }
    .paid { color: green; }
    .unpaid { color: red; }
  </style>
</head>
<body>
  <div class="header">
    <div class="logo">{{companyName}}</div>
    <div class="invoice-meta">
      <div>Invoice {{invoiceNumber}}</div>
      <div>{{date}}</div>
    </div>
  </div>

  <p><strong>Bill to:</strong> {{customerName}}</p>

  <table>
    <thead>
      <tr>
        <th>Item</th>
        <th>Quantity</th>
        <th>Price</th>
        <th>Subtotal</th>
      </tr>
    </thead>
    <tbody>
      {{#each items}}
      <tr>
        <td>{{this.name}}</td>
        <td>{{this.qty}}</td>
        <td>${{this.price}}</td>
        <td>${{this.subtotal}}</td>
      </tr>
      {{/each}}
    </tbody>
  </table>

  {{#if discount}}
  <p>Discount: -${{discount}}</p>
  {{/if}}

  <div class="total">
    Total: ${{total}}
    {{#if isPaid}}
      <span class="paid">PAID</span>
    {{else}}
      <span class="unpaid">DUE</span>
    {{/if}}
  </div>
</body>
</html>

Error Codes

CodeDescription
TEMPLATE_NOT_FOUNDThe specified templateId does not exist
TEMPLATE_INACTIVEThe template has been deactivated
TEMPLATE_ACCESS_DENIEDYou don't have permission to use this template
VALIDATION_FAILEDVariables don't match the template's schema
TEMPLATE_RENDER_ERRORError during Handlebars compilation

Best Practices

  1. Use a variable schema - Catch errors before rendering
  2. Keep templates simple - Complex logic belongs in your application
  3. Test with the preview - Use the dashboard preview before integrating
  4. Version your templates - Create new templates rather than modifying production ones
  5. Use semantic variable names - customerName is better than n1