Back to Medusa

Page

www/apps/resources/app/integrations/guides/resend/page.mdx

2.14.254.3 KB
Original Source

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" } ] } }

{metadata.title}

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:

  • Install and set up Medusa.
  • Integrate Resend into Medusa for sending emails.
  • Build a flow to send an email with Resend when a customer places an order.

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} />


Step 1: Install a Medusa Application

<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:

bash
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.

<Note title="Ran into Errors?">

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.

</Note>

Step 2: Prepare Resend Account

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:

  1. Go to Domains from the sidebar.
  2. Click on Add Domain.

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.


Step 3: Install Resend Dependencies

In this step, you'll install two packages useful for your Resend integration:

  1. resend, which is the Resend SDK:
bash
npm install resend

2. react-email, which is a package created by Resend to create email templates with React:

bash
npm install @react-email/components -E

You'll use these packages in the next steps.


Step 4: Create Resend Module Provider

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>

Create Module Directory

A module is created under the src/modules directory of your Medusa application. So, create the directory src/modules/resend.

Create Service

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."] ]

ts
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:

ts
// ...

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:

  1. Dependencies resolved from the Module's container, which is the module's local registry that the Medusa application adds Framework tools to. In this service, you resolve the Logger utility from the module's container.
  2. The module's options that are passed to the module in Medusa's configuration as you'll see in a later section.

Using the API key passed in the module's options, you initialize the Resend client. You also set the options and logger properties.

Validate Options Method

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:

ts
// 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.

Implement Template Methods

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."] ]

ts
// 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.

Implement Send Method

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."] ]

ts
// 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.

Export Module Definition

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:

ts
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.

Add Module to Configurations

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:

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:

shell
[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:
    • Go to API Keys in the sidebar.
    • Click on the Create API Key button.

- 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.


Step 5: Add Order Confirmation Template

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."] ]

tsx
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:

ts
// 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.

Test Email Out

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:

bash
npm install -D react-email

Then, in src/modules/resend/emails/order-placed.tsx, add the following at the end of the file:

tsx
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:

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:

bash
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.


Step 6: Send Email when Order is Placed

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.

<Note>

Learn more about Medusa's event system in this documentation.

</Note>

Send Order Confirmation Email Workflow

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>

Send Notification Step

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:

ts
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:

  • A string indicating the step's unique name.
  • The step's function definition as a second parameter. It accepts the step's input as a first parameter, and an object of options as a second.

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.

<Note title="Tip">

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.

Workflow Implementation

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."] ]

ts
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:

  1. 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.
  2. Ensure that the order has an email address by using 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.
<Note title="Tip">

when allows you to perform steps based on a condition during execution. Learn more in the Conditions in Workflows documentation.

</Note>

You'll execute the workflow when you create the subscriber next.

Add the Order Placed Subscriber

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."] ]

ts
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:

  • An asynchronous function that's executed whenever the associated event is emitted, which is the order.placed event.
  • A configuration object with an 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.


Test it Out: Place an Order

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:

bash
npm run dev

Then, in the Next.js Starter Storefront's directory (apps/storefront), run the following command to start the storefront:

bash
npm run dev

Then, open the storefront in your browser at http://localhost:8000 and:

  1. Go to Menu -> Store.

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:

bash
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.


Next Steps

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.

More Resend Email Templates

Find more email templates to use with the Resend Module Provider in the following guides:

Learn More About Medusa

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.