A template is a reusable message with {{ variables }} you fill in per send. Author the subject and body once, declare the variables it expects, then send it by template_id — Sendara renders the final email for each recipient. Edit a typo without shipping code; the next send picks it up.
Templates are versioned automatically: every time you change content, the previous version is preserved so you always have a history of what you sent.
Create a template
Create a template with POST /v1/templates. Give it a name, a channel, the content fields for that channel, and the list of variables it uses.
The full set of body fields:
The response is the stored template, including a generated id (prefixed tmpl_), its version (starts at 1), and is_active:
{
"id": "tmpl_8f3a39ab8c2d4e6f",
"name": "Welcome email",
"channel": "email",
"subject": "Welcome, {{ first_name }}",
"body_text": "Hi {{ first_name }} — thanks for joining {{ company }}.",
"body_html": "<h1>Hi {{ first_name }}</h1><p>Thanks for joining {{ company }}.</p>",
"variables": [
{ "name": "first_name", "sample": "Ada", "required": true },
{ "name": "company", "default": "Acme" }
],
"version": 1,
"is_active": true,
"created_at": "2026-06-14T10:00:00Z",
"updated_at": "2026-06-14T10:00:00Z"
}channel also accepts sms, push, and voice, but those channels are still rolling out — email is fully supported today.The variable model
Variables use mustache-style syntax: {{ name }}, with optional inner whitespace. Names are limited to letters, digits, and underscores ([a-zA-Z0-9_]); anything that doesn't match is left untouched in the output. There is no logic — no conditionals, loops, or partials — so an author can safely write {{ first_name }} without ever producing a parse error.
Each declared variable carries metadata:
At render time each variable resolves in this order: the value you pass wins; otherwise the default is used; otherwise, if the variable is required, the request fails with missing_variable; otherwise it renders as an empty string.
{{ tokens }}still get substituted if you pass a value and render empty if you don't. Declaring them lets you mark some required and supply defaults and samples for previews — recommended for anything you send to real users.body_html, every substituted value is HTML-escaped to prevent injection — so {{ name }} set to <b>x</b> renders as text, not markup. In subject and body_text values are inserted verbatim. Keep your HTML structure in the template itself, not in variable values.Preview a render
Before you send, preview exactly what a template produces for a given set of variables with POST /v1/templates/{id}/render. Nothing is sent and nothing is billed — you get the rendered channel payload back.
For an email template, the response is the email payload Sendara would send:
{
"subject": "Welcome, Ada",
"body_text": "Hi Ada — thanks for joining Sendara.",
"body_html": "<h1>Hi Ada</h1><p>Thanks for joining Sendara.</p>"
}vars, while a send uses template_vars (below). Same idea — the field name just differs between the two endpoints.If a required variable is missing you get 400 missing_variable; a structurally invalid template (for example an SMS template that renders an empty body) returns 400 invalid_template.
Send with a template
To send a stored template, call POST /v1/send with template_id and template_vars instead of an inline payload. Sendara renders the template per recipient and sends the result.
Everything else about /v1/send still applies: the idempotency_key is required and retries return the original result, suppressed recipients are rejected with 409 recipient_suppressed, and metadata.from_email must be on a verified domain. See Send email for the full request contract.
template_id works for bulk campaigns. Pass it to POST /v1/send/bulkand Sendara renders the template per recipient against each contact's data.List, update, and delete
The rest of the lifecycle is plain CRUD. List returns the account's templates newest-first:
curl https://api.sendara.dev/v1/templates \
-H "Authorization: Bearer sk_live_xxx"{
"templates": [
{ "id": "tmpl_8f3a39ab8c2d4e6f", "name": "Welcome email",
"channel": "email", "version": 1, "is_active": true,
"created_at": "2026-06-14T10:00:00Z", "updated_at": "2026-06-14T10:00:00Z" }
]
}Fetch one with GET /v1/templates/{id}. Update with PUT /v1/templates/{id} — send only the fields you want to change. Editing any content field (subject, body_text, body_html, or body_json) snapshots the current version and bumps version; metadata-only changes (like name or is_active) don't.
curl -X PUT https://api.sendara.dev/v1/templates/tmpl_8f3a39ab8c2d4e6f \
-H "Authorization: Bearer sk_live_xxx" \
-H "Content-Type: application/json" \
-d '{ "subject": "Welcome aboard, {{ first_name }}" }'Set is_active to false to retire a template without deleting it, or delete it permanently:
curl -X DELETE https://api.sendara.dev/v1/templates/tmpl_8f3a39ab8c2d4e6f \
-H "Authorization: Bearer sk_live_xxx"Image uploads
Email needs images you can hot-link: a logo, a hero, a product shot. Upload one with POST /v1/uploadsand Sendara hosts it on a stable, cacheable URL you can drop straight into a template's body_html.
Uploads are multipart/form-data with a single file part:
curl https://api.sendara.dev/v1/uploads \
-H "Authorization: Bearer sk_live_xxx" \
-F "file=@logo.png"The response gives you the hosted URL:
{
"id": "asset_2b91c0a17e4d5f6a",
"url": "https://api.sendara.dev/v1/assets/asset_2b91c0a17e4d5f6a",
"content_type": "image/png",
"bytes": 18234
}Reference the urlin your HTML — it's served publicly with a long-lived immutable cache header, so it loads fast in every inbox:
<img src="https://api.sendara.dev/v1/assets/asset_2b91c0a17e4d5f6a"
alt="Acme" width="120" />413 payload_too_large, and an unsupported type returns 400 invalid_request. Optimize images before uploading — smaller assets render faster and keep your mail under inbox size limits.Errors
All errors use the standard envelope — { "error": { "code": "...", "message": "..." } }. The ones specific to templates and uploads:
missing_variable(400) — arequiredvariable had no value and no default. Pass it, or give it adefault.invalid_template(400) — the rendered output is invalid for the channel (e.g. an empty SMS body). Fix the template content.not_found(404) — no template with thatidexists on your account.invalid_request(400) — a malformed body, a missingname/channelon create, an unsupported upload type, or a missingfilepart.payload_too_large(413) — an upload over the 2 MiB limit.
See the errors reference for the full list and the API reference for every endpoint.