{{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.
https://api.telnyx.com/v2 and an Authorization: Bearer *** header.
Create a template
Create a template withPOST /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:
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 withGET /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 withGET /email_templates/{id}:
curl
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 withPATCH /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
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 withDELETE /email_templates/{id}:
curl
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’sstrict_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 omitvariables, 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 variableitemis 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’sautoescape 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
curl
200 OK — the absent optional support_note falls back to its schema default:
422 naming it:
curl
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
Setautoescape: 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_bodyexpression 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
subjectortext_body— they are never autoescaped in any mode. - Never mutates your input
template_variablesvalues. - Passes every
html_bodyexpression output through a final idempotent escape boundary, including expressions that already applyescapeorescape_once. Existing escaped entities are preserved rather than double-escaped, while raw markup introduced by a later filter is escaped at emission.
autoescape: true and template_variables: {"user_input": "<script>alert(1)</script>"}, the html_body {{user_input}} renders as <script>alert(1)</script>; 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
Supplytemplate_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 returns200 with the full template object plus the rendered subject, html_body, and text_body replacing the template-string versions:
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:
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 withPOST /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 containstemplate_id and any of subject, html_body, or text_body, the API returns 400 with:
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 returns422 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.