Email templates: rewrite every email your organization sends

Embed
Preview of the card
<a href="https://forumulate.com/guides/email-templates"><img src="https://forumulate.com/guides/_api/public/orgs/046a9bc0-d530-43ae-9bae-af7945008cda/spaces/36aa4df0-1e95-4cef-8ae9-795169441345/pages/c26494e2-e909-46da-bdb9-18bb6810c6e0/social-card.svg?v=3e7ae33eec8b" alt="Email templates: rewrite every email your organization sends" width="600" height="315"></a>

Every email Forumulate sends on your organization's behalf is a template you can rewrite: member invitations, notifications (replies, mentions, moderation notices and the rest), digests and the weekly summary, contact and form alerts, replies from the contact and careers inboxes, status-page alerts, newsletter confirmations, new-post emails, broadcasts and roadmap updates.

You can change each one for the whole organization or for one space, in one language or all of them, and every one is drawn inside your brand: your logo, colours, type, footer and links.

Open it from Advanced → Emails in the console (/o/<org>/emails).

What you can change

Each email has:

  • Subject, the line in the inbox.

  • Preheader, the grey line an inbox shows after the subject. Leave it empty for none.

  • Blocks, the message itself, built from a small set that every mail client can draw:

Block

What it is

Heading

A title, three sizes.

Text

A paragraph. Line breaks are kept. Can be set smaller and quieter.

Button

A link drawn as a button, with the address spelled out underneath unless you turn it off.

Image

An image, optionally linked, up to 600px wide.

Divider

A rule.

Space

Small, medium or large vertical space.

Small print

Quiet text inside the message.

Text, headings and small print accept two pieces of markup: **bold** and [label](https://address).

The list of blocks is deliberately short. An email is read in Outlook on Windows and in Gmail's app as well as in a browser, and each of these blocks has a rendering (tables and inline styles) that survives all of them.

Variables

Write {{variable}} to put a value in. Each email lists the variables it can use, with an example of each, beside the editor: click one to insert it where your cursor is. For example, the contact-inbox reply can use {{recipient.name}}, {{reply.body}} and {{original.subject}}.

Every email can also use:

Variable

Value

{{org.name}}

Your organization's name.

{{space.name}}

The space the email is about, when it is about one.

{{recipient.email}}

The address the email is going to.

{{unsubscribe.url}}

The one-click unsubscribe address, on mail that has one.

Rules that keep a template safe:

  • A misspelled variable is refused when you save. The save answers 422 and names the variable and where it is, for example Unknown variable {{recipient.nmae}} in blocks[0].text. A template that saves is a template every send can fill.

  • Every value is escaped. A name, a topic title or a message a stranger typed into your contact form is always shown as text. There is no syntax that turns escaping off, so a value can never become a link, bold text or HTML. Markup only ever comes from the template you wrote.

  • A block whose variables all come out empty is left out. "Sent from: {{submission.pageUrl}}" disappears when the browser did not say which page a form was sent from, rather than arriving as "Sent from:".

  • A button or link whose address does not work is left out, rather than sent broken. Addresses must be absolute http(s):// or mailto:.

Spaces and languages

A version is saved for a scope (the whole organization, or one space) and a language (one of English, Spanish, French, German, Portuguese and Arabic, or every language).

When an email is sent, the version used is chosen in this order:

  1. this space's version in the recipient's language

  2. this space's version for every language

  3. the organization's version in the recipient's language

  4. the organization's version for every language

  5. the default

The space comes first, then the language. A space that rewrote its welcome email did so for a reason the organization-wide French translation knows nothing about. The recipient's language is the one they chose for Forumulate. When it is not known (a newsletter subscriber with no account, for example), English is used, which falls back to the every-language version.

Reset to default deletes the version for the scope and language you are looking at. The email then falls back down the list above.

Preview and test sends

The preview beside the editor is drawn by the same code that sends the email, using your brand and the example value of each variable. Switch between desktop and phone widths, light and dark, and the HTML and plain-text parts.

Send me a test emails the draft you are looking at, saved or not, to your own address through your organization's mail provider. You get five every ten minutes. Tests only ever go to the address of the person sending them.

Every email has a plain-text part as well as the HTML one. It is made from the same blocks, so you never write it separately.

The sending address

Mail goes out from Forumulate's address by default. An organization can send from its own instead, and carry its brand settings with it. One read returns both, so a settings screen needs a single request:

GET /orgs/{orgId}/email-settings
{
  "emailFrom": "Acme Help <help@acme.example>",
  "branding": { "footer": "Acme Ltd, 1 High Street", "subjectPrefix": "[Acme] ", "hideBranding": false }
}

emailFrom is null until you set one. It needs org.read. To change it, use PATCH /orgs/{orgId}/email-from { "emailFrom": "..." } (send null to clear; up to 320 characters), which needs org.manage_settings. The branding object is the same one …/email-branding reads and writes, described in the next section.

The brand

The Brand tab sets what every email is drawn in:

  • Header: a logo (an image address, or one from your library) with its width and description, or a name shown when there is no logo.

  • Colours: buttons and links, button text, background, message card, text and quiet text, plus a dark set. Mail apps that support dark mode use the dark set, and the emails declare color-scheme: light dark. Turn dark mode off and the emails ask to stay light.

  • Type: a font stack that needs nothing downloaded (system, Helvetica, Arial, Verdana, Georgia and others). You can add a web font in front of it, with its stylesheet address, for the mail apps that load one. Outlook on Windows never does, which is why the stack underneath matters.

  • Footer: footer text, a postal address (bulk mail needs one under CAN-SPAM), footer links and social links.

  • Subject prefix: prepended to every subject, once.

  • Hide the "Sent with Forumulate" line.

A message with an unsubscribe address always carries an Unsubscribe link in its footer, whatever the template says. You cannot remove it, because mail providers require it of bulk mail.

Timing

A saved version shows up in the preview at once and in delivered mail within a minute. The sending worker caches each organization's templates for a minute so that a newsletter to thousands of subscribers reads them once.

API

Reading needs org.read. Every change, preview and test send needs the email.manage capability, which Owners and Admins hold, or the email:manage token scope. The brand is at /orgs/{orgId}/email-branding and needs org.manage_settings to change.

Method and path

What it does

GET /orgs/{orgId}/email-templates

Every customizable email, with the versions saved for each.

GET /orgs/{orgId}/email-templates/{key}

One email: its variables, its default and every saved version.

PUT /orgs/{orgId}/email-templates/{key}

Save a version: { spaceId?, locale?, subject, preheader?, blocks }.

DELETE /orgs/{orgId}/email-templates/{key}

Reset a version. ?spaceId= and ?locale= pick which one.

POST /orgs/{orgId}/email-templates/{key}/preview

Render: { spaceId?, locale?, content?, vars?, scheme? }.

POST /orgs/{orgId}/email-templates/{key}/test

Send yourself a test: { spaceId?, locale?, content? }.

GET / PATCH /orgs/{orgId}/email-branding

Read or change the brand. null clears a key back to its default.

Keys look like "member.invite", "contact.reply", "newsletter.issue" and "notification.topic.reply". GET …/email-templates lists them all.

curl -X PUT "$API/orgs/$ORG/email-templates/member.invite" \
  -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
  -d '{
    "locale": "fr",
    "subject": "Rejoignez {{org.name}}",
    "preheader": "{{inviter.name}} vous invite",
    "blocks": [
      { "type": "heading", "text": "Bienvenue chez {{org.name}}" },
      { "type": "text", "text": "**{{inviter.name}}** vous invite à nous rejoindre." },
      { "type": "button", "label": "Accepter", "url": "{{link}}" }
    ]
  }'

Account emails about a person's own Forumulate sign-in (address verification and password reset) are sent by Forumulate, from our domain, and are not customizable. The API answers 422 if you try.

Bounces, replies and unsubscribes

Four addresses receive mail traffic rather than send it. Only the first one needs anything from you.

Bounces from your own mail provider. When the organization sends through its own provider (Brevo, SendGrid, Postmark, Mailgun or Resend, connected with PUT /orgs/{orgId}/mail/provider), GET /orgs/{orgId}/mail/provider returns a webhookUrl. Paste it into the provider's event webhook setting. It is POST /mail/webhooks/{provider}/{orgId}/{token}, where the token is signed for your organization, so a wrong one is a 401 and an unknown provider a 404. A hard bounce or a spam complaint stops your organization's mail to that address (it shows in GET /orgs/{orgId}/mail/suppressions, and DELETE on the same address clears it); a soft bounce only counts toward the bounce score. What your provider reports applies to your organization alone: it never stops mail that other organizations or the platform itself send to that address. An event type we do not act on answers { "ingested": 0 } rather than an error, because providers switch off webhooks that keep failing.

Bounces from our own sending. Mail that goes out through the platform's account reports its bounces and complaints to POST /mail/events, signed with our own shared secret. It feeds the platform-wide suppression list, which stops all mail to an address. You configure nothing.

Replies by email. The reply addresses in notification emails come back to POST /email/inbound, posted by our inbound mail provider with our shared secret, so there is nothing to set up. Mail to a reply+… address becomes a reply on that thread and a post+… address starts a new topic; replies to private messages and group inboxes land in their conversations, and a status page's status+… address opens or updates an incident. Up to 20 attachments are uploaded and attached to the post.

Unsubscribing. Every notification email carries a signed unsubscribe link, and the same link is its RFC 8058 one-click List-Unsubscribe target. Mail clients POST /mail/unsubscribe?token=… on their own; a person who clicks the link gets GET /mail/unsubscribe?token=…, a short confirmation page. The token says what to stop: email turns email notifications off for that member in this organization, and summary stops only the weekly community summary. An invalid or expired token stops nothing (the page answers 400). The member can turn email back on in their notification settings.

In the API

Every operation below is also reachable from the other surfaces without a dedicated command. forumulate api <Operation> calls it from the CLI, the JavaScript SDK has a typed method for each one (the last column names it; the Python and Go clients are generated from the same spec), and an agent reaches the organization-scoped ones through the MCP call_api tool. Where a CLI command or MCP tool exists too, the last column names it. Operation names link to the API reference.

Email Templates

Operation

Request

What it does

Shortcuts

EmailTemplates_list

GET /orgs/{orgId}/email-templates

List email templates in an organization

client.mail.templates.list()

EmailTemplates_get

GET /orgs/{orgId}/email-templates/{key}

Get the email template

client.mail.templates.get()

EmailTemplates_save

PUT /orgs/{orgId}/email-templates/{key}

Replace the email template

client.mail.templates.save()

EmailTemplates_reset

DELETE /orgs/{orgId}/email-templates/{key}

Delete the email template

client.mail.templates.reset()

EmailTemplates_preview

POST /orgs/{orgId}/email-templates/{key}/preview

Preview the email template

client.mail.templates.preview()

EmailTemplates_test

POST /orgs/{orgId}/email-templates/{key}/test

Test the email template

client.mail.templates.test()

Mail Admin

Operation

Request

What it does

Shortcuts

MailAdmin_list

GET /orgs/{orgId}/mail/suppressions

List suppressions in an organization

client.mail.admin.list()

MailAdmin_suppress

POST /orgs/{orgId}/mail/suppressions

Create a suppression in an organization

client.mail.admin.suppress()

MailAdmin_unsuppress

DELETE /orgs/{orgId}/mail/suppressions

Delete suppressions in an organization

client.mail.admin.unsuppress()

MailAdmin_log

GET /orgs/{orgId}/mail/log

Get the log in an organization (mail)

client.mail.admin.log()

MailAdmin_check

GET /orgs/{orgId}/mail/suppression

Get the suppression in an organization

client.mail.admin.check()

Mail Provider

Operation

Request

What it does

Shortcuts

MailProvider_status

GET /orgs/{orgId}/mail/provider

Get the provider in an organization (mail)

client.mail.provider.status()

MailProvider_connect

PUT /orgs/{orgId}/mail/provider

Replace the provider in an organization (mail)

client.mail.provider.connect()

MailProvider_disconnect

DELETE /orgs/{orgId}/mail/provider

Delete the provider in an organization (mail)

client.mail.provider.disconnect()

MailProvider_test

POST /orgs/{orgId}/mail/provider/test

Test the provider (mail)

client.mail.provider.test()

Org email branding

Operation

Request

What it does

Shortcuts

Org_emailBranding

GET /orgs/{orgId}/email-branding

Get the email branding in an organization

client.orgs.emailBranding()

Org_setEmailBranding

PATCH /orgs/{orgId}/email-branding

Update the email branding in an organization

client.orgs.setEmailBranding()

Org email settings

Operation

Request

What it does

Shortcuts

Org_emailSettings

GET /orgs/{orgId}/email-settings

List email settings in an organization

client.orgs.emailSettings()

Push

Operation

Request

What it does

Shortcuts

Push_publicKey

GET /push/vapid-public-key

Get the vapid public key

client.notifications.push.publicKey()

Push_status

GET /push/status

Get the status

client.notifications.push.status()

Push_subscribe

POST /push/subscribe

Subscribe the push

client.notifications.push.subscribe()

Push_unsubscribe

POST /push/unsubscribe

Unsubscribe the push

client.notifications.push.unsubscribe()

The Forumulate teamUpdated