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
- In your Medusa backend directory, install the plugin and Nodemailer:
123npm install @sam-ael/medusa-plugin-mailer nodemailer# oryarn add @sam-ael/medusa-plugin-mailer nodemailer
- Add the plugin to your Copy to clipboard
medusa-configplugins list:
1234567891011// medusa-config.tsmodule.exports = defineConfig({// ...plugins: [// ...{resolve: "@sam-ael/medusa-plugin-mailer",options: {},},],})
- Set environment variables in Copy to clipboard
.env(see Configuration for the full list):
12345678910111213MAILER_SMTP_HOST=smtp.example.comMAILER_SMTP_PORT=587MAILER_SMTP_USER=your-userMAILER_SMTP_PASS=your-passwordMAILER_SMTP_SECURE=falseMAILER_FROM_NAME_1=My StoreMAILER_FROM_ADDRESS_1=no-reply@example.comMAILER_TEMPLATES_DIR=src/email_templatesSTORE_NAME=My StoreSTORE_URL=https://www.example.com
- Run migrations so the mapping table exists: This creates an empty Copy to clipboard
mailer_event_mappingtable. It does not insert any mappings for you.
1npx medusa db:migrate
- Start the backend and open the admin. You should see Mailer in the sidebar.
- 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_2 … Copy to clipboard_5 No Extra sender profiles (pick by index in a mapping) Copy to clipboardMAILER_FROM_ADDRESS_2 … Copy 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:
- What the plugin listens for — a fixed list of event names in the plugin code (Copy to clipboard
MAILER_EVENTS). You cannot turn listening on/off in the database; you only map the ones you care about. - 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:
12<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:
12345{"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 clipboardamount → Copy 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:
12345678910111213import { 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:
1node scripts/generate-bundled-templates.mjs
Suggested mapping examples
Order confirmation
- Event: Copy to clipboard
order.placed - Template: Copy to clipboard
order_placed.html - Subject: Copy to clipboard
Order #{{ .display_id }} confirmed - Recipient: Order email
- Variables: Name Path Copy to clipboard
first_nameCopy to clipboardcustomer.first_nameCopy to clipboarddisplay_idCopy to clipboarddisplay_idCopy to clipboardtotalCopy to clipboardtotal
Shipment
- Event: Copy to clipboard
shipment.created - Template: Copy to clipboard
order_shipped.html - Subject: Copy to clipboard
Your order is on the way - Variables: Name Path Copy to clipboard
first_nameCopy to clipboardorder.customer.first_nameCopy to clipboarddisplay_idCopy to clipboardorder.display_idCopy to clipboardtracking_numberCopy to clipboardlabels.0.tracking_numberCopy to clipboardtracking_urlCopy to clipboardlabels.0.tracking_urlCopy to clipboardcarrierCopy to clipboardprovider_id
Test the Plugin
- Run your Medusa backend:
123npm run dev# ornpx medusa develop
- Open Admin → Mailer and check that SMTP shows as configured.
- Create an active mapping for Copy to clipboard
order.placed→ Copy to clipboardorder_placed.html(or your own template). - 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.
- 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:
12345678import { 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
1234yarn installyarn build # produces .medusa/serveryarn typecheckyarn test
Peer dependencies should match your host Medusa version as closely as practical.
Additional Resources
License
MIT

