# Templates & Layouts

A template has a **kind**:

- **`email`** — a complete email that a broadcast (`template_id`) or a workflow `send` uses. It is deployed to your own SES account.
- **`layout`** — a branded wrapper with exactly one `{{> content}}` slot. A [composed broadcast](/api-reference/broadcasts#composed-mode-body-and-layout) places its `body` into that slot. A layout is never deployed to SES and never sent on its own.

Both kinds live under `/v1/templates` and are addressed by **slug**.

| Endpoint | Scope |
|---|---|
| [`GET /v1/templates`](/api-reference/endpoints/entities/list-templates) — `?kind=email` or `?kind=layout` narrows to one kind; omitted lists both | `api.templates.view` |
| [`GET /v1/templates/{slug}`](/api-reference/endpoints/entities/get-template) | `api.templates.view` |
| [`PUT /v1/templates/{slug}`](/api-reference/endpoints/entities/put-template) — create or replace | `api.templates.manage` |
| [`POST /v1/templates/render`](/api-reference/endpoints/entities/render-template) | `api.templates.view` |
| [`POST /v1/templates/{slug}/test`](/api-reference/endpoints/entities/test-send-template) | `api.templates.test` |
| [`DELETE /v1/templates/{slug}`](/api-reference/endpoints/entities/delete-template) | `api.templates.manage` |

`PUT` is a full replacement, idempotent, and hash-guarded (`expected_content_hash`); for an email template it deploys to SES before it answers and reports the outcome in `deploy_state`. The [endpoint reference](/api-reference/endpoints/entities/put-template) has the details. An invalid `?kind=` on the list is a `422` on field `kind`.

## Kind

`kind` is `email` or `layout`. It defaults to `email` on create, and an update that omits it keeps the stored kind. It is **fixed at create**: a `PUT` naming a different kind for an existing slug is `422 kind_immutable` — write a new slug instead.

## Layouts

Write a layout with `PUT /v1/templates/{slug}` and `kind: layout`:

```json
{
  "kind": "layout",
  "format": "handlebars",
  "body": "<!doctype html><img src=\"https://cdn.acme.example/logo.png\" alt=\"Acme\" width=\"120\">{{> content}}"
}
```

A layout must contain `{{> content}}` **exactly once, inside ``**, and no other partial. A layout write that breaks the rule is a `422 validation_failed` with one of these `validation_code`s:

| `validation_code` | Cause |
|---|---|
| `layout_missing_slot` | No `{{> content}}` slot. |
| `layout_multiple_slots` | More than one slot. |
| `layout_slot_outside_body` | The slot is outside `<body>…`. |
| `layout_unknown_partial` | Any other partial, e.g. `{{> footer}}`. |
| `subject_not_allowed_for_layout` | A `subject` was sent. A layout wraps a broadcast's body; the broadcast carries the subject. |

The reverse holds too: a `{{> content}}` slot in an **email** template is refused with `content_slot_in_email`. A layout also passes the usual SES checks, including the 500 KB limit.

A layout:

- is **never deployed** to SES — its `deploy_state` is `not_applicable`;
- does **not count** toward your plan's template limit;
- gets the unsubscribe link and postal address added when a broadcast is composed into it, if your organization has set them in its Email compliance settings — don't add your own unsubscribe link when the base URL is set.

### Render, test and delete

- **Render** — `POST /v1/templates/render` on a layout (stored, or a `body` containing the slot) fills the slot with a fixed sample body — a heading, a paragraph and a button — so you see where a broadcast's message lands.
- **Test** — `POST /v1/templates/{slug}/test` on a layout is `422 layout_not_sendable`. Test a broadcast that uses it instead, with [`POST /v1/broadcasts/{id}/test`](/api-reference/endpoints/broadcasts/test-send-broadcast).
- **Delete** — refused for the organization's **default layout** (`422`, `validation_code: default_layout_not_deletable`; make another layout, or the built-in, the default first), and for a layout an unsent broadcast still uses (`409 conflict`, with the broadcasts in `referenced_by`). A sent broadcast does not block deleting its layout.

### The default layout

Every organization has a default layout: a broadcast created with `body` and no `layout` (or `layout: "default"`) uses it, and the choice is frozen into the broadcast at create. Until you choose another, it is the SendOps built-in (`layout: "builtin"` on a broadcast).


  There is no `/v1` endpoint to read or change the default layout. Set it in the dashboard (**Templates → Layouts → Set as default**) or with the MCP tool `templates_author` and `action: make_default`. See [MCP Server](/api-reference/mcp).




<ApiSidePanel
  requestTabs={[
    {
      label: 'cURL',
      lang: 'bash',
      code: `# List the org's layouts (api.templates.view)
curl 'https://api.sendops.dev/v1/templates?kind=layout' \\
  -H "Authorization: Bearer sk_live_..."

# Create or replace a layout (api.templates.manage)
curl -X PUT 'https://api.sendops.dev/v1/templates/brand' \\
  -H "Authorization: Bearer sk_live_..." \\
  -H "Content-Type: application/json" \\
  -d '{
    "kind": "layout",
    "format": "handlebars",
    "body": "{{> content}}"
  }'

# See it with a sample body in the slot
curl -X POST 'https://api.sendops.dev/v1/templates/render' \\
  -H "Authorization: Bearer sk_live_..." \\
  -H "Content-Type: application/json" \\
  -d '{ "template": "brand" }'`,
    },
  ]}
  responseCode={`{
  "slug": "brand",
  "kind": "layout",
  "deploy_state": "not_applicable",
  "...": "..."
}`}
/>