Cosmoner Docs
Guides

Email Templates

Write transactional emails once in the control panel, in code with React Email or in the visual editor, publish them as versions, and send them by alias through the API.

Overview

An email template is an email stored in your project: a subject, a body, and the variables each send fills in. Your code names the template and passes the values, and Cosmoner renders the email and sends it:

{
  "credentialId": "your-credential-id",
  "to": "[email protected]",
  "template": {
    "id": "order-receipt",
    "variables": { "customer": "Ada", "orderNumber": "A-1042" }
  }
}

Use a template rather than sending your own HTML when:

  • people other than developers change the wording. Anyone with the developer role can edit and publish a template in the control panel, with no deploy of your application.
  • you want a history and a way back. Every publish is a numbered version, and rolling back takes effect on the next send.
  • you want the data checked. A send whose variables do not match what the template declares is refused before anything goes out, rather than delivered with a blank where a name should be.

Sending your own subject and html still works exactly as before, and suits one-off messages or emails you already render elsewhere. Templates are free: emails sent from them are billed like any other, one email per recipient. See Email Pricing.

Prerequisites

  • A verified sending domain and an SMTP credential on it — see SMTP Setup. The credential decides the sender address, for templates as for any other send.
  • The developer role or above to create, edit and publish templates. Viewers can open them read-only.

Create a Template

Open Email → Templates in your project and choose New template. Give it:

  • a name, which is only for people;
  • an alias, which is what your code sends by (see below);
  • optionally a folder;
  • an editor:
    • Visual — you write the email like a document, inserting variables, conditions, repeated blocks, buttons and images. You can convert it to code later.
    • Code — a React Email component in TSX, for full control over the layout.

A new template starts from a short example you can replace. Nothing can be sent from it until you publish it.

Aliases

The alias is the stable handle your code names in template.id, such as order-receipt or password-reset. It must:

  • use only lower-case letters, digits, - and _;
  • start with a letter or a digit;
  • be at most 64 characters;
  • be unique in the project.

You can change an alias under the template's Settings tab, but sends that name the old alias are refused from that moment, so deploy the new name in your code first. Every template also has an id that never changes; template.id accepts either, and a send by id is not affected by an alias change.

Variables

Variables are the values a send supplies. Each one has a name, a type, and whether it is required.

TypeA send passesExample
stringText, up to 10,000 characters"Ada"
numberA number2, 19.99
booleantrue or falsetrue
urlAn http, https or mailto address, up to 2,048 characters"https://example.com/track/A-1042"
emailAn email address"[email protected]"
datetimeAn ISO 8601 date and time with a time zone offset"2026-11-03T14:30:00Z"
listAn array of items, each with the same declared fields[{ "name": "Lamp", "qty": 2 }]

Required and optional. A required variable must be in every send. An optional one may be left out or sent as null; it then renders as empty, or as its default when the template declares one.

Lists. A list declares the fields every item has, each with its own type and whether it is required. Items are flat — a list cannot contain another list — and a list holds at most 500 items, or fewer if the template sets its own maximum.

Names start with a letter and use letters, digits and underscores, up to 64 characters. A few reserved words, such as true, false, null and empty, cannot be used; the editor tells you if you pick one.

What a send is checked against. template.variables is checked against the variables of the version being sent before anything goes out. A missing required value, a value of the wrong type, and a name the template does not declare are all refused — an unknown name is usually a typo, and would otherwise leave a gap in a delivered email. See Errors.

Every value is escaped as it is put into the email, so a variable is always text: a customer whose name contains <b> gets those characters, not bold type.

In the Visual Editor

Declare variables on the Variables & data tab: give each a name, a type, whether it is required and a sample value. For a list, add the fields its items have.

In the email, type / to insert a block. Alongside text, headings, lists, columns, buttons and dividers, the menu has the template blocks:

  • Variable inserts a chip that is replaced by the send's value. Select a chip to change its format: as sent, UPPERCASE, lowercase or Capitalized.
  • Link to a variable makes a link whose address each send supplies. Only url variables can be used, so the address is always a web link. A button can point to a variable the same way.
  • Show if… wraps a block that appears only when a variable has a value. The block is left out when the variable is missing, empty, or false.
  • Repeat for each… repeats a block for every item of a list variable. Inside it, insert the item's fields as item.field.
  • Image inserts an image already hosted on the web, by its https:// address, with alt text and an optional width and link.

The Subject field above the email takes variables too: write {{ name }} where the value goes. Preview text, the line most inboxes show after the subject, is optional.

In the Code Editor

A code template is a React Email component that exports three things: its variables, its subject, and the component itself as the default export.

import { defineVariables, Each, If, v, Var, type VariablesOf } from "@cosmoner/email-kit";
import { Body, Button, Container, Html, Section, Tailwind, Text, pixelBasedPreset } from "react-email";

export const variables = defineVariables({
  customer: v.string({ required: true, sample: "Ada" }),
  orderNumber: v.string({ required: true, sample: "A-1042" }),
  placedAt: v.datetime({ required: true }),
  items: v.list(
    {
      name: v.string({ required: true, sample: "Desk lamp" }),
      qty: v.number({ required: true, sample: 2 }),
    },
    { maxItems: 100 },
  ),
  trackingUrl: v.url(),
  discountCode: v.string({ default: "none" }),
});

type Props = VariablesOf<typeof variables>;

export const subject = (props: Props) => `Order ${props.orderNumber} confirmed`;

export default function OrderReceipt(props: Props) {
  return (
    <Html>
      <Tailwind config={{ presets: [pixelBasedPreset] }}>
        <Body className="bg-white font-sans">
          <Container className="mx-auto max-w-[480px] px-6">
            <Text>
              Thanks {props.customer}, your order was placed on{" "}
              <Var value={props.placedAt} format={{ date: "%b %d, %Y" }} />.
            </Text>
            <Section>
              <Each of={props.items}>
                {(item) => (
                  <Text>
                    {item.name} × {item.qty}
                  </Text>
                )}
              </Each>
            </Section>
            <If when={props.trackingUrl}>
              <Button href={props.trackingUrl} className="rounded bg-black px-4 py-2 text-white">
                Track your parcel
              </Button>
            </If>
            <Text>
              Discount code: <Var name="discountCode" format="upcase" />
            </Text>
          </Container>
        </Body>
      </Tailwind>
    </Html>
  );
}
  • defineVariables declares the variables with the builders v.string, v.number, v.boolean, v.url, v.email, v.datetime and v.list. Each takes required, sample, default and description; v.list takes its item fields first, then required, maxItems and sample. VariablesOf turns the declaration into your component's props.
  • subject is a string, or a function of the props.
  • Render a value as {props.customer}, or with <Var> to format it: "upcase", "downcase", "capitalize", { date: "%b %d, %Y" } (strftime codes; dates are formatted in UTC, in English), { round: 2 }, { truncate: 80 } or { default: "there" }. <Var name="…"> names a variable instead of taking a prop.
  • <If when={…}> renders its children only when the variable has a value, with an optional fallback. <Each of={…}> repeats its children for every item of a list. Lists cannot be nested.
  • Tailwind classes inside <Tailwind> are turned into inline styles, as email clients need.
  • More than one file. Add files under Files — up to 50 — and import them from the entry file with a relative path such as ./components/footer. A template can import react-email (or @react-email/components), @cosmoner/email-kit and its own files.

Values are filled in when each email is sent, not when you write it, so a variable has to reach the email unchanged. Use <Var>, <If> and <Each> rather than JavaScript on a variable's value: calling .toUpperCase() or .map() on it, testing it with && or .length, putting it in a style attribute or a <style> element, or using a variable that is not a url as a link's href are refused when the template builds, with a message saying what to change. A template too heavy to render quickly — a long list rendering many fields per item, say — is refused the same way.

The Variables & data tab lists what the template declares once it builds.

Sample Data

Previews and test sends need values. On the Variables & data tab, write sample data as JSON, exactly as you would pass it in template.variables — Start from sample values fills it in from the samples the variables declare. Save sample data keeps it with the template for everyone who opens it. Left empty, previews use the declared samples.

Preview and Test Sends

The preview beside the editor updates as you type, and switches between Desktop, Mobile and Plain text. It renders the email exactly as a send does, with your sample data. Every email sent from a template has a plain-text part, generated from its HTML.

To see the email in a real inbox, choose Send test:

  • send either the draft, as previewed or the published version;
  • pick the sending address from your SMTP credentials;
  • send to up to 5 addresses.

A test email uses the preview's sample data and has [Test] before its subject. It is a real email: it counts toward your send limits and is billed like any other.

Saving and Editing Together

Your draft saves automatically as you edit, and the editor shows when it is saved. The draft is only yours to send once you publish it — sends keep using the published version in the meantime.

If someone else saves the same template after you opened it, saving stops and the editor asks what to keep: Load theirs replaces your edits with their version, Keep mine saves yours over theirs. Nobody's work is overwritten without one of you choosing.

Publish

Publish turns the current draft into the next version — version 1, 2, 3 and so on — and makes it the one sends use, from the very next request. The button is available once the draft is saved and builds without errors. The draft and the published version then match until the next edit.

Publishing is rate-limited per project: if you publish many times in a short period, publishing is refused for a while, and you can try again later.

The header shows which version is live, or Not published for a template that has never been published. A send naming a template that has never been published is refused with 409 EMAIL_TEMPLATE_NOT_PUBLISHED.

Versions and Rolling Back

The Versions tab lists every published version, newest first, with the live one marked. Preview shows a version as it was published, rendered with your sample data.

Roll back to this makes an earlier version the one sends use, from the next request. Your draft stays as it is, and the next publish still gets the next version number. Roll forward again the same way. Through the API, the same action is Roll back to a version.

The 50 most recent versions are kept. Publishing a 51st removes the oldest — never the live one.

A send can also name a version explicitly with template.version, which pins it: a later publish or rollback does not change what that send renders. Any version that is still kept can be sent this way; once it is no longer kept, the send is refused with 404 EMAIL_TEMPLATE_VERSION_NOT_FOUND.

Converting Visual to Code

A visual template can be converted to a code template at any time with Convert to code. The code renders exactly the same email, and you edit it as code from then on. This is one way: a code template cannot be turned back into a visual one. Versions published before the conversion keep sending unchanged.

Send with a Template

Send through the API with an API key carrying email:read, email:write and email-templates:read:

curl https://api.cosmoner.com/v1/projects/$PROJECT_ID/email/send \
  -H "Authorization: Bearer $COSMONER_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "credentialId": "your-credential-id",
    "to": "[email protected]",
    "replyTo": "[email protected]",
    "template": {
      "id": "order-receipt",
      "variables": {
        "customer": "Ada",
        "orderNumber": "A-1042",
        "placedAt": "2026-11-03T14:30:00Z",
        "items": [
          { "name": "Desk lamp", "qty": 2 },
          { "name": "Bulb", "qty": 4 }
        ],
        "trackingUrl": "https://example.com/track/A-1042"
      }
    }
  }'
  • template.id is the alias or the template's id.
  • template.version is optional. Leave it out to send the published version; set it, for example "version": 3, to send that version instead.
  • template.variables holds the values, by name. Leave out optional ones you have no value for.
  • Leave out subject, html and text. The template supplies all three; a send that includes any of them is refused with 422.
  • The sender is the credential's address, as for every send. to takes up to 50 recipients and replyTo up to 5 addresses.

The response says which template and version went out:

{
  "success": true,
  "data": {
    "messageId": "0100018f…",
    "template": { "id": "clx2k4m6p0000abcd12345678", "alias": "order-receipt", "version": 4 }
  }
}

messageId is the same id that webhook events for the email carry, so you can match deliveries, bounces and opens back to the send.

Sending a template goes through every check a send has: billing, sending reputation and your send limits. Template problems are reported by the send call itself, before the email is handed on for delivery — they never arrive later as a webhook event.

Errors

A refused send sends nothing, and a template error never uses any of your quota. The template is checked after the credential, billing and the hourly limit, so those errors come first.

Statuserror.codeWhat happenedWhat to do
403INSUFFICIENT_SCOPEThe API key lacks email-templates:read, email:read or email:writeAdd the missing scope to the key
404EMAIL_TEMPLATE_NOT_FOUNDNo template with that alias or id in the project — perhaps the alias was changed or the template deletedCheck the alias under the template's Settings
404EMAIL_TEMPLATE_VERSION_NOT_FOUNDThe project has no such version of the template, or it is older than the 50 keptSend another version, or leave version out
409EMAIL_TEMPLATE_NOT_PUBLISHEDThe template has never been published, and no version was namedPublish it in the control panel
422EMAIL_TEMPLATE_VARIABLES_INVALIDtemplate.variables does not match what the version declaresFix the values listed in error.details.issues
422EMAIL_TEMPLATE_RENDER_FAILEDThe rendered email grew past 256,000 bytes of HTML, or took too long to renderSend fewer list items, or simplify the template
422VALIDATION_ERRORThe request itself is malformed — for example subject alongside templateFix the request
503EMAIL_TEMPLATE_UNAVAILABLEThe template could not be loaded for a momentRetry — nothing was sent

EMAIL_TEMPLATE_VARIABLES_INVALID lists each problem with the path of the value it concerns:

{
  "success": false,
  "error": {
    "code": "EMAIL_TEMPLATE_VARIABLES_INVALID",
    "message": "The variables do not match what the template declares",
    "details": {
      "issues": [
        { "path": "items.0.qty", "message": "Invalid input" },
        { "path": "trackingUrl", "message": "must be an http, https or mailto URL" }
      ]
    }
  }
}

The send endpoint is rate-limited. A 429 means you are sending too many requests in a short time: back off and retry. The project's hourly, daily and monthly limits are separate and answer 400; see Send Limits. The API reference lists every response of Send an email.

Roles and API Key Scopes

ActionRoleAPI key scopes
View templates, versions and foldersViewer or aboveemail-templates:read
Create, edit, publish, roll back, test send and delete templates; manage foldersDeveloper or aboveemail-templates:read and email-templates:write
Send a templateDeveloper or aboveemail:read, email:write and email-templates:read

Sending a template needs email-templates:read because the send reveals what the template says. A key that can send mail cannot, by that alone, read or change your templates, and a key that manages templates cannot send.

Editing a template's content, publishing and test sends happen in the control panel. Through the API you can list and read templates and their versions, create a template, change its name, alias, description, folder and sample data, roll back, delete, and manage folders — see the Email templates API reference. See Team Roles for everything each role can do.

Folders

Folders keep a long list of templates tidy. Create one with New folder, nest folders up to 5 levels deep, and use a template's Move to folder to file it. Folders that share a parent need different names.

A folder changes only where a template is listed. Moving a template never changes its alias, its versions or how it is sent. Deleting a folder deletes nothing in it: its templates and subfolders move up one level. If a subfolder would then sit next to a folder with the same name, the delete is refused until you rename or move it.

Search narrows the templates you are looking at by name or alias. Choose All templates to search every folder, or Not in a folder for the templates outside them.

Deleting a Template

Delete a template from its Settings tab, or from its menu in the list. Its draft and every version are deleted, and sends naming it are refused with 404 EMAIL_TEMPLATE_NOT_FOUND from then on. This cannot be undone.

Limits

Limit
Templates per project200
Folders per project100, nested up to 5 levels
Versions kept per template50 most recent
Template source256 KB; up to 50 files for a code template
Name / alias / description128 / 64 / 500 characters
Variables per template100
Fields per list50
Items per list500, or the list's own maximum
string value10,000 characters
url value2,048 characters
template.variables in one send64 KB as JSON
Saved sample data64 KB as JSON
Rendered HTML256,000 bytes
Subject998 characters; line breaks become spaces
Recipients per send / per test send50 / 5

On this page