Skip to main content
Email templates store reusable subject and body content so you can send the same message to many recipients without re-sending the content on every request. Templates use Liquid variables — {{first_name}} — so each send renders recipient-specific content from variables you supply at send time. Templates are useful when you:
  • Send the same message repeatedly — onboarding sequences, receipts, password resets.
  • Separate content from code — let designers edit email copy in a template while your application only passes variables.
  • Build agent workflows — an AI agent generates variable values, the template governs structure, and you render to validate the result before sending.
All requests use the base URL https://api.telnyx.com/v2 and an Authorization: Bearer *** header.

Create a template

Create a template with POST /email_templates. Only name is required; subject, html_body, and text_body are optional Liquid template strings.
curl
A successful create returns 201 Created with the template in data:
The variables array is stored on the template and returned on every read. strict_variables, autoescape, and variable_schema persist and are returned on every template response — variable_schema is null when the template uses only the legacy variables array. Save the id — you’ll use it to render, send with, update, and delete the template.
The variables field is descriptive metadata, not a constraint. Passing template_variables with keys that aren’t listed here is not an error — only the Liquid tags actually present in your template’s content are substituted. Use variables to document what a caller should supply.

Manage templates

List templates

List templates for your account with GET /email_templates. Pagination is cursor-based: page_size (default 25, clamped 1–100) and page_cursor.
curl
page_cursor in meta is the opaque cursor for the next page. It is omitted (not null) when there is no next page. Pass it back as the page_cursor query parameter to fetch the next page.

Get a template

Fetch a single template by id with GET /email_templates/{id}:
curl
Returns 200 with the template in data, or 404 if the template doesn’t exist or belongs to another account.

Update a template

Update a template with PATCH /email_templates/{id} or PUT /email_templates/{id}. Both verbs are served by the same partial-update handler, accept the same UpdateEmailTemplateRequest body, and return 200 with the updated template.
curl
Updatable fields: name, subject, html_body, text_body, variables, strict_variables, autoescape, variable_schema. When you change subject or a body and don’t supply variables, the variable list is auto-extracted again from the new content. Set variable_schema to null explicitly to clear the schema; required variables cannot define defaults, and an entry that does (or a non-boolean required value) returns 422.
PUT is not a full replacement. PUT and PATCH are routed to the same handler, and both update only the fields you send — omitted fields are left unchanged on either verb. Sending PUT with a partial body will not clear the fields you left out. To clear a field, send it explicitly as null.

Delete a template

Delete a template with DELETE /email_templates/{id}:
curl
Returns 204 No Content with an empty body. Deleting a template does not affect messages already sent from it; it only prevents future sends and renders that reference the template_id.

Variables

Template content uses Liquid syntax. The renderer is the Solid Liquid engine.

What’s substitutable

Variable substitution applies to all three content fields — subject, html_body, and text_body. Each is rendered independently from the same template_variables map.

Syntax

Missing-variable behavior

Missing-variable behavior depends on the template’s strict_variables flag (default false): In the default legacy mode, a missing variable uses its optional variable_schema default when one is declared; without such a default, it renders as an empty string and does not raise an error. For example, if your template contains {{first_name}}, no schema default exists for first_name, and you send template_variables: {} (or omit the key entirely), the rendered output has an empty string in place of {{first_name}}, and the render or send succeeds. This is by design: templates degrade gracefully so a partial variable set never blocks a send. To enforce that a variable is present instead, enable strict variable validation — or validate the rendered output with the render endpoint before sending, or check your template_variables map in application code.

Auto-extraction limits

When you omit variables, Telnyx extracts the list from your template content with a small set of regexes. It recognizes exactly three shapes:
  • Unfiltered output tags — {{first_name}} → first_name
  • Dot-notation roots in unfiltered output tags — {{user.name}} → user
  • For-loop collections — {% for item in items %} → items (the loop variable item is extracted only when separately referenced in an unfiltered output tag such as {{item.title}})
{% assign %} targets are excluded, since the template creates them rather than receiving them. Anything else is not extracted. Most importantly, an output tag containing a filter is skipped entirely:
A filtered expression like {{user_input | escape}} renders correctly at send time but is not auto-extracted, so variables comes back empty for it. Because variables is descriptive metadata only, this never breaks a send — but it does make the template under-report what a caller must supply. Pass variables explicitly whenever your template uses filters, if/unless conditions, or other complex Liquid expressions.

Escaping and HTML safety

By default the renderer substitutes values as-is — it does not HTML-escape variables. If a variable value can contain untrusted content (for example, user-generated display names), either enable the template’s autoescape flag (recommended), escape the value in your application before passing it in template_variables, or use Liquid’s escape filter in the template: {{user_input | escape}}.

Strict variable validation

Templates default to the legacy behavior: missing variables without schema defaults render as empty strings. Opt in to strict validation with two per-template settings, both of which persist on the template and appear in every response: Each variable_schema entry has exactly two properties: variable_schema is independent of the legacy variables array — variables stays descriptive metadata, while variable_schema drives strict-mode enforcement and optional defaults. There are no other typed-variable features: entries carry only the boolean required and the optional string default. When strict_variables: true, a required variable is missing when it is absent, null, an empty string "", an empty object {}, or an empty array []. Present values — including false and 0 — pass validation. Optional variables never fail; when one is absent and its entry declares a default, that default is used (values you supply always win over the default). Defaults apply in both strict and legacy modes, so an optional variable with a schema default renders that default even on a legacy template. Create a strict template with autoescaping in one request:
curl
Render it with all required variables supplied:
curl
200 OK — the absent optional support_note falls back to its schema default:
Omit a required variable and the render fails with 422 naming it:
curl
The same failure happens at send time: a single send returns 422 and no message is persisted; in a batch, the failed item reports a per-item unprocessable_entity error naming the variable while the rest of the batch still processes (see Batch sending). Explicit Liquid default filters keep working in strict mode — only variables marked required: true in variable_schema are enforced.

HTML autoescaping

Set autoescape: true on a template to have Telnyx escape rendered variable output for you. Both flags default to false, so existing templates keep the legacy no-escape behavior until you opt in. What autoescape does — and does not — do:
  • Escapes only the html_body expression output, at the output boundary: after each expression’s filters run, and before the result is concatenated with the template’s literal markup.
  • Never escapes subject or text_body — they are never autoescaped in any mode.
  • Never mutates your input template_variables values.
  • Passes every html_body expression output through a final idempotent escape boundary, including expressions that already apply escape or escape_once. Existing escaped entities are preserved rather than double-escaped, while raw markup introduced by a later filter is escaped at emission.
With autoescape: true and template_variables: {"user_input": "<script>alert(1)</script>"}, the html_body {{user_input}} renders as &lt;script&gt;alert(1)&lt;/script&gt;; the same expression in text_body renders the raw value unchanged.

Render and preview

POST /email_templates/{id}/render renders a template with a set of variables and returns the Liquid-rendered, pre-send content — without sending anything. This is the key capability that makes templates safe for automated and agent-driven workflows: you can inspect the rendered output, run assertions against it, and catch malformed variables before a single message goes out.
The render output is the Liquid-rendered template. The send pipeline subsequently applies CSS inlining (when inline_css: true), click-link rewriting, and open-tracking pixel injection. Final recipient HTML may therefore differ from the preview. Use render to validate template structure and variable substitution, not as a byte-identical preview of the delivered message.

Request

Supply template_variables in the body. The body itself is optional — if you omit it, rendering starts with an empty caller-supplied variable set and then applies any optional variable_schema defaults.
curl

Response

A successful render returns 200 with the full template object plus the rendered subject, html_body, and text_body replacing the template-string versions:
The response merges the stored template (record type, id, name, variables, strict/autoescape settings, timestamps) with the rendered output. Compare the rendered subject/html_body/text_body against the stored Liquid versions to confirm substitution worked.

Errors

A 422 response from the standalone render endpoint uses the standard error envelope:
Note that on a legacy template (strict_variables: false, the default) missing variables are not an error — optional schema defaults apply in both modes, and other missing variables render as empty strings. On a strict template, a missing required variable is a 422. A 422 from the standalone render endpoint means the Liquid itself is syntactically broken, a filter threw, or a strict-mode required variable was missing; it uses the 10015 standard error envelope shown above.

Use cases

1

Preview UIs

Build a preview pane in any email composer by calling render with draft variables. Because the response is plain JSON, you can render server-side and drop the HTML straight into an <iframe> or your preview component — no client-side Liquid engine needed. The Liquid-rendered output matches the send path’s variable substitution; note that the send pipeline may further modify HTML (CSS inlining, tracking pixel) before final delivery.
2

Agent content validation

When an AI agent generates variable values, render the template before sending to confirm the output is well-formed — that names aren’t blank, that loop bodies populated, that no stray Liquid tags leaked into the rendered text. Treat the render call as a gate: only send once the rendered subject and body pass your assertions.
3

CI checks on template changes

Call render in a test or CI step against a fixture variable set whenever a template changes. A non-empty errors array, or a rendered body that diverges from a recorded snapshot, fails the check before the template reaches production.

Send with a template

Send using a stored template with POST /email_messages. Pass template_id and template_variables; the API renders the template server-side and sends the result. You only supply the addressing — from, to, and any send options like scheduled_at or inline_css. For safe retries, pass an Idempotency-Key HTTP header rather than a request-body field.
curl
When you send with template_id, subject is optional — the template’s rendered subject is used. If the template has no subject or it renders to an empty string, the request returns 400.

Precedence: template content and inline content are mutually exclusive

You cannot mix a template with inline content. If the request contains template_id and any of subject, html_body, or text_body, the API returns 400 with:
There is no merge or override — the presence of template_id means the template fully governs subject, html_body, and text_body. Other top-level fields (from, to, cc, bcc, reply_to, attachments, headers, tags, metadata, scheduled_at, inline_css) combine with the template normally.
Use scheduled_at to schedule a templated send. send_at is a deprecated alias kept for backward compatibility. When scheduled_at is non-null, it takes precedence over send_at; when scheduled_at is omitted or null, send_at is used. Responses always report the field as scheduled_at.
Want to preview before sending? Call POST /email_templates/{id}/render with the same template_variables, inspect the output, then send. The render and send paths use the same Liquid engine and the same template, so the variable substitution matches. Note that the send pipeline may apply additional transformations (CSS inlining, click-link rewriting, open-tracking pixel) before final delivery.

Render errors at send time

If the template’s Liquid fails to render at send time, the request returns 422 with the send endpoint’s non-standard {code: "render_error", message: "..."} error shape. This differs from the standalone render endpoint, which returns 10015 with title and detail. Missing variables that resolve to optional schema defaults or empty strings still allow the message to send — only a genuine Liquid parse or filter failure blocks the send.
The exception is a strict template: when strict_variables is enabled and a required variable is missing, the send returns 422 naming the variable and no message is persisted. In a batch, that item reports a per-item unprocessable_entity error and the rest of the batch still processes.

inline_css with templates

inline_css is a send-time option on POST /email_messages, not a template property. When you set inline_css: true on a send that uses a template, the rendered HTML body has its <style> blocks inlined into element style attributes before the message is sent. This works the same way whether you send inline HTML or via a template.