# Build emails from templates

> Versioned templates with typed variables, built from blocks or uploaded HTML, previewed, published and sent by id or alias.

Source: https://docs.minket.com/templates/
Updated: 2026-10-29

A template is an email the platform keeps for you: a subject, sender fields and content with `{{variables}}`, in numbered versions. A send names the template and passes the variables; the platform renders the published version, escapes every value for where it lands, and sends. The content is a [block document](https://docs.minket.com/templates/#block-documents) or [uploaded HTML](https://docs.minket.com/templates/#uploaded-html).

## Create a template

```sh tab=curl
curl https://api.minket.com/v1/templates \
  -H "Authorization: Bearer $MINKET_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Password reset",
    "alias": "password-reset",
    "subject": "Reset your password, {{name}}",
    "from": "Acme <account@mail.acme.example>",
    "document": {
      "version": 1,
      "blocks": [
        { "type": "heading", "level": "1", "text": "Reset your password" },
        { "type": "paragraph", "text": "Hi {{name}}, the link below works for **{{minutes}} minutes**." },
        { "type": "button", "label": "Choose a new password", "href": "{{reset_url}}" }
      ]
    },
    "variables": [
      { "key": "name", "type": "string", "fallback_value": "there", "required": false },
      { "key": "minutes", "type": "number", "fallback_value": 30, "required": false },
      { "key": "reset_url", "type": "string", "fallback_value": null, "required": true }
    ],
    "publish": true
  }'
```

```ts tab
await minket.templates.create({
  name: "Password reset",
  alias: "password-reset",
  subject: "Reset your password, {{name}}",
  from: "Acme <account@mail.acme.example>",
  document: {
    version: 1,
    blocks: [
      { type: "heading", level: "1", text: "Reset your password" },
      { type: "paragraph", text: "Hi {{name}}, the link below works for **{{minutes}} minutes**." },
      { type: "button", label: "Choose a new password", href: "{{reset_url}}" },
    ],
  },
  variables: [
    { key: "name", type: "string", fallback_value: "there", required: false },
    { key: "minutes", type: "number", fallback_value: 30, required: false },
    { key: "reset_url", type: "string", fallback_value: null, required: true },
  ],
  publish: true,
});
```

`html` takes uploaded markup instead of `document` (one or the other), with an optional `text` part; without one, the text part is derived from the HTML. `alias` is lowercase letters, digits, `-` and `_`, unique in the project; sends and the API take it wherever they take the id.

## Variables

| Form | Use |
|---|---|
| `{{name}}`, `{{contact.first_name}}` | Lowercase keys, dots for nested values. |
| `{{{NAME}}}` | Triple braces, any case, escaped the same way: they never mean raw HTML. |
| `{{{NAME\|there}}}` | An inline fallback, used when the send gives no value. |

Each variable has a `type` (`string`, `number` or `boolean`), a `fallback_value` of that type or null, and `required`. A send that leaves out a required variable without a fallback, or gives a value of the wrong type, is refused with `422 validation_error` naming it; keys the template does not declare are ignored. Every placeholder the content uses is declared when you save, as an optional string, so you only list the ones that need a type, a fallback or `required`.

Values are escaped for where they land: entity-escaped in HTML text and attributes, kept as they are in the text part, never a line break in the subject. In a URL, a placeholder at the very start is the URL (`{{reset_url}}`, or `{{base_url}}/reset`) and the result must be an `https:` or `mailto:` link (images: `https:`), checked once the value is in; a placeholder later in the URL is percent-encoded (`https://acme.example/r?token={{token}}`). A variable can never add markup, a link or a script.

Broadcasts and automations render for a contact, whose fields fill the variables: `{{email}}`, `{{first_name}}`, `{{last_name}}`, any property key (`{{plan}}`), the same under `{{contact.…}}`, and `{{unsubscribe_url}}` and `{{preferences_url}}`. Values the send passes win over the contact's, and a contact value of another type than the template declares leaves the fallback.

## Versions and publishing

Every update (`PATCH /v1/templates/{id}`) saves a new version and makes it `current_version`; `POST /v1/templates/{id}/publish` pins the current version as `published_version`. Sends use the published version, so you can edit and preview a draft while sends keep the published one; a send may pin its own with `template.version`. `GET /v1/templates/{id}/versions` lists the kept versions, newest first: the newest 50 and the published one.

`POST /v1/templates/{id}/duplicate` copies the current version into a new draft template without an alias. Deleting a template deletes its versions; sends that name it fail with `404 not_found`, and emails already sent are unaffected.

## Send with a template

```json
{
  "to": "jane@example.com",
  "template": { "id": "password-reset", "variables": { "name": "Jane", "reset_url": "https://acme.example/reset/3f9c" } }
}
```

The template supplies `subject`, `from`, `reply_to`, `html` and `text`; any of those in the request wins. A template that was never published is refused with `409 invalid_state` unless the send names a `version`.

## Previews

`POST /v1/templates/{id}/preview` renders a version (the current draft unless you pass `version`) with the variables you pass and returns `subject`, `html`, `text` and `missing`. A missing value shows as `[key]` and is listed in `missing` rather than refused, so you can preview before you have every value. The console shows the same preview in a sandboxed frame (no scripts, forms or navigation) with its own Content-Security-Policy.

## Block documents

Templates and broadcasts store content as a block document, which the platform renders to email-safe HTML (table layout, inline styles, the document's width as a cap and fluid below it, alt text on images, a dark-mode stylesheet for the clients that support one) and a plain-text part. Every value is escaped at render time; only an `html` block takes raw HTML, sanitized as [uploaded HTML](https://docs.minket.com/templates/#uploaded-html) is.

```json
{
  "version": 1,
  "settings": { "width": 600, "background": "#f4f4f5", "font": "system", "footer": "Acme, 1 Example Street, Amsterdam" },
  "blocks": [
    { "type": "heading", "level": "1", "text": "October update" },
    { "type": "paragraph", "text": "Hi {{first_name}}, here is what changed this month: **three** new features and one fix. [Release notes](https://acme.example/changelog)" },
    { "type": "image", "src": "https://acme.example/images/october.png", "alt": "The new dashboard", "width": 560 },
    { "type": "button", "label": "Open the dashboard", "href": "https://acme.example/app" },
    { "type": "divider" }
  ]
}
```

| Block | Fields |
|---|---|
| `heading` | `level` (`1`, `2`, `3`), `text`, `style` |
| `paragraph` | `text` with inline markup: `**bold**`, `_italic_`, `[label](https://url)`; `style` |
| `image` | `src`, `alt`, optional `href`, `width` (16 to 1200), `style` |
| `button` | `label`, `href`, `style` |
| `divider`, `spacer` | `style`; `spacer` has `height` (4 to 200) |
| `html` | `html`, sanitized at render |
| `columns` | 2 or 3 columns of up to 20 blocks each |

`style` takes `color` and `background` as hex, `align` and `padding`. `settings.width` is 320 to 800 (default 600), `font` is `system`, `serif` or `mono`, and on marketing mail the unsubscribe and preferences links follow `footer`.

## Uploaded HTML

Uploaded markup is usually React Email, compiled on your machine by the CLI or the SDK; the platform never runs your code. It is sanitized when you save it and again when it renders. The sanitizer keeps what email clients render: the document, `<head>`, `<style>` (rules and media queries), tables, text, links, images, inline styles, `class` and `data-` attributes, and Outlook's conditional comments with their VML. It removes scripts, event handlers (`onclick` and the rest), `javascript:`, `vbscript:` and non-image `data:` URLs, iframes, forms and their controls, `<object>`, `<embed>`, SVG and MathML, `<base>`, `<link>`, `<meta http-equiv="refresh">`, CSS `expression()`, `behavior`, `-moz-binding` and `@import`, and unsafe `url()`s in CSS. Links may be `https:`, `http:`, `mailto:`, `tel:` or relative; images `https:`, `http:`, `cid:` (inline attachments) or raster `data:` URLs.

```sh
minket templates push ./emails/receipt.tsx   # compiles React Email locally, uploads the HTML
```

## Keys

Templates belong to the project, shared by its environments and by live and test keys. Reading and managing them needs a `full_access` key; a `sending_access` key can send with a template but not read or change one. Test keys read, preview and send with templates in test mode, and never change them, since live mail uses the same templates ([API keys](https://docs.minket.com/keys/#scopes)). The sizes, counts and kept versions are in [Limits and retention](https://docs.minket.com/limits/).
