
# 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](publish.md) the draft.

`If-Match` is required and carries the newest version you read (the `ETag` of [get](get.md) or [list versions](list-versions.md)). 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 `requires` in [list](list.md)) 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](list.md)). |
| `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.

```json
{
  "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

```bash
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`
