---
title: "Templates"
description: "Store reusable HTML and text bodies with {{variable}} placeholders and render them when a send is accepted."
---

> **For AI agents:** the complete documentation index is at [llms.txt](/docs/llms.txt). Append `.md` to any page URL for its markdown version.

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.

<Note>
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.
</Note>

## Create a template

```bash
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](#required-variables)). The `201` response:

```json
{
  "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.

<Tabs>
<Tab title="curl">

```bash
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" }]
  }'
```

</Tab>
<Tab title="Node SDK">

```typescript
const { data, error } = await mailfully.emails.send({
  from: "orders@mail.acme.com",
  to: "you@yourcompany.com", // replace with an address you own
  subject: "Your order shipped",
  template: {
    id: "tmpl_01J8ZXA3B4C5D6E7F8G9H0J1K2",
    variables: { name: "Ada", order_id: "ord-1042" },
  },
  tags: [{ name: "category", value: "order-shipped" }],
});
```

</Tab>
</Tabs>

The response is the standard `202` acceptance:

```json
{ "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:

```json
{
  "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`.

```bash
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." }'
```

<Warning>
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.
</Warning>

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

```bash
curl -X DELETE https://api.mailfully.com/v1/templates/tmpl_01J8ZXA3B4C5D6E7F8G9H0J1K2 \
  -H "Authorization: Bearer mf_live_xxxxxxxxxxxx"
```

```json
{
  "id": "tmpl_01J8ZXA3B4C5D6E7F8G9H0J1K2",
  "deleted": true
}
```

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

## Related

- [`POST /v1/templates`](/api-reference/templates/create) and the rest of the [templates reference](/api-reference/templates/list)
- [`POST /v1/emails`](/api-reference/emails/send) — the `template` field on sends
- [Scheduled sends](/guides/scheduled-sends): how render-at-accept interacts with `scheduled_at`
