Skip to content
Dashboard

Email Templates

An email template is a saved email, with a name, a subject line and a body, that your organization’s workflows send to guests and team members. This guide covers the email-template operations the API exposes. Every field is listed on the operation’s page in the API Reference.

OperationRequestPermission
listEmailTemplatesGET /email-templatesmanage_organization
getEmailTemplateGET /email-templates/{id}manage_organization
create_email_templatePOST /email-templatesmanage_organization
modify_email_templatePOST /email-templates/{emailTemplateId}/modifymanage_organization
updateEmailTemplatePATCH /email-templates/{id}manage_organization

Paths are relative to your region’s base URL, for example https://api-us.suiteop.com/api/v1. See Authentication for permissions. There is no view-only permission: reading templates needs manage_organization too. Templates can’t be deleted, duplicated or test-sent through the API.

create_email_template and modify_email_template hand your text to an AI model, which writes the template. updateEmailTemplate writes exactly the values you send. Use it whenever you know the result you want.

GET /email-templates takes no parameters and is not paginated: data is every template in the organization, and meta carries only requestId. Rows are ordered by groupTag, then most recently edited first.

Terminal window
curl https://api-us.suiteop.com/api/v1/email-templates \
-H "Authorization: Bearer sk_live_your_key_here"
{
"data": [
{
"id": "3f2b8c1e-4a6d-4e2f-9b7a-1c5d8e0f2a13",
"name": "Pre-arrival welcome",
"subject": "Your stay at {{property_name}} starts soon",
"category": "transactional",
"groupTag": "guest_journey",
"isPublic": false,
"createdAt": "2026-05-02T10:14:07.412Z",
"updatedAt": "2026-08-13T09:30:00.000Z"
}
],
"meta": { "requestId": "3f1c9a52-…" }
}

Rows don’t include bodies. GET /email-templates/{id} returns the same fields plus:

  • htmlBody: the rendered HTML, which is what gets sent. There is no separate plain-text body.
  • editorJson: the visual editor’s block tree, as a JSON string. A template made of pasted HTML has no usable block tree.

Both are read in full even when a large body is stored separately, so getEmailTemplate is the place to read a body back after a write. A template in another organization returns 404.

category is transactional or marketing. groupTag is guest_journey, upsell, operations, post_stay or uncategorized. Either can be null.

Subjects and bodies use flat {{variable}} tokens, such as {{guest_first_name}}, {{check_in_date}} or {{property_name}}. They are filled in when the email is sent. Tokens also resolve with spaces inside the braces or in a different case ({{ brand_color }}, {{BRAND_COLOR}}).

The API has no operation that lists the available variables. To find them:

  • Use the variable menu in SuiteOp’s template editor or the workflow builder’s variable picker.
  • Read existing templates with getEmailTemplate to see which tokens they use.
  • Name the information you want in a create_email_template brief. The model is given the list of available tokens.

How tokens resolve:

  • Each variable needs its data. Property, reservation, device, task and other variables resolve only when the workflow’s trigger supplies that record. Otherwise they are sent as an empty string, not as the visible {{placeholder}}. Organization-level tokens such as {{organization_name}}, {{brand_logo}} and {{date_now}} always resolve.
  • A few declared tokens are always empty for now, including {{guest_birth_date}}, {{primary_guest_id}} and {{upsell_notes}}.
  • Links: a link or button URL built from a token is kept only when the token is a system-built URL, such as {{property_map_link}} or {{brand_logo}}. A URL made of any other token, for example {{guest_first_name}}, is replaced with #.

POST /email-templates drafts a new template from a plain-language brief. description is the only field, 1 to 20,000 characters. The model picks the name, subject line and layout from it, so say in the brief if you want a particular name, subject or set of {{variable}} tokens.

Terminal window
curl -X POST https://api-us.suiteop.com/api/v1/email-templates \
-H "Authorization: Bearer sk_live_your_key_here" \
-H "Idempotency-Key: 7c9e6679-7425-40de-944b-e07fc1f90ae7" \
-H "Content-Type: application/json" \
-d '{
"description": "A friendly pre-arrival email sent two days before check-in. Name it \"Pre-arrival welcome\". Greet the guest with {{guest_first_name}}, give {{check_in_date}} and {{check_in_time}} at {{property_name}}, and add a button linking to the guest portal."
}'
{
"data": {
"id": "3f2b8c1e-4a6d-4e2f-9b7a-1c5d8e0f2a13",
"name": "Pre-arrival welcome (2)"
},
"meta": { "requestId": "8a4d2f10-…" }
}

The response is 201 with the new template’s id and the name it was stored under.

  • Usable at once. The template is saved as an ordinary template, with no draft or publish step.
  • Names are made unique. If the name is already taken, (2), (3) and so on is appended. Use the returned name, not the one in your brief.
  • Not deterministic. Two identical calls produce different emails.
  • Slow. A call can take tens of seconds, so set a client timeout of a few minutes.
  • Upsells: upsell blocks can only use offers already in your organization’s upsell catalog.
  • No classification. A new template has no category or groupTag. Set them with updateEmailTemplate.
  • Any other body field, such as name or subject, is rejected with 400.

POST /email-templates/{emailTemplateId}/modify applies a plain-language instruction (1 to 20,000 characters) to an existing template. The server loads the current template and shows it to the model, so don’t fetch and send it yourself.

Terminal window
curl -X POST https://api-us.suiteop.com/api/v1/email-templates/3f2b8c1e-4a6d-4e2f-9b7a-1c5d8e0f2a13/modify \
-H "Authorization: Bearer sk_live_your_key_here" \
-H "Idempotency-Key: 1b4e28ba-2fa1-41d2-883f-0016d3cca427" \
-H "Content-Type: application/json" \
-d '{"instruction": "Warmer tone, and translate the whole email into Spanish."}'

The response is 200 with only data.id. Read the result with getEmailTemplate.

  • The whole template is rewritten, and the change goes live immediately. The previous state is kept in the template’s version history in SuiteOp.
  • The name and subject change only when the instruction asks. A new name is made unique with (2) like on create.
  • Pasted-HTML templates are refused with 400. Only templates built in the visual editor can be rewritten.
  • 409 conflict_error means someone saved the template while the model was running. Nothing was written, so send the request again.
  • Unknown template IDs, including another organization’s, return 404.

PATCH /email-templates/{id} sets the fields you send and leaves the rest unchanged. Unlike PATCH /tasks/{id}, the fields go at the top level of the body, not inside a data object.

FieldNotes
name1 to 200 characters, trimmed. Stored exactly as sent: unlike the AI operations, a duplicate name gets no (2)
subjectUp to 500 characters, trimmed. May contain {{variable}} tokens. null clears it
htmlBodyReplaces the body of a pasted-HTML template. Ignored on a template that has a block tree
editorJsonThe visual editor’s block tree as a JSON string. Replaces the tree and re-renders htmlBody from it
categorytransactional or marketing
groupTagguest_journey, upsell, operations, post_stay or uncategorized
expectedUpdatedAtOptional concurrency check. Send the updatedAt you last read, milliseconds included. If the template has changed since: 409
const base = 'https://api-us.suiteop.com/api/v1'
const headers = {
Authorization: `Bearer ${process.env.SUITEOP_API_KEY}`,
'Content-Type': 'application/json',
}
const { data: current } = await (
await fetch(`${base}/email-templates/${templateId}`, { headers })
).json()
const res = await fetch(`${base}/email-templates/${templateId}`, {
method: 'PATCH',
headers,
body: JSON.stringify({
subject: 'See you soon at {{property_name}}',
groupTag: 'guest_journey',
expectedUpdatedAt: current.updatedAt,
}),
})
if (res.status === 409) {
// Someone else saved the template: re-read it and re-apply your change.
}
const { data } = await res.json()

The response is 200 with id, name, subject, category, groupTag, createdAt and updatedAt. It does not include the body; read that back with getEmailTemplate. Every edit moves updatedAt, so use the new value for your next expectedUpdatedAt.

Body rules:

  • Block templates: to change the body, send editorJson. An htmlBody sent without it is silently ignored, and the call still succeeds.
  • Pasted-HTML templates: send htmlBody with the full new HTML.
  • An editorJson that is empty or doesn’t parse as a valid block tree is rejected with 400, and nothing is saved.
  • Any field not in the table, such as isPublic, is rejected with 400.

create_email_template and modify_email_template are model-backed. Besides your credential’s normal limit, they share a per-organization budget with the other AI-backed operations, by default 10 calls per 60 seconds. Going over it returns 429 rate_limit_error with a Retry-After header, and X-RateLimit-Remaining usually stays above 0, so pace retries by Retry-After. See Rate Limits.

An organization that has used up its monthly AI allowance also gets 429. That one doesn’t clear until the next UTC month.

If the model doesn’t produce a usable result after its retries, the call fails with 502 provider_error. For a create, no template is saved.