Sending

Templates

Store your email once, fill in the blanks at send time. Keep copy out of your codebase and out of your deploys.

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:

namestringRequired
Human-readable name for the template.
channelstringRequired
One of email, sms, push, voice. Email is the front door.
subjectstringOptional
Subject line. Required at render time for email; supports {{ variables }}.
body_htmlstringOptional
HTML body. Variable values are HTML-escaped on render to prevent injection.
body_textstringOptional
Plain-text body. Variable values are substituted verbatim.
body_jsonobjectOptional
Opaque design-editor document (e.g. a block layout). Stored verbatim and round-tripped; not rendered server-side.
variablesarrayOptional
Declared {{ variables }} with sample / default / required metadata. See Variables below.

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"
}
Stay email-first. 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:

namestringRequired
The placeholder name. Referenced in the body as {{ name }}.
samplestringOptional
Example value shown in previews and the dashboard. Never substituted in real sends.
defaultstringOptional
Value used when the caller omits this variable. Makes the variable effectively optional.
requiredbooleanOptional
When true and no value or default is provided, render and send fail with missing_variable.

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.

You don't have to declare every variable. Undeclared {{ 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.
In 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>"
}
The render body uses 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.

The same 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" />
filefileRequired
The image part. Allowed types: PNG, JPEG, GIF, WebP.
Uploads are capped at 2 MiB. Larger files return 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) — a required variable had no value and no default. Pass it, or give it a default.
  • 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 that id exists on your account.
  • invalid_request (400) — a malformed body, a missing name/channel on create, an unsupported upload type, or a missing file part.
  • 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.