> ## Documentation Index
> Fetch the complete documentation index at: https://spreecommerce.org/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Email Templates

> Change what Spree's transactional emails say and look like, with Liquid templates written in MJML.

Every email Spree sends — order confirmations, shipping notices, password resets, staff invitations — renders from a **Liquid** template written in **MJML**. Liquid fills in the data; MJML turns a dozen layout tags into the nested tables, inline styles and Outlook workarounds that email clients need, and makes the email stack on phones.

To change an email, you copy its template into your app and edit it. Nothing else changes: Spree still picks the recipient, the language and the sender, and delivers the email through your [SMTP provider](/docs/developer/providers/emails).

<Note>
  Templates read plain data — the same fields the [Store API](/docs/api-reference/store-api/introduction) returns, plus what only an email needs — never Ruby objects. The same template can run outside Ruby, and merchants can edit customer emails safely from the dashboard.
</Note>

## Where templates live

A template sits at its email's path with a `.liquid` extension. To override one, create the file at the same path in your app:

| Email | Template |
| - | - |
| Order confirmation | `server/app/views/spree/order_mailer/confirm_email.liquid` |
| Shipping notification | `server/app/views/spree/fulfillment_mailer/fulfilled_email.liquid` |
| Customer password reset | `server/app/views/spree/customer_mailer/password_reset_email.liquid` |
| The layout around every email | `server/app/views/layouts/spree/base_mailer.liquid` |

The path is also the template's name, for example `spree/order_mailer/confirm_email`. [Email Template Variables](/docs/developer/customization/email-variables) lists every customer email's template and the data it receives.

Start from Spree's own template rather than a blank file: customer emails are in [`spree/emails/app/views`](https://github.com/spree/spree/tree/main/spree/emails/app/views/spree) and staff emails and the layout in [`spree/core/app/views`](https://github.com/spree/spree/tree/main/spree/core/app/views) on GitHub.

Your file wins as soon as it exists, and edits to it show on the next email without a restart. There is nothing to register. The one exception: Rails only reads `server/app/views` if the folder existed when the app started, so restart once after creating that folder. API-only apps often start without it.

## Anatomy of a template

```liquid server/app/views/spree/order_mailer/confirm_email.liquid theme={"theme":"night-owl"}
---
subject: "{{ store.name }} {{ 'order_mailer.confirm_email.subject' | t }} #{{ order.number }}"
---
<mj-section>
  <mj-column css-class="hero">
    <mj-text mj-class="heading">Thanks for your order!</mj-text>
    <mj-text mj-class="greeting">Hi {{ order.customer_name }},</mj-text>
    <mj-button href="{{ store.url }}/account/orders/{{ order.number }}">View your order</mj-button>
  </mj-column>
</mj-section>
{% assign heading = 'order_mailer.confirm_email.order_summary' | t: number: order.number %}
{% render 'spree/shared/order_summary', heading: heading, purchase: order, items: order.items, totals: true %}
```

* **The subject** is the `subject` line at the top, and it is Liquid too.
* **The body** is a list of MJML sections. The layout wraps it with the store's logo and footer.
* **Copy** either comes from Spree's translations through the `t` filter, which keeps one template working in every language, or is written straight into the template for a single-language store.

The layout defines named styles you can use through `mj-class`: `heading`, `greeting`, `lead`, `note`, `section-heading` and `body`. For the MJML tags themselves, see the [MJML documentation](https://documentation.mjml.io).

## Partials

Pull a shared piece in with `{% render %}`. The name maps to a file with a leading underscore, as Rails partials do: `'spree/shared/line_item'` is `app/views/spree/shared/_line_item.liquid`. A partial only sees the values you pass it.

| Partial | Arguments | What it renders |
| - | - | - |
| `spree/shared/order_summary` | `heading`, `purchase`, `items`, `totals` | A headed list of purchased items, with the order's totals when `totals` is true |
| `spree/shared/line_item` | `item` | One purchased item: thumbnail, name, options, quantity and amount |
| `spree/shared/purchase_totals` | `purchase` | Subtotal, discounts, delivery, tax, gift card, fees and total |
| `spree/shared/fulfillment_group` | `group`, `position`, `count` | One parcel of a multi-seller purchase |
| `spree/shared/summary_row` | `label`, `value`, `sub`, `total`, `translate` | One label and amount row inside an `<mj-table css-class="summary">` |

Override a partial the same way as a template, by creating the file at its path in your app. Every email that uses it changes.

## Filters

Besides [Liquid's standard filters](https://shopify.github.io/liquid/filters/abs/), templates have these. The names follow common Liquid conventions where they mean the same thing.

| Filter | Example | Result |
| - | - | - |
| `money` | `{{ '10.5' \| money }}` | `$10.50`, in the email's currency |
| `money_with_currency` | `{{ '10.5' \| money_with_currency }}` | `$10.50 USD` |
| `date` | `{{ order.completed_at \| date: 'long' }}` | A date in the store's time zone. Takes a named format (`short`, `long`, `default`) or a `strftime` pattern such as `'%Y-%m-%d'` |
| `t` | `{{ 'order_mailer.confirm_email.dear_customer' \| t: name: order.customer_name }}` | A Spree translation, with its placeholders filled in |
| `raw` | `{{ product.description_html \| raw }}` | Prints the value without escaping. See below |

Most amounts already arrive formatted, as `display_total`, `display_amount` and the like, so `money` is only needed for a raw amount.

## Escaping

Everything a template prints is HTML-escaped. A customer who types `<a href="https://evil.test">Click to verify</a>` as their name sees that text in the email, not a live link — and so does the store owner reading the new-order notification.

`raw` turns escaping off for one value. Only use it for HTML that was cleaned when it was saved, such as a product's rich-text description. Never use it on a name, an address or a note.

## The plain-text version

Spree builds each email's plain-text version from its HTML, writing links as `label (url)`, so the two can never drift apart. To write the text by hand instead, add a `.text.liquid` file next to the template:

```liquid server/app/views/spree/order_mailer/confirm_email.text.liquid theme={"theme":"night-owl"}
Thanks for your order {{ order.number }}, {{ order.customer_name }}.

Total: {{ order.display_total }}
```

Nothing is escaped in the text version.

## Mistakes surface in development

In development and test, a variable that does not exist raises an error instead of printing nothing, so `{{ order.nubmer }}` fails your spec rather than sending a blank. In production it prints nothing. A template that loops endlessly fails that one email instead of stalling your background jobs.

To look at every email with real data, open the mailer previews at `/rails/mailers` on your Spree server.

## Templates merchants edit in the dashboard

Merchants can edit the emails their customers receive in **Settings → Emails → Templates**, along with the layout and the shared blocks in `spree/shared`. An edit is saved as a draft and goes live when published; publishing renders it with the store's own data first and refuses a template that does not render. An email built from a record the store does not have yet, such as an order confirmation in a store with no orders, is checked for template syntax only. Staff, store-owner and seller emails are not editable and always render from files.

A store's published template comes first, so the lookup for an editable email is:

1. the store's published template in the email's language
2. the store's published template for every language
3. your app's file
4. Spree's file

Your file stays the default the dashboard starts from and reverts to, so overriding a template in code and letting merchants edit it work together. When a Spree upgrade or a change to your file alters a default a merchant has customized, the dashboard tells them and shows what changed.

The same operations are open to integrations through the [Admin API](/docs/api-reference/admin-api/introduction), with the `email_templates` permission:

<Tabs>
  <Tab title="Admin SDK">
    ```typescript theme={"theme":"night-owl"}
    const preview = await client.emailTemplates.preview('spree.order_mailer.confirm_email', {
      body: '<mj-section><mj-column><mj-text>Thanks, {{ order.customer_name }}!</mj-text></mj-column></mj-section>',
    })

    await client.emailTemplates.draft.update('spree.order_mailer.confirm_email', {
      body: '<mj-section><mj-column><mj-text>Thanks, {{ order.customer_name }}!</mj-text></mj-column></mj-section>',
    })
    await client.emailTemplates.publish('spree.order_mailer.confirm_email')
    ```
  </Tab>

  <Tab title="cURL">
    ```bash theme={"theme":"night-owl"}
    curl -X PUT https://your-store.com/api/v3/admin/email_templates/spree.order_mailer.confirm_email/draft \
      -H "X-Spree-Api-Key: sk_xxx" -H "Content-Type: application/json" \
      -d '{"body": "<mj-section><mj-column><mj-text>Thanks, {{ order.customer_name }}!</mj-text></mj-column></mj-section>"}'

    curl -X POST https://your-store.com/api/v3/admin/email_templates/spree.order_mailer.confirm_email/publication \
      -H "X-Spree-Api-Key: sk_xxx"
    ```
  </Tab>
</Tabs>

### Branding

Merchants set the colors and font of their customer emails in **Settings → Emails**, without touching a template. Templates read them from [`store.branding`](/docs/developer/customization/email-variables#store), and Spree's layout uses them throughout. If you override the layout or a template, read colors and fonts from `store.branding` rather than writing them in, so a store's branding keeps applying.

### Making your own customer email editable

A customer email your app adds can be edited like Spree's own. Register it with a class that builds the data its preview renders with:

```ruby server/config/initializers/spree.rb theme={"theme":"night-owl"}
Rails.application.config.after_initialize do
  Spree.editable_email_templates.register(
    'spree/review_mailer/request_email', kind: :email, sample: 'ReviewRequestSample'
  )
end
```

```ruby server/app/services/review_request_sample.rb theme={"theme":"night-owl"}
# Previews with the store's latest completed order, or the one the merchant picks.
class ReviewRequestSample < Spree::Emails::Samples::Order
  def variables
    { order: super[:order], review_url: placeholder_url('reviews/new') }
  end
end
```

A sample builds from the record the merchant picked, else the store's latest one; a store with none is told there is nothing to preview with yet. Pass placeholder URLs, never real tokens, and register only emails your customers receive.

## Your own mailers

A mailer that inherits `Spree::BaseMailer` and renders its own ERB views with `mail` keeps working. Spree wraps its HTML in the same layout as every other email, so it carries the store's logo, header and footer. The `spree/shared/mailer_hero` and `spree/shared/mailer_button` partials are still there for those views.

To give a new email the same data contract as Spree's own, render it from a Liquid template instead:

```ruby server/app/mailers/spree/welcome_mailer.rb theme={"theme":"night-owl"}
module Spree
  class WelcomeMailer < BaseMailer
    def welcome_email(customer, store)
      @current_store = store

      with_store_locale(store) do
        mail_template({ customer: email_data(customer, Spree.api.customer_serializer) }, to: customer.email)
      end
    end
  end
end
```

The template goes at `server/app/views/spree/welcome_mailer/welcome_email.liquid`. `email_data` serializes a record the way templates read it; pass a URL that carries a token as its own variable rather than serializing it.

## Related

* [Email Template Variables](/docs/developer/customization/email-variables) — every customer email and the data it receives
* [Upgrading from 5.6 to 6.0](/docs/developer/upgrades/5.6-to-6.0#emails-render-from-liquid-templates) — moving ERB email overrides to Liquid
* [Sending out Emails](/docs/developer/deployment/emails) — which emails Spree sends, and handing customer emails to your storefront
* [Emails provider setup](/docs/developer/providers/emails) — SMTP configuration


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.