Platform
ScaiWave ScaiGrid ScaiCore ScaiBot ScaiDrive ScaiKey Models Tools & Services
Solutions
Organisations Developers Internet Service Providers Managed Service Providers AI-in-a-Box
Resources
Support Documentation Blog Downloads
Company
About Research Careers Investment Opportunities Contact
Log in

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.

django
1
2
3
4
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
1
2
3
4
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
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
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
1
2
3
4
5
6
7
8
9
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
1
2
3
4
5
6
7
8
{
  "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
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
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
1
2
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
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
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 under the hood — a Mustache implementation extended with Handlebars-style helpers.

Variables#

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

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

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

Conditionals#

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

Iteration#

handlebars
1
2
3
4
5
<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
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
<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
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
{
  "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
1
2
3
4
5
6
7
8
<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
1
2
3
4
5
6
{
  "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
1
2
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
1
2
3
4
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
1
2
3
4
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
1
2
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
1
2
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
1
2
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#

Updated 2026-05-17 01:33:26 View source (.md) rev 1