Templates
Reusable message content: a subject, an HTML and/or plain-text body, and {{ placeholder }} variables filled in at send time.
The template object
{
"id": "…",
"name": "Welcome",
"subject": "Welcome, {{ contact.name }}",
"body_html": "<p>Hello {{ contact.name }}</p>",
"body_text": "Hello {{ contact.name }}",
"status": "published",
"editor_state": null,
"folder_id": null,
"created_at": "…",
"updated_at": "…"
}editor_state is the dashboard editor's own block document. Leave it alone from the API — a template created without one opens in the editor as a single raw-HTML block, which is fine.
Draft and published
status is draft or published, and it is enforced rather than decorative: a draft cannot be used to send.
| Path | What a draft does |
|---|---|
POST /v1/send with template_id | 422 notifyzr.template.error.not_published, before the message is recorded and before quota is spent |
| A broadcast | Refused at creation |
An automation send_email action | The step is recorded as skipped — an automation has no caller to hand a 422 to |
Three rules about the status itself:
- A created template defaults to
draft. Pass"status": "published"to publish on creation. - An update that omits
statuskeeps the stored one. The editor saves constantly and must never unpublish your live content by accident. - A draft may be empty; publishing needs at least one body. Publishing without one is
422 notifyzr.template.error.body_required.
Create, update, delete
POST /v1/templates
GET /v1/templates
GET /v1/templates/{id}
PATCH /v1/templates/{id}
DELETE /v1/templates/{id}| Field | Required | Notes |
|---|---|---|
name | yes | Up to 255 characters. |
subject | no | Up to 255. |
body_html | no | |
body_text | no | Send both for a multipart email. |
status | no | draft (default on create) or published. |
GET /v1/templates takes an optional status filter and is not paginated.
Deleting a template does not affect messages already sent.
Placeholders
A template's body_html and body_text support {{ placeholder }} interpolation, filled at send time.
{
"message": {
"subject": "Welcome aboard",
"template_id": "…",
"variables": { "contact.name": "Ada" }
}
}Keys are flat. "contact.name" fills {{ contact.name }}; a nested {"contact": {"name": …}} does not — the renderer looks the whole placeholder up as a single key rather than walking a path.
Only the template's bodies are rendered. A subject or an inline body[] you send with the request is used verbatim.
In an automation it works the other way round: there is no variables to supply, and placeholders resolve against the triggering event — {{ data.values.email }} reaches a form submission's email field.
An unresolved placeholder renders as an empty string rather than failing the send, so a wrong variable shape shows up as a gap in the email.
Folders
POST /v1/template-folders
GET /v1/template-folders
PATCH /v1/template-folders/{id}
DELETE /v1/template-folders/{id}
POST /v1/templates/{id}/moveFolders are organisational only — they do not affect how a template is resolved. They nest three levels deep; deeper is notifyzr.template_folder.error.depth_exceeded, and moving a folder inside itself is …error.cycle.
{
"id": "…",
"parent_id": null,
"name": "Onboarding",
"template_count": 4,
"descendant_folder_count": 1,
"descendant_template_count": 7,
"created_at": "…",
"updated_at": "…"
}template_count is what sits directly in the folder; the two descendant_ counts include everything below it — which is what a delete confirmation needs to say what it is about to remove.
POST /v1/templates/{id}/move takes the destination folder_id, or null for the root.