Templates
Create, version, and publish email templates with approval workflows and lint checks.
Templates define the structure and content of your outbound emails. Each template has versioned content, automatic linting, and optional approval workflows.
Creating a template
molted templates create \
--name "Order Confirmation" \
--slug order-confirmation \
--type transactional \
--subject "Your order {{orderId}} has been confirmed" \
--body "<h1>Order Confirmed</h1><p>Hi {{customerName}}, your order is on its way.</p>" \
--text "Order Confirmed. Hi {{customerName}}, your order is on its way." \
--variables '[{"name":"orderId","type":"string","required":true},{"name":"customerName","type":"string","required":true}]' \
--auto-publishcurl -X POST https://api.molted.email/v1/templates \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"tenantId": "tenant-abc",
"slug": "order-confirmation",
"name": "Order Confirmation",
"type": "transactional",
"content": {
"subjectTemplate": "Your order {{orderId}} has been confirmed",
"htmlTemplate": "<h1>Order Confirmed</h1><p>Hi {{customerName}}, your order is on its way.</p>",
"textTemplate": "Order Confirmed. Hi {{customerName}}, your order is on its way.",
"variables": [
{ "name": "orderId", "type": "string", "required": true },
{ "name": "customerName", "type": "string", "required": true }
]
},
"autoPublish": true
}'Request body
| Field | Type | Required | Description |
|---|---|---|---|
tenantId | string | Yes | Your tenant identifier. |
slug | string | Yes | URL-friendly identifier (e.g. order-confirmation). |
name | string | Yes | Human-readable template name. |
type | string | No | transactional or marketing. Defaults to marketing. |
approvalRequired | boolean | No | Whether publishing requires approval. Default false. |
content | object | No | Initial template content (see below). |
autoPublish | boolean | No | If true and content is provided, publishes immediately after creation. Cannot be combined with approvalRequired -- see note below. |
autoPublish and approvalRequired interaction: If you set both autoPublish: true and approvalRequired: true, the template is created with approval required and the publish flow is triggered automatically -- but because approval is required, the result is a pending_approval status, not an immediate publish. The version will not go live until it is explicitly approved. If you need immediate publishing, do not set approvalRequired.
Content fields
| Field | Type | Required | Description |
|---|---|---|---|
subjectTemplate | string | Yes | Subject line with variable placeholders. |
htmlTemplate | string | Yes | HTML body with variable placeholders. |
textTemplate | string | No | Plain text fallback body. |
variables | array | No | Variable definitions for validation (see schema below). |
Variables use double curly brace syntax: {{variableName}}.
Variables schema
The variables array defines the expected variables for a template. Each entry is an object with:
| Field | Type | Required | Description |
|---|---|---|---|
name | string | Yes | Variable name matching the {{name}} placeholder in the template. |
type | string | Yes | Data type: string, number, boolean, or date. |
required | boolean | Yes | Whether the variable must be provided in the send payload. |
defaultValue | any | No | Fallback value when the variable is not provided. |
description | string | No | Human-readable description of the variable's purpose. |
[
{ "name": "customerName", "type": "string", "required": true, "description": "Recipient's display name" },
{ "name": "orderId", "type": "string", "required": true },
{ "name": "showPromo", "type": "boolean", "required": false, "defaultValue": false }
]Variables are validated during template linting. If a {{placeholder}} in the template body does not have a matching entry in variables, linting flags an undeclared_variable error. Templates with lint errors are blocked from sending by the policy engine.
To fix an undeclared_variable error, add the missing variable to --variables when creating the template or when adding a new version:
molted templates create \
--name "Welcome" --slug welcome \
--subject "Welcome, {{name}}!" \
--body "<h1>Hello {{name}}</h1>" \
--variables '[{"name":"name","type":"string","required":true}]' \
--auto-publishIf you omit --variables and your template uses {{placeholders}}, the template is created but lint fails. At render time:
- If the placeholder name appears in the render payload, the value is substituted from the payload so ad-hoc renders still work. The template still failed lint, so it cannot be sent through the policy engine.
- If the placeholder is missing from the payload too, the render response includes an
errorsentry such as"Unresolved variable \"name\" is used in the template but not declared and not in the payload", and the placeholder is substituted with an empty string. Send requests through this template are blocked with reasonrender_failed.
Always declare your variables with --variables so lint passes and the policy engine enforces required fields consistently.
Listing templates
molted templates listcurl https://api.molted.email/v1/templates?tenantId=tenant-abc \
-H "Authorization: Bearer YOUR_API_KEY"Returns templates with their current version number and lint status.
IDs vs slugs
get and render accept either the template ID (UUID) or slug. Slugs are scoped to the tenant resolved from your API key. add-version, publish, and approval routes still require the ID (UUID).
The send command also accepts either form via the templateId field. Slugs are the recommended convention for sending because they are human-readable.
To find a template's ID or slug, use list and look for the id and slug fields in the response.
Getting a template
molted templates get TEMPLATE_IDcurl https://api.molted.email/v1/templates/TEMPLATE_ID \
-H "Authorization: Bearer YOUR_API_KEY"TEMPLATE_ID may be either the UUID or the slug. Slug lookups are scoped to your tenant.
Returns the template with its current version's full content (subject, HTML, text templates, variables, and lint results).
Versioning
Templates are versioned. Each update creates a new version that must be published before it takes effect.
Adding a new version
molted templates add-version TEMPLATE_ID \
--subject "Updated: Your order {{orderId}}" \
--body "<h1>Order Update</h1><p>Hi {{customerName}}.</p>" \
--variables '[{"name":"orderId","type":"string","required":true},{"name":"customerName","type":"string","required":true}]'curl -X POST https://api.molted.email/v1/templates/TEMPLATE_ID/versions \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"subjectTemplate": "Updated: Your order {{orderId}}",
"htmlTemplate": "<h1>Order Update</h1><p>Hi {{customerName}}.</p>",
"variables": [
{ "name": "orderId", "type": "string", "required": true },
{ "name": "customerName", "type": "string", "required": true }
]
}'New versions are automatically linted on creation. The lint results are stored with the version.
Publishing a version
molted templates publish TEMPLATE_IDcurl -X POST https://api.molted.email/v1/templates/TEMPLATE_ID/publish \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "requestedBy": "operator@example.com" }'If the template has approvalRequired: true, this creates a pending approval instead of publishing directly:
{
"status": "pending_approval",
"approvalId": "...",
"versionId": "..."
}Otherwise it publishes immediately:
{
"status": "published",
"versionId": "..."
}Reviewing approvals
molted templates approve APPROVAL_ID --reviewed-by "admin@example.com" --comment "Looks good"molted templates reject APPROVAL_ID --reviewed-by "admin@example.com" --reason "Subject too long"curl -X PATCH https://api.molted.email/v1/templates/approvals/APPROVAL_ID \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"reviewedBy": "admin@example.com",
"decision": "approved",
"comment": "Looks good"
}'Set decision to approved or rejected.
Test rendering
Preview how a template renders with sample data:
molted templates render TEMPLATE_ID --payload '{"customerName":"Jane","orderId":"456"}'curl -X POST https://api.molted.email/v1/templates/TEMPLATE_ID/render \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"payload": {
"customerName": "Jane",
"orderId": "456"
}
}'Returns the rendered subject, HTML, and text with variables substituted.
If the template exists but has no published version (for example, it was created without --auto-publish or a lint error blocked auto-publishing), render returns 422 Unprocessable Entity with a message pointing at molted templates publish <id>. This is distinct from 404 Not Found, which indicates the ID or slug doesn't exist.
Template linting
Every version is automatically linted when created. The lint results are stored with the version and determine whether the template can be used for sending.
Lint rules
| Rule | Severity | Description |
|---|---|---|
undeclared_variable | error | A {{placeholder}} is used in the template but not declared in variables. Fix: add the variable to --variables. System variables (see below) are exempt. |
unused_variable | warning | A variable is declared in variables but never referenced in the template. |
system_variable_redeclared | warning | A system variable was passed in variables[]. Remove it -- Molted auto-injects it at send time. |
spam_phrase | warning | Subject or body contains a spam-trigger phrase (e.g. "act now", "click here"). |
insecure_url | error | HTML contains http:// links. Use https:// instead. |
missing_unsubscribe | error | Marketing templates must include an unsubscribe link. Not required for transactional templates. |
missing_physical_address | warning | Marketing templates should include a physical mailing address (CAN-SPAM). Use {{physical_address}} or add one manually. |
Templates with any error-level lint result are blocked from sending by the policy engine with reason template_lint_failed. Warning-level results do not block sends.
System variables
Variables auto-injected by Molted at send time. Use them in templates without declaring them in variables[].
| Variable | Description |
|---|---|
{{unsubscribe_url}} | Per-recipient unsubscribe link. Use this in the marketing-template unsubscribe <a href> -- it satisfies missing_unsubscribe without tripping undeclared_variable. |
{{physical_address}} | The tenant's configured physical mailing address (CAN-SPAM). The send pipeline auto-appends a footer when missing. |
Default template
Every new tenant is provisioned with a _default transactional template. This template is used when you send without specifying a templateId.
Sends using a templateId value that does not match any template slug or ID are blocked by the policy engine with reason template_not_found.
If a template exists but rendering fails (for example, a required variable is missing from the send payload), the send is blocked with reason template_render_failed instead of sending a partially-populated email. The caller should re-submit with the missing variables in the payload.