MOLTED EMAIL

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

CLI
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-publish
curl
curl -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

FieldTypeRequiredDescription
tenantIdstringYesYour tenant identifier.
slugstringYesURL-friendly identifier (e.g. order-confirmation).
namestringYesHuman-readable template name.
typestringNotransactional or marketing. Defaults to marketing.
approvalRequiredbooleanNoWhether publishing requires approval. Default false.
contentobjectNoInitial template content (see below).
autoPublishbooleanNoIf 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

FieldTypeRequiredDescription
subjectTemplatestringYesSubject line with variable placeholders.
htmlTemplatestringYesHTML body with variable placeholders.
textTemplatestringNoPlain text fallback body.
variablesarrayNoVariable 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:

FieldTypeRequiredDescription
namestringYesVariable name matching the {{name}} placeholder in the template.
typestringYesData type: string, number, boolean, or date.
requiredbooleanYesWhether the variable must be provided in the send payload.
defaultValueanyNoFallback value when the variable is not provided.
descriptionstringNoHuman-readable description of the variable's purpose.
Example
[
  { "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:

CLI
molted templates create \
  --name "Welcome" --slug welcome \
  --subject "Welcome, {{name}}!" \
  --body "<h1>Hello {{name}}</h1>" \
  --variables '[{"name":"name","type":"string","required":true}]' \
  --auto-publish

If 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 errors entry 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 reason render_failed.

Always declare your variables with --variables so lint passes and the policy engine enforces required fields consistently.

Listing templates

CLI
molted templates list
curl
curl 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

CLI
molted templates get TEMPLATE_ID
curl
curl 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

CLI
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
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

CLI
molted templates publish TEMPLATE_ID
curl
curl -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:

Pending approval
{
  "status": "pending_approval",
  "approvalId": "...",
  "versionId": "..."
}

Otherwise it publishes immediately:

Published
{
  "status": "published",
  "versionId": "..."
}

Reviewing approvals

Approve
molted templates approve APPROVAL_ID --reviewed-by "admin@example.com" --comment "Looks good"
Reject
molted templates reject APPROVAL_ID --reviewed-by "admin@example.com" --reason "Subject too long"
curl
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:

CLI
molted templates render TEMPLATE_ID --payload '{"customerName":"Jane","orderId":"456"}'
curl
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

RuleSeverityDescription
undeclared_variableerrorA {{placeholder}} is used in the template but not declared in variables. Fix: add the variable to --variables.
unused_variablewarningA variable is declared in variables but never referenced in the template.
spam_phrasewarningSubject or body contains a spam-trigger phrase (e.g. "act now", "click here").
insecure_urlerrorHTML contains http:// links. Use https:// instead.
missing_unsubscribeerrorMarketing templates must include an unsubscribe link. Not required for transactional templates.
missing_physical_addresswarningMarketing 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.