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/emailssends.
Edits reach your users only once you publish. A template never published cannot be sent (409 template_not_published).
status | Meaning |
|---|---|
NEVER_PUBLISHED | Draft only, not sendable yet |
PUBLISHED | The draft is what gets sent |
UNPUBLISHED_CHANGES | The 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.
password-reset. Lowercase letters, digits, - and _, 100 characters at most, unique in the space.MARKDOWN (wrapped in the space design) or HTML (sent as given).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".
| Status | Code | When |
|---|---|---|
| 400 | validation_error, bad_request, domain_not_verified | Missing field, invalid alias, or sender not on a verified domain |
| 409 | alias_taken | The 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.
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"
| Status | Code | When |
|---|---|---|
| 400 | bad_request, domain_not_verified | Empty content, or the sender is no longer on a verified domain |
| 409 | nothing_to_publish | The 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.