order-management
v0.0.74
Published
A starter for Medusa plugins.
Maintainers
Readme
Compatibility
This plugin is compatible with Medusa v2 (e.g. @medusajs/framework and @medusajs/medusa 2.11.x). Check package.json peer dependencies for the exact version range.
Features
- Order Management: Cancel and reorder for both authenticated customers and guest orders
- Authenticated Customer APIs: Store endpoints for logged-in customers to cancel orders (
/store/orders/cancel/:order_id), reorder (/store/orders/reorder/:order_id), manage returns (/store/returns), and manage exchanges (/store/swaps) - Guest Order Portal: OTP-based look-up for guest users with full order management (view orders, initiate returns, cancel, reorder)
- Guest Order Actions: Secure JWT-protected endpoints for guest users to view orders, initiate returns (via Medusa workflow), cancel orders, reorder (all under
/store/guest-orders/:id/...) - Order Confirmation Emails: Optional email notifications when orders are placed (with "Claim Order" support for registered users)
- Status-Based Notifications: Configurable email, SMS, and push notifications for order status changes (pending, shipped, delivered, canceled, etc.)
- Return / Exchange Delivery Window: Configurable per-type windows (
returnValidInDays,exchangeValidInDays) that block return or exchange requests submitted after N days from the fulfillment'sdelivered_attimestamp. Defaults to 7 days each if not configured. The window is checked only when the order has been marked as delivered; undelivered orders are unaffected. - Return Orders Admin Panel: Admin UI and API for return orders (list, filter, search, detail, reject); returns are stored in the Medusa ORDER module and created via
createAndCompleteReturnOrderWorkflow - Exchanges Admin Panel: Admin UI and API for exchanges (list, detail, reject, cancel); exchanges are ORDER module order changes (type
exchange) created by customers via Medusa core workflows (beginExchangeOrderWorkflow, etc.) - Medusa workflows: Customer returns use
createAndCompleteReturnOrderWorkflow; customer exchanges usebeginExchangeOrderWorkflow,orderExchangeRequestItemReturnWorkflow,orderExchangeAddNewItemWorkflow,confirmExchangeRequestWorkflow, andcancelBeginOrderExchangeWorkflow. Admin actions useconfirmReturnReceiveWorkflow,cancelReturnWorkflow, andcancelBeginOrderExchangeWorkflow. - Payment details: Custom module
payment_detailfor customer payment methods (UPI, bank, card). Store CRUD at/store/payment-details. Type-driven validation — UPI requiresupi_id; bank requiresaccount_holder_name,bank_name,account_number,ifsc(optionalbranch_name); card requirescard_holder_name,card_number,expiry_date,cvv. One default per customer; return creation requires at least one default payment method. - Refund payment mapping: Table
refund_payment_mapping(one row per return:return_id,payment_id,is_refunded,refund_mode,images,details). A mapping is created automatically when a return is created. Customer can GET mapping (read-only for all fields) and PUTpayment_idonly whileis_refunded = false; customer can also GET all mappings for an order to see if all refunds are successful. Only admin can setis_refunded,refund_mode(offline/online),images, anddetails.
Plugin structure (high level)
| Area | What the plugin provides |
|------|---------------------------|
| Store (authenticated) | /store/orders/cancel|reorder/:id, /store/returns, /store/swaps, /store/payment-details, /store/refund-payment-mapping/:return_id, /store/orders/:order_id/refund-payment-mappings |
| Store (guest) | /store/otp/request|verify, /store/guest-orders, /store/guest-orders/:id/returns|cancel|reorder|swaps |
| Admin | /admin/returns, /admin/swaps, /admin/refund-payment-mapping/:return_id, /admin/refund-payment-mapping/:return_id/mark-refunded, /admin/orders/:order_id/refund-context |
| Modules | payment_detail, refund_payment_mapping; returns/exchanges use Medusa ORDER module |
| Subscribers | Order confirmation email, status notifications, return/swap event sync |
Important APIs (quick reference)
| Method | Endpoint | Auth | Description |
|--------|----------|------|-------------|
| Store (customer) | | | |
| POST | /store/orders/cancel/:order_id | Customer | Cancel order |
| POST | /store/orders/reorder/:order_id | Customer | Reorder (new cart) |
| GET | /store/returns | Customer | List returns (optional ?order_id=) |
| POST | /store/returns | Customer | Create return |
| GET | /store/returns/:id | Customer | Get return |
| POST | /store/returns/:id/cancel | Customer | Cancel return |
| GET | /store/swaps | Customer | List swaps (optional ?order_id=) |
| POST | /store/swaps | Customer | Create swap |
| GET | /store/swaps/:id | Customer | Get swap |
| POST | /store/swaps/:id/cancel | Customer | Cancel swap |
| GET | /store/payment-details | Customer | List payment details |
| POST | /store/payment-details | Customer | Create payment detail |
| GET | /store/payment-details/:id | Customer | Get payment detail |
| PUT | /store/payment-details/:id | Customer | Update payment detail |
| POST | /store/payment-details/:id/make-default | Customer | Set default payment |
| GET | /store/refund-payment-mapping/:return_id | Customer | Get mapping (own return) |
| PUT | /store/refund-payment-mapping/:return_id | Customer | Update payment_id only |
| GET | /store/orders/:order_id/refund-payment-mappings | Customer | List mappings for order |
| Store (guest) | | | |
| POST | /store/otp/request | — | Request OTP |
| POST | /store/otp/verify | — | Verify OTP, get JWT |
| GET | /store/guest-orders | Guest JWT | List guest orders |
| GET | /store/guest-orders/:id | Guest JWT | Get guest order |
| POST | /store/guest-orders/:id/returns | Guest JWT | Create return |
| POST | /store/guest-orders/:id/cancel | Guest JWT | Cancel order |
| POST | /store/guest-orders/:id/reorder | Guest JWT | Reorder |
| GET/POST | /store/guest-orders/:id/swaps | Guest JWT | List / create swaps |
| Admin | | | |
| GET | /admin/returns | Admin | List returns |
| GET | /admin/returns/:id | Admin | Get return |
| POST | /admin/returns/:id/reject | Admin | Reject return |
| GET | /admin/swaps | Admin | List swaps |
| GET | /admin/swaps/:id | Admin | Get swap |
| POST | /admin/swaps/:id/reject | Admin | Reject swap |
| POST | /admin/swaps/:id/cancel | Admin | Cancel exchange |
| GET | /admin/orders/:order_id/refund-context | Admin | Refund context (return + mapping + payment_detail) |
| PUT | /admin/refund-payment-mapping/:return_id | Admin | Update refund_mode, images, details |
| POST | /admin/refund-payment-mapping/:return_id/mark-refunded | Admin | Mark mapping as refunded |
| POST | /admin/uploads | Admin | Upload file (Medusa core; used by widget for refund images) |
Configuration
The plugin can be configured in your medusa-config.js file:
import { defineConfig } from "@medusajs/framework/utils"
module.exports = defineConfig({
// ...
plugins: [
{
resolve: "order-management",
options: {
// Required options
storefrontUrl: process.env.STOREFRONT_URL || "http://localhost:8000",
jwtSecret: process.env.JWT_SECRET || "medusa-secret-guest-access",
// Email template options
email: {
orderConfirmTemplate: "src/templates/emails/order-confirmation.html", // Path to HTML template file
otpTemplate: "src/templates/emails/otp-verification.html", // Path to HTML template file (required)
},
// Optional SMTP configuration (for email delivery)
smtp: {
enabled: process.env.FORCE_SMTP_REDELIVER === "true",
host: process.env.SMTP_HOST,
port: process.env.SMTP_PORT ? Number(process.env.SMTP_PORT) : undefined,
secure: process.env.SMTP_SECURE === "true",
auth: {
user: process.env.SMTP_AUTH_USER,
pass: process.env.SMTP_AUTH_PASS,
},
from: process.env.SMTP_FROM || process.env.SMTP_AUTH_USER,
},
// Optional: number of days after delivery within which returns are allowed (default: 7)
returnValidInDays: 7,
// Optional: number of days after delivery within which exchanges are allowed (default: 7)
exchangeValidInDays: 7,
// Optional status-based notification configuration
notifications: {
enabled: true,
statusConfig: {
"pending": [
{
template: "src/templates/emails/order-placed.html",
channel: "email"
},
{
template: "src/templates/sms/order-placed.txt",
channel: "sms",
getRecipient: (order, _templateData) =>
order.shipping_address?.phone || order.billing_address?.phone
},
{
template: "src/templates/push/order-placed.txt",
channel: "push"
}
],
"shipped": [
{
template: "src/templates/emails/order-shipped.html",
channel: "email"
}
],
"delivered": [
{
template: "src/templates/emails/order-delivered.html",
channel: "email"
}
],
"canceled": [
{
template: "src/templates/emails/order-canceled.html",
channel: "email"
}
]
}
},
},
},
],
})Configuration Options
Required Options:
storefrontUrl- Your storefront URL (required)jwtSecret- JWT secret for guest order tokens (required)
Email Template Options:
email.orderConfirmTemplate- Path to HTML template file for order confirmation emails (optional)email.otpTemplate- Path to HTML template file for OTP verification emails (required for guest OTP functionality)
Optional SMTP Options:
smtp.enabled- Enable SMTP email delivery (default:false)smtp.host- SMTP server host (required if enabled)smtp.port- SMTP server port (required if enabled)smtp.secure- Use secure connection (default:true)smtp.auth.user- SMTP username (required if enabled)smtp.auth.pass- SMTP password (required if enabled)smtp.from- From email address (optional, defaults tosmtp.auth.user)
Optional Return / Exchange Window Options:
returnValidInDays— Number of days afterfulfillment.delivered_atwithin which a customer may request a return. Defaults to7. Set to a higher value to allow a longer window; the check is skipped entirely if the fulfillment has nodelivered_attimestamp.exchangeValidInDays— Number of days afterfulfillment.delivered_atwithin which a customer may request an exchange. Defaults to7. Independent fromreturnValidInDaysso you can allow different windows for each action type.
When the window has expired the API returns:
{
"type": "RETURN_WINDOW_EXPIRED",
"message": "Returns and exchanges are only allowed within 7 days of delivery. The 7 days window for this order has expired.",
"delivered_at": "2026-03-07T10:46:59.153Z",
"window_days": 7
}Optional Status-Based Notification Options:
notifications.enabled- Enable status-based notifications (default:false)notifications.statusConfig- Map of order status to notification configurations- Each status can have multiple notification configurations
- Each configuration requires:
template- Path to template file (HTML for email, text for SMS/push)channel- Notification channel (supports any string:"email","sms","push", or any custom channel)
- Optional per-config:
getRecipient(order, templateData)- Function that receives the order and template data and returns the recipient for this notification (e.g. mobile number for SMS, customer_id for push). If omitted, the default resolver for the channel is used (e.g.order.emailfor email,shipping_address.phonefor SMS,order.customer_idfor push).
- Push channel: For
channel: "push", the recipient is treated as a customer_id. The code loads all active device tokens for that customer from thenotification_tokensmodule (e.g. when using themedusa-notification-token-managementplugin) and sends one push notification to each token. If the token module is not installed or the customer has no tokens, no push is sent. - SMS channel: The plugin sends the SMS body in a form compatible with medusa-twilio-sms (
content.text) and astext/body/data.textfor other adapters. If you use medusa-twilio-sms and patch it to also accept stringcontentordata.text, usenpx patch-package medusa-twilio-smsafter editing so the fix persists across installs.
Notes:
storefrontUrlandjwtSecretare required. All other options are optional.- Order confirmation emails are only sent if a template path is provided. If no template is configured, emails will not be sent.
- OTP template is mandatory for guest OTP functionality. The guest OTP request API will return an error if
email.otpTemplateis not configured. - OTP templates should include
{{otp}}placeholder for the verification code. - SMTP configuration is optional. If not enabled, emails will use Medusa's default email provider.
- Status-based notifications are optional. If enabled, notifications will be sent whenever an order reaches a configured status.
Payment detail module
The plugin includes a payment_detail module for storing customer payment methods (e.g. UPI, bank, card). To use it:
- Register the module in
medusa-config.js:
import orderManagement from "order-management"
module.exports = defineConfig({
modules: [
// ... other modules
orderManagement.modules["payment-detail"],
],
plugins: [
{ resolve: "order-management", options: { storefrontUrl: "...", jwtSecret: "..." } },
],
})Run migrations from your Medusa project (or from the plugin directory):
npx medusa plugin:db:generate(if using the plugin's migrations from the app).Store API (authenticated customers only):
GET /store/payment-details– list (scoped tocustomer_id)GET /store/payment-details/:id– get onePOST /store/payment-details– create (type,detail_jsononly;is_defaultis rejected)PUT /store/payment-details/:id– update (nois_default; use make-default endpoint)DELETE /store/payment-details/:id– deletePOST /store/payment-details/:id/make-default– only way to set a payment as default (idempotent, atomic)
Validation:
typemust be one ofupi,bank,card.detail_jsonis type-driven:type: "upi"→detail_json.upi_idrequiredtype: "bank"→account_holder_name,bank_name,account_number,ifscrequired;branch_nameoptionaltype: "card"→card_holder_name,card_number,expiry_date(MM/YY),cvvrequired
Default payment: At least one record per customer must have
is_default: true. Only one default per customer (enforced by DB unique partial index and service logic). The only way to change which record is default isPOST /store/payment-details/:id/make-default; create/update APIs rejectis_defaultin the body. Creating a return (POST /store/returns) is blocked if the customer has no default payment method.
Refund payment mapping module
The plugin includes a refund_payment_mapping module: one row per return linking a return to a payment (e.g. for refund destination), with is_refunded, refund_mode, images, and details.
- Table:
refund_payment_mapping—id,return_id(unique),payment_id,is_refunded(defaultfalse),refund_mode(text, nullable; admin-only: use"offline"or"online"),images(JSON array of URLs),details(text, nullable; admin-only notes), timestamps. - Auto-create: When a return is created (store or guest), a mapping row is created; optional
refund_payment_idcan be sent in the create-return body. - Store API:
GET /store/refund-payment-mapping/:return_id— get mapping for one return (customer must own the return). Returns all fields read-only; customer can seeis_refunded,refund_mode,images,details.PUT /store/refund-payment-mapping/:return_id— body{ payment_id }only; allowed only whileis_refunded = false. Request body must not includerefund_mode,images, ordetails(admin-only).GET /store/orders/:order_id/refund-payment-mappings— list all refund payment mappings for the order’s returns (customer must own the order). Use to show “all refunds successful” vs “some pending” (checkis_refundedon each).
- Admin API:
PUT /admin/refund-payment-mapping/:return_id— update admin-only fields. Body:{ refund_mode?: "offline" | "online" | null, images?: string[], details?: string | null }(partial; only provided keys are updated).POST /admin/refund-payment-mapping/:return_id/mark-refunded— setis_refunded = true(idempotent).
- Order detail integration:
GET /admin/orders/:order_id/refund-context— aggregated read returningreturn_id,refund_mapping(id, payment_id, is_refunded, refund_mode, images, details), andpayment_detail(id, type, detail_json, is_default) resolved from the customer's default payment detail. Returns null blocks when no return or mapping exists. An admin widget atorder.details.aftershows refund payment method, Refund mode (Offline/Online), Details, Images (with upload via MedusaPOST /admin/uploads), and Mark refunded (whenis_refunded = false). Only admin can change refund_mode, images, and details. - Registration: Add the module in
medusa-config.js(e.g.orderManagement.modules["refund-payment-mapping"]) and run migrations.
Return Orders Admin Panel
The plugin includes a dedicated section in the Medusa Admin Panel for managing customer return orders. This feature provides administrators with comprehensive tools to view, search, filter, and manage all return orders.
Features
- List View: View all return orders in a table format with key information
- Search: Search returns by return ID, order ID, or customer email
- Filtering: Filter returns by status (requested, received, requires_action, completed, canceled)
- Detail Pages: View detailed information for each return order including:
- Return information (ID, status, refund amount, reason, note)
- Related order information (order ID, customer, totals)
- Return items with quantities
- Status history timeline
- Metadata
- Status Management: Update return status with validation and status history tracking
- Pagination: Load more returns with pagination support
Accessing Return Orders
Once the plugin is installed and configured, you can access the Return Orders section from the Admin Panel sidebar. The section appears as "Return Orders" with a return icon.
API Endpoints
The plugin provides admin API endpoints for return orders:
GET /admin/returns- List all return orders (filtering, search, pagination)GET /admin/returns/:id- Get detailed information for a specific returnPOST /admin/returns/:id/reject- Reject the return
Full request/response details are in the Standalone Return Flow section below.
Return Statuses
The plugin supports the following return statuses:
requested- Return has been requested by the customerreceived- Return items have been receivedrequires_action- Return requires manual interventioncompleted- Return has been completedcanceled- Return has been canceled
Status updates are tracked in the return's metadata with timestamps and admin user IDs.
One active return or exchange per item (Medusa v2)
Medusa v2 does not allow multiple active return/exchange requests for the same order item at the same time. Once an item is part of a return or a swap (exchange), that quantity is reserved until the request is completed or canceled. Duplicate requests for the same quantity will fail validation (prevents double refunds / double fulfillment).
To allow a new request, the first one must be closed or canceled:
| State | Action | Effect |
|-------|--------|--------|
| Swap (exchange) pending / not confirmed | POST /admin/swaps/{swap_id}/reject or POST /admin/swaps/{swap_id}/cancel | Releases reserved quantities; item becomes eligible again. |
| Return created but not received yet | POST /admin/returns/{return_id}/reject | Closes return flow; quantities unlocked. |
| Exchange already created in Medusa | POST /admin/swaps/{swap_id}/cancel | Cancels the exchange; same unlock behavior. |
Storefront gating: Before showing "Request Return" or "Request Exchange", check the order (e.g. GET /admin/orders/{id} or your store API) and block if the item has an active return, active swap (exchange), or claim in progress. Show status instead (e.g. "Return in review", "Exchange pending", "Request rejected — retry available").
Operational pattern: When a customer creates a swap, the exchange is created directly (no approval step). Use statuses such as rejected, cancelled, completed and only allow a new request when status is rejected or cancelled.
Guest Order Portal
The Guest Order Portal allows users who placed orders without an account to view their order status, cancel orders, reorder, initiate returns, and download invoices securely via an OTP (One-Time Password) system.
Security Features
- Strict Separation: Guest orders are strictly filtered by unique guest customer IDs. Registered account orders will never be exposed in the guest portal.
- Access Control: The portal uses short-lived JWT tokens issued upon successful OTP verification.
- Account Protection: Emails belonging to registered accounts are blocked from the guest OTP flow to prevent unauthorized access and encourage secure logins.
Complete API Documentation
For detailed API documentation, usage examples, error handling, and security best practices, see the Guest Order API Guide.
Store API Endpoints
The plugin provides the following store API endpoints for the Guest Order Portal:
Authentication
POST /store/otp/request- Request an OTP for an email or phone number.POST /store/otp/verify- Verify the OTP and receive a guest JWT token.
Guest Order Management
GET /store/guest-orders- List summary of orders for the verified guest identifier.GET /store/guest-orders/:id- Get full details for a specific guest order (includes items, shipping status, etc.).
Guest Order Actions
GET /store/guest-orders/:id- Get full details for a specific guest order (requires guest JWT).POST /store/guest-orders/:id/returns- Initiate a return request for a guest order.POST /store/guest-orders/:id/cancel- Cancel a guest order.POST /store/guest-orders/:id/reorder- Reorder a guest order (creates a new cart with the same items).GET /store/guest-orders/:id/swaps- List exchange requests for the guest order.POST /store/guest-orders/:id/swaps- Create an exchange request (return items + new items).GET /store/guest-orders/:id/swaps/:swap_id- Get one exchange request.POST /store/guest-orders/:id/swaps/:swap_id/cancel- Cancel an exchange request.
Note: All guest order endpoints require the guest JWT in the Authorization: Bearer <token> header or cookie.
Authenticated Customer Order APIs
Logged-in customers (storefront with customer auth) can use these endpoints without the guest portal:
POST /store/orders/cancel/:order_id- Cancel an order (customer must own the order).POST /store/orders/reorder/:order_id- Reorder (creates a new cart with the same items).GET /store/returns,POST /store/returns,GET /store/returns/:id,POST /store/returns/:id/cancel- List and create returns (filtered by customer).GET /store/swaps,POST /store/swaps,GET /store/swaps/:id,POST /store/swaps/:id/cancel- List and create exchanges (filtered by customer).
All require Authorization: Bearer <customer_token>.
Email Templates
The plugin supports custom HTML templates for order confirmation emails. Templates use variable replacement with {{variable_name}} syntax.
Creating Templates
Create HTML template files for email notifications. Place them in your project, for example:
your-project/
src/
templates/
emails/
order-confirmation.htmlExample Email Template (src/templates/emails/order-confirmation.html):
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<style>
body { font-family: Arial, sans-serif; line-height: 1.6; color: #333; }
.container { max-width: 600px; margin: 0 auto; padding: 20px; }
.header { background-color: #4CAF50; color: white; padding: 20px; text-align: center; }
.content { padding: 20px; background-color: #ffffff; }
.order-info { background-color: #f5f5f5; padding: 15px; margin: 15px 0; border-radius: 5px; }
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>Order Confirmation</h1>
</div>
<div class="content">
<div class="order-info">
<p><strong>Order ID:</strong> {{order_id}}</p>
<p><strong>Status:</strong> {{order_status}}</p>
<p><strong>Total:</strong> {{order_total}}</p>
<p><strong>Email:</strong> {{order_email}}</p>
<p><strong>Date:</strong> {{order_date}}</p>
</div>
<p>Thank you for your order!</p>
</div>
</div>
</body>
</html>Template Variables
The following variables are available in order confirmation email templates:
| Variable | Description | Example |
|----------|-------------|---------|
| {{order_id}} | Order ID | order_123 |
| {{order_status}} | Order status | pending |
| {{order_total}} | Order total amount | 99.99 |
| {{currency_code}} | Order currency (ISO 4217, lowercase, e.g. usd, inr) | inr |
| {{order_email}} | Customer email address | [email protected] |
| {{order_date}} | Order date/time (localized, long date + short time) | January 15, 2024 at 10:30 AM |
| {{order_items}} | Order items array (JSON stringified) | [{"title":"Product","quantity":1}] |
| {{shipping_address}} | Shipping address object (JSON stringified) | {"first_name":"John",...} |
| {{billing_address}} | Billing address object (JSON stringified) | {"first_name":"John",...} |
| {{is_registered}} | Whether the customer email has a registered account | true |
| {{claim_link}} | Link for registered users to claim their guest order | http://.../claim?order_id=... |
Status-Based Notifications
The plugin supports automatic notifications (email and SMS) based on order status changes. When an order reaches a configured status, the plugin will automatically send all configured notifications for that status.
📖 For detailed documentation, see the Notifications Guide
Features
- Multi-Channel Support: Send notifications via email and/or SMS
- Status-Based Configuration: Configure different notifications for different order statuses
- Multiple Notifications Per Status: Send multiple notifications for a single status change
- Template-Based: Use custom templates for each notification
- Automatic Triggering: Notifications are automatically triggered when order status changes
Configuration
Configure status-based notifications in your medusa-config.js:
{
resolve: "order-management",
options: {
// ... other options ...
notifications: {
enabled: true,
statusConfig: {
// Status name as key
"pending": [
{
template: "src/templates/emails/order-placed.html",
channel: "email"
},
{
template: "src/templates/sms/order-placed.txt",
channel: "sms"
}
],
"shipped": [
{
template: "src/templates/emails/order-shipped.html",
channel: "email"
}
],
"delivered": [
{
template: "src/templates/emails/order-delivered.html",
channel: "email"
},
{
template: "src/templates/sms/order-delivered.txt",
channel: "sms"
}
],
"canceled": [
{
template: "src/templates/emails/order-canceled.html",
channel: "email"
}
]
}
}
}
}Supported Channels
The notification system supports dynamic channel types, allowing you to configure any channel supported by your notification service:
- email: Sends email notifications to the customer's email address
- sms: Sends SMS notifications to the customer's phone number (from shipping address)
- push: Sends push notifications (browser/mobile notifications) to the customer's device
- Any custom channel: Configure any channel name supported by your notification service (e.g.,
whatsapp,slack,webhook, etc.)
The system automatically resolves recipients and sends notifications through the appropriate channel handler.
Triggered Events
The notification system listens to the following Medusa events:
order.updated- General order status changesorder.placed- When an order is first placedorder.shipment_created- When a shipment is createdorder.fulfillment_created- When fulfillment startsorder.completed- When an order is completedorder.canceled- When an order is canceleddelivery.created- When an order is delivered
Using Push Notifications
The plugin now supports push notifications (browser/mobile push) alongside email and SMS. Here's how to configure them:
{
resolve: "order-management",
options: {
// ... other options ...
notifications: {
enabled: true,
statusConfig: {
"shipped": [
{
template: "src/templates/emails/order-shipped.html",
channel: "email"
},
{
template: "src/templates/push/order-shipped.txt",
channel: "push" // Browser/mobile push notification
}
],
"delivered": [
{
template: "src/templates/emails/order-delivered.html",
channel: "email"
},
{
template: "src/templates/sms/order-delivered.txt",
channel: "sms"
},
{
template: "src/templates/push/order-delivered.txt",
channel: "push" // Browser/mobile push notification
}
]
}
}
}
}Push Notification Template Example (src/templates/push/order-shipped.txt):
📦 Your order {{order_id}} has been shipped! Track your package to stay updated.Important Notes:
- Push notifications require a configured notification service provider (e.g., FCM, OneSignal, Web Push API)
- The recipient resolver for push notifications uses the customer's email to identify the device token
- You may need to implement custom device token registration in your storefront
- Push notification templates should be concise (similar to SMS)
Creating Notification Templates
Note: The plugin includes example templates in src/templates/emails/ and src/templates/sms/ that you can use as a starting point.
Email Templates
Create HTML templates for email notifications. Templates use the same variable replacement syntax as order confirmation emails:
Example: Order Shipped Email (src/templates/emails/order-shipped.html)
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<style>
body { font-family: Arial, sans-serif; line-height: 1.6; color: #333; }
.container { max-width: 600px; margin: 0 auto; padding: 20px; }
.header { background-color: #2196F3; color: white; padding: 20px; text-align: center; }
.content { padding: 20px; background-color: #ffffff; }
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>Your Order Has Shipped!</h1>
</div>
<div class="content">
<p>Hi there,</p>
<p>Great news! Your order <strong>{{order_id}}</strong> has been shipped and is on its way to you.</p>
<p><strong>Order Total:</strong> {{order_total}}</p>
<p><strong>Shipped on:</strong> {{order_date}}</p>
<p>Thank you for your purchase!</p>
</div>
</div>
</body>
</html>SMS Templates
Create plain text templates for SMS notifications:
Example: Order Shipped SMS (src/templates/sms/order-shipped.txt)
Your order {{order_id}} has been shipped! Total: {{order_total}}. Thank you for your purchase.Available Template Variables
All notification templates have access to the same variables as order confirmation emails:
{{order_id}}- Order ID{{order_status}}- Current order status{{order_total}}- Order total amount{{order_email}}- Customer email address{{order_date}}- Order creation/update date{{order_items}}- Order items array (JSON stringified){{shipping_address}}- Shipping address object (JSON stringified){{billing_address}}- Billing address object (JSON stringified){{is_registered}}- Whether the customer has a registered account{{claim_link}}- Link for registered users to claim their order
Common Status Names
Common Medusa order statuses you can configure notifications for:
pending- Order placed, payment pendingawaiting- Awaiting fulfillmentfulfilled- Order fulfilledshipped- Order shippeddelivered- Order deliveredcompleted- Order completedcanceled- Order canceledrequires_action- Order requires action
Note: You can configure notifications for any custom status your store uses.
Extending with Custom Channels
The notification system is designed to be extensible. Any channel name you configure will automatically be handled by the notification service. For example:
notifications: {
enabled: true,
statusConfig: {
"shipped": [
{
template: "src/templates/whatsapp/order-shipped.txt",
channel: "whatsapp" // Custom channel
},
{
template: "src/templates/slack/order-shipped.txt",
channel: "slack" // Custom channel
}
]
}
}Channel Handler Flow:
- System detects configured channel (e.g.,
whatsapp,slack) - Resolves recipient using channel-specific resolver (falls back to email/customer ID)
- Loads and renders the specified template
- Sends notification via Medusa's notification service with the channel name
- Your configured notification provider handles the actual delivery
Requirements:
- Configure a notification provider that supports your custom channel
- The provider should handle the channel name in its notification payload
- Implement custom recipient resolvers if needed (by default, uses email or customer ID)
Best Practices
- Keep SMS/Push Messages Short: SMS and push notifications have character limits, keep messages concise
- Use Descriptive Subject Lines: For email templates, include clear subject information
- Test Templates: Test your templates with sample data before going live
- Provide Value: Only send notifications that provide value to customers
- Include Contact Info: Add customer support contact information in templates
- Mobile-Friendly Emails: Design email templates that work well on mobile devices
- SMS/Push Opt-In: Ensure customers have opted in to receive SMS and push notifications
- Channel Fallbacks: Consider configuring multiple channels (e.g., email + push) for important notifications
Example: Complete Notification Flow
// In medusa-config.js
notifications: {
enabled: true,
statusConfig: {
// Order placed
"pending": [
{
template: "src/templates/emails/order-placed.html",
channel: "email"
}
],
// Order processing
"awaiting": [
{
template: "src/templates/emails/order-processing.html",
channel: "email"
}
],
// Order shipped
"shipped": [
{
template: "src/templates/emails/order-shipped.html",
channel: "email"
},
{
template: "src/templates/sms/order-shipped.txt",
channel: "sms"
}
],
// Order delivered
"delivered": [
{
template: "src/templates/emails/order-delivered.html",
channel: "email"
},
{
template: "src/templates/sms/order-delivered.txt",
channel: "sms"
},
{
template: "src/templates/push/order-delivered.txt",
channel: "push"
}
]
}
}This configuration will:
- Send an email when the order is placed (
pending) - Send an email when the order starts processing (
awaiting) - Send both email and SMS when the order is shipped (
shipped) - Send email, SMS, and push notifications when the order is delivered (
delivered)
Customer-Initiated Swaps
The plugin includes a complete swap/exchange feature that allows customers to request item swaps directly from the storefront. This extends Medusa v2's native OrderExchange functionality with customer-facing APIs and admin management tools.
Features
- Customer-Initiated Swaps: Customers create swaps from the storefront; the exchange is created directly (no approval step).
- Complete Status Lifecycle: Full status tracking from creation →
return_started→return_shipped→return_received→new_items_shipped→completed - Admin Management: Admin panel for viewing, rejecting, and cancelling swaps (no approve step; exchange exists as soon as the customer submits).
- Price Difference Calculation: Automatic calculation of price differences between returned and new items
- Status History: Complete audit trail of all status changes
One active return or exchange per item
Medusa v2 allows only one active return or exchange per order item at a time. To allow a new swap/return for the same item, cancel or reject the existing one first (see One active return or exchange per item in the Return Orders section).
Swap Status Flow
When a customer creates a swap, the exchange is created and confirmed immediately. Status then progresses as:
(created with exchange) → return_started → return_shipped → return_received → new_items_shipped → completed
↘ rejected
↘ cancelled (from any status except completed)Storefront API Endpoints (Authenticated Customers)
All store swap endpoints require customer authentication (Authorization: Bearer <customer_token>).
Create Swap Request
POST /store/swaps
Authorization: Bearer <customer_token>
Content-Type: application/json
{
"order_id": "order_123",
"return_items": [
{
"id": "item_123",
"quantity": 1,
"reason": "Wrong size"
}
],
"new_items": [
{
"variant_id": "variant_456",
"quantity": 1
}
],
"reason": "Size exchange",
"note": "Please send size M instead"
}List Customer Swaps
GET /store/swaps?order_id=order_123&limit=100&offset=0
Authorization: Bearer <customer_token>Optional query: order_id to filter by order; limit and offset for pagination.
Get Swap Details
GET /store/swaps/{swap_id}
Authorization: Bearer <customer_token>Cancel Swap
POST /store/swaps/{swap_id}/cancel
Authorization: Bearer <customer_token>Admin API Endpoints
List All Swaps
GET /admin/swaps?status=requested&order_id=order_123&limit=50&offset=0Get Swap Details
GET /admin/swaps/{swap_id}Reject Swap
POST /admin/swaps/{swap_id}/reject
Content-Type: application/json
{
"reason": "Item out of stock"
}Cancel Exchange
POST /admin/swaps/{swap_id}/cancelCancels an exchange that has already been created in Medusa (has exchange_id). Only available when the swap is not already completed, rejected, or cancelled.
Admin UI
The plugin includes an admin UI for managing swaps:
- Swaps List Page: View all swaps with filtering by status, search by order ID, and pagination
- Swap Detail Page: View swap information including swap details, order, return items, new items, and status history. Actions: Reject and Cancel Exchange
Access the Swaps section from the Admin Panel sidebar.
Storefront Helpers
The plugin exports helpers for use in custom workflows or server-side code (e.g. within the Medusa backend). Import from the built plugin:
import { createSwapRequest, getSwaps, getSwap, cancelSwap } from "order-management/helpers"
// Create a swap request (order_id in payload)
const swap = await createSwapRequest({
orderId: "order_123",
returnItems: [
{ id: "item_123", quantity: 1, reason: "Wrong size" }
],
newItems: [
{ variant_id: "variant_456", quantity: 1 }
],
reason: "Size exchange",
note: "Please send size M"
}, container)
// Get swaps (optionally filtered by order)
const swaps = await getSwaps("order_123", container)
// Get a specific swap
const swapDetails = await getSwap("swap_123", container)
// Cancel a swap
const cancelledSwap = await cancelSwap("swap_123", container)Return helpers are available from order-management/helpers/returns (see Standalone Return Flow section).
Status Transitions
The swap status flow enforces valid transitions (exchange is created directly on customer request; no approval step):
- requested / initial →
rejected,cancelled, or progress toreturn_started - return_started →
return_shipped,cancelled - return_shipped →
return_received,cancelled - return_received →
new_items_shipped,cancelled - new_items_shipped →
completed,cancelled - rejected → (terminal state)
- completed → (terminal state)
- cancelled → (terminal state)
Invalid status transitions will be rejected with a descriptive error message.
Return created_by: When an exchange is created, a return is created by Medusa's workflow without created_by. The plugin sets the return's created_by in store and guest swap routes after the workflow runs, so it matches the order change creator. A subscriber on order.return_requested syncs created_by from the order change when the event is emitted. For exchanges created in the admin and completed via Medusa's admin API, the exchange workflow does not emit that event, so the return's created_by may remain unset unless Medusa adds support (e.g. passing created_by into the workflow or emitting the event).
Standalone Return Flow
The plugin includes a comprehensive standalone return management system that allows customers to initiate returns directly from the storefront. This extends Medusa v2's native Return functionality with customer-facing APIs, admin management tools, and complete status lifecycle tracking.
Features
- Customer-Initiated Returns: Customers can request returns for their orders directly from the storefront
- Complete Status Lifecycle: Full status tracking from
requested→approved→received→refunded→completed - Admin Management: Complete admin panel with action buttons for approving, rejecting, marking as received, and processing refunds
- Automatic Refund Calculation: Automatic calculation of refund amounts based on returned items
- Status History: Complete audit trail of all status changes with timestamps and admin IDs
- Auto-Linking: Automatic linking between Medusa returns and custom return module via event subscribers
- Multi-Channel Notifications: Support for email, SMS, and push notifications for return status changes
Return Status Flow
requested → approved → received → refunded → completed
↘ rejected
↘ cancelled (by customer, only when requested)Storefront API Endpoints
Create Return Request
POST /store/returns
Authorization: Bearer <customer_token>
Content-Type: application/json
{
"order_id": "order_123",
"return_items": [
{
"id": "item_123",
"quantity": 1,
"reason": "Defective item",
"variant_id": "variant_123" // Optional
}
],
"reason": "Product defective",
"note": "Item arrived damaged"
}List Customer Returns
GET /store/returns?order_id=order_123
Authorization: Bearer <customer_token>Get Return Details
GET /store/returns/{return_id}
Authorization: Bearer <customer_token>Create Return for Order
POST /store/orders/{order_id}/returns
Authorization: Bearer <customer_token>
Content-Type: application/json
{
"return_items": [
{
"id": "item_123",
"quantity": 1,
"reason": "Wrong size"
}
],
"reason": "Size issue",
"note": "Ordered wrong size"
}Cancel Return
POST /store/returns/{return_id}/cancel
Authorization: Bearer <customer_token>Note: Returns can only be cancelled when status is requested.
Admin API Endpoints
List All Returns
GET /admin/returns?status=requested&order_id=order_123&limit=50&offset=0Get Return Details
GET /admin/returns/{return_id}Reject Return
POST /admin/returns/{return_id}/reject
Content-Type: application/json
{
"reason": "Outside return window"
}Admin UI
The plugin includes a complete admin UI for managing returns:
- Returns List Page: View all returns with filtering by status, search by return/order ID, and pagination
- Return Detail Page: View complete return information including:
- Return details (ID, status, dates, refund amount, refund status)
- Action button: Reject (for requested returns)
- Medusa return information (when linked)
- Related order information
- Return items list
- Status history timeline
Action Buttons by Status:
- requested: Show "Approve & Process Return" and "Reject Return" buttons
- approved: Show "Mark as Received" button
- received: Show "Process Refund" button
Access the Returns section from the Admin Panel sidebar at /returns.
Status Transitions
The return status flow enforces valid transitions:
- requested →
approved,rejected,cancelled - approved →
received,cancelled - rejected → (terminal state)
- received →
refunded - refunded →
completed - completed → (terminal state)
- cancelled → (terminal state)
Invalid status transitions will be rejected with a descriptive error message.
Event Subscribers
The return flow includes automatic event handling:
Return Created Subscriber
- Listens to:
return.created,order.return_requested - Automatically links Medusa returns to custom return module
- Updates return with
medusa_return_id - Tracks linking history in metadata
Return Received Subscriber
- Listens to:
return.received - Automatically updates return status to
received - Tracks receipt in status history
Return Refunded Subscriber
- Listens to:
refund.created,return.refund_processed - Automatically updates return status to
refunded - Updates
refund_statustorefunded - Tracks refund amount and timestamp
Refund Calculation
Refund amounts are automatically calculated based on:
- Item unit prices at time of order
- Quantity of items being returned
- Stored in
refund_amountfield (in cents)
Example calculation:
refund_amount = Σ (item.unit_price * return_quantity) for each returned itemEligibility and Validation
Returns are validated for:
- Order Ownership: Customer must own the order (or guest token must match the order email)
- Default Payment Method: Customer must have a default payment method before creating a return
- Delivery Window: Return is blocked if more than
returnValidInDaysdays have passed sincefulfillment.delivered_at. If the fulfillment has nodelivered_atthe check is skipped. Configure in plugin options (default: 7 days) - Exchange Window: Exchange is blocked if more than
exchangeValidInDaysdays have passed sincefulfillment.delivered_at(default: 7 days) - Duplicate Prevention: Cannot create a new return or exchange while one is already pending or requested for the same order
Helper Functions
The plugin provides helper functions for return operations:
import {
getOrderReturnData,
validateReturnWindow,
calculateRefundAmount,
canApproveReturn,
canCancelReturn,
canMarkAsReceived,
canProcessRefund,
} from "order-management/helpers/returns"
// Get order data for return
const returnData = await getOrderReturnData("order_123", container)
// Validate return window
const isValid = validateReturnWindow(order.created_at, 30) // 30 days
// Calculate refund amount
const refund = calculateRefundAmount(returnItems, orderItems)
// Check status permissions
const canApprove = canApproveReturn(return.status)
const canCancel = canCancelReturn(return.status)
const canReceive = canMarkAsReceived(return.status)
const canRefund = canProcessRefund(return.status)Integration with Medusa Returns
The custom return module integrates seamlessly with Medusa's native return functionality:
- Return Creation: When approved, creates a Medusa return entity via
orderModuleService.createReturn() - Linking: Stores
medusa_return_idfor bidirectional linking - Status Sync: Event subscribers automatically sync status changes from Medusa to custom module
- View in Medusa: Admin UI provides direct links to view returns in Medusa admin
Metadata Structure
Returns store rich metadata for audit trails:
{
metadata: {
created_at: "2026-02-05T10:30:00Z",
customer_id: "cus_123",
approved_at: "2026-02-05T11:00:00Z",
approved_by: "admin_456",
received_at: "2026-02-10T14:20:00Z",
refunded_at: "2026-02-10T15:00:00Z",
medusa_return_id: "ret_789",
medusa_return_linked_at: "2026-02-05T11:00:30Z",
status_history: [
{
status: "requested",
timestamp: "2026-02-05T10:30:00Z",
customer_id: "cus_123"
},
{
status: "approved",
timestamp: "2026-02-05T11:00:00Z",
admin_id: "admin_456"
},
{
status: "received",
timestamp: "2026-02-10T14:20:00Z",
admin_id: "admin_456"
},
{
status: "refunded",
timestamp: "2026-02-10T15:00:00Z",
admin_id: "admin_456",
refund_amount: 5000 // in cents
}
]
}
}Getting Started
Visit the Quickstart Guide to set up a server.
Visit the Plugins documentation to learn more about plugins and how to create them.
Visit the Docs to learn more about our system requirements.
What is Medusa
Medusa is a set of commerce modules and tools that allow you to build rich, reliable, and performant commerce applications without reinventing core commerce logic. The modules can be customized and used to build advanced ecommerce stores, marketplaces, or any product that needs foundational commerce primitives. All modules are open-source and freely available on npm.
Learn more about Medusa’s architecture and commerce modules in the Docs.
Community & Contributions
The community and core team are available in GitHub Discussions, where you can ask for support, discuss roadmap, and share ideas.
Join our Discord server to meet other community members.
