Skip to content

Mail

Send from a template

Keep a receipt or an invitation as a published template with named variables, and send it with an id and a handful of values.

A template is a subject and a body kept by Lessly Mail, with named placeholders you fill in at send time. You write it once, publish it, and from then on your sending code carries only a template id and a handful of values.

Templates are managed from your product’s authenticated session, not with a sending key from a client app. In the examples below, ... stands for the Mail API base URL that session uses. The one exception is sending an email from a template, which is an ordinary send and goes to the public sending endpoint.

Create a template

An agent calls mail_template_create; over REST it is POST /mail/templates on the Mail API.

curl -X POST ".../templates" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Order receipt",
    "subject": "Receipt for order {{order_id}}",
    "html": "<p>Hi {{FIRST_NAME}}, thanks for order {{order_id}}.</p>",
    "variables": [
      { "name": "order_id", "type": "string" }
    ]
  }'
{
  "data": {
    "id": "3c0f1b7e-6c2a-4f6d-b0a1-9d1e2f3a4b5c",
    "name": "Order receipt",
    "subject": "Receipt for order {{order_id}}",
    "html": "<p>Hi {{FIRST_NAME}}, thanks for order {{order_id}}.</p>",
    "text": null,
    "variables": [
      { "name": "order_id", "type": "string", "optional": false }
    ],
    "status": "draft",
    "createdAt": "2026-08-02T09:00:00.000Z",
    "updatedAt": "2026-08-02T09:00:00.000Z"
  },
  "success": true
}

name, subject and html are required and must not be empty. text is optional: supply it to control the plain-text part yourself, or leave it out and it is derived from the rendered HTML. A new template is always created as a draft.

The rest of the operations:

Declare the variables

A template declares its variables up front. Each declaration has a name, a type, an optional flag and an optional fallback:

{ "name": "plan", "type": "string", "optional": true, "fallback": "Free" }

A template may declare at most 20 variables. Names must be unique within the template and must match [A-Za-z_][A-Za-z0-9_]* — a letter or underscore first, then letters, digits and underscores.

type is one of string, number or boolean. It documents what the variable is meant to hold, and the values you may pass at send time are strings, numbers and booleans. The type does not change rendering: whatever you supply is written into the message as text, so true renders as true and 42 renders as 42.

optional and fallback decide what happens when you pass no value:

DeclarationWith no value supplied
Has a fallbackThe fallback is used, whatever optional says.
optional: true, no fallbackRenders as an empty string.
NeitherThe send is rejected.

Missing values are reported together, so one round trip tells you everything that is absent:

{ "name": "invalid_template_variables",
  "message": "invalid_template_variables: missing order_id, total",
  "missing": ["order_id", "total"] }

Reserved names

Four names are reserved and a template may not declare them.

NameFilled with
FIRST_NAMEthe contact’s first_name property
LAST_NAMEthe contact’s last_name property
EMAILthe contact’s email address
UNSUBSCRIBE_URLthat contact’s personal unsubscribe link

They are filled in for you when a message goes out to a list of contacts, one value per contact. In a direct send there is no contact, so you supply them yourself alongside your own variables if the template uses them. A reserved name never fails a send: with no value it renders as an empty string. Declaring one is rejected when the template is created or updated, with 'FIRST_NAME' is a reserved field.

Write the placeholders

A placeholder is a name in double braces, with or without spaces inside them: {{order_id}} and {{ order_id }} are the same thing. Placeholders work in the subject, in the HTML body and in the plain-text body.

Only plain names are recognised. Anything else in braces — a conditional, a loop, an expression — is not a placeholder and stays in the message exactly as written. A placeholder whose name the template does not declare, and which is not a reserved name, renders as an empty string.

Values are escaped for the place they land in:

WhereEscaping
HTML body&, <, >, " and ' are replaced by their HTML entities, so a value can never introduce markup. Ben & Jerry's renders as Ben &amp; Jerry&#39;s in the source and reads as Ben & Jerry's in the mail client.
SubjectNot HTML-escaped — a subject line is not markup. Line breaks inside a value are collapsed to a single space, so a value can never split the subject or add a header.
Plain-text bodyValues are inserted literally, whether you wrote the text part yourself or it was derived from the HTML.

Publish it

A template is either draft or published, and only a published template can be sent. An agent calls mail_template_publish; over REST, POST /mail/templates/:id/publish.

curl -X POST ".../templates/3c0f1b7e-6c2a-4f6d-b0a1-9d1e2f3a4b5c/publish"

The response is the template with status now published. Publishing is idempotent: publishing an already-published template changes nothing and returns it as it stands.

Only a draft can be edited. PATCH on a published template is refused:

{ "name": "template_not_editable",
  "message": "template 3c0f1b7e-6c2a-4f6d-b0a1-9d1e2f3a4b5c is published and cannot be edited" }

So a published template is a fixed thing your sending code can rely on. To change what a message says, create a new template — typically a copy with the new wording — publish it, and point your code at the new id. A template can be deleted in either state.

Send from it

Replace the inline body with a template object naming a published template and the values for its variables.

curl -X POST "https://public.lessly.dev/$PRODUCT_ID/mail/emails" \
  -H "X-Api-Key: $LESSLY_MAIL_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "from": "Acme <hello@mail.acme.com>",
    "to": "customer@example.com",
    "template": {
      "id": "3c0f1b7e-6c2a-4f6d-b0a1-9d1e2f3a4b5c",
      "variables": { "order_id": "4711", "FIRST_NAME": "Dana" }
    }
  }'

The subject comes from the template, so template is mutually exclusive with subject, html and text; sending both arms in one request is rejected. variables may be omitted when the template needs none. Everything else about the request — recipients, reply_to, headers, tags, scheduled_at — works exactly as it does for an inline send; see send a message.

An id that names no template of yours, or one that is still a draft, is a 404.

What a template send freezes

The template is rendered while your send request is being handled, and the resulting subject and bodies are stored with that message. Two things follow.

A message already created is final. It is not re-rendered later, so it is unaffected by anything you do to the template afterwards — including deleting the template. What went out is what the template said at the moment of the send, and your record of that message keeps showing exactly that. The same holds for a scheduled send, even one set for weeks ahead: its content is fixed when you create it, not when it goes out.

This is why publishing is one-way and published templates cannot be edited. Correcting a live template would otherwise change what some messages say and not others, depending on when each one was created. For a receipt that is the whole point: publishing a new template instead keeps every message traceable to the exact wording it carried.

Next steps

Was this page helpful?
Esc

Start typing to search the docs.

navigateselect