Skip to content
Cowliss is in beta: features and pricing may change.

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.

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.

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.

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.

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.

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.

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 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.