www/apps/resources/app/integrations/guides/resend/page.mdx
import { Card, Prerequisites, Details } from "docs-ui" import { Github } from "@medusajs/icons"
export const ogImage = "https://res.cloudinary.com/dza7lstvk/image/upload/v1732704590/Medusa%20Resources/Integrations_Resend_pam0nf.jpg"
export const metadata = {
title: Integrate Medusa with Resend (Email Notifications),
openGraph: {
images: [
{
url: ogImage,
width: 1600,
height: 900,
type: "image/jpeg"
}
],
},
twitter: {
images: [
{
url: ogImage,
width: 1600,
height: 900,
type: "image/jpeg"
}
]
}
}
In this guide, you'll learn how to integrate Medusa with Resend.
When you install a Medusa application, you get a fully-fledged commerce platform with a Framework for customization. Medusa's architecture supports integrating third-party services, such as an email service, that allow you to build your unique requirements around core commerce flows.
Resend is an email service with an intuitive developer experience to send emails from any application type, including Node.js servers. By integrating Resend with Medusa, you can build flows to send an email when a commerce operation is performed, such as when an order is placed.
This guide will teach you how to:
You can follow this guide whether you're new to Medusa or an advanced Medusa developer.
<Card title="Example Repository" text="Find the full code of the guide in this repository." href="https://github.com/medusajs/examples/tree/main/resend-integration" icon={Github} />
<Prerequisites items={[ { text: "Node.js v20+", link: "https://nodejs.org/en/download" }, { text: "Git CLI tool", link: "https://git-scm.com/downloads" }, { text: "PostgreSQL", link: "https://www.postgresql.org/download/" } ]} />
Start by installing the Medusa application on your machine with the following command:
npx create-medusa-app@latest
You'll first be asked for the project's name. Then, when you're asked whether you want to install the Next.js Starter Storefront, choose Y for yes.
Afterwards, the installation process will start, which will install the Medusa application as a monorepository in a directory with your project's name. The backend is installed in the apps/backend directory, and the Next.js Starter Storefront is installed in the apps/storefront directory.
Once the installation finishes successfully, the Medusa Admin dashboard will open with a form to create a new user. Enter the user's credential and submit the form. Afterwards, you can login with the new user and explore the dashboard.
The Next.js Starter Storefront is also running at http://localhost:8000.
Check out the troubleshooting guides for help.
</Note> <Note>In this guide, all file paths of backend customizations are relative to the apps/backend directory of your Medusa project.
If you don't have a Resend Account, create one on their website.
In addition, Resend allows you to send emails from the address [email protected] only to your account's email, which is useful for development purposes. If you have a custom domain to send emails from, add it to your Resend account's domains:
3. In the form that opens, enter your domain name and select a region close to your users, then click Add.
4. In the domain's details page that opens, you'll find DNS records to add to your DNS provider. After you add them, click on Verify DNS Records. You can start sending emails from your custom domain once it's verified.
<Note title="Tip">The steps to add DNS records are different for each provider, so refer to your provider's documentation or knowledge base for more details.
</Note>You also need an API key to connect to your Resend account from Medusa, but you'll create that one in a later section.
In this step, you'll install two packages useful for your Resend integration:
resend, which is the Resend SDK:npm install resend
2. react-email, which is a package created by Resend to create email templates with React:
npm install @react-email/components -E
You'll use these packages in the next steps.
To integrate third-party services into Medusa, you create a custom module. A module is a re-usable package with functionalities related to a single feature or domain. Medusa integrates the module into your application without implications or side effects on your setup.
Medusa's Notification Module delegates sending notifications to other modules, called module providers. In this step, you'll create a Resend Module Provider that implements sending notifications through the email channel. In later steps, you'll send email notifications with Resend when an order is placed through this provider.
<Note>Learn more about modules in this documentation.
</Note>A module is created under the src/modules directory of your Medusa application. So, create the directory src/modules/resend.
You define a module's functionalities in a service. A service is a TypeScript or JavaScript class that the module exports. In the service's methods, you can connect to the database, which is useful if your module defines tables in the database, or connect to a third-party service.
In this section, you'll create the Resend Module Provider's service and the methods necessary to send an email with Resend.
Start by creating the file src/modules/resend/service.ts with the following content:
export const serviceHighlights1 = [ ["12", "api_key", "The Resend API key."], ["13", "from", "The email address to send the emails from."], ["14", "html_templates", "Option to override or provide custom templates."], ["21", "identifier", "Unique identifier that Medusa will use when registering the provider in the database."], ["22", "resendClient", "Client to send emails with Resend."], ["23", "options", "Options passed to the module provider."], ["24", "logger", "Logger utility to log messages in the terminal."] ]
import {
AbstractNotificationProviderService,
} from "@medusajs/framework/utils"
import {
Logger,
} from "@medusajs/framework/types"
import {
Resend,
} from "resend"
type ResendOptions = {
api_key: string
from: string
html_templates?: Record<string, {
subject?: string
content: string
}>
}
class ResendNotificationProviderService extends AbstractNotificationProviderService {
static identifier = "notification-resend"
private resendClient: Resend
private options: ResendOptions
private logger: Logger
// ...
}
export default ResendNotificationProviderService
A Notification Module Provider's service must extend the AbstractNotificationProviderService. It has a send method that you'll implement soon. The service must also have an identifier static property, which is a unique identifier that the Medusa application will use to register the provider in the database.
The ResendNotificationProviderService class also has the following properties:
resendClient of type Resend (from the Resend SDK you installed in the previous step) to send emails through Resend.options of type ResendOptions. Modules accept options through Medusa's configurations. This ensures that the module is reusable across applications and you don't use sensitive variables like API keys directly in your code. The options that the Resend Module Provider accepts are:
api_key: The Resend API key.from: The email address to send the emails from.html_templates: An optional object to replace the default subject and template that the Resend Module uses. This is also useful to support custom emails in different Medusa application setups.logger property, which is an instance of Medusa's Logger, to log messages.To send requests using the resendClient, you need to initialize it in the class's constructor. So, add the following constructor to ResendNotificationProviderService:
// ...
type InjectedDependencies = {
logger: Logger
}
class ResendNotificationProviderService extends AbstractNotificationProviderService {
// ...
constructor(
{ logger }: InjectedDependencies,
options: ResendOptions
) {
super()
this.resendClient = new Resend(options.api_key)
this.options = options
this.logger = logger
}
}
A module's service accepts two parameters:
Using the API key passed in the module's options, you initialize the Resend client. You also set the options and logger properties.
A Notification Module Provider's service can implement a static validateOptions method that ensures the options passed to the module through Medusa's configurations are valid.
So, add to the ResendNotificationProviderService the validateOptions method:
// other imports...
import {
// other imports...
MedusaError,
} from "@medusajs/framework/utils"
// ...
class ResendNotificationProviderService extends AbstractNotificationProviderService {
// ...
static validateOptions(options: Record<any, any>) {
if (!options.api_key) {
throw new MedusaError(
MedusaError.Types.INVALID_DATA,
"Option `api_key` is required in the provider's options."
)
}
if (!options.from) {
throw new MedusaError(
MedusaError.Types.INVALID_DATA,
"Option `from` is required in the provider's options."
)
}
}
}
In the validateOptions method, you throw an error if the api_key or from options aren't passed to the module. To throw errors, you use MedusaError from the Modules SDK. This ensures errors follow Medusa's conventions and are displayed similar to Medusa's errors.
Each email type has a different template and content. For example, order confirmation emails show the order's details, whereas customer confirmation emails show a greeting message to the customer.
So, add two methods to the ResendNotificationProviderService class that retrieve the email template and subject of a specified template type:
export const serviceHighlights2 = [ ["3", "Templates", "Names of supported template types."], ["7", "templates", "A map of templates for each template type."], ["13", "getTemplate", "Retrieve the template of a template type."], ["26", "getTemplateSubject", "Retrieve the subject line of a template type."] ]
// imports and types...
enum Templates {
ORDER_PLACED = "order-placed",
}
const templates: {[key in Templates]?: (props: unknown) => React.ReactNode} = {
// TODO add templates
}
class ResendNotificationProviderService extends AbstractNotificationProviderService {
// ...
getTemplate(template: Templates) {
if (this.options.html_templates?.[template]) {
return this.options.html_templates[template].content
}
const allowedTemplates = Object.keys(templates)
if (!allowedTemplates.includes(template)) {
return null
}
return templates[template]
}
getTemplateSubject(template: Templates) {
if (this.options.html_templates?.[template]?.subject) {
return this.options.html_templates[template].subject
}
switch(template) {
case Templates.ORDER_PLACED:
return "Order Confirmation"
default:
return "New Email"
}
}
}
You first define a Templates enum, which holds the names of supported template types. You can add more template types to this enum later. You also define a templates variable that specifies the React template for each template type. You'll add templates to this variable later.
In the ResendNotificationProviderService you add two methods:
getTemplate: Retrieve the template of a template type. If the html_templates option is set for the specified template type, you return its content's value. Otherwise, you retrieve the template from the templates variable.getTemplateSubject: Retrieve the subject of a template type. If a subject is passed for the template type in the html_templates, you return its value. Otherwise, you return a subject based on the template type.You'll use these methods in the send method next.
In this section, you'll implement the send method of ResendNotificationProviderService. When you send a notification through the email channel later using the Notification Module, the Notification Module's service will use this send method under the hood to send the email with Resend.
In the send method, you'll retrieve the template and subject of the email template, then send the email using the Resend client.
Add the send method to the ResendNotificationProviderService class:
export const serviceHighlights3 = [
["17", "getTemplate", "Get the template of the specified type."],
["25", "from", "The email address to send the email from."],
["26", "to", "The email address to send the email to"],
["27", "getTemplateSubject", "Get the email subject for the template type."],
["34", "html", "Set the template as an HTML template if its type is string."],
["39", "react", "Set the template as a React template."],
["43", "send", "Send the email using the Resend client."]
]
// other imports...
import {
// ...
ProviderSendNotificationDTO,
ProviderSendNotificationResultsDTO,
} from "@medusajs/framework/types"
import {
// ...
CreateEmailOptions,
} from "resend"
class ResendNotificationProviderService extends AbstractNotificationProviderService {
// ...
async send(
notification: ProviderSendNotificationDTO
): Promise<ProviderSendNotificationResultsDTO> {
const template = this.getTemplate(notification.template as Templates)
if (!template) {
this.logger.error(`Couldn't find an email template for ${notification.template}. The valid options are ${Object.values(Templates)}`)
return {}
}
const commonOptions = {
from: this.options.from,
to: [notification.to],
subject: this.getTemplateSubject(notification.template as Templates),
}
let emailOptions: CreateEmailOptions
if (typeof template === "string") {
emailOptions = {
...commonOptions,
html: template,
}
} else {
emailOptions = {
...commonOptions,
react: template(notification.data),
}
}
const { data, error } = await this.resendClient.emails.send(emailOptions)
if (error || !data) {
if (error) {
this.logger.error("Failed to send email", error)
} else {
this.logger.error("Failed to send email: unknown error")
}
return {}
}
return { id: data.id }
}
}
The send method receives the notification details object as a parameter. Some of its properties include:
to: The address to send the notification to.template: The template type of the notification.data: The data useful for the email type. For example, when sending an order-confirmation email, data would hold the order's details.In the method, you retrieve the template and subject of the email using the methods you defined earlier. Then, you put together the data to pass to Resend, such as the email address to send the notification to and the email address to send from.
Also, if the email's template is a string, it's passed as an HTML template. Otherwise, it's passed as a React template.
Finally, you use the emails.send method of the Resend client to send the email. If an error occurs you log it in the terminal. Otherwise, you return the ID of the send email as received from Resend. Medusa uses this ID when creating the notification in its database.
The ResendNotificationProviderService class now has the methods necessary to start sending emails.
Next, you must export the module provider's definition, which lets Medusa know what module this provider belongs to and its service.
Create the file src/modules/resend/index.ts with the following content:
import {
ModuleProvider,
Modules,
} from "@medusajs/framework/utils"
import ResendNotificationProviderService from "./service"
export default ModuleProvider(Modules.NOTIFICATION, {
services: [ResendNotificationProviderService],
})
You export the module provider's definition using ModuleProvider from the Modules SDK. It accepts as a first parameter the name of the module that this provider belongs to, which is the Notification Module. It also accepts as a second parameter an object having a service property indicating the provider's service.
Finally, to register modules and module providers in Medusa, you must add them to Medusa's configurations.
Medusa's configurations are set in the medusa-config.ts file, which is at the root directory of your Medusa application. The configuration object accepts a modules array, whose value is an array of modules to add to the application.
Add the modules property to the exported configurations in medusa-config.ts:
module.exports = defineConfig({
// ...
modules: [
{
resolve: "@medusajs/medusa/notification",
options: {
providers: [
{
resolve: "./src/modules/resend",
id: "resend",
options: {
channels: ["email"],
api_key: process.env.RESEND_API_KEY,
from: process.env.RESEND_FROM_EMAIL,
},
},
],
},
},
],
})
In the modules array, you pass a module object having the following properties:
resolve: The NPM package of the Notification Module. Since the Resend Module is a Notification Module Provider, it'll be passed in the options of the Notification Module.options: An object of options to pass to the module. It has a providers property which is an array of module providers to register. Each module provider object has the following properties:
resolve: The path to the module provider to register in the application. It can also be the name of an NPM package.id: A unique ID, which Medusa will use along with the identifier static property that you set earlier in the class to identify this module provider.options: An object of options to pass to the module provider. These are the options you expect and use in the module provider's service. You must also specify the channels option, which indicates the channels that this provider sends notifications through.Some of the module's options, such as the Resend API key, are set in environment variables. So, add the following environment variables to .env:
[email protected]
RESEND_API_KEY=
Where:
RESEND_FROM_EMAIL: The email to send emails from. If you've configured the custom domain as explained in Step 2, change this email to an email from your custom domain. Otherwise, you can use [email protected] for development purposes.RESEND_API_KEY is the API key of your Resend account. To retrieve it:
- In the form that opens, enter a name for the API key (for example, Medusa). You can keep its permissions to Full Access or change it to Sending Access. Once you're done, click Add.
- A new pop-up will show with your API key hidden. Copy it before closing the pop-up, since you can't access the key again afterwards. Use its value for the `RESEND_API_KEY` environment variable.
Your Resend Module Provider is all set up. You'll test it out in a later section.
In this step, you'll add a React template for order confirmation emails. You'll create it using the react-email package you installed earlier. You can follow the same steps for other email templates, such as for customer confirmation.
Create the directory src/modules/resend/emails that will hold the email templates. Then, to add the template for order confirmation, create the file src/modules/resend/emails/order-placed.tsx with the following content:
export const templateHighlights = [
["29", "OrderPlacedEmailComponent", "The template React component for order confirmation emails."],
["193", "orderPlacedEmail", "A function that returns the JSX OrderPlacedEmailComponent."]
]
import {
Text,
Column,
Container,
Heading,
Html,
Img,
Row,
Section,
Tailwind,
Head,
Preview,
Body,
Link,
} from "@react-email/components"
import { BigNumberValue, CustomerDTO, OrderDTO } from "@medusajs/framework/types"
type OrderPlacedEmailProps = {
order: OrderDTO & {
customer: CustomerDTO
}
email_banner?: {
body: string
title: string
url: string
}
}
function OrderPlacedEmailComponent({ order, email_banner }: OrderPlacedEmailProps) {
const shouldDisplayBanner = email_banner && "title" in email_banner
const formatter = new Intl.NumberFormat([], {
style: "currency",
currencyDisplay: "narrowSymbol",
currency: order.currency_code,
})
const formatPrice = (price: BigNumberValue) => {
if (typeof price === "number") {
return formatter.format(price)
}
if (typeof price === "string") {
return formatter.format(parseFloat(price))
}
return price?.toString() || ""
}
return (
<Tailwind>
<Html className="font-sans bg-gray-100">
<Head />
<Preview>Thank you for your order from Medusa</Preview>
<Body className="bg-white my-10 mx-auto w-full max-w-2xl">
<Section className="bg-[#27272a] text-white px-6 py-4">
<svg width="15" height="15" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M16.2447 3.92183L12.1688 1.57686C10.8352 0.807712 9.20112 0.807712 7.86753 1.57686L3.77285 3.92183C2.45804 4.69098 1.63159 6.11673 1.63159 7.63627V12.345C1.63159 13.8833 2.45804 15.2903 3.77285 16.0594L7.84875 18.4231C9.18234 19.1923 10.8165 19.1923 12.15 18.4231L16.2259 16.0594C17.5595 15.2903 18.3672 13.8833 18.3672 12.345V7.63627C18.4048 6.11673 17.5783 4.69098 16.2447 3.92183ZM10.0088 14.1834C7.69849 14.1834 5.82019 12.3075 5.82019 10C5.82019 7.69255 7.69849 5.81657 10.0088 5.81657C12.3191 5.81657 14.2162 7.69255 14.2162 10C14.2162 12.3075 12.3379 14.1834 10.0088 14.1834Z" fill="currentColor"></path></svg>
</Section>
<Container className="p-6">
<Heading className="text-2xl font-bold text-center text-gray-800">
Thank you for your order, {order.customer?.first_name || order.shipping_address?.first_name}
</Heading>
<Text className="text-center text-gray-600 mt-2">
We're processing your order and will notify you when it ships.
</Text>
</Container>
{shouldDisplayBanner && (
<Container
className="mb-4 rounded-lg p-7"
style={{
background: "linear-gradient(to right, #3b82f6, #4f46e5)",
}}
>
<Section>
<Row>
<Column align="left">
<Heading className="text-white text-xl font-semibold">
{email_banner.title}
</Heading>
<Text className="text-white mt-2">{email_banner.body}</Text>
</Column>
<Column align="right">
<Link href={email_banner.url} className="font-semibold px-2 text-white underline">
Shop Now
</Link>
</Column>
</Row>
</Section>
</Container>
)}
<Container className="px-6">
<Heading className="text-xl font-semibold text-gray-800 mb-4">
Your Items
</Heading>
<Row>
<Column>
<Text className="text-sm m-0 my-2 text-gray-500">Order ID: #{order.display_id}</Text>
</Column>
</Row>
{order.items?.map((item) => (
<Section key={item.id} className="border-b border-gray-200 py-4">
<Row>
<Column className="w-1/3">
</Column>
<Column className="w-2/3 pl-4">
<Text className="text-lg font-semibold text-gray-800">
{item.product_title}
</Text>
<Text className="text-gray-600">{item.variant_title}</Text>
<Text className="text-gray-800 mt-2 font-bold">
{formatPrice(item.total)}
</Text>
</Column>
</Row>
</Section>
))}
<Section className="mt-8">
<Heading className="text-xl font-semibold text-gray-800 mb-4">
Order Summary
</Heading>
<Row className="text-gray-600">
<Column className="w-1/2">
<Text className="m-0">Subtotal</Text>
</Column>
<Column className="w-1/2 text-right">
<Text className="m-0">
{formatPrice(order.item_total)}
</Text>
</Column>
</Row>
{order.shipping_methods?.map((method) => (
<Row className="text-gray-600" key={method.id}>
<Column className="w-1/2">
<Text className="m-0">{method.name}</Text>
</Column>
<Column className="w-1/2 text-right">
<Text className="m-0">{formatPrice(method.total)}</Text>
</Column>
</Row>
))}
<Row className="text-gray-600">
<Column className="w-1/2">
<Text className="m-0">Tax</Text>
</Column>
<Column className="w-1/2 text-right">
<Text className="m-0">{formatPrice(order.tax_total || 0)}</Text>
</Column>
</Row>
<Row className="border-t border-gray-200 mt-4 text-gray-800 font-bold">
<Column className="w-1/2">
<Text>Total</Text>
</Column>
<Column className="w-1/2 text-right">
<Text>{formatPrice(order.total)}</Text>
</Column>
</Row>
</Section>
</Container>
<Section className="bg-gray-50 p-6 mt-10">
<Text className="text-center text-gray-500 text-sm">
If you have any questions, reply to this email or contact our support team at [email protected].
</Text>
<Text className="text-center text-gray-500 text-sm">
Order Token: {order.id}
</Text>
<Text className="text-center text-gray-400 text-xs mt-4">
© {new Date().getFullYear()} Medusajs, Inc. All rights reserved.
</Text>
</Section>
</Body>
</Html>
</Tailwind >
)
}
export const orderPlacedEmail = (props: OrderPlacedEmailProps) => (
<OrderPlacedEmailComponent {...props} />
)
You define the OrderPlacedEmailComponent which is a React email template that shows the order's details, such as items and their totals. The component accepts an order object as a prop.
You also export an orderPlacedEmail function, which accepts props as an input and returns the OrderPlacedEmailComponent passing it the props. Because you can't use JSX syntax in src/modules/resend/service.ts, you'll import this function instead.
Next, update the templates variable in src/modules/resend/service.ts to assign this template to the order-placed template type:
// other imports...
import { orderPlacedEmail } from "./emails/order-placed"
const templates: {[key in Templates]?: (props: unknown) => React.ReactNode} = {
[Templates.ORDER_PLACED]: orderPlacedEmail,
}
The ResendNotificationProviderService will now use the OrderPlacedEmailComponent as the template of order confirmation emails.
You'll later test out sending the email when an order is placed. However, you can also test out how the email looks like using React Email's CLI tool.
First, install the CLI tool in your Medusa application:
npm install -D react-email
Then, in src/modules/resend/emails/order-placed.tsx, add the following at the end of the file:
const mockOrder = {
"order": {
"id": "order_01JSNXDH9BPJWWKVW03B9E9KW8",
"display_id": 1,
"email": "[email protected]",
"currency_code": "eur",
"total": 20,
"subtotal": 20,
"discount_total": 0,
"shipping_total": 10,
"tax_total": 0,
"item_subtotal": 10,
"item_total": 10,
"item_tax_total": 0,
"customer_id": "cus_01JSNXD6VQC1YH56E4TGC81NWX",
"items": [
{
"id": "ordli_01JSNXDH9C47KZ43WQ3TBFXZA9",
"title": "L",
"subtitle": "Medusa Sweatshirt",
"thumbnail": "https://medusa-public-images.s3.eu-west-1.amazonaws.com/sweatshirt-vintage-front.png",
"variant_id": "variant_01JSNXAQCZ5X81A3NRSVFJ3ZHQ",
"product_id": "prod_01JSNXAQBQ6MFV5VHKN420NXQW",
"product_title": "Medusa Sweatshirt",
"product_description": "Reimagine the feeling of a classic sweatshirt. With our cotton sweatshirt, everyday essentials no longer have to be ordinary.",
"product_subtitle": null,
"product_type": null,
"product_type_id": null,
"product_collection": null,
"product_handle": "sweatshirt",
"variant_sku": "SWEATSHIRT-L",
"variant_barcode": null,
"variant_title": "L",
"variant_option_values": null,
"requires_shipping": true,
"is_giftcard": false,
"is_discountable": true,
"is_tax_inclusive": false,
"is_custom_price": false,
"metadata": {},
"raw_compare_at_unit_price": null,
"raw_unit_price": {
"value": "10",
"precision": 20,
},
"created_at": new Date(),
"updated_at": new Date(),
"deleted_at": null,
"tax_lines": [],
"adjustments": [],
"compare_at_unit_price": null,
"unit_price": 10,
"quantity": 1,
"raw_quantity": {
"value": "1",
"precision": 20,
},
"detail": {
"id": "orditem_01JSNXDH9DK1XMESEZPADYFWKY",
"version": 1,
"metadata": null,
"order_id": "order_01JSNXDH9BPJWWKVW03B9E9KW8",
"raw_unit_price": null,
"raw_compare_at_unit_price": null,
"raw_quantity": {
"value": "1",
"precision": 20,
},
"raw_fulfilled_quantity": {
"value": "0",
"precision": 20,
},
"raw_delivered_quantity": {
"value": "0",
"precision": 20,
},
"raw_shipped_quantity": {
"value": "0",
"precision": 20,
},
"raw_return_requested_quantity": {
"value": "0",
"precision": 20,
},
"raw_return_received_quantity": {
"value": "0",
"precision": 20,
},
"raw_return_dismissed_quantity": {
"value": "0",
"precision": 20,
},
"raw_written_off_quantity": {
"value": "0",
"precision": 20,
},
"created_at": new Date(),
"updated_at": new Date(),
"deleted_at": null,
"item_id": "ordli_01JSNXDH9C47KZ43WQ3TBFXZA9",
"unit_price": null,
"compare_at_unit_price": null,
"quantity": 1,
"fulfilled_quantity": 0,
"delivered_quantity": 0,
"shipped_quantity": 0,
"return_requested_quantity": 0,
"return_received_quantity": 0,
"return_dismissed_quantity": 0,
"written_off_quantity": 0,
},
"subtotal": 10,
"total": 10,
"original_total": 10,
"discount_total": 0,
"discount_subtotal": 0,
"discount_tax_total": 0,
"tax_total": 0,
"original_tax_total": 0,
"refundable_total_per_unit": 10,
"refundable_total": 10,
"fulfilled_total": 0,
"shipped_total": 0,
"return_requested_total": 0,
"return_received_total": 0,
"return_dismissed_total": 0,
"write_off_total": 0,
"raw_subtotal": {
"value": "10",
"precision": 20,
},
"raw_total": {
"value": "10",
"precision": 20,
},
"raw_original_total": {
"value": "10",
"precision": 20,
},
"raw_discount_total": {
"value": "0",
"precision": 20,
},
"raw_discount_subtotal": {
"value": "0",
"precision": 20,
},
"raw_discount_tax_total": {
"value": "0",
"precision": 20,
},
"raw_tax_total": {
"value": "0",
"precision": 20,
},
"raw_original_tax_total": {
"value": "0",
"precision": 20,
},
"raw_refundable_total_per_unit": {
"value": "10",
"precision": 20,
},
"raw_refundable_total": {
"value": "10",
"precision": 20,
},
"raw_fulfilled_total": {
"value": "0",
"precision": 20,
},
"raw_shipped_total": {
"value": "0",
"precision": 20,
},
"raw_return_requested_total": {
"value": "0",
"precision": 20,
},
"raw_return_received_total": {
"value": "0",
"precision": 20,
},
"raw_return_dismissed_total": {
"value": "0",
"precision": 20,
},
"raw_write_off_total": {
"value": "0",
"precision": 20,
},
},
],
"shipping_address": {
"id": "caaddr_01JSNXD6W0TGPH2JQD18K97B25",
"customer_id": null,
"company": "",
"first_name": "safasf",
"last_name": "asfaf",
"address_1": "asfasf",
"address_2": "",
"city": "asfasf",
"country_code": "dk",
"province": "",
"postal_code": "asfasf",
"phone": "",
"metadata": null,
"created_at": "2025-04-25T07:25:48.801Z",
"updated_at": "2025-04-25T07:25:48.801Z",
"deleted_at": null,
},
"billing_address": {
"id": "caaddr_01JSNXD6W0V7RNZH63CPG26K5W",
"customer_id": null,
"company": "",
"first_name": "safasf",
"last_name": "asfaf",
"address_1": "asfasf",
"address_2": "",
"city": "asfasf",
"country_code": "dk",
"province": "",
"postal_code": "asfasf",
"phone": "",
"metadata": null,
"created_at": "2025-04-25T07:25:48.801Z",
"updated_at": "2025-04-25T07:25:48.801Z",
"deleted_at": null,
},
"shipping_methods": [
{
"id": "ordsm_01JSNXDH9B9DDRQXJT5J5AE5V1",
"name": "Standard Shipping",
"description": null,
"is_tax_inclusive": false,
"is_custom_amount": false,
"shipping_option_id": "so_01JSNXAQA64APG6BNHGCMCTN6V",
"data": {},
"metadata": null,
"raw_amount": {
"value": "10",
"precision": 20,
},
"created_at": new Date(),
"updated_at": new Date(),
"deleted_at": null,
"tax_lines": [],
"adjustments": [],
"amount": 10,
"order_id": "order_01JSNXDH9BPJWWKVW03B9E9KW8",
"detail": {
"id": "ordspmv_01JSNXDH9B5RAF4FH3M1HH3TEA",
"version": 1,
"order_id": "order_01JSNXDH9BPJWWKVW03B9E9KW8",
"return_id": null,
"exchange_id": null,
"claim_id": null,
"created_at": new Date(),
"updated_at": new Date(),
"deleted_at": null,
"shipping_method_id": "ordsm_01JSNXDH9B9DDRQXJT5J5AE5V1",
},
"subtotal": 10,
"total": 10,
"original_total": 10,
"discount_total": 0,
"discount_subtotal": 0,
"discount_tax_total": 0,
"tax_total": 0,
"original_tax_total": 0,
"raw_subtotal": {
"value": "10",
"precision": 20,
},
"raw_total": {
"value": "10",
"precision": 20,
},
"raw_original_total": {
"value": "10",
"precision": 20,
},
"raw_discount_total": {
"value": "0",
"precision": 20,
},
"raw_discount_subtotal": {
"value": "0",
"precision": 20,
},
"raw_discount_tax_total": {
"value": "0",
"precision": 20,
},
"raw_tax_total": {
"value": "0",
"precision": 20,
},
"raw_original_tax_total": {
"value": "0",
"precision": 20,
},
},
],
"customer": {
"id": "cus_01JSNXD6VQC1YH56E4TGC81NWX",
"company_name": null,
"first_name": null,
"last_name": null,
"email": "[email protected]",
"phone": null,
"has_account": false,
"metadata": null,
"created_by": null,
"created_at": "2025-04-25T07:25:48.791Z",
"updated_at": "2025-04-25T07:25:48.791Z",
"deleted_at": null,
},
},
}
// @ts-ignore
export default () => <OrderPlacedEmailComponent {...mockOrder} />
You create a mock order object that contains the order's details. Then, you export a default function that returns the OrderPlacedEmailComponent passing it the mock order.
The React Email CLI tool will use the function to render the email template.
Finally, add the following script to package.json:
{
"scripts": {
"dev:email": "email dev --dir ./src/modules/resend/emails"
}
}
This script will run the React Email CLI tool, passing it the directory where the email templates are located.
You can now test out the email template by running the following command:
npm run dev:email
This will start a development server at http://localhost:3000. If you open this URL, you can view your email templates in the browser.
You can make changes to the email template, and the server will automatically reload the changes.
Medusa has an event system that emits an event when a commerce operation is performed. You can then listen and handle that event in an asynchronous function called a subscriber.
So, to send a confirmation email when a customer places an order, which is a commerce operation that Medusa already implements, you don't need to extend or hack your way into Medusa's implementation as you would do with other commerce platforms.
Instead, you'll create a subscriber that listens to the order.placed event and sends an email when the event is emitted.
Learn more about Medusa's event system in this documentation.
</Note>To send the order confirmation email, you need to retrieve the order's details first, then use the Notification Module's service to send the email. To implement this flow, you'll create a workflow.
A workflow is a series of queries and actions, called steps, that complete a task. You construct a workflow like you construct a function, but it's a special function that allows you to track its executions' progress, define roll-back logic, and configure other advanced features. Then, you execute the workflow from other customizations, such as in a subscriber.
<Note>Learn more about workflows in this documentation
</Note>You'll start by implementing the step of the workflow that sends the notification. To do that, create the file src/workflows/steps/send-notification.ts with the following content:
import { Modules } from "@medusajs/framework/utils"
import { createStep, StepResponse } from "@medusajs/framework/workflows-sdk"
import { CreateNotificationDTO } from "@medusajs/framework/types"
export const sendNotificationStep = createStep(
"send-notification",
async (data: CreateNotificationDTO[], { container }) => {
const notificationModuleService = container.resolve(
Modules.NOTIFICATION
)
const notification = await notificationModuleService.createNotifications(data)
return new StepResponse(notification)
}
)
You define the sendNotificationStep using the createStep function that accepts two parameters:
The container property in the second parameter is an instance of the Medusa container, which is a registry of Framework and commerce tools, such as a module's service, that you can resolve to utilize their functionalities.
The Medusa container is accessible by all customizations, such as workflows and subscribers, except for modules. Each module has its own container with Framework tools like the Logger utility.
</Note>In the step function, you resolve the Notification Module's service, and use its createNotifications method, passing it the notification's data that the step receives as an input.
The step returns an instance of StepResponse, which must be returned by any step. It accepts as a parameter the data to return to the workflow that executed this step.
You'll now create the workflow that uses the sendNotificationStep to send the order confirmation email.
Create the file src/workflows/send-order-confirmation.ts with the following content:
export const workflowHighlights = [ ["13", "sendOrderConfirmationWorkflow", "Create the workflow that sends an order confirmation email."], ["16", "useQueryGraphStep", "Retrieve the order's details."], ["48", "sendNotificationStep", "Send the order confirmation email."] ]
import {
createWorkflow,
when,
WorkflowResponse,
} from "@medusajs/framework/workflows-sdk"
import { useQueryGraphStep } from "@medusajs/medusa/core-flows"
import { sendNotificationStep } from "./steps/send-notification"
type WorkflowInput = {
id: string
}
export const sendOrderConfirmationWorkflow = createWorkflow(
"send-order-confirmation",
({ id }: WorkflowInput) => {
const { data: orders } = useQueryGraphStep({
entity: "order",
fields: [
"id",
"display_id",
"email",
"currency_code",
"total",
"items.*",
"shipping_address.*",
"billing_address.*",
"shipping_methods.*",
"customer.*",
"total",
"subtotal",
"discount_total",
"shipping_total",
"tax_total",
"item_subtotal",
"item_total",
"item_tax_total",
],
filters: {
id,
},
options: {
throwIfKeyNotFound: true,
},
})
const notification = when({ orders }, (data) => !!data.orders[0].email)
.then(() => {
return sendNotificationStep([{
to: orders[0].email!,
channel: "email",
template: "order-placed",
data: {
order: orders[0],
},
}])
})
return new WorkflowResponse({
notification,
})
}
)
You create a workflow using createWorkflow from the Workflows SDK. It accepts the workflow's unique name as a first parameter.
It accepts as a second parameter a constructor function, which is the workflow's implementation. The workflow has the following steps:
useQueryGraphStep, which is a step implemented by Medusa that uses Query, a tool that allows you to retrieve data across modules. You use it to retrieve the order's details.when-then. If so, you send the notification using the sendNotificationStep you implemented earlier. You pass it an object with the following properties:
to: The address to send the email to. You pass the customer's email that is stored in the order.channel: The channel to send the notification through, which is email. Since you specified email in the Resend Module Provider's channel option, the Notification Module will delegate the sending to the Resend Module Provider's service.template: The email's template type. You retrieve the template content in the ResendNotificationProviderService's send method based on the template specified here.data: The data to pass to the email template, which is the order's details.when allows you to perform steps based on a condition during execution. Learn more in the Conditions in Workflows documentation.
You'll execute the workflow when you create the subscriber next.
Now that you have the workflow to send an order-confirmation email, you'll execute it in a subscriber that's executed whenever an order is placed.
You create a subscriber in a TypeScript or JavaScript file under the src/subscribers directory. So, create the file src/subscribers/order-placed.ts with the following content:
export const subscriberHighlights = [ ["7", "orderPlacedHandler", "The subscriber function"], ["8", "data", "The event's data payload, which has the order's ID."], ["9", "container", "The Medusa container."], ["11", "sendOrderConfirmationWorkflow", "Execute the workflow to send an email."], ["13", "input", "The input to pass to the workflow."], ["14", "id", "The ID of the order."], ["19", "config", "The subscriber's configurations."], ["20", "event", "The event that the subscriber is listening to."] ]
import type {
SubscriberArgs,
SubscriberConfig,
} from "@medusajs/framework"
import { sendOrderConfirmationWorkflow } from "../workflows/send-order-confirmation"
export default async function orderPlacedHandler({
event: { data },
container,
}: SubscriberArgs<{ id: string }>) {
await sendOrderConfirmationWorkflow(container)
.run({
input: {
id: data.id,
},
})
}
export const config: SubscriberConfig = {
event: "order.placed",
}
A subscriber file exports:
order.placed event.event property indicating the event the subscriber is listening to.The subscriber function accepts the event's details as a first parameter which has a data property that holds the data payload of the event. For example, Medusa emits the order.placed event with the order's ID in the data payload. The function also accepts as a second parameter the Medusa container.
In the function, you execute the sendOrderConfirmationWorkflow by invoking it, passing it the container, then using its run method. The run method accepts an object having an input property, which is the input to pass to the workflow. You pass the ID of the placed order as received in the event's data payload.
This subscriber now runs whenever an order is placed. You'll see this in action in the next section.
To test out the Resend integration, you'll place an order using the Next.js Starter Storefront that you installed as part of installing Medusa.
Start your Medusa application first:
npm run dev
Then, in the Next.js Starter Storefront's directory (apps/storefront), run the following command to start the storefront:
npm run dev
Then, open the storefront in your browser at http://localhost:8000 and:
2. Click on a product, select its options, and add it to the cart.
3. Click on Cart at the top right, then click Go to Cart.
4. On the cart's page, click on the "Go to checkout" button.
5. On the checkout page, when entering the shipping address, make sure to set the email to your Resend account's email if you didn't set up a custom domain.
6. After entering the shipping address, choose a delivery and payment methods, then click the Place Order button.
Once the order is placed, you'll find the following message logged in the Medusa application's terminal:
info: Processing order.placed which has 1 subscribers
This indicates that the order.placed event was emitted and its subscriber, which you added in the previous step, is executed.
If you check the inbox of the email address you specified in the shipping address, you'll find a new email with the order's details.
You've now integrated Medusa with Resend. You can add more templates for other emails, such as customer registration confirmation, user invites, and more. Check out the Events Reference for a list of all events that the Medusa application emits.
Find more email templates to use with the Resend Module Provider in the following guides:
If you're new to Medusa, check out the main documentation, where you'll get a more in-depth learning of all the concepts you've used in this guide and more.
To learn more about the commerce features that Medusa provides, check out Medusa's Commerce Modules.