Templates

Store reusable HTML and text bodies with {{variable}} placeholders and render them when a send is accepted.

A template stores html and text bodies with {{variable}} placeholders. A send references the template by id, supplies variables, and the API renders the bodies at accept time.

Templates have no subject field. Each send supplies its own subject in the send request.

Templates are managed through the API only: the Node SDK has no templates resource, and the dashboard has no templates page. Manage them with curl or any HTTP client. Sending with a template works through the SDK's emails.send and its template field.

Create a template

curl -X POST https://api.mailfully.com/v1/templates \
  -H "Authorization: Bearer mf_live_xxxxxxxxxxxx" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "order-shipped",
    "html": "<h1>Your order shipped</h1><p>Hi {{name}}, order {{order_id}} is on its way.</p>",
    "text": "Hi {{name}}, your order {{order_id}} has shipped.",
    "variables_schema": { "required": ["name", "order_id"] }
  }'

name is required, and at least one of html or text must be present. variables_schema is optional (see Required variables). The 201 response:

{
  "object": "template",
  "id": "tmpl_01J8ZXA3B4C5D6E7F8G9H0J1K2",
  "name": "order-shipped",
  "html": "<h1>Your order shipped</h1><p>Hi {{name}}, order {{order_id}} is on its way.</p>",
  "text": "Hi {{name}}, your order {{order_id}} has shipped.",
  "variables_schema": { "required": ["name", "order_id"] },
  "created_at": "2026-07-08T17:00:00.000Z",
  "updated_at": "2026-07-08T17:00:00.000Z"
}

Templates are org-scoped, not environment-scoped: test-mode and live-mode API keys of the same organization see and mutate the same templates.

Variable syntax

  • Placeholders are {{name}}; whitespace inside the braces is allowed, so {{ name }} also matches.
  • Keys may contain letters, digits, underscores, and dots: [A-Za-z0-9_.].
  • Keys are flat. {{user.name}} looks up the literal key "user.name" in the variables object. It never traverses into a nested object. Pass { "user.name": "Jo" }, not { "user": { "name": "Jo" } }.
  • Values substituted into the html body are HTML-escaped (& < > " ' become entities). Values in the text body are inserted raw.
  • Unknown, null, or missing variables render as an empty string, so the literal placeholder never leaks into a sent email.
  • Numbers and booleans are stringified; objects and arrays are JSON-encoded.
  • Substituted values are not re-scanned, so a {{...}} sequence inside a variable's value is emitted literally.

Send with a template

Reference the template in the send request's template field, with id and optional variables. A template counts as content, satisfying the one-of-html/text/template rule. The examples send to you@yourcompany.com. Swap in an address you own.

curl -X POST https://api.mailfully.com/v1/emails \
  -H "Authorization: Bearer mf_live_xxxxxxxxxxxx" \
  -H "Content-Type: application/json" \
  -d '{
    "from": "orders@mail.acme.com",
    "to": "you@yourcompany.com",
    "subject": "Your order shipped",
    "template": {
      "id": "tmpl_01J8ZXA3B4C5D6E7F8G9H0J1K2",
      "variables": { "name": "Ada", "order_id": "ord-1042" }
    },
    "tags": [{ "name": "category", "value": "order-shipped" }]
  }'

The response is the standard 202 acceptance:

{ "id": "01JZFYK2N8Q4T7W9E1R3M5X0AB" }

If the send also carries inline html or text, the template's rendered output replaces both unconditionally.

A template.id that doesn't exist for your organization fails the send with 404 not_found and the message Template not found.

Required variables

variables_schema is stored as-is and never validated at create or update time. At send time, exactly one part of it is enforced: the required string array. Each listed key must be present and non-null in template.variables, or the send is rejected:

{
  "error": {
    "type": "validation_error",
    "message": "Missing required template variable: order_id",
    "param": "variables.order_id"
  }
}

If variables_schema is null or has no required array, no variable validation happens at all: unknown placeholders render as empty strings.

When rendering happens

Templates render when the API accepts the send (at the 202, not at dispatch). The rendered html and text are persisted on the message, and the delivery worker sends that persisted content without re-rendering.

Consequences:

  • Editing a template never changes messages that are already queued, including sends scheduled days in the future with scheduled_at. They carry the content rendered at accept.
  • Deleting a template never affects accepted sends either. Only new sends referencing the deleted id fail, with 404 not_found.

Update and delete

PATCH /v1/templates/{id} is a partial update: omitted fields are untouched, and an explicit null clears html, text, or variables_schema. Every successful PATCH refreshes updated_at.

curl -X PATCH https://api.mailfully.com/v1/templates/tmpl_01J8ZXA3B4C5D6E7F8G9H0J1K2 \
  -H "Authorization: Bearer mf_live_xxxxxxxxxxxx" \
  -H "Content-Type: application/json" \
  -d '{ "text": "Hi {{name}} - order {{order_id}} shipped." }'

PATCH applies no cross-field check. Setting both html and text to null is legal and leaves a template that renders empty bodies on every future send. Create requires at least one body; update does not re-verify it.

DELETE /v1/templates/{id} is an immediate, hard delete:

curl -X DELETE https://api.mailfully.com/v1/templates/tmpl_01J8ZXA3B4C5D6E7F8G9H0J1K2 \
  -H "Authorization: Bearer mf_live_xxxxxxxxxxxx"
{
  "id": "tmpl_01J8ZXA3B4C5D6E7F8G9H0J1K2",
  "deleted": true
}

The id is not reserved after deletion, and future sends referencing it return 404 not_found.