Welcome Email from a Template (2026): Send Without Redeploying Copy
Create a Rovela welcome template once with full_access (POST /templates), then send from signup with sending_access via template {id, variables}, Idempotency-Key welcome-{userId}, and HTML-escaped variables—curl + Node from live docs.
Why a welcome email belongs in a template
A welcome message is written once and edited forever after—by product, by marketing, by whoever owns onboarding copy. If that HTML lives inside your sign-up handler, every headline tweak is a deploy. Keep the body in a Rovela template instead: create it once, then send with nothing but a template id and a few variables.
This walkthrough follows the live recipe Welcome email from a template, plus Email templates and the Templates API. Examples use curl to create the template and Node fetch to send from the sign-up path—the same shapes the docs show.
It is not Welcome Email Best Practices (subject lines, CTA copy, onboarding UX). It is also not HTML Email Template Development (tables, inline CSS, client quirks). Sibling transactional posts: Send an invoice with a PDF attachment, Next.js magic link, Express OTP.
Keys: full_access vs sending_access
Template management (POST /templates, list, patch, duplicate, delete, publish) needs a full_access API key—or the dashboard Templates page. A sending_access key gets 403 on those routes (Authentication, Templates API).
Sending mail with template: { id, variables } on POST /emails needs only sending_access. That is the right scope for production sign-up workers: they can fire welcomes, not rewrite the copy.
Never put either key in a browser bundle. Call the API from your server.
Prerequisites
- A verified sending domain in the Rovela dashboard (Quick Start).
- A
full_accesskey (or dashboard session) for the one-time create step. - A
sending_accesskey in the app environment asROVELA_API_KEY. - Somewhere to store the returned template id—
WELCOME_TEMPLATE_IDin env is enough.
Create the template once (curl + full_access)
Run this from a laptop or a setup script—not from the hot sign-up path.
curl -X POST https://api.rovela.dev/templates \
-H "Authorization: Bearer re_your_full_access_key" \
-H "Content-Type: application/json" \
-d '{
"name": "Welcome",
"subject": "Welcome to Acme, {{first_name}}",
"html": "<h1>Hi {{first_name}},</h1><p>Your workspace <strong>{{workspace}}</strong> is ready.</p><p><a href=\"{{login_url}}\">Open Acme</a></p>",
"text": "Hi {{first_name}},\n\nYour workspace {{workspace}} is ready.\nOpen Acme: {{login_url}}"
}'
Keep the returned id (a UUID). That is what production sends with:
{"object": "template", "id": "3b0e7c52-...", "name": "Welcome", ...}
Send both html and text. HTML is what most clients show; plain text helps text-only clients, some accessibility setups, and spam filters (Email templates guide).
Placeholders are exact: {{first_name}} is not filled by a key named firstName. A missing key is left as the literal {{first_name}} in the delivered message—on purpose, so you see the bug instead of a silent blank (Templates API).
There is no conditional or loop syntax. If the body needs logic, render HTML in your app and send html directly instead of template.
Send from sign-up (Node + sending_access)
No subject or html in the request: both come from the template, rendered with variables. template is mutually exclusive with html / text; a subject on the send is ignored (Templates API).
Values are inserted as-is, without HTML escaping. Escape anything the user typed before it goes into variables, or a name like <img src=x> becomes markup in the email.
const WELCOME_TEMPLATE_ID = process.env.WELCOME_TEMPLATE_ID;
// Docs warn: no auto-escape. User-typed values go through this first.
function escapeHtml(s) {
return String(s)
.replace(/&/g, "&")
.replace(/</g, "<")
.replace(/>/g, ">")
.replace(/"/g, """)
.replace(/'/g, "'");
}
export async function sendWelcome(user) {
const res = await fetch("https://api.rovela.dev/emails", {
method: "POST",
headers: {
Authorization: "Bearer " + process.env.ROVELA_API_KEY,
"Content-Type": "application/json",
// One welcome per user, however many times sign-up is retried.
"Idempotency-Key": "welcome-" + user.id,
},
body: JSON.stringify({
from: "Acme <hello@mail.acme.com>",
to: user.email,
template: {
id: WELCOME_TEMPLATE_ID,
variables: {
first_name: escapeHtml(user.firstName),
workspace: escapeHtml(user.workspaceName),
login_url: "https://app.acme.com/login",
},
},
}),
});
if (!res.ok) throw new Error("welcome email failed: " + res.status);
return (await res.json()).id;
}
Idempotency
Sign-up handlers retry. With Idempotency-Key: welcome-{userId} (up to 256 characters; retries within 24 hours return the original response), the user gets one welcome, not a stack of them. Same idea as invoice-number keys on invoice attachment sends.
Escaping vs subject / text
The same variables object fills the subject and the text part. An HTML-escaped value can show entities there (Tom & Jerry instead of Tom & Jerry). If names can contain & or <, keep separate variables for HTML and for plain text (welcome-template example).
Trusted URLs you build yourself (like login_url above) usually do not need entity escaping—but still do not put raw user input into an href without URL validation.
What can go wrong
- Literal
{{first_name}}in the inbox — the key is missing fromvariables, or spelled differently. Keys match exactly. - Someone edits the live template — edits apply to the next send, including scheduled ones that have not fired yet. Prefer
POST /templates/:id/duplicate, edit the copy, test a send to yourself, then switchWELCOME_TEMPLATE_ID(Email templates guide). - Batch sends —
POST /emails/batchrejectstemplateoutright (it does not silently drop the body). Welcome mail is onePOST /emailsper user anyway (Templates API). 403creating a template — you used asending_accesskey. Usefull_accessor the dashboard for management.
Publishing a template (POST /templates/:id/publish) records when it first went live and is idempotent—a second publish does not move the timestamp.
Checklist
- Create with
full_access(or dashboard); store the template id in env - Production sender uses
sending_accessonly - Sign-up calls
POST /emailswithtemplate: { id, variables }—no inline HTML - HTML-escape user-typed variables; docs do not auto-escape
- Set
Idempotency-Key: welcome-{userId} - Duplicate → edit → test → switch id when changing live copy
- Do not use
/emails/batchwith templates