---
title: Templates
path: tutorials/templates
status: published
---

# Templates

Templates let you define the HTML, plain text, and subject of an email once, then send it with different data each time. ScaiSend templates are Handlebars/Mustache-compatible — the same syntax SendGrid dynamic templates use.

**Endpoints:** `GET/POST/PATCH/DELETE /v3/templates`, `GET/POST/PATCH/DELETE /v3/templates/{id}/versions`
**Auth:** `templates.read` for reads, `templates.write` for writes, `templates.delete` for deletes.

## The two-level model

A **template** is a named container. A **template version** is an actual renderable document (subject + HTML + plain text). One template has many versions; exactly one is **active** at a time. Sending with a template uses the active version.

```
Template "welcome-email" (id: d-welcome)
├── v1: "Welcome, {{name}}" ........... inactive
├── v2: "Hi {{name}}, welcome to Acme"  inactive
└── v3: "Welcome to Acme, {{name}}" ... active  ← used by sends
```

Version switches let you A/B test subject lines, roll out changes atomically, and roll back fast.

## Creating a template

```bash
curl -X POST https://scaisend.scailabs.ai/v3/templates \
  -H "Authorization: Bearer $SCAISEND_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"name": "welcome-email", "generation": "dynamic"}'
```

```python
import os
import httpx

resp = httpx.post(
    "https://scaisend.scailabs.ai/v3/templates",
    headers={"Authorization": f"Bearer {os.environ['SCAISEND_API_KEY']}"},
    json={"name": "welcome-email", "generation": "dynamic"},
)
template = resp.json()
print(template["id"])  # d-abc123
```

```typescript
const resp = await fetch("https://scaisend.scailabs.ai/v3/templates", {
  method: "POST",
  headers: {
    "Authorization": `Bearer ${process.env.SCAISEND_API_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({ name: "welcome-email", generation: "dynamic" }),
});
const template = await resp.json();
```

Fields:

| Field | Type | Notes |
|-------|------|-------|
| `name` | string (required) | Human-readable name; unique within tenant |
| `generation` | string | `dynamic` (default; uses Handlebars) or `legacy` (uses SendGrid's older `{{ }}` substitutions) |

Response:

```json
{
  "id": "d-abc123",
  "name": "welcome-email",
  "generation": "dynamic",
  "versions": [],
  "created_at": "2026-04-23T10:00:00Z",
  "updated_at": "2026-04-23T10:00:00Z"
}
```

The `id` starts with `d-` for dynamic templates. This is what you pass as `template_id` when sending.

## Adding a version

```bash
curl -X POST https://scaisend.scailabs.ai/v3/templates/d-abc123/versions \
  -H "Authorization: Bearer $SCAISEND_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "v1",
    "subject": "Welcome, {{name}}",
    "html_content": "<h1>Hi {{name}}</h1><p>Your plan: {{plan}}</p>",
    "plain_content": "Hi {{name}}. Your plan: {{plan}}.",
    "active": 1
  }'
```

Fields:

| Field | Type | Notes |
|-------|------|-------|
| `name` | string (required) | 1–255 chars; identifies the version |
| `subject` | string | Template-rendered subject line (≤ 1000 chars) |
| `preheader` | string | Preview text shown in the inbox list (≤ 500 chars); supports variables |
| `html_content` | string | HTML body |
| `plain_content` | string | Plain-text body (generated from HTML if omitted, but provide one — it's a deliverability signal) |
| `editor` | string | UI editor type (typically `"design"` or `"code"`; informational) |
| `active` | 0 or 1 | Set to `1` to immediately make this version the active one |

Every new version defaults to `active: 0`. You can activate later with:

```bash
curl -X POST https://scaisend.scailabs.ai/v3/templates/d-abc123/versions/v_xyz/activate \
  -H "Authorization: Bearer $SCAISEND_API_KEY"
```

## Sending with a template

Once a version is active, send without `content`:

```bash
curl -X POST https://scaisend.scailabs.ai/v3/mail/send \
  -H "Authorization: Bearer $SCAISEND_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "personalizations": [
      {
        "to": [{"email": "ada@example.com"}],
        "dynamic_template_data": {"name": "Ada", "plan": "Pro"}
      }
    ],
    "from": {"email": "hello@mail.example.com"},
    "template_id": "d-abc123"
  }'
```

`dynamic_template_data` becomes the context passed to the template engine. `{{name}}` in the template renders to `Ada`, `{{plan}}` to `Pro`.

## Template syntax

ScaiSend uses [chevron](https://pypi.org/project/chevron/) under the hood — a Mustache implementation extended with Handlebars-style helpers.

### Variables

```handlebars
<p>Hi {{name}}</p>
```

Output is HTML-escaped by default. To output raw HTML (dangerous; only use with trusted content):

```handlebars
<div>{{{raw_html_from_your_db}}}</div>
```

### Conditionals

```handlebars
{{#if subscribed}}
  <p>Thanks for being a subscriber!</p>
{{else}}
  <p><a href="https://...">Subscribe for updates</a></p>
{{/if}}
```

### Iteration

```handlebars
<ul>
  {{#each items}}
    <li>{{this.name}} — ${{this.price}}</li>
  {{/each}}
</ul>
```

Inside an `{{#each}}`, `this` refers to the current item. For primitive arrays (e.g., `["red", "blue"]`), use `{{.}}` to render the value directly.

### Helpers

ScaiSend ships a set of common helpers:

| Helper | Usage | Output |
|--------|-------|--------|
| `uppercase` | `{{uppercase name}}` | Upper-cased string |
| `lowercase` | `{{lowercase name}}` | Lower-cased string |
| `truncate` | `{{truncate text 50}}` | First 50 chars |
| `default` | `{{default name "Guest"}}` | `name` if non-empty, else `"Guest"` |
| `length` | `{{length items}}` | Length of array or string |
| `formatDate` | `{{formatDate date "%Y-%m-%d"}}` | Formatted date |
| `equals` | `{{#equals status "active"}}...{{/equals}}` | Block helper (equality) |
| `greaterThan` | `{{#greaterThan count 5}}...{{/greaterThan}}` | Block helper |
| `lessThan` | `{{#lessThan count 5}}...{{/lessThan}}` | Block helper |
| `and` | `{{#and a b}}...{{/and}}` | Block helper (logical AND) |
| `or` | `{{#or a b}}...{{/or}}` | Block helper (logical OR) |

### Example

```handlebars
<h1>Hi {{default name "there"}},</h1>

{{#if order.items}}
<p>Your order of {{length order.items}} item{{#greaterThan (length order.items) 1}}s{{/greaterThan}}:</p>
<ul>
  {{#each order.items}}
    <li><strong>{{this.name}}</strong> — {{formatDate this.ship_date "%b %d"}} — ${{this.price}}</li>
  {{/each}}
</ul>
<p>Total: <strong>${{order.total}}</strong></p>
{{else}}
<p>Your order is empty.</p>
{{/if}}

<p>— The {{uppercase company_name}} team</p>
```

With data:

```json
{
  "name": "Ada",
  "company_name": "Acme",
  "order": {
    "total": "42.99",
    "items": [
      {"name": "Widget", "price": "12.99", "ship_date": "2026-04-25"},
      {"name": "Gadget", "price": "29.99", "ship_date": "2026-04-26"}
    ]
  }
}
```

Renders as:

```html
<h1>Hi Ada,</h1>
<p>Your order of 2 items:</p>
<ul>
  <li><strong>Widget</strong> — Apr 25 — $12.99</li>
  <li><strong>Gadget</strong> — Apr 26 — $29.99</li>
</ul>
<p>Total: <strong>$42.99</strong></p>
<p>— The ACME team</p>
```

## Preview text (preheader)

The `preheader` field becomes the text shown next to the subject in the inbox list. Set it explicitly — otherwise email clients use the first ~100 chars of the body, which is often unhelpful.

```json
{
  "name": "v1",
  "subject": "Your April receipt",
  "preheader": "Order #{{order_id}} — ${{order_total}}",
  "html_content": "<html>..."
}
```

Preheader supports variables. A typical pattern: use the preheader to summarize the CTA, since it's the first thing the recipient reads.

## Managing versions

List versions of a template:

```bash
curl https://scaisend.scailabs.ai/v3/templates/d-abc123/versions \
  -H "Authorization: Bearer $SCAISEND_API_KEY"
```

Update a version (cannot modify an active version without deactivating first):

```bash
curl -X PATCH https://scaisend.scailabs.ai/v3/templates/d-abc123/versions/v_xyz \
  -H "Authorization: Bearer $SCAISEND_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"subject": "New subject line", "html_content": "<html>..."}'
```

Duplicate a version (useful for starting a new variant from the current active):

```bash
curl -X POST https://scaisend.scailabs.ai/v3/templates/d-abc123/versions/v_xyz/duplicate \
  -H "Authorization: Bearer $SCAISEND_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"name": "v2-experiment-a"}'
```

The duplicate is inactive by default.

Deactivate:

```bash
curl -X POST https://scaisend.scailabs.ai/v3/templates/d-abc123/versions/v_xyz/deactivate \
  -H "Authorization: Bearer $SCAISEND_API_KEY"
```

A template with no active version can't be used for sending — `/v3/mail/send` with that `template_id` returns 400.

## Rendering errors

Template rendering happens in the Worker service, not the API. A rendering error (undefined variable accessed strictly, broken syntax, helper misuse) does not cause a 4xx on `/v3/mail/send`. Instead:

- The message lands in `FAILED` status.
- A `dropped` event is recorded with reason `template_render_error`.
- The message's `error_message` field contains the rendering exception.

Test your templates before relying on them. Send a test email with representative data via a test key — sandbox rendering catches the error the same way a real send would.

## Listing templates

```bash
curl "https://scaisend.scailabs.ai/v3/templates?generation=dynamic" \
  -H "Authorization: Bearer $SCAISEND_API_KEY"
```

Query parameters:

| Parameter | Notes |
|-----------|-------|
| `generation` | `dynamic` or `legacy`; filter by template style |

## Deleting

Deleting a template deletes all its versions. Active sends that reference the template mid-flight complete normally (they've already been snapshot-rendered). Future sends with the template_id return 400.

```bash
curl -X DELETE https://scaisend.scailabs.ai/v3/templates/d-abc123 \
  -H "Authorization: Bearer $SCAISEND_API_KEY"
```

Prefer deactivation (remove the active version) over deletion when you might want the template back.

## What's next

- [Sending Mail](sending-mail) — using `template_id` on a send.
- [Attachments and Images](attachments-and-images) — embedding images in templates.
- [Templates Reference](../reference/templates) — exhaustive endpoint reference.
