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:

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