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.
| Operation | Request | Permission |
|---|---|---|
listEmailTemplates | GET /email-templates | manage_organization |
getEmailTemplate | GET /email-templates/{id} | manage_organization |
create_email_template | POST /email-templates | manage_organization |
modify_email_template | POST /email-templates/{emailTemplateId}/modify | manage_organization |
updateEmailTemplate | PATCH /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.
Listing templates
Section titled “Listing templates”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.
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.
Template variables
Section titled “Template variables”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
getEmailTemplateto see which tokens they use. - Name the information you want in a
create_email_templatebrief. 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#.
Drafting a template with AI
Section titled “Drafting a template with AI”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.
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 returnedname, 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
categoryorgroupTag. Set them withupdateEmailTemplate. - Any other body field, such as
nameorsubject, is rejected with400.
Rewriting a template with AI
Section titled “Rewriting a template with AI”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.
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_errormeans 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.
Editing fields directly
Section titled “Editing fields directly”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.
| Field | Notes |
|---|---|
name | 1 to 200 characters, trimmed. Stored exactly as sent: unlike the AI operations, a duplicate name gets no (2) |
subject | Up to 500 characters, trimmed. May contain {{variable}} tokens. null clears it |
htmlBody | Replaces the body of a pasted-HTML template. Ignored on a template that has a block tree |
editorJson | The visual editor’s block tree as a JSON string. Replaces the tree and re-renders htmlBody from it |
category | transactional or marketing |
groupTag | guest_journey, upsell, operations, post_stay or uncategorized |
expectedUpdatedAt | Optional 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. AnhtmlBodysent without it is silently ignored, and the call still succeeds. - Pasted-HTML templates: send
htmlBodywith the full new HTML. - An
editorJsonthat is empty or doesn’t parse as a valid block tree is rejected with400, and nothing is saved. - Any field not in the table, such as
isPublic, is rejected with400.
Rate limits for AI operations
Section titled “Rate limits for AI operations”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.