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
- Go to your Dashboard
- Click Create Template
- Enter a name and optional description
- Paste your HTML with Handlebars placeholders
- Define your variable schema (optional but recommended)
- 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()
import requests
import os
response = requests.post(
'https://convert.pdfshot.com/convert',
headers={
'Authorization': f'Bearer {os.environ["PDFSHOT_API_KEY"]}',
'Content-Type': 'application/json'
},
json={
'templateId': 'your-template-id',
'variables': {
'customerName': 'Acme Corp',
'invoiceNumber': 'INV-001',
'items': [{'name': 'Widget', 'qty': 2, 'price': 50}],
'total': 100
}
}
)
url = response.json()['url']
curl -X POST https://convert.pdfshot.com/convert \
-H "Authorization: Bearer $PDFSHOT_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"templateId": "your-template-id",
"variables": {
"customerName": "Acme Corp",
"invoiceNumber": "INV-001",
"items": [{"name": "Widget", "qty": 2, "price": 50}],
"total": 100
}
}'
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
| Code | Description |
|---|---|
TEMPLATE_NOT_FOUND | The specified templateId does not exist |
TEMPLATE_INACTIVE | The template has been deactivated |
TEMPLATE_ACCESS_DENIED | You don't have permission to use this template |
VALIDATION_FAILED | Variables don't match the template's schema |
TEMPLATE_RENDER_ERROR | Error during Handlebars compilation |
Best Practices
- Use a variable schema - Catch errors before rendering
- Keep templates simple - Complex logic belongs in your application
- Test with the preview - Use the dashboard preview before integrating
- Version your templates - Create new templates rather than modifying production ones
- Use semantic variable names -
customerNameis better thann1