Templates API

Write an email once, with variables, and send it by id or alias from your code. Templates follow Resend's Templates API, so code written with a Resend SDK works after you change the base URL.

How templates work

A template has a draft and a published version. Creating or editing a template only changes the draft. Publishing copies the draft into the published version, and every send that names the template renders the published version. So you can edit a template that is live in your app, send yourself a test from the dashboard, and publish when it is right.

A template can set a default from, subject and reply_to. A send can override each of them. The body is HTML, or Markdown, which SMTPfast renders into a clean email layout.

Reading templates needs the email:read scope; creating, editing, publishing and deleting them needs email:send, the same split as broadcasts.

Variables

Write a variable as {{{KEY}}} anywhere in the subject, preview text or body, and declare it with a type and an optional fallback value. You can also write a fallback at the point of use: {{{PLAN|free}}}.

html
<p>Hi {{{NAME|there}}},</p>
<p>Your order of {{{PRODUCT}}} comes to {{{PRICE}}}.</p>
<p><a href="{{{RESEND_UNSUBSCRIBE_URL}}}">Unsubscribe</a></p>
  • A variable resolves to the value passed in the send, then its inline fallback, then its declared fallback_value. A declared variable with neither must be given a value: a send that leaves it out answers 422 and names it in missing_variables, before anything is charged or queued. The dashboard preview and test send show it empty instead.
  • Values are HTML-escaped in the HTML body, so a value can never add markup to your email.
  • Keys use letters, digits and underscores, up to 50 characters, and a template can declare up to 50. Types are string and number; a fallback must match its type.
  • FIRST_NAME, LAST_NAME, EMAIL, UNSUBSCRIBE_URL, RESEND_UNSUBSCRIBE_URL, contact and this are reserved. The first three are contact fields: a broadcast fills them from the contact, and a transactional send fills them from the variables you pass under those names.
  • {{{RESEND_UNSUBSCRIBE_URL}}} and {{unsubscribe_url}} become each recipient's own unsubscribe link.

Send with a template

Pass a template object to POST /v1/emails or to each item of POST /v1/emails/batch, with the template id or alias and the variables.

bash
curl -X POST https://smtpfa.st/api/v1/emails \
  -H "Authorization: Bearer $SMTPFAST_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "to": "[email protected]",
    "template": {
      "id": "order-confirmation",
      "variables": { "PRODUCT": "Desk lamp", "PRICE": 49 }
    }
  }'
javascript
const res = await fetch("https://smtpfa.st/api/v1/emails", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.SMTPFAST_API_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    to: "[email protected]",
    template: { id: "order-confirmation", variables: { PRODUCT: "Desk lamp", PRICE: 49 } },
  }),
});
const { id } = await res.json();
  • Only a published template can be sent. A template that was never published answers 422.
  • Every variable that has no fallback and is used without an inline fallback needs a value. If one is missing, the send answers 422 with the keys in missing_variables.
  • html, text and react cannot be sent together with a template.
  • from, subject and reply_to in the request win over the template's defaults. A subject in the request is rendered with the same variables.
  • After rendering, the email is an ordinary send: the sender domain must be verified, suppressed recipients are dropped, plan limits and credits apply, and an Idempotency-Key works as usual. A retry with the same key replays the first email, even if the template was published again since.

Start a broadcast from a template

Pass template_id when you create a broadcast, or choose Start from a template in the broadcast editor. The template's current content, as the template editor shows it, is copied into the new draft, and declared fallbacks are written into the tags so the broadcast renders the same. Any field you send alongside wins. A broadcast still needs an unsubscribe link before it can be sent.

json
POST /v1/broadcasts
{
  "template_id": "product-update",
  "name": "October update",
  "segment_id": "seg_customers"
}

Starter gallery

The Templates page in the dashboard has 10 starters you can copy in one click. They use table layout and inline styles, fit a 600px column, adapt to dark mode in the clients that support it, and need no external fonts or images.

StarterAliasWhat it is for
WelcomewelcomeA warm first email with one clear next step and a short getting-started list.
Email verificationverify-emailA one-time code with a magic link button, for sign-up checks or passwordless sign-in.
Password resetpassword-resetA reset link with an expiry, plus the request details so a reader can spot a bad actor.
ReceiptreceiptA clear payment receipt: amount up top, line items, totals and an invoice link.
Product updateproduct-updateA newsletter with one headline feature and a short list of smaller changes.
Weekly digestweekly-digestThree headline numbers and the week's top items, for a recurring summary.
Event invitationevent-invitationA calendar-style date tile, the essentials at a glance, and RSVP plus add-to-calendar buttons.
Re-engagementre-engagementA friendly nudge for quiet accounts: what changed, one button back, and an honest way out.
Team invitationteam-inviteAn invite to join a workspace, showing who invited them and the role they will get.
Usage alertusage-alertA plan limit warning with a progress bar, what happens next, and an upgrade path.

Template object

json
{
  "object": "template",
  "id": "clx_tpl123",
  "current_version_id": "b2693018-7abb-4b4b-b4cb-aadf72dc06bd",
  "alias": "order-confirmation",
  "name": "Order confirmation",
  "status": "published",
  "published_at": "2026-10-03T09:12:00.000Z",
  "created_at": "2026-10-01T08:00:00.000Z",
  "updated_at": "2026-10-03T09:12:00.000Z",
  "from": "Acme <[email protected]>",
  "subject": "Your order {{{ORDER_ID}}}",
  "reply_to": null,
  "html": "<p>Name: {{{PRODUCT}}}</p><p>Total: {{{PRICE}}}</p>",
  "text": null,
  "variables": [
    {
      "id": "e169aa45-1ecf-4183-9955-b1499d5701d3",
      "key": "PRODUCT",
      "type": "string",
      "fallback_value": "item",
      "created_at": "2026-10-01T08:00:00.000Z",
      "updated_at": "2026-10-01T08:00:00.000Z"
    }
  ],
  "has_unpublished_versions": false,
  "preview_text": null,
  "markdown": null,
  "category": "Billing"
}

The content fields are the draft. preview_text, markdown and category are SMTPfast additions.

POST/v1/templates

Create a template as a draft. It needs a name and html (or markdown). Publish it before sending with it.

Parameters

ParameterTypeRequiredDescription
namestringYesTemplate name
htmlstringYesHTML body. Optional when markdown is given.
aliasstringNoUnique handle in your team, usable wherever the id is
fromstringNoDefault sender
subjectstringNoDefault subject
reply_tostring | string[]NoDefault Reply-To
textstringNoPlain-text body. Generated from the HTML when left out; an empty string stops that.
variablesarrayNoUp to 50 { key, type, fallback_value } objects
markdownstringNoNot in Resend. Markdown body, used when html is empty.
preview_textstringNoNot in Resend. Inbox preview line.
categorystringNoNot in Resend. Label for the dashboard gallery.

Request Body

json
{
  "name": "order-confirmation",
  "alias": "order-confirmation",
  "from": "Acme <[email protected]>",
  "subject": "Your order {{{ORDER_ID}}}",
  "html": "<p>Name: {{{PRODUCT}}}</p><p>Total: {{{PRICE}}}</p>",
  "variables": [
    { "key": "ORDER_ID", "type": "string" },
    { "key": "PRODUCT", "type": "string", "fallback_value": "item" },
    { "key": "PRICE", "type": "number", "fallback_value": 25 }
  ]
}

Response

json
{
  "id": "clx_tpl123",
  "object": "template"
}
GET/v1/templates

List templates, newest first. Rows carry no content.

Parameters

ParameterTypeRequiredDescription
limitnumberNo1 to 100, default 20
afterstringNoTemplate id: return older templates. Not with before.
beforestringNoTemplate id: return newer templates. Not with after.
qstringNoNot in Resend. Search name, alias and subject.

Response

json
{
  "object": "list",
  "data": [
    {
      "id": "clx_tpl123",
      "name": "order-confirmation",
      "status": "published",
      "published_at": "2026-10-03T09:12:00.000Z",
      "created_at": "2026-10-01T08:00:00.000Z",
      "updated_at": "2026-10-03T09:12:00.000Z",
      "alias": "order-confirmation",
      "subject": "Your order {{{ORDER_ID}}}",
      "category": null,
      "has_unpublished_versions": false
    }
  ],
  "has_more": false
}
GET/v1/templates/:id

Get a template by id or alias, with its draft content and variables.

PATCH/v1/templates/:id

Change the draft. Only the fields you send change, and variables replace the whole list. Sends keep using the published version until you publish again.

Not in Resend: pass the updated_at you loaded as expected_updated_at. If someone changed the template since, the request answers 409 with name revision_conflict and current_updated_at, and nothing is overwritten. Create, update and publish answer with the updated_at their own write set, so pass that one on your next change, not one read back later. The dashboard editor always does this.

Request Body

json
{
  "html": "<p>Total: {{{PRICE}}}</p><p>Name: {{{PRODUCT}}}</p>",
  "expected_updated_at": "2026-10-03T09:12:00.000Z"
}

Response

json
{
  "object": "template",
  "id": "clx_tpl123",
  "updated_at": "2026-10-03T09:14:02.511Z",
  "status": "published",
  "published_at": "2026-10-03T09:12:00.000Z",
  "has_unpublished_versions": true
}
POST/v1/templates/:id/publish

Publish the current draft. Sends that name the template use this version from now on. Send an optional body with expected_updated_at to publish only the version you loaded; a newer one answers 409 revision_conflict.

Response

json
{
  "object": "template",
  "id": "clx_tpl123"
}
POST/v1/templates/:id/duplicate

Copy the draft into a new, unpublished template named with "(copy)" appended. The alias is not copied.

Response

json
{
  "object": "template",
  "id": "clx_tpl456"
}
DELETE/v1/templates/:id

Delete a template. Emails already sent with it are not affected; sends that still name it answer 404.

Response

json
{
  "object": "template",
  "id": "clx_tpl123",
  "deleted": true
}

Errors

Template endpoints answer errors in Resend's shape, with error carrying the same text as every other SMTPfast endpoint does.

json
{
  "statusCode": 404,
  "name": "not_found",
  "message": "Template not found",
  "error": "Template not found"
}

A send that leaves out a required variable answers 422 from POST /v1/emails:

json
{
  "error": "Missing values for the template variables ORDER_ID, PRODUCT, which have no fallback",
  "missing_variables": ["ORDER_ID", "PRODUCT"]
}

Differences from Resend

  • markdown, preview_text and category are extra fields, and the list accepts q.
  • A text of exactly "" stops a text part being generated from the HTML, as in Resend, but the email still carries an empty plain-text alternative, as any SMTPfast email sent with text: "" does.
  • Timestamps are ISO 8601.