Docs Go to app →

Email templates

A template is the chrome around a sequence or broadcast email: colors, width, header, footer. The body itself stays in TipTap on the sequence or broadcast. There are three kinds:

KindHow you get itWhat recipients see
VisualDefault. html_source is null, design is the branded (or plain) preset.TipTap body rendered through MJML.
Text onlyReset the design to the plain preset. html_source is null.A text-first layout, still authored in TipTap.
Custom HTMLSet html_source to an HTML layout.Your HTML, with the TipTap body inserted at {{{content}}}.

Custom HTML is a layout wrapper, not a replacement for the body editor. Sequences and broadcasts keep writing the email in TipTap; Letter drops that rendered body into your layout at send time.

This is the option to use when you want your own header, footer, or 600px table chrome and still want the existing composer. It is available from the dashboard, the Management API, the CLI, and @letterapp/mcp.

Custom HTML tags

html_source is a string of HTML (inline, or a file via the CLI’s @path). Max 500 KB. Letter sanitizes it and inlines CSS before send.

{{{content}}} — required

The TipTap body is inserted here. {{content}} is accepted as an alias. Saving a layout without either tag is rejected:

Add a {{{content}}} tag where the email body should go.

Use the triple-brace form so the body HTML is not escaped.

{{unsubscribe_url}} — required in practice

Put the per-recipient unsubscribe URL in a link:

<a href="{{unsubscribe_url}}">Unsubscribe</a>

{{{unsubscribe}}} is an alternative: Letter replaces it with a ready-made <a href="…">Unsubscribe</a>.

If neither unsubscribe tag is present, Letter appends a footer with an unsubscribe link so the email stays compliant. Prefer putting {{unsubscribe_url}} in your own footer so the link matches your design. List-Unsubscribe headers are always added by the renderer; you do not put those in the HTML.

”Sent with Letter” badge

Letter appends the same “Sent with Letter” pill used on Visual and Text-only templates, after your layout (and after any fallback unsubscribe footer). Hide it the same way as the other kinds:

  • Dashboard: Show “Sent with Letter” in the template editor.
  • API: "hide_badge": true on create/update, or design.hideBadge.
  • CLI: --hide-badge true.
  • MCP: hide_badge on create_html_template / update_html_template.

Hiding the badge is a paid-plan perk.

Minimal layout

<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1" />
</head>
<body>
  <table role="presentation" width="100%" cellpadding="0" cellspacing="0">
    <tr>
      <td align="center">
        <table role="presentation" width="600" cellpadding="0" cellspacing="0">
          <tr>
            <td>
              {{{content}}}
            </td>
          </tr>
        </table>
        <p>
          You're receiving this because you signed up.
          <a href="{{unsubscribe_url}}">Unsubscribe</a>.
        </p>
      </td>
    </tr>
  </table>
</body>
</html>

CLI

--html-source accepts inline HTML or @path to a file. Any string flag accepts @path; this is how you ship a real layout.

# Print the tag contract (also `letter templates schema --json`).
letter templates schema

letter templates create --name "Launch layout" --html-source @layout.html
letter templates create --name "Launch layout" --html-source @layout.html --hide-badge true
letter templates list --json
letter templates update tpl_123 --html-source @layout.html
letter templates update tpl_123 --hide-badge true
letter templates update tpl_123 --html-source null   # back to Visual / Text-only

See the CLI for the full command list.

Management API

Authenticate with a workspace PAT (lt_pat_*). Full reference: Templates. Machine-readable: /v1/openapi.json.

POST /v1/projects/{slug}/templates
Authorization: Bearer lt_pat_…
Content-Type: application/json

{
  "name": "Launch layout",
  "html_source": "<!DOCTYPE html>…{{{content}}}…<a href=\"{{unsubscribe_url}}\">Unsubscribe</a>…",
  "hide_badge": false
}

PATCH the same fields. Set "html_source": null to switch the template back to Visual / Text-only.

A Template looks like:

{
  "id": "<uuid>",
  "name": "Launch layout",
  "is_default": false,
  "design": { },
  "html_source": "<!DOCTYPE html>…{{{content}}}…{{unsubscribe_url}}…",
  "created_at": "…",
  "updated_at": "…"
}

html_source is null on Visual and Text-only templates.

MCP

@letterapp/mcp (0.4+) exposes these tools so an agent can create a layout without shell-parsing the CLI. They use the workspace PAT from letter login (~/.letter/credentials.json) or LETTER_PAT. The project ingestion key (LETTER_API_KEY / lt_live_*) cannot create templates.

ToolWhat it does
html_template_guideReturns this contract (tags, examples, auth). Start here.
list_templatesTemplates in the connected project (kind is html or visual).
get_templateOne template, including html_source.
create_html_templateCreate a custom HTML layout. html_source must include {{{content}}}. Optional hide_badge.
update_html_templateUpdate html_source, name, and/or hide_badge. Pass html_source: null to clear.

Optional project on every tool overrides the connected project / LETTER_PROJECT.

What custom HTML does not change

  • Sequence and broadcast bodies stay TipTap (bodyDoc / --body-md).
  • Personalization variables in the body still work.
  • Transactional POST /v1/send is a different path: it takes raw html / text and does not use these templates. See Transactional email.
  • MJML chrome is skipped for custom HTML layouts. The “Sent with Letter” badge is still appended unless you hide it.