Templating
cloudlayer.io uses the Nunjucks templating engine to power its template-based document generation. With templates, you can create professional, data-driven PDFs and images — invoices, certificates, reports, receipts, and more — by combining HTML/CSS layouts with dynamic JSON data.
Overview
All templates support:
- Nunjucks templating syntax — variables, conditionals, loops, filters, and macros
- Built-in Tailwind CSS — link the CDN in your templates for utility-first styling
- Custom formatting functions —
formatCurrency()andformatDateTime()for multicultural support - Auto calculations — predefined templates can compute totals, taxes, and subtotals automatically
- Smart page breaking — PDF templates handle page breaks intelligently for multi-page documents
- Thumbnail generation — optionally generate image previews of your documents
Template Types
There are two ways to use templates with cloudlayer.io: predefined templates from the gallery, or custom templates you build yourself.
Predefined Templates
The fastest way to get started. Browse the template gallery, pick a design, and pass your data.
- Choose a template from the PDF Template Gallery or the Image Template Gallery.
- Copy the
templateIdfrom the gallery page. - Use the sample JSON data as a starting point and customize it with your own values.
{
"templateId": "professional-invoice",
"data": {
"company_name": "Acme Inc.",
"invoice_no": "INV-001",
"items": [
{
"title": "Web Design",
"quantity": 10,
"unit_price": 150.00,
"amount": null
}
]
}
}
Tip: Fields set to
nullin the sample data are auto-calculated by the template. Set"__auto_calculate": falsein your data to supply your own values instead.
Custom Templates
For full control, write your own Nunjucks template and send it with your request. Custom templates are base64-encoded HTML strings that use Nunjucks syntax for dynamic content.
{
"template": "PGh0bWw+PGJvZHk+SGVsbG8ge3tuYW1lfX0hPC9ib2R5PjwvaHRtbD4=",
"data": {
"name": "Alice"
}
}
The template value above is the base64-encoded version of:
<html>
<body>
Hello {{name}}!
</body>
</html>
Nunjucks Template Syntax
Variables
Use double curly braces to output dynamic values from your data.
<h1>Invoice #{{invoice_no}}</h1>
<p>Bill to: {{customer.name}}</p>
<p>Email: {{customer.email}}</p>
Access nested properties with dot notation:
<p>{{company.address.street}}</p>
<p>{{company.address.city}}, {{company.address.state}}</p>
Conditionals
Use {% if %}, {% elif %}, and {% else %} for conditional rendering.
{% if status == "paid" %}
<span class="badge-green">Paid</span>
{% elif status == "pending" %}
<span class="badge-yellow">Pending</span>
{% else %}
<span class="badge-red">Overdue</span>
{% endif %}
You can also check for the existence of a value:
{% if discount %}
<tr>
<td>Discount</td>
<td>-{{formatCurrency(locale, currency, discount)}}</td>
</tr>
{% endif %}
Loops
Use {% for %} to iterate over arrays and objects.
Array iteration:
<table>
<thead>
<tr>
<th>Item</th>
<th>Qty</th>
<th>Price</th>
<th>Amount</th>
</tr>
</thead>
<tbody>
{% for item in items %}
<tr>
<td>{{item.title}}</td>
<td>{{item.quantity}}</td>
<td>{{formatCurrency(locale, currency, item.unit_price)}}</td>
<td>{{formatCurrency(locale, currency, item.amount)}}</td>
</tr>
{% endfor %}
</tbody>
</table>
Object iteration:
{% for key, value in metadata %}
<p><strong>{{key}}:</strong> {{value}}</p>
{% endfor %}
Loop variables:
Inside a {% for %} block, Nunjucks provides special variables:
| Variable | Description |
|---|---|
loop.index | Current iteration (1-based) |
loop.index0 | Current iteration (0-based) |
loop.first | true if first iteration |
loop.last | true if last iteration |
loop.length | Total number of items |
{% for item in items %}
<div class="{% if loop.last %}last-item{% endif %}">
{{loop.index}}. {{item.title}}
</div>
{% endfor %}
Filters
Nunjucks filters transform output values. Apply them with the pipe (|) character.
Common built-in filters:
| Filter | Example | Result |
|---|---|---|
upper | `{{ “hello” | upper }}` |
lower | `{{ “HELLO” | lower }}` |
capitalize | `{{ “hello world” | capitalize }}` |
title | `{{ “hello world” | title }}` |
trim | `{{ ” hello “ | trim }}` |
replace | `{{ “foo” | replace(“o”, “0”) }}` |
truncate | `{{ text | truncate(50) }}` |
round | `{{ 4.567 | round(2) }}` |
default | `{{ missing | default(“N/A”) }}` |
join | `{{ items | join(”, ”) }}` |
length | `{{ items | length }}` |
first | `{{ items | first }}` |
last | `{{ items | last }}` |
safe | `{{ htmlContent | safe }}` |
Chaining filters:
<p>{{ description | truncate(100) | capitalize }}</p>
Custom Functions
cloudlayer.io extends Nunjucks with two custom functions for multicultural formatting.
formatCurrency(locale, currency, amount)
Formats a numeric amount as a localized currency string.
Parameters:
| Parameter | Type | Description |
|---|---|---|
locale | string | Unicode locale identifier (e.g., "en-US", "en-GB", "de-DE", "ja-JP") |
currency | string | ISO 4217 currency code (e.g., "USD", "EUR", "GBP", "JPY") |
amount | number | The numeric amount to format |
Template usage:
<td>{{formatCurrency(locale, currency, item.unit_price)}}</td>
Examples:
| Locale | Currency | Amount | Output |
|---|---|---|---|
en-US | USD | 1500.50 | $1,500.50 |
en-GB | EUR | 150.10 | €150.10 |
de-DE | EUR | 1234.56 | 1.234,56 € |
ja-JP | JPY | 9800 | ¥9,800 |
fr-FR | EUR | 42.00 | 42,00 € |
formatDateTime(locale, dateTime)
Formats a date string into a localized date representation.
Parameters:
| Parameter | Type | Description |
|---|---|---|
locale | string | Unicode locale identifier (e.g., "en-US", "fr-FR") |
dateTime | string | The string representation of the date |
Template usage:
<p>Invoice Date: {{formatDateTime(locale, invoice_date)}}</p>
Examples:
| Locale | Input | Output |
|---|---|---|
en-US | "27 March, 2020" | March 27, 2020 |
fr-FR | "27 March, 2020" | 27 mars 2020 |
de-DE | "27 March, 2020" | 27. März 2020 |
ja-JP | "27 March, 2020" | 2020年3月27日 |
Smart Page Breaking for PDFs
PDF templates support intelligent page breaking so that content flows naturally across multiple pages without awkward mid-element splits.
CSS Page Break Properties
Use standard CSS page break properties in your templates:
/* Always start a new page before this element */
.section-header {
page-break-before: always;
}
/* Avoid breaking inside this element */
.invoice-item {
page-break-inside: avoid;
}
/* Avoid a page break right after this element */
.section-title {
page-break-after: avoid;
}
Practical Example: Multi-Page Invoice
<style>
.invoice-table tr {
page-break-inside: avoid;
}
.totals-section {
page-break-inside: avoid;
}
.terms-section {
page-break-before: always;
}
</style>
<div class="invoice-header">
<!-- Header content -->
</div>
<table class="invoice-table">
{% for item in items %}
<tr>
<td>{{item.title}}</td>
<td>{{item.quantity}}</td>
<td>{{formatCurrency(locale, currency, item.unit_price)}}</td>
</tr>
{% endfor %}
</table>
<div class="totals-section">
<p>Subtotal: {{formatCurrency(locale, currency, subTotal)}}</p>
<p>Tax: {{formatCurrency(locale, currency, totalTax)}}</p>
<p>Total: {{formatCurrency(locale, currency, amount)}}</p>
</div>
<div class="terms-section">
<h3>Terms & Conditions</h3>
<p>{{terms}}</p>
</div>
Tailwind CSS Support
All templates support Tailwind CSS. Link the CDN in your template’s <head> to use utility classes:
<html>
<head>
<script src="https://cdn.tailwindcss.com"></script>
</head>
<body>
<div class="max-w-2xl mx-auto p-8">
<h1 class="text-3xl font-bold text-gray-900">{{company_name}}</h1>
<div class="mt-6 border-t pt-4">
<div class="flex justify-between">
<span class="text-gray-600">Invoice #</span>
<span class="font-medium">{{invoice_no}}</span>
</div>
</div>
</div>
</body>
</html>
Tip: Since cloudlayer.io fetches external resources during rendering, any publicly accessible CSS framework, font, or stylesheet can be used in your templates — not just Tailwind.
Auto Calculations
Predefined templates from the gallery may support auto calculations. When enabled, the template automatically computes derived values like line item totals, subtotals, taxes, and grand totals.
How It Works
- Auto-calculated fields are represented as
nullin the sample data. - When
__auto_calculateistrue(the default), the template computes these fields. - Set
__auto_calculatetofalseif you want to supply your own computed values.
{
"templateId": "professional-invoice",
"data": {
"__auto_calculate": true,
"tax_percentage": 6,
"items": [
{
"title": "Web Design",
"quantity": 10,
"unit_price": 150.00,
"amount": null
}
],
"subTotal": null,
"totalTax": null,
"amount": null
}
}
In this example, amount, subTotal, totalTax, and the item-level amount are all computed automatically:
- Item amount =
quantity * unit_price=1,500.00 - Subtotal = sum of item amounts =
1,500.00 - Tax =
subTotal * (tax_percentage / 100)=90.00 - Total =
subTotal + totalTax=1,590.00
To disable auto calculation and supply your own values:
{
"templateId": "professional-invoice",
"data": {
"__auto_calculate": false,
"items": [
{
"title": "Web Design",
"quantity": 10,
"unit_price": 150.00,
"amount": 1500.00
}
],
"subTotal": 1500.00,
"totalTax": 90.00,
"amount": 1590.00
}
}
Template Data Injection
Data is injected into templates via the data property in your API request. The data object’s keys become variables available in the template.
Inline Request
Send the template and data as part of a single JSON payload:
{
"templateId": "professional-invoice",
"data": {
"company_name": "Acme Inc.",
"invoice_no": "INV-001",
"locale": "en-US",
"currency": "USD",
"items": [...]
}
}
Multipart Request
For larger templates, use multipart/form-data to send the template as a file:
curl --request POST \
--url https://api.cloudlayer.io/v2/template/pdf \
--header 'Content-Type: multipart/form-data' \
--header 'x-api-key: <YOUR-API-KEY>' \
--form 'options={"name": "my-invoice"}' \
--form 'template=@./template.njk' \
--form 'data=@./invoice-data.json'
With multipart requests, the template file does not need to be base64-encoded.
Practical Examples
Invoice Template
A complete invoice template using Tailwind CSS, Nunjucks loops, and the custom formatting functions.
Template (HTML):
<html>
<head>
<script src="https://cdn.tailwindcss.com"></script>
</head>
<body class="bg-white p-8 font-sans">
<div class="max-w-3xl mx-auto">
<!-- Header -->
<div class="flex justify-between items-start mb-8">
<div>
<h1 class="text-2xl font-bold text-gray-900">{{company_name}}</h1>
<p class="text-gray-600">{{address1}}</p>
<p class="text-gray-600">{{city}}, {{state}} {{zip}}</p>
</div>
<div class="text-right">
<h2 class="text-xl font-semibold text-blue-600">INVOICE</h2>
<p class="text-gray-700">#{{invoice_no}}</p>
<p class="text-gray-600">{{formatDateTime(locale, invoice_date)}}</p>
</div>
</div>
<!-- Bill To -->
<div class="mb-8 p-4 bg-gray-50 rounded">
<h3 class="font-semibold text-gray-700 mb-2">Bill To</h3>
<p class="font-medium">{{bill_to_fullname}}</p>
<p class="text-gray-600">{{bill_to_address1}}</p>
<p class="text-gray-600">
{{bill_to_city}}, {{bill_to_state_province_region}} {{bill_to_zip_postal_code}}
</p>
</div>
<!-- Line Items -->
<table class="w-full mb-8">
<thead>
<tr class="border-b-2 border-gray-300">
<th class="text-left py-2 text-gray-700">Description</th>
<th class="text-right py-2 text-gray-700">Qty</th>
<th class="text-right py-2 text-gray-700">Unit Price</th>
<th class="text-right py-2 text-gray-700">Amount</th>
</tr>
</thead>
<tbody>
{% for item in items %}
<tr class="border-b border-gray-200" style="page-break-inside: avoid;">
<td class="py-3">
<p class="font-medium">{{item.title}}</p>
{% if item.description %}
<p class="text-sm text-gray-500">{{item.description | truncate(80)}}</p>
{% endif %}
</td>
<td class="text-right py-3">{{item.quantity}}</td>
<td class="text-right py-3">
{{formatCurrency(locale, currency, item.unit_price)}}
</td>
<td class="text-right py-3">
{{formatCurrency(locale, currency, item.amount)}}
</td>
</tr>
{% endfor %}
</tbody>
</table>
<!-- Totals -->
<div class="flex justify-end" style="page-break-inside: avoid;">
<div class="w-64">
<div class="flex justify-between py-2">
<span class="text-gray-600">Subtotal</span>
<span>{{formatCurrency(locale, currency, subTotal)}}</span>
</div>
<div class="flex justify-between py-2">
<span class="text-gray-600">{{tax_label}} ({{tax_percentage}}%)</span>
<span>{{formatCurrency(locale, currency, totalTax)}}</span>
</div>
<div class="flex justify-between py-2 border-t-2 border-gray-900 font-bold text-lg">
<span>Total</span>
<span>{{formatCurrency(locale, currency, amount)}}</span>
</div>
</div>
</div>
<!-- Notes -->
{% if notes %}
<div class="mt-8 p-4 bg-gray-50 rounded">
<h3 class="font-semibold text-gray-700 mb-2">Notes</h3>
<p class="text-gray-600 text-sm">{{notes}}</p>
</div>
{% endif %}
</div>
</body>
</html>
Sample data:
{
"templateId": "professional-invoice",
"data": {
"__auto_calculate": true,
"locale": "en-US",
"currency": "USD",
"company_name": "Acme Inc.",
"address1": "1711 Bushnell Avenue",
"city": "South Pasadena",
"state": "California",
"zip": "91030",
"invoice_no": "INV-621",
"invoice_date": "27 March, 2020",
"bill_to_fullname": "Terry G. Brown",
"bill_to_address1": "1680 Ralph Drive",
"bill_to_city": "Cleveland",
"bill_to_state_province_region": "OH",
"bill_to_zip_postal_code": "44114",
"tax_label": "Tax",
"tax_percentage": 6,
"items": [
{
"title": "Homepage Design",
"description": "Complete redesign of the main landing page",
"quantity": 40,
"unit_price": 150.10,
"amount": null
},
{
"title": "Backend Development",
"description": "API development and database setup",
"quantity": 40,
"unit_price": 150.00,
"amount": null
},
{
"title": "QA Testing",
"description": "End-to-end testing and bug fixes",
"quantity": 10,
"unit_price": 10.00,
"amount": null
}
],
"subTotal": null,
"totalTax": null,
"amount": null,
"notes": "Payment is due within 30 days. Please include the invoice number with your payment."
}
}
Certificate Template
A simple certificate of completion using custom styling.
Template (HTML):
<html>
<head>
<script src="https://cdn.tailwindcss.com"></script>
<link href="https://fonts.googleapis.com/css2?family=Playfair+Display:wght@700&family=Inter&display=swap" rel="stylesheet">
</head>
<body class="bg-white">
<div class="w-[800px] h-[600px] mx-auto border-8 border-double border-amber-600 p-12 flex flex-col items-center justify-center text-center">
<p class="text-amber-600 uppercase tracking-widest text-sm mb-4">Certificate of Completion</p>
<h1 class="text-4xl font-bold text-gray-900 mb-2" style="font-family: 'Playfair Display', serif;">
{{recipient_name}}
</h1>
<div class="w-24 h-0.5 bg-amber-600 my-4"></div>
<p class="text-gray-600 text-lg mb-2">
has successfully completed the course
</p>
<h2 class="text-2xl font-semibold text-gray-800 mb-6">
{{course_name}}
</h2>
<p class="text-gray-500 mb-8">
Awarded on {{formatDateTime(locale, completion_date)}}
</p>
<div class="flex justify-between w-full mt-auto px-12">
<div class="text-center">
<div class="border-t border-gray-400 pt-2 px-8">
<p class="font-medium">{{instructor_name}}</p>
<p class="text-gray-500 text-sm">Instructor</p>
</div>
</div>
<div class="text-center">
<div class="border-t border-gray-400 pt-2 px-8">
<p class="font-medium">{{organization}}</p>
<p class="text-gray-500 text-sm">Organization</p>
</div>
</div>
</div>
</div>
</body>
</html>
Sample data:
{
"template": "<base64-encoded-template>",
"data": {
"locale": "en-US",
"recipient_name": "Jane Smith",
"course_name": "Advanced Web Development",
"completion_date": "15 January, 2024",
"instructor_name": "Dr. Alex Johnson",
"organization": "Tech Academy"
}
}
Request Types
Inline Request
Send everything as a single JSON payload. The template must be base64-encoded.
curl --request POST \
--url https://api.cloudlayer.io/v2/template/pdf \
--header 'Content-Type: application/json' \
--header 'x-api-key: <YOUR-API-KEY>' \
--data '{
"templateId": "professional-invoice",
"data": { "company_name": "Acme Inc.", ... }
}'
Multipart Request
Send the template and data as separate files. The template does not need to be base64-encoded.
curl --request POST \
--url https://api.cloudlayer.io/v2/template/pdf \
--header 'Content-Type: multipart/form-data' \
--header 'x-api-key: <YOUR-API-KEY>' \
--form 'options={"name": "my-document"}' \
--form 'template=@./my-template.njk' \
--form 'data=@./my-data.json'
Tips and Best Practices
- Use predefined templates when possible — they are professionally designed and tested for print quality.
- Test with small data sets first, then scale up to production data volumes.
- Use
page-break-inside: avoidon table rows and key sections to prevent awkward splits in PDFs. - Keep images external (hosted URLs) when possible for smaller payload sizes, or embed as data URIs for fully self-contained documents.
- Use
formatCurrencyandformatDateTimeinstead of manual formatting to ensure correct localization across all locales. - Set margins to
0when using templates (this is the default for template-based generation) and control spacing with CSS padding instead. - Use the
safefilter ({{ htmlContent | safe }}) when you need to render raw HTML stored in your data — but only with trusted content.