Back to Medusa

{metadata.title}

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

2.14.229.1 KB
Original Source

import { Card, Prerequisites, Details, WorkflowDiagram } from "docs-ui" import { Github, PlaySolid } from "@medusajs/icons"

export const metadata = { title: Integrate Slack (Notification) with Medusa, }

{metadata.title}

In this tutorial, you'll learn how to integrate Slack with Medusa to receive notifications about created orders.

When you install a Medusa application, you get a fully-fledged commerce platform with a Framework for customization. Medusa's architecture facilitates integrating third-party services to customize Medusa's infrastructure for your business needs.

Medusa's Notification Module allows you to customize Medusa's infrastructure to send notifications using the third-party provider that fits your business needs, such as Slack.

In this tutorial, you'll integrate Slack with Medusa to receive notifications about created orders.

Summary

By following this tutorial, you'll learn how to:

  • Install and set up Medusa.
  • Integrate Slack with Medusa.
  • Handle Medusa's order.placed event to send notifications to Slack.

You can follow this tutorial 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/slack-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

First, you'll be asked for the project's name. Then, when prompted about installing the Next.js Starter Storefront, choose "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 credentials and submit the form. Afterwards, you can log in with the new user and explore the dashboard.

<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: Create Slack Module Provider

To integrate third-party services into Medusa, you create a custom module. A module is a reusable package with functionalities related to a single feature or domain.

Medusa's Notification Module provides an interface to send notifications in your Medusa application. It delegates the actual sending of notifications to the underlying provider, such as Slack.

In this step, you'll integrate Slack as a Notification Module Provider. Later, you'll use it to send a notification when an order is created.

<Note>

Refer to the Modules documentation to learn more about modules in Medusa.

</Note>

a. Install Axios

To send requests to Slack, you'll use the Axios library. So, run the following command to install it in your Medusa application:

bash
npm install axios

You'll use Axios in the module's service.

b. Create Module Directory

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

c. Create Slack Module's Service

A module has a service that contains its logic. For Notification Module Providers, the service implements the logic to send notifications with a third-party service.

To create the service of the Slack Notification Module Provider, create the file src/modules/slack/service.ts with the following content:

export const serviceHighlights = [ ["6", "webhook_url", "The Slack webhook URL to send notifications to."], ["7", "admin_url", "The URL of the Medusa Admin dashboard, used to add links in the notifications."], ["13", "identifier", "The unique identifier for the module provider."], ]

ts
import { 
  AbstractNotificationProviderService,
} from "@medusajs/framework/utils"

type Options = {
  webhook_url: string
  admin_url: string
}

type InjectedDependencies = {}

class SlackNotificationProviderService extends AbstractNotificationProviderService {
  static identifier = "slack"
  protected options: Options

  constructor(container: InjectedDependencies, options: Options) {
    super()
    this.options = options
  }
}

A Notification Module Provider's service must extend the AbstractNotificationProviderService class. You'll implement its methods in a bit.

The service must also have an identifier static property, which is a unique identifier for the module. This identifier is used when registering the module in the Medusa container.

The service's constructor receives two parameters:

  • container: The module's container that contains Framework resources available to the module. You don't need to access any resources for this provider.
  • options: Options that are passed to the module provider when it's registered in Medusa's configurations. You define the following options:
    • webhook_url: The Slack webhook URL to send notifications to.
    • admin_url: The URL of the Medusa Admin dashboard, which you'll use to add links in the notifications.
<Note>

You'll learn how to set these options when you add the module provider to Medusa's configurations.

</Note>

In the constructor, you set the options property to the passed options.

In the next sections, you'll implement the methods of the AbstractNotificationProviderService class.

<Note>

Refer to the Create Notification Module Provider guide for detailed information about the methods.

</Note>

d. Implement validateOptions Method

The validateOptions method is used to validate the options passed to the module provider. If the method throws an error, the Medusa application won't start.

So, add the validateOptions method to the SlackNotificationProviderService class:

ts
// other imports...
import { 
  MedusaError,
} from "@medusajs/framework/utils"

class SlackNotificationProviderService extends AbstractNotificationProviderService {
  // ...
  static validateOptions(options: Record<any, any>): void | never {
    if (!options.webhook_url) {
      throw new MedusaError(
        MedusaError.Types.INVALID_ARGUMENT,
        "Webhook URL is required"
      )
    }
    if (!options.admin_url) {
      throw new MedusaError(
        MedusaError.Types.INVALID_ARGUMENT,
        "Admin URL is required"
      )
    }
  }
}

The validateOptions method receives the options passed to the module provider as a parameter.

In the method, you throw an error if any of the options are not set.

e. Implement send Method

When the Medusa application needs to send a notification through a channel (such as Slack), it calls the send method of the channel's module provider.

You'll first add helper methods that you'll use in the send method. Then, you'll implement the send method itself.

getDisplayAmount Method

The first method you'll add is a method to format amounts for displaying them in notifications. So, add the getDisplayAmount method to the SlackNotificationProviderService class:

ts
class SlackNotificationProviderService extends AbstractNotificationProviderService {
  // ...
  private async getDisplayAmount(amount: number, currencyCode: string) {
    return Intl.NumberFormat("en-US", {
      style: "currency",
      currency: currencyCode,
    }).format(amount)
  }
}

The getDisplayAmount method receives an amount and a currency code and returns the formatted amount as a string.

sendOrderNotification Method

Next, you'll add a method to format a Slack message for created orders. So, add the sendOrderNotification method to the SlackNotificationProviderService class:

export const sendOrderNotificationHighlights = [ ["12", "order", "The order to send a notification for."], ["20", "blocks", "Format the slack message as blocks."], ["96", "axios", "Send the formatted message to Slack using Axios."], ]

ts
// other imports...
import { 
  OrderDTO, 
  ProviderSendNotificationDTO, 
  ProviderSendNotificationResultsDTO,
} from "@medusajs/framework/types"
import axios from "axios"

class SlackNotificationProviderService extends AbstractNotificationProviderService {
  // ...
  async sendOrderNotification(notification: ProviderSendNotificationDTO) {
    const order = notification.data?.order as OrderDTO
    if (!order) {
      throw new MedusaError(
        MedusaError.Types.NOT_FOUND,
        "Order not found"
      )
    }
    const shippingAddress = order.shipping_address
    const blocks: Record<string, unknown>[] = [
      {
        type: "section",
        text: {
          type: "mrkdwn",
          text: `Order *<${this.options.admin_url}/orders/${order.id}|#${order.display_id}>* has been created.`,
        },
      },
    ]

    if (shippingAddress) {
      blocks.push({
        type: "section",
        text: {
          type: "mrkdwn",
          text: `*Customer*\n${shippingAddress.first_name} ${
            shippingAddress.last_name
          }\n${order.email}\n*Destination*\n${
            shippingAddress.address_1
          }\n${
            shippingAddress.city
          }, ${(shippingAddress.country_code as string).toUpperCase()}`,
        },
      })
    }

    blocks.push(
      {
        type: "section",
        text: {
          type: "mrkdwn",
          text: `*Subtotal*\t${await this.getDisplayAmount(order.subtotal as number, order.currency_code)}\n*Shipping*\t${
            await this.getDisplayAmount(order.shipping_total as number, order.currency_code)
          }\n*Discount Total*\t${await this.getDisplayAmount(
            order.discount_total as number,
            order.currency_code
          )}\n*Tax*\t${await this.getDisplayAmount(order.tax_total as number, order.currency_code)}\n*Total*\t${
            await this.getDisplayAmount(order.total as number, order.currency_code)
          }`,
        },
      },
      {
        type: "divider",
      }
    )

    await Promise.all(
      order.items?.map(async (item) => {
        const line: Record<string, unknown> = {
          type: "section",
          text: {
            type: "mrkdwn",
            text: `*${item.title}*\n${item.quantity} x ${await this.getDisplayAmount(
              item.unit_price as number,
              order.currency_code
            )}`,
          },
        }
  
        if (item.thumbnail) {
          const url = item.thumbnail
  
          line.accessory = {
            type: "image",
            alt_text: "Item",
            image_url: url,
          }
        }
  
        blocks.push(line)
        blocks.push({
          type: "divider",
        })
      }) || []
    )

    await axios.post(this.options.webhook_url, {
      text: `Order ${order.display_id} was created`,
      blocks,
    })

    return {
      id: order.id,
    }
  }
}

The sendOrderNotification method receives a ProviderSendNotificationDTO object, which is the object passed to the send method that you'll implement next. The object has a data property that contains the order data.

In the method, you format the Slack message to show the order's ID, customer information, shipping address, order items, and total amounts. You also add a link to the order's details page in the Medusa Admin dashboard.

Finally, you send the formatted message to Slack using the axios.post method with the configured webhook URL.

send Method

You can now implement the send method, which is the method that Medusa calls to send notifications using the provider. Add the send method to the SlackNotificationProviderService class:

ts
class SlackNotificationProviderService extends AbstractNotificationProviderService {
  // ...
  async send(
    notification: ProviderSendNotificationDTO
  ): Promise<ProviderSendNotificationResultsDTO> {
    const { template } = notification

    switch (template) {
      case "order-created":
        return this.sendOrderNotification(notification)
      default:
        throw new MedusaError(
          MedusaError.Types.NOT_FOUND,
          `Template ${template} not supported`
        )
    }
  }
}

The send method receives a ProviderSendNotificationDTO object, which contains the notification data and the template to use for sending the notification.

<Note>

The method receives other parameters, which you can find in the Create Notification Module Provider guide.

</Note>

In the method, you check the template property of the notification object. If it's order-created, you call the sendOrderNotification method to send the notification. Otherwise, you throw an error indicating that the template is not supported.

f. Export Module Definition

You've now finished implementing the necessary methods for the Slack Notification Module Provider.

The final piece to a module is its definition, which you export in an index.ts file at the module's root directory. This definition tells Medusa the module's details, including its service.

To create the module's definition, create the file src/modules/slack/index.ts with the following content:

ts
import SlackNotificationProvider from "./service"
import { 
  ModuleProvider, 
  Modules,
} from "@medusajs/framework/utils"

export default ModuleProvider(Modules.NOTIFICATION, {
  services: [SlackNotificationProvider],
})

You use ModuleProvider from the Modules SDK to create the module provider's definition. It accepts two parameters:

  1. The name of the module that this provider belongs to, which is Modules.NOTIFICATION in this case.
  2. An object with a required property services indicating the Module Provider's services.

g. Add Module Provider to Medusa's Configurations

Once you finish building the module, add it to Medusa's configurations to start using it.

In medusa-config.ts, add a modules property:

export const configHighlights = [ ["10", "id", "The ID of the module provider, used to register it."], ["12", "channels", "The channels that this provider is used to send notifications."], ["13", "webhook_url", "The Slack webhook URL to send notifications to."], ["14", "admin_url", "The URL of the Medusa Admin dashboard, used to add links in the notifications."] ]

ts
module.exports = defineConfig({
  // ...
  modules: [
    {
      resolve: "@medusajs/medusa/notification",
      options: {
        providers: [
          {
            resolve: "./src/modules/slack",
            id: "slack",
            options: {
              channels: ["slack"],
              webhook_url: process.env.SLACK_WEBHOOK_URL,
              admin_url: process.env.SLACK_ADMIN_URL,
            },
          },
        ],
      },
    },
  ],
})

To pass a Notification Module Provider to the Notification Module, you add the modules property to the Medusa configuration and pass the Notification Module in its value.

The Notification Module accepts a providers option, which is an array of Notification Module Providers to register.

To register the Slack Notification Module Provider, you add an object to the providers array with the following properties:

  • resolve: The NPM package or path to the module provider. In this case, it's the path to the src/modules/slack directory.
  • id: The ID of the module provider. The Notification Module Provider is then registered with the ID np_{identifier}_{id}, where:
    • {identifier}: The identifier static property defined in the Module Provider's service, which is slack in this case.
    • {id}: The ID set in this configuration, which is also slack in this case.
  • options: The options to pass to the module provider. These are the options you defined in the Options interface of the module provider's service.
    • You must also set a channel option that indicates the channel this provider is used to send notifications.

h. Set Options as Environment Variables

Next, you'll set the options you passed to the Slack Notification Module Provider as environment variables.

To set the webhook URL, you need to create a Slack application and configure a webhook URL. To do that:

  1. Go to your Slack Apps page.
  2. Click the "Create New App" button.

  1. In the pop-up, choose "From scratch".
  2. Enter the app's name and select the workspace to install it in.
  3. Once you're done, click the "Create App" button.

  1. In the app's settings, go to the "Incoming Webhooks" section.
  2. Enable the "Activate Incoming Webhooks" toggle.

  1. In the "Webhook URLs for Your Workspace" section, click the "Add New Webhook" button.

  1. Select the channel or conversation to send notifications to and click the "Allow" button.
  2. Copy the generated webhook URL.

Then, in the .env file of your Medusa application, set the SLACK_WEBHOOK_URL and SLACK_ADMIN_URL environment variables:

shell
SLACK_WEBHOOK_URL=https://hooks.slack.com/...
SLACK_ADMIN_URL=http://localhost:9000/app

In development, the Medusa Admin dashboard is running on http://localhost:9000/app, so you can use that URL for the SLACK_ADMIN_URL environment variable.

You'll test out the integration when you handle the order.placed event in the next step.


Step 3: Handle the order.placed Event

Now that you've integrated Slack with Medusa, you can send notifications to Slack at any point, including when an order is created.

To send a notification to Slack when an order is created, you will:

  • Implement the order details retrieval and notification sending with Slack in a workflow. A workflow is a series of actions, called steps, that complete a task with rollback and retry mechanisms.
  • Listen to the order.placed event in a subscriber. A subscriber is an asynchronous function that listens to events to perform actions, such as execute a workflow, when the event is emitted.

a. Create the Workflow

The workflow to send notifications to Slack will have the following steps:

<WorkflowDiagram workflow={{ name: "orderPlacedNotificationWorkflow", steps: [ { type: "step", name: "useQueryGraphStep", description: "Retrieve the order details.", link: "/references/helper-steps/useQueryGraphStep", depth: 1 }, { type: "step", name: "sendNotificationsStep", description: "Send a notification with a configured provider.", link: "/references/medusa-workflows/steps/sendNotificationsStep", depth: 1 } ] }} hideLegend />

Medusa provides both steps in its @medusajs/medusa/core-flows package.

So, to create the workflow, create the file src/workflows/order-placed-notification.ts with the following content:

export const orderPlacedNotificationWorkflowHighlights = [ ["13", "useQueryGraphStep", "Retrieve the order details using the Query Graph."], ["34", "sendNotificationsStep", "Send a notification to Slack with the order details."] ]

ts
import { 
  createWorkflow, 
} from "@medusajs/framework/workflows-sdk"
import { sendNotificationsStep, useQueryGraphStep } from "@medusajs/medusa/core-flows"

type WorkflowInput = {
  id: string
}

export const orderPlacedNotificationWorkflow = createWorkflow(
  "order-placed-notification",
  ({ id }: WorkflowInput) => {
    const { data: orders } = useQueryGraphStep({
      entity: "order",
      fields: [
        "id",
        "display_id", 
        "email",
        "shipping_address.*",
        "subtotal",
        "shipping_total",
        "currency_code", 
        "discount_total",
        "tax_total",
        "total",
        "items.*",
        "original_total",
      ],
      filters: {
        id,
      },
    })

    sendNotificationsStep([{
      to: "slack-channel", // This will be configured in the Slack app
      channel: "slack",
      template: "order-created",
      data: {
        order: orders[0],
      },
    }])
  }
)

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 function can accept input, which in this case is an object holding the ID of the order placed.

In the workflow's constructor function, you:

  • Retrieve the order's details using the useQueryGraphStep. This step uses Medusa's Query tool to retrieve data across modules. You pass it the order ID to retrieve.
  • Send a notification using the sendNotificationsStep. You pass the step an array of notifications to send, and each notification is an object with the following properties:
    • to: The channel to send the notification to. Since you configure this in the Slack app, you just set it to slack-channel.
    • channel: The channel to use for sending the notification. By setting it to slack, the Notification Module will use the Slack Notification Module Provider to send the notification.
    • template: The template to use for sending the notification, which is order-created in this case.
    • data: The data payload to pass with the notification, which contains the order details.

You now have a workflow that retrieves the order details and sends a notification to Slack when an order is placed.

<Note>

Refer to the Workflows documentation to learn more about workflows and steps.

</Note>

b. Create the Subscriber

To execute the workflow when an order is placed, you need to create a subscriber that listens to the order.placed event and executes the workflow.

To create the subscriber, create the file src/subscribers/order-placed.ts with the following content:

ts
import { SubscriberArgs, SubscriberConfig } from "@medusajs/framework"
import { 
  orderPlacedNotificationWorkflow,
} from "../workflows/order-placed-notification"

export default async function orderPlacedHandler({
  event: { data },
  container,
}: SubscriberArgs<{ id: string }>) {
  await orderPlacedNotificationWorkflow(container)
    .run({
      input: data,
    })
}

export const config: SubscriberConfig = {
  event: "order.placed",
}

A subscriber file must export:

  1. An asynchronous function, which is the subscriber that is executed when the event is emitted.
  2. A configuration object that holds the name of the event that the subscriber listens to, which is order.placed in this case.

The subscriber function receives an object as a parameter that has the following properties:

  • event: An object that holds the event's data payload. The payload of the order.placed event is the ID of the order placed.
  • container: The Medusa container to access the Framework and commerce tools.

In the subscriber function, you execute the orderPlacedNotificationWorkflow by invoking it, passing the Medusa container as a parameter. Then, you chain a run method, passing it the order ID from the event's data payload as input.

<Note>

Refer to the Events and Subscribers documentation to learn more about creating subscribers.

</Note>

Test it Out

You'll now test out the integration by placing an order using the Next.js Starter Storefront.

<Note title="Reminder" forceMultiline>

The Next.js Starter Storefront is available in the apps/storefront directory of your project:

bash
cd apps/storefront
</Note>

First, start the Medusa application by running the following command in the apps/backend directory:

bash
npm run dev

Then, start the Next.js Starter Storefront by running the following command in the storefront's directory:

bash
npm run dev

Next, open the Next.js Starter Storefront in your browser at http://localhost:8000. Add a product to the cart, proceed to checkout, and place an order.

Afterwards, you'll receive a notification in the Slack channel you configured in the Slack app. The notification will contain the order details.

You can also click on the order's ID in the notification to open the order's details page in the Medusa Admin dashboard.


Next Steps

You've now integrated Slack with Medusa to receive notifications about created orders. You can expand on this integration to send other types of notifications, such as order updates. Refer to the Events Reference for a list of events you can listen to in your subscribers.

You can also customize the notifications to include more information or change the format of the messages. Refer to Slack's documentation for more information on how to format messages in Slack.

If you're new to Medusa, check out the main documentation, where you'll get a more in-depth understanding 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.

Troubleshooting

If you encounter issues during your development, check out the troubleshooting guides.

Getting Help

If you encounter issues not covered in the troubleshooting guides:

  1. Visit the Medusa GitHub repository to report issues or ask questions.
  2. Join the Medusa Discord community for real-time support from community members.
  3. Contact the sales team to get help from the Medusa team.