Overview
Icon for Email Notifications

Email Notifications

Send transactional emails via SMTP on store events

Medusa Plugin Mailer

Send transactional emails from your Medusa v2 store when things happen — orders placed, shipments created, customers signed up, and more.

You pick which events matter, point them at HTML templates, and the plugin delivers over SMTP. Nothing is sent until you create an active mapping in the admin.

Medusa Website | Medusa Repository | Medusa Documentation

Features

  • Listen to a fixed set of Medusa events (orders, shipments, customers, returns, and optional custom events).
  • Map each event to a template, subject line, and variable paths in the admin UI.
  • Store mappings in the database — no seeds, no hidden defaults.
  • Ship generic HTML templates out of the box, or point to your own folder.
  • Send through SMTP with one or more “from” profiles.
  • Test sends from the admin without waiting for a real order.
  • Load order, fulfillment, and customer data automatically when resolving template variables.

Prerequisites

  • Node.js v20 or greater
  • A Medusa v2 backend
  • An SMTP provider (e.g. your host’s mail relay, Brevo, Amazon SES, Postmark, Gmail app password, etc.)
  • Optional: a storefront URL and logo URL for branding in templates

How to Install

  1. In your Medusa backend directory, install the plugin and Nodemailer:
npm install @sam-ael/medusa-plugin-mailer nodemailer
# or
yarn add @sam-ael/medusa-plugin-mailer nodemailer
  1. Add the plugin to your Copy to clipboardmedusa-config plugins list:
// medusa-config.ts
module.exports = defineConfig({
// ...
plugins: [
// ...
{
resolve: "@sam-ael/medusa-plugin-mailer",
options: {},
},
],
})
  1. Set environment variables in Copy to clipboard.env (see Configuration for the full list):
MAILER_SMTP_HOST=smtp.example.com
MAILER_SMTP_PORT=587
MAILER_SMTP_USER=your-user
MAILER_SMTP_PASS=your-password
MAILER_SMTP_SECURE=false
MAILER_FROM_NAME_1=My Store
MAILER_FROM_ADDRESS_1=no-reply@example.com
MAILER_TEMPLATES_DIR=src/email_templates
STORE_NAME=My Store
STORE_URL=https://www.example.com
  1. Run migrations so the mapping table exists: This creates an empty Copy to clipboardmailer_event_mapping table. It does not insert any mappings for you.
npx medusa db:migrate
  1. Start the backend and open the admin. You should see Mailer in the sidebar.
  2. Create at least one active mapping (event → template → variables). Until you do that, the plugin will not send mail for that event.

Configuration

Environment variables

Variable Required Description Copy to clipboardMAILER_SMTP_HOST Yes (to send) SMTP host Copy to clipboardMAILER_SMTP_PORT No (default Copy to clipboard587) SMTP port Copy to clipboardMAILER_SMTP_USER Yes (to send) SMTP username Copy to clipboardMAILER_SMTP_PASS Yes (to send) SMTP password Copy to clipboardMAILER_SMTP_SECURE No Set Copy to clipboardtrue for port 465 Copy to clipboardMAILER_FROM_NAME_1 Yes (to send) Display name for sender profile 1 Copy to clipboardMAILER_FROM_ADDRESS_1 Yes (to send) From address for sender profile 1 Copy to clipboardMAILER_FROM_NAME_2Copy to clipboard_5 No Extra sender profiles (pick by index in a mapping) Copy to clipboardMAILER_FROM_ADDRESS_2Copy to clipboard_5 No Copy to clipboardMAILER_TEMPLATES_DIR Recommended Folder of Copy to clipboard.html templates (relative to process cwd or absolute). Falls back to bundled templates if missing Copy to clipboardSTORE_NAME Recommended Used as Copy to clipboard{{ .StoreName }} in templates Copy to clipboardSTORE_URL or Copy to clipboardSTOREFRONT_URL Recommended Used as Copy to clipboard{{ .SiteURL }} / Copy to clipboard{{ .StoreURL }} Copy to clipboardMAILER_LOGO_URL No Absolute image URL for Copy to clipboard{{ .LogoURL }} Copy to clipboardMAILER_SEND_CONCURRENCY No (default Copy to clipboard2) Parallel sends when several mappings match one event Copy to clipboardMAILER_SMTP_DEBUG No Set Copy to clipboardtrue for Nodemailer debug logs Copy to clipboardMAILER_SMTP_NAME No EHLO / HELO hostname

If SMTP is incomplete, the plugin logs a warning and skips sending instead of crashing.

How events and mappings fit together

Think of two layers:

  1. What the plugin listens for — a fixed list of event names in the plugin code (Copy to clipboardMAILER_EVENTS). You cannot turn listening on/off in the database; you only map the ones you care about.
  2. What actually gets emailed — rows you create in Admin (or via API). Each row ties an event name to a template file, subject, variable map, recipient rule, and an active flag.

No active mapping ⇒ no email, even if the event fires.

Template placeholders

Templates use Go-style variables:

<p>Hi {{ .first_name }},</p>
<p>Order #{{ .display_id }} is confirmed.</p>

Always available without mapping:

Placeholder Source Copy to clipboard{{ .StoreName }} Copy to clipboardSTORE_NAME Copy to clipboard{{ .SiteURL }} / Copy to clipboard{{ .StoreURL }} Copy to clipboardSTORE_URL or Copy to clipboardSTOREFRONT_URL Copy to clipboard{{ .RecipientEmail }} resolved recipient Copy to clipboard{{ .Subject }} mapping subject (after substitution) Copy to clipboard{{ .LogoURL }} Copy to clipboardMAILER_LOGO_URL (optional) Copy to clipboard{{ .reason }} default text if you do not map it

Your mapping’s template variables object maps each name to a data path on the loaded entity, for example:

{
"first_name": "customer.first_name",
"display_id": "display_id",
"total": "total"
}

Unmatched Copy to clipboard{{ .Something }} placeholders are removed before the message is sent.

Recipient types

Type Behavior Copy to clipboardcustomer_email Prefer email from the order/customer graph Copy to clipboardorder_email Prefer the order’s Copy to clipboardemail field Copy to clipboardcustom Use the fixed address on the mapping

Events the plugin listens to

These names are hardcoded. Map only the ones you need.

Orders

Event Typical use Copy to clipboardorder.placed Order confirmation Copy to clipboardorder.completed Order finished Copy to clipboardorder.canceled Cancellation notice Copy to clipboardorder.updated Use sparingly (can be noisy) Copy to clipboardorder.fulfillment_created “We’re packing your order” Copy to clipboardorder.fulfillment_canceled Fulfillment canceled Copy to clipboardorder.return_requested Return started Copy to clipboardorder.return_received Return received / refund note

Fulfillment

Prefer the Medusa core-flows names:

Event Typical use Copy to clipboardshipment.created Shipped + tracking Copy to clipboarddelivery.created Delivered

Legacy aliases still listen if something emits them: Copy to clipboardfulfillment.created, Copy to clipboardfulfillment.shipment_created, Copy to clipboardfulfillment.delivery_created.

Customers & RMA

Event Copy to clipboardcustomer.created, Copy to clipboardcustomer.updated Copy to clipboardreturn.created, Copy to clipboardreturn.received Copy to clipboardclaim.created, Copy to clipboardexchange.created

Optional custom events

Your app can emit these; the plugin will handle them if you add mappings. Payloads are not graph-hydrated — put the fields you need on the event data and map paths like Copy to clipboardamountCopy to clipboardamount.

Event Example fields on Copy to clipboarddata Copy to clipboardloyalty.points_credited Copy to clipboardemail, Copy to clipboardamount, Copy to clipboardbalance, Copy to clipboarddescription, Copy to clipboardreference_id Copy to clipboardloyalty.points_debited same Copy to clipboardwallet.credited / Copy to clipboardwallet.debited Copy to clipboardemail, Copy to clipboardamount, Copy to clipboardbalance, Copy to clipboardcurrency_code, Copy to clipboarddescription Copy to clipboardmembership.tier_changed Copy to clipboardemail, Copy to clipboardmembership_name, Copy to clipboardprevious_membership_name, Copy to clipboardreason

Example emit from your backend:

import { Modules } from "@medusajs/framework/utils"
const eventBus = container.resolve(Modules.EVENT_BUS)
await eventBus.emit({
name: "loyalty.points_credited",
data: {
email: "customer@example.com",
amount: 100,
balance: 500,
description: "Thanks for your order",
},
})

What gets loaded for common events

Event pattern Hydrated data Copy to clipboardorder.* (most) Full order (addresses, customer, items, totals, …) Copy to clipboardorder.fulfillment_created Order via Copy to clipboardorder_id Copy to clipboardshipment.created / Copy to clipboarddelivery.created Fulfillment + linked order (including tracking labels when present) Custom loyalty / wallet / membership Event payload only

Bundled templates

The package ships plain, unbranded HTML under Copy to clipboardemail_templates/. Use them as-is or copy them into your Copy to clipboardMAILER_TEMPLATES_DIR and redesign.

Template file Good starting point for Copy to clipboardorder_placed.html Copy to clipboardorder.placed Copy to clipboardorder_confirmed.html Alternate order confirmation Copy to clipboardorder_fulfilled.html Copy to clipboardorder.fulfillment_created Copy to clipboardorder_shipped.html Copy to clipboardshipment.created Copy to clipboardorder_delivered.html Copy to clipboarddelivery.created Copy to clipboardorder_canceled.html Copy to clipboardorder.canceled Copy to clipboardorder_completed.html Copy to clipboardorder.completed Copy to clipboardrefund_confirmed.html Returns / refunds Copy to clipboardcustomer_welcome.html Copy to clipboardcustomer.created Copy to clipboardcustomer_confirmation.html Email confirmation flows Copy to clipboardaccount_deleted.html Account removal Copy to clipboardpoints_credited.html / Copy to clipboardpoints_debited.html Loyalty custom events Copy to clipboardstore_credit_credited.html / Copy to clipboardstore_credit_debited.html Wallet custom events Copy to clipboardmembership_tier_changed.html Membership custom event Copy to clipboardgeneric_notification.html / Copy to clipboardcustom_template.html Free-form messages

Brand with Copy to clipboardSTORE_NAME and optional Copy to clipboardMAILER_LOGO_URL. Regenerate the defaults from the plugin repo with:

node scripts/generate-bundled-templates.mjs

Suggested mapping examples

Order confirmation

  • Event: Copy to clipboardorder.placed
  • Template: Copy to clipboardorder_placed.html
  • Subject: Copy to clipboardOrder #{{ .display_id }} confirmed
  • Recipient: Order email
  • Variables: Name Path Copy to clipboardfirst_name Copy to clipboardcustomer.first_name Copy to clipboarddisplay_id Copy to clipboarddisplay_id Copy to clipboardtotal Copy to clipboardtotal

Shipment

  • Event: Copy to clipboardshipment.created
  • Template: Copy to clipboardorder_shipped.html
  • Subject: Copy to clipboardYour order is on the way
  • Variables: Name Path Copy to clipboardfirst_name Copy to clipboardorder.customer.first_name Copy to clipboarddisplay_id Copy to clipboardorder.display_id Copy to clipboardtracking_number Copy to clipboardlabels.0.tracking_number Copy to clipboardtracking_url Copy to clipboardlabels.0.tracking_url Copy to clipboardcarrier Copy to clipboardprovider_id

Test the Plugin

  1. Run your Medusa backend:
npm run dev
# or
npx medusa develop
  1. Open Admin → Mailer and check that SMTP shows as configured.
  2. Create an active mapping for Copy to clipboardorder.placedCopy to clipboardorder_placed.html (or your own template).
  3. Either:
    • Place a test order in a storefront / Store API, or
    • Use Send email in the Mailer admin with a test address and sample variables.
  4. Confirm the message arrives and that placeholders look correct.

If nothing arrives: check SMTP env vars, that the mapping is active, and that the event name matches the list above (for shipping, prefer Copy to clipboardshipment.created).

Admin API (optional)

Useful for automation or custom UIs. All routes require an authenticated admin user.

Method Path Purpose Copy to clipboardGET Copy to clipboard/admin/mailer/config SMTP status and sender profiles Copy to clipboardGET Copy to clipboard/admin/mailer/templates Template files and detected variables Copy to clipboardGET Copy to clipboard/admin/mailer/mappings List mappings Copy to clipboardPOST Copy to clipboard/admin/mailer/mappings Create mapping Copy to clipboardPOST Copy to clipboard/admin/mailer/mappings/:id Update mapping Copy to clipboardDELETE Copy to clipboard/admin/mailer/mappings/:id Delete mapping Copy to clipboardPOST Copy to clipboard/admin/mailer/send Manual / test send

You can also trigger the same pipeline from code:

import { sendEventEmailsWorkflow } from "@sam-ael/medusa-plugin-mailer/workflows"
await sendEventEmailsWorkflow(container).run({
input: {
event_name: "order.placed",
event_data: { id: orderId },
},
})

Troubleshooting

What you see What to check No emails ever SMTP env incomplete; Admin still says not configured Event fires, no email No active mapping for that exact event name Shipment email never sends Map Copy to clipboardshipment.created, not only older fulfillment aliases Empty fields in the body Variable path wrong for that event’s data shape Wrong “to” address Guest orders use order email; adjust recipient type Two emails for one action Another subscriber in your app is also sending — turn one off

Develop this repository

yarn install
yarn build # produces .medusa/server
yarn typecheck
yarn test

Peer dependencies should match your host Medusa version as closely as practical.

Additional Resources

License

MIT

You may also like

Browse all integrations

Build your own

Develop your own custom integration

Build your own integration with our API to speed up your processes. Make your integration available via npm for it to be shared in our Library with the broader Medusa community.

gift card interface

Ready to build your custom commerce setup?