Skip to content

Email Templates

Helmdesk emails are Handlebars. There are three kinds of file, and knowing which is which is most of the learning curve.

KindWhat it isReferenced by
TemplateThe body of one specific email — the welcome mail, the receipt.You, by templateKey when sending.
LayoutThe branded shell every email shares — header, footer, styles. Injects the body via {{{content}}}.A template, by key.
PartialA 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:

welcome.hbs
{{!-- 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.

Two-step import
// 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:

Response — linked
{
  "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": []
}
Response — layout not found (import still succeeds)
{
  "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:

FieldTypeDescription
(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'stringRelinks the template to that layout.
layout: ''stringRemoves the layout. 'none' works too.

Import API

POST/api/v1/email-templates/importemails:manage

Upload 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": []
}
FieldTypeDescription
filerequiredfileThe .hbs file. Anything else is rejected with 422.
type'template' | 'layout' | 'partial'Defaults to template.
keystringDefaults to the filename without .hbs, lowercased.
namestringDisplay name. Defaults to a title-cased key.
subjectstringOverrides the {{!-- subject: --}} comment. Templates only.
layoutstringKey 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.

POST/api/v1/account/email-templates/importemails:manage

Upload 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.