Templates and email design
The template variables, each brand's email layout and satisfaction email, previews and test sends, ticket values, and member profiles.
How templates work, and every variable, is described in Template variables and syntax.
| Method | Path | Description |
|---|---|---|
GET |
/v1/templates/variables |
The variables, slots and filters, with your custom fields |
GET |
/v1/email-templates/{brand_id} |
A brand’s layout and satisfaction email |
PUT |
/v1/email-templates/{brand_id}/{kind} |
Admin: save the layout or the satisfaction email |
DELETE |
/v1/email-templates/{brand_id}/{kind} |
Admin: go back to the default |
POST |
/v1/templates/preview |
Render a template without saving it |
POST |
/v1/templates/test |
Users only: send the rendered email to yourself |
GET |
/v1/tickets/{id}/variables |
Every variable’s value for a ticket |
POST |
/v1/tickets/{id}/render |
Fill a template for a ticket |
PATCH |
/v1/members/{user_id}/profile |
Users only: change a member’s profile |
Scopes: settings:read for the variables and a brand’s templates, settings:write to save or reset them, and tickets:read (an agent or above) for previews, test sends and the ticket endpoints.
Variables
Section titled “Variables”type TemplateCatalog = { variables: TemplateVariable[]; slots: TemplateSlot[]; filters: string[] };
type TemplateVariable = { key: string; // "contact.first_name", "ticket.fields.order_number" group: "contact" | "company" | "ticket" | "agent" | "team" | "brand" | "mailbox" | "workspace" | "date"; label: string; description: string; example: string; html: boolean; // true for agent.signature, which inserts HTML};type TemplateSlot = { key: "content" | "signature" | "history" | "survey"; label: string; description: string; required_in: TemplateKind[];};type TemplateKind = "reply" | "signature" | "personal_signature" | "auto_reply" | "layout" | "csat" | "subject";GET /v1/templates/variables returns the built-in variables followed by one entry per custom field that is not archived. Build pickers from it rather than keeping your own list.
Email templates
Section titled “Email templates”Each brand has two: its layout and its satisfaction (csat) email.
type EmailTemplate = { brand_id: string; kind: "layout" | "csat"; subject: string; // csat only; "" = the default body_html: string; // layout: the custom layout (mode "custom"); csat: the email body options: EmailLayoutOptions | {}; // layout only is_default: boolean; // true when the brand has not changed it; the fields carry the default updated_at: string | null;};type EmailLayoutOptions = { mode: "simple" | "custom"; style: "plain" | "branded"; logo: boolean; // the brand's logo on top accent: string; // "#rrggbb", or "" for the brand colour font: "system" | "sans" | "serif"; footer_html: string; // takes variables; "" = none history: "none" | "last" | "all"; // quote the previous message, or the whole conversation (up to 10) history_header: string; // {{ message.author }}, {{ message.email }} and {{ message.date }}; "" = the default};GET /v1/email-templates/{brand_id} returns { layout: EmailTemplate, csat: EmailTemplate }.
PUT /v1/email-templates/{brand_id}/{kind} takes any of subject, body_html and options; what you leave out keeps its current value. A layout is up to 128 KB and its footer up to 16 KB; a satisfaction email’s body is up to 128 KB and its subject up to 200 characters, as is the quote line.
curl -X PUT https://api.opendesk.sh/v1/email-templates/$BRAND_ID/layout \ -H "Authorization: Bearer od_live_..." \ -H "Content-Type: application/json" \ -d '{ "options": { "mode": "simple", "style": "branded", "logo": true, "accent": "", "font": "system", "footer_html": "<p>{{ brand.name }}, 1 Main Street, Springfield</p>", "history": "last", "history_header": "" } }'A custom layout (mode: "custom") must contain {{ content }} and may use {{ signature }} and {{ history }}. A satisfaction email body must contain {{ survey }}. Templates are sanitized with the design allowlist: email HTML plus tables, inline style with safe properties, width, height, bgcolor, valign, and mailto: and tel: links.
A template with an unknown variable, a tag that does not parse, a slot it may not use or a missing required slot is 422 validation_failed, with one entry in errors per problem. value is the tag at fault:
{ "status": 422, "code": "validation_failed", "detail": "There is no variable called contact.frist_name.", "errors": [ { "message": "There is no variable called contact.frist_name.", "location": "body.body_html", "value": "{{ contact.frist_name }}" } ]}DELETE /v1/email-templates/{brand_id}/{kind} puts the template back to the default and answers 204.
Preview and test
Section titled “Preview and test”POST /v1/templates/preview renders a template without saving it:
type PreviewRequest = { kind: TemplateKind; body_html?: string; subject?: string; options?: EmailLayoutOptions; brand_id?: string; team_id?: string; mailbox_id?: string; ticket_id?: string; // fill from this ticket, else sample data};type TemplatePreview = { subject: string; html: string; text: string; problems: { message: string; token: string }[] };html is the whole email as the customer would see it: a signature or auto-reply inside its brand’s layout, a layout or satisfaction email around sample content. Values come from ticket_id (a ticket you can see) or sample data, with you as the agent. Problems are returned, not refused, so an editor can show them as you type.
POST /v1/templates/test takes the same body and sends the rendered email to your own login address, with [Test] before the subject. It needs a user, not an API key, and allows 10 an hour per user (429 after that).
Ticket values
Section titled “Ticket values”GET /v1/tickets/{id}/variables?mailbox_id= returns every variable’s value for the ticket, with you as the agent, and the signature your email reply from mailbox_id (else the ticket’s address) would carry:
{ "values": { "contact.first_name": "Jane", "ticket.number": "1042", "team.name": "Billing" }, "signature_html": "<p>Alex Morgan, Support engineer</p>"}POST /v1/tickets/{id}/render takes { body_html } and returns { body_html }, filled for the ticket and sanitized like a reply. The web app uses it to insert saved replies.
curl https://api.opendesk.sh/v1/tickets/$TICKET_ID/render \ -H "Authorization: Bearer $ACCESS_TOKEN" \ -H "OD-Workspace: $WORKSPACE_ID" \ -H "Content-Type: application/json" \ -d '{ "body_html": "<p>Hi {{ contact.first_name | default: \"there\" }},</p>" }'Both need the ticket to be visible to you; a ticket of a private team you are not on is 404. You do not need these to send a reply: a reply posted to POST /v1/tickets/{id}/messages is filled when it is created.
Member profiles
Section titled “Member profiles”type MemberProfile = { title: string; // up to 80 characters phone: string; // up to 40 pronouns: string; // up to 40 signature_html: string; // the personal signature; "" = none, up to 64 KB};Every Member carries its profile. PATCH /v1/members/{user_id}/profile takes any of the four fields and returns the Member. user_id may be me; members edit their own profile and admins anyone’s. Title, phone and pronouns are one line of plain text. The personal signature takes every variable except {{ agent.signature }}.
curl -X PATCH https://api.opendesk.sh/v1/members/me/profile \ -H "Authorization: Bearer $ACCESS_TOKEN" \ -H "OD-Workspace: $WORKSPACE_ID" \ -H "Content-Type: application/json" \ -d '{ "title": "Support engineer", "pronouns": "they/them" }'Events
Section titled “Events”email_template.updated ({ brand_id, kind }) when a brand’s layout or satisfaction email is saved or reset; refetch it. A profile change publishes member.updated.