API reference

Templates

Create, edit and publish transactional templates

Manage the templates sent with POST /v1/emails from code or a deploy script. Transactional emails explained.

Draft and published version

Every template has two versions:

  • the draft, which you edit (dashboard, PATCH, AI assistant);
  • the published version, the only one POST /v1/emails sends.

Edits reach your users only once you publish. A template never published cannot be sent (409 template_not_published).

statusMeaning
NEVER_PUBLISHEDDraft only, not sendable yet
PUBLISHEDThe draft is what gets sent
UNPUBLISHED_CHANGESThe draft differs from the published version; changedFields lists what changed

name and alias are not versioned: they change as soon as you save.

The template object

{
  "id": "c41d7e2a-9b8f-4a6e-b3d2-5f1e0a9c8b7d",
  "name": "Password reset",
  "alias": "password-reset",
  "subject": "Reset your password",
  "preheader": "This link expires in {{ data.expires_in }}",
  "fromName": "Acme",
  "fromEmail": "security@acme.com",
  "replyTo": null,
  "contentFormat": "MARKDOWN",
  "content": "Hi {{ contact.first_name | default: \"there\" }},\n\n[Choose a new password]({{ data.reset_url }})",
  "status": "PUBLISHED",
  "changedFields": [],
  "published": {
    "subject": "Reset your password",
    "preheader": "This link expires in {{ data.expires_in }}",
    "fromName": "Acme",
    "fromEmail": "security@acme.com",
    "replyTo": null,
    "contentFormat": "MARKDOWN",
    "content": "Hi {{ contact.first_name | default: \"there\" }},\n\n[Choose a new password]({{ data.reset_url }})"
  },
  "publishedAt": "2026-10-12T14:03:11Z",
  "publishedBy": "Ada Lovelace",
  "publicationRequest": null,
  "createdVia": "DASHBOARD",
  "createdAt": "2026-10-12T13:40:52Z",
  "updatedAt": "2026-10-12T14:03:11Z",
  "stats": { "sent": 1834, "delivered": 1829, "opened": 1402, "bounced": 5 }
}

The top-level subject to content fields are the draft; published is the version sent, null until the first publication. stats covers the last 30 days. publicationRequest is set when an AI assistant asked a person to publish the draft.

{{ data.* }} reads the variables of the send; {{ contact.* }} reads the recipient's contact, if any. Variables reference.

List templates

GET /v1/templates

Every template of the space, newest first, with its 30-day stats.

curl https://api.kulamail.com/v1/templates \
  -H "Authorization: Bearer ml_your_api_key"

Returns an array of template objects.

Create a template

POST /v1/templates

Creates a template as a draft. Publish it before sending.

name
string required
Shown in the dashboard. 120 characters at most.
alias
string required
The stable key your code sends with, such as password-reset. Lowercase letters, digits, - and _, 100 characters at most, unique in the space.
subject
string required
500 characters at most. Variables allowed.
preheader
string
Inbox preview text after the subject. 500 characters at most.
fromName
string
Sender name. Defaults to the space's.
fromEmail
string
Sender address on a verified domain of the space. Defaults to the space's.
replyTo
string
Reply-to address.
contentFormat
string
MARKDOWN (wrapped in the space design) or HTML (sent as given).
content
string
The body, 1,000,000 characters at most.
curl https://api.kulamail.com/v1/templates \
  -H "Authorization: Bearer ml_your_api_key" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Password reset",
    "alias": "password-reset",
    "subject": "Reset your password",
    "contentFormat": "MARKDOWN",
    "content": "Hi {{ contact.first_name | default: \"there\" }},\n\n[Choose a new password]({{ data.reset_url }})\n\nThis link expires in {{ data.expires_in }}."
  }'

Returns 201 with the template object, status: "NEVER_PUBLISHED".

StatusCodeWhen
400validation_error, bad_request, domain_not_verifiedMissing field, invalid alias, or sender not on a verified domain
409alias_takenThe alias is already used in the space

Get a template

GET /v1/templates/{template}

{template} is the id or the alias.

curl https://api.kulamail.com/v1/templates/password-reset \
  -H "Authorization: Bearer ml_your_api_key"

Update a template

PATCH /v1/templates/{template}

Writes the draft; emails keep using the published version. Same fields as Create a template, all optional: fields left out stay as they are, a blank optional field is cleared.

A new alias takes effect immediately: calls with the old alias get 404.
curl -X PATCH https://api.kulamail.com/v1/templates/password-reset \
  -H "Authorization: Bearer ml_your_api_key" \
  -H "Content-Type: application/json" \
  -d '{ "subject": "Your password reset link" }'

Returns the template with status: "UNPUBLISHED_CHANGES" and changedFields: ["subject"].

Publish a template

POST /v1/templates/{template}/publish

Copies the draft over the published version, used by the next sends. Requires a full access key.

curl -X POST https://api.kulamail.com/v1/templates/password-reset/publish \
  -H "Authorization: Bearer ml_your_api_key"
StatusCodeWhen
400bad_request, domain_not_verifiedEmpty content, or the sender is no longer on a verified domain
409nothing_to_publishThe draft has no unpublished changes

Discard a draft

POST /v1/templates/{template}/discard

Resets the draft to the published version, dropping unpublished changes. Answers 409 template_not_published on a template never published.

Delete a template

DELETE /v1/templates/{template}

Answers 204. Later sends with this template get 404.