Templates API
Email templates define the content sent by flows and direct sends. Templates use NLT (Natural Language Template) syntax for dynamic personalization.
For a user guide on NLT template syntax, see Templates & NLT. For NLT engine internals, see NLT Engine.
Templates are content-only. A template carries the content (name, slug, body_html, and an optional stored subject) — not delivery identity. The sender address, sender name, reply-to, BCC, and content type are set by the consumer of the template (the flow step's email config, the campaign, or the direct-send request), never on the template itself. Sending any of sender_name, from_email, reply_to, bcc, content_type, or status in a create/update request returns 422.
Endpoints
CRUD
| Method | Path | Description |
|---|---|---|
GET | /v1/templates | List all templates |
POST | /v1/templates | Create a template |
GET | /v1/templates/{slug} | Get a template by slug |
PUT | /v1/templates/{slug} | Update a template |
DELETE | /v1/templates/{slug} | Delete a template |
POST | /v1/templates/{slug}/preview | Preview with test data |
POST | /v1/templates/{slug}/preflight | Run pre-send validation checks |
Version History
| Method | Path | Description |
|---|---|---|
GET | /v1/templates/{slug}/versions | List all versions |
POST | /v1/templates/{slug}/versions | Create a version snapshot |
GET | /v1/templates/{slug}/versions/{version_num} | Get a specific version |
POST | /v1/templates/{slug}/versions/{version_num}/restore | Restore from a version |
Template Utilities
| Method | Path | Description |
|---|---|---|
POST | /v1/templates/convert | Convert between NLT and Jinja2 |
POST | /v1/templates/detect-language | Detect template syntax |
POST | /v1/templates/validate | Validate NLT syntax |
POST | /v1/templates/render-inline | Render template without saving |
GET | /v1/templates/preview-context | Get real data for preview |
POST | /v1/templates/test-send | Send a test email |
List Templates
Returns all templates in the workspace, ordered by most recently updated.
Response
subject is the template's stored fallback subject line. It is null when the template has no stored subject (delivery then supplies the subject — see Subject resolution).
Create a Template
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
name | string | Yes | Human-readable template name (1--255 chars) |
slug | string | Yes | URL-safe identifier (unique per workspace, lowercase alphanumeric + hyphens). Used in flow step template_slug. |
body_html | string | Yes | Full HTML email body with NLT expressions for personalization (min 1 char, max 2 MB). |
subject | string | No | Stored fallback subject line (max 255 chars). Supports NLT expressions. Omit for a template with no stored subject — delivery then supplies the subject. See Subject resolution. |
body_ast | object | No | V2 block document (AST). When provided, body_html is still required and remains the authoritative delivery artifact — the AST is stored for the visual editor to re-open, not rendered server-side. |
is_active | boolean | No | Active status. Defaults to true. |
template_language | string | No | Per-template syntax pin: nlt or jinja2. Omit to resolve at open time. |
This is the complete set of accepted fields. Templates are content-only: sender_name, from_email, reply_to, bcc, content_type, and status are not template fields — sending any of them returns 422. Set delivery identity on the flow step, the campaign, or the direct-send request instead.
NLT content is automatically validated on create. The response includes validation_status (valid, has_warnings, or has_errors) and validation_errors with details. Validation is non-blocking -- templates are always saved regardless of validation result.
Response (201 Created)
Returns the full template object (same shape as Get Template).
A create request that omits subject returns the template with subject: null.
Templates created via the visual editor include a body_ast field containing the block document. Templates created via the API with only body_html have body_ast: null until they are opened in the visual editor.
Errors
| Status | Code | Cause |
|---|---|---|
| 422 | Validation error | subject exceeds 255 chars, or a forbidden delivery field (sender_name, from_email, reply_to, bcc, content_type, status) was included |
| 409 | Conflict | Slug already exists in this workspace |
| 403 | Forbidden | Missing templates:write permission |
Get a Template
Response
Same shape as the Create response.
Preview a Template
Render a saved template with test data without sending an email. Useful for verifying NLT expressions. Jinja2 expressions in the template are automatically converted to NLT before rendering.
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
contact | object | No | Contact data for NLT rendering. Include first_name, email, properties, etc. |
trigger_event | object | No | Trigger event with event_name and attributes |
additional_events | object | No | Map of event name to {attributes: {...}} for multi-event flows |
Response
If suppressed is true, a required NLT expression resolved to null. Check suppressed_reason to identify which field is missing from your test data.
Update a Template
Only the provided fields are updated -- omitted fields retain their current values. Every update automatically:
- Snapshots the current template state as a new version (for rollback)
- Increments the template
versionnumber - Re-validates the NLT content
Updatable Fields
| Field | Type | Description |
|---|---|---|
name | string | Template name (1--255 chars) |
subject | string | null | Stored fallback subject line (max 255 chars). See partial-update behavior below. |
body_html | string | HTML body (max 2 MB) |
body_ast | object | V2 block document (AST). When provided, body_html is also required — it stays the authoritative delivery artifact. |
is_active | boolean | Active status |
template_language | string | null | Syntax pin (nlt / jinja2); send null to clear the pin |
expected_version | integer | Optimistic-concurrency guard. When provided and it doesn't match the server's current version, the update is rejected with 409. Omit to skip the check. |
change_summary | string | Recorded in the version history for this update. |
The same content-only contract applies on update: sender_name, from_email, reply_to, bcc, content_type, and status return 422.
subject on update
subject follows standard partial-update semantics:
- Omit
subject— the stored subject is left unchanged. A content-only save (for example, sending onlybody_html) never wipes a previously stored subject. - Send
"subject": "..."— sets a new stored subject. - Send
"subject": null— clears the stored subject (the template falls back to delivery-supplied subjects on send).
Subject resolution
A stored template subject is the last fallback in the send-time subject chain. When Synapse sends an email that uses a template, it resolves the subject in this order and uses the first non-empty value:
- Send-request override — the
subjecton a direct-send request - Flow step — the subject configured on the flow's email step
- Campaign — the campaign's subject (for campaign sends)
- Template — the template's stored
subject - Empty — if none of the above is set, the email sends with an empty subject
This means a stored template subject gives a template a sensible default subject on its own, while a closer source (send override, flow step, or campaign) always wins when set. Leaving the template subject as null preserves the prior behavior — the subject must then come from the flow step, campaign, or send request.
Delete a Template
Returns 204 No Content on success.
Deleting a template that is referenced by an active flow will cause that flow step to fail. Deactivate or update the flow first. This is a permanent delete -- the template and all its versions are removed.
Version History
Every time you update a template, the previous state is automatically saved as a version. You can also create manual snapshots and restore to any previous version.
List Versions
Returns all versions for a template, newest first. The list view excludes the full body_html for performance.
Response
Create a Version Snapshot
Manually snapshot the current template state. Useful before making experimental changes.
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
change_summary | string | No | Description of the snapshot (max 500 chars) |
document_json | object | No | Arbitrary JSON metadata (e.g., editor state) |
Response (201 Created)
Version snapshots capture the template's content (body_html and, when authored in the visual editor, the AST). The stored template subject is not versioned — restoring a version restores the body content and increments the version; it does not change the current subject.
Get a Specific Version
Retrieve the full content of a specific version, including the body_html.
Returns the same shape as the Create Version response.
Restore a Version
Restore a template to a previous version. This automatically snapshots the current state before overwriting, so you never lose work.
Response
Returns the updated template object (same shape as Get Template) with the restored content and an incremented version number.
Restoring creates two new records: a snapshot of the current state (for undo), and the restored template with its version incremented. The original version record remains untouched.
Template Utilities
Stateless endpoints for syntax conversion, detection, validation, inline rendering, and preview data.
Convert Between NLT and Jinja2
Convert template expressions between NLT and Jinja2 syntax. Stateless -- no database access.
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
content | string | Yes | Template HTML to convert |
direction | string | No | jinja2_to_nlt, nlt_to_jinja2, or auto (default). Auto-detects and converts to the opposite syntax. Mixed templates normalize to NLT. |
Response
| Field | Type | Description |
|---|---|---|
content | string | Converted template HTML |
count | integer | Number of expressions converted |
warnings | string[] | Any conversion warnings (e.g., unsupported patterns) |
source_language | string | Detected source language |
target_language | string | Target language after conversion |
Detect Template Language
Identify which template syntax a template uses without converting it.
Response
| Field | Type | Description |
|---|---|---|
language | string | nlt, jinja2, mixed, or none |
nlt_count | integer | Number of NLT expressions found |
jinja2_count | integer | Number of Jinja2 expressions found |
Validate NLT Syntax
Run server-side NLT validation on template content. Tokenizes the template, renders it with provided data, and checks for unresolved expressions and suppression.
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
subject | string | No | Subject line to validate |
body_html | string | No | HTML body to validate |
contact | object | No | Contact data for test render |
trigger_event | object | No | Trigger event data for test render |
Response
| Field | Type | Description |
|---|---|---|
valid | boolean | true if there are no error-severity issues (warnings are allowed) |
issues | array | List of {severity, message, block_text} items. severity is error or warning; block_text is the offending fragment. |
rendered_subject | string | Rendered subject (if contact/event data provided) |
rendered_html | string | Rendered HTML body |
suppressed | boolean | Whether a required field was missing |
suppressed_reason | string | Which required field caused suppression |
Unresolved placeholders
A bare «…» placeholder (the guillemet characters U+00AB … U+00BB) in the subject or body_html — produced when the Smart Suggester inserts a token and it is never filled in — is reported as an error-severity issue, which flips valid to false:
- The
messagenames the field (subjectorbody), andblock_textis the exact«…»token. - A lone stray
«or»(unpaired) is also anerror, with the messageStray placeholder character in {field} — remove or complete the «…» before this template can be sent. - This mirrors the send-time guard: a
«…»left in a template is blocked at send across every channel. Validating first lets the editor surface a blocking error before you send.
This endpoint scans for literal « / » characters. The send-time guard additionally decodes HTML-entity forms («, «, «), so a template whose placeholder is entity-encoded can validate as valid: true yet still be blocked at send. Author placeholders as literal «…» (which the Smart Suggester does) to get the validation-panel error.
Render Inline
Render raw NLT (or Jinja2) subject and body with sample data -- no saved template needed. Jinja2 expressions are automatically converted to NLT before rendering.
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
subject | string | No | Subject line template |
body_html | string | No | HTML body template |
contact | object | No | Contact data |
trigger_event | object | No | Trigger event with attributes |
additional_events | object | No | Map of event name to {attributes: {...}} |
Response
Get Preview Context
Find real contact and event data from your workspace to use for template previews. Never returns fake data -- returns source: "none" when no match is found.
Query Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
event_name | string | No | Trigger event name to find matching data |
additional_events | string | No | Comma-separated additional event names for multi-event flows |
contact_id | string | No | Override: use this specific contact instead of auto-picking |
Response
source Value | Meaning |
|---|---|
real | Both contact and event data found from real records |
partial | Contact found, but no matching event (or vice versa) |
none | No matching data found -- frontend should show raw NLT expressions |
The lookup strategy: if contact_id is provided, use that contact and find their latest matching event. Otherwise, find the most recent event matching event_name and load its contact.
Test Send
Send a test email directly from the template editor. Renders NLT with sample data and sends via Resend. Test sends are not logged to email_logs and do not count toward your email quota.
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
to_emails | string[] | Yes | Recipient email addresses (1--5 max) |
subject | string | No | Subject line (defaults to "(Test) No subject") |
body_html | string | Yes | HTML body with NLT expressions |
sender_name | string | No | From display name |
from_email | string | No | From email address |
reply_to | string | No | Reply-to address |
Response
| Field | Type | Description |
|---|---|---|
sent_count | integer | Number of recipients the message was accepted for. |
failed | string[] | Recipient addresses that did not go out. |
results | array | Per-recipient {email, outcome, reason}. outcome is sent or failed; reason is a short code on a failed outcome, null on sent. |
all_accepted | boolean | true only if every recipient was accepted. |
Unresolved placeholder
test-send takes an inline subject/body_html that is never persisted, so the save-time guard never runs on it — the send-time guard does. A bare «…» in the inline subject or body_html fails every recipient with outcome: "failed", reason: "unresolved_placeholder", and no email is sent:
The subject line is automatically prefixed with [Test] so recipients can distinguish test sends from real emails.
Preflight Check
Run server-side pre-send validation on a saved template. Checks content quality, link health, image hosting, sender domain verification, and NLT syntax. Returns a structured report grouped by category.
Response
| Field | Type | Description |
|---|---|---|
pass | boolean | true if no errors (warnings are allowed) |
errors | array | Blocking issues that should be fixed before sending |
warnings | array | Non-blocking issues worth reviewing |
info | array | Informational checks that passed |
Each item has category (content, links, images, domain, accessibility), message, and severity.
The visual editor runs inline preflight checks automatically as you edit. The API endpoint is useful for CI/CD pipelines or programmatic validation before activating a flow.
Permissions
| Endpoint | Required Permission |
|---|---|
| List, Get, Preview, Validate, Detect, Convert, Render Inline, Preview Context, Preflight | templates:read |
| Create, Update, Delete, Create Version, Restore Version, Test Send | templates:write |