Custom HTML Templates
Overview
Payaca enables you to create professional, branded documents using HTML templates with LiquidJS templating syntax. These templates automatically pull data from your projects, customers, and custom fields to generate dynamic documents like commissioning certificates, installation reports, and compliance documentation.
Benefits over static PDFs:
Responsive design - Perfect display on any device (mobile, tablet, desktop) and prints beautifully
No manual data entry - Data flows automatically from Payaca projects
Easy updates - Change styling or layout without recreating forms
Better images - Full-resolution photos, image galleries, clickable to view full-size
Complete branding - Full control over colors, fonts, layout, and your company logo
Interactive features - Links, expandable sections, dynamic content
Common use cases:
Commissioning certificates (heat pumps, solar PV, etc.)
Installation reports and handover packs
Technical survey documents
Customer proposals and quotations
Compliance and warranty documentation
Service reports and maintenance records
Getting started
Your first template
Create a new file called my-template.html with this minimal structure:
<style>
:host {
display: block;
--primary-color: #2563eb;
} :host * {
margin: 0;
padding: 0;
box-sizing: border-box;
} .document-container {
container-type: inline-size;
container-name: document;
font-family: Arial, sans-serif;
padding: 2rem;
}
</style><div class="document-container">
<img src="{{ account.companyLogo }}" alt="Company Logo" style="height: 60px; margin-bottom: 1rem;"> <h1>Installation Report - {{ project.reference }}</h1> <p><strong>Customer:</strong> {{ customer.name }}</p>
<p><strong>Site Address:</strong> {{ project.siteAddress }}</p>
<p><strong>Date:</strong> {{ 'now' | date: "%d %B %Y" }}</p> <!-- Add your content sections here -->
</div>
Deploying your template
Go to Payaca:
Settings → Templates → Document TemplatesClick "Create document template"
Upload your
.htmlfile (drag & drop or browse)Preview in Desktop and Mobile views
Give it a descriptive name (e.g., "Heat Pump Commissioning Certificate")
Click "Create template"
Using your template
Open any project in Payaca
Go to the Files section
Click "Generate Document"
Select your template from the dropdown
Preview the document with real project data
Click "Generate" to create the document
Important: When updating templates, download the current version first as Payaca doesn't keep version history!
Template structure
Shadow DOM compatibility (critical requirement)
Payaca templates run in a Shadow DOM environment. This means you MUST follow specific CSS rules or your template won't display correctly.
Required CSS patterns:
<style>
/* Use :host instead of :root for CSS variables */
:host {
display: block;
--primary-color: #2563eb;
--secondary-color: #64748b;
--border-color: #e2e8f0;
} /* Use :host * instead of * for universal selector */
:host * {
margin: 0;
padding: 0;
box-sizing: border-box;
} /* Use .document-container instead of body */
.document-container {
container-type: inline-size;
container-name: document;
font-family: Arial, sans-serif;
color: #1e293b;
padding: 2rem;
}
</style>
Forbidden elements (will not work):
<html>,<head>,<body>tagsExternal CSS files (
<link rel="stylesheet">)All JavaScript (including
<script>tags, inline JavaScript, event handlers likeonclick,onload, etc.):rootselector in CSS (use:hostinstead)References to
document.bodyin CSS
What works:
Inline
<style>tags with Shadow DOM compatible CSSAll standard HTML5 tags (div, p, h1-h6, table, img, etc.)
CSS variables via
:hostEmbedded images (base64 encoded or external URLs)
Container queries for responsive design
Print-specific styles using
@media print
Correct structure template:
<style>
:host {
display: block;
--primary-color: #2563eb;
} :host * {
margin: 0;
padding: 0;
} .document-container {
container-type: inline-size;
container-name: document;
} /* Your custom styles here */
</style><div class="document-container">
<!-- Your content here -->
</div>
Variables and data
Templates use LiquidJS syntax with double curly braces to insert dynamic data. Learn more at LiquidJS documentation.
Account (your company) variables:
{{ account.companyName }} <!-- Your company name -->
{{ account.companyLogo }} <!-- Your company logo URL -->
{{ account.address }} <!-- Auto-formatted full address -->
{{ account.email }} <!-- Company email -->
{{ account.phone }} <!-- Company phone -->
{{ account.companyRegistrationNumber }} <!-- Registration number -->
Address component access:
{{ account.address['line1'] }} <!-- "123 High Street" -->
{{ account.address['line2'] }} <!-- "Unit 4" -->
{{ account.address.city }} <!-- "London" -->
{{ account.address.postalCode }} <!-- "SW1A 1AA" -->
{{ account.address.country }} <!-- "United Kingdom" -->
Project variables:
{{ project.reference }} <!-- Project reference number -->
{{ project.siteAddress }} <!-- Auto-formatted site address -->
{{ project.siteAddress['line1'] }} <!-- Address line 1 -->
{{ project.siteAddress.city }} <!-- City -->
{{ project.siteAddress.postalCode }} <!-- Postcode -->
Customer variables:
{{ customer.name }} <!-- Customer name -->
{{ customer.primaryContact.name }} <!-- Primary contact full name -->
{{ customer.primaryContact.firstName }} <!-- First name only -->
Custom fields:
Access your custom fields using this syntax:
{{ project.customFields.fieldsetId.fieldId }}
Real examples:
<!-- Property information -->
{{ project.customFields.propertyCharacteristics.yearBuilt }}
{{ project.customFields.propertyCharacteristics.propertyType }}
{{ project.customFields.propertyCharacteristics.numberOfBedrooms }}<!-- Electrical system -->
{{ project.customFields.electricalSystem.mainsVoltage }}
{{ project.customFields.electricalSystem.consumerUnitType }}
{{ project.customFields.electricalSystem.earthingSystem }}<!-- Heat pump design -->
{{ project.customFields.heatPumpDesignSpec.manufacturer }}
{{ project.customFields.heatPumpDesignSpec.modelNumber }}
{{ project.customFields.heatPumpDesignSpec.capacity }}
Date formatting:
Use the date filter with format codes. See LiquidJS date documentation.
{{ 'now' | date: "%d %B %Y" }} <!-- "17 November 2025" -->
{{ 'now' | date: "%d/%m/%Y" }} <!-- "17/11/2025" -->
{{ project.customFields.survey.surveyDate | date: "%d %B %Y" }}
Common date format codes:
%d- Day (01-31)%m- Month number (01-12)%B- Full month name ("November")%b- Short month ("Nov")%Y- Four-digit year (2025)%y- Two-digit year (25)
Conditionals:
Show content only when data exists:
{% if customer.email %}
<p><strong>Email:</strong> {{ customer.email }}</p>
{% endif %}{% if project.customFields.warranty.included %}
<div class="warranty-section">
<h2>Warranty Information</h2>
<p>Duration: {{ project.customFields.warranty.duration }} years</p>
</div>
{% endif %}
Loops:
Iterate over lists or repeating data:
{% for item in project.customFields.materials.items %}
<tr>
<td>{{ item.description }}</td>
<td>{{ item.quantity }}</td>
</tr>
{% endfor %}
Learn more about LiquidJS syntax at LiquidJS Tags documentation.
Working with images
Critical: Upload fields are ALWAYS arrays
This is the most common mistake when creating templates. Every upload/image field in Payaca is an array, even if it only contains one image. You MUST loop through the array to display images.
Wrong approach (will not work):
<!-- DON'T DO THIS -->
<img src="{{ project.customFields.survey.sitePhoto }}" alt="Site Photo">
Correct approach:
<!-- CORRECT - Loop through array and use .url property -->
{% if project.customFields.survey.sitePhoto %}
{% for image in project.customFields.survey.sitePhoto %}
<img src="{{ image.url }}" alt="Site Photo">
{% endfor %}
{% endif %}
Image properties available:
{{ image.url }}- Full resolution image (recommended for professional documents){{ image.thumbnailUrl }}- Smaller thumbnail version (faster loading, lower quality)
Recommended pattern: Clickable images with size constraints
This pattern creates professional-looking images that open full-size in a new tab when clicked:
{% if project.customFields.survey.equipmentPhoto %}
<div class="image-field">
{% for image in project.customFields.survey.equipmentPhoto %}
<a href="{{ image.url }}" target="_blank" rel="noopener" class="image-link">
<img src="{{ image.url }}" alt="Equipment Photo">
</a>
{% endfor %}
</div>
{% endif %}
Required CSS for clickable images with size constraints:
<style>
.image-field {
margin: 1rem 0;
} .image-link {
display: inline-block;
cursor: pointer;
border: 2px solid var(--border-color);
border-radius: 4px;
transition: border-color 0.2s ease, box-shadow 0.2s ease;
overflow: hidden;
} .image-link:hover {
border-color: var(--primary-color);
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15);
} .image-field img {
max-width: 100%;
max-height: 400px;
width: auto;
height: auto;
object-fit: contain;
display: block;
}
</style>
Why these size constraints?
max-width: 100%- Prevents images wider than the container from overflowingmax-height: 400px- Prevents excessively tall images from dominating the pageobject-fit: contain- Maintains aspect ratio while respecting constraintsUsers can click to view full-resolution in new tab
Multiple images pattern (photo grid):
<div class="photo-grid">
{% for image in project.customFields.survey.propertyPhotos %}
<div class="photo-item">
<a href="{{ image.url }}" target="_blank" rel="noopener">
<img src="{{ image.url }}" alt="Property Photo">
</a>
</div>
{% endfor %}
</div>
<style>
.photo-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
gap: 1rem;
} .photo-item img {
width: 100%;
height: 200px;
object-fit: cover;
border-radius: 4px;
}
</style>
Responsive design
Use container queries, NOT media queries
Shadow DOM requires container queries for responsive behavior. Media queries only work for print styles.
Required setup:
<style>
.document-container {
container-type: inline-size;
container-name: document;
}
</style>
Container queries for responsive layouts:
<style>
/* Base styles */
.field-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 1.5rem;
} /* Tablet and below - use container query */
@container document (max-width: 768px) {
.field-grid {
grid-template-columns: repeat(2, 1fr);
}
} /* Mobile - use container query */
@container document (max-width: 480px) {
.field-grid {
grid-template-columns: 1fr;
}
}
</style>
Media queries ONLY for print:
<style>
@media print {
.document-container {
padding: 0;
} .section-header {
background: white;
color: black;
border: 2px solid var(--primary-color);
print-color-adjust: exact;
-webkit-print-color-adjust: exact;
} .page-break {
page-break-after: always;
} a {
text-decoration: none;
color: inherit;
}
}
</style>
Auto-responsive grid pattern:
This pattern automatically adjusts columns based on available space:
<style>
.field-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 1.5rem;
}
</style>
Desktop: 3-4 columns (depending on screen width)
Tablet: 2 columns
Mobile: 1 column
Automatically adjusts without breakpoints!
Print-friendly color preservation:
<style>
.section-header {
background: var(--primary-color);
color: white;
print-color-adjust: exact;
-webkit-print-color-adjust: exact;
}
</style>
Advanced patterns
Checkbox visualization
Display boolean/checkbox fields with visual checkboxes:
<div class="checkbox-field">
<div class="checkbox-container">
<div class="checkbox-box {% if project.customFields.survey.workCompleted %}checked{% endif %}">
<span class="checkbox-checkmark">✓</span>
</div>
</div>
<span class="checkbox-label">Work completed and tested</span>
</div>
<style>
.checkbox-field {
display: flex;
align-items: center;
gap: 0.75rem;
margin: 0.5rem 0;
} .checkbox-box {
width: 24px;
height: 24px;
border: 2px solid var(--primary-color);
border-radius: 4px;
display: flex;
align-items: center;
justify-content: center;
background: white;
} .checkbox-box.checked {
background: var(--primary-color);
} .checkbox-checkmark {
display: none;
color: white;
font-weight: bold;
font-size: 18px;
} .checkbox-box.checked .checkbox-checkmark {
display: block;
} .checkbox-label {
font-size: 14px;
}
</style>
Data tables
<table class="data-table">
<thead>
<tr>
<th>Component</th>
<th>Specification</th>
<th>Quantity</th>
</tr>
</thead>
<tbody>
{% for item in project.customFields.materials.components %}
<tr>
<td>{{ item.component }}</td>
<td>{{ item.specification }}</td>
<td>{{ item.quantity }}</td>
</tr>
{% endfor %}
</tbody>
</table>
<style>
.data-table {
width: 100%;
border-collapse: collapse;
margin: 1rem 0;
} .data-table th {
background: var(--primary-color);
color: white;
padding: 0.75rem;
text-align: left;
font-weight: 600;
} .data-table td {
padding: 0.75rem;
border-bottom: 1px solid var(--border-color);
} .data-table tr:hover {
background: #f8fafc;
}
</style>
Signature boxes (coming soon)
Digital signature capture via the customer portal is currently in development. When available, customers will be able to sign documents directly through the portal, and signatures will appear automatically in your templates.
Signature boxes will display captured signatures with:
Signature image (captured via customer portal)
Signatory name
Date of signature
Placeholder line if not yet signed
This feature will be ideal for:
Commissioning certificate sign-off
Installation handover documentation
Customer acceptance forms
Warranty registration
Conditional sections
Show entire sections only when relevant:
{% if project.customFields.heatPump.systemType == "air-source" %}
<div class="section">
<h2>Air Source Heat Pump Specifications</h2>
<!-- ASHP specific content -->
</div>
{% endif %}{% if project.customFields.solar.panelsInstalled %}
<div class="section">
<h2>Solar PV System Details</h2>
<!-- Solar content -->
</div>
{% endif %}
Embedded company logo (base64)
For templates that need to work offline or without external dependencies:
<img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAA..." alt="Company Logo" style="height: 60px;">
Or use the dynamic account logo variable:
<img src="{{ account.companyLogo }}" alt="{{ account.companyName }}" style="height: 60px;">
Testing and troubleshooting
Before uploading to Payaca
Check all field names are spelled correctly (case-sensitive)
Verify custom field IDs match your account's fieldsets
Test upload fields use array loop pattern
Confirm Shadow DOM CSS rules (
:host, not:root)Check container queries for responsive (not media queries)
Validate HTML syntax
Common issues and solutions
Issue: Fields show as blank/undefined
✓ Check spelling (case-sensitive)
✓ Use
project.customFieldsnotcustomFieldValues✓ Remove
.readablesuffix (old syntax)✓ Verify fieldset and field IDs match your account
Issue: Images not displaying
✓ Upload fields are arrays - must use
{% for image in field %}✓ Use
{{ image.url }}or{{ image.thumbnailUrl }}property✓ Wrap in conditional
{% if field %}...{% endif %}✓ Check image field has data in project
Issue: Layout not responsive
✓ Using container queries (
@container), not media queries✓
.document-containerhascontainer-type: inline-size✓ Grid uses
repeat(auto-fit, minmax(250px, 1fr))pattern
Issue: Styles not applying
✓ Using
:hostinstead of:root✓ Using
:host *instead of*✓ Using
.document-containerinstead ofbody✓ All CSS is in inline
<style>tag
Issue: Print layout broken
✓ Using
@media printfor print-specific styles✓ Added
print-color-adjust: exactto preserve colors✓ Using
page-break-after: alwaysfor section breaks
Issue: Addresses not formatting
✓ Use
{{ project.siteAddress }}for auto-formatted address✓ Use
{{ account.address }}for company address✓ Access components:
postalCodenotpostcode,countrynotcounty
Testing checklist
✓ Preview in Payaca desktop view
✓ Preview in Payaca mobile view
✓ Generate document from real project with data
✓ Print preview (File → Print in browser)
✓ Check all images load and are clickable
✓ Verify all custom field values display correctly
✓ Test with project that has missing data (empty fields)
✓ Check conditional sections show/hide appropriately
Debugging techniques
Add temporary debug output to see what data is available:
<!-- Temporary debug section - remove before production -->
<div style="border: 2px solid red; padding: 1rem; margin: 1rem 0;">
<p>DEBUG: Project Reference = {{ project.reference }}</p>
<p>DEBUG: Customer Name = {{ customer.name }}</p>
<p>DEBUG: Field Value = {{ project.customFields.fieldset.field }}</p>
</div>
Complete reference
Account variables (your company)
Variable | Description | Example Output |
| Your company name | "ABC Renewables Ltd" |
| Your company logo URL | "https://..." |
| Auto-formatted company address | "123 High St, London, SW1A 1AA" |
| Company email | |
| Company phone | "01234 567890" |
| Registration number | "12345678" |
Account address components
Variable | Description | Example Output |
| Address line 1 | "123 High Street" |
| Address line 2 | "Unit 4" |
| City | "London" |
| Postcode | "SW1A 1AA" |
| Country | "United Kingdom" |
Project variables
Variable | Description | Example Output |
| Project reference number | "PROJ-2025-001" |
| Auto-formatted site address | "45 Oak Lane, Bristol, BS1 1AA" |
| Site address line 1 | "45 Oak Lane" |
| Site address line 2 | "Clifton" |
| Site city | "Bristol" |
| Site postcode | "BS1 1AA" |
| Site country | "United Kingdom" |
Customer variables
Variable | Description | Example Output |
| Customer name | "John Smith" |
| Primary contact full name | "John Smith" |
| Primary contact first name | "John" |
Custom field variables
Variable | Description | Example Output |
| Any custom field value | Varies by field type |
| Example: Survey date | "2025-11-17" |
| Example: Property type | "Detached" |
Date format codes
Code | Output | Example |
| Day (01-31) | 17 |
| Month number (01-12) | 11 |
| Full month name | November |
| Short month name | Nov |
| Four-digit year | 2025 |
| Two-digit year | 25 |
| Full day name | Monday |
| Short day name | Mon |
Common format combinations:
"%d %B %Y"→ "17 November 2025""%d/%m/%Y"→ "17/11/2025""%B %d, %Y"→ "November 17, 2025""%d-%m-%Y"→ "17-11-2025"
LiquidJS filters reference
Learn more at LiquidJS Filters documentation.
Filter | Description | Example |
| Format date |
|
| Convert to uppercase |
|
| Convert to lowercase |
|
| Capitalize first letter |
|
| Fallback if empty |
|
| Round number |
|
| Replace text |
|
Field type reference
Field Type | How to Display | Example |
Text | Direct output |
|
Number | Direct output |
|
Date | Use date filter |
|
Boolean/Checkbox | Conditional |
|
Upload/Image | Loop through array |
|
Dropdown | Direct output |
|
External resources
This guide covers all essential patterns and techniques for creating professional HTML document templates in Payaca. For additional support, contact Payaca support or consult the Payaca Help Center.
