---
title: "Create a template"
method: POST
path: "/api/v1/templates"
tags: ["Template Management"]
---

# Create a template

`POST /api/v1/templates`

## Request

- Base URL: `https://api.imagestep.dev`
- URL: `https://api.imagestep.dev/api/v1/templates`
- Auth: API key in header `Authorization`

Creates version 1 and answers `201` with it. `name`, `html`, `width` and `height` are required. `html` ≤ 256 KB and `css` ≤ 128 KB (UTF-8 bytes), `width` / `height` 1–4096 px; anything outside is `400 invalid_param` naming the field. `variables` defaults to the `{{ … }}` names found in the html and css. Each account may own at most 200 templates (`422 resource_limit_exceeded`).

Rendering is `POST /api/v1/jobs {"op": "render_template", "templateId": …, "items": [...]}`, or one row now with `POST /api/v1/images/render`.

## Headers

- `Idempotency-Key` string

## Request body

- TemplateData: HTML/CSS render template with {{ var }} placeholders; the input of the render_template op
  - `id` string: Template id (`tpl_…`; a built-in's is `builtin-template-…`). A version read by `id@version` carries that spelling here.
  - `name` string: Display name (required on create; at most 200 characters)
  - `slug` string: URL-safe handle, unique per account: lowercase letters, digits and hyphens. Generated from the name when omitted; one you send must be free. `builtin-` is reserved.
  - `description` string: Free text for people; not rendered. `""` on a PUT empties it.
  - `html` string: Body HTML (required on create; ≤ 256 KB as UTF-8). Scripts never run; every request it makes is blocked at render time except `data:` URIs and this product's own asset host.
  - `css` string: Stylesheet (≤ 128 KB as UTF-8). Like the html, it loads nothing but `data:` URIs and this product's own asset host; any other `@import` or `url()` is blocked at render time.
  - `width` integer: Viewport / output width in px (required on create; 1–4096)
  - `height` integer: Viewport / output height in px (required on create; 1–4096)
  - `variables` string[]: Placeholder names (at most 100, each letters, digits, `_` and `.`). Derived from the html/css when omitted. A render fills `{{ name }}` HTML-escaped and `{{{ name }}}` raw; a name the row does not carry renders as nothing.
  - `version` integer: Current version; every PUT increments it and keeps the old one readable as `id@version`
  - `builtIn` boolean: Whether this is a built-in template (read-only; PUT and DELETE are 403)
  - `createdAt` string, date-time: When the template — or, read as `id@version`, that version — was created
  - `updatedAt` string, date-time: When the current version was saved; absent on a version read as `id@version`

## Response `201`

The template, at version 1

## Other responses

- `400`: - `invalid_param` on `name` — missing, or over 200 characters - `invalid_param` on `html` / `css` — `html` missing, or either over its limit - `invalid_param` on `width` / `height` — missing, or outside 1–4096 - `invalid_param` on `variables` — more than 100, or a name that is not an identifier (letters, digits, `_` and `.`) - `invalid_param` — the `slug` you sent is malformed, starts with `builtin-`, or is already one of yours
- `409`: Conflict — `error.code` is `idempotency_key_reuse` (the key already means a different request) or `request_in_progress` (the first request under this key has not settled yet; retryable).
- `422`: `resource_limit_exceeded` — the account already owns 200 templates
- `429`: Rate limited — `error.code` is `rate_limited` and `retryable` is true. `Retry-After` says how many seconds to wait.

---

[API](https://skmtc.dev/imagestep/apis/imagestep-api.md) · [All operations](https://skmtc.dev/imagestep/apis/imagestep-api/llms.txt) · [OpenAPI document](https://skmtc.dev/imagestep/apis/imagestep-api/revisions/5e8e922a3085?raw)
