Templates
Templates are reusable message designs for email, push, Telegram, and on-site channels.
Email types: marketing and transactional
Every email template is either marketing or transactional (category). New templates are marketing unless you choose otherwise: in the editor’s Email type field, in the Email type column of the templates list (which can also filter by type), from a Send Email step in an automation, or through the API.
Marketing (MARKETING, default) | Transactional (TRANSACTIONAL) | |
|---|---|---|
| What it is for | Newsletters, onboarding and nurture series, promotions, discounts, win-back | Password resets, receipts, account and security notices, replies to the contact |
| How often a contact gets it | Once. A contact who already received the template is skipped (Marketing template already sent to this contact) | As often as it is triggered |
| Contact opted out of email | Not sent. The contact needs EMAIL consent OPTED_IN | Sent anyway |
| Frequency caps and quiet hours | Applied (workspace and campaign) | Not applied |
| Suppression lists | Applied | Applied |
“Already received” counts messages for that template whose status is SENDING, SENT, DELIVERED, OPENED, CLICKED or BOUNCED; failed and skipped attempts do not count, so a skipped marketing email can still go out later.
The template’s category decides. A campaign of type TRANSACTIONAL only matters for emails sent without a template (inline subject and body) and for other channels. Replies from the Inbox are always transactional.
Keep the category honest: an email that sells something (a discount, an upgrade, a “come back” reminder) is marketing even when an event triggers it.
List Templates
GET /api/workspaces/{workspaceId}/templates
Returns all templates ordered by last updated.
Auth: Session required
Response:
[
{
"id": "tpl_abc123",
"name": "Welcome Email",
"channel": "EMAIL",
"category": "MARKETING",
"subject": "Welcome to {{workspace.name}}!",
"body": "<h1>Hello {{contact.firstName}}</h1>...",
"design": { ... },
"createdAt": "2026-01-15T10:00:00.000Z",
"updatedAt": "2026-01-20T14:00:00.000Z"
}
]Create Template
POST /api/workspaces/{workspaceId}/templates
Auth: Session required
Request Body:
{
"name": "Welcome Email",
"channel": "EMAIL",
"category": "MARKETING",
"subject": "Welcome to {{workspace.name}}!",
"body": "<h1>Hello {{contact.firstName}}</h1><p>Thanks for joining!</p>",
"design": {
"editor": "plate",
"blocks": [...]
}
}| Field | Type | Required | Description |
|---|---|---|---|
name | string | Yes | Template name |
channel | string | Yes | Target channel |
category | string | No | MARKETING (default) or TRANSACTIONAL; email templates only, see Email types. Can be changed with PATCH |
subject | string | No | Email subject line |
body | any | Yes | Template content (HTML or structured) |
design | object | No | Rich editor design data (Plate AI) |
Send a Test Email
POST /api/workspaces/{workspaceId}/templates/{templateId}/send-test
Sends the rendered email template once, with [TEST] prefixed to the subject. Available from the editor header and the templates list.
Auth: Session required (MEMBER or above)
Request Body:
{ "contactId": "clx...", "email": "me@example.com", "locale": "pt-BR" }| Field | Type | Required | Description |
|---|---|---|---|
contactId | string | One of the two | Renders with this contact’s data and language |
email | string | One of the two | Address to send to; overrides the contact’s email when both are given. Without a contact, sample data (Test User) is used |
locale | string | No | Translation to send; defaults to the contact’s locale, then the template’s default |
sampleData | object | No | Extra or overriding token values |
Response: { "sent": true, "to": "me@example.com" }
Errors: 400 — neither field given, the contact has no email, or the template is not an email template · 502 — the provider rejected the send
Personalization Tokens
Templates use {{path}} tokens resolved against the render context. Paths are dot-separated.
| Token | Description |
|---|---|
{{contact.firstName}} | Contact’s first name |
{{contact.lastName}} | Contact’s last name |
{{contact.fullName}} | First and last name joined |
{{contact.email}} | Contact’s email |
{{contact.phone}} | Contact’s phone |
{{contact.locale}} | Contact’s locale |
{{contact.<attribute>}} | Custom attribute value (attributes are merged into contact) |
{{workspace.name}} | Workspace name |
{{workspace.slug}} | Workspace slug |
{{campaign.id}} | Campaign ID, when sent from a campaign |
{{unsubscribeUrl}} | Auto-generated unsubscribe link |
{{preferencesUrl}} | Preference center link |
{{dwc:name}} | Dynamic content block by name |
Filters
Append | filter to a token: {{contact.createdAt | date}}. Available filters: date, datetime, relative, uppercase, lowercase, capitalize.
Conditionals
{{#if contact.plan == "pro"}}Thanks for being a Pro member!{{#else}}Upgrade today.{{/if}}Supported comparisons: ==, !=, >, <, or a bare path for truthiness.
Languages
A template has a defaultLocale and any number of translations, each with its own locale. Any real language tag is accepted (pt, pt-BR, es-MX, zh-Hant-TW): the language must be an ISO 639 code and the region an ISO 3166 country, and the tag is stored normalised (pt_br becomes pt-BR). In the dashboard, Add Language is a text field with suggestions, not a fixed list.
Language fallback
Selecting a template in a campaign selects the template and all of its translations. Each contact then receives the closest language available:
- The contact’s exact locale (
en-CA). - The same language without the region (
en). - Another regional variant of that language (
en-GBwhen the contact isen-CA). - The same three steps for the workspace default language.
- The template’s default language.
A contact with no locale uses the workspace default. When the campaign setting missing locale behavior is skip, a contact whose language has no match is skipped instead of receiving the default. Every message records the language it was sent in, shown in the message detail and used by A/B statistics.
Template A/B tests
A template can carry its own variants, tested across every campaign that sends it. Open a template and click A/B test.
- Each variant has a name, a traffic share, an optional subject and an optional HTML body. Empty fields fall back to the template (or the translation) for that language.
- A variant’s subject can be written per language (the Subject per language section of each variant). The sender uses the subject in the recipient’s language, else the variant’s general subject, else the translated template subject. This is how one email can test ten subject lines across twenty-five languages.
- Languages scope a variant. With none selected the variant applies to everyone; selecting Portuguese means only contacts resolved to Portuguese can receive it, and the others split between the remaining variants.
- Each contact is assigned by a stable hash of contact, template and language, so the same person keeps the same variant.
- Results show recipients, unique opens and clicks, rates, lift against the first variant, confidence from a two-proportion z-test, and a per-language breakdown.
- Declare winner sends the winning variant to everyone from that point on, within its language scope; Reset splits traffic again.
Template variants and the A/B split node in a campaign are independent: the node decides which branch a contact takes, and the template variant decides which wording that branch sends.
API
GET /api/workspaces/{workspaceId}/templates/{templateId}/abreturns the variants with their statistics.PUTthe same path with{ enabled, metric, variants: [{ id?, name, weight, locales, subject?, subjectByLocale?, body? }] }saves them; variants left out are deleted.subjectByLocaleis a map such as{ "pt-BR": "…", "it": "…" }; leaving it out keeps the stored subjects.POST /api/workspaces/{workspaceId}/templates/{templateId}/ab/winnerwith{ "variantId": "…" }(ornull) declares or clears the winner.