Skip to content

Send from a template

Create and publish reusable email templates, manage revisions and send with variables.

Create and publish a Mail template to reuse a subject and body with values supplied at send time. Your application then sends the template ID and variables instead of keeping the wording in its code.

Before you start

You need
Why
Where it comes from
A product with Mail available
A template belongs to exactly one product
A credential for the Mail API
Managing templates is an authenticated call to https://api.lepsto.com
Only needed for the last step, the send itself

Use different credentials for template management and public sending:

Managing templates
Sending from one
Host
https://api.lepsto.com
https://public.lepsto.com/{product_id}/mail
Credential
A session token or an lsk_ API key, as Authorization: Bearer — see API and keys
The product public key (lpk_) — see authenticate the public API
Covers
Creating, editing, publishing, rollback, archiving
POST /emails and GET /emails/{id}

Authenticate the management examples below on https://api.lepsto.com. The final send uses the public host and a public key. Neither credential replaces the other.

Management calls select a product through the credential. The examples omit product selection and use the credential’s default product:

Credential
The product it acts on
Selecting another
A static API key (lsk_…)
The product the key belongs to
Not possible. Naming a different product with X-Product-Id returns 403, even if the key’s creator belongs to that product.
A session token, including one exchanged from an API key
The session’s default product
X-Product-Id may select another product the caller belongs to. Without the header the default stays in effect.

If your session can access several products, set X-Product-Id explicitly. Start with a read-only template listing to confirm which product you are working in:

curl "https://api.lepsto.com/mail/templates" \
  -H "Authorization: Bearer $LEPSTO_API_CREDENTIAL" \
  -H "X-Product-Id: $LEPSTO_PRODUCT_ID"

You can add X-Product-Id to every management example below. Without it, each call uses the credential’s default product.

This optional header applies to management calls. Public sends select their product with {product_id} in the URL; that value does not select the product for a management call. See API and keys for both contracts.

Create, publish and send: the short path

Create a draft, publish it, then send from its live revision. Edit and publish again whenever you need different wording:

  1. Create a draft. Call mail_template_create, or POST https://api.lepsto.com/mail/templates, with a name, subject, HTML and variable declarations. Check status: "draft", currentRevision: null and draftRevision: 1. Save the permanent template id.
  2. Publish the draft. Call mail_template_publish, or POST https://api.lepsto.com/mail/templates/{id}/publish. Check status: "published" and the new currentRevision. Only published templates can send.
  3. Send a message. Use the public-key request below, or mail_email_send through MCP, with a template object. The public request needs a public key and a verified sender domain. Check the returned message ID.
  4. Update the wording when needed. PATCH creates or edits a draft; publishing makes it live. Sends between those operations still use the previous live revision.

Where templates are edited

Open Mail → Templates to list templates and inspect one. Its detail page shows status, variables, a body preview and HTML source. Revisions lists history newest first, with revision number, state, author, creation time and publication time. Live identifies the revision used for sends, Draft the open draft and Superseded the other revisions.

The workspace offers deletion, the Archived switch and rollback. Use MCP or REST to create or edit content, publish a draft or discard it. The available operations are:

REST (on https://api.lepsto.com)
MCP tool
What it does
POST /mail/templates
Creates a template. Its first revision is an open draft.
GET /mail/templates
Lists templates. Archived ones are hidden unless include_archived=true.
GET /mail/templates/{id}
Reads one template, with its usage rollup.
PATCH /mail/templates/{id}
Edits it. Content lands in the open draft.
POST /mail/templates/{id}/publish
Makes the open draft live.
GET /mail/templates/{id}/revisions
The revision history, oldest first.
GET /mail/templates/{id}/revisions/{n}
One revision’s content.
POST /mail/templates/{id}/revisions/{n}/restore
Makes an earlier revision live again.
DELETE /mail/templates/{id}/draft
Throws the open draft away.
POST /mail/templates/{id}/archive
Hides it from listings and pickers.
POST /mail/templates/{id}/unarchive
Puts it back.
DELETE /mail/templates/{id}
Removes the template and its history.

Create a template

Use the Acme receipt example below. Replace the sender domain, recipient, product ID, credentials, template ID and order values with your own. Management calls need the credential described above.

curl -X POST "https://api.lepsto.com/mail/templates" \
  -H "Authorization: Bearer $LEPSTO_API_CREDENTIAL" \
  -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",
    "archivedAt": null,
    "currentRevision": null,
    "draftRevision": 1,
    "createdAt": "2026-08-02T09:00:00.000Z",
    "updatedAt": "2026-08-02T09:00:00.000Z"
  },
  "success": true
}

Supply nonempty name, subject and html. Include text to control the plain-text body; if omitted, Mail derives it from the rendered HTML. Creation always produces a draft.

Check the result: a new template has status: "draft", currentRevision: null and draftRevision: 1. Publish before using it in a send.

Save the template id. Edits, publications, rollback and archiving keep this identifier.

Declare the variables

Declare each variable’s name and type, plus optional and fallback when needed:

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

Declare at most 20 variables, with unique names matching [A-Za-z_][A-Za-z0-9_]*. Start a name with a letter or underscore; subsequent characters may also be digits.

Choose string, number or boolean for type. Send values as strings, numbers or booleans. Rendering inserts their text representation, so 42 and true render as 42 and true regardless of the declaration’s descriptive type.

If you omit a value, its declaration determines the result:

Declaration
With no value supplied
Has a fallback
The fallback is used, whatever optional says.
optional: true, no fallback
Renders as an empty string.
Neither
The send is rejected.

A missing-value error reports all absent required variables together:

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

Reserved names

Do not declare these four reserved variables:

Name
Filled with
FIRST_NAME
the contact’s first_name property
LAST_NAME
the contact’s last_name property
EMAIL
the contact’s email address
UNSUBSCRIBE_URL
that contact’s personal unsubscribe link

Broadcasts fill these names for each audience contact; see send one message to a list of people. A direct send has no contact context, so pass their values yourself if you need them. An omitted reserved value renders empty and never fails the send. Declaring a reserved name returns an error such as 'FIRST_NAME' is a reserved field at creation or update.

Write the placeholders

Use double braces around a plain variable name in the subject, HTML or plain text. {{order_id}} and {{ order_id }} both insert the same value.

Braces containing conditions, loops or expressions remain literal text; Mail recognizes only plain names. An undeclared, non-reserved placeholder renders as an empty string.

Mail handles inserted values according to their destination:

Where
Escaping
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.
Subject
Not 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 body
Values are inserted literally, whether you wrote the text part yourself or it was derived from the HTML.

Publish a draft

Content is stored in numbered revisions, beginning with revision 1. Editing a published template opens the next number only when no draft exists. Further edits change that draft; they do not create more revisions. Publishing closes it, so the next edit starts a new number.

The template exposes both revision pointers:

  • currentRevision points to the live revision used for sends; it is null before first publication.
  • draftRevision points to the unpublished draft; it is null when no draft is open.

There is at most one open draft. Before first publication, status is draft; after any revision goes live, status remains published. Sends require a published template.

The template’s top-level subject, html, text and variables show the live revision. Before first publication, they show the only draft, so reading a newly created template returns the content you wrote.

Publish the open draft:

curl -X POST "https://api.lepsto.com/mail/templates/3c0f1b7e-6c2a-4f6d-b0a1-9d1e2f3a4b5c/publish" \
  -H "Authorization: Bearer $LEPSTO_API_CREDENTIAL"

Check the result: publication returns status: "published", sets currentRevision to the newly live revision and clears draftRevision to null. Repeating publish without an open draft changes nothing and returns the existing template.

Edit a published template

Send content changes with PATCH. Mail leaves the live revision alone and updates the open draft, copying the live revision into a new draft if necessary:

curl -X PATCH "https://api.lepsto.com/mail/templates/3c0f1b7e-6c2a-4f6d-b0a1-9d1e2f3a4b5c" \
  -H "Authorization: Bearer $LEPSTO_API_CREDENTIAL" \
  -H "Content-Type: application/json" \
  -d '{ "subject": "Your receipt for order {{order_id}}" }'
{
  "data": {
    "id": "3c0f1b7e-6c2a-4f6d-b0a1-9d1e2f3a4b5c",
    "name": "Order receipt",
    "subject": "Receipt for order {{order_id}}",
    "status": "published",
    "archivedAt": null,
    "currentRevision": 1,
    "draftRevision": 2
  },
  "success": true
}

Check the result: draftRevision becomes 2, while the returned subject still describes live revision 1. The edited subject is stored in revision 2. A send before publication uses revision 1; publish revision 2 to use the new wording.

Additional PATCH calls edit revision 2. Revision 3 starts only after publication closes that draft and a later edit needs a new one.

Renaming is immediate: name is not versioned and changing it opens no draft. A PATCH with both name and content renames immediately while keeping the content changes unpublished.

The workspace marks templates with pending edits using an Unpublished draft badge and identifies both the open revision and the revision used for sending.

Read the revision history

curl "https://api.lepsto.com/mail/templates/3c0f1b7e-6c2a-4f6d-b0a1-9d1e2f3a4b5c/revisions" \
  -H "Authorization: Bearer $LEPSTO_API_CREDENTIAL"
{
  "data": [
    {
      "revision": 1,
      "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 }],
      "live": true,
      "createdBy": "9b2c4d6e-0a1b-4c3d-8e5f-6a7b8c9d0e1f",
      "createdAt": "2026-08-02T09:00:00.000Z",
      "publishedAt": "2026-08-02T09:04:00.000Z"
    },
    {
      "revision": 2,
      "subject": "Your 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 }],
      "live": false,
      "createdBy": "9b2c4d6e-0a1b-4c3d-8e5f-6a7b8c9d0e1f",
      "createdAt": "2026-09-19T11:00:00.000Z",
      "publishedAt": null
    }
  ],
  "success": true
}

The API returns revisions oldest first, with their authored content. createdBy is the author’s identity or null if unknown. publishedAt records first publication; null identifies the open draft.

Use live to find the revision currently rendered by sends. It follows the template’s live pointer, so rollback can make an older revision live without changing publication timestamps.

Read one revision with GET /mail/templates/{id}/revisions/{n}. It uses the same shape, and a missing number returns 404. Preview in the workspace’s revision table shows that revision’s stored body with placeholders unrendered.

Roll back to an earlier revision

Restore the revision number you want sends to use:

curl -X POST "https://api.lepsto.com/mail/templates/3c0f1b7e-6c2a-4f6d-b0a1-9d1e2f3a4b5c/revisions/1/restore" \
  -H "Authorization: Bearer $LEPSTO_API_CREDENTIAL"

Check the result: currentRevision returns to 1, and subsequent sends render revision 1.

Restore changes only the live pointer. It creates no revision, alters no authored content and leaves history numbering unchanged. Revision 2 remains readable and can be restored later.

Restore returns these failures:

Case
Answer
The template has never been published — there is no live revision to roll back from
409 template_never_published
The template has no revision with that number
404
The revision named is the open draft. It has never been live, so making it live is publishing, not restoring
409 revision_is_open_draft — use publish instead

Restoring the already-live revision succeeds without changing anything.

In the workspace, select Make live and confirm to restore a previously published revision. The action is unavailable for the open draft.

Discard a draft

Delete an open draft to abandon unpublished changes:

curl -X DELETE "https://api.lepsto.com/mail/templates/3c0f1b7e-6c2a-4f6d-b0a1-9d1e2f3a4b5c/draft" \
  -H "Authorization: Bearer $LEPSTO_API_CREDENTIAL"
{ "data": { "discarded": true, "revision": 2 }, "success": true }

Check the result: the response confirms discarded: true and the removed revision number. The template has no open draft, and its live content remains unchanged.

Discard returns 404 when there is no draft. If it is the only revision because the template was never published, it returns 409 draft_is_only_revision; delete that template instead.

Archive a template

Archive a template to hide it from normal lists and pickers while keeping it available by ID:

curl -X POST "https://api.lepsto.com/mail/templates/3c0f1b7e-6c2a-4f6d-b0a1-9d1e2f3a4b5c/archive" \
  -H "Authorization: Bearer $LEPSTO_API_CREDENTIAL"

Archiving sets archivedAt to an ISO 8601 UTC timestamp. The template disappears from default GET /mail/templates results and pickers. Use include_archived=true to list it, or select Show archived in Mail → Templates. The workspace stores that choice in ?archived=1 so a shared link reproduces the list.

Unarchive clears archivedAt and restores normal listing. Repeating archive or unarchive in its existing state changes nothing. Reading, editing, publishing, revision history and rollback all remain available while archived.

An archived template still sends by ID. Its live revision continues to render. IDs stored in application code, queues or scheduled-send requests keep working.

Archiving changes visibility only. It does not retire a sending integration. Mail → Logs keeps archived templates in its template filter so their historical sends remain searchable.

To retire a template, stop new sends against its ID first. Check usage.email_count and usage.last_sent_at on GET /mail/templates/{id}. Deleting with DELETE /mail/templates/{id} removes the template and all revisions; new sends against that ID return 404. Already-created messages keep their content, as described below.

Send from a template

Send a template object containing the published template ID and variable values:

curl -X POST "https://public.lepsto.com/$PRODUCT_ID/mail/emails" \
  -H "Authorization: Bearer $LEPSTO_PUBLIC_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" }
    }
  }'
{ "data": { "id": "9f1f0d9c-1c74-4a2e-9f9f-2b6d8a0b3c11" }, "success": true }

The template supplies its subject. Combining template with subject, html or text returns template is mutually exclusive with subject/html/text. Omit variables if none are needed. Recipients, reply_to, headers, tags and scheduled_at work as for inline messages, including scheduling.

In Mail → Emails → Send test, choose inline content or a published template. The template form creates fields for declared variables. It lists published, non-archived templates using the default template listing.

A missing template or one never published returns 404. Archiving does not prevent a send by ID.

Check the result: the send returns a message ID and uses the template’s live revision. Unpublished edits do not reach recipients.

What a template send freezes

Mail renders the template when it accepts the send request and stores the resulting subject and bodies on the message.

Already-created messages are not rendered again. Later edits, publication, rollback, archive or deletion leave their content unchanged. Scheduled messages follow the same rule: wording is fixed when you create them, even if delivery is weeks later.

Use the message record to check the exact wording sent. It preserves the rendered content independently of later changes to the template.

Next steps

Was this page helpful?
Esc

Start typing to search the docs.

↑↓navigate↵select