Skip to content

Templates API

Email templates define the content sent by flows and direct sends. Templates use NLT (Natural Language Template) syntax for dynamic personalization.

Note

For a user guide on NLT template syntax, see Templates & NLT. For NLT engine internals, see NLT Engine.

Note

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

MethodPathDescription
GET/v1/templatesList all templates
POST/v1/templatesCreate 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}/previewPreview with test data
POST/v1/templates/{slug}/preflightRun pre-send validation checks

Version History

MethodPathDescription
GET/v1/templates/{slug}/versionsList all versions
POST/v1/templates/{slug}/versionsCreate a version snapshot
GET/v1/templates/{slug}/versions/{version_num}Get a specific version
POST/v1/templates/{slug}/versions/{version_num}/restoreRestore from a version

Template Utilities

MethodPathDescription
POST/v1/templates/convertConvert between NLT and Jinja2
POST/v1/templates/detect-languageDetect template syntax
POST/v1/templates/validateValidate NLT syntax
POST/v1/templates/render-inlineRender template without saving
GET/v1/templates/preview-contextGet real data for preview
POST/v1/templates/test-sendSend a test email

List Templates

Returns all templates in the workspace, ordered by most recently updated.

bash
curl https://synapse-api.pyrx.tech/v1/templates \
-H "Authorization: Bearer <jwt>" \
-H "Content-Type: application/json"

Response

json
[
{
"id": "8f14e45f-ceea-467f-a83c-01a01ba3c5db",
"name": "Claim Submitted Notification",
"slug": "claim-submitted-notification",
"subject": "Claim {the Reference Number from the trigger event}: Submitted",
"body_html": "<h1>...</h1>",
"body_ast": {"schema_version": 2, "children": [...]},
"body_nlt_schema": [],
"version": 3,
"is_active": true,
"environment": "live",
"validation_status": "valid",
"validation_errors": [],
"validated_at": "2026-04-01T14:30:00Z",
"created_at": "2026-03-15T08:00:00Z",
"updated_at": "2026-04-01T14:30:00Z"
}
]
Note

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

bash
curl -X POST https://synapse-api.pyrx.tech/v1/templates \
-H "Authorization: Bearer <jwt>" \
-H "Content-Type: application/json" \
-d '{
"name": "Order Confirmation",
"slug": "order-confirmation",
"subject": "Order {the Order ID from the trigger event} Confirmed",
"body_html": "<h1>Thank you, {the user'\''s First Name, or \"there\"}!</h1><p>Your order {the Order ID from the trigger event} has been confirmed.</p><p>Total: {the Amount from the trigger event, as \"currency\"}</p>"
}'

Request Body

FieldTypeRequiredDescription
namestringYesHuman-readable template name (1--255 chars)
slugstringYesURL-safe identifier (unique per workspace, lowercase alphanumeric + hyphens). Used in flow step template_slug.
body_htmlstringYesFull HTML email body with NLT expressions for personalization (min 1 char, max 2 MB).
subjectstringNoStored 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_astobjectNoV2 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_activebooleanNoActive status. Defaults to true.
template_languagestringNoPer-template syntax pin: nlt or jinja2. Omit to resolve at open time.
Warning

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).

json
{
"id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"name": "Order Confirmation",
"slug": "order-confirmation",
"subject": "Order {the Order ID from the trigger event} Confirmed",
"body_html": "...",
"body_ast": null,
"body_nlt_schema": [],
"version": 1,
"is_active": true,
"environment": "live",
"validation_status": "valid",
"validation_errors": [],
"validated_at": "2026-04-07T10:30:00Z",
"created_at": "2026-04-07T10:30:00Z",
"updated_at": "2026-04-07T10:30:00Z"
}

A create request that omits subject returns the template with subject: null.

Note

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

StatusCodeCause
422Validation errorsubject exceeds 255 chars, or a forbidden delivery field (sender_name, from_email, reply_to, bcc, content_type, status) was included
409ConflictSlug already exists in this workspace
403ForbiddenMissing templates:write permission

Get a Template

bash
curl https://synapse-api.pyrx.tech/v1/templates/order-confirmation \
-H "Authorization: Bearer <jwt>"

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.

bash
curl -X POST https://synapse-api.pyrx.tech/v1/templates/order-confirmation/preview \
-H "Authorization: Bearer <jwt>" \
-H "Content-Type: application/json" \
-d '{
"contact": {
"first_name": "Jane",
"email": "[email protected]",
"properties": {"plan": "growth"}
},
"trigger_event": {
"event_name": "order_completed",
"attributes": {
"Order ID": "ORD-2026-0042",
"Amount": 149.99
}
},
"additional_events": {
"payment": {
"attributes": {"Amount": 250.00}
}
}
}'

Request Body

FieldTypeRequiredDescription
contactobjectNoContact data for NLT rendering. Include first_name, email, properties, etc.
trigger_eventobjectNoTrigger event with event_name and attributes
additional_eventsobjectNoMap of event name to {attributes: {...}} for multi-event flows

Response

json
{
"subject": "Order ORD-2026-0042 Confirmed",
"html": "<h1>Thank you, Jane!</h1><p>Your order ORD-2026-0042 has been confirmed.</p><p>Total: $149.99</p>",
"suppressed": false,
"suppressed_reason": null
}
Tip

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

bash
curl -X PUT https://synapse-api.pyrx.tech/v1/templates/order-confirmation \
-H "Authorization: Bearer <jwt>" \
-H "Content-Type: application/json" \
-d '{
"subject": "Your Order {the Order ID from the trigger event} is Confirmed!",
"body_html": "<h1>Hi {the user'\''s First Name, or \"there\"}!</h1>..."
}'

Only the provided fields are updated -- omitted fields retain their current values. Every update automatically:

  1. Snapshots the current template state as a new version (for rollback)
  2. Increments the template version number
  3. Re-validates the NLT content

Updatable Fields

FieldTypeDescription
namestringTemplate name (1--255 chars)
subjectstring | nullStored fallback subject line (max 255 chars). See partial-update behavior below.
body_htmlstringHTML body (max 2 MB)
body_astobjectV2 block document (AST). When provided, body_html is also required — it stays the authoritative delivery artifact.
is_activebooleanActive status
template_languagestring | nullSyntax pin (nlt / jinja2); send null to clear the pin
expected_versionintegerOptimistic-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_summarystringRecorded 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 only body_html) never wipes a previously stored subject.
  • Send "subject": "..." — sets a new stored subject.
  • Send "subject": nullclears 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:

  1. Send-request override — the subject on a direct-send request
  2. Flow step — the subject configured on the flow's email step
  3. Campaign — the campaign's subject (for campaign sends)
  4. Template — the template's stored subject
  5. 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

bash
curl -X DELETE https://synapse-api.pyrx.tech/v1/templates/order-confirmation \
-H "Authorization: Bearer <jwt>"

Returns 204 No Content on success.

Warning

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.

bash
curl https://synapse-api.pyrx.tech/v1/templates/order-confirmation/versions \
-H "Authorization: Bearer <jwt>"

Response

json
[
{
"id": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
"version": 2,
"change_summary": null,
"created_by": "mem_abc123",
"created_at": "2026-04-07T14:00:00Z"
},
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"version": 1,
"change_summary": "Initial version",
"created_by": "mem_abc123",
"created_at": "2026-04-07T10:30:00Z"
}
]

Create a Version Snapshot

Manually snapshot the current template state. Useful before making experimental changes.

bash
curl -X POST https://synapse-api.pyrx.tech/v1/templates/order-confirmation/versions \
-H "Authorization: Bearer <jwt>" \
-H "Content-Type: application/json" \
-d '{
"change_summary": "Before A/B test experiment",
"document_json": {"editor_state": "..."}
}'

Request Body

FieldTypeRequiredDescription
change_summarystringNoDescription of the snapshot (max 500 chars)
document_jsonobjectNoArbitrary JSON metadata (e.g., editor state)

Response (201 Created)

json
{
"id": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
"template_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"version": 3,
"body_html": "<h1>Hi {the user's First Name, or \"there\"}!</h1>...",
"document_json": {"editor_state": "..."},
"change_summary": "Before A/B test experiment",
"created_by": "mem_abc123",
"created_at": "2026-04-07T15:00:00Z"
}
Note

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.

bash
curl https://synapse-api.pyrx.tech/v1/templates/order-confirmation/versions/2 \
-H "Authorization: Bearer <jwt>"

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.

bash
curl -X POST https://synapse-api.pyrx.tech/v1/templates/order-confirmation/versions/1/restore \
-H "Authorization: Bearer <jwt>"

Response

Returns the updated template object (same shape as Get Template) with the restored content and an incremented version number.

Note

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.

bash
curl -X POST https://synapse-api.pyrx.tech/v1/templates/convert \
-H "Authorization: Bearer <jwt>" \
-H "Content-Type: application/json" \
-d '{
"content": "<p>Hello {{ UserAttribute['\''First Name'\''] }}</p>",
"direction": "auto"
}'

Request Body

FieldTypeRequiredDescription
contentstringYesTemplate HTML to convert
directionstringNojinja2_to_nlt, nlt_to_jinja2, or auto (default). Auto-detects and converts to the opposite syntax. Mixed templates normalize to NLT.

Response

json
{
"content": "<p>Hello {the user's First Name}</p>",
"count": 1,
"warnings": [],
"source_language": "jinja2",
"target_language": "nlt"
}
FieldTypeDescription
contentstringConverted template HTML
countintegerNumber of expressions converted
warningsstring[]Any conversion warnings (e.g., unsupported patterns)
source_languagestringDetected source language
target_languagestringTarget language after conversion

Detect Template Language

Identify which template syntax a template uses without converting it.

bash
curl -X POST https://synapse-api.pyrx.tech/v1/templates/detect-language \
-H "Authorization: Bearer <jwt>" \
-H "Content-Type: application/json" \
-d '{
"content": "<p>Hello {the user'\''s First Name}</p><p>{{ some_var }}</p>"
}'

Response

json
{
"language": "mixed",
"nlt_count": 1,
"jinja2_count": 1
}
FieldTypeDescription
languagestringnlt, jinja2, mixed, or none
nlt_countintegerNumber of NLT expressions found
jinja2_countintegerNumber 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.

bash
curl -X POST https://synapse-api.pyrx.tech/v1/templates/validate \
-H "Authorization: Bearer <jwt>" \
-H "Content-Type: application/json" \
-d '{
"subject": "Hello {the user'\''s First Name}",
"body_html": "<p>Your order {the Order ID from the trigger event} is ready.</p>",
"contact": {"first_name": "Jane", "properties": {}},
"trigger_event": {"attributes": {"Order ID": "ORD-001"}}
}'

Request Body

FieldTypeRequiredDescription
subjectstringNoSubject line to validate
body_htmlstringNoHTML body to validate
contactobjectNoContact data for test render
trigger_eventobjectNoTrigger event data for test render

Response

json
{
"valid": true,
"issues": [],
"rendered_subject": "Hello Jane",
"rendered_html": "<p>Your order ORD-001 is ready.</p>",
"suppressed": false,
"suppressed_reason": null
}
FieldTypeDescription
validbooleantrue if there are no error-severity issues (warnings are allowed)
issuesarrayList of {severity, message, block_text} items. severity is error or warning; block_text is the offending fragment.
rendered_subjectstringRendered subject (if contact/event data provided)
rendered_htmlstringRendered HTML body
suppressedbooleanWhether a required field was missing
suppressed_reasonstringWhich 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:

json
{
"valid": false,
"issues": [
{
"severity": "error",
"message": "Unresolved placeholder in body — fill in every «…» value before this template can be sent",
"block_text": "«first_name»"
}
],
"rendered_subject": "Welcome",
"rendered_html": "<p>Hi «first_name», thanks for joining.</p>",
"suppressed": false,
"suppressed_reason": null
}
  • The message names the field (subject or body), and block_text is the exact «…» token.
  • A lone stray « or » (unpaired) is also an error, with the message Stray 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.
Note

This endpoint scans for literal « / » characters. The send-time guard additionally decodes HTML-entity forms (&laquo;, &#171;, &#xAB;), 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.

bash
curl -X POST https://synapse-api.pyrx.tech/v1/templates/render-inline \
-H "Authorization: Bearer <jwt>" \
-H "Content-Type: application/json" \
-d '{
"subject": "Hello {the user'\''s First Name}",
"body_html": "<p>Your balance is {the Amount from the trigger event, as \"currency\"}.</p>",
"contact": {"first_name": "Jane", "properties": {}},
"trigger_event": {"attributes": {"Amount": 42.50}},
"additional_events": {}
}'

Request Body

FieldTypeRequiredDescription
subjectstringNoSubject line template
body_htmlstringNoHTML body template
contactobjectNoContact data
trigger_eventobjectNoTrigger event with attributes
additional_eventsobjectNoMap of event name to {attributes: {...}}

Response

json
{
"subject": "Hello Jane",
"html": "<p>Your balance is $42.50.</p>",
"suppressed": false,
"suppressed_reason": null
}

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.

bash
curl "https://synapse-api.pyrx.tech/v1/templates/preview-context?event_name=order_completed&additional_events=payment,shipment" \
-H "Authorization: Bearer <jwt>"

Query Parameters

ParameterTypeRequiredDescription
event_namestringNoTrigger event name to find matching data
additional_eventsstringNoComma-separated additional event names for multi-event flows
contact_idstringNoOverride: use this specific contact instead of auto-picking

Response

json
{
"contact": {
"first_name": "Jane",
"last_name": "Doe",
"email": "[email protected]",
"phone": "+6591234567",
"properties": {"plan": "growth", "country": "SG"}
},
"contact_meta": {
"id": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
"first_name": "Jane",
"last_name": "Doe",
"email": "[email protected]"
},
"trigger_event": {
"event_name": "order_completed",
"attributes": {"Order ID": "ORD-2026-0042", "Amount": 149.99}
},
"additional_events": {
"payment": {"attributes": {"Amount": 250.00}},
"shipment": {"attributes": {"Tracking Number": "SG123456789"}}
},
"source": "real"
}
source ValueMeaning
realBoth contact and event data found from real records
partialContact found, but no matching event (or vice versa)
noneNo matching data found -- frontend should show raw NLT expressions
Note

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.

bash
curl -X POST https://synapse-api.pyrx.tech/v1/templates/test-send \
-H "Authorization: Bearer <jwt>" \
-H "Content-Type: application/json" \
-d '{
"to_emails": ["[email protected]"],
"subject": "Welcome to Acme",
"body_html": "<h1>Hello {the user'\''s First Name}!</h1>",
"sender_name": "Acme",
"from_email": "[email protected]"
}'

Request Body

FieldTypeRequiredDescription
to_emailsstring[]YesRecipient email addresses (1--5 max)
subjectstringNoSubject line (defaults to "(Test) No subject")
body_htmlstringYesHTML body with NLT expressions
sender_namestringNoFrom display name
from_emailstringNoFrom email address
reply_tostringNoReply-to address

Response

json
{
"sent_count": 1,
"failed": [],
"results": [
{ "email": "[email protected]", "outcome": "sent", "reason": null }
],
"all_accepted": true
}
FieldTypeDescription
sent_countintegerNumber of recipients the message was accepted for.
failedstring[]Recipient addresses that did not go out.
resultsarrayPer-recipient {email, outcome, reason}. outcome is sent or failed; reason is a short code on a failed outcome, null on sent.
all_acceptedbooleantrue 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:

json
{
"sent_count": 0,
"failed": [],
"results": [
{ "email": "[email protected]", "outcome": "failed", "reason": "unresolved_placeholder" }
],
"all_accepted": false
}
Tip

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.

bash
curl -X POST https://synapse-api.pyrx.tech/v1/templates/order-confirmation/preflight \
-H "Authorization: Bearer <jwt>"

Response

json
{
"template_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"slug": "order-confirmation",
"pass": true,
"errors": [],
"warnings": [
{
"category": "images",
"message": "Image hosted externally: https://example.com/logo.png",
"severity": "warning"
}
],
"info": [
{
"category": "content",
"message": "Template has 4 blocks and 2 NLT expressions",
"severity": "info"
}
]
}
FieldTypeDescription
passbooleantrue if no errors (warnings are allowed)
errorsarrayBlocking issues that should be fixed before sending
warningsarrayNon-blocking issues worth reviewing
infoarrayInformational checks that passed

Each item has category (content, links, images, domain, accessibility), message, and severity.

Tip

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

EndpointRequired Permission
List, Get, Preview, Validate, Detect, Convert, Render Inline, Preview Context, Preflighttemplates:read
Create, Update, Delete, Create Version, Restore Version, Test Sendtemplates:write