Templates
Templates are the email templates an automation sends, each addressed by the key its author gave the file. A key is unique per app, so two apps may each have a welcome, and a send only ever uses its own app’s. Every route that names a key also takes appId, and a request without it is a 422 naming the parameter. A template is Liquid HTML (a subject and a body rendered with the automation’s props as the scope) or a doc; both are edited in the dashboard as well as pushed from a repo, and a push, create and update here write either; every version records who wrote it, so a push over a template edited here is a conflict until the repo pulls. A template has versions and nothing else, no switch, because a send resolves the latest ready version at the moment it sends, so fixing copy reaches executions already in flight.
GET /v1/templates
Section titled “GET /v1/templates”List the organization’s email templates
Auth: Session token.
Query parameters
{ "cursor": "string", "limit": 50, "appId": "string", "q": "string"}| Field | Type | Required | Constraints |
|---|---|---|---|
cursor |
string | no | |
limit |
integer | no | default 50; 1 ≤ x ≤ 100 |
appId |
string | no | length 0–200 |
q |
string | no | length 0–200 |
Response 200
{ "data": [ { "key": "string", "appId": "string", "tags": {}, "purpose": "string", "kind": "liquid", "latestVersion": { "id": "string", "status": "compiling", "appId": "string", "error": "string", "protocol": 42, "source": "push", "pushedAt": "2026-01-15T09:30:00.000Z", "bundle": "string" }, "automations": [ "string" ], "template": { "kind": "liquid", "subject": "string", "purpose": "string", "tags": [], "preview": "string", "html": "string" } } ], "meta": { "requestId": "string", "nextCursor": "string", "hasMore": true, "warnings": [ { "code": "string", "message": "string", "suggestion": "string" } ] }}| Field | Type | Required | Constraints |
|---|---|---|---|
data |
object[] | yes | |
data[] |
object | yes | |
data[].key |
string | yes | length 0–64 |
data[].appId |
string | yes | |
data[].tags |
any | yes | |
data[].purpose |
string | null | yes | |
data[].kind |
“liquid” | “doc” | “module” | yes | |
data[].latestVersion |
object | yes | |
data[].latestVersion.id |
string | yes | |
data[].latestVersion.status |
“compiling” | “ready” | “failed” | yes | |
data[].latestVersion.appId |
string | yes | |
data[].latestVersion.error |
string | null | yes | |
data[].latestVersion.protocol |
integer | yes | -2147483648 ≤ x ≤ 2147483647 |
data[].latestVersion.source |
“push” | “api” | yes | |
data[].latestVersion.pushedAt |
string (date-time) | yes | |
data[].latestVersion.bundle |
string | yes | |
data[].automations |
string[] | yes | |
data[].template |
object | object | null | yes | |
meta |
object | no | |
meta.requestId |
string | no | |
meta.nextCursor |
string | null | no | |
meta.hasMore |
boolean | no | |
meta.warnings |
object[] | no | |
meta.warnings[] |
object | no | |
meta.warnings[].code |
string | yes | |
meta.warnings[].message |
string | yes | |
meta.warnings[].suggestion |
string | no |
Errors follow the shared envelope and code table.
POST /v1/templates
Section titled “POST /v1/templates”Create a template
Admin only. Writes the same version a push of emails/<key>.html or emails/<key>.md would: Liquid HTML (kind: "liquid", a subject and an html body rendered with the automation’s props as the scope) or a Maily doc (kind: "doc", whose {{variables}} are filled from the person’s traits and the automation’s props, props winning). The version records that the API wrote it, so a later push of the same key is a conflict until the repo pulls. Refused with conflict when the app already has a template of that key.
Auth: Session token.
Request body
{ "data": { "appId": "string", "key": "string", "template": { "kind": "liquid", "subject": "string", "purpose": "string", "tags": [], "preview": "string", "html": "string" } }}| Field | Type | Required | Constraints |
|---|---|---|---|
data |
object | yes | |
data.appId |
string | yes | length 0–200 |
data.key |
string | yes | length 0–64 |
data.template |
object | object | yes |
Response 201
{ "data": { "key": "string", "appId": "string", "tags": {}, "purpose": "string", "kind": "liquid", "latestVersion": { "id": "string", "status": "compiling", "appId": "string", "error": "string", "protocol": 42, "source": "push", "pushedAt": "2026-01-15T09:30:00.000Z", "bundle": "string" }, "automations": [ "string" ], "template": { "kind": "liquid", "subject": "string", "purpose": "string", "tags": [], "preview": "string", "html": "string" } }, "meta": { "requestId": "string", "nextCursor": "string", "hasMore": true, "warnings": [ { "code": "string", "message": "string", "suggestion": "string" } ] }}| Field | Type | Required | Constraints |
|---|---|---|---|
data |
object | yes | |
data.key |
string | yes | length 0–64 |
data.appId |
string | yes | |
data.tags |
any | yes | |
data.purpose |
string | null | yes | |
data.kind |
“liquid” | “doc” | “module” | yes | |
data.latestVersion |
object | yes | |
data.latestVersion.id |
string | yes | |
data.latestVersion.status |
“compiling” | “ready” | “failed” | yes | |
data.latestVersion.appId |
string | yes | |
data.latestVersion.error |
string | null | yes | |
data.latestVersion.protocol |
integer | yes | -2147483648 ≤ x ≤ 2147483647 |
data.latestVersion.source |
“push” | “api” | yes | |
data.latestVersion.pushedAt |
string (date-time) | yes | |
data.latestVersion.bundle |
string | yes | |
data.automations |
string[] | yes | |
data.template |
object | object | null | yes | |
meta |
object | no | |
meta.requestId |
string | no | |
meta.nextCursor |
string | null | no | |
meta.hasMore |
boolean | no | |
meta.warnings |
object[] | no | |
meta.warnings[] |
object | no | |
meta.warnings[].code |
string | yes | |
meta.warnings[].message |
string | yes | |
meta.warnings[].suggestion |
string | no |
Errors follow the shared envelope and code table.
PATCH /v1/templates/{key}
Section titled “PATCH /v1/templates/{key}”Save a template’s next version
Admin only. The template, Liquid HTML or a doc, becomes the key’s newest version; running automations send it from their next email on. A later push that carries a different file of the same key is a conflict it has to confirm or force.
Auth: Session token.
Query parameters
{ "appId": "string"}| Field | Type | Required | Constraints |
|---|---|---|---|
appId |
string | yes |
Request body
{ "data": { "template": { "kind": "liquid", "subject": "string", "purpose": "string", "tags": [], "preview": "string", "html": "string" } }}| Field | Type | Required | Constraints |
|---|---|---|---|
data |
object | yes | |
data.template |
object | object | yes |
Response 200
{ "data": { "key": "string", "appId": "string", "tags": {}, "purpose": "string", "kind": "liquid", "latestVersion": { "id": "string", "status": "compiling", "appId": "string", "error": "string", "protocol": 42, "source": "push", "pushedAt": "2026-01-15T09:30:00.000Z", "bundle": "string" }, "automations": [ "string" ], "template": { "kind": "liquid", "subject": "string", "purpose": "string", "tags": [], "preview": "string", "html": "string" } }, "meta": { "requestId": "string", "nextCursor": "string", "hasMore": true, "warnings": [ { "code": "string", "message": "string", "suggestion": "string" } ] }}| Field | Type | Required | Constraints |
|---|---|---|---|
data |
object | yes | |
data.key |
string | yes | length 0–64 |
data.appId |
string | yes | |
data.tags |
any | yes | |
data.purpose |
string | null | yes | |
data.kind |
“liquid” | “doc” | “module” | yes | |
data.latestVersion |
object | yes | |
data.latestVersion.id |
string | yes | |
data.latestVersion.status |
“compiling” | “ready” | “failed” | yes | |
data.latestVersion.appId |
string | yes | |
data.latestVersion.error |
string | null | yes | |
data.latestVersion.protocol |
integer | yes | -2147483648 ≤ x ≤ 2147483647 |
data.latestVersion.source |
“push” | “api” | yes | |
data.latestVersion.pushedAt |
string (date-time) | yes | |
data.latestVersion.bundle |
string | yes | |
data.automations |
string[] | yes | |
data.template |
object | object | null | yes | |
meta |
object | no | |
meta.requestId |
string | no | |
meta.nextCursor |
string | null | no | |
meta.hasMore |
boolean | no | |
meta.warnings |
object[] | no | |
meta.warnings[] |
object | no | |
meta.warnings[].code |
string | yes | |
meta.warnings[].message |
string | yes | |
meta.warnings[].suggestion |
string | no |
Errors follow the shared envelope and code table.
GET /v1/templates/{key}
Section titled “GET /v1/templates/{key}”Inspect one email template
Auth: Session token.
Query parameters
{ "appId": "string"}| Field | Type | Required | Constraints |
|---|---|---|---|
appId |
string | yes |
Response 200
{ "data": { "key": "string", "appId": "string", "tags": {}, "purpose": "string", "kind": "liquid", "latestVersion": { "id": "string", "status": "compiling", "appId": "string", "error": "string", "protocol": 42, "source": "push", "pushedAt": "2026-01-15T09:30:00.000Z", "bundle": "string" }, "automations": [ "string" ], "template": { "kind": "liquid", "subject": "string", "purpose": "string", "tags": [], "preview": "string", "html": "string" } }, "meta": { "requestId": "string", "nextCursor": "string", "hasMore": true, "warnings": [ { "code": "string", "message": "string", "suggestion": "string" } ] }}| Field | Type | Required | Constraints |
|---|---|---|---|
data |
object | yes | |
data.key |
string | yes | length 0–64 |
data.appId |
string | yes | |
data.tags |
any | yes | |
data.purpose |
string | null | yes | |
data.kind |
“liquid” | “doc” | “module” | yes | |
data.latestVersion |
object | yes | |
data.latestVersion.id |
string | yes | |
data.latestVersion.status |
“compiling” | “ready” | “failed” | yes | |
data.latestVersion.appId |
string | yes | |
data.latestVersion.error |
string | null | yes | |
data.latestVersion.protocol |
integer | yes | -2147483648 ≤ x ≤ 2147483647 |
data.latestVersion.source |
“push” | “api” | yes | |
data.latestVersion.pushedAt |
string (date-time) | yes | |
data.latestVersion.bundle |
string | yes | |
data.automations |
string[] | yes | |
data.template |
object | object | null | yes | |
meta |
object | no | |
meta.requestId |
string | no | |
meta.nextCursor |
string | null | no | |
meta.hasMore |
boolean | no | |
meta.warnings |
object[] | no | |
meta.warnings[] |
object | no | |
meta.warnings[].code |
string | yes | |
meta.warnings[].message |
string | yes | |
meta.warnings[].suggestion |
string | no |
Errors follow the shared envelope and code table.
POST /v1/templates/{key}/preview
Section titled “POST /v1/templates/{key}/preview”Render a template with the props you pass
Auth: Session token.
Query parameters
{ "appId": "string"}| Field | Type | Required | Constraints |
|---|---|---|---|
appId |
string | yes |
Request body
{ "data": { "props": { "key": {} } }}| Field | Type | Required | Constraints |
|---|---|---|---|
data |
object | yes | |
data.props |
record<string, any> | no |
Response 200
{ "data": { "subject": "string", "html": "string", "text": "string", "versionId": "string" }, "meta": { "requestId": "string", "nextCursor": "string", "hasMore": true, "warnings": [ { "code": "string", "message": "string", "suggestion": "string" } ] }}| Field | Type | Required | Constraints |
|---|---|---|---|
data |
object | yes | |
data.subject |
string | yes | |
data.html |
string | yes | |
data.text |
string | yes | |
data.versionId |
string | yes | |
meta |
object | no | |
meta.requestId |
string | no | |
meta.nextCursor |
string | null | no | |
meta.hasMore |
boolean | no | |
meta.warnings |
object[] | no | |
meta.warnings[] |
object | no | |
meta.warnings[].code |
string | yes | |
meta.warnings[].message |
string | yes | |
meta.warnings[].suggestion |
string | no |
Errors follow the shared envelope and code table.
POST /v1/templates/{key}/test-send
Section titled “POST /v1/templates/{key}/test-send”Send a rendered template to your own address
Sends through your organization’s default sender. The test goes to you regardless of your consent settings, costs nothing, and shows as a test. Its subject starts with [test] .
Auth: Session token.
Query parameters
{ "appId": "string"}| Field | Type | Required | Constraints |
|---|---|---|---|
appId |
string | yes |
Request body
{ "data": { "props": {} }}| Field | Type | Required | Constraints |
|---|---|---|---|
data |
object | yes | |
data.props |
record<string, any> | no | default {} |
Response 201
{ "data": { "id": "string", "orgId": "string", "profileId": "string", "automation": "string", "executionId": "string", "appId": "string", "broadcastId": "string", "kind": "automation", "step": "string", "channel": "email", "status": "sent", "dryRun": true, "recipient": "string", "providerId": "string", "error": "string", "createdAt": "2026-01-15T09:30:00.000Z", "updatedAt": "2026-01-15T09:30:00.000Z", "sentAt": "2026-01-15T09:30:00.000Z", "settledAt": "2026-01-15T09:30:00.000Z", "openedAt": "2026-01-15T09:30:00.000Z", "openCount": 42, "spamScore": 42, "spamSymbols": [ "string" ] }, "meta": { "requestId": "string", "nextCursor": "string", "hasMore": true, "warnings": [ { "code": "string", "message": "string", "suggestion": "string" } ] }}| Field | Type | Required | Constraints |
|---|---|---|---|
data |
object | yes | |
data.id |
string | yes | |
data.orgId |
string | yes | |
data.profileId |
string | null | yes | |
data.automation |
string | null | yes | |
data.executionId |
string | null | yes | |
data.appId |
string | null | yes | |
data.broadcastId |
string | null | yes | |
data.kind |
“automation” | “test” | “api” | “broadcast” | yes | |
data.step |
string | yes | |
data.channel |
“email” | “webhook” | yes | |
data.status |
“sent” | “delivered” | “bounced” | “complained” | “failed” | “skipped_disabled” | “skipped_paused” | “skipped_suspended” | “skipped_suppressed” | “skipped_quota” | “skipped_unknown_purpose” | “skipped_frequency_cap” | “skipped_consent” | “skipped_domain” | “skipped_ssrf” | “skipped_spam” | “would_send” | “would_skip_disabled” | “would_skip_paused” | “would_skip_suspended” | “would_skip_suppressed” | “would_skip_quota” | “would_skip_unknown_purpose” | “would_skip_frequency_cap” | “would_skip_consent” | “would_skip_domain” | “would_skip_ssrf” | “would_skip_spam” | yes | |
data.dryRun |
boolean | yes | |
data.recipient |
string | null | yes | |
data.providerId |
string | null | yes | |
data.error |
string | null | yes | |
data.createdAt |
string (date-time) | yes | |
data.updatedAt |
string (date-time) | yes | |
data.sentAt |
string (date-time) | null | yes | |
data.settledAt |
string (date-time) | null | yes | |
data.openedAt |
string (date-time) | null | yes | |
data.openCount |
integer | yes | -2147483648 ≤ x ≤ 2147483647 |
data.spamScore |
number | null | yes | |
data.spamSymbols |
string[] | null | yes | |
meta |
object | no | |
meta.requestId |
string | no | |
meta.nextCursor |
string | null | no | |
meta.hasMore |
boolean | no | |
meta.warnings |
object[] | no | |
meta.warnings[] |
object | no | |
meta.warnings[].code |
string | yes | |
meta.warnings[].message |
string | yes | |
meta.warnings[].suggestion |
string | no |
Errors follow the shared envelope and code table.
DELETE /v1/templates/{key}
Section titled “DELETE /v1/templates/{key}”Delete an email template
Admin only. Deleting a key deletes every version of it in its app, and is refused while an automation still sends it, naming the automations.
Auth: Session token.
Query parameters
{ "appId": "string"}| Field | Type | Required | Constraints |
|---|---|---|---|
appId |
string | yes |
Response 200
{ "data": { "deleted": true }, "meta": { "requestId": "string", "nextCursor": "string", "hasMore": true, "warnings": [ { "code": "string", "message": "string", "suggestion": "string" } ] }}| Field | Type | Required | Constraints |
|---|---|---|---|
data |
object | yes | |
data.deleted |
true | yes | |
meta |
object | no | |
meta.requestId |
string | no | |
meta.nextCursor |
string | null | no | |
meta.hasMore |
boolean | no | |
meta.warnings |
object[] | no | |
meta.warnings[] |
object | no | |
meta.warnings[].code |
string | yes | |
meta.warnings[].message |
string | yes | |
meta.warnings[].suggestion |
string | no |
Errors follow the shared envelope and code table.