> ## Documentation Index
> Fetch the complete documentation index at: https://docs.helohq.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Templates

> Store reusable email content and use it to send messages

Templates let you store your email content in Helo, then reference it by ID when you send. Instead of including the subject, HTML, and text in every request, you pass a template ID and the data to fill it with.

Templates can include dynamic content. Use placeholders like `{{ firstName }}` to insert values from your data, and logic like conditionals and loops to vary the content for each recipient. Helo fills in the template each time you send. See [Template syntax](#template-syntax) for details.

Templates are a good fit when:

* The same email is sent many times with different data (password resets, receipts, newsletters)
* You want to update email content without deploying code changes
* You'd like to author emails in [MJML](https://mjml.io) and have Helo compile them to HTML

## Build templates in the dashboard

You can create and edit templates in the Templates section of your Helo dashboard. The template builder gives you everything you need to design an email without writing API requests:

* **Starter templates** — Start from a pre-built template for common email scenarios, then customize it to match your brand.
* **Live preview** — See the rendered email as you edit. Add sample data to fill in your variables and check how each recipient's version will look.
* **MJML support** — Write responsive emails in [MJML](https://mjml.io) and Helo compiles them to HTML for you on the fly, allowing you to see the output before you send.

Templates you create in the dashboard are the same as templates created via the API.

## Template content

Every template has a `name` and a `subject`, plus at least one of `html`, `mjml`, or `text`:

| Field | Description | Maximum length |
| - | - | - |
| `name` | A name for the template. Must be unique within your account. | 64 characters |
| `subject` | The subject line. | 256 characters |
| `html` | The HTML body. | 500,000 characters |
| `mjml` | MJML source. Helo compiles it to HTML when you save the template. | 500,000 characters |
| `text` | The plain-text body. | 100,000 characters |

HTML compiled from MJML must also be within the `html` limit.

### Authoring with MJML

If you supply `mjml`, Helo compiles it to HTML when you create or update the template. Invalid MJML is rejected at that point, so you find out about markup errors before you send. The compiled output is returned in the template's `html` field.

A template is authored either in MJML or in plain HTML — not both:

* You can't supply `html` and `mjml` in the same request.
* Updating a template's `html` clears its stored `mjml`, switching it back to plain HTML authoring.
* Removing a template's `mjml` (by setting it to `null`) keeps the HTML already compiled from it.

## Template syntax

Templates use [Liquid](https://shopify.github.io/liquid/) syntax. The subject, HTML, and text all support it. Inline templates in send requests support it too.

Output a variable with double curly braces:

```liquid theme={null}
Hi {{ firstName }}, your order #{{ order.id }} has shipped.
```

Use tags for logic, such as conditionals and loops:

```liquid theme={null}
{% if order.items.size > 1 %}
  <p>Your {{ order.items.size }} items are on their way:</p>
{% else %}
  <p>Your item is on its way:</p>
{% endif %}

<ul>
{% for item in order.items %}
  <li>{{ item.name }} × {{ item.quantity }}</li>
{% endfor %}
</ul>
```

Filters transform values:

```liquid theme={null}
{{ firstName | capitalize }}
{{ order.total | round: 2 }}
{{ promoCode | default: "WELCOME10" }}
```

> [!NOTE]
>
> Variable values are output as-is, without HTML escaping. If your data might contain user-supplied content, escape it in the template with the `escape` filter: `{{ comment | escape }}`.

Helo's own placeholders, such as `{{{ helo: unsubscribe }}}` for [unsubscribe links](https://docs.helohq.com/core/sending-broadcasts.md#unsubscribe-links), are left untouched by the template engine.

### Rendering limits

To keep sending fast and reliable, each render is limited in how much work it can do:

| Limit | Value |
| - | - |
| Steps executed per render (each loop iteration counts) | 250,000 |
| Range length, e.g. `(1..n)` | 100,000 |
| Characters generated per render | 2,000,000 |
| Array items created by filters per render | 100,000 |

These limits are far above what real emails need. A render that exceeds one fails with a `templating_error`.

## Create a template

```
POST /templates
```

**Example request**

```bash command-line theme={null}
curl --location 'https://api.helohq.com/templates' \
--header "Content-Type: application/json" \
--header "Accept: application/json" \
--header "Authorization: Bearer $HELO_API_KEY" \
--data-raw '{
  "name": "order-shipped",
  "subject": "Hi {{ firstName }}, your order has shipped",
  "html": "<p>Hi {{ firstName }}, your order #{{ orderId }} is on its way.</p>",
  "text": "Hi {{ firstName }}, your order #{{ orderId }} is on its way."
}'
```

**Response**

```json theme={null}
{
  "id": "0199a1b2-3c4d-7e5f-8a9b-0c1d2e3f4a5b",
  "createdAt": "2026-10-26T12:00:00Z",
  "updatedAt": "2026-10-26T12:00:00Z",
  "version": 1793016000000000,
  "name": "order-shipped",
  "subject": "Hi {{ firstName }}, your order has shipped",
  "html": "<p>Hi {{ firstName }}, your order #{{ orderId }} is on its way.</p>",
  "text": "Hi {{ firstName }}, your order #{{ orderId }} is on its way."
}
```

## Send with a template

To send a stored template, pass its ID in the `template` object along with the `data` to fill it with. This works for [transactional](https://docs.helohq.com/core/sending-transactional.md) messages, [broadcasts](https://docs.helohq.com/core/sending-broadcasts.md), and single broadcast messages.

```bash command-line theme={null}
curl --location 'https://api.helohq.com/send/transactional' \
--header "X-Helo-Channel-Id: $HELO_CHANNEL_ID" \
--header "Content-Type: application/json" \
--header "Accept: application/json" \
--header "Authorization: Bearer $HELO_API_KEY" \
--data-raw '{
  "from": "Acme <hello@acme.com>",
  "to": [{ "email": "jane@example.com", "name": "Jane" }],
  "template": {
    "id": "0199a1b2-3c4d-7e5f-8a9b-0c1d2e3f4a5b",
    "data": {
      "firstName": "Jane",
      "orderId": "1234"
    }
  }
}'
```

When you reference a template by `id`, don't include `subject`, `html`, or `text` in the `template` object.

If the template doesn't exist, or isn't available to the sending channel, the request fails with `errorCode: "template_not_found"`.

For broadcasts, the template's content is captured when the broadcast is accepted. Editing the template afterwards doesn't affect a broadcast that's already been submitted.

## Preview a template

You can render a template with sample data to check the output before you send. Rendering never sends or saves anything.

```
POST /templates/{id}/render
```

Renders a saved template.

```
POST /templates/render
```

Renders content you supply in the request (`subject`, plus `html` or `mjml`, and `text`) without saving it. This is useful for live previews while you edit a template.

**Example request**

```bash command-line theme={null}
curl --location "https://api.helohq.com/templates/$TEMPLATE_ID/render" \
--header "Content-Type: application/json" \
--header "Accept: application/json" \
--header "Authorization: Bearer $HELO_API_KEY" \
--data-raw '{
  "data": {
    "firstName": "Jane",
    "orderId": "1234"
  }
}'
```

**Response**

```json theme={null}
{
  "subject": "Hi Jane, your order has shipped",
  "html": "<p>Hi Jane, your order #1234 is on its way.</p>",
  "text": "Hi Jane, your order #1234 is on its way."
}
```

By default, CSS is inlined into the rendered HTML, just as it would be for a send. Set `inlineStyles` to `false` to see the HTML without inlining.

## Update a template

```
PATCH /templates/{id}
```

Only the fields you provide are changed. Omitted fields keep their existing values.

* Set `html`, `mjml`, or `text` to `null` to remove it. Blank values are rejected.
* An update can't leave a template with no HTML or text content.
* `name` and `subject` can be changed but not cleared.

### Avoid overwriting concurrent edits

Every template has a `version` that changes on each update. To make an update conditional, pass the `version` from your last read of the template:

```json theme={null}
{
  "subject": "Your order is on its way",
  "version": 1793016000000000
}
```

If the template was modified since that version, the update is rejected with HTTP 409. Fetch the latest template and try again. Without `version`, the last write wins.

## Restrict a template to specific channels

By default, a template is available to every [Channel](https://docs.helohq.com/core/channels.md) in your account. To restrict it, set `channelIds` when you create or update the template. Only those channels can then send it.

To remove the restriction, update the template with `channelIds` set to `null` or an empty array.

You can also filter the template list by channel with the `channelIds` query parameter on `GET /templates`.

## Manage templates via the Templates API

All template functionality is available via the API. You can:

* List/search templates or retrieve a template's details
* Create, update, and delete templates
* Render a saved template, or ad-hoc content, with sample data

[Check out the Templates API reference](https://docs.helohq.com/api-reference/templates/list-all-templates.md)


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.