Email Templates
Helmdesk emails are Handlebars. There are three kinds of file, and knowing which is which is most of the learning curve.
| Kind | What it is | Referenced by |
|---|---|---|
| Template | The body of one specific email — the welcome mail, the receipt. | You, by templateKey when sending. |
| Layout | The branded shell every email shares — header, footer, styles. Injects the body via {{{content}}}. | A template, by key. |
| Partial | A reusable fragment — a button, an address block. | Any template or layout, via {{> name}}. |
Template format
A template is a plain .hbs file. Two optional metadata comments let the file carry its own subject line and layout, so the whole thing lives in your repo:
{{!-- subject: Welcome to {{companyName}} --}}
{{!-- layout: branded --}}
<h1>Hello {{name}}</h1>
<p>Welcome aboard!</p>
{{> footer}}Both are read at import time. The subject becomes the template’s subject line; the layout key is resolved and linked to the template, which is what makes it show up in the dashboard’s Layout dropdown and get applied on every send.
Setting the layout
A layout has to exist before a template can point at it. Import the layout first, then the template — otherwise there is nothing to link to.
// 1. The shell
await helmdesk.emails.templates.import(
'<div class="wrap"><img src="{{brandLogoUrl}}">{{{content}}}</div>',
'branded.hbs',
{ type: 'layout' }
)
// 2. A template that uses it
const result = await helmdesk.emails.templates.import(
'<h1>Hello {{name}}</h1>',
'welcome.hbs',
{ type: 'template', subject: 'Welcome!', layout: 'branded' }
)The layout option and the {{!-- layout: --}} comment do the same thing; the option wins if you use both. Check layoutLinked in the response to confirm it took:
{
"id": "7d2e4c19-3f5a-4b8e-9c01-2a6d8f4b7e35",
"key": "welcome",
"name": "Welcome",
"type": "template",
"subject": "Welcome!",
"variables": ["name"],
"referencedPartials": [],
"layoutKey": "branded",
"layoutId": "a1b2c3d4-5e6f-4a7b-8c9d-0e1f2a3b4c5d",
"layoutLinked": true,
"warnings": []
}{
"key": "welcome",
"type": "template",
"layoutKey": "branded",
"layoutId": null,
"layoutLinked": false,
"warnings": [
"Layout \"branded\" does not exist in this project, so the template was imported without a layout. Import the layout first (type=layout), then re-import this template."
]
}A missing layout never fails the import
An unresolved layout is a warning, not an error — the template saves and sends, just unwrapped. If your emails arrive without branding, check layoutLinked on the import response before looking anywhere else.
Changing or removing a layout
How the layout field behaves on re-import:
| Field | Type | Description |
|---|---|---|
(omitted) | — | Keeps whatever layout the template already has. Re-importing a body with no layout comment will not wipe a layout you picked in the dashboard. |
layout: 'other' | string | Relinks the template to that layout. |
layout: '' | string | Removes the layout. 'none' works too. |
Import API
/api/v1/email-templates/importemails:manageUpload a .hbs file as a template, layout, or partial (multipart form data). Re-importing an existing key updates it in place — templates get a new version.
Request
// Using the SDK
await helmdesk.emails.templates.import(
fileContent, // string or Blob
'welcome.hbs',
{ type: 'template', layout: 'branded' }
)
// Using fetch
const form = new FormData()
form.append('file', file)
form.append('type', 'template')
form.append('layout', 'branded')
await fetch('https://helmdesk.dev/api/v1/email-templates/import', {
method: 'POST',
headers: { Authorization: 'Bearer sk_live_...' },
body: form,
})Response
{
"id": "7d2e4c19-3f5a-4b8e-9c01-2a6d8f4b7e35",
"key": "welcome",
"name": "Welcome",
"type": "template",
"subject": "Welcome!",
"variables": ["name", "activationUrl"],
"referencedPartials": ["footer"],
"layoutKey": "branded",
"layoutId": "a1b2c3d4-5e6f-4a7b-8c9d-0e1f2a3b4c5d",
"layoutLinked": true,
"warnings": []
}| Field | Type | Description |
|---|---|---|
filerequired | file | The .hbs file. Anything else is rejected with 422. |
type | 'template' | 'layout' | 'partial' | Defaults to template. |
key | string | Defaults to the filename without .hbs, lowercased. |
name | string | Display name. Defaults to a title-cased key. |
subject | string | Overrides the {{!-- subject: --}} comment. Templates only. |
layout | string | Key of an existing layout. Overrides the {{!-- layout: --}} comment. Templates only. |
Shared templates
Account-level templates, layouts, and partials are shared across every project. Reference them with the @account/ prefix. They are environment-less — there is no sandbox copy, and they are read-only while the dashboard is in sandbox mode.
/api/v1/account/email-templates/importemails:manageUpload a .hbs file to the account scope (shared across all projects).
Request
// Upload a shared layout
await helmdesk.emails.templates.import(
layoutHtml,
'house-style.hbs',
{ type: 'layout', scope: 'account' }
)
// A shared template that uses it
await helmdesk.emails.templates.import(
invoiceHtml,
'invoice-paid.hbs',
{ type: 'template', scope: 'account', layout: 'house-style' }
)
// Send it from any project
await helmdesk.emails.send({
templateKey: '@account/invoice-paid',
to: { email: 'user@example.com' },
})Response
{
"id": "c9e1a740-62b3-4d5f-8a17-90b2c4d6e8f1",
"key": "invoice-paid",
"name": "Invoice Paid",
"type": "template",
"scope": "account",
"subject": "Your invoice is paid",
"referencedPartials": ["@account/footer"],
"layoutKey": "house-style",
"layoutLinked": true,
"variables": ["invoiceNumber", "amount"],
"warnings": []
}Project templates cannot use shared layouts
A project template links to a layout by id, and layouts are per-project — so {{!-- layout: @account/house-style --}} on a project template returns a warning and no link. Either import the layout into the project, or move the template to the account scope where shared layouts resolve.
Resolution order
- • Templates: the
@account/prefix picks the shared one explicitly; a bare key is the project’s own. - • Layouts: project layouts first, then account layouts with the same key as a fallback.
- • Partials: project partials take priority; account partials fill in unmatched keys.
Sandbox
Project templates, layouts, and partials exist separately per environment. An sk_sandbox_ key imports into the sandbox plane, so you can iterate without touching production. Promote a finished sandbox template to live from the dashboard. See Sandbox.