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.
{{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.
{{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.