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
Template management endpoints (get, add-version, publish, render) require the template ID (a UUID returned when you create the template). Slugs do not work with these endpoints.
The send command is different -- it accepts either a template slug or ID via the templateId field. Slugs are the recommended convention for sending because they are human-readable.
To find a template's ID, use list and look for the id field 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 must be the template's UUID, not its slug.
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. |
unused_variable | warning | A variable is declared in variables but never referenced in the template. |
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.
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.