Skip to content

Making Templates

kittendevv edited this page Apr 25, 2026 · 6 revisions

You can install custom HTML templates from a manifest URL (remote templates), or upload them as a .zip file (local templates).

Template Types

Invio supports three types of templates:

Type Description Updatable
Built-in Shipped with Invio (professional-modern, minimalist-clean) No
Remote Installed from a manifest URL Yes (from source URL)
Local Uploaded as a .zip file No (re-upload to update)

Concepts

  • A Template is an HTML file rendered with a simple Mustache-like engine.
  • Data comes from the invoice and from Business Settings.
  • Built-in IDs: professional-modern, minimalist-clean.
  • Installed templates are stored in the DB (HTML) and optional assets under /app/data/templates/<id>/<version>/....

Install from a manifest (Remote Templates)

Use Settings → Templates → "Remote Template" → "Install from manifest", or call the API:

curl -u $ADMIN_USER:$ADMIN_PASS \
  -H "Content-Type: application/json" \
  -X POST "$BACKEND_URL/api/v1/templates/install-from-manifest" \
  -d '{"url":"https://example.com/invio-template/manifest.yaml"}'

Manifest (YAML or JSON) minimal schema:

id: clean-lines
name: Clean Lines
version: "1.0.0"
invio: ">=0.1.0"
html:
  path: index.html
  url: https://example.com/clean-lines/index.html
# license: MIT
# source:
#   manifestUrl: https://example.com/clean-lines/manifest.yaml
#   homepage: https://example.com/clean-lines/

Remote templates can be updated by clicking the "Update" button, which re-fetches from the original manifest URL.

Upload a .zip file (Local Templates)

Use Settings → Templates → "Local Template" → upload a .zip file, or call the API:

curl -u $ADMIN_USER:$ADMIN_PASS \
  -X POST "$BACKEND_URL/api/v1/templates/upload" \
  -F "file=@my-template.zip"

Zip Structure

Your .zip file must contain:

my-template.zip
├── manifest.yaml (or manifest.json)
└── index.html (or path specified in manifest)

Local Manifest Schema

id: my-custom-template
name: My Custom Template
version: "1.0.0"
html:
  path: index.html

Note: Local templates do not have an html.url field since the HTML is included in the zip.

Creating a Local Template

  1. Create your template HTML file
  2. Create a manifest.yaml with your template metadata
  3. Zip both files together (files should be at the root of the zip)
  4. Upload via Settings → Templates → "Local Template"

Security guardrails: Templates cannot include iframes/objects/embeds or inline on* handlers.

Update or delete

  • Remote templates: Update via POST /api/v1/templates/:id/update (re-fetches from manifest URL)
  • Local templates: Re-upload a new .zip file with the same id to replace
  • Delete any installed template: DELETE /api/v1/templates/:id (built-ins are protected)

Template context

Available variables your HTML can use (subset):

Company

  • companyName, companyAddress, companyEmail, companyPhone, companyTaxId
  • logoUrl (prefer this; system tries to inline remote logos as data URIs)

Invoice

  • invoiceNumber, issueDate, dueDate, currency, status

Customer

  • customerName, customerEmail, customerPhone, customerAddress, customerTaxId

Items

  • items (array of { description, quantity, unitPrice, lineTotal, notes })

Totals

  • subtotal, discountAmount?, discountPercentage?, taxRate?, taxAmount?, total
  • taxSummary? (array of { label, percent, taxable, amount }) and hasTaxSummary
  • netSubtotal (subtotal after discount, before tax)

Payment/Notes

  • paymentTerms?, paymentMethods?, bankAccount?, notes?

Extras

  • brandLogoLeft is always true in current renderer
  • highlightColor and highlightColorLight for custom branding

Internationalization

  • Template labels (like "INVOICE", "BILL TO", "TOTAL") are automatically translated based on the locale setting
  • The backend injects localized labels via the i18n system
  • No need to hardcode labels in your template — the system handles this
  • See Guides/Adding Translations for details on adding new languages

Blocks (conditionals/loops):

{{#items}}
  <tr>
    <td>{{description}}</td>
    <td class="num">{{quantity}}</td>
    <td class="num">{{unitPrice}}</td>
    <td class="num">{{lineTotal}}</td>
  </tr>
{{/items}}

Defaults: You can use {{var || 'Default'}} to fall back when a value is empty.

Clone this wiki locally