Save an email template draft
PUT /v1/email-templates/{family}/{locale} · permission email-templates:write · Startup plan and above
Saves the content as a new draft version. The published version does not change until you publish the draft.
If-Match is required and carries the newest version you read (the ETag of get or list versions). For a slot with no versions yet, send If-Match: "0"; * does not match an empty slot. A mismatch returns 412.
Before saving, the content goes through the same checks as a real send:
- It must render in the Liquid sandbox with sample data.
- The HTML must be balanced.
- The family's code or link (see
requiresin list) must appear in both the HTML and the plain-text body. - Links may point only to platform hosts.
- Size limits apply.
A slot keeps at most 50 versions besides the published one; older drafts are dropped.
Path parameters
| Parameter | Description |
|---|---|
family |
Email family, for example verify_email or email_otp (see List email templates). |
locale |
Locale tag, for example en or de. |
Request
| Field | Type | Required | Description |
|---|---|---|---|
subject |
string | yes | Liquid, plain text, 1 to 200 characters, no line breaks. |
html |
string | yes | Liquid HTML fragment, at most 256 KiB. The platform adds the layout. |
text_body |
string | yes | Liquid plain-text body, at most 64 KiB. |
from_name |
string | no | Sender display name, at most 64 characters, no line breaks. Empty or missing uses the application or environment name. |
Response
200 OK with the new draft and ETag set to its version.
{
"family": "verify_email",
"locale": "en",
"version": 3,
"status": "draft",
"subject": "Confirm your email for {{ application.name }}",
"html": "<p>Hi,</p><p><a href=\"{{ link }}\">Confirm your email</a></p>",
"text_body": "Confirm your email: {{ link }}",
"from_name": null,
"source_version": null,
"created_at": "2026-10-19T09:00:00Z",
"published_at": null,
"latest_version": 3,
"published_version": 2
}
Errors
| HTTP | code | When |
|---|---|---|
| 400 | unknown_field |
The body has an unknown field or is not a JSON object. |
| 404 | not_found |
The family or locale is unknown, or the version does not exist. |
| 403 | plan_feature_unavailable |
The environment's plan does not include custom email templates (Startup and above; required_plan says which). |
| 412 | precondition_failed |
If-Match is not the newest version (someone saved in between), or is * on an empty slot. |
| 413 | payload_too_large |
The body is larger than 2 MiB. |
| 428 | precondition_required |
If-Match is missing. |
| 422 | validation_failed |
The content fails validation; errors[] names each field (/subject, /html, /text_body, /from_name, /family) and its rule. |
| 422 | template_forbidden_construct |
The template uses a Liquid construct outside the sandbox (for example {% include %}); errors[] names the field. |
| 401 | unauthenticated |
The credential is missing, invalid or revoked. |
| 403 | insufficient_permissions |
The credential lacks the required permission. |
| 429 | rate_limited |
The rate limit was exceeded; retry after the Retry-After period. |
| 503 | service_unavailable |
A dependency is temporarily unavailable; retry after the Retry-After period. |
Validation rules in errors[] include required, too_long, invalid, unbalanced_html, missing_code, missing_link, missing_code_or_link, template_render_failed, template_too_large and family_not_available.
Example
curl -X PUT https://api.eu.keenauth.com/v1/email-templates/verify_email/en \
-H "Authorization: Bearer $KEENAUTH_SECRET_KEY" \
-H "Content-Type: application/json" \
-H 'If-Match: "2"' \
-d '{"subject":"Confirm your email for {{ application.name }}","html":"<p><a href=\"{{ link }}\">Confirm your email</a></p>","text_body":"Confirm your email: {{ link }}"}'
CLI: keenauth email-templates put verify_email en --if-match '"2"' --data @template.json