Email templates

Move message bodies out of your application and into the product.

If the same HTML is being built inside a function somewhere in your codebase, it is a template that has not been extracted yet. The visible cost is that changing a heading needs a deploy; the hidden one is that the person who wants the change usually cannot make it.

When a template is worth it

  • Yes — messages you send repeatedly with different values: receipts, welcomes, password resets, shipping notices.
  • No — one-off mail, or bodies assembled from data that only exists at send time. Render those yourself and pass the result as html.
The substitution here is deliberately plain: {{name}} is replaced with a string, and that is the whole language. There are no conditionals, no loops, and no filters — so there is nothing to escape, and nothing to inject. If a message needs logic, build it in your application where you can test it.

Create the template

A template has a name, a subject, and at least one of html or text.

curl -X POST https://api.rovela.dev/templates \
  -H "Authorization: Bearer <session token>" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Order receipt",
    "subject": "Your order {{order_id}} is confirmed",
    "html": "<h1>Thanks, {{name}}</h1><p>Order <strong>{{order_id}}</strong> is confirmed.</p>",
    "text": "Thanks, {{name}}. Order {{order_id}} is confirmed."
  }'

Send both bodies. html is what most people see; text is what a text-only client, a screen reader configured for plain text, and some spam filters see. A message with only html is a weaker signal than one with both.

Send with it

Reference the id and supply the variables. The template's subject is used unless the send provides one.

curl -X POST https://api.rovela.dev/emails \
  -H "Authorization: Bearer re_your_api_key" \
  -H "Content-Type: application/json" \
  -d '{
    "from": "Acme <hello@yourdomain.com>",
    "to": "alice@example.com",
    "template": {
      "id": "<template_uuid>",
      "variables": {"name": "Alice", "order_id": "A-10422"}
    }
  }'

The rendered body for that send is the template with {{name}} replaced by Alice and {{order_id}} by A-10422.

Variables you do not pass

A placeholder with no matching key is left in the output exactly as written. That is deliberate — replacing it with an empty string would produce "Thanks, . Order A-10422 is confirmed.", which reads like a bug in your copy rather than like a missing value. As it stands, an email that says {{name}} tells you precisely what went wrong.

If you see a literal {{name}} in a delivered email, the bug is in the sending code: the key is missing from variables, or is spelled differently — keys are matched exactly, so {{UserName}} and {{userName}} are different variables.

Drafts and publishing

Templates carry a published state, updated with POST /templates/:id/publish. Publishing records when a template first went live and is idempotent, so a second call does not move the date.

The usual workflow is to POST /templates/:id/duplicate, edit the copy, and point the send at the new id — rather than editing the template that production is currently using. Edits take effect on the next send, which includes anything scheduled but not yet dispatched.

Testing a change

Send it to yourself before you change the send path in production. Because the send returns an id immediately and renders the body server-side, the fastest check is to send once to your own address and read the message, rather than reading the template back and reasoning about what it will render.