

# Introduction

Medusa is a digital commerce platform with a built-in Framework for customization.

Medusa ships with three main tools:

1. A suite of [Commerce Modules](https://docs.medusajs.com/resources/commerce-modules/index.html.md) with core commerce functionalities, such as tracking inventory, calculating cart totals, accepting payments, managing orders, and much more.
2. A [Framework](https://docs.medusajs.com/learn/fundamentals/framework/index.html.md) for building custom functionalities specific to your business, product, or industry. This includes tools for introducing custom API endpoints, business logic, and data models; building workflows and automations; and integrating with third-party services.
3. A customizable admin dashboard for merchants to configure and operate their store.

When you install Medusa, you get a fully fledged commerce platform with all the features you need to get off the ground. However, unlike other platforms, Medusa is built with customization in mind. You don't need to build hacky workarounds that are difficult to maintain and scale. Your efforts go into building features that brings your business's vision to life.

***

## Who should use Medusa

Medusa is for businesses and teams looking for a digital commerce platform with the tools to implement unique requirements that other platforms aren't built to support.

Businesses of all sizes can use Medusa, from small start ups to large enterprises. Also, technical teams of all sizes can build with Medusa; all it takes is a developer to manage and deploy Medusa projects.

Below are some stories from companies that use Medusa:

- [Use Case: D2C](https://medusajs.com/blog/matt-sleeps/): How Matt Sleeps built a unique D2C experience with Medusa
- [Use Case: OMS](https://medusajs.com/blog/makro-pro/): How Makro Pro Built an OMS with Medusa
- [Use Case: Marketplace](https://medusajs.com/blog/foraged/): How Foraged built a custom marketplace with Medusa
- [Use Case: POS](https://medusajs.com/blog/tekla-pos/): How Tekla built a global webshop and a POS system with Medusa
- [Use Case: B2B](https://medusajs.com/blog/visionary/): How Visionary built B2B commerce with Medusa
- [Use Case: Platform](https://medusajs.com/blog/catalog/): How Catalog built a B2B platform for SMBs with Medusa

***

## Who is this documentation for

This documentation introduces you to Medusa's concepts and how they help you build your business use case. The documentation is structured to gradually introduce Medusa's concepts, with easy-to-follow examples along the way.

By following this documentation, you'll be able to create custom commerce experiences that would otherwise take large engineering teams months to build.

### How to use the documentation

This documentation is split into the following sections:

|Section|Description|
|---|---|---|
|Main Documentation|The documentation you're currently reading. It's recommended to follow the chapters in this documentation to understand the core concepts of Medusa and how to use them before jumping into the other sections.|
|Product|Documentation for the |
|Build|Recipes|
|Tools|Guides on how to setup and use Medusa's CLI tools, |
|API Routes References|References of the |
|References|Useful during your development with Medusa to learn about different APIs and how to use them. Its references include the |
|User Guide|Guides that introduce merchants and store managers to the Medusa Admin dashboard and helps them understand how to use the dashboard to manage their store.|

To get started, check out the [Installation chapter](https://docs.medusajs.com/learn/installation/index.html.md).

### Using with LLM Editors

This documentation provides an [llms-full.txt](https://docs.medusajs.com/llms-full.txt/index.html.md) file to support LLM editors. To access the documentation directly from your editor and benefit from code generation, add [https://docs.medusajs.com/llms-full.txt](https://docs.medusajs.com/llms-full.txt/index.html.md) as a custom documentation source in your LLM editor, such as [Cursor](https://docs.cursor.com/context/@-symbols/@-docs).

***

## Useful Links

- Need Help? Refer to our [GitHub repository](https://github.com/medusajs/medusa) for [issues](https://github.com/medusajs/medusa/issues) and [discussions](https://github.com/medusajs/medusa/discussions).
- [Join the community on Discord](https://discord.gg/medusajs).
- Have questions or need more support? Contact our [sales team](https://medusajs.com/contact/).
- Facing issues in your development? Refer to our [troubleshooting guides](https://docs.medusajs.com/resources/troubleshooting/index.html.md).


# Build Medusa Application

In this chapter, you'll learn how to create a production build of your Medusa application to be deployed to a hosting provider.

Next chapters explain how to deploy the Medusa application.

## build Command

The Medusa CLI tool has a [build](https://docs.medusajs.com/resources/medusa-cli/commands/build/index.html.md) command which creates a standalone build of the Medusa application that:

- Doesn't rely on the source TypeScript files.
- Can be copied to a production server reliably.

So, to create the production build, run the following command in the root of your Medusa application:

```bash
npx medusa build
```

***

## Build Output

The `build` command creates a `.medusa` directory in the root of your project that contains your build assets. Don't commit this directory to your repository.

The `.medusa` directory contains the following directories:

- `.medusa/server`: Contains the production build of your Medusa application.
- `.medusa/server/public/admin`: Contains the production build of the admin dashboard.

### Separate Admin Build

The `build` command accepts a `--admin-only` option that outputs the admin to the `.medusa/admin` directory. This is useful when deploying the admin dashboard separately, such as on Vercel:

```bash
npx medusa build --admin-only
```

***

## Start Built Medusa Application

To start the Medusa application after running the `build` command:

- Change to the `.medusa/server` directory and install the dependencies:

```bash npm2yarn
cd .medusa/server && npm install
```

- When running the application locally, make sure to copy the `.env` file from the root project's directory. In production, use system environment variables instead.

```bash title=".medusa/server"
cp ../../.env .env.production
```

When `NODE_ENV=production`, the Medusa application loads the environment variables from `.env.production`. Learn more about environment variables in [this guide](https://docs.medusajs.com/learn/fundamentals/environment-variables/index.html.md).

- Set `NODE_ENV` to `production` in the system environment variable, then start the Medusa application from `.medusa/server`:

```bash npm2yarn title=".medusa/server"
export NODE_ENV=production
npm run start
```

***

## Deploying Production Build

The next chapter covers how you generally deploy the production build.

You can also refer to the [deployment how-to guides](https://docs.medusajs.com/resources/deployment/index.html.md) for platform-specific how-to guides.


# Install Medusa

In this chapter, you'll learn how to install and run a Medusa application.

## Create Medusa Application

A Medusa application is made up of a Node.js server and an admin dashboard. You can optionally install the [Next.js Starter Storefront](https://docs.medusajs.com/resources/nextjs-starter/index.html.md) separately either while installing the Medusa application or at a later point.

### Prerequisites

- [Node.js v20+](https://nodejs.org/en/download)
- [Git CLI tool](https://git-scm.com/downloads)
- [PostgreSQL](https://www.postgresql.org/download/)

To create a Medusa application, use the `create-medusa-app` command:

```bash
npx create-medusa-app@latest my-medusa-store
```

Where `my-medusa-store` is the name of the project's directory and PostgreSQL database created for the project. When you run the command, you'll be asked whether you want to install the Next.js Starter Storefront.

After answering the prompts, the command installs the Medusa application in a directory with your project name, and sets up a PostgreSQL database that the application connects to.

If you chose to install the storefront with the Medusa application, the storefront is installed in a separate directory named `{project-name}-storefront`.

![Diagram showcasing an overview of the installation directories](https://res.cloudinary.com/dza7lstvk/image/upload/v1745856132/Medusa%20Resources/installation-dirs_x8jux4.jpg)

### Successful Installation Result

Once the installation finishes successfully, the Medusa application will run at `http://localhost:9000`.

The Medusa Admin dashboard also runs at `http://localhost:9000/app`. The installation process opens the Medusa Admin dashboard in your default browser to create a user. You can later log in with that user.

If you also installed the Next.js Starter Storefront, it'll be running at `http://localhost:8000`.

You can stop the servers for the Medusa application and Next.js Starter Storefront by exiting the installation command. To run the server for the Medusa application again, refer to [this section](#run-medusa-application-in-development).

![Diagram showcasing the server and applications running after successful installation](https://res.cloudinary.com/dza7lstvk/image/upload/v1745856706/Medusa%20Resources/success-overview_bj4pbt.jpg)

### Troubleshooting Installation Errors

If you ran into an error during your installation, refer to the following troubleshooting guides for help:

1. [create-medusa-app troubleshooting guides](https://docs.medusajs.com/resources/troubleshooting/create-medusa-app-errors/index.html.md).
2. [CORS errors](https://docs.medusajs.com/resources/troubleshooting/cors-errors/index.html.md).
3. [All troubleshooting guides](https://docs.medusajs.com/resources/troubleshooting/index.html.md).

If you can't find your error reported anywhere, please open a [GitHub issue](https://github.com/medusajs/medusa/issues/new/choose).

***

## Run Medusa Application in Development

To run the Medusa application in development, change to your application's directory and run the following command:

```bash npm2yarn
npm run dev
```

This runs your Medusa server at `http://localhost:9000`, and the Medusa Admin dashboard `http://localhost:9000/app`.

![Diagram showcasing the server and application running when you start the Medusa application](https://res.cloudinary.com/dza7lstvk/image/upload/v1745856966/Medusa%20Resources/start-overview_aetplx.jpg)

For details on starting and configuring the Next.js Starter Storefront, refer to [this documentation](https://docs.medusajs.com/resources/nextjs-starter/index.html.md).

The application will restart if you make any changes to code under the `src` directory, except for admin customizations which are hot reloaded, providing you with a seamless developer experience without having to refresh your browser to see the changes.

***

## Create Medusa Admin User

Aside from creating an admin user in the admin dashboard, you can create a user with Medusa's CLI tool.

Run the following command in your Medusa application's directory to create a new admin user:

```bash
npx medusa user -e admin@medusajs.com -p supersecret
```

Replace `admin@medusajs.com` and `supersecret` with the user's email and password respectively.

You can then use the user's credentials to log into the Medusa Admin application.

***

## Project Files

Your Medusa application's project will have the following files and directories:

![A diagram of the directories overview](https://res.cloudinary.com/dza7lstvk/image/upload/v1732803813/Medusa%20Book/medusa-dir-overview_v7ks0j.jpg)

### src

This directory is the central place for your custom development. It includes the following sub-directories:

- `admin`: Holds your admin dashboard's custom [widgets](https://docs.medusajs.com/learn/fundamentals/admin/widgets/index.html.md) and [UI routes](https://docs.medusajs.com/learn/fundamentals/admin/ui-routes/index.html.md).
- `api`: Holds your custom [API routes](https://docs.medusajs.com/learn/fundamentals/api-routes/index.html.md) that are added as endpoints in your Medusa application.
- `jobs`: Holds your [scheduled jobs](https://docs.medusajs.com/learn/fundamentals/scheduled-jobs/index.html.md) that run at a specified interval during your Medusa application's runtime.
- `links`: Holds your [module links](https://docs.medusajs.com/learn/fundamentals/module-links/index.html.md) that build associations between data models of different modules.
- `modules`: Holds your custom [modules](https://docs.medusajs.com/learn/fundamentals/modules/index.html.md) that implement custom business logic.
- `scripts`: Holds your custom [scripts](https://docs.medusajs.com/learn/fundamentals/custom-cli-scripts/index.html.md) to be executed using Medusa's CLI tool.
- `subscribers`: Holds your [event listeners](https://docs.medusajs.com/learn/fundamentals/events-and-subscribers/index.html.md) that are executed asynchronously whenever an event is emitted.
- `workflows`: Holds your custom [flows](https://docs.medusajs.com/learn/fundamentals/workflows/index.html.md) that can be executed from anywhere in your application.

### medusa-config.ts

This file holds your [Medusa configurations](https://docs.medusajs.com/learn/configurations/medusa-config/index.html.md), such as your PostgreSQL database configurations.

### .medusa

The `.medusa` directory holds types and other files that are generated by Medusa when you run the `build` command. Don't modify any files or commit them to your repository.

***

## Configure Medusa Application

By default, your Medusa application is equipped with the basic configuration to start your development.

If you run into issues with configurations, such as CORS configurations, or need to make changes to the default configuration, refer to [this guide on all available configurations](https://docs.medusajs.com/learn/configurations/medusa-config/index.html.md).

***

## Update Medusa Application

Refer to [this documentation](https://docs.medusajs.com/learn/update/index.html.md) to learn how to update your Medusa project.

***

## Next Steps

In the next chapters, you'll learn about the architecture of your Medusa application, then learn how to customize your application to build custom features.


# Medusa Deployment Overview

In this chapter, you’ll learn the general approach to deploying the Medusa application.

## Medusa Project Components

A standard Medusa project is made up of:

- Medusa application: The Medusa server and the Medusa Admin.
- One or more storefronts

![Diagram showcasing the connection between the three deployed components](https://res.cloudinary.com/dza7lstvk/image/upload/v1708600807/Medusa%20Book/deployment-options_ceuuvo.jpg)

You deploy the Medusa application, with the server and admin, separately from the storefront.

***

## Deploying the Medusa Application

You must deploy the Medusa application before the storefront, as it connects to the server and won’t work without a deployed Medusa server URL.

The Medusa application must be deployed to a hosting provider supporting Node.js server deployments, such as Railway, DigitalOcean, AWS, Heroku, etc…

![Diagram showcasing how the Medusa server and its associated services would be deployed](https://res.cloudinary.com/dza7lstvk/image/upload/v1708600972/Medusa%20Book/backend_deployment_pgexo3.jpg)

Your server connects to a PostgreSQL database, Redis, and other services relevant for your setup. Most hosting providers support deploying and managing these databases along with your Medusa server (such as Railway and DigitalOcean).

When you deploy your Medusa application, you also deploy the Medusa Admin. For optimal experience, your hosting provider and plan must offer at least 2GB of RAM.

### Deploy Server and Worker Instances

By default, Medusa runs all processes in a single instance. This includes the server that handles incoming requests and the worker that processes background tasks. While this works for development, it’s not optimal for production environments as many background tasks can be long-running or resource-heavy.

Instead, you should deploy two instances:

- A server instance, which handles incoming requests to the application’s API routes.
- A worker instance, which processes background tasks, including scheduled jobs and subscribers.

You don’t need to set up different projects for each instance. Instead, you can configure the Medusa application to run in different modes based on environment variables.

Learn more about worker modes and how to configure them in the [Worker Mode chapter](https://docs.medusajs.com/learn/production/worker-mode/index.html.md).

### How to Deploy Medusa?

Medusa Cloud is our managed services offering that makes deploying and operating Medusa applications possible without having to worry about configuring, scaling, and maintaining infrastructure. Medusa Cloud hosts your server, Admin dashboard, database, and Redis instance.

With Medusa Cloud, you maintain full customization control as you deploy your own modules and customizations directly from GitHub:

- Push to deploy.
- Multiple testing environments.
- Preview environments for new PRs.
- Test on production-like data.

[Sign up and learn more about Medusa Cloud](https://medusajs.com/pricing)

To self-host Medusa, the [next chapter](https://docs.medusajs.com/learn/deployment/general/index.html.md) explains the general steps to deploy your Medusa application. Refer to [this reference](https://docs.medusajs.com/resources/deployment/index.html.md) to find how-to deployment guides for general and specific hosting providers.

***

## Deploying the Storefront

The storefront is deployed separately from the Medusa application, and the hosting options depend on the tools and frameworks you use to create the storefront.

If you’re using the Next.js Starter storefront, you may deploy the storefront to any hosting provider that supports frontend frameworks, such as Vercel.

Per Vercel’s [license and plans](https://vercel.com/pricing), their free plan can only be used for personal, non-commercial projects. So, you can deploy the storefront on the free plan for development purposes, but for commercial projects, you must update your Vercel plan.

Refer to [this reference](https://docs.medusajs.com/resources/deployment/index.html.md) to find how-to deployment guides for specific hosting providers.


# Storefront Development

The Medusa application is made up of a Node.js server and an admin dashboard. Storefronts are installed, built, and hosted separately from the Medusa application, giving you the flexibility to choose the frontend tech stack that you and your team are proficient in, and implement unique design systems and user experience.

You can build your storefront from scratch with your preferred tech stack, or start with our Next.js Starter storefront. The Next.js Starter storefront provides rich commerce features and a sleek design. Developers and businesses can use it as-is or build on top of it to tailor it for the business's unique use case, design, and customer experience.

- [Install Next.js Starter Storefront](https://docs.medusajs.com/resources/nextjs-starter/index.html.md)
- [Build Custom Storefront](https://docs.medusajs.com/resources/storefront-development/index.html.md)

***

## Passing a Publishable API Key in Storefront Requests

When sending a request to an API route starting with `/store`, you must include a publishable API key in the header of your request.

A publishable API key sets the scope of your request to one or more sales channels.

Then, when you retrieve products, only products of those sales channels are retrieved. This also ensures you retrieve correct inventory data, and associate created orders with the scoped sales channel.

Learn more about passing the publishable API key in [this storefront development guide](https://docs.medusajs.com/resources/storefront-development/publishable-api-keys/index.html.md).


# Updating Medusa

In this chapter, you'll learn about updating your Medusa application and packages.

Medusa's current version is v{config.version.number}. {releaseNoteText}

## Medusa Versioning

When Medusa puts out a new release, all packages are updated to the same version. This ensures that all packages are compatible with each other, and makes it easier for you to switch between versions.

This doesn't apply to the design-system packages, including `@medusajs/ui`, `@medusajs/ui-presets`, and `@medusajs/ui-icons`. These packages are versioned independently. However, you don't need to install and manage them separately in your Medusa application, as they are included in the `@medusajs/admin-sdk`. If you're using them in a standalone project, such as a storefront or custom admin dashboard, refer to [this section in the Medusa UI documentation](https://docs.medusajs.com/ui/installation/standalone-project#updating-ui-packages/index.html.md) for update instructions.

Medusa updates the version number `major.minor.patch` according to the following rules:

- **patch**: A patch release includes bug fixes and minor improvements. It doesn't include breaking changes. For example, if the current version is `2.0.0`, the next patch release will be `2.0.1`.
- **minor**: A minor release includes new features, fixes, improvements, and breaking changes. For example, if the current version is `2.0.0`, the next minor release will be `2.1.0`.
- **major**: A major release includes significant changes to the entire codebase and architecture. For those, the update process will be more elaborate. For example, if the current version is `2.0.0`, the next major release would be `3.0.0`.

***

## Check Installed Version

To check the currently installed version of Medusa in your project, run the following command in your Medusa application:

```bash
npx medusa -v
```

This will show you the installed version of Medusa and the [Medusa CLI tool](https://docs.medusajs.com/resources/medusa-cli/index.html.md), which should be the same.

***

## Check Latest Version

The documentation shows the current version at the top right of the navigation bar. When a new version is released, you'll find a blue dot on the version number. Clicking it will take you to the [release notes on GitHub](https://github.com/medusajs/medusa/releases).

You can also star the [Medusa repository on GitHub](https://github.com/medusajs/medusa) to receive updates about new releases on your GitHub dashboard. Our team also shares updates on new releases on our social media channels.

***

## Update Medusa Application

Before updating a Medusa application, make sure to check the [release notes](https://github.com/medusajs/medusa/releases) for any breaking changes that require actions from your side.

Then, to update your Medusa application, bump the version of all `@medusajs/*` dependencies in your `package.json`. Then, re-install dependencies:

```bash npm2yarn
npm install
```

This will update all Medusa packages to the latest version.

### Running Migrations

Releases may include changes to the database, such as new tables, updates to existing tables, updates after adding links, or data migration scripts.

So, after updating Medusa, run the following command to migrate the latest changes to your database:

```bash
npx medusa db:migrate
```

This will run all pending migrations, sync links, and run data migration scripts.

### Reverting an Update

Before reverting an update, if you already ran the migrations, you have to first identify the modules who had migrations. Then, before reverting, run the `db:rollback` command for each of those modules.

For example, if the version you updated to had migrations for the Cart and Product Modules, run the following command:

```bash
npx medusa db:rollback cart product
```

Then, revert the update by changing the version of all `@medusajs/*` dependencies in your `package.json` to the previous version and re-installing dependencies:

```bash npm2yarn
npm  install
```

Finally, run the migrations to sync link changes:

```bash
npx medusa db:migrate
```

***

## Understanding Codebase Changes

In the Medusa codebase, our team uses the following [TSDoc](https://tsdoc.org/) tags to indicate changes made in the latest version for a specific piece of code:

- `@deprecated`: Indicates that a piece of code is deprecated and will be removed in a future version. The tag's message will include details on what to use instead. However, our updates are always backward-compatible, allowing you to update your codebase at your own pace.
- `@version`: Indicates the version when a piece of code was available from. A piece of code that has this tag will only be available starting from the specified version.

***

## Update Plugin Project

If you have a Medusa plugin project, you only need to update its `@medusajs/*` dependencies in the `package.json` file to the latest version. Then, re-install dependencies:

```bash npm2yarn
npm  install
```


# Medusa Application Configuration

In this chapter, you'll learn available configurations in the Medusa application. You can change the application's configurations to customize the behavior of the application, its integrated modules and plugins, and more.

## Configuration File

All configurations of the Medusa application are stored in the `medusa.config.ts` file. The file exports an object created using the `defineConfig` utility. For example:

```ts title="medusa.config.ts"
import { loadEnv, defineConfig } from "@medusajs/framework/utils"

loadEnv(process.env.NODE_ENV || "development", process.cwd())

module.exports = defineConfig({
  projectConfig: {
    databaseUrl: process.env.DATABASE_URL,
    http: {
      storeCors: process.env.STORE_CORS!,
      adminCors: process.env.ADMIN_CORS!,
      authCors: process.env.AUTH_CORS!,
      jwtSecret: process.env.JWT_SECRET || "supersecret",
      cookieSecret: process.env.COOKIE_SECRET || "supersecret",
    },
  },
})

```

The `defineConfig` utility accepts an object having the following properties:

- [projectConfig](#project-configurations-projectConfig): Essential configurations related to the Medusa application, such as database and CORS configurations.
- [admin](#admin-configurations-admin): Configurations related to the Medusa Admin.
- [modules](#module-configurations-modules): Configurations related to registered modules.
- [plugins](#plugin-configurations-plugins): Configurations related to registered plugins.
- [featureFlags](#feature-flags-featureFlags): Configurations to manage enabled beta features in the Medusa application.

### Using Environment Variables

Notice that you use the `loadEnv` utility to load environment variables. Learn more about it in the [Environment Variables chapter](https://docs.medusajs.com/learn/fundamentals/environment-variables/index.html.md).

By using this utility, you can use environment variables as the values of your configurations. It's highly recommended that you use environment variables for secret values, such as API keys and database credentials, or for values that change based on the environment, such as the application's Cross Origin Resource Sharing (CORS) configurations.

For example, you can set the `DATABASE_URL` environment variable in your `.env` file:

```bash
DATABASE_URL=postgres://postgres@localhost/medusa-store
```

Then, use the value in `medusa-config.ts`:

```ts title="medusa-config.ts"
module.exports = defineConfig({
  projectConfig: {
    databaseUrl: process.env.DATABASE_URL,
    // ...
  },
  // ...
})
```

***

## Project Configurations (`projectConfig`)

The `projectConfig` object contains essential configurations related to the Medusa application, such as database and CORS configurations.

### databaseDriverOptions

The `projectConfig.databaseDriverOptions` configuration is an object of additional options used to configure the PostgreSQL connection. For example, you can support TLS/SSL connection using this configuration's `ssl` property.

This configuration is useful for production databases, which can be supported by setting the `rejectUnauthorized` attribute of `ssl` object to `false`. During development, it's recommended not to pass the `ssl.rejectUnauthorized` option.

#### Example

```ts title="medusa-config.ts"
module.exports = defineConfig({
  projectConfig: {
    databaseDriverOptions: process.env.NODE_ENV !== "development" ?
      { connection: { ssl: { rejectUnauthorized: false } } } : {},
    // ...
  },
  // ...
})
```

When you disable `rejectUnauthorized`, make sure to also add `?ssl_mode=disable` to the end of the [databaseUrl](#databaseUrl) as well.

#### Properties

- connection: (\`object\`)

  - ssl: (\`object\` | \`boolean\`)

  - pool: (\`object\`)

    - min: (\`number\`)

    - max: (\`number\`)

    - idleTimeoutMillis: (\`number\`)

    - reapIntervalMillis: (\`number\`)

    - createRetryIntervalMillis: (\`number\`)
- idle\_in\_transaction\_session\_timeout: (\`number\`)

### databaseLogging

The `projectConfig.databaseLogging` configuration specifies whether database messages should be logged to the console. It is `false` by default.

#### Example

```ts title="medusa-config.ts"
module.exports = defineConfig({
  projectConfig: {
    databaseLogging: true,
    // ...
  },
  // ...
})
```

### databaseName

The `projectConfig.databaseName` configuration determines the name of the database to connect to. If the name is specified in the [databaseUrl](#databaseUrl) configuration, you don't have to use this configuration.

After setting the database credentials, you can create and setup the database using the [db:setup](https://docs.medusajs.com/resources/medusa-cli/commands/db#dbsetup/index.html.md) command of the Medusa CLI.

#### Example

```ts title="medusa-config.ts"
module.exports = defineConfig({
  projectConfig: {
    databaseName: process.env.DATABASE_NAME ||
      "medusa-store",
    // ...
  },
  // ...
})
```

### databaseSchema

The `projectConfig.databaseSchema` configuration specifies the PostgreSQL database schema to connect to, which is `public` by default. Use this configuration only if you want to connect to a different schema.

#### Example

```ts title="medusa-config.ts"
module.exports = defineConfig({
  projectConfig: {
    databaseSchema: process.env.DATABASE_SCHEMA ||
      "custom",
    // ...
  },
  // ...
})
```

### databaseUrl

The `projectConfig.databaseUrl` configuration specifies the PostgreSQL connection URL of the database to connect to. Its format is:

```bash
postgres://[user][:password]@[host][:port]/[dbname]
```

Where:

- `[user]`: (required) your PostgreSQL username. If not specified, the system's username is used by default. The database user that you use must have create privileges. If you're using the `postgres` superuser, then it should have these privileges by default. Otherwise, make sure to grant your user create privileges. You can learn how to do that in [PostgreSQL's documentation](https://www.postgresql.org/docs/current/ddl-priv.html).
- `[:password]`: an optional password for the user. When provided, make sure to put `:` before the password.
- `[host]`: (required) your PostgreSQL host. When run locally, it should be `localhost`.
- `[:port]`: an optional port that the PostgreSQL server is listening on. By default, it's `5432`. When provided, make sure to put `:` before the port.
- `[dbname]`: the name of the database. If not set, then you must provide the database name in the [databaseName](#databasename) configuration.

You can learn more about the connection URL format in [PostgreSQL’s documentation](https://www.postgresql.org/docs/current/libpq-connect.html).

After setting the database URL, you can create and setup the database using the [db:setup](https://docs.medusajs.com/resources/medusa-cli/commands/db#dbsetup/index.html.md) command of the Medusa CLI.

#### Example

For example, set the following database URL in your environment variables:

```bash
DATABASE_URL=postgres://postgres@localhost/medusa-store
```

Then, use the value in `medusa-config.ts`:

```ts title="medusa-config.ts"
module.exports = defineConfig({
  projectConfig: {
    databaseUrl: process.env.DATABASE_URL,
    // ...
  },
  // ...
})
```

### http

The `http` configures the application's http-specific settings, such as the JWT secret, CORS configurations, and more.

#### http.jwtSecret

The `projectConfig.http.jwtSecret` configuration is a random string used to create authentication tokens in the HTTP layer. This configuration is not required in development, but must be set in production.

In a development environment, if this option is not set the default value is `supersecret`. However, in production, if this configuration is not set, an error is thrown and the application crashes. This is to ensure that you set a secure value for the JWT secret in production.

#### Example

```ts title="medusa-config.ts"
module.exports = defineConfig({
  projectConfig: {
    http: {
      jwtSecret: process.env.JWT_SECRET || "supersecret",
    },
    // ...
  },
  // ...
})
```

#### http.jwtExpiresIn

The `projectConfig.http.jwtExpiresIn` configuration specifies the expiration time for the JWT token. Its value format is based off the [ms package](https://github.com/vercel/ms).

If not provided, the default value is `1d`.

#### Example

```ts title="medusa-config.ts"
module.exports = defineConfig({
  projectConfig: {
    http: {
      jwtExpiresIn: process.env.JWT_EXPIRES_IN || "2d",
    },
    // ...
  },
  // ...
})
```

#### http.cookieSecret

The `projectConfig.http.cookieSecret` configuration is a random string used to sign cookies in the HTTP layer. This configuration is not required in development, but must be set in production.

In a development environment, if this option is not set the default value is `supersecret`. However, in production, if this configuration is not set, an error is thrown and the application crashes. This is to ensure that you set a secure value for the cookie secret in production.

#### Example

```ts title="medusa-config.ts"
module.exports = defineConfig({
  projectConfig: {
    http: {
      cookieSecret: process.env.COOKIE_SECRET || "supersecret",
    },
    // ...
  },
  // ...
})
```

#### http.authCors

The `projectConfig.http.authCors` configuration specifies the accepted URLs or patterns for API routes starting with `/auth`. It can either be one accepted origin, or a comma-separated list of accepted origins.

Every origin in that list must either be:

- A full URL. For example, `http://localhost:7001`. The URL must not end with a backslash;
- Or a regular expression pattern that can match more than one origin. For example, `.example.com`. The regex pattern that Medusa tests for is `^([\/~@;%#'])(.*?)\1([gimsuy]*)$`.

Since the `/auth` routes are used for authentication for both store and admin routes, it's recommended to set this configuration's value to a combination of the [storeCors](#httpstoreCors) and [adminCors](#httpadminCors) configurations.

Some example values of common use cases:

```bash
# Allow different ports locally starting with 700
AUTH_CORS=/http:\/\/localhost:700\d+$/

# Allow any origin ending with vercel.app. For example, admin.vercel.app
AUTH_CORS=/vercel\.app$/

# Allow all HTTP requests
AUTH_CORS=/http:\/\/.+/
```

Then, set the configuration in `medusa-config.ts`:

```ts title="medusa-config.ts"
module.exports = defineConfig({
  projectConfig: {
    http: {
      authCors: process.env.AUTH_CORS,
    },
    // ...
  },
  // ...
})
```

If you’re adding the value directly within `medusa-config.ts`, make sure to add an extra escaping `/` for every backslash in the pattern. For example:

```ts title="medusa-config.ts"
module.exports = defineConfig({
  projectConfig: {
    http: {
      authCors: "/http:\\/\\/localhost:700\\d+$/",
    },
    // ...
  },
  // ...
})
```

#### http.storeCors

The `projectConfig.http.storeCors` configuration specifies the accepted URLs or patterns for API routes starting with `/store`. It can either be one accepted origin, or a comma-separated list of accepted origins.

Every origin in that list must either be:

- A full URL. For example, `http://localhost:7001`. The URL must not end with a backslash;
- Or a regular expression pattern that can match more than one origin. For example, `.example.com`. The regex pattern that Medusa tests for is `^([\/~@;%#'])(.*?)\1([gimsuy]*)$`.

Some example values of common use cases:

```bash
# Allow different ports locally starting with 800
STORE_CORS=/http:\/\/localhost:800\d+$/

# Allow any origin ending with vercel.app. For example, storefront.vercel.app
STORE_CORS=/vercel\.app$/

# Allow all HTTP requests
STORE_CORS=/http:\/\/.+/
```

Then, set the configuration in `medusa-config.ts`:

```ts title="medusa-config.ts"
module.exports = defineConfig({
  projectConfig: {
    http: {
      storeCors: process.env.STORE_CORS,
    },
    // ...
  },
  // ...
})
```

If you’re adding the value directly within `medusa-config.ts`, make sure to add an extra escaping `/` for every backslash in the pattern. For example:

```ts title="medusa-config.ts"
module.exports = defineConfig({
  projectConfig: {
    http: {
      storeCors: "/vercel\\.app$/",
    },
    // ...
  },
  // ...
})
```

#### http.adminCors

The `projectConfig.http.adminCors` configuration specifies the accepted URLs or patterns for API routes starting with `/admin`. It can either be one accepted origin, or a comma-separated list of accepted origins.

Every origin in that list must either be:

- A full URL. For example, `http://localhost:7001`. The URL must not end with a backslash;
- Or a regular expression pattern that can match more than one origin. For example, `.example.com`. The regex pattern that Medusa tests for is `^([\/~@;%#'])(.*?)\1([gimsuy]*)$`.

Some example values of common use cases:

```bash
# Allow different ports locally starting with 700
ADMIN_CORS=/http:\/\/localhost:700\d+$/

# Allow any origin ending with vercel.app. For example, admin.vercel.app
ADMIN_CORS=/vercel\.app$/

# Allow all HTTP requests
ADMIN_CORS=/http:\/\/.+/
```

Then, set the configuration in `medusa-config.ts`:

```ts title="medusa-config.ts"
module.exports = defineConfig({
  projectConfig: {
    http: {
      adminCors: process.env.ADMIN_CORS,
    },
    // ...
  },
  // ...
})
```

If you’re adding the value directly within `medusa-config.ts`, make sure to add an extra escaping `/` for every backslash in the pattern. For example:

```ts title="medusa-config.ts"
module.exports = defineConfig({
  projectConfig: {
    http: {
      adminCors: "/vercel\\.app$/",
    },
    // ...
  },
  // ...
})
```

#### http.compression

The `projectConfig.http.compression` configuration modifies the HTTP compression settings at the application layer. If you have access to the HTTP server, the recommended approach would be to enable it there. However, some platforms don't offer access to the HTTP layer and in those cases, this is a good alternative.

If you enable HTTP compression and you want to disable it for specific API Routes, you can pass in the request header `"x-no-compression": true`. Learn more in the [API Reference](https://docs.medusajs.com/api/store#http-compression).

For example:

```ts title="medusa-config.ts"
module.exports = defineConfig({
  projectConfig: {
    http: {
      compression: {
        enabled: true,
        level: 6,
        memLevel: 8,
        threshold: 1024,
      },
    },
    // ...
  },
  // ...
})
```

This configuation is an object that accepts the following properties:

- enabled: (\`boolean\`)
- level: (\`number\`) The level of zlib compression to apply to responses. A higher level will result in better compression but will take longer to complete. A lower level will result in less compression but will be much faster.
- memLevel: (\`number\`) How much memory should be allocated to the internal compression state. It value is between \`1\` (minimum level) and \`9\` (maximum level).
- threshold: (\`number\` | \`string\`) The minimum response body size that compression is applied on. Its value can be the number of bytes or any string accepted by the \[bytes]\(https://www.npmjs.com/package/bytes) package.

#### http.authMethodsPerActor

The `projectConfig.http.authMethodsPerActor` configuration specifies the supported authentication providers per actor type (such as `user`, `customer`, or any custom actor).

For example, you can allow Google login for `customers`, and allow email/password logins for `users` in the admin.

`authMethodsPerActor` is a an object whose key is the actor type (for example, `user`), and the value is an array of supported auth provider IDs (for example, `emailpass`).

Learn more about actor types in the [Auth Identity and Actor Type documentation](https://docs.medusajs.com/resources/commerce-modules/auth/auth-identity-and-actor-types/index.html.md).

For example:

```ts title="medusa-config.ts"
module.exports = defineConfig({
  projectConfig: {
    http: {
      authMethodsPerActor: {
        user: ["emailpass"],
        customer: ["emailpass", "google"],
      },
    },
    // ...
  },
  // ...
})
```

The above configurations allow admin users to login using email/password, and customers to login using email/password and Google.

#### http.restrictedFields

The `projectConfig.http.restrictedFields` configuration specifies the fields that can't be selected in API routes (using the `fields` query parameter) unless they're allowed in the [request's Query configurations](https://docs.medusajs.com/learn/fundamentals/module-links/query#request-query-configurations/index.html.md). This is useful to restrict sensitive fields from being exposed in the API.

For example, you can restrict selecting customers in store API routes:

```ts title="medusa-config.ts"
module.exports = defineConfig({
  projectConfig: {
    http: {
      restrictedFields: {
        store: ["customer", "customers"],
      },
    },
    // ...
  },
  // ...
})
```

The `restrictedFields` configuration accepts the following properties:

- store: (\`string\[]\`)

### redisOptions

The `projectConfig.redisOptions` configuration defines options to pass to `ioredis`, which creates the Redis connection used to store the Medusa server session. Refer to [ioredis’s RedisOptions documentation](https://redis.github.io/ioredis/index.html#RedisOptions)
for the list of available options.

#### Example

```ts title="medusa-config.ts"
module.exports = defineConfig({
  projectConfig: {
    redisOptions: {
      connectionName: process.env.REDIS_CONNECTION_NAME ||
        "medusa",
    },
    // ...
  },
  // ...
})
```

### redisPrefix

The `projectConfig.redisPrefix` configuration defines a prefix on all keys stored in Redis for the Medusa server session. The default value is `sess:`.

The value of this configuration is prepended to `sess:`. For example, if you set it to `medusa:`, then a key stored in Redis is prefixed by `medusa:sess`.

This configuration is not used for modules that also connect to Redis, such as the [Redis Cache Module](https://docs.medusajs.com/resources/infrastructure-modules/cache/redis/index.html.md).

#### Example

```ts title="medusa-config.ts"
module.exports = defineConfig({
  projectConfig: {
    redisPrefix: process.env.REDIS_URL || "medusa:",
    // ...
  },
  // ...
})
```

### redisUrl

The `projectConfig.redisUrl` configuration specifies the connection URL to Redis to store the Medusa server session. When specified, the Medusa server uses Redis to store the session data. Otherwie, the session data is stored in-memory.

This configuration is not used for modules that also connect to Redis, such as the [Redis Cache Module](https://docs.medusajs.com/resources/infrastructure-modules/cache/redis/index.html.md). You'll have to configure the Redis connection for those modules separately.

You must first have Redis installed. You can refer to [Redis's installation guide](https://redis.io/docs/getting-started/installation/).

The Redis connection URL has the following format:

```bash
redis[s]://[[username][:password]@][host][:port][/db-number]
```

Where:

- `redis[s]`: the protocol used to connect to Redis. Use `rediss` for a secure connection.
- `[[username][:password]@]`: an optional username and password for the Redis server.
- `[host]`: the host of the Redis server. When run locally, it should be `localhost`.
- `[:port]`: an optional port that the Redis server is listening on. By default, it's `6379`.
- `[/db-number]`: an optional database number to connect to. By default, it's `0`.

For a local Redis installation, the connection URL should be `redis://localhost:6379` unless you’ve made any changes to the Redis configuration during installation.

#### Example

```ts title="medusa-config.ts"
module.exports = defineConfig({
  projectConfig: {
    redisUrl: process.env.REDIS_URL ||
      "redis://localhost:6379",
    // ...
  },
  // ...
})
```

### sessionOptions

The `projectConfig.sessionOptions` configuration defines additional options to pass to [express-session](https://www.npmjs.com/package/express-session), which is used to store the Medusa server session.

This configuration is not used for modules that also connect to Redis, such as the [Redis Cache Module](https://docs.medusajs.com/resources/infrastructure-modules/cache/redis/index.html.md).

#### Example

```ts title="medusa-config.ts"
module.exports = defineConfig({
  projectConfig: {
    sessionOptions: {
      name: process.env.SESSION_NAME || "custom",
    },
    // ...
  },
  // ...
})
```

#### Properties

- name: (\`string\`)
- resave: (\`boolean\`)
- rolling: (\`boolean\`)
- saveUninitialized: (\`boolean\`)
- secret: (\`string\`) The secret to sign the session ID cookie. By default, the value of \[http.cookieSecret]\(#httpcookieSecret) is used. Refer to \[express-session’s documentation]\(https://www.npmjs.com/package/express-session#secret) for details.
- ttl: (\`number\`) The time-to-live (TTL) of the session ID cookie in milliseconds. It is used when calculating the \`Expires\` \`Set-Cookie\` attribute of cookies. Refer to \[express-session’s documentation]\(https://www.npmjs.com/package/express-session#cookie) for more details.

### workerMode

The `projectConfig.workerMode` configuration specifies the worker mode of the Medusa application. You can learn more about it in the [Worker Mode chapter](https://docs.medusajs.com/learn/production/worker-mode/index.html.md).

The value for this configuration can be one of the following:

- `shared`: run the application in a single process, meaning the worker and server run in the same process.
- `worker`: run the a worker process only.
- `server`: run the application server only.

#### Example

```ts title="medusa-config.ts"
module.exports = defineConfig({
  projectConfig: {
    workerMode: process.env.WORKER_MODE || "shared",
    // ...
  },
  // ...
})
```

***

## Admin Configurations (`admin`)

The `admin` object contains configurations related to the Medusa Admin.

### backendUrl

The `admin.backendUrl` configuration specifies the URL of the Medusa application. Its default value is the browser origin. This is useful to set when running the admin on a separate domain.

#### Example

```ts title="medusa-config.ts"
module.exports = defineConfig({
  admin: {
    backendUrl: process.env.MEDUSA_BACKEND_URL ||
      "http://localhost:9000",
  },
  // ...
})
```

### disable

The `admin.disable` configuration specifies whether to disable the Medusa Admin. If disabled, the Medusa Admin will not be compiled and you can't access it at `/app` path of your application. The default value is `false`.

#### Example

```ts title="medusa-config.ts"
module.exports = defineConfig({
  admin: {
    disable: process.env.ADMIN_DISABLED === "true" ||
      false,
  },
  // ...
})
```

### path

The `admin.path` configuration indicates the path to the admin dashboard, which is `/app` by default. The value must start with `/` and can't end with a `/`.

The value cannot be one of the reserved paths:

- `/admin`
- `/store`
- `/auth`
- `/`

When using Docker, make sure that the root path of the Docker image isn't the same as the admin's path. For example, if the Docker image's root path is `/app`, change
the value of the `admin.path` configuration, since it's `/app` by default.

#### Example

```ts title="medusa-config.ts"
module.exports = defineConfig({
  admin: {
    path: process.env.ADMIN_PATH || `/app`,
  },
  // ...
})
```

### storefrontUrl

The `admin.storefrontUrl` configuration specifies the URL of the Medusa storefront application. This URL is used as a prefix to some links in the admin that require performing actions in the storefront.

For example, this URL is used as a prefix to shareable payment links for orders with outstanding amounts.

#### Example

```js title="medusa-config.js"
module.exports = defineConfig({
  admin: {
    storefrontUrl: process.env.MEDUSA_STOREFRONT_URL ||
      "http://localhost:8000",
  },
  // ...
})
```

### vite

The `admin.vite` configration specifies Vite configurations for the Medusa Admin. Its value is a function that receives the default Vite configuration and returns the modified configuration. The default value is `undefined`.

Learn about configurations you can pass to Vite in [Vite's documentation](https://vite.dev/config/).

#### Example

For example, if you're using a third-party library that isn't ESM-compatible, add it to Vite's `optimizeDeps` configuration:

```ts title="medusa-config.ts"
module.exports = defineConfig({
  admin: {
    vite: () => {
      return {
        optimizeDeps: {
          include: ["qs"],
        },
      }
    },
  },
  // ...
})
```

***

## Module Configurations (`modules`)

The `modules` configuration allows you to register and configure the [modules](https://docs.medusajs.com/learn/fundamentals/modules/index.html.md) registered in the Medusa application. Medusa's commerce and Infrastructure Modules are configured by default. So, you only need to pass your custom modules, or override the default configurations of the existing modules.

`modules` is an array of objects for the modules to register. Each object has the following properties:

1. `resolve`: a string indicating the path to the module, or the module's NPM package name. For example, `./src/modules/my-module`.
2. `options`: (optional) an object indicating the [options to pass to the module](https://docs.medusajs.com/learn/fundamentals/modules/options/index.html.md). This object is specific to the module and its configurations. For example, your module may require an API key option, which you can pass in this object.

For modules that are part of a plugin, learn about registering them in the [Register Modules in Plugins](#register-modules-in-plugins) section.

### Example

To register a custom module:

```ts title="medusa-config.ts"
module.exports = defineConfig({
  modules: [
    {
      resolve: "./src/modules/cms",
      options: {
        apiKey: process.env.CMS_API_KEY,
      },
    },
  ],
  // ...
})
```

You can also override the default configurations of Medusa's modules. For example, to add a Notification Module Provider to the Notification Module:

```ts title="medusa-config.ts"
module.exports = defineConfig({
  modules: [
    {
      resolve: "@medusajs/medusa/notification",
      options: {
        providers: [
          // default provider
          {
            resolve: "@medusajs/medusa/notification-local",
            id: "local",
            options: {
              name: "Local Notification Provider",
              channels: ["feed"],
            },
          },
          // custom provider
          {
            resolve: "./src/modules/my-notification",
            id: "my-notification",
            options: {
              channels: ["email"],
              // provider options...
            },
          },
        ],
      },
    },
  ],
  // ...
})
```

***

## Plugin Configurations (`plugins`)

The `plugins` configuration allows you to register and configure the [plugins](https://docs.medusajs.com/learn/fundamentals/plugins/index.html.md) registered in the Medusa application. Plugins include re-usable Medusa customizations, such as modules, workflows, API routes, and more.

Aside from installing the plugin with NPM, you must also register it in the `medusa.config.ts` file.

The `plugins` configuration is an array of objects for the plugins to register. Each object has the following properties:

- A string, which is the name of the plugin's package as specified in the plugin's `package.json` file. This is useful if the plugin doesn't require any options.
- An object having the following properties:
  - `resolve`: The name of the plugin's package as specified in the plugin's `package.json` file.
  - `options`: An object that includes [options to be passed to the modules](https://docs.medusajs.com/learn/fundamentals/modules/options#pass-options-to-a-module-in-a-plugin/index.html.md) within the plugin.

### Example

```ts title="medusa-config.ts"
module.exports = {
  plugins: [
    `medusa-my-plugin-1`,
    {
      resolve: `medusa-my-plugin`,
      options: {
        apiKey: process.env.MY_API_KEY ||
          `test`,
      },
    },
    // ...
  ],
  // ...
}
```

The above configuration registers two plugins: `medusa-my-plugin-1` and `medusa-my-plugin`. The latter plugin requires an API key option, which is passed in the `options` object.

### Register Modules in Plugins

When you register a plugin, its modules are automatically registered in the Medusa application. You don't have to register them manually in the `modules` configuration.

However, this isn't the case for module providers. If your plugin includes a module provider, you must register it in the `modules` configuration, referencing the module provider's path.

For example:

```ts title="medusa-config.ts"
module.exports = {
  plugins: [
    `medusa-my-plugin`,
  ],
  modules: [
    {
      resolve: "@medusajs/medusa/notification",
      options: {
        providers: [
          // ...
          {
            resolve: "medusa-my-plugin/providers/my-notification",
            id: "my-notification",
            options: {
              channels: ["email"],
              // provider options...
            },
          },
        ],
      },
    },
  ],
  // ...
}
```

***

## Feature Flags (`featureFlags`)

The `featureFlags` configuration allows you to manage enabled beta features in the Medusa application.

Some features in the Medusa application are guarded by a feature flag. This ensures constant shipping of new features while maintaining the engine’s stability. You can enable or disable these features using the `featureFlags` configuration.

The `featureFlags`'s value is an object whose keys are the names of the feature flags, and their values a boolean indicating whether the feature flag is enabled.

Only enable feature flags in testing or development environments. Enabling a feature flag may introduce breaking changes or unexpected behavior.

You can find available feature flags and their key name [here](https://github.com/medusajs/medusa/tree/develop/packages/medusa/src/loaders/feature-flags).

### Example

```ts title="medusa-config.ts"
module.exports = defineConfig({
  featureFlags: {
    index_engine: true,
    // ...
  },
  // ...
})
```

After enabling a feature flag, make sure to run migrations, as the feature may introduce database changes:

```bash
npx medusa db:migrate
```


# Using TypeScript Aliases

By default, Medusa doesn't support TypeScript aliases in production.

If you prefer using TypeScript aliases, install following development dependencies:

```bash npm2yarn
npm install --save-dev tsc-alias rimraf
```

Where `tsc-alias` is a package that resolves TypeScript aliases, and `rimraf` is a package that removes files and directories.

Then, add a new `resolve:aliases` script to your `package.json` and update the `build` script:

```json title="package.json"
{
  "scripts": {
    // other scripts...
    "resolve:aliases": "tsc --showConfig -p tsconfig.json > tsconfig.resolved.json && tsc-alias -p tsconfig.resolved.json && rimraf tsconfig.resolved.json",
    "build": "medusa build && npm run resolve:aliases"
  }
}
```

You can now use TypeScript aliases in your Medusa application. For example, add the following in `tsconfig.json`:

```json title="tsconfig.json"
{
  "compilerOptions": {
    // ...
    "paths": {
      "@/*": ["./src/*"]
    }
  }
}
```

Now, you can import modules, for example, using TypeScript aliases:

```ts
import { BrandModuleService } from "@/modules/brand/service"
```


# Build Custom Features

In the upcoming chapters, you'll follow step-by-step guides to build custom features in Medusa. These guides gradually introduce Medusa's concepts to help you understand what they are and how to use them.

By following these guides, you'll add brands to the Medusa application that you can associate with products.

To build a custom feature in Medusa, you need three main tools:

- [Module](https://docs.medusajs.com/learn/fundamentals/modules/index.html.md): a package with commerce logic for a single domain. It defines new tables to add to the database, and a class of methods to manage these tables.
- [Workflow](https://docs.medusajs.com/learn/fundamentals/workflows/index.html.md): a tool to perform an operation comprising multiple steps with built-in rollback and retry mechanisms.
- [API route](https://docs.medusajs.com/learn/fundamentals/api-routes/index.html.md): a REST endpoint that exposes commerce features to clients, such as the admin dashboard or a storefront. The API route executes a workflow that implements the commerce feature using modules.

![Diagram showcasing the flow of a custom developed feature](https://res.cloudinary.com/dza7lstvk/image/upload/v1725867628/Medusa%20Book/custom-development_nofvp6.jpg)

***

## Next Chapters: Brand Module Example

The next chapters will guide you to:

1. Build a Brand Module that creates a `Brand` data model and provides data-management features.
2. Add a workflow to create a brand.
3. Expose an API route that allows admin users to create a brand using the workflow.


# Customize Medusa Admin Dashboard

In the previous chapters, you've customized your Medusa application to [add brands](https://docs.medusajs.com/learn/customization/custom-features/module/index.html.md), [expose an API route to create brands](https://docs.medusajs.com/learn/customization/custom-features/api-route/index.html.md), and [linked brands to products](https://docs.medusajs.com/learn/customization/extend-features/define-link/index.html.md).

After customizing and extending your application with new features, you may need to provide an interface for admin users to utilize these features. The Medusa Admin dashboard is extendable, allowing you to:

- Insert components, called [widgets](https://docs.medusajs.com/learn/fundamentals/admin/widgets/index.html.md), on existing pages.
- Add new pages, called [UI Routes](https://docs.medusajs.com/learn/fundamentals/admin/ui-routes/index.html.md).

From these customizations, you can send requests to custom API routes, allowing admin users to manage custom resources on the dashboard

***

## Next Chapters: View Brands in Dashboard

In the next chapters, you'll continue with the brands example to:

- Add a new section to the product details page that shows the product's brand.
- Add a new page in the dashboard that shows all brands in the store.


# Extend Core Commerce Features

In the upcoming chapters, you'll learn about the concepts and tools to extend Medusa's core commerce features.

In other commerce platforms, you extend core features and models through hacky workarounds that can introduce unexpected issues and side effects across the platform. It also makes your application difficult to maintain and upgrade in the long run.

The Medusa Framework and orchestration tools mitigate these issues while supporting all your customization needs:

- [Module Links](https://docs.medusajs.com/learn/fundamentals/module-links/index.html.md): Link data models of different modules without building direct dependencies, ensuring that the Medusa application integrates your modules without side effects.
- [Workflow Hooks](https://docs.medusajs.com/learn/fundamentals/workflows/workflow-hooks/index.html.md): inject custom functionalities into a workflow at predefined points, called hooks. This allows you to perform custom actions as a part of a core workflow without hacky workarounds.
- [Additional Data in API Routes](https://docs.medusajs.com/learn/fundamentals/api-routes/additional-data/index.html.md): Configure core API routes to accept request parameters relevant to your customizations. These parameters are passed to the underlying workflow's hooks, where you can manage your custom data as part of an existing flow.

***

## Next Chapters: Link Brands to Products Example

The next chapters explain how to use the tools mentioned above with step-by-step guides. You'll continue with the [brands example from the previous chapters](https://docs.medusajs.com/learn/customization/custom-features/index.html.md) to:

- Link brands from the custom [Brand Module](https://docs.medusajs.com/learn/customization/custom-features/module/index.html.md) to products from Medusa's [Product Module](https://docs.medusajs.com/resources/commerce-modules/product/index.html.md).
- Extend the core product-creation workflow and the API route that uses it to allow setting the brand of a newly created product.
- Retrieve a product's associated brand's details.


# Integrate Third-Party Systems

Commerce applications often connect to third-party systems that provide additional or specialized features. For example, you may integrate a Content-Management System (CMS) for rich content features, a payment provider to process credit-card payments, and a notification service to send emails.

The Medusa Framework facilitates integrating these systems and orchestrating operations across them, saving you the effort of managing them yourself. You won't find those capabilities in other commerce platforms that in these scenarios become a bottleneck to building customizations and iterating quickly.

In Medusa, you integrate a third-party system by:

1. Creating a module whose service provides the methods to connect to and perform operations in the third-party system.
2. Building workflows that complete tasks spanning across systems. You use the module that integrates a third-party system in the workflow's steps.
3. Executing the workflows you built in an [API route](https://docs.medusajs.com/learn/fundamentals/api-routes/index.html.md), at a scheduled time, or when an event is emitted.

***

## Next Chapters: Sync Brands Example

In the previous chapters, you've [added brands](https://docs.medusajs.com/learn/customization/custom-features/module/index.html.md) to your Medusa application. In the next chapters, you will:

1. Integrate a dummy third-party CMS in the Brand Module.
2. Sync brands to the CMS when a brand is created.
3. Sync brands from the CMS at a daily schedule.


# Customizations Next Steps: Learn the Fundamentals

The previous guides introduced Medusa's different concepts and how you can use them to customize Medusa for a realistic use case, You added brands to your application, linked them to products, customized the admin dashboard, and integrated a third-party CMS.

The next chapters will cover each of these concepts in depth, with the different ways you can use them, their options or configurations, and more advanced features that weren't covered in the previous guides. While you can start building with Medusa, it's highly recommended to follow the next chapters for a better understanding of Medusa's fundamentals.

## Useful Guides

The following guides and references are useful for your development journey:

3. [Commerce Modules](https://docs.medusajs.com/resources/commerce-modules/index.html.md): Browse the list of Commerce Modules in Medusa and their references to learn how to use them.
4. [Service Factory Reference](https://docs.medusajs.com/resources/service-factory-reference/index.html.md): Learn about the methods generated by `MedusaService` with examples.
5. [Workflows Reference](https://docs.medusajs.com/resources/medusa-workflows-reference/index.html.md): Browse the list of core workflows and their hooks that are useful for your customizations.
6. [Admin Injection Zones](https://docs.medusajs.com/resources/admin-widget-injection-zones/index.html.md): Browse the injection zones in the Medusa Admin to learn where you can inject widgets.

***

## More Examples in Recipes

In the [Recipes](https://docs.medusajs.com/resources/recipes/index.html.md) documentation, you'll also find step-by-step guides for different use cases, such as building a marketplace, digital products, and more.


# Re-Use Customizations with Plugins

In the previous chapters, you've learned important concepts related to creating modules, implementing commerce features in workflows, exposing those features in API routes, customizing the Medusa Admin dashboard with Admin Extensions, and integrating third-party systems.

You've implemented the brands example within a single Medusa application. However, this approach is not scalable when you want to reuse your customizations across multiple projects.

To reuse your customizations across multiple Medusa applications, such as implementing brands in different projects, you can create a plugin. A plugin is an NPM package that encapsulates your customizations and can be installed in any Medusa application. Plugins can include modules, workflows, API routes, Admin Extensions, and more.

![Diagram showcasing how the Brand Plugin would add its resources to any application it's installed in](https://res.cloudinary.com/dza7lstvk/image/upload/v1737540091/Medusa%20Book/brand-plugin_bk9zi9.jpg)

Medusa provides the tooling to create a plugin package, test it in a local Medusa application, and publish it to NPM.

To learn more about plugins and how to create them, refer to [this chapter](https://docs.medusajs.com/learn/fundamentals/plugins/index.html.md).


# Configure Instrumentation

In this chapter, you'll learn about observability in Medusa and how to configure instrumentation with OpenTelemetry.

## Observability with OpenTelemtry

Medusa uses [OpenTelemetry](https://opentelemetry.io/) for instrumentation and reporting. When configured, it reports traces for:

- HTTP requests
- Workflow executions
- Query usages
- Database queries and operations

***

## How to Configure Instrumentation in Medusa?

### Prerequisites

- [An exporter to visualize your application's traces, such as Zipkin.](https://zipkin.io/pages/quickstart.html)

### Install Dependencies

Start by installing the following OpenTelemetry dependencies in your Medusa project:

```bash npm2yarn
npm install @opentelemetry/sdk-node @opentelemetry/resources @opentelemetry/sdk-trace-node @opentelemetry/instrumentation-pg
```

Also, install the dependencies relevant for the exporter you use. If you're using Zipkin, install the following dependencies:

```bash npm2yarn
npm install @opentelemetry/exporter-zipkin
```

### Add instrumentation.ts

Next, create the file `instrumentation.ts` with the following content:

```ts title="instrumentation.ts"
import { registerOtel } from "@medusajs/medusa"
import { ZipkinExporter } from "@opentelemetry/exporter-zipkin"

// If using an exporter other than Zipkin, initialize it here.
const exporter = new ZipkinExporter({
  serviceName: "my-medusa-project",
})

export function register() {
  registerOtel({
    serviceName: "medusajs",
    // pass exporter
    exporter,
    instrument: {
      http: true,
      workflows: true,
      query: true,
    },
  })
}
```

In the `instrumentation.ts` file, you export a `register` function that uses Medusa's `registerOtel` utility function. You also initialize an instance of the exporter, such as Zipkin, and pass it to the `registerOtel` function.

`registerOtel` accepts an object where you can pass any [NodeSDKConfiguration](https://open-telemetry.github.io/opentelemetry-js/interfaces/_opentelemetry_sdk_node.NodeSDKConfiguration.html) property along with the following properties:

The `NodeSDKConfiguration` properties are accepted since Medusa v2.5.1.

- serviceName: (\`string\`) The name of the service traced.
- exporter: (\[SpanExporter]\(https://open-telemetry.github.io/opentelemetry-js/interfaces/\_opentelemetry\_sdk\_trace\_base.SpanExporter.html)) An instance of an exporter, such as Zipkin.
- instrument: (\`object\`) Options specifying what to trace.

  - http: (\`boolean\`) Whether to trace HTTP requests.

  - query: (\`boolean\`) Whether to trace Query usages.

  - workflows: (\`boolean\`) Whether to trace Workflow executions.

  - db: (\`boolean\`) Whether to trace database queries and operations.
- instrumentations: (\[Instrumentation\[]]\(https://open-telemetry.github.io/opentelemetry-js/interfaces/\_opentelemetry\_instrumentation.Instrumentation.html)) Additional instrumentation options that OpenTelemetry accepts.

***

## Test it Out

To test it out, start your exporter, such as Zipkin.

Then, start your Medusa application:

```bash npm2yarn
npm run dev
```

Try to open the Medusa Admin or send a request to an API route.

If you check traces in your exporter, you'll find new traces reported.

### Trace Span Names

Trace span names start with the following keywords based on what it's reporting:

- `{methodName} {URL}` when reporting HTTP requests, where `{methodName}` is the HTTP method, and `{URL}` is the URL the request is sent to.
- `route:` when reporting route handlers running on an HTTP request.
- `middleware:` when reporting a middleware running on an HTTP request.
- `workflow:` when reporting a workflow execution.
- `step:` when reporting a step in a workflow execution.
- `query.graph:` when reporting Query usages.
- `pg.query:` when reporting database queries and operations.


# Logging

In this chapter, you’ll learn how to use Medusa’s logging utility.

## Logger Class

Medusa provides a `Logger` class with advanced logging functionalities. This includes configuring logging levels or saving logs to a file.

The Medusa application registers the `Logger` class in the Medusa container and each module's container as `logger`.

***

## How to Log a Message

Resolve the `logger` using the Medusa container to log a message in your resource.

For example, create the file `src/jobs/log-message.ts` with the following content:

```ts title="src/jobs/log-message.ts" highlights={highlights}
import { MedusaContainer } from "@medusajs/framework/types"
import { ContainerRegistrationKeys } from "@medusajs/framework/utils"

export default async function myCustomJob(
  container: MedusaContainer
) {
  const logger = container.resolve(ContainerRegistrationKeys.LOGGER)

  logger.info("I'm using the logger!")
}

export const config = {
  name: "test-logger",
  // execute every minute
  schedule: "* * * * *",
}
```

This creates a scheduled job that resolves the `logger` from the Medusa container and uses it to log a message.

### Test the Scheduled Job

To test out the above scheduled job, start the Medusa application:

```bash npm2yarn
npm run dev
```

After a minute, you'll see the following message as part of the logged messages:

```text
info:    I'm using the logger!
```

***

## Log Levels

The `Logger` class has the following methods:

- `info`: The message is logged with level `info`.
- `warn`: The message is logged with level `warn`.
- `error`: The message is logged with level `error`.
- `debug`: The message is logged with level `debug`.

Each of these methods accepts a string parameter to log in the terminal with the associated level.

***

## Logging Configurations

### Log Level

The available log levels, from lowest to highest levels, are:

1. `silly`
2. `debug`
3. `verbose`
4. `http` (default, meaning only HTTP requests are logged)
5. `info`
6. `warn`
7. `error`

You can change that by setting the `LOG_LEVEL` environment variable to the minimum level you want to be logged.

For example:

```bash
LOG_LEVEL=error
```

This logs `error` messages only.

The environment variable must be set as a system environment variable and not in `.env`.

### Save Logs in a File

Aside from showing the logs in the terminal, you can save the logs in a file by setting the `LOG_FILE` environment variable to the path of the file relative to the Medusa server’s root directory.

For example:

```bash
LOG_FILE=all.log
```

Your logs are now saved in the `all.log` file at the root of your Medusa application.

The environment variable must be set as a system environment variable and not in `.env`.

***

## Show Log with Progress

The `Logger` class has an `activity` method used to log a message of level `info`. If the Medusa application is running in a development environment, a spinner starts to show the activity's progress.

For example:

```ts title="src/jobs/log-message.ts"
import { MedusaContainer } from "@medusajs/framework/types"
import { ContainerRegistrationKeys } from "@medusajs/framework/utils"

export default async function myCustomJob(
  container: MedusaContainer
) {
  const logger = container.resolve(ContainerRegistrationKeys.LOGGER)

  const activityId = logger.activity("First log message")

  logger.progress(activityId, `Second log message`)

  logger.success(activityId, "Last log message")
}
```

The `activity` method returns the ID of the started activity. This ID can then be passed to one of the following methods of the `Logger` class:

- `progress`: Log a message of level `info` that indicates progress within that same activity.
- `success`: Log a message of level `info` that indicates that the activity has succeeded. This also ends the associated activity.
- `failure`: Log a message of level `error` that indicates that the activity has failed. This also ends the associated activity.

If you configured the `LOG_LEVEL` environment variable to a level higher than those associated with the above methods, their messages won’t be logged.


# Medusa Testing Tools

In this chapter, you'll learn about Medusa's testing tools and how to install and configure them.

## @medusajs/test-utils Package

Medusa provides a Testing Framework to create integration tests for your custom API routes, modules, or other Medusa customizations.

To use the Testing Framework, install `@medusajs/test-utils` as a `devDependency`:

```bash npm2yarn
npm install --save-dev @medusajs/test-utils@latest
```

***

## Install and Configure Jest

Writing tests with `@medusajs/test-utils`'s tools requires installing and configuring Jest in your project.

Run the following command to install the required Jest dependencies:

```bash npm2yarn
npm install --save-dev jest @types/jest @swc/jest
```

Then, create the file `jest.config.js` with the following content:

```js title="jest.config.js"
const { loadEnv } = require("@medusajs/framework/utils")
loadEnv("test", process.cwd())

module.exports = {
  transform: {
    "^.+\\.[jt]s$": [
      "@swc/jest",
      {
        jsc: {
          parser: { syntax: "typescript", decorators: true },
        },
      },
    ],
  },
  testEnvironment: "node",
  moduleFileExtensions: ["js", "ts", "json"],
  modulePathIgnorePatterns: ["dist/"],
  setupFiles: ["./integration-tests/setup.js"],
}

if (process.env.TEST_TYPE === "integration:http") {
  module.exports.testMatch = ["**/integration-tests/http/*.spec.[jt]s"]
} else if (process.env.TEST_TYPE === "integration:modules") {
  module.exports.testMatch = ["**/src/modules/*/__tests__/**/*.[jt]s"]
} else if (process.env.TEST_TYPE === "unit") {
  module.exports.testMatch = ["**/src/**/__tests__/**/*.unit.spec.[jt]s"]
}
```

Next, create the `integration-tests/setup.js` file with the following content:

```js title="integration-tests/setup.js"
const { MetadataStorage } = require("@mikro-orm/core")

MetadataStorage.clear()
```

***

## Add Test Commands

Finally, add the following scripts to `package.json`:

```json title="package.json"
"scripts": {
  // ...
  "test:integration:http": "TEST_TYPE=integration:http NODE_OPTIONS=--experimental-vm-modules jest --silent=false --runInBand --forceExit",
  "test:integration:modules": "TEST_TYPE=integration:modules NODE_OPTIONS=--experimental-vm-modules jest --silent --runInBand --forceExit",
  "test:unit": "TEST_TYPE=unit NODE_OPTIONS=--experimental-vm-modules jest --silent --runInBand --forceExit"
},
```

You now have two commands:

- `test:integration:http` to run integration tests (for example, for API routes and workflows) available under the `integration-tests/http` directory.
- `test:integration:modules` to run integration tests for modules available in any `__tests__` directory under `src/modules`.
- `test:unit` to run unit tests in any `__tests__` directory under the `src` directory.

Medusa's Testing Framework works for integration tests only. You can write unit tests using Jest.

***

## Test Tools and Writing Tests

The next chapters explain how to use the testing tools provided by `@medusajs/test-utils` to write tests.


# Admin Development

In this chapter, you'll learn about the Medusa Admin dashboard and the possible ways to customize it.

## What is the Medusa Admin?

The Medusa Admin is an intuitive dashboard that allows merchants to manage their ecommerce store. It provides management featuers related to products, orders, customers, and more.

To explore more what you can do with the Medusa Admin, check out the [User Guide](https://docs.medusajs.com/user-guide/index.html.md). These user guides are designed for merchants and provide the steps to perform any task within the Medusa Admin.

The Medusa Admin is built with [Vite](https://vite.dev/). When you [install the Medusa application](https://docs.medusajs.com/learn/installation/index.html.md), you also install the Medusa Admin. Then, when you start the Medusa application, you can access the Medusa Admin at `http://localhost:9000/app`.

If you don't have an admin user, use the [Medusa CLI](https://docs.medusajs.com/resources/medusa-cli/commands/user/index.html.md) to create one.

***

## How to Customize the Medusa Admin?

You can customize the Medusa Admin dashboard by:

- Adding new sections to existing pages using [Widgets](https://docs.medusajs.com/learn/fundamentals/admin/widgets/index.html.md).
- Adding new pages using [UI Routes](https://docs.medusajs.com/learn/fundamentals/admin/ui-routes/index.html.md).

The next chapters will cover these two topics in detail.

### What You Can't Customize in the Medusa Admin

You can't customize the admin dashboard's layout, design, or the content of the existing pages (aside from injecting widgets).

If your use case requires heavy customization of the admin dashboard, you can build a custom admin dashboard using Medusa's [Admin API routes](https://docs.medusajs.com/api/admin).

***

## Medusa UI Package

Medusa provides a Medusa UI package to facilitate your admin development through ready-made components and ensure a consistent design between your customizations and the dashboard’s design.

Refer to the [Medusa UI documentation](https://docs.medusajs.com/ui/index.html.md) to learn how to install it and use its components.

***

## Admin Components List

To build admin customizations that match the Medusa Admin's designs and layouts, refer to [this guide](https://docs.medusajs.com/resources/admin-components/index.html.md) to find common components.


# Custom CLI Scripts

In this chapter, you'll learn how to create and execute custom scripts from Medusa's CLI tool.

## What is a Custom CLI Script?

A custom CLI script is a function to execute through Medusa's CLI tool. This is useful when creating custom Medusa tooling to run through the CLI.

***

## How to Create a Custom CLI Script?

To create a custom CLI script, create a TypeScript or JavaScript file under the `src/scripts` directory. The file must default export a function.

For example, create the file `src/scripts/my-script.ts` with the following content:

```ts title="src/scripts/my-script.ts"
import { 
  ExecArgs,
  IProductModuleService,
} from "@medusajs/framework/types"
import { Modules } from "@medusajs/framework/utils"

export default async function myScript({ container }: ExecArgs) {
  const productModuleService: IProductModuleService = container.resolve(
    Modules.PRODUCT
  )

  const [, count] = await productModuleService
    .listAndCountProducts()

  console.log(`You have ${count} product(s)`)
}
```

The function receives as a parameter an object having a `container` property, which is an instance of the Medusa Container. Use it to resolve resources in your Medusa application.

***

## How to Run Custom CLI Script?

To run the custom CLI script, run the Medusa CLI's `exec` command:

```bash
npx medusa exec ./src/scripts/my-script.ts
```

***

## Custom CLI Script Arguments

Your script can accept arguments from the command line. Arguments are passed to the function's object parameter in the `args` property.

For example:

```ts
import { ExecArgs } from "@medusajs/framework/types"

export default async function myScript({ args }: ExecArgs) {
  console.log(`The arguments you passed: ${args}`)
}
```

Then, pass the arguments in the `exec` command after the file path:

```bash
npx medusa exec ./src/scripts/my-script.ts arg1 arg2
```


# API Routes

In this chapter, you’ll learn what API Routes are and how to create them.

## What is an API Route?

An API Route is an endpoint. It exposes commerce features to external applications, such as storefronts, the admin dashboard, or third-party systems.

The Medusa core application provides a set of admin and store API routes out-of-the-box. You can also create custom API routes to expose your custom functionalities.

***

## How to Create an API Route?

An API Route is created in a TypeScript or JavaScript file under the `src/api` directory of your Medusa application. The file’s name must be `route.ts` or `route.js`.

![Example of API route in the application's directory structure](https://res.cloudinary.com/dza7lstvk/image/upload/v1732808645/Medusa%20Book/route-dir-overview_dqgzmk.jpg)

Each file exports API Route handler functions for at least one HTTP method (`GET`, `POST`, `DELETE`, etc…).

For example, to create a `GET` API Route at `/hello-world`, create the file `src/api/hello-world/route.ts` with the following content:

```ts title="src/api/hello-world/route.ts"
import type {
  MedusaRequest,
  MedusaResponse,
} from "@medusajs/framework/http"

export const GET = (
  req: MedusaRequest,
  res: MedusaResponse
) => {
  res.json({
    message: "[GET] Hello world!",
  })
}
```

### Test API Route

To test the API route above, start the Medusa application:

```bash npm2yarn
npm run dev
```

Then, send a `GET` request to the `/hello-world` API Route:

```bash
curl http://localhost:9000/hello-world
```

***

## When to Use API Routes

You're exposing custom functionality to be used by a storefront, admin dashboard, or any external application.


# Environment Variables

In this chapter, you'll learn how environment variables are loaded in Medusa.

## System Environment Variables

The Medusa application loads and uses system environment variables.

For example, if you set the `PORT` environment variable to `8000`, the Medusa application runs on that port instead of `9000`.

In production, you should always use system environment variables that you set through your hosting provider.

***

## Environment Variables in .env Files

During development, it's easier to set environment variables in a `.env` file in your repository.

Based on your `NODE_ENV` system environment variable, Medusa will try to load environment variables from the following `.env` files:

As of [Medusa v2.5.0](https://github.com/medusajs/medusa/releases/tag/v2.5.0), `NODE_ENV` defaults to `production` when using `medusa start`. Otherwise, it defaults to `development`.

|\`.env\`|
|---|---|
|\`NODE\_ENV\`|\`.env\`|
|\`NODE\_ENV\`|\`.env.production\`|
|\`NODE\_ENV\`|\`.env.staging\`|
|\`NODE\_ENV\`|\`.env.test\`|

### Set Environment in `loadEnv`

In the `medusa-config.ts` file of your Medusa application, you'll find a `loadEnv` function used that accepts `process.env.NODE_ENV` as a first parameter.

This function is responsible for loading the correct `.env` file based on the value of `process.env.NODE_ENV`.

To ensure that the correct `.env` file is loaded as shown in the table above, only specify `development`, `production`, `staging` or `test` as the value of `process.env.NODE_ENV` or as the parameter of `loadEnv`.

***

## Environment Variables for Admin Customizations

Since the Medusa Admin is built on top of [Vite](https://vite.dev/), you prefix the environment variables you want to use in a widget or UI route with `VITE_`. Then, you can access or use them with the `import.meta.env` object.

Learn more in the [Admin Environment Variables](https://docs.medusajs.com/learn/fundamentals/admin/environment-variables/index.html.md) chapter.

***

## Predefined Medusa Environment Variables

The Medusa application uses the following predefined environment variables that you can set:

You should opt for setting configurations in `medusa-config.ts` where possible. For a full list of Medusa configurations, refer to the [Medusa Configurations chapter](https://docs.medusajs.com/learn/configurations/medusa-config/index.html.md).

|Environment Variable|Description|Default|
|---|---|---|
|\`HOST\`|The host to run the Medusa application on.|\`localhost\`|
|\`PORT\`|The port to run the Medusa application on.|\`9000\`|
|\`DATABASE\_URL\`|The URL to connect to the PostgreSQL database. Only used if |\`postgres://localhost/medusa-starter-default\`|
|\`STORE\_CORS\`|URLs of storefronts that can access the Medusa backend's Store APIs. Only used if |\`http://localhost:8000\`|
||URLs of admin dashboards that can access the Medusa backend's Admin APIs. Only used if |\`http://localhost:7000,http://localhost:7001,http://localhost:5173\`|
||URLs of clients that can access the Medusa backend's authentication routes. Only used if |\`http://localhost:7000,http://localhost:7001,http://localhost:5173\`|
||A random string used to create authentication tokens in the http layer. Only used if |-|
|\`COOKIE\_SECRET\`|A random string used to create cookie tokens in the http layer. Only used if |-|
|\`MEDUSA\_BACKEND\_URL\`|The URL to the Medusa backend. Only used if |-|
|\`DB\_HOST\`|The host for the database. It's used when generating migrations for a plugin, and when running integration tests.|\`localhost\`|
|\`DB\_USERNAME\`|The username for the database. It's used when generating migrations for a plugin, and when running integration tests.|-|
|\`DB\_PASSWORD\`|The password for the database user. It's used when generating migrations for a plugin, and when running integration tests.|-|
|\`DB\_TEMP\_NAME\`|The database name to create for integration tests.|-|
|\`LOG\_LEVEL\`|The allowed levels to log. Learn more in the |\`silly\`|
|\`LOG\_FILE\`|The file to save logs in. By default, logs aren't saved in any file. Learn more in the |-|
|\`MEDUSA\_DISABLE\_TELEMETRY\`|Whether to disable analytics data collection. Learn more in the |-|


# Events and Subscribers

In this chapter, you’ll learn about Medusa's event system, and how to handle events with subscribers.

## Handle Core Commerce Flows with Events

When building commerce digital applications, you'll often need to perform an action after a commerce operation is performed. For example, sending an order confirmation email when the customer places an order, or syncing data that's updated in Medusa to a third-party system.

Medusa emits events when core commerce features are performed, and you can listen to and handle these events in asynchronous functions. You can think of Medusa's events like you'd think about webhooks in other commerce platforms, but instead of having to setup separate applications to handle webhooks, your efforts only go into writing the logic right in your Medusa codebase.

You listen to an event in a subscriber, which is an asynchronous function that's executed when its associated event is emitted.

![A diagram showcasing an example of how an event is emitted when an order is placed.](https://res.cloudinary.com/dza7lstvk/image/upload/v1732277948/Medusa%20Book/order-placed-event-example_e4e4kw.jpg)

Subscribers are useful to perform actions that aren't integral to the original flow. For example, you can handle the `order.placed` event in a subscriber that sends a confirmation email to the customer. The subscriber has no impact on the original order-placement flow, as it's executed outside of it.

If the action you're performing is integral to the main flow of the core commerce feature, use [workflow hooks](https://docs.medusajs.com/learn/fundamentals/workflows/workflow-hooks/index.html.md) instead.

### List of Emitted Events

Find a list of all emitted events in [this reference](https://docs.medusajs.com/resources/references/events/index.html.md).

***

## How to Create a Subscriber?

You create a subscriber in a TypeScript or JavaScript file under the `src/subscribers` directory. The file exports the function to execute and the subscriber's configuration that indicate what event(s) it listens to.

For example, create the file `src/subscribers/product-created.ts` with the following content:

![Example of subscriber file in the application's directory structure](https://res.cloudinary.com/dza7lstvk/image/upload/v1732866244/Medusa%20Book/subscriber-dir-overview_pusyeu.jpg)

```ts title="src/subscribers/product-created.ts"
import { SubscriberArgs, type SubscriberConfig } from "@medusajs/framework"
import { sendOrderConfirmationWorkflow } from "../workflows/send-order-confirmation"

export default async function orderPlacedHandler({
  event: { data },
  container,
}: SubscriberArgs<{ id: string }>) {
  const logger = container.resolve("logger")

  logger.info("Sending confirmation email...")

  await sendOrderConfirmationWorkflow(container)
    .run({
      input: {
        id: data.id,
      },
    })
}

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

This subscriber file exports:

- An asynchronous subscriber function that's executed whenever the associated event, which is `order.placed` is triggered.
- A configuration object with an `event` property whose value is the event the subscriber is listening to. You can also pass an array of event names to listen to multiple events in the same subscriber.

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

- `event`: An object with the event's details. The `data` property contains the data payload of the event emitted, which is the order's ID in this case.
- `container`: The [Medusa container](https://docs.medusajs.com/learn/fundamentals/medusa-container/index.html.md) that you can use to resolve registered resources.

In the subscriber function, you use the container to resolve the Logger utility and log a message in the console. Also, assuming you have a [workflow](https://docs.medusajs.com/learn/fundamentals/workflows/index.html.md) that sends an order confirmation email, you execute it in the subscriber.

***

## Test the Subscriber

To test the subscriber, start the Medusa application:

```bash npm2yarn
npm run dev
```

Then, try placing an order either using Medusa's API routes or the [Next.js Starter Storefront](https://docs.medusajs.com/resources/nextjs-starter/index.html.md). You'll see the following message in the terminal:

```bash
info:    Processing order.placed which has 1 subscribers
Sending confirmation email...
```

The first message indicates that the `order.placed` event was emitted, and the second one is the message logged from the subscriber.

***

## Event Module

The subscription and emitting of events is handled by an Event Module, an Infrastructure Module that implements the pub/sub functionalities of Medusa's event system.

Medusa provides two Event Modules out of the box:

- [Local Event Module](https://docs.medusajs.com/resources/infrastructure-modules/event/local/index.html.md), used by default. It's useful for development, as you don't need additional setup to use it.
- [Redis Event Module](https://docs.medusajs.com/resources/infrastructure-modules/event/redis/index.html.md), which is useful in production. It uses [Redis](https://redis.io/) to implement Medusa's pub/sub events system.

Medusa's [architecture](https://docs.medusajs.com/learn/introduction/architecture/index.html.md) also allows you to build a custom Event Module that uses a different service or logic to implement the pub/sub system. Learn how to build an Event Module in [this guide](https://docs.medusajs.com/resources/infrastructure-modules/event/create/index.html.md).


# Medusa Container

In this chapter, you’ll learn about the Medusa container and how to use it.

## What is the Medusa Container?

The Medusa container is a registry of Framework and commerce tools that's accessible across your application. Medusa automatically registers these tools in the container, including custom ones that you've built, so that you can use them in your customizations.

In other platforms, if you have a resource A (for example, a class) that depends on a resource B, you have to manually add resource B to the container or specify it beforehand as A's dependency, which is often done in a file separate from A's code. This becomes difficult to manage as you maintain larger applications with many changing dependencies.

Medusa simplifies this process by giving you access to the container, with the tools or resources already registered, at all times in your customizations. When you reach a point in your code where you need a tool, you resolve it from the container and use it.

For example, consider you're creating an API route that retrieves products based on filters using [Query](https://docs.medusajs.com/learn/fundamentals/module-links/query/index.html.md), a tool that fetches data across the application. In the API route's function, you can resolve Query from the container passed to the API route and use it:

```ts highlights={highlights}
import { MedusaRequest, MedusaResponse } from "@medusajs/framework"
import { ContainerRegistrationKeys } from "@medusajs/framework/utils"

export async function GET(
  req: MedusaRequest,
  res: MedusaResponse
) {
  const query = req.scope.resolve("query")

  const { data: products } = await query.graph({
    entity: "product",
    fields: ["*"],
    filters: {
      id: "prod_123",
    },
  })

  res.json({
    products,
  })
}
```

The API route accepts as a first parameter a request object that has a `scope` property, which is the Medusa container. It has a `resolve` method that resolves a resource from the container by the key it's registered with.

You can learn more about how Query works in [this chapter](https://docs.medusajs.com/learn/fundamentals/module-links/query/index.html.md).

***

## List of Resources Registered in the Medusa Container

Find a full list of the registered resources and their registration key in [this reference](https://docs.medusajs.com/resources/medusa-container-resources/index.html.md)

***

## How to Resolve From the Medusa Container

This section gives quick examples of how to resolve resources from the Medusa container in customizations other than an API route, which is covered in the section above.

### Subscriber

A [subscriber](https://docs.medusajs.com/learn/fundamentals/events-and-subscribers/index.html.md) function, which is executed when an event is emitted, accepts as a parameter an object with a `container` property, whose value is the Medusa container. Use its `resolve` method to resolve a resource by its registration key:

```ts highlights={subscriberHighlights}
import { SubscriberArgs, type SubscriberConfig } from "@medusajs/framework"
import { ContainerRegistrationKeys } from "@medusajs/framework/utils"

export default async function productCreateHandler({
  event: { data },
  container,
}: SubscriberArgs<{ id: string }>) {
  const query = container.resolve(ContainerRegistrationKeys.QUERY)

  const { data: products } = await query.graph({
    entity: "product",
    fields: ["*"],
    filters: {
      id: data.id,
    },
  })

  console.log(`You created a product with the title ${products[0].title}`)
}

export const config: SubscriberConfig = {
  event: `product.created`,
}
```

### Scheduled Job

A [scheduled job](https://docs.medusajs.com/learn/fundamentals/scheduled-jobs/index.html.md) function, which is executed at a specified interval, accepts the Medusa container as a parameter. Use its `resolve` method to resolve a resource by its registration key:

```ts highlights={scheduledJobHighlights}
import { MedusaContainer } from "@medusajs/framework/types"
import { ContainerRegistrationKeys } from "@medusajs/framework/utils"

export default async function myCustomJob(
  container: MedusaContainer
) {
  const query = container.resolve(ContainerRegistrationKeys.QUERY)

  const { data: products } = await query.graph({
    entity: "product",
    fields: ["*"],
    filters: {
      id: "prod_123",
    },
  })

  console.log(
    `You have ${products.length} matching your filters.`
  )
}

export const config = {
  name: "every-minute-message",
  // execute every minute
  schedule: "* * * * *",
}
```

### Workflow Step

A [step in a workflow](https://docs.medusajs.com/learn/fundamentals/workflows/index.html.md), which is a special function where you build durable execution logic across multiple modules, accepts in its second parameter a `container` property, whose value is the Medusa container. Use its `resolve` method to resolve a resource by its registration key:

```ts highlights={workflowStepsHighlight}
import {
  createStep,
  StepResponse,
} from "@medusajs/framework/workflows-sdk"
import { ContainerRegistrationKeys } from "@medusajs/framework/utils"

const step1 = createStep("step-1", async (_, { container }) => {
  const query = container.resolve(ContainerRegistrationKeys.QUERY)

  const { data: products } = await query.graph({
    entity: "product",
    fields: ["*"],
    filters: {
      id: "prod_123",
    },
  })

  return new StepResponse(products)
})
```

### Module Services and Loaders

A [module](https://docs.medusajs.com/learn/fundamentals/modules/index.html.md), which is a package of functionalities for a single feature or domain, has its own container, so it can't resolve resources from the Medusa container.

Learn more about the module's container in [this chapter](https://docs.medusajs.com/learn/fundamentals/modules/container/index.html.md).


# Framework Overview

In this chapter, you'll learn about the Medusa Framework and how it facilitates building customizations in your Medusa application.

## What is the Medusa Framework?

All commerce application require some degree of customization. So, it's important to choose a platform that facilitates building those customizations.

When you build customizations with other ecommerce platforms, they require you to pull data through HTTP APIs, run custom logic that span across systems in a separate application, and manually ensure data consistency across systems. This adds significant overhead and slows down development as you spend time managing complex distributed systems.

The Medusa Framework eliminates this overhead by providing powerful low-level APIs and tools that let you build any type of customization directly within your Medusa project. You can build custom features, orchestrate operations and query data seamlessy across systems, extend core functionality, and automate tasks in your Medusa application.

With the Medusa Framework, you can focus your efforts on building meaningful business customizations and continuously delivering new features.

Using the Medusa Framework, you can build customizations like:

- [Product Reviews](https://docs.medusajs.com/resources/how-to-tutorials/tutorials/product-reviews/index.html.md)
- [Deep integration with an ERP system](https://docs.medusajs.com/resources/recipes/erp/index.html.md)
- [CMS integration with seamless content retrieval](https://docs.medusajs.com/resources/integrations/guides/sanity/index.html.md)
- [Custom item pricing in the cart](https://docs.medusajs.com/resources/examples/guides/custom-item-price/index.html.md)
- [Automated restock notifications](https://docs.medusajs.com/resources/recipes/commerce-automation/restock-notification/index.html.md)
- [Re-usable payment provider integrations](https://docs.medusajs.com/resources/references/payment/provider/index.html.md)

### Framework Concepts and Tools

- [Medusa Container](https://docs.medusajs.com/learn/fundamentals/medusa-container/index.html.md)
- [Modules](https://docs.medusajs.com/learn/fundamentals/modules/index.html.md)
- [Module Links](https://docs.medusajs.com/learn/fundamentals/module-links/index.html.md)
- [Query](https://docs.medusajs.com/learn/fundamentals/module-links/query/index.html.md)
- [Data Models](https://docs.medusajs.com/learn/fundamentals/data-models/index.html.md)
- [Workflows](https://docs.medusajs.com/learn/fundamentals/workflows/index.html.md)
- [API Routes](https://docs.medusajs.com/learn/fundamentals/api-routes/index.html.md)
- [Events and Subscribers](https://docs.medusajs.com/learn/fundamentals/events-and-subscribers/index.html.md)
- [Scheduled Jobs](https://docs.medusajs.com/learn/fundamentals/scheduled-jobs/index.html.md)
- [Plugins](https://docs.medusajs.com/learn/fundamentals/plugins/index.html.md)

***

## Build Custom Features

The Medusa Framework allows you to build custom features tailored to your business needs.

To create a custom feature, you can create a [module](https://docs.medusajs.com/learn/fundamentals/modules/index.html.md) that contains your feature's data models and the logic to manage them. A module is integrated into your Medusa application without side effects.

### Data Model

```ts highlights={modelHighlights}
import { model } from "@medusajs/framework/utils"

export const Post = model.define("post", {
  id: model.id().primaryKey(),
  title: model.text(),
})
```

### Service

```ts highlights={serviceHighlights}
import { MedusaService } from "@medusajs/framework/utils"
import { Post } from "./post"

export class BlogModuleService extends MedusaService({
  Post,
}){
  // CRUD methods generated by MedusaService
}
```

### Module Definition

```ts
import { Module } from "@medusajs/framework/utils"
import { BlogModuleService } from "./service"

export const BLOG_MODULE = "blog"

export default Module(BLOG_MODULE, {
  service: BlogModuleService,
})
```

Then, you can build commerce features and flows in [workflows](https://docs.medusajs.com/learn/fundamentals/workflows/index.html.md) that use your module. By using workflows, you benefit from features like [rollback mechanism](https://docs.medusajs.com/learn/fundamentals/workflows/compensation-function/index.html.md) and [retry configuration](https://docs.medusajs.com/learn/fundamentals/workflows/retry-failed-steps/index.html.md).

### Step

```ts highlights={stepHighlights}
import { createStep, StepResponse } from "@medusajs/framework/workflows-sdk"
import { BlogModuleService, BLOG_MODULE } from "../../modules/blog"

type Input = {
  title: string
}

const createPostStep = createStep(
  "create-post", 
  async (input: Input, { container }) => {
    const blogModuleService: BlogModuleService = container.resolve(
      BLOG_MODULE
    )

    const post = await blogModuleService.createPosts(input.title)
    
    return new StepResponse(post, post.id)
  },
  async (postId, { container }) => {
    if (!postId) {
      return
    }

    const blogModuleService: BlogModuleService = container.resolve(
      BLOG_MODULE
    )

    await blogModuleService.deletePosts(postId)
  }
)
```

### Workflow

```ts
import { createWorkflow, WorkflowResponse } from "@medusajs/framework/workflows-sdk"
import { createPostStep } from "./steps"

type Input = {
  title: string
}

export const createPostWorkflow = createWorkflow(
  "create-post",
  (input: Input) => {
    const post = createPostStep(input)

    return new WorkflowResponse(post)
  }
)
```

Finally, you can expose your custom feature with [API routes](https://docs.medusajs.com/learn/fundamentals/api-routes/index.html.md) that are built on top of your module and workflows.

```ts title="API Route Example"
import type {
  MedusaRequest,
  MedusaResponse,
} from "@medusajs/framework/http"
import { createPostWorkflow } from "../../../workflows/create-post"

type PostRequestBody = {
  title: string
}

export const POST = async (
  req: MedusaRequest<PostRequestBody>,
  res: MedusaResponse
) => {
  const { result } = await createPostWorkflow(req.scope)
    .run({
      input: result.validatedBody,
    })

  return res.json(result)
}
```

### Examples

The following tutorials are step-by-step guides that show you how to build custom features using the Medusa Framework.

- [Product Reviews](https://docs.medusajs.com/resources/how-to-tutorials/tutorials/product-reviews/index.html.md): Build a product reviews feature in your Medusa application.
- [Wishlist](https://docs.medusajs.com/resources/plugins/guides/wishlist/index.html.md): Build a wishlist feature in your Medusa application.

### Start Learning

To learn more about the different concepts useful for building custom features, check out the following chapters:

- [Modules](https://docs.medusajs.com/learn/fundamentals/modules/index.html.md)
- [Workflows](https://docs.medusajs.com/learn/fundamentals/workflows/index.html.md)
- [API Routes](https://docs.medusajs.com/learn/fundamentals/api-routes/index.html.md)

***

## Extend Existing Features

The Medusa Framework is flexible and extensible, allowing you to extend and build on top of existing models and features.

To associate new properties and relations with an existing model, you can create a [module](https://docs.medusajs.com/learn/fundamentals/modules/index.html.md) with data models that define these additions. Then, you can define a [module link](https://docs.medusajs.com/learn/fundamentals/module-links/index.html.md) that associates two data models from separate modules.

### Module Link

```ts highlights={defineLinkHighlights}
import BrandModule from "../modules/brand"
import ProductModule from "@medusajs/medusa/product"
import { defineLink } from "@medusajs/framework/utils"

export default defineLink(
  {
    linkable: ProductModule.linkable.product,
    isList: true,
  },
  BrandModule.linkable.brand
)
```

### Data Model

```ts
import { model } from "@medusajs/framework/utils"

export const Brand = model.define("brand", {
  id: model.id().primaryKey(),
  name: model.text(),
})
```

### Service

```ts
import { MedusaService } from "@medusajs/framework/utils"
import { Brand } from "./models/brand"

class BrandModuleService extends MedusaService({
  Brand,
}) {

}

export default BrandModuleService
```

### Module Definition

```ts
import { Module } from "@medusajs/framework/utils"
import BrandModuleService from "./service"

export const BRAND_MODULE = "brand"

export default Module(BRAND_MODULE, {
  service: BrandModuleService,
})
```

Then, you can [hook into existing workflows](https://docs.medusajs.com/learn/fundamentals/workflows/workflow-hooks/index.html.md) to perform custom actions as part of existing features and flows. For example, you can create a brand when a product is created.

```ts title="Workflow Hook Example" highlights={hookHighlights}
import { createProductsWorkflow } from "@medusajs/medusa/core-flows"
import { StepResponse } from "@medusajs/framework/workflows-sdk"
import { Modules } from "@medusajs/framework/utils"
import { LinkDefinition } from "@medusajs/framework/types"
import { BRAND_MODULE } from "../../modules/brand"
import BrandModuleService from "../../modules/brand/service"

createProductsWorkflow.hooks.productsCreated(
  (async ({ products, additional_data }, { container }) => {
    if (!additional_data?.brand_id) {
      return new StepResponse([], [])
    }

    const brandModuleService: BrandModuleService = container.resolve(
      BRAND_MODULE
    )
    
    const brand = await brandModuleService.createBrands({
      name: additional_data.brand_name,
    })
  })
)
```

You can also build custom workflows using your custom module and Medusa's modules, and use [existing workflows and steps](https://docs.medusajs.com/resources/medusa-workflows-reference/index.html.md) within your custom workflows.

### Examples

The following tutorials are step-by-step guides that show you how to extend existing features using the Medusa Framework.

- [Custom Item Pricing](https://docs.medusajs.com/resources/examples/guides/custom-item-price/index.html.md): Add products with custom items to the cart.
- [Loyalty Points System](https://docs.medusajs.com/resources/how-to-tutorials/tutorials/loyalty-points/index.html.md): Reward and allow customers to redeem loyalty points.

### Start Learning

To learn more about the different concepts useful for extending features, check out the following chapters:

- [Modules](https://docs.medusajs.com/learn/fundamentals/modules/index.html.md)
- [Module Links](https://docs.medusajs.com/learn/fundamentals/module-links/index.html.md)
- [Workflow Hooks](https://docs.medusajs.com/learn/fundamentals/workflows/workflow-hooks/index.html.md)

***

## Integrate Third-Party Services

The Medusa Framework provides the tools and infrastructure to build a middleware solution for your commerce ecosystem. You can integrate third-party services, perform operations across systems, and query data from multiple sources.

### Orchestrate Operations Across Systems

The Medusa Framework solves one of the biggest hurdles for ecommerce platforms: orchestrating operations across systems. Medusa has a built-in durable execution engine to help complete tasks that span multiple systems.

You can integrate a third-party service in a [module](https://docs.medusajs.com/learn/fundamentals/modules/index.html.md). This module provides an interface to perform operations with the third-party service.

### Service

```ts highlights={erpServiceHighlights}
type Options = {
  apiKey: string
}

export default class ErpModuleService {
  private options: Options
  private client

  constructor({}, options: Options) {
    this.options = options
    // TODO initialize client that connects to ERP
  }

  async getProducts() {
    // assuming client has a method to fetch products
    return this.client.getProducts()
  }

  // TODO add more methods
}
```

### Module Definition

```ts
import { Module } from "@medusajs/framework/utils"
import ErpModuleService from "./service"

export const ERP_MODULE = "erp"

export default Module(ERP_MODULE, {
  service: ErpModuleService,
})
```

Then, you can build [workflows](https://docs.medusajs.com/learn/fundamentals/workflows/index.html.md) that perform operations across systems. In the workflow, you can use your module to interact with the integrated third-party service.

For example, you can create a workflow that syncs products from your ERP system to your Medusa application.

### Workflow

```ts highlights={erpWorkflowHighlights}
import { 
  createWorkflow, 
  WorkflowResponse,
  transform,
} from "@medusajs/framework/workflows-sdk"
import { createProductsWorkflow } from "@medusajs/medusa/core-flows"

export const syncFromErpWorkflow = createWorkflow(
  "sync-from-erp",
  () => {
    const erpProducts = getProductsFromErpStep()

    const productsToCreate = transform({
      erpProducts,
    }, (data) => {
      // TODO prepare ERP products to be created in Medusa
      return data.erpProducts.map((erpProduct) => {
        return {
          title: erpProduct.title,
          external_id: erpProduct.id,
          variants: erpProduct.variants.map((variant) => ({
            title: variant.title,
            metadata: {
              external_id: variant.id,
            },
          })),
          // other data...
        }
      })
    })

    createProductsWorkflow.runAsStep({
      input: {
        products: productsToCreate,
      },
    })

    return new WorkflowResponse({
      erpProducts,
    })
  }
)
```

### Step

```ts
import { createStep, StepResponse } from "@medusajs/framework/workflows-sdk"
import { ERP_MODULE } from "../../modules/erp"
import { ErpModuleService } from "../../modules/erp/service"

const getProductsFromErpStep = createStep(
  "get-products-from-erp",
  async (_, { container }) => {
    const erpModuleService: ErpModuleService = container.resolve(
      ERP_MODULE
    )

    const products = await erpModuleService.getProducts()

    return new StepResponse(products)
  }
)
```

By using a workflow to manage operations across systems, you benefit from features like [rollback mechanism](https://docs.medusajs.com/learn/fundamentals/workflows/compensation-function/index.html.md), [background long-running execution](https://docs.medusajs.com/learn/fundamentals/workflows/long-running-workflow/index.html.md), [retry configuration](https://docs.medusajs.com/learn/fundamentals/workflows/retry-failed-steps/index.html.md), and more. This is essential for building a middleware solution that performs operations across systems, as you don't have to worry about data inconsistencies or failures.

You can then execute this workflow at a specific interval using [scheduled jobs](https://docs.medusajs.com/learn/fundamentals/scheduled-jobs/index.html.md) or when an event occurs using [events and subscribers](https://docs.medusajs.com/learn/fundamentals/events-and-subscribers/index.html.md). You can also expose its features to client applications using an [API route](https://docs.medusajs.com/learn/fundamentals/api-routes/index.html.md).

### Scheduled Job

```ts highlights={syncProductsJobHighlights}
import {
  MedusaContainer,
} from "@medusajs/framework/types"
import { syncFromErpWorkflow } from "../workflows/sync-from-erp"

export default async function syncProductsJob(container: MedusaContainer) {
  await syncFromErpWorkflow(container).run({})
}

export const config = {
  name: "daily-product-sync",
  schedule: "0 0 * * *", // Every day at midnight
}
```

### Event Subscriber

```ts highlights={productsCreatedHandlerHighlights}
import { SubscriberArgs, type SubscriberConfig } from "@medusajs/framework"
import { sendOrderConfirmationWorkflow } from "../workflows/send-order-confirmation"

export default async function productsCreatedHandler({
  event: { data },
  container,
}: SubscriberArgs<{ id: string }[]>) {
  await syncFromErpWorkflow(container).run({})
}

export const config: SubscriberConfig = {
  event: `product.created`,
}
```

### API Route

```ts highlights={apiRouteHighlights}
import type {
  MedusaRequest,
  MedusaResponse,
} from "@medusajs/framework/http"
import { syncFromErpWorkflow } from "../../../workflows/sync-from-erp"

export const POST = async (
  req: MedusaRequest,
  res: MedusaResponse
) => {
  const { result } = await syncFromErpWorkflow(req.scope).run({})

  return res.status(200).json(result)
}
```

#### Examples

The following tutorials are step-by-step guides that show you how to orchestrate operations across third-party services using the Medusa Framework.

- [Migrate Data from Magento](https://docs.medusajs.com/resources/integrations/guides/magento/index.html.md): Migrate data from Magento to your Medusa application.
- [Integrate Third-Party Services](https://docs.medusajs.com/resources/integrations/index.html.md): Integrate CMS, Fulfillment, Payment, and other third-party services.

#### Start Learning

To learn more about the different concepts useful for integrating third-party services, check out the following chapters:

- [Modules](https://docs.medusajs.com/learn/fundamentals/modules/index.html.md)
- [Workflows](https://docs.medusajs.com/learn/fundamentals/workflows/index.html.md)
- [API Routes](https://docs.medusajs.com/learn/fundamentals/api-routes/index.html.md)
- [Events and Subscribers](https://docs.medusajs.com/learn/fundamentals/events-and-subscribers/index.html.md)
- [Scheduled Jobs](https://docs.medusajs.com/learn/fundamentals/scheduled-jobs/index.html.md)

### Query Data Across Systems

Another essential feature for integrating third-party services is querying data across those systems efficiently.

The Framework allows you to build links not only between Medusa data models, but also virtual data models using [read-only module links](https://docs.medusajs.com/learn/fundamentals/module-links/read-only/index.html.md). You can build a [module](https://docs.medusajs.com/learn/fundamentals/modules/index.html.md) that provides the logic to query data from a third-party service, then create a read-only link between an existing data model and a virtual one from the third-party service.

### Read-Only Link

```ts highlights={readOnlyLinkHighlights}
import BrandModule from "../modules/brand"
import ProductModule from "@medusajs/medusa/product"
import { defineLink } from "@medusajs/framework/utils"

export default defineLink(
  {
    linkable: ProductModule.linkable.product,
    field: "id",
  },
  {
    ...BrandModule.linkable.brand.id,
    primaryKey: "product_id",
  },
  {
    readOnly: true,
  }
)
```

### Module Service

```ts highlights={brandModuleService}
type BrandModuleOptions = {
  apiKey: string
}

export default class BrandModuleService {
  private client

  constructor({}, options: BrandModuleOptions) {
    this.client = new Client(options)
  }

  async list(
    filter: {
      id: string | string[]
    }
  ) {
    return this.client.getBrands(filter)
    /**
     - Example of returned data:
     - 
     - [
     -   {
     -     "id": "brand_123",
     -     "name": "Brand 123",
     -     "product_id": "prod_321"
     -   },
     -   {
     -     "id": "post_456",
     -     "name": "Brand 456",
     -     "product_id": "prod_654"
     -   }
     - ]
    */
  }
}
```

### Module Definition

```ts
import { Module } from "@medusajs/framework/utils"
import BrandModuleService from "./service"

export const BRAND_MODULE = "brand"

export default Module(BRAND_MODULE, {
  service: BrandModuleService,
})
```

Then, you can use [Query](https://docs.medusajs.com/learn/fundamentals/module-links/query/index.html.md) to retrieve a product and its brand from the third-party service in a single query.

```ts title="Query Example" highlights={queryHighlights}
const { result } = await query.graph({
  entity: "product",
  fields: ["id", "brand.*"],
  filters: {
    id: "prod_123",
  },
})

// result = [{
//   id: "prod_123",
//   brand: {
//     id: "brand_123",
//     name: "Brand 123",
//     product_id: "prod_123"
//   }
//   ...
// }]
```

Query simplifies the process of retrieving data across systems, as you can retrieve data from multiple sources in a single query.

#### Examples

The following tutorials are step-by-step guides that show you how to query data across systems using the Medusa Framework.

- [Integrate Sanity CMS](https://docs.medusajs.com/resources/integrations/guides/sanity/index.html.md): Query data from third-party services using read-only links.

#### Start Learning

To learn more about the different concepts useful for querying data across systems, check out the following chapters:

- [Modules](https://docs.medusajs.com/learn/fundamentals/modules/index.html.md)
- [Read-Only Links](https://docs.medusajs.com/learn/fundamentals/module-links/read-only/index.html.md)
- [Query](https://docs.medusajs.com/learn/fundamentals/module-links/query/index.html.md)

***

## Automate Tasks

The Medusa Framework provides the tools to automate tasks in your Medusa application. Automation is useful when you want to perform a task periodically, such as syncing data, or when an event occurs, such as sending a confirmation email when an order is placed.

To build the task to be automated, you first create a [workflow](https://docs.medusajs.com/learn/fundamentals/workflows/index.html.md) that contains the task's logic, such as syncing data or sending an email.

### Step

```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)
  }
)
```

### Workflow

```ts
import { 
  createWorkflow, 
  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) => {
    // @ts-ignore
    const { data: orders } = useQueryGraphStep({
      entity: "order",
      fields: [
        "id",
        "email",
        "currency_code",
        "total",
        "items.*",
      ],
      filters: {
        id,
      },
    })
    
    const notification = sendNotificationStep([{
      to: orders[0].email,
      channel: "email",
      template: "order-placed",
      data: {
        order: orders[0],
      },
    }])

    return new WorkflowResponse(notification)
  }
)
```

Then, you can execute this workflow when an event occurs using a [subscriber](https://docs.medusajs.com/learn/fundamentals/events-and-subscribers/index.html.md), or at a specific interval using a [scheduled job](https://docs.medusajs.com/learn/fundamentals/scheduled-jobs/index.html.md).

### Event Subscriber

```ts highlights={orderPlacedHandlerHighlights}
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",
}
```

### Scheduled Job

```ts highlights={orderConfirmationJobHighlights}
import type {
  MedusaContainer,
} from "@medusajs/framework/types"
import { sendOrderConfirmationWorkflow } from "../workflows/send-order-confirmation"

export default async function orderConfirmationJob(
  container: MedusaContainer
) {
  await sendOrderConfirmationWorkflow(container).run({
    input: {
      id: "order_123",
    },
  })
}
export const config = {
  name: "order-confirmation-job",
  schedule: "0 0 * * *", // Every day at midnight
}
```

### Examples

The following guides are step-by-step guides that show you how to automate tasks using the Medusa Framework.

- [Restock Notifications](https://docs.medusajs.com/resources/recipes/commerce-automation/restock-notification/index.html.md): Send restock notifications to customers when a product is back in stock.
- [Sync Data from and to ERP](https://docs.medusajs.com/resources/recipes/erp#sync-products-from-erp/index.html.md): Sync data between your Medusa application and an ERP system.

### Start Learning

To learn more about the different concepts useful for automating tasks, check out the following chapters:

- [Workflows](https://docs.medusajs.com/learn/fundamentals/workflows/index.html.md)
- [Events and Subscribers](https://docs.medusajs.com/learn/fundamentals/events-and-subscribers/index.html.md)
- [Scheduled Jobs](https://docs.medusajs.com/learn/fundamentals/scheduled-jobs/index.html.md)

***

## Re-Use Customizations Across Applications

If you have custom features that you want to re-use across multiple Medusa applications, or you want to publish your customizations for the community to use, you can build a [plugin](https://docs.medusajs.com/learn/fundamentals/plugins/index.html.md).

A plugin encapsulates your customizations in a single package. The customizations include [modules](https://docs.medusajs.com/learn/fundamentals/modules/index.html.md), [workflows](https://docs.medusajs.com/learn/fundamentals/workflows/index.html.md), [API routes](https://docs.medusajs.com/learn/fundamentals/api-routes/index.html.md), and more.

![Diagram showcasing a wishlist plugin installed in a Medusa application](https://res.cloudinary.com/dza7lstvk/image/upload/v1737540762/Medusa%20Book/plugin-diagram_oepiis.jpg)

You can then publish that plugin to NPM and install it in any Medusa application. This allows you to re-use your customizations efficiently across multiple projects, or share them with the community.

### Examples

The following tutorials are step-by-step guides that show you how to build plugins using the Medusa Framework.

- [Wishlist Plugin](https://docs.medusajs.com/resources/plugins/guides/wishlist/index.html.md): Build a wishlist plugin for your Medusa application.
- [Migrate Data from Magento Plugin](https://docs.medusajs.com/resources/integrations/guides/magento/index.html.md): Build a plugin that migrates data from Magento to your Medusa application.

### Start Learning

To learn more about the different concepts useful for building plugins, check out the following chapters:

- [Plugins](https://docs.medusajs.com/learn/fundamentals/plugins/index.html.md)


# Plugins

In this chapter, you'll learn what a plugin is in Medusa.

Plugins are available starting from [Medusa v2.3.0](https://github.com/medusajs/medusa/releases/tag/v2.3.0).

## What is a Plugin?

A plugin is a package of reusable Medusa customizations that you can install in any Medusa application. The supported customizations are [Modules](https://docs.medusajs.com/learn/fundamentals/modules/index.html.md), [API Routes](https://docs.medusajs.com/learn/fundamentals/api-routes/index.html.md), [Workflows](https://docs.medusajs.com/learn/fundamentals/workflows/index.html.md), [Workflow Hooks](https://docs.medusajs.com/learn/fundamentals/workflows/workflow-hooks/index.html.md), [Links](https://docs.medusajs.com/learn/fundamentals/module-links/index.html.md), [Subscribers](https://docs.medusajs.com/learn/fundamentals/events-and-subscribers/index.html.md), [Scheduled Jobs](https://docs.medusajs.com/learn/fundamentals/scheduled-jobs/index.html.md), and [Admin Extensions](https://docs.medusajs.com/learn/fundamentals/admin/index.html.md).

Plugins allow you to reuse your Medusa customizations across multiple projects or share them with the community. They can be published to npm and installed in any Medusa project.

![Diagram showcasing a wishlist plugin installed in a Medusa application](https://res.cloudinary.com/dza7lstvk/image/upload/v1737540762/Medusa%20Book/plugin-diagram_oepiis.jpg)

Learn how to create a wishlist plugin in [this guide](https://docs.medusajs.com/resources/plugins/guides/wishlist/index.html.md).

***

## Plugin vs Module

A [module](https://docs.medusajs.com/learn/fundamentals/modules/index.html.md) is an isolated package related to a single domain or functionality, such as product reviews or integrating a Content Management System. A module can't access any resources in the Medusa application that are outside its codebase.

A plugin, on the other hand, can contain multiple Medusa customizations, including modules. Your plugin can define a module, then build flows around it.

For example, in a plugin, you can define a module that integrates a third-party service, then add a workflow that uses the module when a certain event occurs to sync data to that service.

- You want to reuse your Medusa customizations across multiple projects.
- You want to share your Medusa customizations with the community.

- You want to build a custom feature related to a single domain or integrate a third-party service. Instead, use a [module](https://docs.medusajs.com/learn/fundamentals/modules/index.html.md). You can wrap that module in a plugin if it's used in other customizations, such as if it has a module link or it's used in a workflow.

***

## How to Create a Plugin?

The next chapter explains how you can create and publish a plugin.


# Data Models

In this chapter, you'll learn what a data model is and how to create a data model.

## What is a Data Model?

A data model represents a table in the database. You create data models using Medusa's data modeling language (DML). It simplifies defining a table's columns, relations, and indexes with straightforward methods and configurations.

You create a data model in a [module](https://docs.medusajs.com/learn/fundamentals/modules/index.html.md). The module's service provides the methods to store and manage those data models. Then, you can resolve the module's service in other customizations, such as a [workflow](https://docs.medusajs.com/learn/fundamentals/workflows/index.html.md), to manage the data models' records.

***

## How to Create a Data Model

In a module, you can create a data model in a TypeScript or JavaScript file under the module's `models` directory.

So, for example, assuming you have a Blog Module at `src/modules/blog`, you can create a `Post` data model by creating the `src/modules/blog/models/post.ts` file with the following content:

![Updated directory overview after adding the data model](https://res.cloudinary.com/dza7lstvk/image/upload/v1732806790/Medusa%20Book/blog-dir-overview-1_jfvovj.jpg)

```ts title="src/modules/blog/models/post.ts"
import { model } from "@medusajs/framework/utils"

const Post = model.define("post", {
  id: model.id().primaryKey(),
  title: model.text(),
})

export default Post
```

You define the data model using the `define` method of the DML. It accepts two parameters:

1. The first one is the name of the data model's table in the database. Use snake-case names.
2. The second is an object, which is the data model's schema. The schema's properties are defined using the `model`'s methods, such as `text` and `id`.
   - Data models automatically have the date properties `created_at`, `updated_at`, and `deleted_at`, so you don't need to add them manually.

The code snippet above defines a `Post` data model with `id` and `title` properties.

***

## Generate Migrations

After you create a data model in a module, then [register that module in your Medusa configurations](https://docs.medusajs.com/learn/fundamentals/modules#4-add-module-to-medusas-configurations/index.html.md), you must generate a migration to create the data model's table in the database.

A migration is a TypeScript or JavaScript file that defines database changes made by a module. Migrations are useful when you re-use a module or you're working in a team, so that when one member of a team makes a database change, everyone else can reflect it on their side by running the migrations.

For example, to generate a migration for the Blog Module, run the following command in your Medusa application's directory:

If you're creating the module in a plugin, use the [plugin:db:generate command](https://docs.medusajs.com/resources/medusa-cli/commands/plugin#plugindbgenerate/index.html.md) instead.

```bash
npx medusa db:generate blog
```

The `db:generate` command of the Medusa CLI accepts one or more module names to generate the migration for. It will create a migration file for the Blog Module in the directory `src/modules/blog/migrations` similar to the following:

```ts
import { Migration } from "@mikro-orm/migrations"

export class Migration20241121103722 extends Migration {

  async up(): Promise<void> {
    this.addSql("create table if not exists \"post\" (\"id\" text not null, \"title\" text not null, \"created_at\" timestamptz not null default now(), \"updated_at\" timestamptz not null default now(), \"deleted_at\" timestamptz null, constraint \"post_pkey\" primary key (\"id\"));")
  }

  async down(): Promise<void> {
    this.addSql("drop table if exists \"post\" cascade;")
  }

}
```

In the migration class, the `up` method creates the table `post` and defines its columns using PostgreSQL syntax. The `down` method drops the table.

### Run Migrations

To reflect the changes in the generated migration file on the database, run the `db:migrate` command:

If you're creating the module in a plugin, run this command on the Medusa application that the plugin is installed in.

```bash
npx medusa db:migrate
```

This creates the `post` table in the database.

### Migrations on Data Model Changes

Whenever you make a change to a data model, you must generate and run the migrations.

For example, if you add a new column to the `Post` data model, you must generate a new migration and run it.

***

## Manage Data Models

Your module's service should extend the [service factory](https://docs.medusajs.com/learn/fundamentals/modules/service-factory/index.html.md), which generates data-management methods for your module's data models.

For example, the Blog Module's service would have methods like `retrievePost` and `createPosts`.

Refer to the [Service Factory](https://docs.medusajs.com/learn/fundamentals/modules/service-factory/index.html.md) chapter to learn more about how to extend the service factory and manage data models, and refer to the [Service Factory Reference](https://docs.medusajs.com/resources/service-factory-reference/index.html.md) for the full list of generated methods and how to use them.


# Define Module Link

In this chapter, you’ll learn what a module link is and how to define one.

## What is a Module Link?

Medusa's modular architecture isolates modules from one another to ensure they can be integrated into your application without side effects. Module isolation has other benefits, which you can learn about in the [Module Isolation chapter](https://docs.medusajs.com/learn/fundamentals/modules/isolation/index.html.md). Since modules are isolated, you can't access another module's data models to add a relation to it or extend it. Instead, you use a module link.

A module link forms an association between two data models of different modules while maintaining module isolation. Using module links, you can build virtual relations between your custom data models and data models in the Commerce Modules, which is useful as you extend the features provided by the Commerce Modules. Then, Medusa creates a link table in the database to store the IDs of the linked records. You'll learn more about link tables later in this chapter.

For example, the [Brand Customizations Tutorial](https://docs.medusajs.com/learn/customization/extend-features/index.html.md) shows how to create a Brand Module that adds the concept of brands to your application, then link those brands to a product.

***

## How to Define a Module Link?

### 1. Create Link File

Module links are defined in a TypeScript or JavaScript file under the `src/links` directory. The file defines the link using `defineLink` from the Modules SDK and exports it.

For example:

```ts title="src/links/blog-product.ts" highlights={highlights}
import BlogModule from "../modules/blog"
import ProductModule from "@medusajs/medusa/product"
import { defineLink } from "@medusajs/framework/utils"

export default defineLink(
  ProductModule.linkable.product,
  BlogModule.linkable.post
)
```

The `defineLink` function accepts as parameters the link configurations of each module's data model. A module has a special `linkable` property that holds these configurations for its data models.

In this example, you define a module link between the `blog` module's `post` data model and the Product Module's `Product` data model.

### 2. Sync Links

After defining the link, run the `db:sync-links` command:

```bash
npx medusa db:sync-links
```

The Medusa application creates a new table for your module link to store the IDs of linked records.

You can also use the `db:migrate` command, which runs both the migrations and syncs the links.

Use either of these commands whenever you make changes to your link definitions. For example, run this command if you remove your link definition file.

***

### Module Link's Database Table

When you define a module link, the Medusa application creates a table in the database for that module link. The table's name is a combination of the names of the two data models linked in the format `module1_table1_module2_table2`, where:

- `module1` and `module2` are the names of the modules.
- `table1` and `table2` are the table names of the data models.

For example, if you define a link between the `Product` data model from the [Product Module](https://docs.medusajs.com/resources/commerce-modules/product/index.html.md) and a `Post` data model from a Blog Module, the table name would be `product_product_blog_post`.

The table has two columns, each storing the ID of a record from the linked data models. For example, the `product_product_blog_post` table would have columns `product_id` and `post_id`. These columns store only the IDs of the linked records and do not hold a foreign key constraint.

Then, when you create links between records of the data models, the IDs of these data models are stored as a new record in the link's table.

You can also add custom columns in the link table as explained in the [Add Columns to Link Table chapter](https://docs.medusajs.com/learn/fundamentals/module-links/custom-columns/index.html.md).

![Diagram illustration for module links](https://res.cloudinary.com/dza7lstvk/image/upload/v1741696766/Medusa%20Book/custom-links_vezsx8.jpg)

***

## When to Use Module Links

- You want to create a relation between data models from different modules.
- You want to extend the data model of another module.

You want to create a relationship between data models in the same module. Use data model relationships instead.

***

## Define a List Module Link

By default, a module link establishes a one-to-one relation: a record of a data model is linked to one record of the other data model.

To specify that a data model can have multiple of its records linked to the other data model's record, use the `isList` option.

For example:

```ts
import BlogModule from "../modules/blog"
import ProductModule from "@medusajs/medusa/product"
import { defineLink } from "@medusajs/framework/utils"

export default defineLink(
  ProductModule.linkable.product,
  {
    linkable: BlogModule.linkable.post,
    isList: true,
  }
)
```

In this case, you pass an object of configuration as a parameter instead. The object accepts the following properties:

- `linkable`: The data model's link configuration.
- `isList`: Whether multiple records can be linked to one record of the other data model.

In this example, a record of `product` can be linked to more than one record of `post`.

### Many-to-Many Module Link

Your module link can also establish a many-to-many relation between the linked data models. To do this, enable `isList` on both sides of the link.

For example:

```ts
import BlogModule from "../modules/blog"
import ProductModule from "@medusajs/medusa/product"
import { defineLink } from "@medusajs/framework/utils"

export default defineLink(
  {
    linkable: ProductModule.linkable.product,
    isList: true,
  },
  {
    linkable: BlogModule.linkable.post,
    isList: true,
  }
)
```

***

## Set Delete Cascades on Link

To enable delete cascade on a link so that when a record is deleted, its linked records are also deleted, pass the `deleteCascade` property in the object passed to `defineLink`.

For example:

```ts
import BlogModule from "../modules/blog"
import ProductModule from "@medusajs/medusa/product"
import { defineLink } from "@medusajs/framework/utils"

export default defineLink(
  ProductModule.linkable.product,
  {
    linkable: BlogModule.linkable.post,
    deleteCascade: true,
  }
)
```

In this example, when a product is deleted, its linked `post` record is also deleted.

***

## Renaming Participants in a Module Link

As mentioned in the [Module Link's Database Table](#module-links-database-table) section, the name of a link's table consists of the names of the modules and the data models' table names.

So, if you rename a module or a data model's table, then run the `db:sync-links` or `db:migrate` commands, you'll be asked to delete the old link table and create a new one.

A data model's table name is passed in the first parameter of `model.define`, and a module's name is passed in the first parameter of `Module` in the module's `index.ts` file.

For example, if you have the link table `product_product_blog_post` and you rename the Blog Module from `blog` to `article`, Medusa considers the old link definition deleted. Then, when you run the `db:sync-links` or `db:migrate` command, Medusa will ask if you want to delete the old link table, and will create a new one with the new name `product_product_article_post`.

To resolve this, you can rename the link table in the link definition.

### Rename Link Table

If you need to rename a module or its data model's table, you can persist the old name by passing a third parameter to `defineLink`. This parameter is an object of additional configurations. It accepts a `database` property that allows you to configure the link's table name.

For example, after renaming the Blog Module to `article`, you can persist the old name `blog` in the link table name:

```ts highlights={renameHighlights}
import ArticleModule from "../modules/article"
import ProductModule from "@medusajs/medusa/product"
import { defineLink } from "@medusajs/framework/utils"

export default defineLink(
  ProductModule.linkable.product,
  {
    linkable: ArticleModule.linkable.post,
    isList: true,
  },
  {
    database: {
      table: "product_product_blog_post",
    },
  }
)
```

In this example, you set the `table` property in the `database` object to the old link table name `product_product_blog_post`, ensuring that the old link table is not deleted.

This is enough to rename the link table when you rename a module. If you renamed a data model's table, you need to also run the `db:sync-links` or `db:migrate` commands, which will update the column names in the link table automatically:

```bash
npx medusa db:migrate
```

***

## Delete Module Link Definition

To delete a module link definition, remove the link file from the `src/links` directory. Then, run the `db:sync-links` or `db:migrate` command to delete the link table from the database:

```bash
npx medusa db:migrate
```


# Modules

In this chapter, you’ll learn about modules and how to create them.

## What is a Module?

A module is a reusable package of functionalities related to a single domain or integration. Medusa comes with multiple pre-built modules for core commerce needs, such as the [Cart Module](https://docs.medusajs.com/resources/commerce-modules/cart/index.html.md) that holds the data models and business logic for cart operations.

When building a commerce application, you often need to introduce custom behavior specific to your products, tech stack, or your general ways of working. In other commerce platforms, introducing custom business logic and data models requires setting up separate applications to manage these customizations.

Medusa removes this overhead by allowing you to easily write custom modules that integrate into the Medusa application without affecting the existing setup. You can also re-use your modules across Medusa projects.

As you learn more about Medusa, you will see that modules are central to customizations and integrations. With modules, your Medusa application can turn into a middleware solution for your commerce ecosystem.

- You want to build a custom feature related to a single domain or integrate a third-party service.

- You want to create a reusable package of customizations that include not only modules, but also API routes, workflows, and other customizations. Instead, use a [plugin](https://docs.medusajs.com/learn/fundamentals/plugins/index.html.md).

***

## How to Create a Module?

In a module, you define data models that represent new tables in the database, and you manage these models in a class called a service. Then, the Medusa application registers the module's service in the [Medusa container](https://docs.medusajs.com/learn/fundamentals/medusa-container/index.html.md) so that you can build commerce flows and features around the functionalities provided by the module.

In this section, you'll build a Blog Module that has a `Post` data model and a service to manage that data model. You'll also expose an API endpoint to create a blog post.

Modules are created in a sub-directory of `src/modules`. So, start by creating the directory `src/modules/blog`.

### 1. Create Data Model

A data model represents a table in the database. You create data models using Medusa's data modeling language (DML). It simplifies defining a table's columns, relations, and indexes with straightforward methods and configurations.

You create a data model in a TypeScript or JavaScript file under the `models` directory of a module. So, to create a `Post` data model in the Blog Module, create the file `src/modules/blog/models/post.ts` with the following content:

![Updated directory overview after adding the data model](https://res.cloudinary.com/dza7lstvk/image/upload/v1732806790/Medusa%20Book/blog-dir-overview-1_jfvovj.jpg)

```ts title="src/modules/blog/models/post.ts"
import { model } from "@medusajs/framework/utils"

const Post = model.define("post", {
  id: model.id().primaryKey(),
  title: model.text(),
})

export default Post
```

You define the data model using the `define` method of the DML. It accepts two parameters:

1. The first one is the name of the data model's table in the database. Use snake-case names.
2. The second is an object, which is the data model's schema. The schema's properties are defined using the `model`'s methods, such as `text` and `id`.
   - Data models automatically have the date properties `created_at`, `updated_at`, and `deleted_at`, so you don't need to add them manually.

Learn about other property types in [this chapter](https://docs.medusajs.com/learn/fundamentals/data-models/properties#property-types/index.html.md).

The code snippet above defines a `Post` data model with `id` and `title` properties.

### 2. Create Service

You perform database operations on your data models in a service, which is a class exported by the module and acts like an interface to its functionalities. Medusa registers the service in its [container](https://docs.medusajs.com/learn/fundamentals/medusa-container/index.html.md), allowing you to resolve and use it when building custom commerce flows.

You define a service in a `service.ts` or `service.js` file at the root of your module's directory. So, to create the Blog Module's service, create the file `src/modules/blog/service.ts` with the following content:

![Updated directory overview after adding the service](https://res.cloudinary.com/dza7lstvk/image/upload/v1732807230/Medusa%20Book/blog-dir-overview-2_avzb9l.jpg)

```ts title="src/modules/blog/service.ts" highlights={highlights}
import { MedusaService } from "@medusajs/framework/utils"
import Post from "./models/post"

class BlogModuleService extends MedusaService({
  Post,
}){
}

export default BlogModuleService
```

Your module's service extends a class generated by `MedusaService` from the Modules SDK. This class comes with generated methods for data-management Create, Read, Update, and Delete (CRUD) operations on each of your modules, saving you time that can be spent on building custom business logic.

The `MedusaService` function accepts an object of data models to generate methods for. You can pass all data models in your module in this object.

For example, the `BlogModuleService` now has a `createPosts` method to create post records, and a `retrievePost` method to retrieve a post record. The suffix of each method (except for `retrieve`) is the pluralized name of the data model.

Find all methods generated by the `MedusaService` in [this reference](https://docs.medusajs.com/resources/service-factory-reference/index.html.md)

If a module doesn't have data models, such as when it's integrating a third-party service, it doesn't need to extend `MedusaService`.

### 3. Export Module Definition

The final piece to a module is its definition, which is exported in an `index.ts` file at its root directory. This definition tells Medusa the name of the module and its main service. Medusa will then register the main service in the container under the module's name.

So, to export the definition of the Blog Module, create the file `src/modules/blog/index.ts` with the following content:

![Updated directory overview after adding the module definition](https://res.cloudinary.com/dza7lstvk/image/upload/v1732808511/Medusa%20Book/blog-dir-overview-3_dcgjaa.jpg)

```ts title="src/modules/blog/index.ts" highlights={moduleDefinitionHighlights}
import BlogModuleService from "./service"
import { Module } from "@medusajs/framework/utils"

export const BLOG_MODULE = "blog"

export default Module(BLOG_MODULE, {
  service: BlogModuleService,
})
```

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

1. The name that the module's main service is registered under (`blog`). The module name can contain only alphanumeric characters and underscores.
2. An object with a required property `service` indicating the module's main service.

You export `BLOG_MODULE` to reference the module's name more reliably when resolving its service in other customizations.

### 4. Add Module to Medusa's Configurations

If you're creating the module in a plugin, this step isn't required as the module is registered when the plugin is registered. Learn more about plugins in [this documentation](https://docs.medusajs.com/learn/fundamentals/plugins/index.html.md).

Once you finish building the module, add it to Medusa's configurations to start using it. Medusa will then register the module's main service in the Medusa container, allowing you to resolve and use it in other customizations.

In `medusa-config.ts`, add a `modules` property and pass an array with your custom module:

```ts title="medusa-config.ts" highlights={[["7"]]}
module.exports = defineConfig({
  projectConfig: {
    // ...
  },
  modules: [
    {
      resolve: "./src/modules/blog",
    },
  ],
})
```

Each object in the `modules` array has a `resolve` property, whose value is either a path to the module's directory, or an `npm` package’s name.

### 5. Generate Migrations

Since data models represent tables in the database, you define how they're created in the database with migrations. A migration is a TypeScript or JavaScript file that defines database changes made by a module.

Migrations are useful when you re-use a module or you're working in a team, so that when one member of a team makes a database change, everyone else can reflect it on their side by running the migrations.

You don't have to write migrations yourself. Medusa's CLI tool has a command that generates the migrations for you. You can also use this command again when you make changes to the module at a later point, and it will generate new migrations for that change.

To generate a migration for the Blog Module, run the following command in your Medusa application's directory:

If you're creating the module in a plugin, use the [plugin:db:generate command](https://docs.medusajs.com/resources/medusa-cli/commands/plugin#plugindbgenerate/index.html.md) instead.

```bash
npx medusa db:generate blog
```

The `db:generate` command of the Medusa CLI accepts one or more module names to generate the migration for. It will create a migration file for the Blog Module in the directory `src/modules/blog/migrations` similar to the following:

```ts
import { Migration } from "@mikro-orm/migrations"

export class Migration20241121103722 extends Migration {

  async up(): Promise<void> {
    this.addSql("create table if not exists \"post\" (\"id\" text not null, \"title\" text not null, \"created_at\" timestamptz not null default now(), \"updated_at\" timestamptz not null default now(), \"deleted_at\" timestamptz null, constraint \"post_pkey\" primary key (\"id\"));")
  }

  async down(): Promise<void> {
    this.addSql("drop table if exists \"post\" cascade;")
  }

}
```

In the migration class, the `up` method creates the table `post` and defines its columns using PostgreSQL syntax. The `down` method drops the table.

### 6. Run Migrations

To reflect the changes in the generated migration file on the database, run the `db:migrate` command:

If you're creating the module in a plugin, run this command on the Medusa application that the plugin is installed in.

```bash
npx medusa db:migrate
```

This creates the `post` table in the database.

***

## Test the Module

Since the module's main service is registered in the Medusa container, you can resolve it in other customizations to use its methods.

To test out the Blog Module, you'll add the functionality to create a post in a [workflow](https://docs.medusajs.com/learn/fundamentals/workflows/index.html.md), which is a special function that performs a task in a series of steps with rollback logic. Then, you'll expose an [API route](https://docs.medusajs.com/learn/fundamentals/api-routes/index.html.md) that creates a blog post by executing the workflow.

By building a commerce feature in a workflow, you can execute it in other customizations while ensuring data consistency across systems. If an error occurs during execution, every step has its own rollback logic to undo its actions. Workflows have other special features which you can learn about in [this chapter](https://docs.medusajs.com/learn/fundamentals/workflows/index.html.md).

To create the workflow, create the file `src/workflows/create-post.ts` with the following content:

```ts title="src/workflows/create-post.ts" highlights={workflowHighlights}
import { 
  createStep, 
  createWorkflow, 
  StepResponse, 
  WorkflowResponse,
} from "@medusajs/framework/workflows-sdk"
import { BLOG_MODULE } from "../modules/blog"
import BlogModuleService from "../modules/blog/service"

type CreatePostWorkflowInput = {
  title: string
}

const createPostStep = createStep(
  "create-post",
  async ({ title }: CreatePostWorkflowInput, { container }) => {
    const blogModuleService: BlogModuleService = container.resolve(BLOG_MODULE)

    const post = await blogModuleService.createPosts({
      title,
    })

    return new StepResponse(post, post)
  },
  async (post, { container }) => {
    const blogModuleService: BlogModuleService = container.resolve(BLOG_MODULE)

    await blogModuleService.deletePosts(post.id)
  }
)

export const createPostWorkflow = createWorkflow(
  "create-post",
  (postInput: CreatePostWorkflowInput) => {
    const post = createPostStep(postInput)

    return new WorkflowResponse(post)
  }
)
```

The workflow has a single step `createPostStep` that creates a post. In the step, you resolve the Blog Module's service from the Medusa container, which the step receives as a parameter. Then, you create the post using the method `createPosts` of the service, which was generated by `MedusaService`.

The step also has a compensation function, which is a function passed as a third-parameter to `createStep` that implements the logic to rollback the change made by a step in case an error occurs during the workflow's execution.

You'll now execute that workflow in an API route to expose the feature of creating blog posts to clients. To create an API route, create the file `src/api/blog/posts/route.ts` with the following content:

```ts
import type { 
  MedusaRequest, 
  MedusaResponse,
} from "@medusajs/framework/http"
import { 
  createPostWorkflow,
} from "../../../workflows/create-post"

export async function POST(
  req: MedusaRequest, 
  res: MedusaResponse
) {
  const { result: post } = await createPostWorkflow(req.scope)
    .run({
      input: {
        title: "My Post",
      },
    })

  res.json({
    post,
  })
}
```

This adds a `POST` API route at `/blog/posts`. In the API route, you execute the `createPostWorkflow` by invoking it, passing it the Medusa container in `req.scope`, then invoking the `run` method. In the `run` method, you pass the workflow's input in the `input` property.

To test this out, start the Medusa application:

```bash npm2yarn
npm run dev
```

Then, send a `POST` request to `/blog/posts`:

```bash
curl -X POST http://localhost:9000/blog/posts
```

This will create a post and return it in the response:

```json
{
  "post": {
    "id": "123...",
    "title": "My Post",
    "created_at": "...",
    "updated_at": "..."
  }
}
```

You can also execute the workflow from a [subscriber](https://docs.medusajs.com/learn/fundamentals/events-and-subscribers/index.html.md) when an event occurs, or from a [scheduled job](https://docs.medusajs.com/learn/fundamentals/scheduled-jobs/index.html.md) to run it at a specified interval.


# Scheduled Jobs

In this chapter, you’ll learn about scheduled jobs and how to use them.

## What is a Scheduled Job?

When building your commerce application, you may need to automate tasks and run them repeatedly at a specific schedule. For example, you need to automatically sync products to a third-party service once a day.

In other commerce platforms, this feature isn't natively supported. Instead, you have to setup a separate application to execute cron jobs, which adds complexity as to how you expose this task to be executed in a cron job, or how do you debug it when it's not running within the platform's tooling.

Medusa removes this overhead by supporting this feature natively with scheduled jobs. A scheduled job is an asynchronous function that the Medusa application runs at the interval you specify during the Medusa application's runtime. Your efforts are only spent on implementing the functionality performed by the job, such as syncing products to an ERP.

- You want the action to execute at a specified schedule while the Medusa application **isn't** running. Instead, use the operating system's equivalent of a cron job.
- You want to execute the action once when the application loads. Use [loaders](https://docs.medusajs.com/learn/fundamentals/modules/loaders/index.html.md) instead.
- You want to execute the action if an event occurs. Use [subscribers](https://docs.medusajs.com/learn/fundamentals/events-and-subscribers/index.html.md) instead.

***

## How to Create a Scheduled Job?

You create a scheduled job in a TypeScript or JavaScript file under the `src/jobs` directory. The file exports the asynchronous function to run, and the configurations indicating the schedule to run the function.

For example, create the file `src/jobs/hello-world.ts` with the following content:

![Example of scheduled job file in the application's directory structure](https://res.cloudinary.com/dza7lstvk/image/upload/v1732866423/Medusa%20Book/scheduled-job-dir-overview_ediqgm.jpg)

```ts title="src/jobs/hello-world.ts" highlights={highlights}
import { MedusaContainer } from "@medusajs/framework/types"

export default async function greetingJob(container: MedusaContainer) {
  const logger = container.resolve("logger")

  logger.info("Greeting!")
}

export const config = {
  name: "greeting-every-minute",
  schedule: "* * * * *",
}
```

You export an asynchronous function that receives the [Medusa container](https://docs.medusajs.com/learn/fundamentals/medusa-container/index.html.md) as a parameter. In the function, you resolve the [Logger utility](https://docs.medusajs.com/learn/debugging-and-testing/logging/index.html.md) from the Medusa container and log a message.

You also export a `config` object that has the following properties:

- `name`: A unique name for the job.
- `schedule`: A string that holds a [cron expression](https://crontab.guru/) indicating the schedule to run the job.

This scheduled job executes every minute and logs into the terminal `Greeting!`.

### Test the Scheduled Job

To test out your scheduled job, start the Medusa application:

```bash npm2yarn
npm run dev
```

After a minute, the following message will be logged to the terminal:

```bash
info:    Greeting!
```

***

## Example: Sync Products Once a Day

In this section, you'll find a brief example of how you use a scheduled job to sync products to a third-party service.

When implementing flows spanning across systems or [modules](https://docs.medusajs.com/learn/fundamentals/modules/index.html.md), you use [workflows](https://docs.medusajs.com/learn/fundamentals/workflows/index.html.md). A workflow is a task made up of a series of steps, and you construct it like you would a regular function, but it's a special function that supports rollback mechanism in case of errors, background execution, and more.

You can learn how to create a workflow in [this chapter](https://docs.medusajs.com/learn/fundamentals/workflows/index.html.md), but this example assumes you already have a `syncProductToErpWorkflow` implemented. To execute this workflow once a day, create a scheduled job at `src/jobs/sync-products.ts` with the following content:

```ts title="src/jobs/sync-products.ts"
import { MedusaContainer } from "@medusajs/framework/types"
import { syncProductToErpWorkflow } from "../workflows/sync-products-to-erp"

export default async function syncProductsJob(container: MedusaContainer) {
  await syncProductToErpWorkflow(container)
    .run()
}

export const config = {
  name: "sync-products-job",
  schedule: "0 0 * * *",
}
```

In the scheduled job function, you execute the `syncProductToErpWorkflow` by invoking it and passing it the container, then invoking the `run` method. You also specify in the exported configurations the schedule `0 0 * * *` which indicates midnight time of every day.

The next time you start the Medusa application, it will run this job every day at midnight.


# Workflows

In this chapter, you’ll learn about workflows and how to define and execute them.

## What is a Workflow?

In digital commerce you typically have many systems involved in your operations. For example, you may have an ERP system that holds product master data and accounting reports, a CMS system for content, a CRM system for managing customer campaigns, a payment service to process credit cards, and so on. Sometimes you may even have custom built applications that need to participate in the commerce stack. One of the biggest challenges when operating a stack like this is ensuring consistency in the data spread across systems.

Medusa has a built-in durable execution engine to help complete tasks that span multiple systems. You orchestrate your operations across systems in Medusa instead of having to manage it yourself. Other commerce platforms don't have this capability, which makes them a bottleneck to building customizations and iterating quickly.

A workflow is a series of queries and actions, called steps, that complete a task. You construct a workflow similar to how you create a JavaScript function.

However, unlike regular functions, workflows:

- Create an internal representation of your steps, allowing you to track them and their progress.
- Support defining roll-back logic for each step, so that you can handle errors gracefully and your data remain consistent across systems.
- Perform long actions asynchronously, giving you control over when a step starts and finishes.

You implement all custom flows within workflows, then execute them from [API routes](https://docs.medusajs.com/learn/fundamentals/api-routes/index.html.md), [subscribers](https://docs.medusajs.com/learn/fundamentals/events-and-subscribers/index.html.md), and [scheduled jobs](https://docs.medusajs.com/learn/fundamentals/scheduled-jobs/index.html.md).

***

## How to Create and Execute a Workflow?

### 1. Create the Steps

A workflow is made of a series of steps. A step is created using `createStep` from the Workflows SDK.

Create the file `src/workflows/hello-world.ts` with the following content:

![Example of workflow file in the application's directory structure](https://res.cloudinary.com/dza7lstvk/image/upload/v1732866980/Medusa%20Book/workflow-dir-overview_xklukj.jpg)

```ts title="src/workflows/hello-world.ts" highlights={step1Highlights}
import { createStep, StepResponse } from "@medusajs/framework/workflows-sdk"

const step1 = createStep(
  "step-1", 
  async () => {
    return new StepResponse(`Hello from step one!`)
  }
)
```

The `createStep` function accepts the step's unique name as a first parameter, and the step's function as a second parameter.

Steps must return an instance of `StepResponse`, whose parameter is the data to return to the workflow executing the step.

Steps can accept input parameters. For example, add the following to `src/workflows/hello-world.ts`:

```ts title="src/workflows/hello-world.ts" highlights={step2Highlights}
type WorkflowInput = {
  name: string
}

const step2 = createStep(
  "step-2", 
  async ({ name }: WorkflowInput) => {
    return new StepResponse(`Hello ${name} from step two!`)
  }
)
```

This adds another step whose function accepts as a parameter an object with a `name` property.

### 2. Create a Workflow

Next, add the following to the same file to create the workflow using the `createWorkflow` function:

```ts title="src/workflows/hello-world.ts" highlights={workflowHighlights}
import {
  // other imports...
  createWorkflow,
  WorkflowResponse,
} from "@medusajs/framework/workflows-sdk"

// ...

const myWorkflow = createWorkflow(
  "hello-world",
  function (input: WorkflowInput) {
    const str1 = step1()
    // to pass input
    const str2 = step2(input)

    return new WorkflowResponse({
      message: str2,
    })
  }
)

export default myWorkflow
```

The `createWorkflow` function accepts the workflow's unique name as a first parameter, and the workflow's function as a second parameter. The workflow can accept input which is passed as a parameter to the function.

The workflow must return an instance of `WorkflowResponse`, whose parameter is returned to workflow executors.

### 3. Execute the Workflow

You can execute a workflow from different customizations:

- Execute in an API route to expose the workflow's functionalities to clients.
- Execute in a subscriber to use the workflow's functionalities when a commerce operation is performed.
- Execute in a scheduled job to run the workflow's functionalities automatically at a specified repeated interval.

To execute the workflow, invoke it passing the [Medusa container](https://docs.medusajs.com/learn/fundamentals/medusa-container/index.html.md) as a parameter. Then, use its `run` method:

### API Route

```ts title="src/api/workflow/route.ts" highlights={[["11"], ["12"], ["13"], ["14"], ["15"], ["16"]]} collapsibleLines="1-6" expandButtonLabel="Show Imports"
import type {
  MedusaRequest,
  MedusaResponse,
} from "@medusajs/framework/http"
import myWorkflow from "../../workflows/hello-world"

export async function GET(
  req: MedusaRequest,
  res: MedusaResponse
) {
  const { result } = await myWorkflow(req.scope)
    .run({
      input: {
        name: "John",
      },
    })

  res.send(result)
}
```

### Subscriber

```ts title="src/subscribers/order-placed.ts" highlights={[["11"], ["12"], ["13"], ["14"], ["15"], ["16"]]} collapsibleLines="1-6" expandButtonLabel="Show Imports"
import {
  type SubscriberConfig,
  type SubscriberArgs,
} from "@medusajs/framework"
import myWorkflow from "../workflows/hello-world"

export default async function handleOrderPlaced({
  event: { data },
  container,
}: SubscriberArgs<{ id: string }>) {
  const { result } = await myWorkflow(container)
    .run({
      input: {
        name: "John",
      },
    })

  console.log(result)
}

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

### Scheduled Job

```ts title="src/jobs/message-daily.ts" highlights={[["7"], ["8"], ["9"], ["10"], ["11"], ["12"]]}
import { MedusaContainer } from "@medusajs/framework/types"
import myWorkflow from "../workflows/hello-world"

export default async function myCustomJob(
  container: MedusaContainer
) {
  const { result } = await myWorkflow(container)
    .run({
      input: {
        name: "John",
      },
    })

  console.log(result.message)
}

export const config = {
  name: "run-once-a-day",
  schedule: `0 0 * * *`,
};
```

### 4. Test Workflow

To test out your workflow, start your Medusa application:

```bash npm2yarn
npm run dev
```

Then, if you added the API route above, send a `GET` request to `/workflow`:

```bash
curl http://localhost:9000/workflow
```

You’ll receive the following response:

```json title="Example Response"
{
  "message": "Hello John from step two!"
}
```

***

## Access Medusa Container in Workflow Steps

A step receives an object as a second parameter with configurations and context-related properties. One of these properties is the `container` property, which is the [Medusa container](https://docs.medusajs.com/learn/fundamentals/medusa-container/index.html.md) to allow you to resolve Framework and commerce tools in your application.

For example, consider you want to implement a workflow that returns the total products in your application. Create the file `src/workflows/product-count.ts` with the following content:

```ts title="src/workflows/product-count.ts" highlights={highlights} collapsibleLines="1-7" expandButtonLabel="Show Imports"
import {
  createStep,
  StepResponse,
  createWorkflow,
  WorkflowResponse,
} from "@medusajs/framework/workflows-sdk"

const getProductCountStep = createStep(
  "get-product-count", 
  async (_, { container }) => {
    const productModuleService = container.resolve("product")

    const [, count] = await productModuleService.listAndCountProducts()

    return new StepResponse(count)
  }
)

const productCountWorkflow = createWorkflow(
  "product-count",
  function () {
    const count = getProductCountStep()

    return new WorkflowResponse({
      count,
    })
  }
)

export default productCountWorkflow
```

In `getProductCountStep`, you use the `container` to resolve the Product Module's main service. Then, you use its `listAndCountProducts` method to retrieve the total count of products and return it in the step's response. You then execute this step in the `productCountWorkflow`.

You can now execute this workflow in a custom API route, scheduled job, or subscriber to get the total count of products.

Find a full list of the registered resources in the Medusa container and their registration key in [this reference](https://docs.medusajs.com/resources/medusa-container-resources/index.html.md). You can use these resources in your custom workflows.


# Medusa's Architecture

In this chapter, you'll learn about the architectural layers in Medusa.

Find the full architectural diagram at the [end of this chapter](#full-diagram-of-medusas-architecture).

## HTTP, Workflow, and Module Layers

Medusa is a headless commerce platform. So, storefronts, admin dashboards, and other clients consume Medusa's functionalities through its API routes.

In a common Medusa application, requests go through four layers in the stack. In order of entry, those are:

1. API Routes (HTTP): Our API Routes are the typical entry point. The Medusa server is based on Express.js, which handles incoming requests. It can also connect to a Redis database that stores the server session data.
2. Workflows: API Routes consume workflows that hold the opinionated business logic of your application.
3. Modules: Workflows use domain-specific modules for resource management.
4. Data store: Modules query the underlying datastore, which is a PostgreSQL database in common cases.

These layers of stack can be implemented within [plugins](https://docs.medusajs.com/learn/fundamentals/plugins/index.html.md).

![This diagram illustrates the entry point of requests into the Medusa application through API routes. It shows a storefront and an admin that can send a request to the HTTP layer. The HTTP layer then uses workflows to handle the business logic. Finally, the workflows use modules to query and manipulate data in the data stores.](https://res.cloudinary.com/dza7lstvk/image/upload/v1727175296/Medusa%20Book/http-layer_sroafr.jpg)

***

## Database Layer

The Medusa application injects into each module, including your [custom modules](https://docs.medusajs.com/learn/fundamentals/modules/index.html.md), a connection to the configured PostgreSQL database. Modules use that connection to read and write data to the database.

Modules can be implemented within [plugins](https://docs.medusajs.com/learn/fundamentals/plugins/index.html.md).

![This diagram illustrates how modules connect to the database.](https://res.cloudinary.com/dza7lstvk/image/upload/v1727175379/Medusa%20Book/db-layer_pi7tix.jpg)

***

## Third-Party Integrations Layer

Third-party services and systems are integrated through Medusa's Commerce and Infrastructure Modules. You also create custom third-party integrations through a [custom module](https://docs.medusajs.com/learn/fundamentals/modules/index.html.md).

Modules can be implemented within [plugins](https://docs.medusajs.com/learn/fundamentals/plugins/index.html.md).

### Commerce Modules

[Commerce Modules](https://docs.medusajs.com/resources/commerce-modules/index.html.md) integrate third-party services relevant for commerce or user-facing features. For example, you can integrate [Stripe](https://docs.medusajs.com/resources/commerce-modules/payment/payment-provider/stripe/index.html.md) through a Payment Module Provider, or [ShipStation](https://docs.medusajs.com/resources/integrations/guides/shipstation/index.html.md) through a Fulfillment Module Provider.

You can also integrate third-party services for custom functionalities. For example, you can integrate [Sanity](https://docs.medusajs.com/resources/integrations/guides/sanity/index.html.md) for rich CMS capabilities, or [Odoo](https://docs.medusajs.com/resources/recipes/erp/odoo/index.html.md) to sync your Medusa application with your ERP system.

You can replace any of the third-party services mentioned above to build your preferred commerce ecosystem.

![Diagram illustrating the Commerce Modules integration to third-party services](https://res.cloudinary.com/dza7lstvk/image/upload/v1727175357/Medusa%20Book/service-commerce_qcbdsl.jpg)

### Infrastructure Modules

[Infrastructure Modules](https://docs.medusajs.com/resources/infrastructure-modules/index.html.md) integrate third-party services and systems that customize Medusa's infrastructure. Medusa has the following Infrastructure Modules:

- [Analytics Module](https://docs.medusajs.com/resources/infrastructure-modules/analytics/index.html.md): Tracks and analyzes user interactions and system events with third-party analytic providers. You can integrate [PostHog](https://docs.medusajs.com/resources/infrastructure-modules/analytics/posthog/index.html.md) as the analytics provider.
- [Cache Module](https://docs.medusajs.com/resources/infrastructure-modules/cache/index.html.md): Caches data that require heavy computation. You can integrate a custom module to handle the caching with services like Memcached, or use the existing [Redis Cache Module](https://docs.medusajs.com/resources/infrastructure-modules/cache/redis/index.html.md).
- [Event Module](https://docs.medusajs.com/resources/infrastructure-modules/event/index.html.md): A pub/sub system that allows you to subscribe to events and trigger them. You can integrate [Redis](https://docs.medusajs.com/resources/infrastructure-modules/event/redis/index.html.md) as the pub/sub system.
- [File Module](https://docs.medusajs.com/resources/infrastructure-modules/file/index.html.md): Manages file uploads and storage, such as upload of product images. You can integrate [AWS S3](https://docs.medusajs.com/resources/infrastructure-modules/file/s3/index.html.md) for file storage.
- [Locking Module](https://docs.medusajs.com/resources/infrastructure-modules/locking/index.html.md): Manages access to shared resources by multiple processes or threads, preventing conflict between processes and ensuring data consistency. You can integrate [Redis](https://docs.medusajs.com/resources/infrastructure-modules/locking/redis/index.html.md) for locking.
- [Notification Module](https://docs.medusajs.com/resources/infrastructure-modules/notification/index.html.md): Sends notifications to customers and users, such as for order updates or newsletters. You can integrate [SendGrid](https://docs.medusajs.com/resources/infrastructure-modules/notification/sendgrid/index.html.md) for sending emails.
- [Workflow Engine Module](https://docs.medusajs.com/resources/infrastructure-modules/workflow-engine/index.html.md): Orchestrates workflows that hold the business logic of your application. You can integrate [Redis](https://docs.medusajs.com/resources/infrastructure-modules/workflow-engine/redis/index.html.md) to orchestrate workflows.

All of the third-party services mentioned above can be replaced to help you build your preferred architecture and ecosystem.

![Diagram illustrating the Infrastructure Modules integration to third-party services and systems](https://res.cloudinary.com/dza7lstvk/image/upload/v1727175342/Medusa%20Book/service-arch_ozvryw.jpg)

***

## Full Diagram of Medusa's Architecture

The following diagram illustrates Medusa's architecture including all its layers.

![Full diagram illustrating Medusa's architecture combining all the different layers.](https://res.cloudinary.com/dza7lstvk/image/upload/v1727174897/Medusa%20Book/architectural-diagram-full.jpg)


# General Medusa Application Deployment Guide

In this document, you'll learn the general steps to deploy your Medusa application. How you apply these steps depend on your chosen hosting provider or platform.

Find how-to guides for specific platforms in [this documentation](https://docs.medusajs.com/resources/deployment/index.html.md).

Want Medusa to manage and maintain your infrastructure? [Sign up and learn more about Medusa Cloud](https://medusajs.com/pricing)

Medusa Cloud is our managed services offering that makes deploying and operating Medusa applications possible without having to worry about configuring, scaling, and maintaining infrastructure. Medusa Cloud hosts your server, Admin dashboard, database, and Redis instance.

With Medusa Cloud, you maintain full customization control as you deploy your own modules and customizations directly from GitHub:

- Push to deploy.
- Multiple testing environments.
- Preview environments for new PRs.
- Test on production-like data.

### Prerequisites

- [Medusa application’s codebase hosted on GitHub repository.](https://docs.medusajs.com/learn/index.html.md)

## What You'll Deploy

When you deploy the Medusa application, you need to deploy the following resources:

1. PostgreSQL database: This is the database that will hold your Medusa application's details.
2. Redis database: This is the database that will store the Medusa server's session.
3. Medusa application in [server and worker mode](https://docs.medusajs.com/learn/production/worker-mode/index.html.md), where:
   - The server mode handles incoming API requests and serving the Medusa Admin dashboard.
   - The worker mode handles background tasks, such as scheduled jobs and subscribers.

So, when choosing a hosting provider, make sure it supports deploying these resources. Also, for optimal experience, the hosting provider and plan must offer at least 2GB of RAM.

***

## 1. Configure Medusa Application

### Worker Mode

The `workerMode` configuration determines which mode the Medusa application runs in. When you deploy the Medusa application, you deploy two instances: one in server mode, and one in worker mode.

Learn more about worker mode in the [Worker Module chapter](https://docs.medusajs.com/learn/production/worker-mode/index.html.md).

So, add the following configuration in `medusa-config.ts`:

```ts title="medusa-config.ts"
module.exports = defineConfig({
  projectConfig: {
    // ...
    workerMode: process.env.MEDUSA_WORKER_MODE as "shared" | "worker" | "server",
  },
})
```

Later, you’ll set different values of the `MEDUSA_WORKER_MODE` environment variable for each Medusa application deployment or instance.

### Configure Medusa Admin

The Medusa Admin is served by the Medusa server application. So, you need to disable it in the worker Medusa application only.

To disable the Medusa Admin in the worker Medusa application while keeping it enabled in the server Medusa application, add the following configuration in `medusa-config.ts`:

```ts title="medusa-config.ts"
module.exports = defineConfig({
  // ...
  admin: {
    disable: process.env.DISABLE_MEDUSA_ADMIN === "true",
  },
})
```

Later, you’ll set different values of the `DISABLE_MEDUSA_ADMIN` environment variable for each Medusa application instance.

### Configure Redis URL

The `redisUrl` configuration specifies the connection URL to Redis to store the Medusa server's session.

Learn more in the [Medusa Configuration documentation](https://docs.medusajs.com/learn/configurations/medusa-config#redisurl/index.html.md).

So, add the following configuration in `medusa-config.ts` :

```ts title="medusa-config.ts"
module.exports = defineConfig({
  projectConfig: {
    // ...
    redisUrl: process.env.REDIS_URL,
  },
})
```

***

## 2. Add predeploy Script

Before you start the Medusa application in production, you should always run migrations and sync links.

So, add the following script in `package.json`:

```json
"scripts": {
  // ...
  "predeploy": "medusa db:migrate"
},
```

***

## 3. Install Production Modules and Providers

By default, your Medusa application uses modules and providers useful for development, such as the In-Memory Cache Module or the Local File Module Provider.

It’s highly recommended to instead use modules and providers suitable for production, including:

- [Redis Cache Module](https://docs.medusajs.com/resources/infrastructure-modules/cache/redis/index.html.md)
- [Redis Event Bus Module](https://docs.medusajs.com/resources/infrastructure-modules/event/redis/index.html.md)
- [Workflow Engine Redis Module](https://docs.medusajs.com/resources/infrastructure-modules/workflow-engine/redis/index.html.md)
- [S3 File Module Provider](https://docs.medusajs.com/resources/infrastructure-modules/file/s3/index.html.md) (or other file module providers production-ready).
- [SendGrid Notification Module Provider](https://docs.medusajs.com/resources/infrastructure-modules/notification/sendgrid/index.html.md) (or other notification module providers production-ready).

Then, add these modules in `medusa-config.ts`:

```ts title="medusa-config.ts"
import { Modules } from "@medusajs/framework/utils"

module.exports = defineConfig({
  // ...
  modules: [
    {
      resolve: "@medusajs/medusa/cache-redis",
      options: {
        redisUrl: process.env.REDIS_URL,
      },
    },
    {
      resolve: "@medusajs/medusa/event-bus-redis",
      options: {
        redisUrl: process.env.REDIS_URL,
      },
    },
    {
      resolve: "@medusajs/medusa/workflow-engine-redis",
      options: {
        redis: {
          url: process.env.REDIS_URL,
        },
      },
    },
  ],
})
```

Check out the [Integrations](https://docs.medusajs.com/resources/integrations/index.html.md) and [Infrastructure Modules](https://docs.medusajs.com/resources/infrastructure-modules/index.html.md) documentation for other modules and providers to use.

***

## 4. Create PostgreSQL and Redis Databases

Your Medusa application must connect to PostgreSQL and Redis databases. So, before you deploy it, create production PostgreSQL and Redis databases.

If your hosting provider doesn't support databases, you can use [Neon for PostgreSQL database hosting](https://neon.tech/), and [Redis Cloud for the Redis database hosting](https://redis.io/cloud/).

After hosting both databases, keep their connection URLs for the next steps.

***

## 5. Deploy Medusa Application in Server Mode

As mentioned earlier, you'll deploy two instances or create two deployments of your Medusa application: one in server mode, and the other in worker mode.

The deployment steps depend on your hosting provider. This section provides the general steps to perform during the deployment.

### Set Environment Variables

When setting the environment variables of the Medusa application, set the following variables:

```bash
COOKIE_SECRET=supersecret # TODO GENERATE SECURE SECRET
JWT_SECRET=supersecret  # TODO GENERATE SECURE SECRET
STORE_CORS= # STOREFRONT URL
ADMIN_CORS= # ADMIN URL
AUTH_CORS= # STOREFRONT AND ADMIN URLS, SEPARATED BY COMMAS
DISABLE_MEDUSA_ADMIN=false
MEDUSA_WORKER_MODE=server
PORT=9000
DATABASE_URL= # POSTGRES DATABASE URL
REDIS_URL= # REDIS DATABASE URL
```

Where:

- The value of `COOKIE_SECRET` and `JWT_SECRET` must be a randomly generated secret.
- `STORE_CORS`'s value is the URL of your storefront. If you don’t have it yet, you can skip adding it for now.
- `ADMIN_CORS`'s value is the URL of the admin dashboard, which is the same as the server Medusa application. You can add it later if you don't currently have it.
- `AUTH_CORS`'s value is the URLs of any application authenticating users, customers, or other actor types, such as the storefront and admin URLs. The URLs are separated by commas. If you don’t have the URLs yet, you can set its value later.
- Set `DISABLE_MEDUSA_ADMIN`'s value to `false` so that the admin is built with the server application.
- Set the PostgreSQL database's connection URL as the value of `DATABASE_URL`
- Set the Redis database's connection URL as the value of `REDIS_URL`.

Feel free to add any other relevant environment variables, such as for integrations and Infrastructure Modules. If you're using environment variables in your admin customizations, make sure to set them as well, as they're inlined during the build process.

### Set Start Command

The Medusa application's production build, which is created using the `build` command, outputs the Medusa application to `.medusa/server`. So, you must install the dependencies in the `.medusa/server` directory, then run the `start` command in it.

If your hosting provider doesn't support setting a current-working directory, set the start command to the following:

```bash npm2yarn
cd .medusa/server && npm install && npm run predeploy && npm run start
```

Notice that you run the `predeploy` command before starting the Medusa application to run migrations and sync links whenever there's an update.

### Set Backend URL in Admin Configuration

The Medusa Admin is built and hosted statically. To send requests to the Medusa server application, you must set the backend URL in the Medusa Admin's configuration.

After you’ve obtained the Medusa application’s URL, add the following configuration to `medusa-config.ts`:

```ts title="medusa-config.ts"
module.exports = defineConfig({
  // ...
  admin: {
    // ...
    backendUrl: process.env.MEDUSA_BACKEND_URL,
  },
})
```

Then, push the changes to the GitHub repository or deployed application.

In your hosting provider, add or modify the following environment variables for the Medusa application in server mode:

```bash
ADMIN_CORS= # MEDUSA APPLICATION URL
AUTH_CORS= # ADD MEDUSA APPLICATION URL
MEDUSA_BACKEND_URL= # URL TO DEPLOYED MEDUSA APPLICATION
```

Where you set the value of `ADMIN_CORS` and `MEDUSA_BACKEND_URL` to the Medusa application’s URL, and you add the URL to `AUTH_CORS`.

After setting the environment variables, make sure to restart the deployment for the changes to take effect.

Remember to separate URLs in `AUTH_CORS` by commas.

***

## 6. Deploy Medusa Application in Worker Mode

Next, you'll deploy the Medusa application in worker mode.

As explained in the previous section, the deployment steps depend on your hosting provider. This section provides the general steps to perform during the deployment.

### Set Environment Variables

When setting the environment variables of the Medusa application, set the following variables:

```bash
COOKIE_SECRET=supersecret # TODO GENERATE SECURE SECRET
JWT_SECRET=supersecret  # TODO GENERATE SECURE SECRET
DISABLE_MEDUSA_ADMIN=true
MEDUSA_WORKER_MODE=worker
PORT=9000
DATABASE_URL= # POSTGRES DATABASE URL
REDIS_URL= # REDIS DATABASE URL
```

Where:

- The value of `COOKIE_SECRET` and `JWT_SECRET` must be a randomly generated secret.
- Set `DISABLE_MEDUSA_ADMIN`'s value to `true` so that the admin isn't built with the worker application.
- Set the PostgreSQL database's connection URL as the value of `DATABASE_URL`
- Set the Redis database's connection URL as the value of `REDIS_URL`.

Feel free to add any other relevant environment variables, such as for integrations and Infrastructure Modules.

### Set Start Command

The Medusa application's production build, which is created using the `build` command, outputs the Medusa application to `.medusa/server`. So, you must install the dependencies in the `.medusa/server` directory, then run the `start` command in it.

If your hosting provider doesn't support setting a current-working directory, set the start command to the following:

```bash npm2yarn
cd .medusa/server && npm install && npm run start
```

***

## 7. Test Deployed Application

Once the application is deployed and live, go to `<APP_URL>/health`, where `<APP_URL>` is the URL of the Medusa application in server mode. If the deployment was successful, you’ll see the `OK` response.

The Medusa Admin is also available at `<APP_URL>/app`.

***

## Create Admin User

If your hosting provider supports running commands in your Medusa application's directory, run the following command to create an admin user:

```bash
npx medusa user -e admin-medusa@test.com -p supersecret
```

Replace the email `admin-medusa@test.com` and password `supersecret` with the credentials you want.

You can use these credentials to log into the Medusa Admin dashboard.


# Worker Mode of Medusa Instance

In this chapter, you'll learn about the different modes of running a Medusa instance and how to configure the mode.

## What is Worker Mode?

By default, the Medusa application runs both the server, which handles all incoming requests, and the worker, which processes background tasks, in a single process. While this setup is suitable for development, it is not optimal for production environments where background tasks can be long-running or resource-intensive.

In a production environment, you should deploy two separate instances of your Medusa application:

1. A server instance that handles incoming requests to the application's API routes.
2. A worker instance that processes background tasks. This includes scheduled jobs and subscribers.

You don't need to set up different projects for each instance. Instead, you can configure the Medusa application to run in different modes based on environment variables, as you'll see later in this chapter.

This separation ensures that the server instance remains responsive to incoming requests, while the worker instance processes tasks in the background.

![Diagram showcasing how the server and worker work together](https://res.cloudinary.com/dza7lstvk/image/upload/fl_lossy/f_auto/r_16/ar_16:9,c_pad/v1/Medusa%20Book/medusa-worker_klkbch.jpg?_a=BATFJtAA0)

***

## How to Set Worker Mode

You can set the worker mode of your application using the `projectConfig.workerMode` configuration in the `medusa-config.ts`. The `workerMode` configuration accepts the following values:

- `shared`: (default) run the application in a single process, meaning the worker and server run in the same process.
- `worker`: run a worker process only.
- `server`: run the application server only.

Instead of creating different projects with different worker mode configurations, you can set the worker mode using an environment variable. Then, the worker mode configuration will change based on the environment variable.

For example, set the worker mode in `medusa-config.ts` to the following:

```ts title="medusa-config.ts"
module.exports = defineConfig({
  projectConfig: {
    workerMode: process.env.WORKER_MODE || "shared",
    // ...
  },
  // ...
})
```

You set the worker mode configuration to the `process.env.WORKER_MODE` environment variable and set a default value of `shared`.

Then, in the deployed server Medusa instance, set `WORKER_MODE` to `server`, and in the worker Medusa instance, set `WORKER_MODE` to `worker`:

### Server Medusa Instance

```bash
WORKER_MODE=server
```

### Worker Medusa Instance

```bash
WORKER_MODE=worker
```

### Disable Admin in Worker Mode

Since the worker instance only processes background tasks, you should disable the admin interface in it. That will save resources in the worker instance.

To disable the admin interface, set the `admin.disable` configuration in the `medusa-config.ts` file:

```ts title="medusa-config.ts"
module.exports = defineConfig({
  admin: {
    disable: process.env.ADMIN_DISABLED === "true" ||
      false,
  },
  // ...
})
```

Similar to before, you set the value in an environment variable, allowing you to enable or disable the admin interface based on the environment.

Then, in the deployed server Medusa instance, set `ADMIN_DISABLED` to `false`, and in the worker Medusa instance, set `ADMIN_DISABLED` to `true`:

### Server Medusa Instance

```bash
ADMIN_DISABLED=false
```

### Worker Medusa Instance

```bash
ADMIN_DISABLED=true
```


# Usage Information

At Medusa, we strive to provide the best experience for developers using our platform. For that reason, Medusa collects anonymous and non-sensitive data that provides a global understanding of how users are using Medusa.

***

## Purpose

As an open source solution, we work closely and constantly interact with our community to ensure that we provide the best experience for everyone using Medusa.

We are capable of getting a general understanding of how developers use Medusa and what general issues they run into through different means such as our Discord server, GitHub issues and discussions, and occasional one-on-one sessions.

However, although these methods can be insightful, they’re not enough to get a full and global understanding of how developers are using Medusa, especially in production.

Collecting this data allows us to understand certain details such as:

- What operating system do most Medusa developers use?
- What version of Medusa is widely used?
- What parts of the Medusa Admin are generally undiscovered by our users?
- How much data do users manage through our Medusa Admin? Is it being used for large number of products, orders, and other types of data?
- What Node version is globally used? Should we focus our efforts on providing support for versions that we don’t currently support?

***

## Medusa Application Analytics

This section covers which data in the Medusa application are collected and how to opt out of it.

### Collected Data in the Medusa Application

The following data is being collected on your Medusa application:

- Unique project ID generated with UUID.
- Unique machine ID generated with UUID.
- Operating system information including Node version or operating system platform used.
- The version of the Medusa application and Medusa CLI are used.

Data is only collected when the Medusa application is run with the command `medusa start`.

### How to Opt Out

If you prefer to disable data collection, you can do it either by setting the following environment variable to true:

```bash
MEDUSA_DISABLE_TELEMETRY=true
```

Or, you can run the following command in the root of your Medusa application project to disable it:

```bash
npx medusa telemetry --disable
```

***

## Admin Analytics

This section covers which data in the admin are collected and how to opt out of it.

### Collected Data in Admin

Users have the option to [enable or disable the anonymization](#how-to-enable-anonymization) of the collected data.

The following data is being collected on your admin:

- The name of the store.
- The email of the user.
- The total number of products, orders, discounts, and users.
- The number of regions and their names.
- The currencies used in the store.
- Errors that occur while using the admin.

### How to Enable Anonymization

To enable anonymization of your data from the Medusa Admin:

1. Go to Settings → Personal Information.
2. In the Usage insights section, click on the “Edit preferences” button.
3. Enable the "Anonymize my usage data” toggle.
4. Click on the “Submit and close” button.

### How to Opt-Out

To opt out of analytics collection in the Medusa Admin, set the following environment variable:

```bash
MEDUSA_FF_ANALYTICS=false
```


# Guide: Create Brand API Route

In the previous two chapters, you created a [Brand Module](https://docs.medusajs.com/learn/customization/custom-features/module/index.html.md) that added the concepts of brands to your application, then created a [workflow to create a brand](https://docs.medusajs.com/learn/customization/custom-features/workflow/index.html.md). In this chapter, you'll expose an API route that allows admin users to create a brand using the workflow from the previous chapter.

An API Route is an endpoint that acts as an entry point for other clients to interact with your Medusa customizations, such as the admin dashboard, storefronts, or third-party systems.

The Medusa core application provides a set of [admin](https://docs.medusajs.com/api/admin) and [store](https://docs.medusajs.com/api/store) API routes out-of-the-box. You can also create custom API routes to expose your custom functionalities.

### Prerequisites

- [createBrandWorkflow](https://docs.medusajs.com/learn/customization/custom-features/workflow/index.html.md)

## 1. Create the API Route

You create an API route in a `route.{ts,js}` file under a sub-directory of the `src/api` directory. The file exports API Route handler functions for at least one HTTP method (`GET`, `POST`, `DELETE`, etc…).

Learn more about API routes [in this guide](https://docs.medusajs.com/learn/fundamentals/api-routes/index.html.md).

The route's path is the path of `route.{ts,js}` relative to `src/api`. So, to create the API route at `/admin/brands`, create the file `src/api/admin/brands/route.ts` with the following content:

![Directory structure of the Medusa application after adding the route](https://res.cloudinary.com/dza7lstvk/image/upload/v1732869882/Medusa%20Book/brand-route-dir-overview-2_hjqlnf.jpg)

```ts title="src/api/admin/brands/route.ts"
import {
  MedusaRequest,
  MedusaResponse,
} from "@medusajs/framework/http"
import { 
  createBrandWorkflow,
} from "../../../workflows/create-brand"

type PostAdminCreateBrandType = {
  name: string
}

export const POST = async (
  req: MedusaRequest<PostAdminCreateBrandType>,
  res: MedusaResponse
) => {
  const { result } = await createBrandWorkflow(req.scope)
    .run({
      input: req.validatedBody,
    })

  res.json({ brand: result })
}
```

You export a route handler function with its name (`POST`) being the HTTP method of the API route you're exposing.

The function receives two parameters: a `MedusaRequest` object to access request details, and `MedusaResponse` object to return or manipulate the response. The `MedusaRequest` object's `scope` property is the [Medusa container](https://docs.medusajs.com/learn/fundamentals/medusa-container/index.html.md) that holds Framework tools and custom and core modules' services.

`MedusaRequest` accepts the request body's type as a type argument.

In the API route's handler, you execute the `createBrandWorkflow` by invoking it and passing the Medusa container `req.scope` as a parameter, then invoking its `run` method. You pass the workflow's input in the `input` property of the `run` method's parameter. You pass the request body's parameters using the `validatedBody` property of `MedusaRequest`.

You return a JSON response with the created brand using the `res.json` method.

***

## 2. Create Validation Schema

The API route you created accepts the brand's name in the request body. So, you'll create a schema used to validate incoming request body parameters.

Medusa uses [Zod](https://zod.dev/) to create validation schemas. These schemas are then used to validate incoming request bodies or query parameters.

Learn more about API route validation in [this chapter](https://docs.medusajs.com/learn/fundamentals/api-routes/validation/index.html.md).

You create a validation schema in a TypeScript or JavaScript file under a sub-directory of the `src/api` directory. So, create the file `src/api/admin/brands/validators.ts` with the following content:

![Directory structure of Medusa application after adding validators file](https://res.cloudinary.com/dza7lstvk/image/upload/v1732869806/Medusa%20Book/brand-route-dir-overview-1_yfyjss.jpg)

```ts title="src/api/admin/brands/validators.ts"
import { z } from "zod"

export const PostAdminCreateBrand = z.object({
  name: z.string(),
})
```

You export a validation schema that expects in the request body an object having a `name` property whose value is a string.

You can then replace `PostAdminCreateBrandType` in `src/api/admin/brands/route.ts` with the following:

```ts title="src/api/admin/brands/route.ts"
// ...
import { z } from "zod"
import { PostAdminCreateBrand } from "./validators"

type PostAdminCreateBrandType = z.infer<typeof PostAdminCreateBrand>

// ...
```

***

## 3. Add Validation Middleware

A middleware is a function executed before the route handler when a request is sent to an API Route. It's useful to guard API routes, parse custom request body types, and apply validation on an API route.

Learn more about middlewares in [this chapter](https://docs.medusajs.com/learn/fundamentals/api-routes/middlewares/index.html.md).

Medusa provides a `validateAndTransformBody` middleware that accepts a Zod validation schema and returns a response error if a request is sent with body parameters that don't satisfy the validation schema.

Middlewares are defined in the special file `src/api/middlewares.ts`. So, to add the validation middleware on the API route you created in the previous step, create the file `src/api/middlewares.ts` with the following content:

![Directory structure of the Medusa application after adding the middleware](https://res.cloudinary.com/dza7lstvk/image/upload/v1732869977/Medusa%20Book/brand-route-dir-overview-3_kcx511.jpg)

```ts title="src/api/middlewares.ts"
import { 
  defineMiddlewares,
  validateAndTransformBody,
} from "@medusajs/framework/http"
import { PostAdminCreateBrand } from "./admin/brands/validators"

export default defineMiddlewares({
  routes: [
    {
      matcher: "/admin/brands",
      method: "POST",
      middlewares: [
        validateAndTransformBody(PostAdminCreateBrand),
      ],
    },
  ],
})
```

You define the middlewares using the `defineMiddlewares` function and export its returned value. The function accepts an object having a `routes` property, which is an array of middleware objects.

In the middleware object, you define three properties:

- `matcher`: a string or regular expression indicating the API route path to apply the middleware on. You pass the create brand's route `/admin/brand`.
- `method`: The HTTP method to restrict the middleware to, which is `POST`.
- `middlewares`: An array of middlewares to apply on the route. You pass the `validateAndTransformBody` middleware, passing it the Zod schema you created earlier.

The Medusa application will now validate the body parameters of `POST` requests sent to `/admin/brands` to ensure they match the Zod validation schema. If not, an error is returned in the response specifying the issues to fix in the request body.

***

## Test API Route

To test out the API route, start the Medusa application with the following command:

```bash npm2yarn
npm run dev
```

Since the `/admin/brands` API route has a `/admin` prefix, it's only accessible by authenticated admin users.

So, to retrieve an authenticated token of your admin user, send a `POST` request to the `/auth/user/emailpass` API Route:

```bash
curl -X POST 'http://localhost:9000/auth/user/emailpass' \
-H 'Content-Type: application/json' \
--data-raw '{
    "email": "admin@medusa-test.com",
    "password": "supersecret"
}'
```

Make sure to replace the email and password with your admin user's credentials.

Don't have an admin user? Refer to [this guide](https://docs.medusajs.com/learn/installation#create-medusa-admin-user/index.html.md).

Then, send a `POST` request to `/admin/brands`, passing the token received from the previous request in the `Authorization` header:

```bash
curl -X POST 'http://localhost:9000/admin/brands' \
-H 'Content-Type: application/json' \
-H 'Authorization: Bearer {token}' \
--data '{
    "name": "Acme"
}'
```

This returns the created brand in the response:

```json title="Example Response"
{
  "brand": {
    "id": "01J7AX9ES4X113HKY6C681KDZJ",
    "name": "Acme",
    "created_at": "2024-09-09T08:09:34.244Z",
    "updated_at": "2024-09-09T08:09:34.244Z"
  }
}
```

***

## Summary

By following the previous example chapters, you implemented a custom feature that allows admin users to create a brand. You did that by:

1. Creating a module that defines and manages a `brand` table in the database.
2. Creating a workflow that uses the module's service to create a brand record, and implements the compensation logic to delete that brand in case an error occurs.
3. Creating an API route that allows admin users to create a brand.

***

## Next Steps: Associate Brand with Product

Now that you have brands in your Medusa application, you want to associate a brand with a product, which is defined in the [Product Module](https://docs.medusajs.com/resources/commerce-modules/product/index.html.md).

In the next chapters, you'll learn how to build associations between data models defined in different modules.


# Guide: Implement Brand Module

In this chapter, you'll build a Brand Module that adds a `brand` table to the database and provides data-management features for it.

A module is a reusable package of functionalities related to a single domain or integration. Medusa comes with multiple pre-built modules for core commerce needs, such as the [Cart Module](https://docs.medusajs.com/resources/commerce-modules/cart/index.html.md) that holds the data models and business logic for cart operations.

In a module, you create data models and business logic to manage them. In the next chapters, you'll see how you use the module to build commerce features.

![Diagram showcasing an overview of the Brand Module](https://res.cloudinary.com/dza7lstvk/image/upload/v1746546820/Medusa%20Resources/brand-module_pg86gm.jpg)

Learn more about modules in [this chapter](https://docs.medusajs.com/learn/fundamentals/modules/index.html.md).

## 1. Create Module Directory

Modules are created in a sub-directory of `src/modules`. So, start by creating the directory `src/modules/brand` that will hold the Brand Module's files.

![Directory structure in Medusa project after adding the brand directory](https://res.cloudinary.com/dza7lstvk/image/upload/v1732868844/Medusa%20Book/brand-dir-overview-1_hxwvgx.jpg)

***

## 2. Create Data Model

A data model represents a table in the database. You create data models using Medusa's Data Model Language (DML). It simplifies defining a table's columns, relations, and indexes with straightforward methods and configurations.

Learn more about data models in [this chapter](https://docs.medusajs.com/learn/fundamentals/modules#1-create-data-model/index.html.md).

You create a data model in a TypeScript or JavaScript file under the `models` directory of a module. So, to create a data model that represents a new `brand` table in the database, create the file `src/modules/brand/models/brand.ts` with the following content:

![Directory structure in module after adding the brand data model](https://res.cloudinary.com/dza7lstvk/image/upload/v1732868920/Medusa%20Book/brand-dir-overview-2_lexhdl.jpg)

```ts title="src/modules/brand/models/brand.ts"
import { model } from "@medusajs/framework/utils"

export const Brand = model.define("brand", {
  id: model.id().primaryKey(),
  name: model.text(),
})
```

You create a `Brand` data model which has an `id` primary key property, and a `name` text property.

You define the data model using the `define` method of the DML. It accepts two parameters:

1. The first one is the name of the data model's table in the database. Use snake-case names.
2. The second is an object, which is the data model's schema.

Learn about other property types in [this chapter](https://docs.medusajs.com/learn/fundamentals/data-models/properties/index.html.md).

***

## 3. Create Module Service

You perform database operations on your data models in a service, which is a class exported by the module and acts like an interface to its functionalities.

In this step, you'll create the Brand Module's service that provides methods to manage the `Brand` data model. In the next chapters, you'll use this service when exposing custom features that involve managing brands.

Learn more about services in [this chapter](https://docs.medusajs.com/learn/fundamentals/modules#2-create-service/index.html.md).

You define a service in a `service.ts` or `service.js` file at the root of your module's directory. So, create the file `src/modules/brand/service.ts` with the following content:

![Directory structure in module after adding the service](https://res.cloudinary.com/dza7lstvk/image/upload/v1732868984/Medusa%20Book/brand-dir-overview-3_jo7baj.jpg)

```ts title="src/modules/brand/service.ts" highlights={serviceHighlights}
import { MedusaService } from "@medusajs/framework/utils"
import { Brand } from "./models/brand"

class BrandModuleService extends MedusaService({
  Brand,
}) {

}

export default BrandModuleService
```

The `BrandModuleService` extends a class returned by `MedusaService` from the Modules SDK. This function generates a class with data-management methods for your module's data models.

The `MedusaService` function receives an object of the module's data models as a parameter, and generates methods to manage those data models. So, the `BrandModuleService` now has methods like `createBrands` and `retrieveBrand` to manage the `Brand` data model.

You'll use these methods in the [next chapter](https://docs.medusajs.com/learn/customization/custom-features/workflow/index.html.md).

Find a reference of all generated methods in [this guide](https://docs.medusajs.com/resources/service-factory-reference/index.html.md).

***

## 4. Export Module Definition

A module must export a definition that tells Medusa the name of the module and its main service. This definition is exported in an `index.ts` file at the module's root directory.

So, to export the Brand Module's definition, create the file `src/modules/brand/index.ts` with the following content:

![Directory structure in module after adding the definition file](https://res.cloudinary.com/dza7lstvk/image/upload/v1732869045/Medusa%20Book/brand-dir-overview-4_nf8ymw.jpg)

```ts title="src/modules/brand/index.ts"
import { Module } from "@medusajs/framework/utils"
import BrandModuleService from "./service"

export const BRAND_MODULE = "brand"

export default Module(BRAND_MODULE, {
  service: BrandModuleService,
})
```

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

1. The module's name (`brand`). You'll use this name when you use this module in other customizations.
2. An object with a required property `service` indicating the module's main service.

You export `BRAND_MODULE` to reference the module's name more reliably in other customizations.

***

## 5. Add Module to Medusa's Configurations

To start using your module, you must add it to Medusa's configurations in `medusa-config.ts`.

The object passed to `defineConfig` in `medusa-config.ts` accepts a `modules` property, whose value is an array of modules to add to the application. So, add the following in `medusa-config.ts`:

```ts title="medusa-config.ts"
module.exports = defineConfig({
  // ...
  modules: [
    {
      resolve: "./src/modules/brand",
    },
  ],
})
```

The Brand Module is now added to your Medusa application. You'll start using it in the [next chapter](https://docs.medusajs.com/learn/customization/custom-features/workflow/index.html.md).

***

## 6. Generate and Run Migrations

A migration is a TypeScript or JavaScript file that defines database changes made by a module. Migrations ensure that your module is re-usable and removes friction when working in a team, making it easy to reflect changes across team members' databases.

Learn more about migrations in [this chapter](https://docs.medusajs.com/learn/fundamentals/modules#5-generate-migrations/index.html.md).

[Medusa's CLI tool](https://docs.medusajs.com/resources/medusa-cli/index.html.md) allows you to generate migration files for your module, then run those migrations to reflect the changes in the database. So, run the following commands in your Medusa application's directory:

```bash
npx medusa db:generate brand
npx medusa db:migrate
```

The `db:generate` command accepts as an argument the name of the module to generate the migrations for, and the `db:migrate` command runs all migrations that haven't been run yet in the Medusa application.

***

## Next Step: Create Brand Workflow

The Brand Module now creates a `brand` table in the database and provides a class to manage its records.

In the next chapter, you'll implement the functionality to create a brand in a workflow. You'll then use that workflow in a later chapter to expose an endpoint that allows admin users to create a brand.


# Guide: Create Brand Workflow

This chapter builds on the work from the [previous chapter](https://docs.medusajs.com/learn/customization/custom-features/module/index.html.md) where you created a Brand Module.

After adding custom modules to your application, you build commerce features around them using workflows. A workflow is a series of queries and actions, called steps, that complete a task spanning across modules. You construct a workflow similar to a regular function, but it's a special function that allows you to define roll-back logic, retry configurations, and more advanced features.

The workflow you'll create in this chapter will use the Brand Module's service to implement the feature of creating a brand. In the [next chapter](https://docs.medusajs.com/learn/customization/custom-features/api-route/index.html.md), you'll expose an API route that allows admin users to create a brand, and you'll use this workflow in the route's implementation.

Learn more about workflows in [this chapter](https://docs.medusajs.com/learn/fundamentals/workflows/index.html.md).

### Prerequisites

- [Brand Module](https://docs.medusajs.com/learn/customization/custom-features/module/index.html.md)

***

## 1. Create createBrandStep

A workflow consists of a series of steps, each step created in a TypeScript or JavaScript file under the `src/workflows` directory. A step is defined using `createStep` from the Workflows SDK

The workflow you're creating in this guide has one step to create the brand. So, create the file `src/workflows/create-brand.ts` with the following content:

![Directory structure in the Medusa project after adding the file for createBrandStep](https://res.cloudinary.com/dza7lstvk/image/upload/v1732869184/Medusa%20Book/brand-workflow-dir-overview-1_fjvf5j.jpg)

```ts title="src/workflows/create-brand.ts"
import {
  createStep,
  StepResponse,
} from "@medusajs/framework/workflows-sdk"
import { BRAND_MODULE } from "../modules/brand"
import BrandModuleService from "../modules/brand/service"

export type CreateBrandStepInput = {
  name: string
}

export const createBrandStep = createStep(
  "create-brand-step",
  async (input: CreateBrandStepInput, { container }) => {
    const brandModuleService: BrandModuleService = container.resolve(
      BRAND_MODULE
    )

    const brand = await brandModuleService.createBrands(input)

    return new StepResponse(brand, brand.id)
  }
)
```

You create a `createBrandStep` using the `createStep` function. It accepts the step's unique name as a first parameter, and the step's function as a second parameter.

The step function receives two parameters: input passed to the step when it's invoked, and an object of general context and configurations. This object has a `container` property, which is the Medusa container.

The [Medusa container](https://docs.medusajs.com/learn/fundamentals/medusa-container/index.html.md) is a registry of Framework and commerce tools accessible in your customizations, such as a workflow's step. The Medusa application registers the services of core and custom modules in the container, allowing you to resolve and use them.

So, In the step function, you use the Medusa container to resolve the Brand Module's service and use its generated `createBrands` method, which accepts an object of brands to create.

Learn more about the generated `create` method's usage in [this reference](https://docs.medusajs.com/resources/service-factory-reference/methods/create/index.html.md).

A step must return an instance of `StepResponse`. Its first parameter is the data returned by the step, and the second is the data passed to the compensation function, which you'll learn about next.

### Add Compensation Function to Step

You define for each step a compensation function that's executed when an error occurs in the workflow. The compensation function defines the logic to roll-back the changes made by the step. This ensures your data remains consistent if an error occurs, which is especially useful when you integrate third-party services.

Learn more about the compensation function in [this chapter](https://docs.medusajs.com/learn/fundamentals/workflows/compensation-function/index.html.md).

To add a compensation function to the `createBrandStep`, pass it as a third parameter to `createStep`:

```ts title="src/workflows/create-brand.ts"
export const createBrandStep = createStep(
  // ...
  async (id: string, { container }) => {
    const brandModuleService: BrandModuleService = container.resolve(
      BRAND_MODULE
    )

    await brandModuleService.deleteBrands(id)
  }
)
```

The compensation function's first parameter is the brand's ID which you passed as a second parameter to the step function's returned `StepResponse`. It also accepts a context object with a `container` property as a second parameter, similar to the step function.

In the compensation function, you resolve the Brand Module's service from the Medusa container, then use its generated `deleteBrands` method to delete the brand created by the step. This method accepts the ID of the brand to delete.

Learn more about the generated `delete` method's usage in [this reference](https://docs.medusajs.com/resources/service-factory-reference/methods/delete/index.html.md).

So, if an error occurs during the workflow's execution, the brand that was created by the step is deleted to maintain data consistency.

***

## 2. Create createBrandWorkflow

You can now create the workflow that runs the `createBrandStep`. A workflow is created in a TypeScript or JavaScript file under the `src/workflows` directory. In the file, you use `createWorkflow` from the Workflows SDK to create the workflow.

Add the following content in the same `src/workflows/create-brand.ts` file:

```ts title="src/workflows/create-brand.ts"
// other imports...
import {
  // ...
  createWorkflow,
  WorkflowResponse,
} from "@medusajs/framework/workflows-sdk"

// ...

type CreateBrandWorkflowInput = {
  name: string
}

export const createBrandWorkflow = createWorkflow(
  "create-brand",
  (input: CreateBrandWorkflowInput) => {
    const brand = createBrandStep(input)

    return new WorkflowResponse(brand)
  }
)
```

You create the `createBrandWorkflow` using the `createWorkflow` function. This function accepts two parameters: the workflow's unique name, and the workflow's constructor function holding the workflow's implementation.

The constructor function accepts the workflow's input as a parameter. In the function, you invoke the `createBrandStep` you created in the previous step to create a brand.

A workflow must return an instance of `WorkflowResponse`. It accepts as a parameter the data to return to the workflow's executor.

***

## Next Steps: Expose Create Brand API Route

You now have a `createBrandWorkflow` that you can execute to create a brand.

In the next chapter, you'll add an API route that allows admin users to create a brand. You'll learn how to create the API route, and execute in it the workflow you implemented in this chapter.


# Create Brands UI Route in Admin

In this chapter, you'll add a UI route to the admin dashboard that shows all [brands](https://docs.medusajs.com/learn/customization/custom-features/module/index.html.md) in a new page. You'll retrieve the brands from the server and display them in a table with pagination.

### Prerequisites

- [Brands Module](https://docs.medusajs.com/learn/customization/custom-features/module/index.html.md)

## 1. Get Brands API Route

In a [previous chapter](https://docs.medusajs.com/learn/customization/extend-features/query-linked-records/index.html.md), you learned how to add an API route that retrieves brands and their products using [Query](https://docs.medusajs.com/learn/fundamentals/module-links/query/index.html.md). You'll expand that API route to support pagination, so that on the admin dashboard you can show the brands in a paginated table.

Replace or create the `GET` API route at `src/api/admin/brands/route.ts` with the following:

```ts title="src/api/admin/brands/route.ts" highlights={apiRouteHighlights}
// other imports...
import {
  MedusaRequest,
  MedusaResponse,
} from "@medusajs/framework/http"

export const GET = async (
  req: MedusaRequest,
  res: MedusaResponse
) => {
  const query = req.scope.resolve("query")
  
  const { 
    data: brands, 
    metadata: { count, take, skip } = {},
  } = await query.graph({
    entity: "brand",
    ...req.queryConfig,
  })

  res.json({ 
    brands,
    count,
    limit: take,
    offset: skip,
  })
}
```

In the API route, you use Query's `graph` method to retrieve the brands. In the method's object parameter, you spread the `queryConfig` property of the request object. This property holds configurations for pagination and retrieved fields.

The query configurations are combined from default configurations, which you'll add next, and the request's query parameters:

- `fields`: The fields to retrieve in the brands.
- `limit`: The maximum number of items to retrieve.
- `offset`: The number of items to skip before retrieving the returned items.

When you pass pagination configurations to the `graph` method, the returned object has the pagination's details in a `metadata` property, whose value is an object having the following properties:

- `count`: The total count of items.
- `take`: The maximum number of items returned in the `data` array.
- `skip`: The number of items skipped before retrieving the returned items.

You return in the response the retrieved brands and the pagination configurations.

Learn more about pagination with Query in [this chapter](https://docs.medusajs.com/learn/fundamentals/module-links/query#apply-pagination/index.html.md).

***

## 2. Add Default Query Configurations

Next, you'll set the default query configurations of the above API route and allow passing query parameters to change the configurations.

Medusa provides a `validateAndTransformQuery` middleware that validates the accepted query parameters for a request and sets the default Query configuration. So, in `src/api/middlewares.ts`, add a new middleware configuration object:

```ts title="src/api/middlewares.ts"
import { 
  defineMiddlewares,
  validateAndTransformQuery,
} from "@medusajs/framework/http"
import { createFindParams } from "@medusajs/medusa/api/utils/validators"
// other imports...

export const GetBrandsSchema = createFindParams()

export default defineMiddlewares({
  routes: [
    // ...
    {
      matcher: "/admin/brands",
      method: "GET",
      middlewares: [
        validateAndTransformQuery(
          GetBrandsSchema,
          {
            defaults: [
              "id",
              "name",
              "products.*",
            ],
            isList: true,
          }
        ),
      ],
    },

  ],
})
```

You apply the `validateAndTransformQuery` middleware on the `GET /admin/brands` API route. The middleware accepts two parameters:

- A [Zod](https://zod.dev/) schema that a request's query parameters must satisfy. Medusa provides `createFindParams` that generates a Zod schema with the following properties:
  - `fields`: A comma-separated string indicating the fields to retrieve.
  - `limit`: The maximum number of items to retrieve.
  - `offset`: The number of items to skip before retrieving the returned items.
  - `order`: The name of the field to sort the items by. Learn more about sorting in [the API reference](https://docs.medusajs.com/api/admin#sort-order)
- An object of Query configurations having the following properties:
  - `defaults`: An array of default fields and relations to retrieve.
  - `isList`: Whether the API route returns a list of items.

By applying the above middleware, you can pass pagination configurations to `GET /admin/brands`, which will return a paginated list of brands. You'll see how it works when you create the UI route.

Learn more about using the `validateAndTransformQuery` middleware to configure Query in [this chapter](https://docs.medusajs.com/learn/fundamentals/module-links/query#request-query-configurations/index.html.md).

***

## 3. Initialize JS SDK

In your custom UI route, you'll retrieve the brands by sending a request to the Medusa server. Medusa has a [JS SDK](https://docs.medusajs.com/resources/js-sdk/index.html.md) that simplifies sending requests to the core API route.

If you didn't follow the [previous chapter](https://docs.medusajs.com/learn/customization/customize-admin/widget/index.html.md), create the file `src/admin/lib/sdk.ts` with the following content:

![The directory structure of the Medusa application after adding the file](https://res.cloudinary.com/dza7lstvk/image/upload/v1733414606/Medusa%20Book/brands-admin-dir-overview-1_jleg0t.jpg)

```ts title="src/admin/lib/sdk.ts"
import Medusa from "@medusajs/js-sdk"

export const sdk = new Medusa({
  baseUrl: import.meta.env.VITE_BACKEND_URL || "/",
  debug: import.meta.env.DEV,
  auth: {
    type: "session",
  },
})
```

You initialize the SDK passing it the following options:

- `baseUrl`: The URL to the Medusa server.
- `debug`: Whether to enable logging debug messages. This should only be enabled in development.
- `auth.type`: The authentication method used in the client application, which is `session` in the Medusa Admin dashboard.

Notice that you use `import.meta.env` to access environment variables in your customizations because the Medusa Admin is built on top of Vite. Learn more in [this chapter](https://docs.medusajs.com/learn/fundamentals/admin/environment-variables/index.html.md).

You can now use the SDK to send requests to the Medusa server.

Learn more about the JS SDK and its options in [this reference](https://docs.medusajs.com/resources/js-sdk/index.html.md).

***

## 4. Add a UI Route to Show Brands

You'll now add the UI route that shows the paginated list of brands. A UI route is a React component created in a `page.tsx` file under a sub-directory of `src/admin/routes`. The file's path relative to src/admin/routes determines its path in the dashboard.

Learn more about UI routes in [this chapter](https://docs.medusajs.com/learn/fundamentals/admin/ui-routes/index.html.md).

So, to add the UI route at the `localhost:9000/app/brands` path, create the file `src/admin/routes/brands/page.tsx` with the following content:

![Directory structure of the Medusa application after adding the UI route.](https://res.cloudinary.com/dza7lstvk/image/upload/v1733472011/Medusa%20Book/brands-admin-dir-overview-3_syytld.jpg)

```tsx title="src/admin/routes/brands/page.tsx" highlights={uiRouteHighlights}
import { defineRouteConfig } from "@medusajs/admin-sdk"
import { TagSolid } from "@medusajs/icons"
import { 
  Container,
} from "@medusajs/ui"
import { useQuery } from "@tanstack/react-query"
import { sdk } from "../../lib/sdk"
import { useMemo, useState } from "react"

const BrandsPage = () => {
  // TODO retrieve brands

  return (
    <Container className="divide-y p-0">
      {/* TODO show brands */}
    </Container>
  )
}

export const config = defineRouteConfig({
  label: "Brands",
  icon: TagSolid,
})

export default BrandsPage
```

A route's file must export the React component that will be rendered in the new page. It must be the default export of the file. You can also export configurations that add a link in the sidebar for the UI route. You create these configurations using `defineRouteConfig` from the Admin Extension SDK.

So far, you only show a container. In admin customizations, use components from the [Medusa UI package](https://docs.medusajs.com/ui/index.html.md) to maintain a consistent user interface and design in the dashboard.

### Retrieve Brands From API Route

You'll now update the UI route to retrieve the brands from the API route you added earlier.

First, add the following type in `src/admin/routes/brands/page.tsx`:

```tsx title="src/admin/routes/brands/page.tsx"
type Brand = {
  id: string
  name: string
}
type BrandsResponse = {
  brands: Brand[]
  count: number
  limit: number
  offset: number
}
```

You define the type for a brand, and the type of expected response from the `GET /admin/brands` API route.

To display the brands, you'll use Medusa UI's [DataTable](https://docs.medusajs.com/ui/components/data-table/index.html.md) component. So, add the following imports in `src/admin/routes/brands/page.tsx`:

```tsx title="src/admin/routes/brands/page.tsx"
import { 
  // ...
  Heading,
  createDataTableColumnHelper,
  DataTable,
  DataTablePaginationState,
  useDataTable,
} from "@medusajs/ui"
```

You import the `DataTable` component and the following utilities:

- `createDataTableColumnHelper`: A utility to create columns for the data table.
- `DataTablePaginationState`: A type that holds the pagination state of the data table.
- `useDataTable`: A hook to initialize and configure the data table.

You also import the `Heading` component to show a heading above the data table.

Next, you'll define the table's columns. Add the following before the `BrandsPage` component:

```tsx title="src/admin/routes/brands/page.tsx"
const columnHelper = createDataTableColumnHelper<Brand>()

const columns = [
  columnHelper.accessor("id", {
    header: "ID",
  }),
  columnHelper.accessor("name", {
    header: "Name",
  }),
]
```

You use the `createDataTableColumnHelper` utility to create columns for the data table. You define two columns for the ID and name of the brands.

Then, replace the `// TODO retrieve brands` in the component with the following:

```tsx title="src/admin/routes/brands/page.tsx" highlights={queryHighlights}
const limit = 15
const [pagination, setPagination] = useState<DataTablePaginationState>({
  pageSize: limit,
  pageIndex: 0,
})
const offset = useMemo(() => {
  return pagination.pageIndex * limit
}, [pagination])

const { data, isLoading } = useQuery<BrandsResponse>({
  queryFn: () => sdk.client.fetch(`/admin/brands`, {
    query: {
      limit,
      offset,
    },
  }),
  queryKey: [["brands", limit, offset]],
})

// TODO configure data table
```

To enable pagination in the `DataTable` component, you need to define a state variable of type `DataTablePaginationState`. It's an object having the following properties:

- `pageSize`: The maximum number of items per page. You set it to `15`.
- `pageIndex`: A zero-based index of the current page of items.

You also define a memoized `offset` value that indicates the number of items to skip before retrieving the current page's items.

Then, you use `useQuery` from [Tanstack (React) Query](https://tanstack.com/query/latest) to query the Medusa server. Tanstack Query provides features like asynchronous state management and optimized caching.

Do not install Tanstack Query as that will cause unexpected errors in your development. If you prefer installing it for better auto-completion in your code editor, make sure to install `v5.64.2` as a development dependency.

In the `queryFn` function that executes the query, you use the JS SDK's `client.fetch` method to send a request to your custom API route. The first parameter is the route's path, and the second is an object of request configuration and data. You pass the query parameters in the `query` property.

This sends a request to the [Get Brands API route](#1-get-brands-api-route), passing the pagination query parameters. Whenever `currentPage` is updated, the `offset` is also updated, which will send a new request to retrieve the brands for the current page.

### Display Brands Table

Finally, you'll display the brands in a  data table. Replace the `// TODO configure data table` in the component with the following:

```tsx title="src/admin/routes/brands/page.tsx"
const table = useDataTable({
  columns,
  data: data?.brands || [],
  getRowId: (row) => row.id,
  rowCount: data?.count || 0,
  isLoading,
  pagination: {
    state: pagination,
    onPaginationChange: setPagination,
  },
})
```

You use the `useDataTable` hook to initialize and configure the data table. It accepts an object with the following properties:

- `columns`: The columns of the data table. You created them using the `createDataTableColumnHelper` utility.
- `data`: The brands to display in the table.
- `getRowId`: A function that returns a unique identifier for a row.
- `rowCount`: The total count of items. This is used to determine the number of pages.
- `isLoading`: A boolean indicating whether the data is loading.
- `pagination`: An object to configure pagination. It accepts the following properties:
  - `state`: The pagination state of the data table.
  - `onPaginationChange`: A function to update the pagination state.

Then, replace the `{/* TODO show brands */}` in the return statement with the following:

```tsx title="src/admin/routes/brands/page.tsx"
<DataTable instance={table}>
  <DataTable.Toolbar className="flex flex-col items-start justify-between gap-2 md:flex-row md:items-center">
    <Heading>Brands</Heading>
  </DataTable.Toolbar>
  <DataTable.Table />
  <DataTable.Pagination />
</DataTable>
```

This renders the data table that shows the brands with pagination. The `DataTable` component accepts the `instance` prop, which is the object returned by the `useDataTable` hook.

***

## Test it Out

To test out the UI route, start the Medusa application:

```bash npm2yarn
npm run dev
```

Then, open the admin dashboard at `http://localhost:9000/app`. After you log in, you'll find a new "Brands" sidebar item. Click on it to see the brands in your store. You can also go to `http://localhost:9000/app/brands` to see the page.

![A new sidebar item is added for the new brands UI route. The UI route shows the table of brands with pagination.](https://res.cloudinary.com/dza7lstvk/image/upload/v1733421074/Medusa%20Book/Screenshot_2024-12-05_at_7.46.52_PM_slcdqd.png)

***

## Summary

By following the previous chapters, you:

- Injected a widget into the product details page to show the product's brand.
- Created a UI route in the Medusa Admin that shows the list of brands.

***

## Next Steps: Integrate Third-Party Systems

Your customizations often span across systems, where you need to retrieve data or perform operations in a third-party system.

In the next chapters, you'll learn about the concepts that facilitate integrating third-party systems in your application. You'll integrate a dummy third-party system and sync the brands between it and the Medusa application.


# Guide: Add Product's Brand Widget in Admin

In this chapter, you'll customize the product details page of the Medusa Admin dashboard to show the product's [brand](https://docs.medusajs.com/learn/customization/custom-features/module/index.html.md). You'll create a widget that is injected into a pre-defined zone in the page, and in the widget you'll retrieve the product's brand from the server and display it.

### Prerequisites

- [Brands linked to products](https://docs.medusajs.com/learn/customization/extend-features/define-link/index.html.md)

## 1. Initialize JS SDK

In your custom widget, you'll retrieve the product's brand by sending a request to the Medusa server. Medusa has a [JS SDK](https://docs.medusajs.com/resources/js-sdk/index.html.md) that simplifies sending requests to the server's API routes.

So, you'll start by configuring the JS SDK. Create the file `src/admin/lib/sdk.ts` with the following content:

![The directory structure of the Medusa application after adding the file](https://res.cloudinary.com/dza7lstvk/image/upload/v1733414606/Medusa%20Book/brands-admin-dir-overview-1_jleg0t.jpg)

```ts title="src/admin/lib/sdk.ts"
import Medusa from "@medusajs/js-sdk"

export const sdk = new Medusa({
  baseUrl: import.meta.env.VITE_BACKEND_URL || "/",
  debug: import.meta.env.DEV,
  auth: {
    type: "session",
  },
})
```

You initialize the SDK passing it the following options:

- `baseUrl`: The URL to the Medusa server.
- `debug`: Whether to enable logging debug messages. This should only be enabled in development.
- `auth.type`: The authentication method used in the client application, which is `session` in the Medusa Admin dashboard.

Notice that you use `import.meta.env` to access environment variables in your customizations because the Medusa Admin is built on top of Vite. Learn more in [this chapter](https://docs.medusajs.com/learn/fundamentals/admin/environment-variables/index.html.md).

You can now use the SDK to send requests to the Medusa server.

Learn more about the JS SDK and its options in [this reference](https://docs.medusajs.com/resources/js-sdk/index.html.md).

***

## 2. Add Widget to Product Details Page

You'll now add a widget to the product-details page. A widget is a React component that's injected into pre-defined zones in the Medusa Admin dashboard. It's created in a `.tsx` file under the `src/admin/widgets` directory.

Learn more about widgets in [this documentation](https://docs.medusajs.com/learn/fundamentals/admin/widgets/index.html.md).

To create a widget that shows a product's brand in its details page, create the file `src/admin/widgets/product-brand.tsx` with the following content:

![Directory structure of the Medusa application after adding the widget](https://res.cloudinary.com/dza7lstvk/image/upload/v1733414684/Medusa%20Book/brands-admin-dir-overview-2_eq5xhi.jpg)

```tsx title="src/admin/widgets/product-brand.tsx" highlights={highlights}
import { defineWidgetConfig } from "@medusajs/admin-sdk"
import { DetailWidgetProps, AdminProduct } from "@medusajs/framework/types"
import { clx, Container, Heading, Text } from "@medusajs/ui"
import { useQuery } from "@tanstack/react-query"
import { sdk } from "../lib/sdk"

type AdminProductBrand = AdminProduct & {
  brand?: {
    id: string
    name: string
  }
}

const ProductBrandWidget = ({ 
  data: product,
}: DetailWidgetProps<AdminProduct>) => {
  const { data: queryResult } = useQuery({
    queryFn: () => sdk.admin.product.retrieve(product.id, {
      fields: "+brand.*",
    }),
    queryKey: [["product", product.id]],
  })
  const brandName = (queryResult?.product as AdminProductBrand)?.brand?.name

  return (
    <Container className="divide-y p-0">
      <div className="flex items-center justify-between px-6 py-4">
        <div>
          <Heading level="h2">Brand</Heading>
        </div>
      </div>
      <div
        className={clx(
          `text-ui-fg-subtle grid grid-cols-2 items-center px-6 py-4`
        )}
      >
        <Text size="small" weight="plus" leading="compact">
          Name
        </Text>

        <Text
          size="small"
          leading="compact"
          className="whitespace-pre-line text-pretty"
        >
          {brandName || "-"}
        </Text>
      </div>
    </Container>
  )
}

export const config = defineWidgetConfig({
  zone: "product.details.before",
})

export default ProductBrandWidget
```

A widget's file must export:

- A React component to be rendered in the specified injection zone. The component must be the file's default export.
- A configuration object created with `defineWidgetConfig` from the Admin Extension SDK. The function receives an object as a parameter that has a `zone` property, whose value is the zone to inject the widget to.

Since the widget is injected at the top of the product details page, the widget receives the product's details as a parameter.

In the widget, you use [Tanstack (React) Query](https://tanstack.com/query/latest) to query the Medusa server. Tanstack Query provides features like asynchronous state management and optimized caching. In the `queryFn` function that executes the query, you use the JS SDK to send a request to the [Get Product API Route](https://docs.medusajs.com/api/admin#products_getproductsid), passing `+brand.*` in the `fields` query parameter to retrieve the product's brand.

Do not install Tanstack Query as that will cause unexpected errors in your development. If you prefer installing it for better auto-completion in your code editor, make sure to install `v5.64.2` as a development dependency.

You then render a section that shows the brand's name. In admin customizations, use components from the [Medusa UI package](https://docs.medusajs.com/ui/index.html.md) to maintain a consistent user interface and design in the dashboard.

***

## Test it Out

To test out your widget, start the Medusa application:

```bash npm2yarn
npm run dev
```

Then, open the admin dashboard at `http://localhost:9000/app`. After you log in, open the page of a product that has a brand. You'll see a new section at the top showing the brand's name.

![The widget is added as the first section of the product details page.](https://res.cloudinary.com/dza7lstvk/image/upload/v1733414415/Medusa%20Book/Screenshot_2024-12-05_at_5.59.25_PM_y85m14.png)

***

## Admin Components Guides

When building your widget, you may need more complicated components. For example, you may add a form to the above widget to set the product's brand.

The [Admin Components guides](https://docs.medusajs.com/resources/admin-components/index.html.md) show you how to build and use common components in the Medusa Admin, such as forms, tables, JSON data viewer, and more. The components in the guides also follow the Medusa Admin's design convention.

***

## Next Chapter: Add UI Route for Brands

In the next chapter, you'll add a UI route that displays the list of brands in your application and allows admin users.


# Guide: Extend Create Product Flow

After linking the [custom Brand data model](https://docs.medusajs.com/learn/customization/custom-features/module/index.html.md) and Medusa's [Product Module](https://docs.medusajs.com/resources/commerce-modules/product/index.html.md) in the [previous chapter](https://docs.medusajs.com/learn/customization/extend-features/define-link/index.html.md), you'll extend the create product workflow and API route to allow associating a brand with a product.

Some API routes, including the [Create Product API route](https://docs.medusajs.com/api/admin#products_postproducts), accept an `additional_data` request body parameter. This parameter can hold custom data that's passed to the [hooks](https://docs.medusajs.com/learn/fundamentals/workflows/workflow-hooks/index.html.md) of the workflow executed in the API route, allowing you to consume those hooks and perform actions with the custom data.

So, in this chapter, to extend the create product flow and associate a brand with a product, you will:

- Consume the [productsCreated](https://docs.medusajs.com/resources/references/medusa-workflows/createProductsWorkflow#productsCreated/index.html.md) hook of the [createProductsWorkflow](https://docs.medusajs.com/resources/references/medusa-workflows/createProductsWorkflow/index.html.md), which is executed within the workflow after the product is created. You'll link the product with the brand passed in the `additional_data` parameter.
- Extend the Create Product API route to allow passing a brand ID in `additional_data`.

To learn more about the `additional_data` property and the API routes that accept additional data, refer to [this chapter](https://docs.medusajs.com/learn/fundamentals/api-routes/additional-data/index.html.md).

### Prerequisites

- [Brand Module](https://docs.medusajs.com/learn/customization/custom-features/module/index.html.md)
- [Defined link between the Brand and Product data models.](https://docs.medusajs.com/learn/customization/extend-features/define-link/index.html.md)

***

## 1. Consume the productsCreated Hook

A workflow hook is a point in a workflow where you can inject a step to perform a custom functionality. Consuming a workflow hook allows you to extend the features of a workflow and, consequently, the API route that uses it.

Learn more about the workflow hooks in [this chapter](https://docs.medusajs.com/learn/fundamentals/workflows/workflow-hooks/index.html.md).

The [createProductsWorkflow](https://docs.medusajs.com/resources/references/medusa-workflows/createProductsWorkflow/index.html.md) used in the [Create Product API route](https://docs.medusajs.com/api/admin#products_postproducts) has a `productsCreated` hook that runs after the product is created. You'll consume this hook to link the created product with the brand specified in the request parameters.

To consume the `productsCreated` hook, create the file `src/workflows/hooks/created-product.ts` with the following content:

![Directory structure after creating the hook's file.](https://res.cloudinary.com/dza7lstvk/image/upload/v1733384338/Medusa%20Book/brands-hook-dir-overview_ltwr5h.jpg)

```ts title="src/workflows/hooks/created-product.ts" highlights={hook1Highlights}
import { createProductsWorkflow } from "@medusajs/medusa/core-flows"
import { StepResponse } from "@medusajs/framework/workflows-sdk"
import { Modules } from "@medusajs/framework/utils"
import { LinkDefinition } from "@medusajs/framework/types"
import { BRAND_MODULE } from "../../modules/brand"
import BrandModuleService from "../../modules/brand/service"

createProductsWorkflow.hooks.productsCreated(
  (async ({ products, additional_data }, { container }) => {
    if (!additional_data?.brand_id) {
      return new StepResponse([], [])
    }

    const brandModuleService: BrandModuleService = container.resolve(
      BRAND_MODULE
    )
    // if the brand doesn't exist, an error is thrown.
    await brandModuleService.retrieveBrand(additional_data.brand_id as string)

    // TODO link brand to product
  })
)
```

Workflows have a special `hooks` property to access its hooks and consume them. Each hook, such as `productsCreated`, accepts a step function as a parameter. The step function accepts the following parameters:

1. An object having an `additional_data` property, which is the custom data passed in the request body under `additional_data`. The object will also have properties passed from the workflow to the hook, which in this case is the `products` property that holds an array of the created products.
2. An object of properties related to the step's context. It has a `container` property whose value is the [Medusa container](https://docs.medusajs.com/learn/fundamentals/medusa-container/index.html.md) to resolve Framework and commerce tools.

In the step, if a brand ID is passed in `additional_data`, you resolve the Brand Module's service and use its generated `retrieveBrand` method to retrieve the brand by its ID. The `retrieveBrand` method will throw an error if the brand doesn't exist.

### Link Brand to Product

Next, you want to create a link between the created products and the brand. To do so, you use Link, which is a class from the Modules SDK that provides methods to manage linked records.

Learn more about Link in [this chapter](https://docs.medusajs.com/learn/fundamentals/module-links/link/index.html.md).

To use Link in the `productsCreated` hook, replace the `TODO` with the following:

```ts title="src/workflows/hooks/created-product.ts" highlights={hook2Highlights}
const link = container.resolve("link")
const logger = container.resolve("logger")

const links: LinkDefinition[] = []

for (const product of products) {
  links.push({
    [Modules.PRODUCT]: {
      product_id: product.id,
    },
    [BRAND_MODULE]: {
      brand_id: additional_data.brand_id,
    },
  })
}

await link.create(links)

logger.info("Linked brand to products")

return new StepResponse(links, links)
```

You resolve Link from the container. Then you loop over the created products to assemble an array of links to be created. After that, you pass the array of links to Link's `create` method, which will link the product and brand records.

Each property in the link object is the name of a module, and its value is an object having a `{model_name}_id` property, where `{model_name}` is the snake-case name of the module's data model. Its value is the ID of the record to be linked. The link object's properties must be set in the same order as the link configurations passed to `defineLink`.

![Diagram showcasing how the order of defining a link affects creating the link](https://res.cloudinary.com/dza7lstvk/image/upload/v1733386156/Medusa%20Book/remote-link-brand-product-exp_fhjmg4.jpg)

Finally, you return an instance of `StepResponse` returning the created links.

### Dismiss Links in Compensation

You can pass as a second parameter of the hook a compensation function that undoes what the step did. It receives as a first parameter the returned `StepResponse`'s second parameter, and the step context object as a second parameter.

To undo creating the links in the hook, pass the following compensation function as a second parameter to `productsCreated`:

```ts title="src/workflows/hooks/created-product.ts"
createProductsWorkflow.hooks.productsCreated(
  // ...
  (async (links, { container }) => {
    if (!links?.length) {
      return
    }

    const link = container.resolve("link")

    await link.dismiss(links)
  })
)
```

In the compensation function, if the `links` parameter isn't empty, you resolve Link from the container and use its `dismiss` method. This method removes a link between two records. It accepts the same parameter as the `create` method.

***

## 2. Configure Additional Data Validation

Now that you've consumed the `productsCreated` hook, you want to configure the `/admin/products` API route that creates a new product to accept a brand ID in its `additional_data` parameter.

You configure the properties accepted in `additional_data` in the `src/api/middlewares.ts` that exports middleware configurations. So, create the file (or, if already existing, add to the file) `src/api/middlewares.ts` the following content:

![Directory structure after adding the middelwares file](https://res.cloudinary.com/dza7lstvk/image/upload/v1733386868/Medusa%20Book/brands-middleware-dir-overview_uczos1.jpg)

```ts title="src/api/middlewares.ts"
import { defineMiddlewares } from "@medusajs/framework/http"
import { z } from "zod"

// ...

export default defineMiddlewares({
  routes: [
    // ...
    {
      matcher: "/admin/products",
      method: ["POST"],
      additionalDataValidator: {
        brand_id: z.string().optional(),
      },
    },
  ],
})
```

Objects in `routes` accept an `additionalDataValidator` property that configures the validation rules for custom properties passed in the `additional_data` request parameter. It accepts an object whose keys are custom property names, and their values are validation rules created using [Zod](https://zod.dev/).

So, `POST` requests sent to `/admin/products` can now pass the ID of a brand in the `brand_id` property of `additional_data`.

***

## Test it Out

To test it out, first, retrieve the authentication token of your admin user by sending a `POST` request to `/auth/user/emailpass`:

```bash
curl -X POST 'http://localhost:9000/auth/user/emailpass' \
-H 'Content-Type: application/json' \
--data-raw '{
    "email": "admin@medusa-test.com",
    "password": "supersecret"
}'
```

Make sure to replace the email and password in the request body with your user's credentials.

Then, send a `POST` request to `/admin/products` to create a product, and pass in the `additional_data` parameter a brand's ID:

```bash
curl -X POST 'http://localhost:9000/admin/products' \
-H 'Content-Type: application/json' \
-H 'Authorization: Bearer {token}' \
--data '{
    "title": "Product 1",
    "options": [
      {
        "title": "Default option",
        "values": ["Default option value"]
      }
    ],
    "shipping_profile_id": "{shipping_profile_id}",
    "additional_data": {
        "brand_id": "{brand_id}"
    }
}'
```

Make sure to replace `{token}` with the token you received from the previous request, `shipping_profile_id` with the ID of a shipping profile in your application, and `{brand_id}` with the ID of a brand in your application. You can retrieve the ID of a shipping profile either from the Medusa Admin, or the [List Shipping Profiles API route](https://docs.medusajs.com/api/admin#shipping-profiles_getshippingprofiles).

The request creates a product and returns it.

In the Medusa application's logs, you'll find the message `Linked brand to products`, indicating that the workflow hook handler ran and linked the brand to the products.

***

## Next Steps: Query Linked Brands and Products

Now that you've extending the create-product flow to link a brand to it, you want to retrieve the brand details of a product. You'll learn how to do so in the next chapter.


# Guide: Query Product's Brands

In the previous chapters, you [defined a link](https://docs.medusajs.com/learn/customization/extend-features/define-link/index.html.md) between the [custom Brand Module](https://docs.medusajs.com/learn/customization/custom-features/module/index.html.md) and Medusa's [Product Module](https://docs.medusajs.com/resources/commerce-modules/product/index.html.md), then [extended the create-product flow](https://docs.medusajs.com/learn/customization/extend-features/extend-create-product/index.html.md) to link a product to a brand.

In this chapter, you'll learn how to retrieve a product's brand (and vice-versa) in two ways: Using Medusa's existing API route, or in customizations, such as a custom API route.

### Prerequisites

- [Brand Module](https://docs.medusajs.com/learn/customization/custom-features/module/index.html.md)
- [Defined link between the Brand and Product data models.](https://docs.medusajs.com/learn/customization/extend-features/define-link/index.html.md)

***

## Approach 1: Retrieve Brands in Existing API Routes

Medusa's existing API routes accept a `fields` query parameter that allows you to specify the fields and relations of a model to retrieve. So, when you send a request to the [List Products](https://docs.medusajs.com/api/admin#products_getproducts), [Get Product](https://docs.medusajs.com/api/admin#products_getproductsid), or any product-related store or admin routes that accept a `fields` query parameter, you can specify in this parameter to return the product's brands.

Learn more about using the `fields` query parameter to retrieve custom linked data models in the [Retrieve Custom Linked Data Models from Medusa's API Routes](https://docs.medusajs.com/learn/fundamentals/api-routes/retrieve-custom-links/index.html.md) chapter.

For example, send the following request to retrieve the list of products with their brands:

```bash
curl 'http://localhost:9000/admin/products?fields=+brand.*' \
--header 'Authorization: Bearer {token}'
```

Make sure to replace `{token}` with your admin user's authentication token. Learn how to retrieve it in the [API reference](https://docs.medusajs.com/api/store#authentication).

Any product that is linked to a brand will have a `brand` property in its object:

```json title="Example Product Object"
{
  "id": "prod_123",
  // ...
  "brand": {
    "id": "01JEB44M61BRM3ARM2RRMK7GJF",
    "name": "Acme",
    "created_at": "2024-12-05T09:59:08.737Z",
    "updated_at": "2024-12-05T09:59:08.737Z",
    "deleted_at": null
  }
}
```

By using the `fields` query parameter, you don't have to re-create existing API routes to get custom data models that you linked to core data models.

### Limitations: Filtering by Brands in Existing API Routes

While you can retrieve linked records using the `fields` query parameter of an existing API route, you can't filter by linked records.

Instead, you'll have to create a custom API route that uses Query to retrieve linked records with filters, as explained in the [Query documentation](https://docs.medusajs.com/learn/fundamentals/module-links/query#apply-filters-and-pagination-on-linked-records/index.html.md).

***

## Approach 2: Use Query to Retrieve Linked Records

You can also retrieve linked records using Query. Query allows you to retrieve data across modules with filters, pagination, and more. You can resolve Query from the Medusa container and use it in your API route or workflow.

Learn more about Query in [this chapter](https://docs.medusajs.com/learn/fundamentals/module-links/query/index.html.md).

For example, you can create an API route that retrieves brands and their products. If you followed the [Create Brands API route chapter](https://docs.medusajs.com/learn/customization/custom-features/api-route/index.html.md), you'll have the file `src/api/admin/brands/route.ts` with a `POST` API route. Add a new `GET` function to the same file:

```ts title="src/api/admin/brands/route.ts" highlights={highlights}
// other imports...
import {
  MedusaRequest,
  MedusaResponse,
} from "@medusajs/framework/http"

export const GET = async (
  req: MedusaRequest,
  res: MedusaResponse
) => {
  const query = req.scope.resolve("query")
  
  const { data: brands } = await query.graph({
    entity: "brand",
    fields: ["*", "products.*"],
  })

  res.json({ brands })
}
```

This adds a `GET` API route at `/admin/brands`. In the API route, you resolve Query from the Medusa container. Query has a `graph` method that runs a query to retrieve data. It accepts an object having the following properties:

- `entity`: The data model's name as specified in the first parameter of `model.define`.
- `fields`: An array of properties and relations to retrieve. You can pass:
  - A property's name, such as `id`, or `*` for all properties.
  - A relation or linked model's name, such as `products` (use the plural name since brands are linked to list of products). You suffix the name with `.*` to retrieve all its properties.

`graph` returns an object having a `data` property, which is the retrieved brands. You return the brands in the response.

### Test it Out

To test the API route out, send a `GET` request to `/admin/brands`:

```bash
curl 'http://localhost:9000/admin/brands' \
-H 'Authorization: Bearer {token}'
```

Make sure to replace `{token}` with your admin user's authentication token. Learn how to retrieve it in the [API reference](https://docs.medusajs.com/api/store#authentication).

This returns the brands in your store with their linked products. For example:

```json title="Example Response"
{
  "brands": [
    {
      "id": "123",
      // ...
      "products": [
        {
          "id": "prod_123",
          // ...
        }
      ]
    }
  ]
}
```

### Limitations: Filtering by Brand in Query

While you can use Query to retrieve linked records, you can't filter by linked records.

For an alternative approach, refer to the [Query documentation](https://docs.medusajs.com/learn/fundamentals/module-links/query#apply-filters-and-pagination-on-linked-records/index.html.md).

***

## Summary

By following the examples of the previous chapters, you:

- Defined a link between the Brand and Product modules's data models, allowing you to associate a product with a brand.
- Extended the create-product workflow and route to allow setting the product's brand while creating the product.
- Queried a product's brand, and vice versa.

***

## Next Steps: Customize Medusa Admin

Clients, such as the Medusa Admin dashboard, can now use brand-related features, such as creating a brand or setting the brand of a product.

In the next chapters, you'll learn how to customize the Medusa Admin to show a product's brand on its details page, and to show a new page with the list of brands in your store.


# Guide: Define Module Link Between Brand and Product

In this chapter, you'll learn how to define a module link between a brand defined in the [custom Brand Module](https://docs.medusajs.com/learn/customization/custom-features/module/index.html.md), and a product defined in the [Product Module](https://docs.medusajs.com/resources/commerce-modules/product/index.html.md) that's available in your Medusa application out-of-the-box.

Modules are [isolated](https://docs.medusajs.com/learn/fundamentals/modules/isolation/index.html.md) from other resources, ensuring that they're integrated into the Medusa application without side effects. However, you may need to associate data models of different modules, or you're trying to extend data models from Commerce Modules with custom properties. To do that, you define module links.

A module link forms an association between two data models of different modules while maintaining module isolation. You can then manage and query linked records of the data models using Medusa's Modules SDK.

In this chapter, you'll define a module link between the `Brand` data model of the Brand Module, and the `Product` data model of the Product Module. In later chapters, you'll manage and retrieve linked product and brand records.

Learn more about module links in [this chapters](https://docs.medusajs.com/learn/fundamentals/module-links/index.html.md).

### Prerequisites

- [Brand Module having a Brand data model](https://docs.medusajs.com/learn/customization/custom-features/module/index.html.md)

## 1. Define Link

Links are defined in a TypeScript or JavaScript file under the `src/links` directory. The file defines and exports the link using `defineLink` from the Modules SDK.

So, to define a link between the `Product` and `Brand` models, create the file `src/links/product-brand.ts` with the following content:

![The directory structure of the Medusa application after adding the link.](https://res.cloudinary.com/dza7lstvk/image/upload/v1733329897/Medusa%20Book/brands-link-dir-overview_t1rhlp.jpg)

```ts title="src/links/product-brand.ts" highlights={highlights}
import BrandModule from "../modules/brand"
import ProductModule from "@medusajs/medusa/product"
import { defineLink } from "@medusajs/framework/utils"

export default defineLink(
  {
    linkable: ProductModule.linkable.product,
    isList: true,
  },
  BrandModule.linkable.brand
)
```

You import each module's definition object from the `index.ts` file of the module's directory. Each module object has a special `linkable` property that holds the data models' link configurations.

The `defineLink` function accepts two parameters of the same type, which is either:

- The data model's link configuration, which you access from the Module's `linkable` property;
- Or an object that has two properties:
  - `linkable`: the data model's link configuration, which you access from the Module's `linkable` property.
  - `isList`: A boolean indicating whether many records of the data model can be linked to the other model.

So, in the above code snippet, you define a link between the `Product` and `Brand` data models. Since a brand can be associated with multiple products, you enable `isList` in the `Product` model's object.

***

## 2. Sync the Link to the Database

A module link is represented in the database as a table that stores the IDs of linked records. So, after defining the link, run the following command to create the module link's table in the database:

```bash
npx medusa db:migrate
```

This command reflects migrations on the database and syncs module links, which creates a table for the `product-brand` link.

You can also run the `npx medusa db:sync-links` to just sync module links without running migrations.

***

## Next Steps: Extend Create Product Flow

In the next chapter, you'll extend Medusa's workflow and API route that create a product to allow associating a brand with a product. You'll also learn how to link brand and product records.


# Guide: Integrate Third-Party Brand System

In the previous chapters, you've created a [Brand Module](https://docs.medusajs.com/learn/customization/custom-features/module/index.html.md) that adds brands to your application. In this chapter, you'll integrate a dummy Content-Management System (CMS) in a new module. The module's service will provide methods to retrieve and manage brands in the CMS. You'll later use this service to sync data from and to the CMS.

Learn more about modules in [this chapter](https://docs.medusajs.com/learn/fundamentals/modules/index.html.md).

## 1. Create Module Directory

You'll integrate the third-party system in a new CMS Module. So, create the directory `src/modules/cms` that will hold the module's resources.

![Directory structure after adding the directory for the CMS Module](https://res.cloudinary.com/dza7lstvk/image/upload/v1733492447/Medusa%20Book/cms-dir-overview-1_gasguk.jpg)

***

## 2. Create Module Service

Next, you'll create the module's service. It will provide methods to connect and perform actions with the third-party system.

Create the CMS Module's service at `src/modules/cms/service.ts` with the following content:

![Directory structure after adding the CMS Module's service](https://res.cloudinary.com/dza7lstvk/image/upload/v1733492583/Medusa%20Book/cms-dir-overview-2_zwcwh3.jpg)

```ts title="src/modules/cms/service.ts" highlights={serviceHighlights}
import { Logger, ConfigModule } from "@medusajs/framework/types"

export type ModuleOptions = {
  apiKey: string
}

type InjectedDependencies = {
  logger: Logger
  configModule: ConfigModule
}

class CmsModuleService {
  private options_: ModuleOptions
  private logger_: Logger

  constructor({ logger }: InjectedDependencies, options: ModuleOptions) {
    this.logger_ = logger
    this.options_ = options

    // TODO initialize SDK
  }
}

export default CmsModuleService
```

You create a `CmsModuleService` that will hold the methods to connect to the third-party CMS. A service's constructor accepts two parameters:

1. The module's container. Since a module is [isolated](https://docs.medusajs.com/learn/fundamentals/modules/isolation/index.html.md), it has a [local container](https://docs.medusajs.com/learn/fundamentals/modules/container/index.html.md) different than the Medusa container you use in other customizations. This container holds Framework tools like the [Logger utility](https://docs.medusajs.com/learn/debugging-and-testing/logging/index.html.md) and resources within the module.
2. Options passed to the module when it's later added in Medusa's configurations. These options are useful to pass secret keys or configurations that ensure your module is re-usable across applications. For the CMS Module, you accept the API key to connect to the dummy CMS as an option.

When integrating a third-party system that has a Node.js SDK or client, you can initialize that client in the constructor to be used in the service's methods.

### Integration Methods

Next, you'll add methods that simulate sending requests to a third-party CMS. You'll use these methods later to sync brands from and to the CMS.

Add the following methods in the `CmsModuleService`:

```ts title="src/modules/cms/service.ts" highlights={methodsHighlights}
export class CmsModuleService {
  // ...

  // a dummy method to simulate sending a request,
  // in a realistic scenario, you'd use an SDK, fetch, or axios clients
  private async sendRequest(url: string, method: string, data?: any) {
    this.logger_.info(`Sending a ${method} request to ${url}.`)
    this.logger_.info(`Request Data: ${JSON.stringify(data, null, 2)}`)
    this.logger_.info(`API Key: ${JSON.stringify(this.options_.apiKey, null, 2)}`)
  }

  async createBrand(brand: Record<string, unknown>) {
    await this.sendRequest("/brands", "POST", brand)
  }

  async deleteBrand(id: string) {
    await this.sendRequest(`/brands/${id}`, "DELETE")
  }

  async retrieveBrands(): Promise<Record<string, unknown>[]> {
    await this.sendRequest("/brands", "GET")

    return []
  }
}
```

The `sendRequest` method sends requests to the third-party CMS. Since this guide isn't using a real CMS, it only simulates the sending by logging messages in the terminal.

You also add three methods that use the `sendRequest` method:

- `createBrand` that creates a brand in the third-party system.
- `deleteBrand` that deletes the brand in the third-party system.
- `retrieveBrands` to retrieve a brand from the third-party system.

***

## 3. Export Module Definition

After creating the module's service, you'll export the module definition indicating the module's name and service.

Create the file `src/modules/cms/index.ts` with the following content:

![Directory structure of the Medusa application after adding the module definition file](https://res.cloudinary.com/dza7lstvk/image/upload/v1733492991/Medusa%20Book/cms-dir-overview-3_b0byks.jpg)

```ts title="src/modules/cms/index.ts"
import { Module } from "@medusajs/framework/utils"
import CmsModuleService from "./service"

export const CMS_MODULE = "cms"

export default Module(CMS_MODULE, {
  service: CmsModuleService,
})
```

You use `Module` from the Modules SDK to export the module's defintion, indicating that the module's name is `cms` and its service is `CmsModuleService`.

***

## 4. Add Module to Medusa's Configurations

Finally, add the module to the Medusa configurations at `medusa-config.ts`:

```ts title="medusa-config.ts"
module.exports = defineConfig({
  // ...
  modules: [
    // ...
    {
      resolve: "./src/modules/cms",
      options: {
        apiKey: process.env.CMS_API_KEY,
      },
    },
  ], 
})
```

The object passed in `modules` accept an `options` property, whose value is an object of options to pass to the module. These are the options you receive in the `CmsModuleService`'s constructor.

You can add the `CMS_API_KEY` environment variable to `.env`:

```bash
CMS_API_KEY=123
```

***

## Next Steps: Sync Brand From Medusa to CMS

You can now use the CMS Module's service to perform actions on the third-party CMS.

In the next chapter, you'll learn how to emit an event when a brand is created, then handle that event to sync the brand from Medusa to the third-party service.


# Guide: Schedule Syncing Brands from Third-Party

In the previous chapters, you've [integrated a third-party CMS](https://docs.medusajs.com/learn/customization/integrate-systems/service/index.html.md) and implemented the logic to [sync created brands](https://docs.medusajs.com/learn/customization/integrate-systems/handle-event/index.html.md) from Medusa to the CMS.

However, when you integrate a third-party system, you want the data to be in sync between the Medusa application and the system. One way to do so is by automatically syncing the data once a day.

You can create an action to be automatically executed at a specified interval using scheduled jobs. A scheduled job is an asynchronous function with a specified schedule of when the Medusa application should run it. Scheduled jobs are useful to automate repeated tasks.

Learn more about scheduled jobs in [this chapter](https://docs.medusajs.com/learn/fundamentals/scheduled-jobs/index.html.md).

In this chapter, you'll create a scheduled job that triggers syncing the brands from the third-party CMS to Medusa once a day. You'll implement the syncing logic in a workflow, and execute that workflow in the scheduled job.

### Prerequisites

- [CMS Module](https://docs.medusajs.com/learn/customization/integrate-systems/service/index.html.md)

***

## 1. Implement Syncing Workflow

You'll start by implementing the syncing logic in a workflow, then execute the workflow later in the scheduled job.

Workflows have a built-in durable execution engine that helps you complete tasks spanning multiple systems. Also, their rollback mechanism ensures that data is consistent across systems even when errors occur during execution.

Learn more about workflows in [this chapter](https://docs.medusajs.com/learn/fundamentals/workflows/index.html.md).

This workflow will have three steps:

1. `retrieveBrandsFromCmsStep` to retrieve the brands from the CMS.
2. `createBrandsStep` to create the brands retrieved in the first step that don't exist in Medusa.
3. `updateBrandsStep` to update the brands retrieved in the first step that exist in Medusa.

### retrieveBrandsFromCmsStep

To create the step that retrieves the brands from the third-party CMS, create the file `src/workflows/sync-brands-from-cms.ts` with the following content:

![Directory structure of the Medusa application after creating the file.](https://res.cloudinary.com/dza7lstvk/image/upload/v1733494196/Medusa%20Book/cms-dir-overview-6_z1omsi.jpg)

```ts title="src/workflows/sync-brands-from-cms.ts" collapsibleLines="1-7" expandButtonLabel="Show Imports"
import {
  createStep,
  StepResponse,
} from "@medusajs/framework/workflows-sdk"
import CmsModuleService from "../modules/cms/service"
import { CMS_MODULE } from "../modules/cms"

const retrieveBrandsFromCmsStep = createStep(
  "retrieve-brands-from-cms",
  async (_, { container }) => {
    const cmsModuleService: CmsModuleService = container.resolve(
      CMS_MODULE
    )

    const brands = await cmsModuleService.retrieveBrands()

    return new StepResponse(brands)
  }
)
```

You create a `retrieveBrandsFromCmsStep` that resolves the CMS Module's service and uses its `retrieveBrands` method to retrieve the brands in the CMS. You return those brands in the step's response.

### createBrandsStep

The brands retrieved in the first step may have brands that don't exist in Medusa. So, you'll create a step that creates those brands. Add the step to the same `src/workflows/sync-brands-from-cms.ts` file:

```ts title="src/workflows/sync-brands-from-cms.ts" highlights={createBrandsHighlights} collapsibleLines="1-8" expandButtonLabel="Show Imports"
// other imports...
import BrandModuleService from "../modules/brand/service"
import { BRAND_MODULE } from "../modules/brand"

// ...

type CreateBrand = {
  name: string
}

type CreateBrandsInput = {
  brands: CreateBrand[]
}

export const createBrandsStep = createStep(
  "create-brands-step",
  async (input: CreateBrandsInput, { container }) => {
    const brandModuleService: BrandModuleService = container.resolve(
      BRAND_MODULE
    )

    const brands = await brandModuleService.createBrands(input.brands)

    return new StepResponse(brands, brands)
  },
  async (brands, { container }) => {
    if (!brands) {
      return
    }

    const brandModuleService: BrandModuleService = container.resolve(
      BRAND_MODULE
    )

    await brandModuleService.deleteBrands(brands.map((brand) => brand.id))
  }
)
```

The `createBrandsStep` accepts the brands to create as an input. It resolves the [Brand Module](https://docs.medusajs.com/learn/customization/custom-features/module/index.html.md)'s service and uses the generated `createBrands` method to create the brands.

The step passes the created brands to the compensation function, which deletes those brands if an error occurs during the workflow's execution.

Learn more about compensation functions in [this chapter](https://docs.medusajs.com/learn/fundamentals/workflows/compensation-function/index.html.md).

### Update Brands Step

The brands retrieved in the first step may also have brands that exist in Medusa. So, you'll create a step that updates their details to match that of the CMS. Add the step to the same `src/workflows/sync-brands-from-cms.ts` file:

```ts title="src/workflows/sync-brands-from-cms.ts" highlights={updateBrandsHighlights}
// ...

type UpdateBrand = {
  id: string
  name: string
}

type UpdateBrandsInput = {
  brands: UpdateBrand[]
}

export const updateBrandsStep = createStep(
  "update-brands-step",
  async ({ brands }: UpdateBrandsInput, { container }) => {
    const brandModuleService: BrandModuleService = container.resolve(
      BRAND_MODULE
    )

    const prevUpdatedBrands = await brandModuleService.listBrands({
      id: brands.map((brand) => brand.id),
    })

    const updatedBrands = await brandModuleService.updateBrands(brands)

    return new StepResponse(updatedBrands, prevUpdatedBrands)
  },
  async (prevUpdatedBrands, { container }) => {
    if (!prevUpdatedBrands) {
      return
    }

    const brandModuleService: BrandModuleService = container.resolve(
      BRAND_MODULE
    )

    await brandModuleService.updateBrands(prevUpdatedBrands)
  }
)
```

The `updateBrandsStep` receives the brands to update in Medusa. In the step, you retrieve the brand's details in Medusa before the update to pass them to the compensation function. You then update the brands using the Brand Module's `updateBrands` generated method.

In the compensation function, which receives the brand's old data, you revert the update using the same `updateBrands` method.

### Create Workflow

Finally, you'll create the workflow that uses the above steps to sync the brands from the CMS to Medusa. Add to the same `src/workflows/sync-brands-from-cms.ts` file the following:

```ts title="src/workflows/sync-brands-from-cms.ts"
// other imports...
import {
  // ...
  createWorkflow,
  transform,
  WorkflowResponse,
} from "@medusajs/framework/workflows-sdk"

// ...

export const syncBrandsFromCmsWorkflow = createWorkflow(
  "sync-brands-from-system",
  () => {
    const brands = retrieveBrandsFromCmsStep()

    // TODO create and update brands
  }
)
```

In the workflow, you only use the `retrieveBrandsFromCmsStep` for now, which retrieves the brands from the third-party CMS.

Next, you need to identify which brands must be created or updated. Since workflows are constructed internally and are only evaluated during execution, you can't access values to perform data manipulation directly. Instead, use [transform](https://docs.medusajs.com/learn/fundamentals/workflows/variable-manipulation/index.html.md) from the Workflows SDK that gives you access to the real-time values of the data, allowing you to create new variables using those values.

Learn more about data manipulation using `transform` in [this chapter](https://docs.medusajs.com/learn/fundamentals/workflows/variable-manipulation/index.html.md).

So, replace the `TODO` with the following:

```ts title="src/workflows/sync-brands-from-cms.ts"
const { toCreate, toUpdate } = transform(
  {
    brands,
  },
  (data) => {
    const toCreate: CreateBrand[] = []
    const toUpdate: UpdateBrand[] = []

    data.brands.forEach((brand) => {
      if (brand.external_id) {
        toUpdate.push({
          id: brand.external_id as string,
          name: brand.name as string,
        })
      } else {
        toCreate.push({
          name: brand.name as string,
        })
      }
    })

    return { toCreate, toUpdate }
  }
)

// TODO create and update the brands
```

`transform` accepts two parameters:

1. The data to be passed to the function in the second parameter.
2. A function to execute only when the workflow is executed. Its return value can be consumed by the rest of the workflow.

In `transform`'s function, you loop over the brands array to check which should be created or updated. This logic assumes that a brand in the CMS has an `external_id` property whose value is the brand's ID in Medusa.

You now have the list of brands to create and update. So, replace the new `TODO` with the following:

```ts title="src/workflows/sync-brands-from-cms.ts"
const created = createBrandsStep({ brands: toCreate })
const updated = updateBrandsStep({ brands: toUpdate })

return new WorkflowResponse({
  created,
  updated,
})
```

You first run the `createBrandsStep` to create the brands that don't exist in Medusa, then the `updateBrandsStep` to update the brands that exist in Medusa. You pass the arrays returned by `transform` as the inputs for the steps.

Finally, you return an object of the created and updated brands. You'll execute this workflow in the scheduled job next.

***

## 2. Schedule Syncing Task

You now have the workflow to sync the brands from the CMS to Medusa. Next, you'll create a scheduled job that runs this workflow once a day to ensure the data between Medusa and the CMS are always in sync.

A scheduled job is created in a TypeScript or JavaScript file under the `src/jobs` directory. So, create the file `src/jobs/sync-brands-from-cms.ts` with the following content:

![Directory structure of the Medusa application after adding the scheduled job](https://res.cloudinary.com/dza7lstvk/image/upload/v1733494592/Medusa%20Book/cms-dir-overview-7_dkjb9s.jpg)

```ts title="src/jobs/sync-brands-from-cms.ts"
import { MedusaContainer } from "@medusajs/framework/types"
import { syncBrandsFromCmsWorkflow } from "../workflows/sync-brands-from-cms"

export default async function (container: MedusaContainer) {
  const logger = container.resolve("logger")

  const { result } = await syncBrandsFromCmsWorkflow(container).run()

  logger.info(
    `Synced brands from third-party system: ${
      result.created.length
    } brands created and ${result.updated.length} brands updated.`)
}

export const config = {
  name: "sync-brands-from-system",
  schedule: "0 0 * * *", // change to * * * * * for debugging
}
```

A scheduled job file must export:

- An asynchronous function that will be executed at the specified schedule. This function must be the file's default export.
- An object of scheduled jobs configuration. It has two properties:
  - `name`: A unique name for the scheduled job.
  - `schedule`: A string that holds a [cron expression](https://crontab.guru/) indicating the schedule to run the job.

The scheduled job function accepts as a parameter the [Medusa container](https://docs.medusajs.com/learn/fundamentals/medusa-container/index.html.md) used to resolve Framework and commerce tools. You then execute the `syncBrandsFromCmsWorkflow` and use its result to log how many brands were created or updated.

Based on the cron expression specified in `config.schedule`, Medusa will run the scheduled job every day at midnight. You can also change it to `* * * * *` to run it every minute for easier debugging.

***

## Test it Out

To test out the scheduled job, start the Medusa application:

```bash npm2yarn
npm run dev
```

If you set the schedule to `* * * * *` for debugging, the scheduled job will run in a minute. You'll see in the logs how many brands were created or updated.

***

## Summary

By following the previous chapters, you utilized the Medusa Framework and orchestration tools to perform and automate tasks that span across systems.

With Medusa, you can integrate any service from your commerce ecosystem with ease. You don't have to set up separate applications to manage your different customizations, or worry about data inconsistency across systems. Your efforts only go into implementing the business logic that ties your systems together.


# Guide: Sync Brands from Medusa to Third-Party

In the [previous chapter](https://docs.medusajs.com/learn/customization/integrate-systems/service/index.html.md), you created a CMS Module that integrates a dummy third-party system. You can now perform actions using that module within your custom flows.

In another previous chapter, you [added a workflow](https://docs.medusajs.com/learn/customization/custom-features/workflow/index.html.md) that creates a brand. After integrating the CMS, you want to sync that brand to the third-party system as well.

Medusa has an event system that emits events when an operation is performed. It allows you to listen to those events and perform an asynchronous action in a function called a [subscriber](https://docs.medusajs.com/learn/fundamentals/events-and-subscribers/index.html.md). This is useful to perform actions that aren't integral to the original flow, such as syncing data to a third-party system.

Learn more about Medusa's event system and subscribers in [this chapter](https://docs.medusajs.com/learn/fundamentals/events-and-subscribers/index.html.md).

In this chapter, you'll modify the `createBrandWorkflow` you created before to emit a custom event that indicates a brand was created. Then, you'll listen to that event in a subscriber to sync the brand to the third-party CMS. You'll implement the sync logic within a workflow that you execute in the subscriber.

### Prerequisites

- [createBrandWorkflow](https://docs.medusajs.com/learn/customization/custom-features/workflow/index.html.md)
- [CMS Module](https://docs.medusajs.com/learn/customization/integrate-systems/service/index.html.md)

## 1. Emit Event in createBrandWorkflow

Since syncing the brand to the third-party system isn't integral to creating a brand, you'll emit a custom event indicating that a brand was created.

Medusa provides an `emitEventStep` that allows you to emit an event in your workflows. So, in the `createBrandWorkflow` defined in `src/workflows/create-brand.ts`, use the `emitEventStep` helper step after the `createBrandStep`:

```ts title="src/workflows/create-brand.ts" highlights={eventHighlights}
// other imports...
import { 
  emitEventStep,
} from "@medusajs/medusa/core-flows"

// ...

export const createBrandWorkflow = createWorkflow(
  "create-brand",
  (input: CreateBrandInput) => {
    // ...

    emitEventStep({
      eventName: "brand.created",
      data: {
        id: brand.id,
      },
    })

    return new WorkflowResponse(brand)
  }
)
```

The `emitEventStep` accepts an object parameter having two properties:

- `eventName`: The name of the event to emit. You'll use this name later to listen to the event in a subscriber.
- `data`: The data payload to emit with the event. This data is passed to subscribers that listen to the event. You add the brand's ID to the data payload, informing the subscribers which brand was created.

You'll learn how to handle this event in a later step.

***

## 2. Create Sync to Third-Party System Workflow

The subscriber that will listen to the `brand.created` event will sync the created brand to the third-party CMS. So, you'll implement the syncing logic in a workflow, then execute the workflow in the subscriber.

Workflows have a built-in durable execution engine that helps you complete tasks spanning multiple systems. Also, their rollback mechanism ensures that data is consistent across systems even when errors occur during execution.

Learn more about workflows in [this chapter](https://docs.medusajs.com/learn/fundamentals/workflows/index.html.md).

You'll create a `syncBrandToSystemWorkflow` that has two steps:

- `useQueryGraphStep`: a step that Medusa provides to retrieve data using [Query](https://docs.medusajs.com/learn/fundamentals/module-links/query/index.html.md). You'll use this to retrieve the brand's details using its ID.
- `syncBrandToCmsStep`: a step that you'll create to sync the brand to the CMS.

### syncBrandToCmsStep

To implement the step that syncs the brand to the CMS, create the file `src/workflows/sync-brands-to-cms.ts` with the following content:

![Directory structure of the Medusa application after adding the file](https://res.cloudinary.com/dza7lstvk/image/upload/v1733493547/Medusa%20Book/cms-dir-overview-4_u5t0ug.jpg)

```ts title="src/workflows/sync-brands-to-cms.ts" highlights={syncStepHighlights} collapsibleLines="1-6" expandButtonLabel="Show Imports"
import { createStep, StepResponse } from "@medusajs/framework/workflows-sdk"
import { InferTypeOf } from "@medusajs/framework/types"
import { Brand } from "../modules/brand/models/brand"
import { CMS_MODULE } from "../modules/cms"
import CmsModuleService from "../modules/cms/service"

type SyncBrandToCmsStepInput = {
  brand: InferTypeOf<typeof Brand>
}

const syncBrandToCmsStep = createStep(
  "sync-brand-to-cms",
  async ({ brand }: SyncBrandToCmsStepInput, { container }) => {
    const cmsModuleService: CmsModuleService = container.resolve(CMS_MODULE)

    await cmsModuleService.createBrand(brand)

    return new StepResponse(null, brand.id)
  },
  async (id, { container }) => {
    if (!id) {
      return
    }

    const cmsModuleService: CmsModuleService = container.resolve(CMS_MODULE)

    await cmsModuleService.deleteBrand(id)
  }
)
```

You create the `syncBrandToCmsStep` that accepts a brand as an input. In the step, you resolve the CMS Module's service from the [Medusa container](https://docs.medusajs.com/learn/fundamentals/medusa-container/index.html.md) and use its `createBrand` method. This method will create the brand in the third-party CMS.

You also pass the brand's ID to the step's compensation function. In this function, you delete the brand in the third-party CMS if an error occurs during the workflow's execution.

Learn more about compensation functions in [this chapter](https://docs.medusajs.com/learn/fundamentals/workflows/compensation-function/index.html.md).

### Create Workflow

You can now create the workflow that uses the above step. Add the workflow to the same `src/workflows/sync-brands-to-cms.ts` file:

```ts title="src/workflows/sync-brands-to-cms.ts" highlights={syncWorkflowHighlights}
// other imports...
import { 
  // ...
  createWorkflow, 
  WorkflowResponse,
} from "@medusajs/framework/workflows-sdk"
import { useQueryGraphStep } from "@medusajs/medusa/core-flows"

// ...

type SyncBrandToCmsWorkflowInput = {
  id: string
}

export const syncBrandToCmsWorkflow = createWorkflow(
  "sync-brand-to-cms",
  (input: SyncBrandToCmsWorkflowInput) => {
    // @ts-ignore
    const { data: brands } = useQueryGraphStep({
      entity: "brand",
      fields: ["*"],
      filters: {
        id: input.id,
      },
      options: {
        throwIfKeyNotFound: true,
      },
    })

    syncBrandToCmsStep({
      brand: brands[0],
    } as SyncBrandToCmsStepInput)

    return new WorkflowResponse({})
  }
)
```

You create a `syncBrandToCmsWorkflow` that accepts the brand's ID as input. The workflow has the following steps:

- `useQueryGraphStep`: Retrieve the brand's details using Query. You pass the brand's ID as a filter, and set the `throwIfKeyNotFound` option to true so that the step throws an error if a brand with the specified ID doesn't exist.
- `syncBrandToCmsStep`: Create the brand in the third-party CMS.

You'll execute this workflow in the subscriber next.

Learn more about `useQueryGraphStep` in [this reference](https://docs.medusajs.com/resources/references/helper-steps/useQueryGraphStep/index.html.md).

***

## 3. Handle brand.created Event

You now have a workflow with the logic to sync a brand to the CMS. You need to execute this workflow whenever the `brand.created` event is emitted. So, you'll create a subscriber that listens to and handle the event.

Subscribers are created in a TypeScript or JavaScript file under the `src/subscribers` directory. So, create the file `src/subscribers/brand-created.ts` with the following content:

![Directory structure of the Medusa application after adding the subscriber](https://res.cloudinary.com/dza7lstvk/image/upload/v1733493774/Medusa%20Book/cms-dir-overview-5_iqqwvg.jpg)

```ts title="src/subscribers/brand-created.ts" highlights={subscriberHighlights}
import type {
  SubscriberConfig,
  SubscriberArgs,
} from "@medusajs/framework"
import { syncBrandToCmsWorkflow } from "../workflows/sync-brands-to-cms"

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

export const config: SubscriberConfig = {
  event: "brand.created",
}
```

A subscriber file must export:

- The asynchronous function that's executed when the event is emitted. This must be the file's default export.
- An object that holds the subscriber's configurations. It has an `event` property that indicates the name of the event that the subscriber is listening to.

The subscriber function accepts an object parameter that has two properties:

- `event`: An object of event details. Its `data` property holds the event's data payload, which is the brand's ID.
- `container`: The Medusa container used to resolve Framework and commerce tools.

In the function, you execute the `syncBrandToCmsWorkflow`, passing it the data payload as an input. So, everytime a brand is created, Medusa will execute this function, which in turn executes the workflow to sync the brand to the CMS.

Learn more about subscribers in [this chapter](https://docs.medusajs.com/learn/fundamentals/events-and-subscribers/index.html.md).

***

## Test it Out

To test the subscriber and workflow out, you'll use the [Create Brand API route](https://docs.medusajs.com/learn/customization/custom-features/api-route/index.html.md) you created in a previous chapter.

First, start the Medusa application:

```bash npm2yarn
npm run dev
```

Since the `/admin/brands` API route has a `/admin` prefix, it's only accessible by authenticated admin users. So, to retrieve an authenticated token of your admin user, send a `POST` request to the `/auth/user/emailpass` API Route:

```bash
curl -X POST 'http://localhost:9000/auth/user/emailpass' \
-H 'Content-Type: application/json' \
--data-raw '{
    "email": "admin@medusa-test.com",
    "password": "supersecret"
}'
```

Make sure to replace the email and password with your admin user's credentials.

Don't have an admin user? Refer to [this guide](https://docs.medusajs.com/learn/installation#create-medusa-admin-user/index.html.md).

Then, send a `POST` request to `/admin/brands`, passing the token received from the previous request in the `Authorization` header:

```bash
curl -X POST 'http://localhost:9000/admin/brands' \
-H 'Content-Type: application/json' \
-H 'Authorization: Bearer {token}' \
--data '{
    "name": "Acme"
}'
```

This request returns the created brand. If you check the logs, you'll find the `brand.created` event was emitted, and that the request to the third-party system was simulated:

```plain
info:    Processing brand.created which has 1 subscribers
http:    POST /admin/brands ← - (200) - 16.418 ms
info:    Sending a POST request to /brands.
info:    Request Data: {
  "id": "01JEDWENYD361P664WRQPMC3J8",
  "name": "Acme",
  "created_at": "2024-12-06T11:42:32.909Z",
  "updated_at": "2024-12-06T11:42:32.909Z",
  "deleted_at": null
}
info:    API Key: "123"
```

***

## Next Chapter: Sync Brand from Third-Party CMS to Medusa

You can also automate syncing data from a third-party system to Medusa at a regular interval. In the next chapter, you'll learn how to sync brands from the third-party CMS to Medusa once a day.


# Admin Development Constraints

This chapter lists some constraints of admin widgets and UI routes.

## Arrow Functions

Widget and UI route components must be created as arrow functions.

```ts highlights={arrowHighlights}
// Don't
function ProductWidget() {
  // ...
}

// Do
const ProductWidget = () => {
  // ...
}
```

***

## Widget Zone

A widget zone's value must be wrapped in double or single quotes. It can't be a template literal or a variable.

```ts highlights={zoneHighlights}
// Don't
export const config = defineWidgetConfig({
  zone: `product.details.before`,
})

// Don't
const ZONE = "product.details.after"
export const config = defineWidgetConfig({
  zone: ZONE,
})

// Do
export const config = defineWidgetConfig({
  zone: "product.details.before",
})
```


# Write Integration Tests

In this chapter, you'll learn about `medusaIntegrationTestRunner` from Medusa's Testing Framework and how to use it to write integration tests.

### Prerequisites

- [Testing Tools Setup](https://docs.medusajs.com/learn/debugging-and-testing/testing-tools/index.html.md)

## medusaIntegrationTestRunner Utility

The `medusaIntegrationTestRunner` is from Medusa's Testing Framework and it's used to create integration tests in your Medusa project. It runs a full Medusa application, allowing you test API routes, workflows, or other customizations.

For example:

```ts title="integration-tests/http/test.spec.ts" highlights={highlights}
import { medusaIntegrationTestRunner } from "@medusajs/test-utils"

medusaIntegrationTestRunner({
  testSuite: ({ api, getContainer }) => {
    // TODO write tests...
  },
})

jest.setTimeout(60 * 1000)
```

The `medusaIntegrationTestRunner` function accepts an object as a parameter. The object has a required property `testSuite`.

`testSuite`'s value is a function that defines the tests to run. The function accepts as a parameter an object that has the following properties:

- `api`: a set of utility methods used to send requests to the Medusa application. It has the following methods:
  - `get`: Send a `GET` request to an API route.
  - `post`: Send a `POST` request to an API route.
  - `delete`: Send a `DELETE` request to an API route.
- `getContainer`: a function that retrieves the Medusa Container. Use the `getContainer().resolve` method to resolve resources from the Medusa Container.

The tests in the `testSuite` function are written using [Jest](https://jestjs.io/).

### Jest Timeout

Since your tests connect to the database and perform actions that require more time than the typical tests, make sure to increase the timeout in your test:

```ts title="integration-tests/http/test.spec.ts"
// in your test's file
jest.setTimeout(60 * 1000)
```

***

### Run Tests

Run the following command to run your tests:

```bash npm2yarn
npm run test:integration
```

If you don't have a `test:integration` script in `package.json`, refer to the [Medusa Testing Tools chapter](https://docs.medusajs.com/learn/debugging-and-testing/testing-tools#add-test-commands/index.html.md).

This runs your Medusa application and runs the tests available under the `src/integrations/http` directory.

***

## Other Options and Inputs

Refer to [the Test Tooling Reference](https://docs.medusajs.com/resources/test-tools-reference/medusaIntegrationTestRunner/index.html.md) for other available parameter options and inputs of the `testSuite` function.

***

## Database Used in Tests

The `medusaIntegrationTestRunner` function creates a database with a random name before running the tests. Then, it drops that database after all the tests end.

To manage that database, such as changing its name or perform operations on it in your tests, refer to [the Test Tooling Reference](https://docs.medusajs.com/resources/test-tools-reference/medusaIntegrationTestRunner/index.html.md).

***

## Example Integration Tests

The next chapters provide examples of writing integration tests for API routes and workflows.


# Environment Variables in Admin Customizations

In this chapter, you'll learn how to use environment variables in your admin customizations.

To learn how environment variables are generally loaded in Medusa based on your application's environment, check out [this chapter](https://docs.medusajs.com/learn/fundamentals/environment-variables/index.html.md).

## How to Set Environment Variables

This only applies to customizations in a Medusa project. For plugins, refer to the [Environment Variables in Plugins](#environment-variables-in-plugins) section.

The Medusa Admin is built on top of [Vite](https://vite.dev/). To set an environment variable that you want to use in a widget or UI route, prefix the environment variable with `VITE_`.

For example:

```plain
VITE_MY_API_KEY=sk_123
```

***

## How to Use Environment Variables

To access or use an environment variable starting with `VITE_`, use the `import.meta.env` object.

For example:

```tsx highlights={[["8"]]}
import { defineWidgetConfig } from "@medusajs/admin-sdk"
import { Container, Heading } from "@medusajs/ui"

const ProductWidget = () => {
  return (
    <Container className="divide-y p-0">
      <div className="flex items-center justify-between px-6 py-4">
        <Heading level="h2">API Key: {import.meta.env.VITE_MY_API_KEY}</Heading>
      </div>
    </Container>
  )
}

export const config = defineWidgetConfig({
  zone: "product.details.before",
})

export default ProductWidget
```

In this example, you display the API key in a widget using `import.meta.env.VITE_MY_API_KEY`.

### Type Error on import.meta.env

If you receive a type error on `import.meta.env`, create the file `src/admin/vite-env.d.ts` with the following content:

```ts title="src/admin/vite-env.d.ts"
/// <reference types="vite/client" />
```

This file tells TypeScript to recognize the `import.meta.env` object and enhances the types of your custom environment variables.

***

## Check Node Environment in Admin Customizations

To check the current environment, Vite exposes two variables:

- `import.meta.env.DEV`: Returns `true` if the current environment is development.
- `import.meta.env.PROD`: Returns `true` if the current environment is production.

Learn more about other Vite environment variables in the [Vite documentation](https://vite.dev/guide/env-and-mode).

***

## Environment Variables in Production

When you build the Medusa application, including the Medusa Admin, with the `build` command, the environment variables are inlined into the build. This means that you can't change the environment variables without rebuilding the application.

For example, the `VITE_MY_API_KEY` environment variable in the example above will be replaced with the actual value during the build process.

***

## Environment Variables in Plugins

As explained in the [previous section](#environment-variables-in-production), environment variables are inlined into the build. This presents a limitation for plugins, where you can't use environment variables.

Instead, only the following global variable is available in plugins:

- `__BACKEND_URL__`: The URL of the Medusa backend, as set in the [Medusa configurations](https://docs.medusajs.com/learn/configurations/medusa-config#backendurl/index.html.md).
- `__BASE__`: The base path of the Medusa Admin. (For example, `/app`).
- `__STOREFRONT_URL__`: The URL of the Medusa Storefront, as set in the [Medusa configurations](https://docs.medusajs.com/learn/configurations/medusa-config#storefronturl/index.html.md).

You can use those variables in your Medusa Admin customizations of a plugin. For example:

```tsx highlights={[["8"]]}
import { defineWidgetConfig } from "@medusajs/admin-sdk"
import { Container, Heading } from "@medusajs/ui"

const ProductWidget = () => {
  return (
    <Container className="divide-y p-0">
      <div className="flex items-center justify-between px-6 py-4">
        <Heading level="h2">Backend URL: {__BACKEND_URL__}</Heading>
      </div>
    </Container>
  )
}

export const config = defineWidgetConfig({
  zone: "product.details.before",
})

export default ProductWidget
```


# Write Tests for Modules

In this chapter, you'll learn about `moduleIntegrationTestRunner` from Medusa's Testing Framework and how to use it to write integration tests for a module's main service.

### Prerequisites

- [Testing Tools Setup](https://docs.medusajs.com/learn/debugging-and-testing/testing-tools/index.html.md)

## moduleIntegrationTestRunner Utility

`moduleIntegrationTestRunner` creates integration tests for a module. The integration tests run on a test Medusa application with only the specified module enabled.

For example, assuming you have a `blog` module, create a test file at `src/modules/blog/__tests__/service.spec.ts`:

```ts title="src/modules/blog/__tests__/service.spec.ts"
import { moduleIntegrationTestRunner } from "@medusajs/test-utils"
import { BLOG_MODULE } from ".."
import BlogModuleService from "../service"
import Post from "../models/post"

moduleIntegrationTestRunner<BlogModuleService>({
  moduleName: BLOG_MODULE,
  moduleModels: [Post],
  resolve: "./src/modules/blog",
  testSuite: ({ service }) => {
    // TODO write tests
  },
})

jest.setTimeout(60 * 1000)
```

The `moduleIntegrationTestRunner` function accepts as a parameter an object with the following properties:

- `moduleName`: The name of the module.
- `moduleModels`: An array of models in the module. Refer to [this section](#write-tests-for-modules-without-data-models) if your module doesn't have data models.
- `resolve`: The path to the module's directory.
- `testSuite`: A function that defines the tests to run.

The `testSuite` function accepts as a parameter an object having the `service` property, which is an instance of the module's main service.

The type argument provided to the `moduleIntegrationTestRunner` function is used as the type of the `service` property.

The tests in the `testSuite` function are written using [Jest](https://jestjs.io/).

***

## Run Tests

Run the following command to run your module integration tests:

```bash npm2yarn
npm run test:integration:modules
```

If you don't have a `test:integration:modules` script in `package.json`, refer to the [Medusa Testing Tools chapter](https://docs.medusajs.com/learn/debugging-and-testing/testing-tools#add-test-commands/index.html.md).

This runs your Medusa application and runs the tests available in any `__tests__` directory under the `src/modules` directory.

***

## Pass Module Options

If your module accepts options, you can set them using the `moduleOptions` property of the `moduleIntegrationTestRunner`'s parameter.

For example:

```ts
import { moduleIntegrationTestRunner } from "@medusajs/test-utils"
import BlogModuleService from "../service"

moduleIntegrationTestRunner<BlogModuleService>({
  moduleOptions: {
    apiKey: "123",
  },
  // ...
})
```

***

## Write Tests for Modules without Data Models

If your module doesn't have a data model, pass a dummy model in the `moduleModels` property.

For example:

```ts
import { moduleIntegrationTestRunner } from "@medusajs/test-utils"
import BlogModuleService from "../service"
import { model } from "@medusajs/framework/utils"

const DummyModel = model.define("dummy_model", {
  id: model.id().primaryKey(),
})

moduleIntegrationTestRunner<BlogModuleService>({
  moduleModels: [DummyModel],
  // ...
})

jest.setTimeout(60 * 1000)
```

***

### Other Options and Inputs

Refer to [the Test Tooling Reference](https://docs.medusajs.com/resources/test-tools-reference/moduleIntegrationTestRunner/index.html.md) for other available parameter options and inputs of the `testSuite` function.

***

## Database Used in Tests

The `moduleIntegrationTestRunner` function creates a database with a random name before running the tests. Then, it drops that database after all the tests end.

To manage that database, such as changing its name or perform operations on it in your tests, refer to [the Test Tooling Reference](https://docs.medusajs.com/resources/test-tools-reference/moduleIntegrationTestRunner/index.html.md).


# Admin Routing Customizations

The Medusa Admin dashboard uses [React Router](https://reactrouter.com) under the hood to manage routing. So, you can have more flexibility in routing-related customizations using some of React Router's utilities, hooks, and components.

In this chapter, you'll learn about routing-related customizations that you can use in your admin customizations using React Router.

`react-router-dom` is available in your project by default through the Medusa packages. You don't need to install it separately.

## Link to a Page

To link to a page in your admin customizations, you can use the `Link` component from `react-router-dom`. For example:

```tsx title="src/admin/widgets/product-widget.tsx" highlights={highlights}
import { defineWidgetConfig } from "@medusajs/admin-sdk"
import { Container } from "@medusajs/ui"
import { Link } from "react-router-dom"

// The widget
const ProductWidget = () => {
  return (
    <Container className="divide-y p-0">
      <Link to={"/orders"}>View Orders</Link>
    </Container>
  )
}

// The widget's configurations
export const config = defineWidgetConfig({
  zone: "product.details.before",
})

export default ProductWidget
```

This adds a widget to a product's details page with a link to the Orders page. The link's path must be without the `/app` prefix.

***

## Admin Route Loader

Route loaders are available starting from Medusa v2.5.1.

In your UI route or any other custom admin route, you may need to retrieve data to use it in your route component. For example, you may want to fetch a list of products to display on a custom page.

To do that, you can export a `loader` function in the route file, which is a [React Router loader](https://reactrouter.com/6.29.0/route/loader#loader). In this function, you can fetch and return data asynchronously. Then, in your route component, you can use the [useLoaderData](https://reactrouter.com/6.29.0/hooks/use-loader-data#useloaderdata) hook from React Router to access the data.

For example, consider the following UI route created at `src/admin/routes/custom/page.tsx`:

```tsx title="src/admin/routes/custom/page.tsx" highlights={loaderHighlights}
import { Container, Heading } from "@medusajs/ui"
import {
  useLoaderData,
} from "react-router-dom"

export async function loader() {
  // TODO fetch products

  return {
    products: [],
  }
}

const CustomPage = () => {
  const { products } = useLoaderData() as Awaited<ReturnType<typeof loader>>

  return (
    <div>
      <Container className="divide-y p-0">
        <div className="flex items-center justify-between px-6 py-4">
          <Heading level="h2">Products count: {products.length}</Heading>
        </div>
      </Container>
    </div>
  )
}

export default CustomPage
```

In this example, you first export a `loader` function that can be used to fetch data, such as products. The function returns an object with a `products` property.

Then, in the `CustomPage` route component, you use the `useLoaderData` hook from React Router to access the data returned by the `loader` function. You can then use the data in your component.

### Route Parameters

You can also access route params in the loader function. For example, consider the following UI route created at `src/admin/routes/custom/[id]/page.tsx`:

```tsx title="src/admin/routes/custom/[id]/page.tsx" highlights={loaderParamHighlights}
import { Container, Heading } from "@medusajs/ui"
import {
  useLoaderData,
  LoaderFunctionArgs,
} from "react-router-dom"

export async function loader({ params }: LoaderFunctionArgs) {
  const { id } = params
  // TODO fetch product by id

  return {
    id,
  }
}

const CustomPage = () => {
  const { id } = useLoaderData() as Awaited<ReturnType<typeof loader>>

  return (
    <div>
      <Container className="divide-y p-0">
        <div className="flex items-center justify-between px-6 py-4">
          <Heading level="h2">Product ID: {id}</Heading>
        </div>
      </Container>
    </div>
  )
}

export default CustomPage
```

Because the UI route has a route parameter `[id]`, you can access the `id` parameter in the `loader` function. The loader function accepts as a parameter an object of type `LoaderFunctionArgs` from React Router. This object has a `params` property that contains the route parameters.

In the loader, you can fetch data asynchronously using the route parameter and return it. Then, in the route component, you can access the data using the `useLoaderData` hook.

### When to Use Route Loaders

A route loader is executed before the route is loaded. So, it will block navigation until the loader function is resolved.

Only use route loaders when the route component needs data essential before rendering. Otherwise, use the JS SDK with Tanstack (React) Query as explained in [this chapter](https://docs.medusajs.com/learn/fundamentals/admin/tips#send-requests-to-api-routes/index.html.md). This way, you can fetch data asynchronously and update the UI when the data is available. You can also use a loader to prepare some initial data that's used in the route component before the data is retrieved.

***

## Other React Router Utilities

### Route Handles

Route handles are available starting from Medusa v2.5.1.

In your UI route or any route file, you can export a `handle` object to define [route handles](https://reactrouter.com/start/framework/route-module#handle). The object is passed to the loader and route contexts.

For example:

```tsx title="src/admin/routes/custom/page.tsx"
export const handle = {
  sandbox: true,
}
```

### React Router Components and Hooks

Refer to [react-router-dom’s documentation](https://reactrouter.com/en/6.29.0) for components and hooks that you can use in your admin customizations.


# Admin Development Tips

In this chapter, you'll find some tips for your admin development.

## Send Requests to API Routes

To send a request to an API route in the Medusa Application, use Medusa's [JS SDK](https://docs.medusajs.com/resources/js-sdk/index.html.md) with [Tanstack Query](https://tanstack.com/query/latest). Both of these tools are installed in your project by default.

Do not install Tanstack Query as that will cause unexpected errors in your development. If you prefer installing it for better auto-completion in your code editor, make sure to install `v5.64.2` as a development dependency.

First, create the file `src/admin/lib/config.ts` to setup the SDK for use in your customizations:

### Medusa Project

```ts title="src/admin/lib/config.ts"
import Medusa from "@medusajs/js-sdk"

export const sdk = new Medusa({
  baseUrl: import.meta.env.VITE_BACKEND_URL || "/",
  debug: import.meta.env.DEV,
  auth: {
    type: "session",
  },
})
```

### Medusa Plugin

```ts title="src/admin/lib/config.ts"
import Medusa from "@medusajs/js-sdk"

export const sdk = new Medusa({
  baseUrl: __BACKEND_URL__ || "/",
  auth: {
    type: "session",
  },
})
```

Notice that you use `import.meta.env` in a Medusa project to access environment variables in your customizations, whereas in a plugin you use the global variable `__BACKEND_URL__` to access the backend URL. You can learn more in the [Admin Environment Variables](https://docs.medusajs.com/learn/fundamentals/admin/environment-variables/index.html.md) chapter.

Learn more about the JS SDK's configurations [this documentation](https://docs.medusajs.com/resources/js-sdk#js-sdk-configurations/index.html.md).

Then, use the configured SDK with the `useQuery` Tanstack Query hook to send `GET` requests, and `useMutation` hook to send `POST` or `DELETE` requests.

For example:

### Query

```tsx title="src/admin/widgets/product-widget.ts" highlights={queryHighlights}
import { defineWidgetConfig } from "@medusajs/admin-sdk"
import { Button, Container } from "@medusajs/ui"
import { useQuery } from "@tanstack/react-query"
import { sdk } from "../lib/config"
import { DetailWidgetProps, HttpTypes } from "@medusajs/framework/types"

const ProductWidget = () => {
  const { data, isLoading } = useQuery({
    queryFn: () => sdk.admin.product.list(),
    queryKey: ["products"],
  })
  
  return (
    <Container className="divide-y p-0">
      {isLoading && <span>Loading...</span>}
      {data?.products && (
        <ul>
          {data.products.map((product) => (
            <li key={product.id}>{product.title}</li>
          ))}
        </ul>
      )}
    </Container>
  )
}

export const config = defineWidgetConfig({
  zone: "product.list.before",
})

export default ProductWidget
```

### Mutation

```tsx title="src/admin/widgets/product-widget.ts" highlights={mutationHighlights}
import { defineWidgetConfig } from "@medusajs/admin-sdk"
import { Button, Container } from "@medusajs/ui"
import { useMutation } from "@tanstack/react-query"
import { sdk } from "../lib/config"
import { DetailWidgetProps, HttpTypes } from "@medusajs/framework/types"

const ProductWidget = ({ 
  data: productData,
}: DetailWidgetProps<HttpTypes.AdminProduct>) => {
  const { mutateAsync } = useMutation({
    mutationFn: (payload: HttpTypes.AdminUpdateProduct) => 
      sdk.admin.product.update(productData.id, payload),
    onSuccess: () => alert("updated product"),
  })

  const handleUpdate = () => {
    mutateAsync({
      title: "New Product Title",
    })
  }
  
  return (
    <Container className="divide-y p-0">
      <Button onClick={handleUpdate}>Update Title</Button>
    </Container>
  )
}

export const config = defineWidgetConfig({
  zone: "product.details.before",
})

export default ProductWidget
```

You can also send requests to custom routes as explained in the [JS SDK reference](https://docs.medusajs.com/resources/js-sdk/index.html.md).

### Use Route Loaders for Initial Data

You may need to retrieve data before your component is rendered, or you may need to pass some initial data to your component to be used while data is being fetched. In those cases, you can use a [route loader](https://docs.medusajs.com/learn/fundamentals/admin/routing/index.html.md).

***

## Global Variables in Admin Customizations

In your admin customizations, you can use the following global variables:

- `__BASE__`: The base path of the Medusa Admin, as set in the [admin.path](https://docs.medusajs.com/learn/configurations/medusa-config#path/index.html.md) configuration in `medusa-config.ts`.
- `__BACKEND_URL__`: The URL to the Medusa backend, as set in the [admin.backendUrl](https://docs.medusajs.com/learn/configurations/medusa-config#backendurl/index.html.md) configuration in `medusa-config.ts`.
- `__STOREFRONT_URL__`: The URL to the storefront, as set in the [admin.storefrontUrl](https://docs.medusajs.com/learn/configurations/medusa-config#storefrontUrl/index.html.md) configuration in `medusa-config.ts`.

***

## Admin Translations

The Medusa Admin dashboard can be displayed in languages other than English, which is the default. Other languages are added through community contributions.

Learn how to add a new language translation for the Medusa Admin in [this guide](https://docs.medusajs.com/learn/resources/contribution-guidelines/admin-translations/index.html.md).


# Admin UI Routes

In this chapter, you’ll learn how to create a UI route in the admin dashboard.

## What is a UI Route?

The Medusa Admin dashboard is customizable, allowing you to add new pages, called UI routes. You create a UI route as a React component showing custom content that allow admin users to perform custom actions.

For example, you can add a new page to show and manage product reviews, which aren't available natively in Medusa.

***

## How to Create a UI Route?

### Prerequisites

- [Medusa application installed](https://docs.medusajs.com/learn/installation/index.html.md)

You create a UI route in a `page.tsx` file under a sub-directory of `src/admin/routes` directory. The file's path relative to `src/admin/routes` determines its path in the dashboard. The file’s default export must be the UI route’s React component.

For example, create the file `src/admin/routes/custom/page.tsx` with the following content:

![Example of UI route file in the application's directory structure](https://res.cloudinary.com/dza7lstvk/image/upload/v1732867243/Medusa%20Book/ui-route-dir-overview_tgju25.jpg)

```tsx title="src/admin/routes/custom/page.tsx"
import { Container, Heading } from "@medusajs/ui"

const CustomPage = () => {
  return (
    <Container className="divide-y p-0">
      <div className="flex items-center justify-between px-6 py-4">
        <Heading level="h2">This is my custom route</Heading>
      </div>
    </Container>
  )
}

export default CustomPage
```

You add a new route at `http://localhost:9000/app/custom`. The `CustomPage` component holds the page's content, which currently only shows a heading.

In the route, you use [Medusa UI](https://docs.medusajs.com/ui/index.html.md), a package that Medusa maintains to allow you to customize the dashboard with the same components used to build it.

The UI route component must be created as an arrow function.

### Test the UI Route

To test the UI route, start the Medusa application:

```bash npm2yarn
npm run dev
```

Then, after logging into the admin dashboard, open the page `http://localhost:9000/app/custom` to see your custom page.

***

## Show UI Route in the Sidebar

To add a sidebar item for your custom UI route, export a configuration object in the UI route's file:

```tsx title="src/admin/routes/custom/page.tsx" highlights={highlights}
import { defineRouteConfig } from "@medusajs/admin-sdk"
import { ChatBubbleLeftRight } from "@medusajs/icons"
import { Container, Heading } from "@medusajs/ui"

const CustomPage = () => {
  return (
    <Container className="divide-y p-0">
      <div className="flex items-center justify-between px-6 py-4">
        <Heading level="h2">This is my custom route</Heading>
      </div>
    </Container>
  )
}

export const config = defineRouteConfig({
  label: "Custom Route",
  icon: ChatBubbleLeftRight,
})

export default CustomPage
```

The configuration object is created using `defineRouteConfig` from the Medusa Framework. It accepts the following properties:

- `label`: the sidebar item’s label.
- `icon`: an optional React component used as an icon in the sidebar.

The above example adds a new sidebar item with the label `Custom Route` and an icon from the [Medusa UI Icons package](https://docs.medusajs.com/ui/icons/overview/index.html.md).

### Nested UI Routes

Consider that along the UI route above at `src/admin/routes/custom/page.tsx` you create a nested UI route at `src/admin/routes/custom/nested/page.tsx` that also exports route configurations:

![Example of nested UI route file in the application's directory structure](https://res.cloudinary.com/dza7lstvk/image/upload/v1732867243/Medusa%20Book/ui-route-dir-overview_tgju25.jpg)

```tsx title="src/admin/routes/custom/nested/page.tsx"
import { defineRouteConfig } from "@medusajs/admin-sdk"
import { Container, Heading } from "@medusajs/ui"

const NestedCustomPage = () => {
  return (
    <Container className="divide-y p-0">
      <div className="flex items-center justify-between px-6 py-4">
        <Heading level="h2">This is my nested custom route</Heading>
      </div>
    </Container>
  )
}

export const config = defineRouteConfig({
  label: "Nested Route",
})

export default NestedCustomPage
```

This UI route is shown in the sidebar as an item nested in the parent "Custom Route" item. Nested items are only shown when the parent sidebar items (in this case, "Custom Route") are clicked.

#### Caveats

Some caveats for nested UI routes in the sidebar:

- Nested dynamic UI routes, such as one created at `src/admin/routes/custom/[id]/page.tsx` aren't added to the sidebar as it's not possible to link to a dynamic route. If the dynamic route exports route configurations, a warning is logged in the browser's console.
- Nested routes in setting pages aren't shown in the sidebar to follow the admin's design conventions.
- The `icon` configuration is ignored for the sidebar item of nested UI route to follow the admin's design conventions.

### Route Under Existing Admin Route

You can add a custom UI route under an existing route. For example, you can add a route under the orders route:

```tsx title="src/admin/routes/orders/nested/page.tsx"
import { defineRouteConfig } from "@medusajs/admin-sdk"
import { Container, Heading } from "@medusajs/ui"

const NestedOrdersPage = () => {
  return (
    <Container className="divide-y p-0">
      <div className="flex items-center justify-between px-6 py-4">
        <Heading level="h1">Nested Orders Page</Heading>
      </div>
    </Container>
  )
}

export const config = defineRouteConfig({
  label: "Nested Orders",
  nested: "/orders",
})

export default NestedOrdersPage
```

The `nested` property passed to `defineRouteConfig` specifies which route this custom route is nested under. This route will now show in the sidebar under the existing "Orders" sidebar item.

***

## Create Settings Page

To create a page under the settings section of the admin dashboard, create a UI route under the path `src/admin/routes/settings`.

For example, create a UI route at `src/admin/routes/settings/custom/page.tsx`:

![Example of settings UI route file in the application's directory structure](https://res.cloudinary.com/dza7lstvk/image/upload/v1732867435/Medusa%20Book/setting-ui-route-dir-overview_kytbh8.jpg)

```tsx title="src/admin/routes/settings/custom/page.tsx"
import { defineRouteConfig } from "@medusajs/admin-sdk"
import { Container, Heading } from "@medusajs/ui"

const CustomSettingPage = () => {
  return (
    <Container className="divide-y p-0">
      <div className="flex items-center justify-between px-6 py-4">
        <Heading level="h1">Custom Setting Page</Heading>
      </div>
    </Container>
  )
}

export const config = defineRouteConfig({
  label: "Custom",
})

export default CustomSettingPage
```

This adds a page under the path `/app/settings/custom`. An item is also added to the settings sidebar with the label `Custom`.

***

## Path Parameters

A UI route can accept path parameters if the name of any of the directories in its path is of the format `[param]`.

For example, create the file `src/admin/routes/custom/[id]/page.tsx` with the following content:

![Example of UI route file with path parameters in the application's directory structure](https://res.cloudinary.com/dza7lstvk/image/upload/v1732867748/Medusa%20Book/path-param-ui-route-dir-overview_kcfbev.jpg)

```tsx title="src/admin/routes/custom/[id]/page.tsx" highlights={[["5", "", "Retrieve the path parameter."], ["10", "{id}", "Show the path parameter."]]}
import { useParams } from "react-router-dom"
import { Container, Heading } from "@medusajs/ui"

const CustomPage = () => {
  const { id } = useParams()

  return (
    <Container className="divide-y p-0">
      <div className="flex items-center justify-between px-6 py-4">
        <Heading level="h1">Passed ID: {id}</Heading>
      </div>
    </Container>
  )
}

export default CustomPage
```

You access the passed parameter using `react-router-dom`'s [useParams hook](https://reactrouter.com/en/main/hooks/use-params).

If you run the Medusa application and go to `localhost:9000/app/custom/123`, you'll see `123` printed in the page.

***

## Admin Components List

To build admin customizations that match the Medusa Admin's designs and layouts, refer to [this guide](https://docs.medusajs.com/resources/admin-components/index.html.md) to find common components.

***

## More Routes Customizations

For more customizations related to routes, refer to the [Routing Customizations chapter](https://docs.medusajs.com/learn/fundamentals/admin/routing/index.html.md).


# Seed Data with Custom CLI Script

In this chapter, you'll learn how to seed data using a custom CLI script.

## How to Seed Data

To seed dummy data for development or demo purposes, use a custom CLI script.

In the CLI script, use your custom workflows or Medusa's existing workflows, which you can browse in [this reference](https://docs.medusajs.com/resources/medusa-workflows-reference/index.html.md), to seed data.

### Example: Seed Dummy Products

In this section, you'll follow an example of creating a custom CLI script that seeds fifty dummy products.

First, install the [Faker](https://fakerjs.dev/) library to generate random data in your script:

```bash npm2yarn
npm install --save-dev @faker-js/faker
```

Then, create the file `src/scripts/demo-products.ts` with the following content:

```ts title="src/scripts/demo-products.ts" highlights={highlights} collapsibleLines="1-12" expandButtonLabel="Show Imports"
import { ExecArgs } from "@medusajs/framework/types"
import { faker } from "@faker-js/faker"
import { 
  ContainerRegistrationKeys, 
  Modules, 
  ProductStatus,
} from "@medusajs/framework/utils"
import { 
  createInventoryLevelsWorkflow,
  createProductsWorkflow,
} from "@medusajs/medusa/core-flows"

export default async function seedDummyProducts({
  container,
}: ExecArgs) {
  const salesChannelModuleService = container.resolve(
    Modules.SALES_CHANNEL
  )
  const logger = container.resolve(
    ContainerRegistrationKeys.LOGGER
  )
  const query = container.resolve(
    ContainerRegistrationKeys.QUERY
  )

  const defaultSalesChannel = await salesChannelModuleService
    .listSalesChannels({
      name: "Default Sales Channel",
    })

  const sizeOptions = ["S", "M", "L", "XL"]
  const colorOptions = ["Black", "White"]
  const currency_code = "eur"
  const productsNum = 50

  // TODO seed products
}
```

So far, in the script, you:

- Resolve the Sales Channel Module's main service to retrieve the application's default sales channel. This is the sales channel the dummy products will be available in.
- Resolve the Logger to log messages in the terminal, and Query to later retrieve data useful for the seeded products.
- Initialize some default data to use when seeding the products next.

Next, replace the `TODO` with the following:

```ts title="src/scripts/demo-products.ts"
const productsData = new Array(productsNum).fill(0).map((_, index) => {
  const title = faker.commerce.product() + "_" + index
  return {
    title,
    is_giftcard: true,
    description: faker.commerce.productDescription(),
    status: ProductStatus.PUBLISHED,
    options: [
      {
        title: "Size",
        values: sizeOptions,
      },
      {
        title: "Color",
        values: colorOptions,
      },
    ],
    images: [
      {
        url: faker.image.urlPlaceholder({
          text: title,
        }),
      },
      {
        url: faker.image.urlPlaceholder({
          text: title,
        }),
      },
    ],
    variants: new Array(10).fill(0).map((_, variantIndex) => ({
      title: `${title} ${variantIndex}`,
      sku: `variant-${variantIndex}${index}`,
      prices: new Array(10).fill(0).map((_, priceIndex) => ({
        currency_code,
        amount: 10 * priceIndex,
      })),
      options: {
        Size: sizeOptions[Math.floor(Math.random() * 3)],
      },
    })),
    shipping_profile_id: "sp_123",
    sales_channels: [
      {
        id: defaultSalesChannel[0].id,
      },
    ],
  }
})

// TODO seed products
```

You generate fifty products using the sales channel and variables you initialized, and using Faker for random data, such as the product's title or images.

Then, replace the new `TODO` with the following:

```ts title="src/scripts/demo-products.ts"
const { result: products } = await createProductsWorkflow(container).run({
  input: {
    products: productsData,
  },
})

logger.info(`Seeded ${products.length} products.`)

// TODO add inventory levels
```

You create the generated products using the `createProductsWorkflow` imported previously from `@medusajs/medusa/core-flows`. It accepts the product data as input, and returns the created products.

Only thing left is to create inventory levels for the products. So, replace the last `TODO` with the following:

```ts title="src/scripts/demo-products.ts"
logger.info("Seeding inventory levels.")

const { data: stockLocations } = await query.graph({
  entity: "stock_location",
  fields: ["id"],
})

const { data: inventoryItems } = await query.graph({
  entity: "inventory_item",
  fields: ["id"],
})

const inventoryLevels = inventoryItems.map((inventoryItem) => ({
  location_id: stockLocations[0].id,
  stocked_quantity: 1000000,
  inventory_item_id: inventoryItem.id,
}))

await createInventoryLevelsWorkflow(container).run({
  input: {
    inventory_levels: inventoryLevels,
  },
})

logger.info("Finished seeding inventory levels data.")
```

You use Query to retrieve the stock location, to use the first location in the application, and the inventory items.

Then, you generate inventory levels for each inventory item, associating it with the first stock location.

Finally, you use the `createInventoryLevelsWorkflow` from Medusa's core workflows to create the inventory levels.

### Test Script

To test out the script, run the following command in your project's directory:

```bash
npx medusa exec ./src/scripts/demo-products.ts
```

This seeds the products to your database. If you run your Medusa application and view the products in the dashboard, you'll find fifty new products.


# Admin Widgets

In this chapter, you’ll learn more about widgets and how to use them.

## What is an Admin Widget?

The Medusa Admin dashboard's pages are customizable to insert widgets of custom content in pre-defined injection zones. You create these widgets as React components that allow admin users to perform custom actions.

For example, you can add a widget on the product details page that allow admin users to sync products to a third-party service.

***

## How to Create a Widget?

### Prerequisites

- [Medusa application installed](https://docs.medusajs.com/learn/installation/index.html.md)

You create a widget in a `.tsx` file under the `src/admin/widgets` directory. The file’s default export must be the widget, which is the React component that renders the custom content. The file must also export the widget’s configurations indicating where to insert the widget.

For example, create the file `src/admin/widgets/product-widget.tsx` with the following content:

![Example of widget file in the application's directory structure](https://res.cloudinary.com/dza7lstvk/image/upload/v1732867137/Medusa%20Book/widget-dir-overview_dqsbct.jpg)

```tsx title="src/admin/widgets/product-widget.tsx" highlights={widgetHighlights}
import { defineWidgetConfig } from "@medusajs/admin-sdk"
import { Container, Heading } from "@medusajs/ui"

// The widget
const ProductWidget = () => {
  return (
    <Container className="divide-y p-0">
      <div className="flex items-center justify-between px-6 py-4">
        <Heading level="h2">Product Widget</Heading>
      </div>
    </Container>
  )
}

// The widget's configurations
export const config = defineWidgetConfig({
  zone: "product.details.before",
})

export default ProductWidget
```

You export the `ProductWidget` component, which shows the heading `Product Widget`. In the widget, you use [Medusa UI](https://docs.medusajs.com/ui/index.html.md), a package that Medusa maintains to allow you to customize the dashboard with the same components used to build it.

To export the widget's configurations, you use `defineWidgetConfig` from the Admin Extension SDK. It accepts as a parameter an object with the `zone` property, whose value is a string or an array of strings, each being the name of the zone to inject the widget into.

In the example above, the widget is injected at the top of a product’s details.

The widget component must be created as an arrow function.

### Test the Widget

To test out the widget, start the Medusa application:

```bash npm2yarn
npm run dev
```

Then, open a product’s details page. You’ll find your custom widget at the top of the page.

***

## Props Passed in Detail Pages

Widgets that are injected into a details page receive a `data` prop, which is the main data of the details page.

For example, a widget injected into the `product.details.before` zone receives the product's details in the `data` prop:

```tsx title="src/admin/widgets/product-widget.tsx" highlights={detailHighlights}
import { defineWidgetConfig } from "@medusajs/admin-sdk"
import { Container, Heading } from "@medusajs/ui"
import { 
  DetailWidgetProps, 
  AdminProduct,
} from "@medusajs/framework/types"

// The widget
const ProductWidget = ({ 
  data,
}: DetailWidgetProps<AdminProduct>) => {
  return (
    <Container className="divide-y p-0">
      <div className="flex items-center justify-between px-6 py-4">
        <Heading level="h2">
          Product Widget {data.title}
        </Heading>
      </div>
    </Container>
  )
}

// The widget's configurations
export const config = defineWidgetConfig({
  zone: "product.details.before",
})

export default ProductWidget
```

The props type is `DetailWidgetProps`, and it accepts as a type argument the expected type of `data`. For the product details page, it's `AdminProduct`.

***

## Injection Zone

Refer to [this reference](https://docs.medusajs.com/resources/admin-widget-injection-zones/index.html.md) for the full list of injection zones and their props.

***

## Admin Components List

To build admin customizations that match the Medusa Admin's designs and layouts, refer to [this guide](https://docs.medusajs.com/resources/admin-components/index.html.md) to find common components.


# Pass Additional Data to Medusa's API Route

In this chapter, you'll learn how to pass additional data in requests to Medusa's API Route.

## Why Pass Additional Data?

Some of Medusa's API Routes accept an `additional_data` parameter whose type is an object. The API Route passes the `additional_data` to the workflow, which in turn passes it to its hooks.

This is useful when you have a link from your custom module to a Commerce Module, and you want to perform an additional action when a request is sent to an existing API route.

For example, the [Create Product API Route](https://docs.medusajs.com/api/admin#products_postproducts) accepts an `additional_data` parameter. If you have a data model linked to it, you consume the `productsCreated` hook to create a record of the data model using the custom data and link it to the product.

### API Routes Accepting Additional Data

### API Routes List

- Campaigns
  - [Create Campaign](https://docs.medusajs.com/api/admin#campaigns_postcampaigns)
  - [Update Campaign](https://docs.medusajs.com/api/admin#campaigns_postcampaignsid)
- Cart
  - [Create Cart](https://docs.medusajs.com/api/store#carts_postcarts)
  - [Update Cart](https://docs.medusajs.com/api/store#carts_postcartsid)
- Collections
  - [Create Collection](https://docs.medusajs.com/api/admin#collections_postcollections)
  - [Update Collection](https://docs.medusajs.com/api/admin#collections_postcollectionsid)
- Customers
  - [Create Customer](https://docs.medusajs.com/api/admin#customers_postcustomers)
  - [Update Customer](https://docs.medusajs.com/api/admin#customers_postcustomersid)
  - [Create Address](https://docs.medusajs.com/api/admin#customers_postcustomersidaddresses)
  - [Update Address](https://docs.medusajs.com/api/admin#customers_postcustomersidaddressesaddress_id)
- Draft Orders
  - [Create Draft Order](https://docs.medusajs.com/api/admin#draft-orders_postdraftorders)
- Orders
  - [Complete Orders](https://docs.medusajs.com/api/admin#orders_postordersidcomplete)
  - [Cancel Order's Fulfillment](https://docs.medusajs.com/api/admin#orders_postordersidfulfillmentsfulfillment_idcancel)
  - [Create Shipment](https://docs.medusajs.com/api/admin#orders_postordersidfulfillmentsfulfillment_idshipments)
  - [Create Fulfillment](https://docs.medusajs.com/api/admin#orders_postordersidfulfillments)
- Products
  - [Create Product](https://docs.medusajs.com/api/admin#products_postproducts)
  - [Update Product](https://docs.medusajs.com/api/admin#products_postproductsid)
  - [Create Product Variant](https://docs.medusajs.com/api/admin#products_postproductsidvariants)
  - [Update Product Variant](https://docs.medusajs.com/api/admin#products_postproductsidvariantsvariant_id)
  - [Create Product Option](https://docs.medusajs.com/api/admin#products_postproductsidoptions)
  - [Update Product Option](https://docs.medusajs.com/api/admin#products_postproductsidoptionsoption_id)
- Product Tags
  - [Create Product Tag](https://docs.medusajs.com/api/admin#product-tags_postproducttags)
  - [Update Product Tag](https://docs.medusajs.com/api/admin#product-tags_postproducttagsid)
- Product Types
  - [Create Product Type](https://docs.medusajs.com/api/admin#product-types_postproducttypes)
  - [Update Product Type](https://docs.medusajs.com/api/admin#product-types_postproducttypesid)
- Promotions
  - [Create Promotion](https://docs.medusajs.com/api/admin#promotions_postpromotions)
  - [Update Promotion](https://docs.medusajs.com/api/admin#promotions_postpromotionsid)

***

## How to Pass Additional Data

### 1. Specify Validation of Additional Data

Before passing custom data in the `additional_data` object parameter, you must specify validation rules for the allowed properties in the object.

To do that, use the middleware route object defined in `src/api/middlewares.ts`.

For example, create the file `src/api/middlewares.ts` with the following content:

```ts title="src/api/middlewares.ts"
import { defineMiddlewares } from "@medusajs/framework/http"
import { z } from "zod"

export default defineMiddlewares({
  routes: [
    {
      method: "POST",
      matcher: "/admin/products",
      additionalDataValidator: {
        brand: z.string().optional(),
      },
    },
  ],
})
```

The middleware route object accepts an optional parameter `additionalDataValidator` whose value is an object of key-value pairs. The keys indicate the name of accepted properties in the `additional_data` parameter, and the value is [Zod](https://zod.dev/) validation rules of the property.

In this example, you indicate that the `additional_data` parameter accepts a `brand` property whose value is an optional string.

Refer to [Zod's documentation](https://zod.dev) for all available validation rules.

### 2. Pass the Additional Data in a Request

You can now pass a `brand` property in the `additional_data` parameter of a request to the Create Product API Route.

For example:

```bash
curl -X POST 'http://localhost:9000/admin/products' \
-H 'Content-Type: application/json' \
-H 'Authorization: Bearer {token}' \
--data '{
    "title": "Product 1",
    "options": [
      {
        "title": "Default option",
        "values": ["Default option value"]
      }
    ],
    "shipping_profile_id": "{shipping_profile_id}",
    "additional_data": {
        "brand": "Acme"
    }
}'
```

Make sure to replace the `{token}` in the authorization header with an admin user's authentication token, and `{shipping_profile_id}` with an existing shipping profile's ID.

In this request, you pass in the `additional_data` parameter a `brand` property and set its value to `Acme`.

The `additional_data` is then passed to hooks in the `createProductsWorkflow` used by the API route.

***

## Use Additional Data in a Hook

Learn about workflow hooks in [this guide](https://docs.medusajs.com/learn/fundamentals/workflows/workflow-hooks/index.html.md).

Step functions consuming the workflow hook can access the `additional_data` in the first parameter.

For example, consider you want to store the data passed in `additional_data` in the product's `metadata` property.

To do that, create the file `src/workflows/hooks/product-created.ts` with the following content:

```ts title="src/workflows/hooks/product-created.ts"
import { StepResponse } from "@medusajs/framework/workflows-sdk"
import { createProductsWorkflow } from "@medusajs/medusa/core-flows"
import { Modules } from "@medusajs/framework/utils"

createProductsWorkflow.hooks.productsCreated(
  async ({ products, additional_data }, { container }) => {
    if (!additional_data?.brand) {
      return
    }

    const productModuleService = container.resolve(
      Modules.PRODUCT
    )

    await productModuleService.upsertProducts(
      products.map((product) => ({
        ...product,
        metadata: {
          ...product.metadata,
          brand: additional_data.brand,
        },
      }))
    )

    return new StepResponse(products, {
      products,
      additional_data,
    })
  }
)
```

This consumes the `productsCreated` hook, which runs after the products are created.

If `brand` is passed in `additional_data`, you resolve the Product Module's main service and use its `upsertProducts` method to update the products, adding the brand to the `metadata` property.

### Compensation Function

Hooks also accept a compensation function as a second parameter to undo the actions made by the step function.

For example, pass the following second parameter to the `productsCreated` hook:

```ts title="src/workflows/hooks/product-created.ts"
createProductsWorkflow.hooks.productsCreated(
  async ({ products, additional_data }, { container }) => {
    // ...
  },
  async ({ products, additional_data }, { container }) => {
    if (!additional_data.brand) {
      return
    }

    const productModuleService = container.resolve(
      Modules.PRODUCT
    )

    await productModuleService.upsertProducts(
      products
    )
  }
)
```

This updates the products to their original state before adding the brand to their `metadata` property.


# Throwing and Handling Errors

In this guide, you'll learn how to throw errors in your Medusa application, how it affects an API route's response, and how to change the default error handler of your Medusa application.

## Throw MedusaError

When throwing an error in your API routes, middlewares, workflows, or any customization, throw a `MedusaError` from the Medusa Framework.

The Medusa application's API route error handler then wraps your thrown error in a uniform object and returns it in the response.

For example:

```ts
import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http"
import { MedusaError } from "@medusajs/framework/utils"

export const GET = async (
  req: MedusaRequest,
  res: MedusaResponse
) => {
  if (!req.query.q) {
    throw new MedusaError(
      MedusaError.Types.INVALID_DATA,
      "The `q` query parameter is required."
    )
  }

  // ...
}
```

The `MedusaError` class accepts in its constructor two parameters:

1. The first is the error's type. `MedusaError` has a static property `Types` that you can use. `Types` is an enum whose possible values are explained in the next section.
2. The second is the message to show in the error response.

### Error Object in Response

The error object returned in the response has two properties:

- `type`: The error's type.
- `message`: The error message, if available.
- `code`: A common snake-case code. Its values can be:
  - `invalid_request_error` for the `DUPLICATE_ERROR` type.
  - `api_error`:  for the `DB_ERROR` type.
  - `invalid_state_error` for `CONFLICT` error type.
  - `unknown_error` for any unidentified error type.
  - For other error types, this property won't be available unless you provide a code as a third parameter to the `MedusaError` constructor.

### MedusaError Types

|Type|Description|Status Code|
|---|---|---|---|---|
|\`DB\_ERROR\`|Indicates a database error.|\`500\`|
|\`DUPLICATE\_ERROR\`|Indicates a duplicate of a record already exists. For example, when trying to create a customer whose email is registered by another customer.|\`422\`|
|\`INVALID\_ARGUMENT\`|Indicates an error that occurred due to incorrect arguments or other unexpected state.|\`500\`|
|\`INVALID\_DATA\`|Indicates a validation error.|\`400\`|
|\`UNAUTHORIZED\`|Indicates that a user is not authorized to perform an action or access a route.|\`401\`|
|\`NOT\_FOUND\`|Indicates that the requested resource, such as a route or a record, isn't found.|\`404\`|
|\`NOT\_ALLOWED\`|Indicates that an operation isn't allowed.|\`400\`|
|\`CONFLICT\`|Indicates that a request conflicts with another previous or ongoing request. The error message in this case is ignored for a default message.|\`409\`|
|\`PAYMENT\_AUTHORIZATION\_ERROR\`|Indicates an error has occurred while authorizing a payment.|\`422\`|
|Other error types|Any other error type results in an |\`500\`|

***

## Override Error Handler

The `defineMiddlewares` function used to apply middlewares on routes accepts an `errorHandler` in its object parameter. Use it to override the default error handler for API routes.

This error handler will also be used for errors thrown in Medusa's API routes and resources.

For example, create `src/api/middlewares.ts` with the following:

```ts title="src/api/middlewares.ts" collapsibleLines="1-8" expandMoreLabel="Show Imports"
import { 
  defineMiddlewares, 
  MedusaNextFunction, 
  MedusaRequest, 
  MedusaResponse,
} from "@medusajs/framework/http"
import { MedusaError } from "@medusajs/framework/utils"

export default defineMiddlewares({
  errorHandler: (
    error: MedusaError | any, 
    req: MedusaRequest, 
    res: MedusaResponse, 
    next: MedusaNextFunction
  ) => {
    res.status(400).json({
      error: "Something happened.",
    })
  },
})
```

The `errorHandler` property's value is a function that accepts four parameters:

1. The error thrown. Its type can be `MedusaError` or any other thrown error type.
2. A request object of type `MedusaRequest`.
3. A response object of type `MedusaResponse`.
4. A function of type MedusaNextFunction that executes the next middleware in the stack.

This example overrides Medusa's default error handler with a handler that always returns a `400` status code with the same message.


# Handling CORS in API Routes

In this chapter, you’ll learn about the CORS middleware and how to configure it for custom API routes.

## CORS Overview

Cross-Origin Resource Sharing (CORS) allows only configured origins to access your API Routes.

For example, if you allow only origins starting with `http://localhost:7001` to access your Admin API Routes, other origins accessing those routes get a CORS error.

### CORS Configurations

The `storeCors` and `adminCors` properties of Medusa's `http` configuration set the allowed origins for routes starting with `/store` and `/admin` respectively.

These configurations accept a URL pattern to identify allowed origins.

For example:

```js title="medusa-config.ts"
module.exports = defineConfig({
  projectConfig: {
    http: {
      storeCors: "http://localhost:8000",
      adminCors: "http://localhost:7001",
      // ...
    },
  },
})
```

This allows the `http://localhost:7001` origin to access the Admin API Routes, and the `http://localhost:8000` origin to access Store API Routes.

Learn more about the CORS configurations in [this resource guide](https://docs.medusajs.com/learn/configurations/medusa-config#http/index.html.md).

***

## CORS in Store and Admin Routes

To disable the CORS middleware for a route, export a `CORS` variable in the route file with its value set to `false`.

For example:

```ts title="src/api/store/custom/route.ts" highlights={[["15"]]}
import type { 
  MedusaRequest, 
  MedusaResponse,
} from "@medusajs/framework/http"

export const GET = (
  req: MedusaRequest,
  res: MedusaResponse
) => {
  res.json({
    message: "[GET] Hello world!",
  })
}

export const CORS = false
```

This disables the CORS middleware on API Routes at the path `/store/custom`.

***

## CORS in Custom Routes

If you create a route that doesn’t start with `/store` or `/admin`, you must apply the CORS middleware manually. Otherwise, all requests to your API route lead to a CORS error.

You can do that in the exported middlewares configurations in `src/api/middlewares.ts`.

For example:

```ts title="src/api/middlewares.ts" highlights={highlights} collapsibleLines="1-10" expandButtonLabel="Show Imports"
import { defineMiddlewares } from "@medusajs/framework/http"
import type { 
  MedusaNextFunction, 
  MedusaRequest, 
  MedusaResponse, 
} from "@medusajs/framework/http"
import { ConfigModule } from "@medusajs/framework/types"
import { parseCorsOrigins } from "@medusajs/framework/utils"
import cors from "cors"

export default defineMiddlewares({
  routes: [
    {
      matcher: "/custom*",
      middlewares: [
        (
          req: MedusaRequest, 
          res: MedusaResponse, 
          next: MedusaNextFunction
        ) => {
          const configModule: ConfigModule =
            req.scope.resolve("configModule")

          return cors({
            origin: parseCorsOrigins(
              configModule.projectConfig.http.storeCors
            ),
            credentials: true,
          })(req, res, next)
        },
      ],
    },
  ],
})
```

This retrieves the configurations exported from `medusa-config.ts` and applies the `storeCors` to routes starting with `/custom`.


# HTTP Methods

In this chapter, you'll learn about how to add new API routes for each HTTP method.

## HTTP Method Handler

An API route is created for every HTTP method you export a handler function for in a route file.

Allowed HTTP methods are: `GET`, `POST`, `DELETE`, `PUT`, `PATCH`, `OPTIONS`, and `HEAD`.

For example, create the file `src/api/hello-world/route.ts` with the following content:

```ts title="src/api/hello-world/route.ts"
import type {
  MedusaRequest,
  MedusaResponse,
} from "@medusajs/framework/http"

export const GET = async (
  req: MedusaRequest,
  res: MedusaResponse
) => {
  res.json({
    message: "[GET] Hello world!",
  })
}

export const POST = async (
  req: MedusaRequest,
  res: MedusaResponse
) => {
  res.json({
    message: "[POST] Hello world!",
  })
}
```

This adds two API Routes:

- A `GET` route at `http://localhost:9000/hello-world`.
- A `POST` route at `http://localhost:9000/hello-world`.


# Configure Request Body Parser

In this chapter, you'll learn how to configure the request body parser for your API routes.

## Default Body Parser Configuration

The Medusa application configures the body parser by default to parse JSON, URL-encoded, and text request content types. You can parse other data types by adding the relevant [Express middleware](https://expressjs.com/en/guide/using-middleware.html) or preserve the raw body data by configuring the body parser, which is useful for webhook requests.

This chapter shares some examples of configuring the body parser for different data types or use cases.

***

## Preserve Raw Body Data for Webhooks

If your API route receives webhook requests, you might want to preserve the raw body data. To do this, you can configure the body parser to parse the raw body data and store it in the `req.rawBody` property.

To do that, create the file `src/api/middlewares.ts` with the following content:

```ts title="src/api/middlewares.ts" highlights={preserveHighlights}
import { defineMiddlewares } from "@medusajs/framework/http"

export default defineMiddlewares({
  routes: [
    {
      method: ["POST"],
      bodyParser: { preserveRawBody: true },
      matcher: "/custom",
    },
  ],
})
```

The middleware route object passed to `routes` accepts a `bodyParser` property whose value is an object of configuration for the default body parser. By enabling the `preserveRawBody` property, the raw body data is preserved and stored in the `req.rawBody` property.

Learn more about [middlewares](https://docs.medusajs.com/learn/fundamentals/api-routes/middlewares/index.html.md).

You can then access the raw body data in your API route handler:

```ts title="src/api/custom/route.ts"
import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http"

export async function POST(
  req: MedusaRequest,
  res: MedusaResponse
) {
  console.log(req.rawBody)

  // TODO use raw body
}
```

***

## Configure Request Body Size Limit

By default, the body parser limits the request body size to `100kb`. If a request body exceeds that size, the Medusa application throws an error.

You can configure the body parser to accept larger request bodies by setting the `sizeLimit` property of the `bodyParser` object in a middleware route object. For example:

```ts title="src/api/middlewares.ts" highlights={sizeLimitHighlights}
import { defineMiddlewares } from "@medusajs/framework/http"

export default defineMiddlewares({
  routes: [
    {
      method: ["POST"],
      bodyParser: { sizeLimit: "2mb" },
      matcher: "/custom",
    },
  ],
})
```

The `sizeLimit` property accepts one of the following types of values:

- A string representing the size limit in bytes (For example, `100kb`, `2mb`, `5gb`). It is passed to the [bytes](https://www.npmjs.com/package/bytes) library to parse the size.
- A number representing the size limit in bytes. For example, `1024` for 1kb.

***

## Configure File Uploads

To accept file uploads in your API routes, you can configure the [Express Multer middleware](https://expressjs.com/en/resources/middleware/multer.html) on your route.

The `multer` package is available through the `@medusajs/medusa` package, so you don't need to install it. However, for better typing support, install the `@types/multer` package as a development dependency:

```bash npm2yarn
npm install --save-dev @types/multer
```

Then, to configure file upload for your route, create the file `src/api/middlewares.ts` with the following content:

```ts title="src/api/middlewares.ts" highlights={uploadHighlights}
import { defineMiddlewares } from "@medusajs/framework/http"
import multer from "multer"

const upload = multer({ storage: multer.memoryStorage() })

export default defineMiddlewares({
  routes: [
    {
      method: ["POST"],
      matcher: "/custom",
      middlewares: [
        // @ts-ignore
        upload.array("files"),
      ],
    },
  ],
})
```

In the example above, you configure the `multer` middleware to store the uploaded files in memory. Then, you apply the `upload.array("files")` middleware to the route to accept file uploads. By using the `array` method, you accept multiple file uploads with the same `files` field name.

You can then access the uploaded files in your API route handler:

```ts title="src/api/custom/route.ts"
import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http"

export async function POST(
  req: MedusaRequest,
  res: MedusaResponse
) {
  const files = req.files as Express.Multer.File[]

  // TODO handle files
}
```

The uploaded files are stored in the `req.files` property as an array of Multer file objects that have properties like `filename` and `mimetype`.

### Uploading Files using File Module Provider

The recommended way to upload the files to storage using the configured [File Module Provider](https://docs.medusajs.com/resources/infrastructure-modules/file/index.html.md) is to use the [uploadFilesWorkflow](https://docs.medusajs.com/resources/references/medusa-workflows/uploadFilesWorkflow/index.html.md):

```ts title="src/api/custom/route.ts"
import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http"
import { MedusaError } from "@medusajs/framework/utils"
import { uploadFilesWorkflow } from "@medusajs/medusa/core-flows"

export async function POST(
  req: MedusaRequest,
  res: MedusaResponse
) {
  const files = req.files as Express.Multer.File[]

  if (!files?.length) {
    throw new MedusaError(
      MedusaError.Types.INVALID_DATA,
      "No files were uploaded"
    )
  }

  const { result } = await uploadFilesWorkflow(req.scope).run({
    input: {
      files: files?.map((f) => ({
        filename: f.originalname,
        mimeType: f.mimetype,
        content: f.buffer.toString("binary"),
        access: "public",
      })),
    },
  })

  res.status(200).json({ files: result })
}
```

Check out the [uploadFilesWorkflow reference](https://docs.medusajs.com/resources/references/medusa-workflows/uploadFilesWorkflow/index.html.md) for details on the expected input and output of the workflow.


# API Route Parameters

In this chapter, you’ll learn about path, query, and request body parameters.

## Path Parameters

To create an API route that accepts a path parameter, create a directory within the route file's path whose name is of the format `[param]`.

For example, to create an API Route at the path `/hello-world/:id`, where `:id` is a path parameter, create the file `src/api/hello-world/[id]/route.ts` with the following content:

```ts title="src/api/hello-world/[id]/route.ts" highlights={singlePathHighlights}
import type {
  MedusaRequest,
  MedusaResponse,
} from "@medusajs/framework/http"

export const GET = async (
  req: MedusaRequest,
  res: MedusaResponse
) => {
  res.json({
    message: `[GET] Hello ${req.params.id}!`,
  })
}
```

The `MedusaRequest` object has a `params` property. `params` holds the path parameters in key-value pairs.

### Multiple Path Parameters

To create an API route that accepts multiple path parameters, create within the file's path multiple directories whose names are of the format `[param]`.

For example, to create an API route at `/hello-world/:id/name/:name`, create the file `src/api/hello-world/[id]/name/[name]/route.ts` with the following content:

```ts title="src/api/hello-world/[id]/name/[name]/route.ts" highlights={multiplePathHighlights}
import type {
  MedusaRequest,
  MedusaResponse,
} from "@medusajs/framework/http"

export const GET = async (
  req: MedusaRequest,
  res: MedusaResponse
) => {
  res.json({
    message: `[GET] Hello ${
      req.params.id
    } - ${req.params.name}!`,
  })
}
```

You access the `id` and `name` path parameters using the `req.params` property.

***

## Query Parameters

You can access all query parameters in the `query` property of the `MedusaRequest` object. `query` is an object of key-value pairs, where the key is a query parameter's name, and the value is its value.

For example:

```ts title="src/api/hello-world/route.ts" highlights={queryHighlights}
import type {
  MedusaRequest,
  MedusaResponse,
} from "@medusajs/framework/http"

export const GET = async (
  req: MedusaRequest,
  res: MedusaResponse
) => {
  res.json({
    message: `Hello ${req.query.name}`,
  })
}
```

The value of `req.query.name` is the value passed in `?name=John`, for example.

### Validate Query Parameters

You can apply validation rules on received query parameters to ensure they match specified rules and types.

Learn more in [this documentation](https://docs.medusajs.com/learn/fundamentals/api-routes/validation#how-to-validate-request-query-paramters/index.html.md).

***

## Request Body Parameters

The Medusa application parses the body of any request having a JSON, URL-encoded, or text request content types. The request body parameters are set in the `MedusaRequest`'s `body` property.

Learn more about configuring body parsing in [this guide](https://docs.medusajs.com/learn/fundamentals/api-routes/parse-body/index.html.md).

For example:

```ts title="src/api/hello-world/route.ts" highlights={bodyHighlights}
import type {
  MedusaRequest,
  MedusaResponse,
} from "@medusajs/framework/http"

type HelloWorldReq = {
  name: string
}

export const POST = async (
  req: MedusaRequest<HelloWorldReq>,
  res: MedusaResponse
) => {
  res.json({
    message: `[POST] Hello ${req.body.name}!`,
  })
}
```

In this example, you use the `name` request body parameter to create the message in the returned response.

The `MedusaRequest` type accepts a type argument that indicates the type of the request body. This is useful for auto-completion and to avoid typing errors.

To test it out, send the following request to your Medusa application:

```bash
curl -X POST 'http://localhost:9000/hello-world' \
-H 'Content-Type: application/json' \
--data-raw '{
  "name": "John"
}'
```

This returns the following JSON object:

```json
{
  "message": "[POST] Hello John!"
}
```

### Validate Body Parameters

You can apply validation rules on received body parameters to ensure they match specified rules and types.

Learn more in [this documentation](https://docs.medusajs.com/learn/fundamentals/api-routes/validation#how-to-validate-request-body/index.html.md).


# Override API Routes

In this chapter, you'll learn the approach recommended when you need to override an existing API route in Medusa.

## Approaches to Consider Before Overriding API Routes

While building customizations in your Medusa application, you may need to make changes to existing API routes for your business use case.

Medusa provides the following approaches to customize API routes:

|Approach|Description|
|---|---|
|Pass Additional Data|Pass custom data to the API route with custom validation.|
|Perform Custom Logic within an Existing Flows|API routes execute workflows to perform business logic, which may have hooks that allow you to perform custom logic.|
|Use Custom Middlewares|Use custom middlewares to perform custom logic before the API route is executed. However, you cannot remove or replace middlewares applied to existing API routes.|
|Listen to Events in Subscribers|Functionalities in API routes may trigger events that you can handle in subscribers. This is useful if you're performing an action that isn't integral to the API route's core functionality or response.|

If the above approaches do not meet your needs, you can consider the approaches mentioned in the rest of this chapter.

***

## Replicate, Don't Override API Routes

If the approaches mentioned in the [section above](#approaches-to-consider-before-overriding-api-routes) do not meet your needs, you can replicate an existing API route and modify it to suit your requirements.

By replicating instead of overriding, the original API route remains intact, allowing you to easily revert to the original functionality if needed. You can also update your Medusa version without worrying about breaking changes in the original API route.

***

## How to Replicate an API Route?

Medusa's API routes are generally slim and use logic contained in [workflows](https://docs.medusajs.com/learn/fundamentals/workflows/index.html.md). So, creating a custom route based on the original route is straightforward.

You can view the source code for Medusa's API routes in the [Medusa GitHub repository](https://github.com/medusajs/medusa/tree/develop/packages/medusa/src/api).

For example, if you need to allow vendors to access the `POST /admin/products` API route, you can create an API route in your Medusa project at `src/api/vendor/products/route.ts` with the [same code as the original route](https://github.com/medusajs/medusa/blob/develop/packages/medusa/src/api/admin/products/route.ts#L88). Then, you can make changes to it or its middlewares.

***

## When to Replicate an API Route?

Some examples of when you might want to replicate an API route include:

|Use Case|Description|
|---|---|
|Custom Validation|You want to change the validation logic for a specific API route, and the |
|Change Authentication|You want to remove required authentication for a specific API route, or you want to allow custom |
|Custom Response|You want to change the response format of an existing API route.|
|Override Middleware|You want to override the middleware applied on existing API routes. Because of |


# Middlewares

In this chapter, you’ll learn about middlewares and how to create them.

## What is a Middleware?

A middleware is a function executed when a request is sent to an API Route. It's executed before the route handler function.

Middlewares are used to guard API routes, parse request content types other than `application/json`, manipulate request data, and more.

![Diagram showcasing how a middleware is executed when a request is sent to an API route.](https://res.cloudinary.com/dza7lstvk/image/upload/v1746775148/Medusa%20Book/middleware-overview_wc2ws5.jpg)

As Medusa's server is based on Express, you can use any [Express middleware](https://expressjs.com/en/resources/middleware.html).

### Middleware Types

There are two types of middlewares:

|Type|Description|Example|
|---|---|---|
|Global Middleware|A middleware that applies to all routes matching a specified pattern.|\`/custom\*\`|
|Route Middleware|A middleware that applies to routes matching a specified pattern and HTTP method(s).|A middleware that applies to all |

These middlewares generally have the same definition and usage, but they differ in the routes they apply to. You'll learn how to create both types in the following sections.

***

## How to Create a Middleware?

Middlewares of all types are defined in the special file `src/api/middlewares.ts`. Use the `defineMiddlewares` function from the Medusa Framework to define the middlewares, and export its value.

For example:

### Global Middleware

```ts title="src/api/middlewares.ts"
import { 
  defineMiddlewares,
  MedusaNextFunction, 
  MedusaRequest, 
  MedusaResponse, 
} from "@medusajs/framework/http"

export default defineMiddlewares({
  routes: [
    {
      matcher: "/custom*",
      middlewares: [
        (
          req: MedusaRequest, 
          res: MedusaResponse, 
          next: MedusaNextFunction
        ) => {
          console.log("Received a request!")

          next()
        },
      ],
    },
  ],
})
```

### Route Middleware

```ts title="src/api/middlewares.ts" highlights={highlights}
import { 
  defineMiddlewares,
  MedusaNextFunction, 
  MedusaRequest, 
  MedusaResponse, 
} from "@medusajs/framework/http"

export default defineMiddlewares({
  routes: [
    {
      matcher: "/custom*",
      method: ["POST", "PUT"],
      middlewares: [
        (
          req: MedusaRequest, 
          res: MedusaResponse, 
          next: MedusaNextFunction
        ) => {
          console.log("Received a request!")

          next()
        },
      ],
    },
  ],
})
```

The `defineMiddlewares` function accepts a middleware configurations object that has the property `routes`. `routes`'s value is an array of middleware route objects, each having the following properties:

- `matcher`: a string or regular expression indicating the API route path to apply the middleware on. The regular expression must be compatible with [path-to-regexp](https://github.com/pillarjs/path-to-regexp).
- `middlewares`: An array of global and route middleware functions.
- `method`: (optional) By default, a middleware is applied on all HTTP methods for a route. You can specify one or more HTTP methods to apply the middleware to in this option, making it a route middleware.

### Test the Middleware

To test the middleware:

1. Start the application:

```bash npm2yarn
npm run dev
```

2. Send a request to any API route starting with `/custom`. If you specified an HTTP method in the `method` property, make sure to use that method.
3. See the following message in the terminal:

```bash
Received a request!
```

***

## When to Use Middlewares

Middlewares are useful for:

- [Protecting API routes](https://docs.medusajs.com/learn/fundamentals/api-routes/protected-routes/index.html.md) to ensure that only authenticated users can access them.
- [Validating](https://docs.medusajs.com/learn/fundamentals/api-routes/validation/index.html.md) request query and body parameters.
- [Parsing](https://docs.medusajs.com/learn/fundamentals/api-routes/parse-body/index.html.md) request content types other than `application/json`.
- [Applying CORS](https://docs.medusajs.com/learn/fundamentals/api-routes/cors/index.html.md) configurations to custom API routes.

***

## Middleware Function Parameters

The middleware function accepts three parameters:

1. A request object of type `MedusaRequest`.
2. A response object of type `MedusaResponse`.
3. A function of type `MedusaNextFunction` that executes the next middleware in the stack.

You must call the `next` function in the middleware. Otherwise, other middlewares and the API route handler won’t execute.

For example:

```ts title="src/api/middlewares.ts"
import { 
  MedusaNextFunction, 
  MedusaRequest, 
  MedusaResponse, 
  defineMiddlewares,
} from "@medusajs/framework/http"

export default defineMiddlewares({
  routes: [
    {
      matcher: "/custom*",
      middlewares: [
        (
          req: MedusaRequest, 
          res: MedusaResponse, 
          next: MedusaNextFunction
        ) => {
          console.log("Received a request!", req.body)

          next()
        },
      ],
    },
  ],
})
```

This middleware logs the request body to the terminal, then calls the `next` function to execute the next middleware in the stack.

***

## Middleware for Routes with Path Parameters

To indicate a path parameter in a middleware's `matcher` pattern, use the format `:{param-name}`.

A middleware applied on a route with path parameters is a route middleware.

For example:

```ts title="src/api/middlewares.ts" collapsibleLines="1-7" expandMoreLabel="Show Imports" highlights={pathParamHighlights}
import { 
  MedusaNextFunction, 
  MedusaRequest, 
  MedusaResponse, 
  defineMiddlewares,
} from "@medusajs/framework/http"

export default defineMiddlewares({
  routes: [
    {
      matcher: "/custom/:id",
      middlewares: [
        // ...
      ],
    },
  ],
})
```

This applies a middleware to the routes defined in the file `src/api/custom/[id]/route.ts`.

***

## Request URLs with Trailing Backslashes

A middleware whose `matcher` pattern doesn't end with a backslash won't be applied for requests to URLs with a trailing backslash.

For example, consider you have the following middleware:

```ts title="src/api/middlewares.ts" collapsibleLines="1-7" expandMoreLabel="Show Imports"
import { 
  MedusaNextFunction, 
  MedusaRequest, 
  MedusaResponse, 
  defineMiddlewares,
} from "@medusajs/framework/http"

export default defineMiddlewares({
  routes: [
    {
      matcher: "/custom",
      middlewares: [
        (
          req: MedusaRequest, 
          res: MedusaResponse, 
          next: MedusaNextFunction
        ) => {
          console.log("Received a request!")

          next()
        },
      ],
    },
  ],
})
```

If you send a request to `http://localhost:9000/custom`, the middleware will run.

However, if you send a request to `http://localhost:9000/custom/`, the middleware won't run.

In general, avoid adding trailing backslashes when sending requests to API routes.

***

## How Are Middlewares Ordered and Applied?

The information explained in this section is applicable starting from [Medusa v2.6](https://github.com/medusajs/medusa/releases/tag/v2.6).

### Middleware and Routes Execution Order

The Medusa application registers middlewares and API route handlers in the following order, stacking them on top of each other:

![Diagram showcasing the order in which middlewares and route handlers are registered.](https://res.cloudinary.com/dza7lstvk/image/upload/v1746776911/Medusa%20Book/middleware-registration-overview_spc02f.jpg)

1. Global middlewares in the following order:
   1. Global middleware defined in the Medusa's core.
   2. Global middleware defined in the plugins (in the order the plugins are registered in).
   3. Global middleware you define in the application.
2. Route middlewares in the following order:
   1. Route middleware defined in the Medusa's core.
   2. Route middleware defined in the plugins (in the order the plugins are registered in).
   3. Route middleware you define in the application.
3. API routes in the following order:
   1. API routes defined in the Medusa's core.
   2. API routes defined in the plugins (in the order the plugins are registered in).
   3. API routes you define in the application.

Then, when a request is sent to an API route, the stack is executed in order: global middlewares are executed first, then the route middlewares, and finally the route handlers.

![Diagram showcasing the order in which middlewares and route handlers are executed when a request is sent to an API route.](https://res.cloudinary.com/dza7lstvk/image/upload/v1746776172/Medusa%20Book/middleware-order-overview_h7kzfl.jpg)

For example, consider you have the following middlewares:

```ts title="src/api/middlewares.ts"
export default defineMiddlewares({
  routes: [
    {
      matcher: "/custom",
      middlewares: [
        (req, res, next) => {
          console.log("Global middleware")
          next()
        },
      ],
    },
    {
      matcher: "/custom",
      method: ["GET"],
      middlewares: [
        (req, res, next) => {
          console.log("Route middleware")
          next()
        },
      ],
    },
  ],
})
```

When you send a request to `/custom` route, the following messages are logged in the terminal:

```bash
Global middleware
Route middleware
Hello from custom! # message logged from API route handler
```

The global middleware runs first, then the route middleware, and finally the route handler, assuming that it logs the message `Hello from custom!`.

### Middlewares Sorting

On top of the previous ordering, Medusa sorts global and route middlewares based on their matcher pattern in the following order:

1. Wildcard matchers. For example, `/custom*`.
2. Regex matchers. For example, `/custom/(products|collections)`.
3. Static matchers without parameters. For example, `/custom`.
4. Static matchers with parameters. For example, `/custom/:id`.

For example, if you have the following middlewares:

```ts title="src/api/middlewares.ts"
export default defineMiddlewares({
  routes: [
    {
      matcher: "/custom/:id",
      middlewares: [/* ... */],
    },
    {
      matcher: "/custom",
      middlewares: [/* ... */],
    },
    {
      matcher: "/custom*",
      method: ["GET"],
      middlewares: [/* ... */],
    },
    {
      matcher: "/custom/:id",
      method: ["GET"],
      middlewares: [/* ... */],
    },
  ],
})
```

The global middlewares are sorted into the following order before they're registered:

1. Global middleware `/custom`.
2. Global middleware `/custom/:id`.

And the route middlewares are sorted into the following order before they're registered:

1. Route middleware `/custom*`.
2. Route middleware `/custom/:id`.

![Diagram showcasing the order in which middlewares are sorted before being registered.](https://res.cloudinary.com/dza7lstvk/image/upload/v1746777297/Medusa%20Book/middleware-registration-sorting_oyfqhw.jpg)

Then, the middlwares are registered in the order mentioned earlier, with global middlewares first, then the route middlewares.

***

## Overriding Middlewares

A middleware can not override an existing middleware. Instead, middlewares are added to the end of the middleware stack.

For example, if you define a custom validation middleware, such as `validateAndTransformBody`, on an existing route, then both the original and the custom validation middleware will run.

Similarly, if you add an [authenticate](https://docs.medusajs.com/learn/fundamentals/api-routes/protected-routes#protect-custom-api-routes/index.html.md) middleware to an existing route, both the original and the custom authentication middleware will run. So, you can't override the original authentication middleware.

### Alternative Solution to Overriding Middlewares

If you need to change the middlewares applied to a route, you can create a custom [API route](https://docs.medusajs.com/learn/fundamentals/api-routes/index.html.md) that executes the same functionality as the original route, but with the middlewares you want.

Learn more in the [Override API Routes](https://docs.medusajs.com/learn/fundamentals/api-routes/override/index.html.md) chapter.


# Retrieve Custom Links from Medusa's API Route

In this chapter, you'll learn how to retrieve custom data models linked to existing Medusa data models from Medusa's API routes.

## Why Retrieve Custom Linked Data Models?

Often, you'll link custom data models to existing Medusa data models to implement custom features or expand on existing ones.

For example, to add brands for products, you can create a `Brand` data model in a Brand Module, then [define a link](https://docs.medusajs.com/learn/fundamentals/module-links/index.html.md) to the [Product Module](https://docs.medusajs.com/resources/commerce-modules/product/index.html.md)'s `Product` data model.

When you implement this customization, you might need to retrieve the brand of a product using the existing [Get Product API Route](https://docs.medusajs.com/api/admin#products_getproductsid). You can do this by passing the linked data model's name in the `fields` query parameter of the API route.

***

## How to Retrieve Custom Linked Data Models Using `fields`?

Most of Medusa's API routes accept a `fields` query parameter that allows you to specify the fields and relations to retrieve in the resource, such as a product.

For example, to retrieve the brand of a product, you can pass the `brand` field in the `fields` query parameter of the [Get Product API Route](https://docs.medusajs.com/api/admin#products_getproductsid):

```bash
curl 'http://localhost:9000/admin/products/{id}?fields=*brand' \
-H 'Authorization: Bearer {access_token}'
```

The `fields` query parameter accepts a comma-separated list of fields and relations to retrieve. To learn more about using the `fields` query parameter, refer to the [API Reference](https://docs.medusajs.com/api/store#select-fields-and-relations).

By prefixing `brand` with an asterisk (`*`), you retrieve all the default fields of the product, including the `brand` field. If you don't include the `*` prefix, the response will only include the product's brand.

***

## API Routes that Restrict Retrievable Fields

Some of Medusa's API routes restrict the fields and relations you can retrieve, which means you can't pass your custom linked data models in the `fields` query parameter. Medusa makes this restriction to ensure the API routes are performant and secure.

The API routes that restrict the fields and relations you can retrieve are:

- [Customer Store API Routes](https://docs.medusajs.com/api/store#customers)
- [Customer Admin API Routes](https://docs.medusajs.com/api/admin#customers)
- [Product Category Admin API Routes](https://docs.medusajs.com/api/admin#product-categories)

### How to Override Allowed Fields and Relations

For these routes, you need to override the allowed fields and relations to be retrieved. You can do this by adding a [middleware](https://docs.medusajs.com/learn/fundamentals/api-routes/middlewares/index.html.md) to those routes.

For example, to allow retrieving the `b2b_company` of a customer using the [Get Customer Admin API Route](https://docs.medusajs.com/api/admin#customers_getcustomersid), create the file `src/api/middlewares.ts` with the following content:

Learn how to create a middleware in the [Middlewares](https://docs.medusajs.com/learn/fundamentals/api-routes/middlewares/index.html.md) chapter.

```ts title="src/api/middlewares.ts" highlights={highlights}
import { defineMiddlewares } from "@medusajs/medusa"

export default defineMiddlewares({
  routes: [
    {
      matcher: "/store/customers/me",
      method: "GET",
      middlewares: [
        (req, res, next) => {
          req.allowed?.push("b2b_company")
          next()
        },
      ],
    },
  ],
})
```

In this example, you apply a middleware to the [Get Customer Admin API Route](https://docs.medusajs.com/api/admin#customers_getcustomersid).

The request object passed to middlewares has an `allowed` property that contains the fields and relations that can be retrieved. So, you modify the `allowed` array to include the `b2b_company` field.

You can now retrieve the `b2b_company` field using the `fields` query parameter of the [Get Customer Admin API Route](https://docs.medusajs.com/api/admin#customers_getcustomersid):

```bash
curl 'http://localhost:9000/admin/customers/{id}?fields=*b2b_company' \
-H 'Authorization: Bearer {access_token}'
```

In this example, you retrieve the `b2b_company` relation of the customer using the `fields` query parameter.


# Protected API Routes

In this chapter, you’ll learn how to create protected API routes.

## What is a Protected API Route?

By default, an API route is publicly accessible, meaning that any user can access it without authentication. This is useful for public API routes that allow users to browse products, view collections, and so on.

A protected API route is an API route that requires requests to be user-authenticated before performing the route's functionality. Otherwise, the request fails, and the user is prevented access.

Protected API routes are useful for routes that require user authentication, such as creating a product or managing an order. These routes must only be accessed by authenticated admin users.

Refer to the API Reference for [Admin](https://docs.medusajs.com/api/admin#authentication) and [Store](https://docs.medusajs.com/api/store#authentication) to learn how to send authenticated requests.

***

## Default Protected Routes

Any API route, including your custom API routes, are protected if they start with the following prefixes:

|Route Prefix|Access|
|---|---|
|\`/admin\`|Only authenticated admin users can access.|
|\`/store/customers/me\`|Only authenticated customers can access.|

Refer to the API Reference for [Admin](https://docs.medusajs.com/api/admin#authentication) and [Store](https://docs.medusajs.com/api/store#authentication) to learn how to send authenticated requests.

### Opt-Out of Default Authentication Requirement

If you create a custom API route under a prefix that is protected by default, you can opt-out of the authentication requirement by exporting an `AUTHENTICATE` variable in the route file with its value set to `false`.

For example, to disable authentication requirement for a custom API route created at `/admin/custom`, you can export an `AUTHENTICATE` variable in the route file:

```ts title="src/api/admin/custom/route.ts" highlights={[["15"]]}
import type { 
  AuthenticatedMedusaRequest, 
  MedusaResponse,
} from "@medusajs/framework/http"

export const GET = async (
  req: AuthenticatedMedusaRequest, 
  res: MedusaResponse
) => {
  res.json({
    message: "Hello",
  })
}

export const AUTHENTICATE = false
```

Now, any request sent to the `/admin/custom` API route is allowed, regardless if the admin user is authenticated.

***

## Protect Custom API Routes

You can protect API routes using the `authenticate` [middleware](https://docs.medusajs.com/learn/fundamentals/api-routes/middlewares/index.html.md) from the Medusa Framework. When applied to a route, the middleware checks that:

- The correct actor type (for example, `user`, `customer`, or a custom actor type) is authenticated.
- The correct authentication method is used (for example, `session`, `bearer`, or `api-key`).

For example, you can add the `authenticate` middleware in the `src/api/middlewares.ts` file to protect a custom API route:

```ts title="src/api/middlewares.ts" highlights={highlights}
import { 
  defineMiddlewares,
  authenticate,
} from "@medusajs/framework/http"

export default defineMiddlewares({
  routes: [
    {
      matcher: "/custom/admin*",
      middlewares: [authenticate("user", ["session", "bearer", "api-key"])],
    },
    {
      matcher: "/custom/customer*",
      middlewares: [authenticate("customer", ["session", "bearer"])],
    },
  ],
})
```

The `authenticate` middleware function accepts three parameters:

1. The type of user authenticating. Use `user` for authenticating admin users, and `customer` for authenticating customers. You can also pass `*` to allow all types of users, or pass an array of actor types.
2. An array of types of authentication methods allowed. Both `user` and `customer` scopes support `session` and `bearer`. The `admin` scope also supports the `api-key` authentication method.
3. An optional object of configurations accepting the following properties:
   - `allowUnauthenticated`: (default: `false`) A boolean indicating whether authentication is required. For example, you may have an API route where you want to access the logged-in customer if available, but guest customers can still access it too.
   - `allowUnregistered` (default: `false`): A boolean indicating if unregistered users should be allowed access. This is useful when you want to allow users who aren’t registered to access certain routes.

### Example: Custom Actor Type

For example, to require authentication of a custom actor type `manager` to an API route:

```ts title="src/api/middlewares.ts"
import { 
  defineMiddlewares,
  authenticate,
} from "@medusajs/framework/http"

export default defineMiddlewares({
  routes: [
    {
      matcher: "/manager*",
      middlewares: [authenticate("manager", ["session", "bearer"])],
    },
  ],
})
```

Refer to the [Custom Actor-Type Guide](https://docs.medusajs.com/resources/commerce-modules/auth/create-actor-type/index.html.md) for detailed explanation on how to create a custom actor type and apply authentication middlewares.

### Example: Allow Multiple Actor Types

To allow multiple actor types to access an API route, pass an array of actor types to the `authenticate` middleware:

```ts title="src/api/middlewares.ts"
import { 
  defineMiddlewares,
  authenticate,
} from "@medusajs/framework/http"

export default defineMiddlewares({
  routes: [
    {
      matcher: "/custom*",
      middlewares: [authenticate(["user", "customer"], ["session", "bearer"])],
    },
  ],
})
```

### Override Authentication for Medusa's API Routes

In some cases, you may want to override the authentication requirement for Medusa's API routes. For example, you may want to allow custom actor types to access existing protected API routes.

It's not possible to change the [authentication middleware](https://docs.medusajs.com/learn/fundamentals/api-routes/middlewares/index.html.md) applied to an existing API route. Instead, you need to replicate the API route and apply the authentication middleware to it.

Learn more in the [Override API Routes](https://docs.medusajs.com/learn/fundamentals/api-routes/override/index.html.md) chapter.

***

## Access Authentication Details in API Routes

To access the authentication details in an API route, such as the logged-in user's ID, set the type of the first request parameter to `AuthenticatedMedusaRequest`. It extends `MedusaRequest`:

```ts highlights={[["7", "AuthenticatedMedusaRequest"]]}
import type {
  AuthenticatedMedusaRequest,
  MedusaResponse,
} from "@medusajs/framework/http"

export const GET = async (
  req: AuthenticatedMedusaRequest,
  res: MedusaResponse
) => {
  // ...
}
```

The `auth_context.actor_id` property of `AuthenticatedMedusaRequest` holds the ID of the authenticated user or customer. If there isn't any authenticated user or customer, `auth_context` is `undefined`.

For example:

```ts title="src/api/store/custom/route.ts" highlights={[["10", "actor_id"]]}
import type {
  AuthenticatedMedusaRequest,
  MedusaResponse,
} from "@medusajs/framework/http"

export const GET = async (
  req: AuthenticatedMedusaRequest,
  res: MedusaResponse
) => {
  const id = req.auth_context?.actor_id

  // ...
}
```

In this example, you retrieve the ID of the authenticated user, customer, or custom actor type from the `auth_context` property of the `AuthenticatedMedusaRequest` object.

If you opt-out of authentication in a route as mentioned in the [Opt-Out section](#opt-out-of-default-authentication-requirement), you can't access the authenticated user or customer anymore. Use the [authenticate middleware](#protect-custom-api-routes) instead to protect the route.

### Retrieve Logged-In Customer's Details

You can access the logged-in customer’s ID in all API routes starting with `/store` using the `auth_context.actor_id` property of the `AuthenticatedMedusaRequest` object. You can then use [Query](https://docs.medusajs.com/learn/fundamentals/module-links/query/index.html.md) to retrieve the customer details, or pass the ID to a [workflow](https://docs.medusajs.com/learn/fundamentals/workflows/index.html.md) that performs business logic.

For example:

```ts title="src/api/store/custom/route.ts" highlights={customerHighlights}
import type {
  AuthenticatedMedusaRequest,
  MedusaResponse,
} from "@medusajs/framework/http"

export const GET = async (
  req: AuthenticatedMedusaRequest,
  res: MedusaResponse
) => {
  const customerId = req.auth_context?.actor_id
  const query = req.scope.resolve("query")

  const { data: [customer] } = await query.graph({
    entity: "customer",
    fields: ["*"],
    filters: {
      id: customerId,
    },
  }, {
    throwIfKeyNotFound: true,
  })

  // do something with the customer data...
}
```

In this example, you retrieve the customer's ID and resolve Query from the [Medusa container](https://docs.medusajs.com/learn/fundamentals/medusa-container/index.html.md).

Then, you use Query to retrieve the customer details. The `throwIfKeyNotFound` option throws an error if the customer with the specified ID is not found.

After that, you can use the customer's details in your API route.

### Retrieve Logged-In Admin User's Details

You can access the logged-in admin user’s ID in all API routes starting with `/admin` using the `auth_context.actor_id` property of the `AuthenticatedMedusaRequest` object. You can then use [Query](https://docs.medusajs.com/learn/fundamentals/module-links/query/index.html.md) to retrieve the user details, or pass the ID to a [workflow](https://docs.medusajs.com/learn/fundamentals/workflows/index.html.md) that performs business logic.

For example:

```ts title="src/api/admin/custom/route.ts" highlights={adminHighlights}
import type {
  AuthenticatedMedusaRequest,
  MedusaResponse,
} from "@medusajs/framework/http"

export const GET = async (
  req: AuthenticatedMedusaRequest,
  res: MedusaResponse
) => {
  const userId = req.auth_context?.actor_id
  const query = req.scope.resolve("query")

  const { data: [user] } = await query.graph({
    entity: "user",
    fields: ["*"],
    filters: {
      id: userId,
    },
  }, {
    throwIfKeyNotFound: true,
  })

  // do something with the user data...
}
```

In this example, you retrieve the admin user's ID and resolve Query from the [Medusa container](https://docs.medusajs.com/learn/fundamentals/medusa-container/index.html.md).

Then, you use Query to retrieve the user details. The `throwIfKeyNotFound` option throws an error if the user with the specified ID is not found.

After that, you can use the user's details in your API route.


# API Route Response

In this chapter, you'll learn how to send a response in your API route.

## Send a JSON Response

To send a JSON response, use the `json` method of the `MedusaResponse` object passed as the second parameter of your API route handler.

For example:

```ts title="src/api/custom/route.ts" highlights={jsonHighlights}
import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http"

export const GET = async (
  req: MedusaRequest,
  res: MedusaResponse
) => {
  res.json({
    message: "Hello, World!",
  })
}
```

This API route returns the following JSON object:

```json
{
  "message": "Hello, World!"
}
```

***

## Set Response Status Code

By default, setting the JSON data using the `json` method returns a response with a `200` status code.

To change the status code, use the `status` method of the `MedusaResponse` object.

For example:

```ts title="src/api/custom/route.ts" highlights={statusHighlight}
import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http"

export const GET = async (
  req: MedusaRequest,
  res: MedusaResponse
) => {
  res.status(201).json({
    message: "Hello, World!",
  })
}
```

The response of this API route has the status code `201`.

***

## Change Response Content Type

To return response data other than a JSON object, use the `writeHead` method of the `MedusaResponse` object. It allows you to set the response headers, including the content type.

For example, to create an API route that returns an event stream:

```ts highlights={streamHighlights}
import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http"

export const GET = async (
  req: MedusaRequest,
  res: MedusaResponse
) => {
  res.writeHead(200, {
    "Content-Type": "text/event-stream",
    "Cache-Control": "no-cache",
    Connection: "keep-alive",
  })

  const interval = setInterval(() => {
    res.write("Streaming data...\n")
  }, 3000)

  req.on("end", () => {
    clearInterval(interval)
    res.end()
  })
}
```

The `writeHead` method accepts two parameters:

1. The first one is the response's status code.
2. The second is an object of key-value pairs to set the headers of the response.

This API route opens a stream by setting the `Content-Type` in the header to `text/event-stream`. It then simulates a stream by creating an interval that writes the stream data every three seconds.

***

## Do More with Responses

The `MedusaResponse` type is based on [Express's Response](https://expressjs.com/en/api.html#res). Refer to their API reference for other uses of responses.


# Request Body and Query Parameter Validation

In this chapter, you'll learn how to validate request body and query parameters in your custom API route.

## Request Validation

Consider you're creating a `POST` API route at `/custom`. It accepts two parameters `a` and `b` that are required numbers, and returns their sum.

Medusa provides two middlewares to validate the request body and query paramters of incoming requests to your custom API routes:

- `validateAndTransformBody` to validate the request's body parameters against a schema.
- `validateAndTransformQuery` to validate the request's query parameters against a schema.

Both middlewares accept a [Zod](https://zod.dev/) schema as a parameter, which gives you flexibility in how you define your validation schema with complex rules.

The next steps explain how to add request body and query parameter validation to the API route mentioned earlier.

***

## How to Validate Request Body

### Step 1: Create Validation Schema

Medusa uses [Zod](https://zod.dev/) to create validation schemas. These schemas are then used to validate incoming request bodies or query parameters.

To create a validation schema with Zod, create a `validators.ts` file in any `src/api` subfolder. This file holds Zod schemas for each of your API routes.

For example, create the file `src/api/custom/validators.ts` with the following content:

```ts title="src/api/custom/validators.ts"
import { z } from "zod"

export const PostStoreCustomSchema = z.object({
  a: z.number(),
  b: z.number(),
})
```

The `PostStoreCustomSchema` variable is a Zod schema that indicates the request body is valid if:

1. It's an object.
2. It has a property `a` that is a required number.
3. It has a property `b` that is a required number.

### Step 2: Add Request Body Validation Middleware

To use this schema for validating the body parameters of requests to `/custom`, use the `validateAndTransformBody` middleware provided by `@medusajs/framework/http`. It accepts the Zod schema as a parameter.

For example, create the file `src/api/middlewares.ts` with the following content:

```ts title="src/api/middlewares.ts"
import { 
  defineMiddlewares,
  validateAndTransformBody,
} from "@medusajs/framework/http"
import { PostStoreCustomSchema } from "./custom/validators"

export default defineMiddlewares({
  routes: [
    {
      matcher: "/custom",
      method: "POST",
      middlewares: [
        validateAndTransformBody(PostStoreCustomSchema),
      ],
    },
  ],
})
```

This applies the `validateAndTransformBody` middleware on `POST` requests to `/custom`. It uses the `PostStoreCustomSchema` as the validation schema.

#### How the Validation Works

If a request's body parameters don't pass the validation, the `validateAndTransformBody` middleware throws an error indicating the validation errors.

If a request's body parameters are validated successfully, the middleware sets the validated body parameters in the `validatedBody` property of `MedusaRequest`.

### Step 3: Use Validated Body in API Route

In your API route, consume the validated body using the `validatedBody` property of `MedusaRequest`.

For example, create the file `src/api/custom/route.ts` with the following content:

```ts title="src/api/custom/route.ts" highlights={routeHighlights}
import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http"
import { z } from "zod"
import { PostStoreCustomSchema } from "./validators"

type PostStoreCustomSchemaType = z.infer<
  typeof PostStoreCustomSchema
>

export const POST = async (
  req: MedusaRequest<PostStoreCustomSchemaType>,
  res: MedusaResponse
) => {
  res.json({
    sum: req.validatedBody.a + req.validatedBody.b,
  })
}
```

In the API route, you use the `validatedBody` property of `MedusaRequest` to access the values of the `a` and `b` properties.

To pass the request body's type as a type parameter to `MedusaRequest`, use Zod's `infer` type that accepts the type of a schema as a parameter.

### Test it Out

To test out the validation, send a `POST` request to `/custom` passing `a` and `b` body parameters. You can try sending incorrect request body parameters to test out the validation.

For example, if you omit the `a` parameter, you'll receive a `400` response code with the following response data:

```json
{
  "type": "invalid_data",
  "message": "Invalid request: Field 'a' is required"
}
```

***

## How to Validate Request Query Parameters

The steps to validate the request query parameters are the similar to that of [validating the body](#how-to-validate-request-body).

### Step 1: Create Validation Schema

The first step is to create a schema with Zod with the rules of the accepted query parameters.

Consider that the API route accepts two query parameters `a` and `b` that are numbers, similar to the previous section.

Create the file `src/api/custom/validators.ts` with the following content:

```ts title="src/api/custom/validators.ts"
import { z } from "zod"

export const PostStoreCustomSchema = z.object({
  a: z.preprocess(
      (val) => {
        if (val && typeof val === "string") {
          return parseInt(val)
        }
        return val
      },
      z
        .number()
    ),
  b: z.preprocess(
    (val) => {
      if (val && typeof val === "string") {
        return parseInt(val)
      }
      return val
    },
    z
      .number()
  ),
})
```

Since a query parameter's type is originally a string or array of strings, you have to use Zod's `preprocess` method to validate other query types, such as numbers.

For both `a` and `b`, you transform the query parameter's value to an integer first if it's a string, then, you check that the resulting value is a number.

### Step 2: Add Request Query Validation Middleware

Next, you'll use the schema to validate incoming requests' query parameters to the `/custom` API route.

Add the `validateAndTransformQuery` middleware to the API route in the file `src/api/middlewares.ts`:

```ts title="src/api/middlewares.ts"
import { 
  validateAndTransformQuery,
  defineMiddlewares,
} from "@medusajs/framework/http"
import { PostStoreCustomSchema } from "./custom/validators"

export default defineMiddlewares({
  routes: [
    {
      matcher: "/custom",
      method: "POST",
      middlewares: [
        validateAndTransformQuery(
          PostStoreCustomSchema,
          {}
        ),
      ],
    },
  ],
})
```

The `validateAndTransformQuery` accepts two parameters:

- The first one is the Zod schema to validate the query parameters against.
- The second one is an object of options for retrieving data using Query, which you can learn more about in [this chapter](https://docs.medusajs.com/learn/fundamentals/module-links/query/index.html.md).

#### How the Validation Works

If a request's query parameters don't pass the validation, the `validateAndTransformQuery` middleware throws an error indicating the validation errors.

If a request's query parameters are validated successfully, the middleware sets the validated query parameters in the `validatedQuery` property of `MedusaRequest`.

### Step 3: Use Validated Query in API Route

Finally, use the validated query in the API route. The `MedusaRequest` parameter has a `validatedQuery` parameter that you can use to access the validated parameters.

For example, create the file `src/api/custom/route.ts` with the following content:

```ts title="src/api/custom/route.ts"
import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http"

export const GET = async (
  req: MedusaRequest,
  res: MedusaResponse
) => {
  const a = req.validatedQuery.a as number
  const b = req.validatedQuery.b as number

  res.json({
    sum: a + b,
  })
}
```

In the API route, you use the `validatedQuery` property of `MedusaRequest` to access the values of the `a` and `b` properties as numbers, then return in the response their sum.

### Test it Out

To test out the validation, send a `POST` request to `/custom` with `a` and `b` query parameters. You can try sending incorrect query parameters to see how the validation works.

For example, if you omit the `a` parameter, you'll receive a `400` response code with the following response data:

```json
{
  "type": "invalid_data",
  "message": "Invalid request: Field 'a' is required"
}
```

***

## Learn More About Validation Schemas

To see different examples and learn more about creating a validation schema, refer to [Zod's documentation](https://zod.dev).


# Emit Workflow and Service Events

In this chapter, you'll learn about event types and how to emit an event in a service or workflow.

## Event Types

In your customization, you can emit an event, then listen to it in a subscriber and perform an asynchronus action, such as send a notification or data to a third-party system.

There are two types of events in Medusa:

1. Workflow event: an event that's emitted in a workflow after a commerce feature is performed. For example, Medusa emits the `order.placed` event after a cart is completed.
2. Service event: an event that's emitted to track, trace, or debug processes under the hood. For example, you can emit an event with an audit trail.

### Which Event Type to Use?

**Workflow events** are the most common event type in development, as most custom features and customizations are built around workflows.

Some examples of workflow events:

1. When a user creates a blog post and you're emitting an event to send a newsletter email.
2. When you finish syncing products to a third-party system and you want to notify the admin user of new products added.
3. When a customer purchases a digital product and you want to generate and send it to them.

You should only go for a **service event** if you're emitting an event for processes under the hood that don't directly affect front-facing features.

Some examples of service events:

1. When you're tracing data manipulation and changes, and you want to track every time some custom data is changed.
2. When you're syncing data with a search engine.

***

## Emit Event in a Workflow

To emit a workflow event, use the `emitEventStep` helper step provided in the `@medusajs/medusa/core-flows` package.

For example:

```ts highlights={highlights}
import { 
  createWorkflow,
} from "@medusajs/framework/workflows-sdk"
import {
  emitEventStep,
} from "@medusajs/medusa/core-flows"

const helloWorldWorkflow = createWorkflow(
  "hello-world",
  () => {
    // ...

    emitEventStep({
      eventName: "custom.created",
      data: {
        id: "123",
        // other data payload
      },
    })
  }
)
```

The `emitEventStep` accepts an object having the following properties:

- `eventName`: The event's name.
- `data`: The data payload as an object. You can pass any properties in the object, and subscribers listening to the event will receive this data in the event's payload.

In this example, you emit the event `custom.created` and pass in the data payload an ID property.

### Test it Out

If you execute the workflow, the event is emitted and you can see it in your application's logs.

Any subscribers listening to the event are executed.

***

## Emit Event in a Service

To emit a service event:

1. Resolve `event_bus` from the module's container in your service's constructor:

### Extending Service Factory

```ts title="src/modules/blog/service.ts" highlights={["9"]}
import { IEventBusService } from "@medusajs/framework/types"
import { MedusaService } from "@medusajs/framework/utils"

class BlogModuleService extends MedusaService({
  Post,
}){
  protected eventBusService_: AbstractEventBusModuleService

  constructor({ event_bus }) {
    super(...arguments)
    this.eventBusService_ = event_bus
  }
}
```

### Without Service Factory

```ts title="src/modules/blog/service.ts" highlights={["6"]}
import { IEventBusService } from "@medusajs/framework/types"

class BlogModuleService {
  protected eventBusService_: AbstractEventBusModuleService

  constructor({ event_bus }) {
    this.eventBusService_ = event_bus
  }
}
```

2. Use the event bus service's `emit` method in the service's methods to emit an event:

```ts title="src/modules/blog/service.ts" highlights={serviceHighlights}
class BlogModuleService {
  // ...
  performAction() {
    // TODO perform action

    this.eventBusService_.emit({
      name: "custom.event",
      data: {
        id: "123",
        // other data payload
      },
    })
  }
}
```

The method accepts an object having the following properties:

- `name`: The event's name.
- `data`: The data payload as an object. You can pass any properties in the object, and subscribers listening to the event will receive this data in the event's payload.

3. By default, the Event Module's service isn't injected into your module's container. To add it to the container, pass it in the module's registration object in `medusa-config.ts` in the `dependencies` property:

### Module Registration

```ts title="medusa-config.ts" highlights={depsHighlight}
import { Modules } from "@medusajs/framework/utils"

module.exports = defineConfig({
  // ...
  modules: [
    {
      resolve: "./src/modules/blog",
      dependencies: [
        Modules.EVENT_BUS,
      ],
    },
  ],
})
```

### Module Provider Registration

```ts title="medusa-config.ts" highlights={depsHighlight}
import { Modules } from "@medusajs/framework/utils"

module.exports = defineConfig({
  // ...
  modules: [
    {
      resolve: "@medusajs/medusa/payment",
      dependencies: [
        Modules.EVENT_BUS,
      ],
      options: {
        providers: [
          {
            resolve: "./src/modules/my-provider",
            id: "my-provider",
            options: {
              // ...
            },
          },
        ],
      },
    },
  ],
})
```

The `dependencies` property accepts an array of module registration keys. The specified modules' main services are injected into the module's container.

If a module has providers, the dependencies are also injected into the providers' containers.

### Test it Out

If you execute the `performAction` method of your service, the event is emitted and you can see it in your application's logs.

Any subscribers listening to the event are also executed.


# Event Data Payload

In this chapter, you'll learn how subscribers receive an event's data payload.

## Access Event's Data Payload

When events are emitted, they’re emitted with a data payload.

The object that the subscriber function receives as a parameter has an `event` property, which is an object holding the event payload in a `data` property with additional context.

For example:

```ts title="src/subscribers/product-created.ts" highlights={highlights} collapsibleLines="1-5" expandButtonLabel="Show Imports"
import type {
  SubscriberArgs,
  SubscriberConfig,
} from "@medusajs/framework"

export default async function productCreateHandler({
  event,
}: SubscriberArgs<{ id: string }>) {
  const productId = event.data.id
  console.log(`The product ${productId} was created`)
}

export const config: SubscriberConfig = {
  event: "product.created",
}
```

The `event` object has the following properties:

- data: (\`object\`) The data payload of the event. Its properties are different for each event.
- name: (string) The name of the triggered event.
- metadata: (\`object\`) Additional data and context of the emitted event.

This logs the product ID received in the `product.created` event’s data payload to the console.

{/* ---

## List of Events with Data Payload

Refer to [this reference](!resources!/references/events) for a full list of events emitted by Medusa and their data payloads. */}


# Create a Plugin

In this chapter, you'll learn how to create a Medusa plugin and publish it.

A [plugin](https://docs.medusajs.com/learn/fundamentals/plugins/index.html.md) is a package of reusable Medusa customizations that you can install in any Medusa application. By creating and publishing a plugin, you can reuse your Medusa customizations across multiple projects or share them with the community.

Plugins are available starting from [Medusa v2.3.0](https://github.com/medusajs/medusa/releases/tag/v2.3.0).

## 1. Create a Plugin Project

Plugins are created in a separate Medusa project. This makes the development and publishing of the plugin easier. Later, you'll install that plugin in your Medusa application to test it out and use it.

Medusa's `create-medusa-app` CLI tool provides the option to create a plugin project. Run the following command to create a new plugin project:

```bash
npx create-medusa-app my-plugin --plugin
```

This will create a new Medusa plugin project in the `my-plugin` directory.

### Plugin Directory Structure

After the installation is done, the plugin structure will look like this:

![Directory structure of a plugin project](https://res.cloudinary.com/dza7lstvk/image/upload/v1737019441/Medusa%20Book/project-dir_q4xtri.jpg)

- `src/`: Contains the Medusa customizations.
- `src/admin`: Contains [admin extensions](https://docs.medusajs.com/learn/fundamentals/admin/index.html.md).
- `src/api`: Contains [API routes](https://docs.medusajs.com/learn/fundamentals/api-routes/index.html.md) and [middlewares](https://docs.medusajs.com/learn/fundamentals/api-routes/middlewares/index.html.md). You can add store, admin, or any custom API routes.
- `src/jobs`: Contains [scheduled jobs](https://docs.medusajs.com/learn/fundamentals/scheduled-jobs/index.html.md).
- `src/links`: Contains [module links](https://docs.medusajs.com/learn/fundamentals/module-links/index.html.md).
- `src/modules`: Contains [modules](https://docs.medusajs.com/learn/fundamentals/modules/index.html.md).
- `src/provider`: Contains [module providers](#create-module-providers).
- `src/subscribers`: Contains [subscribers](https://docs.medusajs.com/learn/fundamentals/events-and-subscribers/index.html.md).
- `src/workflows`: Contains [workflows](https://docs.medusajs.com/learn/fundamentals/workflows/index.html.md). You can also add [hooks](https://docs.medusajs.com/learn/fundamentals/workflows/add-workflow-hook/index.html.md) under `src/workflows/hooks`.
- `package.json`: Contains the plugin's package information, including general information and dependencies.
- `tsconfig.json`: Contains the TypeScript configuration for the plugin.

***

## 2. Prepare Plugin

### Package Name

Before developing, testing, and publishing your plugin, make sure its name in `package.json` is correct. This is the name you'll use to install the plugin in your Medusa application.

For example:

```json title="package.json"
{
  "name": "@myorg/plugin-name",
  // ...
}
```

### Package Keywords

Medusa scrapes NPM for a list of plugins that integrate third-party services, to later showcase them on the Medusa website. If you want your plugin to appear in that listing, make sure to add the `medusa-v2` and `medusa-plugin-integration` keywords to the `keywords` field in `package.json`.

Only plugins that integrate third-party services are listed in the Medusa integrations page. If your plugin doesn't integrate a third-party service, you can skip this step.

```json title="package.json"
{
  "keywords": [
    "medusa-plugin-integration",
    "medusa-v2"
  ],
  // ...
}
```

In addition, make sure to use one of the following keywords based on your integration type:

|Keyword|Description|Example|
|---|---|---|
|\`medusa-plugin-analytics\`|Analytics service integration|Google Analytics|
|\`medusa-plugin-auth\`|Authentication service integration|Auth0|
|\`medusa-plugin-cms\`|CMS service integration|Contentful|
|\`medusa-plugin-notification\`|Notification service integration|Twilio SMS|
|\`medusa-plugin-payment\`|Payment service integration|PayPal|
|\`medusa-plugin-search\`|Search service integration|MeiliSearch|
|\`medusa-plugin-shipping\`|Shipping service integration|DHL|
|\`medusa-plugin-other\`|Other third-party integrations|Sentry|

### Package Dependencies

Your plugin project will already have the dependencies mentioned in this section. Unless you made changes to the dependencies, you can skip this section.

In the `package.json` file you must have the Medusa dependencies as `devDependencies` and `peerDependencies`. In addition, you must have `@swc/core` as a `devDependency`, as it's used by the plugin CLI tools.

For example, assuming `2.5.0` is the latest Medusa version:

```json title="package.json"
{
  "devDependencies": {
    "@medusajs/admin-sdk": "2.5.0",
    "@medusajs/cli": "2.5.0",
    "@medusajs/framework": "2.5.0",
    "@medusajs/medusa": "2.5.0",
    "@medusajs/test-utils": "2.5.0",
    "@medusajs/ui": "4.0.4",
    "@medusajs/icons": "2.5.0",
    "@swc/core": "1.5.7",
  },
  "peerDependencies": {
    "@medusajs/admin-sdk": "2.5.0",
    "@medusajs/cli": "2.5.0",
    "@medusajs/framework": "2.5.0",
    "@medusajs/test-utils": "2.5.0",
    "@medusajs/medusa": "2.5.0",
    "@medusajs/ui": "4.0.3",
    "@medusajs/icons": "2.5.0",
  }
}
```

### Package Exports

Your plugin project will already have the exports mentioned in this section. Unless you made changes to the exports or you created your plugin before [Medusa v2.7.0](https://github.com/medusajs/medusa/releases/tag/v2.7.0), you can skip this section.

In the `package.json` file, make sure your plugin has the following exports:

```json title="package.json"
{
  "exports": {
    "./package.json": "./package.json",
    "./workflows": "./.medusa/server/src/workflows/index.js",
    "./.medusa/server/src/modules/*": "./.medusa/server/src/modules/*/index.js",
    "./providers/*": "./.medusa/server/src/providers/*/index.js",
    "./admin": {
      "import": "./.medusa/server/src/admin/index.mjs",
      "require": "./.medusa/server/src/admin/index.js",
      "default": "./.medusa/server/src/admin/index.js"
    },
    "./*": "./.medusa/server/src/*.js"
  }
}
```

Aside from the `./package.json`, `./providers`, and `./admin`, these exports are only a recommendation. You can cherry-pick the files and directories you want to export.

The plugin exports the following files and directories:

- `./package.json`: The `package.json` file. Medusa needs to access the `package.json` when registering the plugin.
- `./workflows`: The workflows exported in `./src/workflows/index.ts`.
- `./.medusa/server/src/modules/*`: The definition file of modules. This is useful if you create links to the plugin's modules in the Medusa application.
- `./providers/*`: The definition file of module providers. This is useful if your plugin includes a module provider, allowing you to register the plugin's providers in Medusa applications. Learn more in the [Create Module Providers](#create-module-providers) section.
- `./admin`: The admin extensions exported in `./src/admin/index.ts`.
- `./*`: Any other files in the plugin's `src` directory.

***

## 3. Publish Plugin Locally for Development and Testing

Medusa's CLI tool provides commands to simplify developing and testing your plugin in a local Medusa application. You start by publishing your plugin in the local package registry, then install it in your Medusa application. You can then watch for changes in the plugin as you develop it.

### Publish and Install Local Package

### Prerequisites

- [Medusa application installed.](https://docs.medusajs.com/learn/installation/index.html.md)

The first time you create your plugin, you need to publish the package into a local package registry, then install it in your Medusa application. This is a one-time only process.

To publish the plugin to the local registry, run the following command in your plugin project:

```bash title="Plugin project"
npx medusa plugin:publish
```

This command uses [Yalc](https://github.com/wclr/yalc) under the hood to publish the plugin to a local package registry. The plugin is published locally under the name you specified in `package.json`.

Next, navigate to your Medusa application:

```bash title="Medusa application"
cd ~/path/to/medusa-app
```

Make sure to replace `~/path/to/medusa-app` with the path to your Medusa application.

Then, if your project was created before v2.3.1 of Medusa, make sure to install `yalc` as a development dependency:

```bash npm2yarn badgeLabel="Medusa Application" badgeColor="green"
npm install --save-dev yalc
```

After that, run the following Medusa CLI command to install the plugin:

```bash title="Medusa application"
npx medusa plugin:add @myorg/plugin-name
```

Make sure to replace `@myorg/plugin-name` with the name of your plugin as specified in `package.json`. Your plugin will be installed from the local package registry into your Medusa application.

### Register Plugin in Medusa Application

After installing the plugin, you need to register it in your Medusa application in the configurations defined in `medusa-config.ts`.

Add the plugin to the `plugins` array in the `medusa-config.ts` file:

```ts title="medusa-config.ts" highlights={pluginHighlights}
module.exports = defineConfig({
  // ...
  plugins: [
    {
      resolve: "@myorg/plugin-name",
      options: {},
    },
  ],
})
```

The `plugins` configuration is an array of objects where each object has a `resolve` key whose value is the name of the plugin package.

#### Pass Module Options through Plugin

Each plugin configuration also accepts an `options` property, whose value is an object of options to pass to the plugin's modules.

For example:

```ts title="medusa-config.ts" highlights={pluginOptionsHighlight}
module.exports = defineConfig({
  // ...
  plugins: [
    {
      resolve: "@myorg/plugin-name",
      options: {
        apiKey: true,
      },
    },
  ],
})
```

The `options` property in the plugin configuration is passed to all modules in the plugin. Learn more about module options in [this chapter](https://docs.medusajs.com/learn/fundamentals/modules/options/index.html.md).

### Watch Plugin Changes During Development

While developing your plugin, you can watch for changes in the plugin and automatically update the plugin in the Medusa application using it. This is the only command you'll continuously need during your plugin development.

To do that, run the following command in your plugin project:

```bash title="Plugin project"
npx medusa plugin:develop
```

This command will:

- Watch for changes in the plugin. Whenever a file is changed, the plugin is automatically built.
- Publish the plugin changes to the local package registry. This will automatically update the plugin in the Medusa application using it. You can also benefit from real-time HMR updates of admin extensions.

### Start Medusa Application

You can start your Medusa application's development server to test out your plugin:

```bash npm2yarn badgeLabel="Medusa Application" badgeColor="green"
npm run dev
```

While your Medusa application is running and the plugin is being watched, you can test your plugin while developing it in the Medusa application.

***

## 4. Create Customizations in the Plugin

You can now build your plugin's customizations. The following guide explains how to build different customizations in your plugin.

- [Create a module](https://docs.medusajs.com/learn/fundamentals/modules/index.html.md)
- [Create a module link](https://docs.medusajs.com/learn/fundamentals/module-links/index.html.md)
- [Create a workflow](https://docs.medusajs.com/learn/fundamentals/workflows/index.html.md)
- [Add a workflow hook](https://docs.medusajs.com/learn/fundamentals/workflows/add-workflow-hook/index.html.md)
- [Create an API route](https://docs.medusajs.com/learn/fundamentals/api-routes/index.html.md)
- [Add a subscriber](https://docs.medusajs.com/learn/fundamentals/events-and-subscribers/index.html.md)
- [Add a scheduled job](https://docs.medusajs.com/learn/fundamentals/scheduled-jobs/index.html.md)
- [Add an admin widget](https://docs.medusajs.com/learn/fundamentals/admin/widgets/index.html.md)
- [Add an admin UI route](https://docs.medusajs.com/learn/fundamentals/admin/ui-routes/index.html.md)

While building those customizations, you can test them in your Medusa application by [watching the plugin changes](#watch-plugin-changes-during-development) and [starting the Medusa application](#start-medusa-application).

### Generating Migrations for Modules

During your development, you may need to generate migrations for modules in your plugin. To do that, first, add the following environment variables in your plugin project:

```plain title="Plugin project"
DB_USERNAME=postgres
DB_PASSWORD=123...
DB_HOST=localhost
DB_PORT=5432
DB_NAME=db_name
```

You can add these environment variables in a `.env` file in your plugin project. The variables are:

- `DB_USERNAME`: The username of the PostgreSQL user to connect to the database.
- `DB_PASSWORD`: The password of the PostgreSQL user to connect to the database.
- `DB_HOST`: The host of the PostgreSQL database. Typically, it's `localhost` if you're running the database locally.
- `DB_PORT`: The port of the PostgreSQL database. Typically, it's `5432` if you're running the database locally.
- `DB_NAME`: The name of the PostgreSQL database to connect to.

Then, run the following command in your plugin project to generate migrations for the modules in your plugin:

```bash title="Plugin project"
npx medusa plugin:db:generate
```

This command generates migrations for all modules in the plugin.

Finally, run these migrations on the Medusa application that the plugin is installed in using the `db:migrate` command:

```bash title="Medusa application"
npx medusa db:migrate
```

The migrations in your application, including your plugin, will run and update the database.

### Importing Module Resources

In the [Prepare Plugin](#2-prepare-plugin) section, you learned about [exported resources](#package-exports) in your plugin.

These exports allow you to import your plugin resources in your Medusa application, including workflows, links and modules.

For example, to import the plugin's workflow in your Medusa application:

`@myorg/plugin-name` is the plugin package's name.

```ts
import { Workflow1, Workflow2 } from "@myorg/plugin-name/workflows"
import BlogModule from "@myorg/plugin-name/modules/blog"
// import other files created in plugin like ./src/types/blog.ts
import BlogType from "@myorg/plugin-name/types/blog"
```

### Create Module Providers

The [exported resources](#package-exports) also allow you to import module providers in your plugin and register them in the Medusa application's configuration. A module provider is a module that provides the underlying logic or integration related to a commerce or Infrastructure Module.

For example, assuming your plugin has a [Notification Module Provider](https://docs.medusajs.com/resources/infrastructure-modules/notification/index.html.md) called `my-notification`, you can register it in your Medusa application's configuration like this:

`@myorg/plugin-name` is the plugin package's name.

```ts highlights={[["9"]]} title="medusa-config.ts"
module.exports = defineConfig({
  // ...
  modules: [
    {
      resolve: "@medusajs/medusa/notification",
      options: {
        providers: [
          {
            resolve: "@myorg/plugin-name/providers/my-notification",
            id: "my-notification",
            options: {
              channels: ["email"],
              // provider options...
            },
          },
        ],
      },
    },
  ],
})
```

You pass to `resolve` the path to the provider relative to the plugin package. So, in this example, the `my-notification` provider is located in `./src/providers/my-notification/index.ts` of the plugin.

To learn how to create module providers, refer to the following guides:

- [File Module Provider](https://docs.medusajs.com/resources/references/file-provider-module/index.html.md)
- [Notification Module Provider](https://docs.medusajs.com/resources/references/notification-provider-module/index.html.md)
- [Auth Module Provider](https://docs.medusajs.com/resources/references/auth/provider/index.html.md)
- [Payment Module Provider](https://docs.medusajs.com/resources/references/payment/provider/index.html.md)
- [Fulfillment Module Provider](https://docs.medusajs.com/resources/references/fulfillment/provider/index.html.md)
- [Tax Module Provider](https://docs.medusajs.com/resources/references/tax/provider/index.html.md)

***

## 5. Publish Plugin to NPM

Make sure to add the keywords mentioned in the [Package Keywords](#package-keywords) section in your plugin's `package.json` file.

Medusa's CLI tool provides a command that bundles your plugin to be published to npm. Once you're ready to publish your plugin publicly, run the following command in your plugin project:

```bash
npx medusa plugin:build
```

The command will compile an output in the `.medusa/server` directory.

You can now publish the plugin to npm using the [NPM CLI tool](https://docs.npmjs.com/downloading-and-installing-node-js-and-npm). Run the following command to publish the plugin to npm:

```bash
npm publish
```

If you haven't logged in before with your NPM account, you'll be asked to log in first. Then, your package is published publicly to be used in any Medusa application.

### Install Public Plugin in Medusa Application

You install a plugin that's published publicly using your package manager. For example:

```bash npm2yarn
npm install @myorg/plugin-name
```

Where `@myorg/plugin-name` is the name of your plugin as published on NPM.

Then, register the plugin in your Medusa application's configurations as explained in [this section](#register-plugin-in-medusa-application).

***

## Update a Published Plugin

To update the Medusa dependencies in a plugin, refer to [this documentation](https://docs.medusajs.com/learn/update#update-plugin-project/index.html.md).

If you've published a plugin and you've made changes to it, you'll have to publish the update to NPM again.

First, run the following command to change the version of the plugin:

```bash
npm version <type>
```

Where `<type>` indicates the type of version update you’re publishing. For example, it can be `major` or `minor`. Refer to the [npm version documentation](https://docs.npmjs.com/cli/v10/commands/npm-version) for more information.

Then, re-run the same commands for publishing a plugin:

```bash
npx medusa plugin:build
npm publish
```

This will publish an updated version of your plugin under a new version.


# Add Data Model Check Constraints

In this chapter, you'll learn how to add check constraints to your data model.

## What is a Check Constraint?

A check constraint is a condition that must be satisfied by records inserted into a database table, otherwise an error is thrown.

For example, if you have a data model with a `price` property, you want to only allow positive number values. So, you add a check constraint that fails when inserting a record with a negative price value.

***

## How to Set a Check Constraint?

To set check constraints on a data model, use the `checks` method. This method accepts an array of check constraints to apply on the data model.

For example, to set a check constraint on a `price` property that ensures its value can only be a positive number:

```ts highlights={checks1Highlights}
import { model } from "@medusajs/framework/utils"

const CustomProduct = model.define("custom_product", {
  // ...
  price: model.bigNumber(),
})
.checks([
  (columns) => `${columns.price} >= 0`,
])
```

The item passed in the array parameter of `checks` can be a callback function that accepts as a parameter an object whose keys are the names of the properties in the data model schema, and values the respective column name in the database.

The function returns a string indicating the [SQL check constraint expression](https://www.postgresql.org/docs/current/ddl-constraints.html#DDL-CONSTRAINTS-CHECK-CONSTRAINTS). In the expression, use the `columns` parameter to access a property's column name.

You can also pass an object to the `checks` method:

```ts highlights={checks2Highlights}
import { model } from "@medusajs/framework/utils"

const CustomProduct = model.define("custom_product", {
  // ...
  price: model.bigNumber(),
})
.checks([
  {
    name: "custom_product_price_check",
    expression: (columns) => `${columns.price} >= 0`,
  },
])
```

The object accepts the following properties:

- `name`: The check constraint's name.
- `expression`: A function similar to the one that can be passed to the array. It accepts an object of columns and returns an [SQL check constraint expression](https://www.postgresql.org/docs/current/ddl-constraints.html#DDL-CONSTRAINTS-CHECK-CONSTRAINTS).

***

## Apply in Migrations

After adding the check constraint, make sure to generate and run migrations if you already have the table in the database. Otherwise, the check constraint won't be reflected.

To generate a migration for the data model's module then reflect it on the database, run the following command:

```bash
npx medusa db:generate custom_module
npx medusa db:migrate
```

The first command generates the migration under the `migrations` directory of your module's directory, and the second reflects it on the database.


# Infer Type of Data Model

In this chapter, you'll learn how to infer the type of a data model.

## How to Infer Type of Data Model?

Consider you have a `Post` data model. You can't reference this data model in a type, such as a workflow input or service method output types, since it's a variable.

Instead, Medusa provides `InferTypeOf` that transforms your data model to a type.

For example:

```ts
import { InferTypeOf } from "@medusajs/framework/types"
import { Post } from "../modules/blog/models/post" // relative path to the model

export type Post = InferTypeOf<typeof Post>
```

`InferTypeOf` accepts as a type argument the type of the data model.

Since the `Post` data model is a variable, use the `typeof` operator to pass the data model as a type argument to `InferTypeOf`.

You can now use the `Post` type to reference a data model in other types, such as in workflow inputs or service method outputs:

```ts title="Example Service"
// other imports...
import { InferTypeOf } from "@medusajs/framework/types"
import { Post } from "../models/post"

type Post = InferTypeOf<typeof Post>

class BlogModuleService extends MedusaService({ Post }) {
  async doSomething(): Promise<Post> {
    // ...
  }
}
```


# Data Model Database Index

In this chapter, you’ll learn how to define a database index on a data model.

You can also define an index on a property as explained in the [Properties chapter](https://docs.medusajs.com/learn/fundamentals/data-models/properties#define-database-index-on-property/index.html.md).

## Define Database Index on Data Model

A data model has an `indexes` method that defines database indices on its properties.

The index can be on multiple columns (composite index). For example:

```ts highlights={dataModelIndexHighlights}
import { model } from "@medusajs/framework/utils"

const MyCustom = model.define("my_custom", {
  id: model.id().primaryKey(),
  name: model.text(),
  age: model.number(),
}).indexes([
  {
    on: ["name", "age"],
  },
])

export default MyCustom
```

The `indexes` method receives an array of indices as a parameter. Each index is an object with a required `on` property indicating the properties to apply the index on.

In the above example, you define a composite index on the `name` and `age` properties.

### Index Conditions

An index can have conditions. For example:

```ts highlights={conditionHighlights}
import { model } from "@medusajs/framework/utils"

const MyCustom = model.define("my_custom", {
  id: model.id().primaryKey(),
  name: model.text(),
  age: model.number(),
}).indexes([
  {
    on: ["name", "age"],
    where: {
      age: 30,
    },
  },
])

export default MyCustom
```

The index object passed to `indexes` accepts a `where` property whose value is an object of conditions. The object's key is a property's name, and its value is the condition on that property.

In the example above, the composite index is created on the `name` and `age` properties when the `age`'s value is `30`.

A property's condition can be a negation. For example:

```ts highlights={negationHighlights}
import { model } from "@medusajs/framework/utils"

const MyCustom = model.define("my_custom", {
  id: model.id().primaryKey(),
  name: model.text(),
  age: model.number().nullable(),
}).indexes([
  {
    on: ["name", "age"],
    where: {
      age: {
        $ne: null,
      },
    },
  },
])

export default MyCustom
```

A property's value in `where` can be an object having a `$ne` property. `$ne`'s value indicates what the specified property's value shouldn't be.

In the example above, the composite index is created on the `name` and `age` properties when `age`'s value is not `null`.

### Unique Database Index

The object passed to `indexes` accepts a `unique` property indicating that the created index must be a unique index.

For example:

```ts highlights={uniqueHighlights}
import { model } from "@medusajs/framework/utils"

const MyCustom = model.define("my_custom", {
  id: model.id().primaryKey(),
  name: model.text(),
  age: model.number(),
}).indexes([
  {
    on: ["name", "age"],
    unique: true,
  },
])

export default MyCustom
```

This creates a unique composite index on the `name` and `age` properties.


# Manage Relationships

In this chapter, you'll learn how to manage relationships between data models when creating, updating, or retrieving records using the module's main service.

This chapter applies to data model relationships within the same module. To manage linked data models across modules, check out [Links](https://docs.medusajs.com/learn/fundamentals/module-links/index.html.md) and [Query](https://docs.medusajs.com/learn/fundamentals/module-links/query/index.html.md).

## Manage One-to-One Relationship

### BelongsTo Side of One-to-One

When you create a record of a data model that belongs to another through a one-to-one relation, pass the ID of the other data model's record in the `{relation}_id` property, where `{relation}` is the name of the relation property.

For example, assuming you have the [User and Email data models from the previous chapter](https://docs.medusajs.com/learn/fundamentals/data-models/relationships#one-to-one-relationship/index.html.md), set an email's user ID as follows:

```ts highlights={belongsHighlights}
// when creating an email
const email = await helloModuleService.createEmails({
  // other properties...
  user_id: "123",
})

// when updating an email
const email = await helloModuleService.updateEmails({
  id: "321",
  // other properties...
  user_id: "123",
})
```

In the example above, you pass the `user_id` property when creating or updating an email to specify the user it belongs to.

### HasOne Side

When you create a record of a data model that has one of another, pass the ID of the other data model's record in the relation property.

For example, assuming you have the [User and Email data models from the previous chapter](https://docs.medusajs.com/learn/fundamentals/data-models/relationships#one-to-one-relationship/index.html.md), set a user's email ID as follows:

```ts highlights={hasOneHighlights}
// when creating a user
const user = await helloModuleService.createUsers({
  // other properties...
  email: "123",
})

// when updating a user
const user = await helloModuleService.updateUsers({
  id: "321",
  // other properties...
  email: "123",
})
```

In the example above, you pass the `email` property when creating or updating a user to specify the email it has.

***

## Manage One-to-Many Relationship

In a one-to-many relationship, you can only manage the associations from the `belongsTo` side.

When you create a record of the data model on the `belongsTo` side, pass the ID of the other data model's record in the `{relation}_id` property, where `{relation}` is the name of the relation property.

For example, assuming you have the [Product and Store data models from the previous chapter](https://docs.medusajs.com/learn/fundamentals/data-models/relationships#one-to-many-relationship/index.html.md), set a product's store ID as follows:

```ts highlights={manyBelongsHighlights}
// when creating a product
const product = await helloModuleService.createProducts({
  // other properties...
  store_id: "123",
})

// when updating a product
const product = await helloModuleService.updateProducts({
  id: "321",
  // other properties...
  store_id: "123",
})
```

In the example above, you pass the `store_id` property when creating or updating a product to specify the store it belongs to.

***

## Manage Many-to-Many Relationship

If your many-to-many relation is represented with a [pivotEntity](https://docs.medusajs.com/learn/fundamentals/data-models/relationships#many-to-many-with-custom-columns/index.html.md), refer to [this section](#manage-many-to-many-relationship-with-pivotentity) instead.

### Create Associations

When you create a record of a data model that has a many-to-many relationship to another data model, pass an array of IDs of the other data model's records in the relation property.

For example, assuming you have the [Order and Product data models from the previous chapter](https://docs.medusajs.com/learn/fundamentals/data-models/relationships#many-to-many-relationship/index.html.md), set the association between products and orders as follows:

```ts highlights={manyHighlights}
// when creating a product
const product = await helloModuleService.createProducts({
  // other properties...
  orders: ["123", "321"],
})

// when creating an order
const order = await helloModuleService.createOrders({
  id: "321",
  // other properties...
  products: ["123", "321"],
})
```

In the example above, you pass the `orders` property when you create a product, and you pass the `products` property when you create an order.

### Update Associations

When you use the `update` methods generated by the service factory, you also pass an array of IDs as the relation property's value to add new associated records.

However, this removes any existing associations to records whose IDs aren't included in the array.

For example, assuming you have the [Order and Product data models from the previous chapter](https://docs.medusajs.com/learn/fundamentals/data-models/relationships#many-to-many-relationship/index.html.md), you update the product's related orders as so:

```ts
const product = await helloModuleService.updateProducts({
  id: "123",
  // other properties...
  orders: ["321"],
})
```

If the product was associated with an order, and you don't include that order's ID in the `orders` array, the association between the product and order is removed.

So, to add a new association without removing existing ones, retrieve the product first to pass its associated orders when updating the product:

```ts highlights={updateAssociationHighlights}
const product = await helloModuleService.retrieveProduct(
  "123",
  {
    relations: ["orders"],
  }
)

const updatedProduct = await helloModuleService.updateProducts({
  id: product.id,
  // other properties...
  orders: [
    ...product.orders.map((order) => order.id),
    "321",
  ],
})
```

This keeps existing associations between the product and orders, and adds a new one.

***

## Manage Many-to-Many Relationship with pivotEntity

If your many-to-many relation is represented without a [pivotEntity](https://docs.medusajs.com/learn/fundamentals/data-models/relationships#many-to-many-with-custom-columns/index.html.md), refer to [this section](#manage-many-to-many-relationship) instead.

If you have a many-to-many relation with a `pivotEntity` specified, make sure to pass the data model representing the pivot table to [MedusaService](https://docs.medusajs.com/learn/fundamentals/modules/service-factory/index.html.md) that your module's service extends.

For example, assuming you have the [Order, Product, and OrderProduct models from the previous chapter](https://docs.medusajs.com/learn/fundamentals/data-models/relationships#many-to-many-with-custom-columns/index.html.md), add `OrderProduct` to `MedusaService`'s object parameter:

```ts highlights={["4"]}
class BlogModuleService extends MedusaService({
  Order,
  Product,
  OrderProduct,
}) {}
```

This will generate Create, Read, Update and Delete (CRUD) methods for the `OrderProduct` data model, which you can use to create relations between orders and products and manage the extra columns in the pivot table.

For example:

```ts
// create order-product association
const orderProduct = await blogModuleService.createOrderProducts({
  order_id: "123",
  product_id: "123",
  metadata: {
    test: true,
  },
})

// update order-product association
const orderProduct = await blogModuleService.updateOrderProducts({
  id: "123",
  metadata: {
    test: false,
  },
})

// delete order-product association
await blogModuleService.deleteOrderProducts("123")
```

Since the `OrderProduct` data model belongs to the `Order` and `Product` data models, you can set its order and product as explained in the [one-to-many relationship section](#manage-one-to-many-relationship) using `order_id` and `product_id`.

Refer to the [service factory reference](https://docs.medusajs.com/resources/service-factory-reference/index.html.md) for a full list of generated methods and their usages.

***

## Retrieve Records of Relation

The `list`, `listAndCount`, and `retrieve` methods of a module's main service accept as a second parameter an object of options.

To retrieve the records associated with a data model's records through a relationship, pass in the second parameter object a `relations` property whose value is an array of relationship names.

For example, assuming you have the [Order and Product data models from the previous chapter](https://docs.medusajs.com/learn/fundamentals/data-models/relationships#many-to-many-relationship/index.html.md), you retrieve a product's orders as follows:

```ts highlights={retrieveHighlights}
const product = await blogModuleService.retrieveProducts(
  "123",
  {
    relations: ["orders"],
  }
)
```

In the example above, the retrieved product has an `orders` property, whose value is an array of orders associated with the product.


# Data Model Properties

In this chapter, you'll learn about the different property types you can use in a data model and how to configure a data model's properties.

## Data Model's Default Properties

By default, Medusa creates the following properties for every data model:

- `created_at`: A [dateTime](#dateTime) property that stores when a record of the data model was created.
- `updated_at`: A [dateTime](#dateTime) property that stores when a record of the data model was updated.
- `deleted_at`: A [dateTime](#dateTime) property that stores when a record of the data model was deleted. When you soft-delete a record, Medusa sets the `deleted_at` property to the current date.

***

## Property Types

This section covers the different property types you can define in a data model's schema using the `model` methods.

### id

The `id` method defines an automatically generated string ID property. The generated ID is a unique string that has a mix of letters and numbers.

For example:

```ts highlights={idHighlights}
import { model } from "@medusajs/framework/utils"

const Post = model.define("post", {
  id: model.id(),
  // ...
})

export default Post
```

### text

The `text` method defines a string property.

For example:

```ts highlights={textHighlights}
import { model } from "@medusajs/framework/utils"

const Post = model.define("post", {
  name: model.text(),
  // ...
})

export default Post
```

### number

The `number` method defines a number property.

For example:

```ts highlights={numberHighlights}
import { model } from "@medusajs/framework/utils"

const Post = model.define("post", {
  age: model.number(),
  // ...
})

export default Post
```

### float

This property is only available after [Medusa v2.1.2](https://github.com/medusajs/medusa/releases/tag/v2.1.2).

The `float` method defines a number property that allows for values with decimal places.

Use this property type when it's less important to have high precision for numbers with large decimal places. Alternatively, for higher percision, use the [bigNumber property](#bignumber).

For example:

```ts highlights={floatHighlights}
import { model } from "@medusajs/framework/utils"

const Post = model.define("post", {
  rating: model.float(),
  // ...
})

export default Post
```

### bigNumber

The `bigNumber` method defines a number property that expects large numbers, such as prices.

Use this property type when it's important to have high precision for numbers with large decimal places. Alternatively, for less percision, use the [float property](#float).

For example:

```ts highlights={bigNumberHighlights}
import { model } from "@medusajs/framework/utils"

const Post = model.define("post", {
  price: model.bigNumber(),
  // ...
})

export default Post
```

### boolean

The `boolean` method defines a boolean property.

For example:

```ts highlights={booleanHighlights}
import { model } from "@medusajs/framework/utils"

const Post = model.define("post", {
  hasAccount: model.boolean(),
  // ...
})

export default Post
```

### enum

The `enum` method defines a property whose value can only be one of the specified values.

For example:

```ts highlights={enumHighlights}
import { model } from "@medusajs/framework/utils"

const Post = model.define("post", {
  color: model.enum(["black", "white"]),
  // ...
})

export default Post
```

The `enum` method accepts an array of possible string values.

### dateTime

The `dateTime` method defines a timestamp property.

For example:

```ts highlights={dateTimeHighlights}
import { model } from "@medusajs/framework/utils"

const Post = model.define("post", {
  date_of_birth: model.dateTime(),
  // ...
})

export default Post
```

### json

The `json` method defines a property whose value is a stringified JSON object.

For example:

```ts highlights={jsonHighlights}
import { model } from "@medusajs/framework/utils"

const Post = model.define("post", {
  metadata: model.json(),
  // ...
})

export default Post
```

### array

The `array` method defines an array of strings property.

For example:

```ts highlights={arrHightlights}
import { model } from "@medusajs/framework/utils"

const Post = model.define("post", {
  names: model.array(),
  // ...
})

export default Post
```

### Properties Reference

Refer to the [Data Model Language (DML) reference](https://docs.medusajs.com/resources/references/data-model/index.html.md) for a full reference of the properties.

***

## Set Primary Key Property

To set any `id`, `text`, or `number` property as a primary key, use the `primaryKey` method.

For example:

```ts highlights={highlights}
import { model } from "@medusajs/framework/utils"

const Post = model.define("post", {
  id: model.id().primaryKey(),
  // ...
})

export default Post
```

In the example above, the `id` property is defined as the data model's primary key.

***

## Property Default Value

Use the `default` method on a property's definition to specify the default value of a property.

For example:

```ts highlights={defaultHighlights}
import { model } from "@medusajs/framework/utils"

const Post = model.define("post", {
  color: model
    .enum(["black", "white"])
    .default("black"),
  age: model
    .number()
    .default(0),
  // ...
})

export default Post
```

In this example, you set the default value of the `color` enum property to `black`, and that of the `age` number property to `0`.

***

## Make Property Optional

Use the `nullable` method to indicate that a property’s value can be `null`. This is useful when you want a property to be optional.

For example:

```ts highlights={nullableHighlights}
import { model } from "@medusajs/framework/utils"

const Post = model.define("post", {
  price: model.bigNumber().nullable(),
  // ...
})

export default Post
```

In the example above, the `price` property is configured to allow `null` values, making it optional.

***

## Unique Property

The `unique` method indicates that a property’s value must be unique in the database through a unique index.

For example:

```ts highlights={uniqueHighlights}
import { model } from "@medusajs/framework/utils"

const User = model.define("user", {
  email: model.text().unique(),
  // ...
})

export default User
```

In this example, multiple users can’t have the same email.

***

## Define Database Index on Property

Use the `index` method on a property's definition to define a database index.

For example:

```ts highlights={dbIndexHighlights}
import { model } from "@medusajs/framework/utils"

const Post = model.define("post", {
  id: model.id().primaryKey(),
  name: model.text().index(
    "IDX_MY_CUSTOM_NAME"
  ),
})

export default Post
```

The `index` method optionally accepts the name of the index as a parameter.

In this example, you define an index on the `name` property.

***

## Define a Searchable Property

Methods generated by the [service factory](https://docs.medusajs.com/learn/fundamentals/modules/service-factory/index.html.md) that accept filters, such as `list{ModelName}s`, accept a `q` property as part of the filters.

When the `q` filter is passed, the data model's searchable properties are queried to find matching records.

Use the `searchable` method on a `text` property to indicate that it's searchable.

For example:

```ts highlights={searchableHighlights}
import { model } from "@medusajs/framework/utils"

const Post = model.define("post", {
  title: model.text().searchable(),
  // ...
})

export default Post
```

In this example, the `title` property is searchable.

### Search Example

If you pass a `q` filter to the `listPosts` method:

```ts
const posts = await blogModuleService.listPosts({
  q: "New Products",
})
```

This retrieves records that include `New Products` in their `title` property.


# Migrations

In this chapter, you'll learn what a migration is and how to generate a migration or write it manually.

## What is a Migration?

A migration is a TypeScript or JavaScript file that defines database changes made by a module. Migrations are useful when you re-use a module or you're working in a team, so that when one member of a team makes a database change, everyone else can reflect it on their side by running the migrations.

The migration's file has a class with two methods:

- The `up` method reflects changes on the database.
- The `down` method reverts the changes made in the `up` method.

***

## Generate Migration

Instead of you writing the migration manually, the Medusa CLI tool provides a [db:generate](https://docs.medusajs.com/resources/medusa-cli/commands/db#dbgenerate/index.html.md) command to generate a migration for a modules' data models.

For example, assuming you have a `blog` Module, you can generate a migration for it by running the following command:

```bash
npx medusa db:generate blog
```

This generates a migration file under the `migrations` directory of the Blog Module. You can then run it to reflect the changes in the database as mentioned in [this section](#run-the-migration).

***

## Write a Migration Manually

You can also write migrations manually. To do that, create a file in the `migrations` directory of the module and in it, a class that has an `up` and `down` method. The class's name should be of the format `Migration{YEAR}{MONTH}{DAY}{HOUR}{MINUTE}.ts` to ensure migrations are ran in the correct order.

For example:

```ts title="src/modules/blog/migrations/Migration202507021059.ts"
import { Migration } from "@mikro-orm/migrations"

export class Migration202507021059 extends Migration {

  async up(): Promise<void> {
    this.addSql("create table if not exists \"author\" (\"id\" text not null, \"name\" text not null, \"created_at\" timestamptz not null default now(), \"updated_at\" timestamptz not null default now(), \"deleted_at\" timestamptz null, constraint \"author_pkey\" primary key (\"id\"));")
  }

  async down(): Promise<void> {
    this.addSql("drop table if exists \"author\" cascade;")
  }

}
```

The migration class in the file extends the `Migration` class imported from `@mikro-orm/migrations`. In the `up` and `down` method of the migration class, you use the `addSql` method provided by MikroORM's `Migration` class to run PostgreSQL syntax.

In the example above, the `up` method creates the table `author`, and the `down` method drops the table if the migration is reverted.

Refer to [MikroORM's documentation](https://mikro-orm.io/docs/migrations#migration-class) for more details on writing migrations.

***

## Run the Migration

To run your migration, run the following command:

This command also syncs module links. If you don't want that, use the `--skip-links` option.

```bash
npx medusa db:migrate
```

This reflects the changes in the database as implemented in the migration's `up` method.

***

## Rollback the Migration

To rollback or revert the last migration you ran for a module, run the following command:

```bash
npx medusa db:rollback blog
```

This rolls back the last ran migration on the Blog Module.

### Caution: Rollback Migration before Deleting

If you need to delete a migration file, make sure to rollback the migration first. Otherwise, you might encounter issues when generating and running new migrations.

For example, if you delete the migration of the Blog Module, then try to create a new one, Medusa will create a brand new migration that re-creates the tables or indices. If those are still in the database, you might encounter errors.

So, always rollback the migration before deleting it.

***

## More Database Commands

To learn more about the Medusa CLI's database commands, refer to [this CLI reference](https://docs.medusajs.com/resources/medusa-cli/commands/db/index.html.md).


# Module Link Direction

In this chapter, you'll learn about the difference in module link directions, and which to use based on your use case.

The details in this chapter don't apply to [Read-Only Module Links](https://docs.medusajs.com/learn/fundamentals/module-links/read-only/index.html.md). Refer to the [Read-Only Module Links chapter](https://docs.medusajs.com/learn/fundamentals/module-links/read-only/index.html.md) for more information on read-only links and their direction.

## Link Direction

The module link's direction depends on the order you pass the data model configuration parameters to `defineLink`.

For example, the following defines a link from the Blog Module's `post` data model to the Product Module's `product` data model:

```ts
export default defineLink(
  BlogModule.linkable.post,
  ProductModule.linkable.product
)
```

Whereas the following defines a link from the Product Module's `product` data model to the Blog Module's `post` data model:

```ts
export default defineLink(
  ProductModule.linkable.product,
  BlogModule.linkable.post
)
```

The above links are two different links that serve different purposes.

***

## Which Link Direction to Use?

### Extend Data Models

If you're adding a link to a data model to extend it and add new fields, define the link from the main data model to the custom data model.

For example, consider you want to add a `subtitle` custom field to the `product` data model. To do that, you define a `Subtitle` data model in your module, then define a link from the `Product` data model to it:

```ts
export default defineLink(
  ProductModule.linkable.product,
  BlogModule.linkable.subtitle
)
```

### Associate Data Models

If you're linking data models to indicate an association between them, define the link from the custom data model to the main data model.

For example, consider you have `Post` data model representing a blog post, and you want to associate a blog post with a product. To do that, define a link from the `Post` data model to `Product`:

```ts
export default defineLink(
  BlogModule.linkable.post,
  ProductModule.linkable.product
)
```


# Add Columns to a Link Table

In this chapter, you'll learn how to add custom columns to a link definition's table and manage them.

## Link Table's Default Columns

When you define a link between two data models, Medusa creates a link table in the database to store the IDs of the linked records. You can learn more about the created table in the [Module Links chapter](https://docs.medusajs.com/learn/fundamentals/module-links/index.html.md).

In various cases, you might need to store additional data in the link table. For example, if you define a link between a `product` and a `post`, you might want to store the publish date of the product's post in the link table.

In those cases, you can add a custom column to a link's table in the link definition. You can later set that column whenever you create or update a link between the linked records.

***

## How to Add Custom Columns to a Link's Table?

The `defineLink` function used to define a link accepts a third parameter, which is an object of options.

To add custom columns to a link's table, pass in the third parameter of `defineLink` a `database` property:

```ts highlights={linkHighlights}
import BlogModule from "../modules/blog"
import ProductModule from "@medusajs/medusa/product"
import { defineLink } from "@medusajs/framework/utils"

export default defineLink(
  ProductModule.linkable.product,
  BlogModule.linkable.blog,
  {
    database: {
      extraColumns: {
        metadata: {
          type: "json",
        },
      },
    },
  }
)
```

This adds to the table created for the link between `product` and `blog` a `metadata` column of type `json`.

### Database Options

The `database` property defines configuration for the table created in the database.

Its `extraColumns` property defines custom columns to create in the link's table.

`extraColumns`'s value is an object whose keys are the names of the columns, and values are the column's configurations as an object.

### Column Configurations

The column's configurations object accepts the following properties:

- `type`: The column's type. Possible values are:
  - `string`
  - `text`
  - `integer`
  - `boolean`
  - `date`
  - `time`
  - `datetime`
  - `enum`
  - `json`
  - `array`
  - `enumArray`
  - `float`
  - `double`
  - `decimal`
  - `bigint`
  - `mediumint`
  - `smallint`
  - `tinyint`
  - `blob`
  - `uuid`
  - `uint8array`
- `defaultValue`: The column's default value.
- `nullable`: Whether the column can have `null` values.

***

## Set Custom Column when Creating Link

The object you pass to Link's `create` method accepts a `data` property. Its value is an object whose keys are custom column names, and values are the value of the custom column for this link.

For example:

Learn more about Link, how to resolve it, and its methods in [this chapter](https://docs.medusajs.com/learn/fundamentals/module-links/link/index.html.md).

```ts
await link.create({
  [Modules.PRODUCT]: {
    product_id: "123",
  },
  [BLOG_MODULE]: {
    post_id: "321",
  },
  data: {
    metadata: {
      test: true,
    },
  },
})
```

***

## Retrieve Custom Column with Link

To retrieve linked records with their custom columns, use [Query](https://docs.medusajs.com/learn/fundamentals/module-links/query/index.html.md). A module link's definition, exported by a file under `src/links`, has a special `entryPoint` property. Use this property when specifying the `entity` property in Query's `graph` method.

For example:

```ts highlights={retrieveHighlights}
import productPostLink from "../links/product-post"

// ...

const { data } = await query.graph({
  entity: productPostLink.entryPoint,
  fields: ["metadata", "product.*", "post.*"],
  filters: {
    product_id: "prod_123",
  },
})
```

This retrieves the product of id `prod_123` and its linked `post` records.

In the `fields` array you pass `metadata`, which is the custom column to retrieve of the link.

***

## Update Custom Column's Value

Link's `create` method updates a link's data if the link between the specified records already exists.

So, to update the value of a custom column in a created link, use the `create` method again passing it a new value for the custom column.

For example:

```ts
await link.create({
  [Modules.PRODUCT]: {
    product_id: "123",
  },
  [BLOG_MODULE]: {
    post_id: "321",
  },
  data: {
    metadata: {
      test: false,
    },
  },
})
```


# Data Model Relationships

In this chapter, you’ll learn how to define relationships between data models in your module.

## What is a Relationship Property?

A relationship property defines an association in the database between two models. It's created using the Data Model Language (DML) methods, such as `hasOne` or `belongsTo`.

When you generate a migration for these data models, the migrations include foreign key columns or pivot tables, based on the relationship's type.

You want to create a relation between data models in the same module.

You want to create a relationship between data models in different modules. Use module links instead.

***

## One-to-One Relationship

A one-to-one relationship indicates that one record of a data model belongs to or is associated with another.

To define a one-to-one relationship, create relationship properties in the data models using the following methods:

1. `hasOne`: indicates that the model has one record of the specified model.
2. `belongsTo`: indicates that the model belongs to one record of the specified model.

For example:

```ts highlights={oneToOneHighlights}
import { model } from "@medusajs/framework/utils"

const User = model.define("user", {
  id: model.id().primaryKey(),
  email: model.hasOne(() => Email),
})

const Email = model.define("email", {
  id: model.id().primaryKey(),
  user: model.belongsTo(() => User, {
    mappedBy: "email",
  }),
})
```

In the example above, a user has one email, and an email belongs to one user.

The `hasOne` and `belongsTo` methods accept a function as the first parameter. The function returns the associated data model.

The `belongsTo` method also requires passing as a second parameter an object with the property `mappedBy`. Its value is the name of the relationship property in the other data model.

### Optional Relationship

To make the relationship optional on the `hasOne` or `belongsTo` side, use the `nullable` method on either property as explained in [this chapter](https://docs.medusajs.com/learn/fundamentals/data-models/properties#make-property-optional/index.html.md).

### One-sided One-to-One Relationship

If the one-to-one relationship is only defined on one side, pass `undefined` to the `mappedBy` property in the `belongsTo` method.

For example:

```ts highlights={oneToOneUndefinedHighlights}
import { model } from "@medusajs/framework/utils"

const User = model.define("user", {
  id: model.id().primaryKey(),
})

const Email = model.define("email", {
  id: model.id().primaryKey(),
  user: model.belongsTo(() => User, {
    mappedBy: undefined,
  }),
})
```

### One-to-One Relationship in the Database

When you generate the migrations of data models that have a one-to-one relationship, the migration adds to the table of the data model that has the `belongsTo` property:

1. A column of the format `{relation_name}_id` to store the ID of the record of the related data model. For example, the `email` table will have a `user_id` column.
2. A foreign key on the `{relation_name}_id` column to the table of the related data model.

![Diagram illustrating the relation between user and email records in the database](https://res.cloudinary.com/dza7lstvk/image/upload/v1726733492/Medusa%20Book/one-to-one_cj5np3.jpg)

***

## One-to-Many Relationship

A one-to-many relationship indicates that one record of a data model has many records of another data model.

To define a one-to-many relationship, create relationship properties in the data models using the following methods:

1. `hasMany`: indicates that the model has more than one record of the specified model.
2. `belongsTo`: indicates that the model belongs to one record of the specified model.

For example:

```ts highlights={oneToManyHighlights}
import { model } from "@medusajs/framework/utils"

const Store = model.define("store", {
  id: model.id().primaryKey(),
  products: model.hasMany(() => Product),
})

const Product = model.define("product", {
  id: model.id().primaryKey(),
  store: model.belongsTo(() => Store, {
    mappedBy: "products",
  }),
})
```

In this example, a store has many products, but a product belongs to one store.

### Optional Relationship

To make the relationship optional on the `belongsTo` side, use the `nullable` method on the property as explained in [this chapter](https://docs.medusajs.com/learn/fundamentals/data-models/properties#make-property-optional/index.html.md).

### One-to-Many Relationship in the Database

When you generate the migrations of data models that have a one-to-many relationship, the migration adds to the table of the data model that has the `belongsTo` property:

1. A column of the format `{relation_name}_id` to store the ID of the record of the related data model. For example, the `product` table will have a `store_id` column.
2. A foreign key on the `{relation_name}_id` column to the table of the related data model.

![Diagram illustrating the relation between a store and product records in the database](https://res.cloudinary.com/dza7lstvk/image/upload/v1726733937/Medusa%20Book/one-to-many_d6wtcw.jpg)

***

## Many-to-Many Relationship

A many-to-many relationship indicates that many records of a data model can be associated with many records of another data model.

To define a many-to-many relationship, create relationship properties in the data models using the `manyToMany` method.

For example:

```ts highlights={manyToManyHighlights}
import { model } from "@medusajs/framework/utils"

const Order = model.define("order", {
  id: model.id().primaryKey(),
  products: model.manyToMany(() => Product, {
    mappedBy: "orders",
    pivotTable: "order_product",
    joinColumn: "order_id",
    inverseJoinColumn: "product_id",
  }),
})

const Product = model.define("product", {
  id: model.id().primaryKey(),
  orders: model.manyToMany(() => Order, {
    mappedBy: "products",
  }),
})
```

The `manyToMany` method accepts two parameters:

1. A function that returns the associated data model.
2. An object of optional configuration. Only one of the data models in the relation can define the `pivotTable`, `joinColumn`, and `inverseJoinColumn` configurations, and it's considered the owner data model. The object can accept the following properties:
   - `mappedBy`: The name of the relationship property in the other data model. If not set, the property's name is inferred from the associated data model's name.
   - `pivotTable`: The name of the pivot table created in the database for the many-to-many relation. If not set, the pivot table is inferred by combining the names of the data models' tables in alphabetical order, separating them by `_`, and pluralizing the last name. For example, `order_products`.
   - `joinColumn`: The name of the column in the pivot table that points to the owner model's primary key.
   - `inverseJoinColumn`: The name of the column in the pivot table that points to the owned model's primary key.

The `pivotTable`, `joinColumn`, and `inverseJoinColumn` properties are only available after [Medusa v2.0.7](https://github.com/medusajs/medusa/releases/tag/v2.0.7).

Following [Medusa v2.1.0](https://github.com/medusajs/medusa/releases/tag/v2.1.0), if `pivotTable`, `joinColumn`, and `inverseJoinColumn` aren't specified on either model, the owner is decided based on alphabetical order. So, in the example above, the `Order` data model would be the owner.

In this example, an order is associated with many products, and a product is associated with many orders. Since the `pivotTable`, `joinColumn`, and `inverseJoinColumn` configurations are defined on the order, it's considered the owner data model.

### Many-to-Many Relationship in the Database

When you generate the migrations of data models that have a many-to-many relationship, the migration adds a new pivot table. Its name is either the name you specify in the `pivotTable` configuration or the inferred name combining the names of the data models' tables in alphabetical order, separating them by `_`, and pluralizing the last name. For example, `order_products`.

The pivot table has a column with the name `{data_model}_id` for each of the data model's tables. It also has foreign keys on each of these columns to their respective tables.

The pivot table has columns with foreign keys pointing to the primary key of the associated tables. The column's name is either:

- The value of the `joinColumn` configuration for the owner table, and the `inverseJoinColumn` configuration for the owned table;
- Or the inferred name `{table_name}_id`.

![Diagram illustrating the relation between order and product records in the database](https://res.cloudinary.com/dza7lstvk/image/upload/v1726734269/Medusa%20Book/many-to-many_fzy5pq.jpg)

### Many-To-Many with Custom Columns

To add custom columns to the pivot table between two data models having a many-to-many relationship, you must define a new data model that represents the pivot table.

For example:

```ts highlights={manyToManyColumnHighlights}
import { model } from "@medusajs/framework/utils"

export const Order = model.define("order_test", {
  id: model.id().primaryKey(),
  products: model.manyToMany(() => Product, {
    pivotEntity: () => OrderProduct,
  }),
})

export const Product = model.define("product_test", {
  id: model.id().primaryKey(),
  orders: model.manyToMany(() => Order),
})

export const OrderProduct = model.define("orders_products", {
  id: model.id().primaryKey(),
  order: model.belongsTo(() => Order, {
    mappedBy: "products",
  }),
  product: model.belongsTo(() => Product, {
    mappedBy: "orders",
  }),
  metadata: model.json().nullable(),
})
```

The `Order` and `Product` data models have a many-to-many relationship. To add extra columns to the created pivot table, you pass a `pivotEntity` option to the `products` relation in `Order` (since `Order` is the owner). The value of `pivotEntity` is a function that returns the data model representing the pivot table.

The `OrderProduct` model defines, aside from the ID, the following properties:

- `order`: A relation that indicates this model belongs to the `Order` data model. You set the `mappedBy` option to the many-to-many relation's name in the `Order` data model.
- `product`: A relation that indicates this model belongs to the `Product` data model. You set the `mappedBy` option to the many-to-many relation's name in the `Product` data model.
- `metadata`: An extra column to add to the pivot table of type `json`. You can add other columns as well to the model.

***

## Set Relationship Name in the Other Model

The relationship property methods accept as a second parameter an object of options. The `mappedBy` property defines the name of the relationship in the other data model.

This is useful if the relationship property’s name is different from that of the associated data model.

As seen in previous examples, the `mappedBy` option is required for the `belongsTo` method.

For example:

```ts highlights={relationNameHighlights}
import { model } from "@medusajs/framework/utils"

const User = model.define("user", {
  id: model.id().primaryKey(),
  email: model.hasOne(() => Email, {
    mappedBy: "owner",
  }),
})

const Email = model.define("email", {
  id: model.id().primaryKey(),
  owner: model.belongsTo(() => User, {
    mappedBy: "email",
  }),
})
```

In this example, you specify in the `User` data model’s relationship property that the name of the relationship in the `Email` data model is `owner`.

***

## Cascades

When an operation is performed on a data model, such as record deletion, the relationship cascade specifies what related data model records should be affected by it.

For example, if a store is deleted, its products should also be deleted.

The `cascades` method used on a data model configures which child records an operation is cascaded to.

For example:

```ts highlights={highlights}
import { model } from "@medusajs/framework/utils"

const Store = model.define("store", {
  id: model.id().primaryKey(),
  products: model.hasMany(() => Product),
})
.cascades({
  delete: ["products"],
})

const Product = model.define("product", {
  id: model.id().primaryKey(),
  store: model.belongsTo(() => Store, {
    mappedBy: "products",
  }),
})
```

The `cascades` method accepts an object. Its key is the operation’s name, such as `delete`. The value is an array of relationship property names that the operation is cascaded to.

In the example above, when a store is deleted, its associated products are also deleted.


# Link

In this chapter, you’ll learn what Link is and how to use it to manage links.

As of [Medusa v2.2.0](https://github.com/medusajs/medusa/releases/tag/v2.2.0), Remote Link has been deprecated in favor of Link. They have the same usage, so you only need to change the key used to resolve the tool from the Medusa container as explained below.

## What is Link?

Link is a class with utility methods to manage links between data models. It’s registered in the Medusa container under the `link` registration name.

For example:

```ts collapsibleLines="1-9" expandButtonLabel="Show Imports"
import { 
  MedusaRequest, 
  MedusaResponse,
} from "@medusajs/framework/http"
import { 
  ContainerRegistrationKeys,
} from "@medusajs/framework/utils"

export async function POST(
  req: MedusaRequest,
  res: MedusaResponse
): Promise<void> {
  const link = req.scope.resolve(
    ContainerRegistrationKeys.LINK
  )
    
  // ...
}
```

You can use its methods to manage links, such as create or delete links.

***

## Create Link

To create a link between records of two data models, use the `create` method of Link.

For example:

```ts
import { Modules } from "@medusajs/framework/utils"

// ...

await link.create({
  [Modules.PRODUCT]: {
    product_id: "prod_123",
  },
  "helloModuleService": {
    my_custom_id: "mc_123",
  },
})
```

The `create` method accepts as a parameter an object. The object’s keys are the names of the linked modules.

The keys (names of linked modules) must be in the same [direction](https://docs.medusajs.com/learn/fundamentals/module-links/directions/index.html.md) of the link definition.

The value of each module’s property is an object, whose keys are of the format `{data_model_snake_name}_id`, and values are the IDs of the linked record.

So, in the example above, you link a record of the `MyCustom` data model in a `hello` module to a `Product` record in the Product Module.

### Enforced Integrity Constraints on Link Creation

Medusa enforces integrity constraints on links based on the link's relation type. So, an error is thrown in the following scenarios:

- If the link is one-to-one and one of the linked records already has a link to another record of the same data model. For example:

```ts
// no error
await link.create({
  [Modules.PRODUCT]: {
    product_id: "prod_123",
  },
  "helloModuleService": {
    my_custom_id: "mc_123",
  },
})

// throws an error because `prod_123` already has a link to `mc_123`
await link.create({
  [Modules.PRODUCT]: {
    product_id: "prod_123",
  },
  "helloModuleService": {
    my_custom_id: "mc_456",
  },
})
```

- If the link is one-to-many and the "one" side already has a link to another record of the same data model. For example, if a product can have many `MyCustom` records, but a `MyCustom` record can only have one product:

```ts
// no error
await link.create({
  [Modules.PRODUCT]: {
    product_id: "prod_123",
  },
  "helloModuleService": {
    my_custom_id: "mc_123",
  },
})

// also no error
await link.create({
  [Modules.PRODUCT]: {
    product_id: "prod_123",
  },
  "helloModuleService": {
    my_custom_id: "mc_456",
  },
})

// throws an error because `mc_123` already has a link to `prod_123`
await link.create({
  [Modules.PRODUCT]: {
    product_id: "prod_456",
  },
  "helloModuleService": {
    my_custom_id: "mc_123",
  },
})
```

There are no integrity constraints in a many-to-many link, so you can create multiple links between the same records.

***

## Dismiss Link

To remove a link between records of two data models, use the `dismiss` method of Link.

For example:

```ts
import { Modules } from "@medusajs/framework/utils"

// ...

await link.dismiss({
  [Modules.PRODUCT]: {
    product_id: "prod_123",
  },
  "helloModuleService": {
    my_custom_id: "mc_123",
  },
})
```

The `dismiss` method accepts the same parameter type as the [create method](#create-link).

The keys (names of linked modules) must be in the same [direction](https://docs.medusajs.com/learn/fundamentals/module-links/directions/index.html.md) of the link definition.

***

## Cascade Delete Linked Records

If a record is deleted, use the `delete` method of Link to delete all linked records.

For example:

```ts
import { Modules } from "@medusajs/framework/utils"

// ...

await productModuleService.deleteVariants([variant.id])

await link.delete({
  [Modules.PRODUCT]: {
    product_id: "prod_123",
  },
})
```

This deletes all records linked to the deleted product.

***

## Restore Linked Records

If a record that was previously soft-deleted is now restored, use the `restore` method of Link to restore all linked records.

For example:

```ts
import { Modules } from "@medusajs/framework/utils"

// ...

await productModuleService.restoreProducts(["prod_123"])

await link.restore({
  [Modules.PRODUCT]: {
    product_id: "prod_123",
  },
})
```


# Query

In this chapter, you’ll learn about Query and how to use it to fetch data from modules.

## What is Query?

Query fetches data across modules. It’s a set of methods registered in the Medusa container under the `query` key.

In all resources that can access the [Medusa Container](https://docs.medusajs.com/learn/fundamentals/medusa-container/index.html.md), such as API routes or workflows, you can resolve Query to fetch data across custom modules and Medusa’s Commerce Modules.

***

## Query Example

For example, create the route `src/api/query/route.ts` with the following content:

```ts title="src/api/query/route.ts" highlights={exampleHighlights} collapsibleLines="1-8" expandButtonLabel="Show Imports"
import {
  MedusaRequest,
  MedusaResponse,
} from "@medusajs/framework/http"
import {
  ContainerRegistrationKeys,
} from "@medusajs/framework/utils"

export const GET = async (
  req: MedusaRequest,
  res: MedusaResponse
) => {
  const query = req.scope.resolve(ContainerRegistrationKeys.QUERY)

  const { data: posts } = await query.graph({
    entity: "post",
    fields: ["id", "title"],
  })

  res.json({ posts })
}
```

In the above example, you resolve Query from the Medusa container using the `ContainerRegistrationKeys.QUERY` (`query`) key.

Then, you run a query using its `graph` method. This method accepts as a parameter an object with the following required properties:

- `entity`: The data model's name, as specified in the first parameter of the `model.define` method used for the data model's definition.
- `fields`: An array of the data model’s properties to retrieve in the result.

The method returns an object that has a `data` property, which holds an array of the retrieved data. For example:

```json title="Returned Data"
{
  "data": [
    {
      "id": "123",
      "title": "My Post"
    }
  ]
}
```

***

## Querying the Graph

When you use the `query.graph` method, you're running a query through an internal graph that the Medusa application creates.

This graph collects data models of all modules in your application, including commerce and custom modules, and identifies relations and links between them.

***

## Retrieve Linked Records

Retrieve the records of a linked data model by passing in `fields` the data model's name suffixed with `.*`.

For example:

```ts highlights={[["6"]]}
const { data: posts } = await query.graph({
  entity: "post",
  fields: [
    "id", 
    "title",
    "product.*",
  ],
})
```

`.*` means that all of data model's properties should be retrieved. You can also retrieve specific properties by replacing the `*` with the property name, for each property.

For example:

```ts
const { data: posts } = await query.graph({
  entity: "post",
  fields: [
    "id", 
    "title",
    "product.id",
    "product.title",
  ],
})
```

In the example above, you retrieve only the `id` and `title` properties of the `product` linked to a `post`.

### Retrieve List Link Records

If the linked data model has `isList` enabled in the link definition, pass in `fields` the data model's plural name suffixed with `.*`.

For example:

```ts highlights={[["6"]]}
const { data: posts } = await query.graph({
  entity: "post",
  fields: [
    "id", 
    "title",
    "products.*",
  ],
})
```

In the example above, you retrieve all products linked to a post.

### Apply Filters and Pagination on Linked Records

Consider you want to apply filters or pagination configurations on the product(s) linked to `post`. To do that, you must query the module link's table instead.

As mentioned in the [Module Link](https://docs.medusajs.com/learn/fundamentals/module-links/index.html.md) documentation, Medusa creates a table for your module link. So, not only can you retrieve linked records, but you can also retrieve the records in a module link's table.

A module link's definition, exported by a file under `src/links`, has a special `entryPoint` property. Use this property when specifying the `entity` property in Query's `graph` method.

For example:

```ts highlights={queryLinkTableHighlights}
import ProductPostLink from "../../../links/product-post"

// ...

const { data: productCustoms } = await query.graph({
  entity: ProductPostLink.entryPoint,
  fields: ["*", "product.*", "post.*"],
  pagination: {
    take: 5,
    skip: 0,
  },
})
```

In the object passed to the `graph` method:

- You pass the `entryPoint` property of the link definition as the value for `entity`. So, Query will retrieve records from the module link's table.
- You pass three items to the `field` property:
  - `*` to retrieve the link table's fields. This is useful if the link table has [custom columns](https://docs.medusajs.com/learn/fundamentals/module-links/custom-columns/index.html.md).
  - `product.*` to retrieve the fields of a product record linked to a `Post` record.
  - `post.*` to retrieve the fields of a `Post` record linked to a product record.

You can then apply any [filters](#apply-filters) or [pagination configurations](#apply-pagination) on the module link's table. For example, you can apply filters on the `product_id`, `post_id`, and any other custom columns you defined in the link table.

The returned `data` is similar to the following:

```json title="Example Result"
[{
  "id": "123",
  "product_id": "prod_123",
  "post_id": "123",
  "product": {
    "id": "prod_123",
    // other product fields...
  },
  "post": {
    "id": "123",
    // other post fields...
  }
}]
```

***

## Apply Filters

```ts highlights={[["4"], ["5"], ["6"]]}
const { data: posts } = await query.graph({
  entity: "post",
  fields: ["id", "title"],
  filters: {
    id: "post_123",
  },
})
```

The `query.graph` function accepts a `filters` property. You can use this property to filter retrieved records.

In the example above, you filter the `post` records by the ID `post_123`.

You can also filter by multiple values of a property. For example:

```ts highlights={[["4"], ["5"], ["6"], ["7"], ["8"], ["9"]]}
const { data: posts } = await query.graph({
  entity: "post",
  fields: ["id", "title"],
  filters: {
    id: [
      "post_123",
      "post_321",
    ],
  },
})
```

In the example above, you filter the `post` records by multiple IDs.

Filters don't apply on fields of linked data models from other modules. Refer to the [Retrieve Linked Records](#retrieve-linked-records) section for an alternative solution.

### Advanced Query Filters

Under the hood, Query uses one of the following methods from the data model's module's service to retrieve records:

- `listX` if you don't pass [pagination parameters](#apply-pagination). For example, `listPosts`.
- `listAndCountX` if you pass pagination parameters. For example, `listAndCountPosts`.

Both methods accepts a filter object that can be used to filter records.

Those filters don't just allow you to filter by exact values. You can also filter by properties that don't match a value, match multiple values, and other filter types.

Refer to the [Service Factory Reference](https://docs.medusajs.com/resources/service-factory-reference/tips/filtering/index.html.md) for examples of advanced filters. The following sections provide some quick examples.

#### Filter by Not Matching a Value

```ts highlights={[["4"], ["5"], ["6"], ["7"], ["8"]]}
const { data: posts } = await query.graph({
  entity: "post",
  fields: ["id", "title"],
  filters: {
    title: {
      $ne: null,
    },
  },
})
```

In the example above, only posts that have a title are retrieved.

#### Filter by Not Matching Multiple Values

```ts highlights={[["4"], ["5"], ["6"], ["7"], ["8"]]}
const { data: posts } = await query.graph({
  entity: "post",
  fields: ["id", "title"],
  filters: {
    title: {
      $nin: ["My Post", "Another Post"],
    },
  },
})
```

In the example above, only posts that don't have the title `My Post` or `Another Post` are retrieved.

#### Filter by a Range

```ts highlights={[["10"], ["11"], ["12"], ["13"], ["14"], ["15"]]}
const startToday = new Date()
startToday.setHours(0, 0, 0, 0)

const endToday = new Date()
endToday.setHours(23, 59, 59, 59)

const { data: posts } = await query.graph({
  entity: "post",
  fields: ["id", "title"],
  filters: {
    published_at: {
      $gt: startToday,
      $lt: endToday,
    },
  },
})
```

In the example above, only posts that were published today are retrieved.

#### Filter Text by Like Value

This filter only applies to text-like properties, including `text`, `id`, and `enum` properties.

```ts highlights={[["4"], ["5"], ["6"], ["7"], ["8"]]}
const { data: posts } = await query.graph({
  entity: "post",
  fields: ["id", "title"],
  filters: {
    title: {
      $like: "%My%",
    },
  },
})
```

In the example above, only posts that have the word `My` in their title are retrieved.

#### Filter a Relation's Property

```ts highlights={[["4"], ["5"], ["6"], ["7"], ["8"]]}
const { data: posts } = await query.graph({
  entity: "post",
  fields: ["id", "title"],
  filters: {
    author: {
      name: "John",
    },
  },
})
```

While it's not possible to filter by a linked data model's property, you can filter by a relation's property (that is, the property of a related data model that is defined in the same module).

In the example above, only posts that have an author with the name `John` are retrieved.

***

## Apply Pagination

```ts highlights={[["8", "skip", "The number of records to skip before fetching the results."], ["9", "take", "The number of records to fetch."]]}
const { 
  data: posts,
  metadata: { count, take, skip } = {},
} = await query.graph({
  entity: "post",
  fields: ["id", "title"],
  pagination: {
    skip: 0,
    take: 10,
  },
})
```

The `graph` method's object parameter accepts a `pagination` property to configure the pagination of retrieved records.

To paginate the returned records, pass the following properties to `pagination`:

- `skip`: (required to apply pagination) The number of records to skip before fetching the results.
- `take`: The number of records to fetch.

When you provide the pagination fields, the `query.graph` method's returned object has a `metadata` property. Its value is an object having the following properties:

- skip: (\`number\`) The number of records skipped.
- take: (\`number\`) The number of records requested to fetch.
- count: (\`number\`) The total number of records.

### Sort Records

```ts highlights={[["5"], ["6"], ["7"]]}
const { data: posts } = await query.graph({
  entity: "post",
  fields: ["id", "title"],
  pagination: {
    order: {
      name: "DESC",
    },
  },
})
```

Sorting doesn't work on fields of linked data models from other modules.

To sort returned records, pass an `order` property to `pagination`.

The `order` property is an object whose keys are property names, and values are either:

- `ASC` to sort records by that property in ascending order.
- `DESC` to sort records by that property in descending order.

***

## Configure Query to Throw Errors

By default, if Query doesn't find records matching your query, it returns an empty array. You can add option to configure Query to throw an error when no records are found.

The `query.graph` method accepts as a second parameter an object that can have a `throwIfKeyNotFound` property. Its value is a boolean indicating whether to throw an error if no record is found when filtering by IDs. By default, it's `false`.

For example:

```ts
const { data: posts } = await query.graph({
  entity: "post",
  fields: ["id", "title"],
  filters: {
    id: "post_123",
  },
}, {
  throwIfKeyNotFound: true,
})
```

In the example above, if no post is found with the ID `post_123`, Query will throw an error. This is useful to stop execution when a record is expected to exist.

### Throw Error on Related Data Model

The `throwIfKeyNotFound` option can also be used to throw an error if the ID of a related data model's record (in the same module) is passed in the filters, and the related record doesn't exist.

For example:

```ts
const { data: posts } = await query.graph({
  entity: "post",
  fields: ["id", "title", "author.*"],
  filters: {
    id: "post_123",
    author_id: "author_123",
  },
}, {
  throwIfKeyNotFound: true,
})
```

In the example above, Query throws an error either if no post is found with the ID `post_123` or if its found but its author ID isn't `author_123`.

In the above example, it's assumed that a post belongs to an author, so it has an `author_id` property. However, this also works in the opposite case, where an author has many posts.

For example:

```ts
const { data: posts } = await query.graph({
  entity: "author",
  fields: ["id", "name", "posts.*"],
  filters: {
    id: "author_123",
    posts: {
      id: "post_123",
    },
  },
}, {
  throwIfKeyNotFound: true,
})
```

In the example above, Query throws an error if no author is found with the ID `author_123` or if the author is found but doesn't have a post with the ID `post_123`.

***

## Request Query Configurations

For API routes that retrieve a single or list of resources, Medusa provides a `validateAndTransformQuery` middleware that:

- Validates accepted query parameters, as explained in [this documentation](https://docs.medusajs.com/learn/fundamentals/api-routes/validation/index.html.md).
- Parses configurations that are received as query parameters to be passed to Query.

Using this middleware allows you to have default configurations for retrieved fields and relations or pagination, while allowing clients to customize them per request.

### Step 1: Add Middleware

The first step is to use the `validateAndTransformQuery` middleware on the `GET` route. You add the middleware in `src/api/middlewares.ts`:

```ts title="src/api/middlewares.ts"
import { 
  validateAndTransformQuery,
  defineMiddlewares,
} from "@medusajs/framework/http"
import { createFindParams } from "@medusajs/medusa/api/utils/validators"

export const GetCustomSchema = createFindParams()

export default defineMiddlewares({
  routes: [
    {
      matcher: "/customs",
      method: "GET",
      middlewares: [
        validateAndTransformQuery(
          GetCustomSchema,
          {
            defaults: [
              "id",
              "title",
              "products.*",
            ],
            isList: true,
          }
        ),
      ],
    },
  ],
})
```

The `validateAndTransformQuery` accepts two parameters:

1. A Zod validation schema for the query parameters, which you can learn more about in the [API Route Validation documentation](https://docs.medusajs.com/learn/fundamentals/api-routes/validation/index.html.md). Medusa has a `createFindParams` utility that generates a Zod schema that accepts four query parameters:
   1. `fields`: The fields and relations to retrieve in the returned resources.
   2. `offset`: The number of items to skip before retrieving the returned items.
   3. `limit`: The maximum number of items to return.
   4. `order`: The fields to order the returned items by in ascending or descending order.
2. A Query configuration object. It accepts the following properties:
   1. `defaults`: An array of default fields and relations to retrieve in each resource.
   2. `isList`: A boolean indicating whether a list of items are returned in the response.
   3. `allowed`: An array of fields and relations allowed to be passed in the `fields` query parameter.
   4. `defaultLimit`: A number indicating the default limit to use if no limit is provided. By default, it's `50`.

### Step 2: Use Configurations in API Route

After applying this middleware, your API route now accepts the `fields`, `offset`, `limit`, and `order` query parameters mentioned above.

The middleware transforms these parameters to configurations that you can pass to Query in your API route handler. These configurations are stored in the `queryConfig` parameter of the `MedusaRequest` object.

As of [Medusa v2.2.0](https://github.com/medusajs/medusa/releases/tag/v2.2.0), `remoteQueryConfig` has been deprecated in favor of `queryConfig`. Their usage is still the same, only the property name has changed.

For example, Create the file `src/api/customs/route.ts` with the following content:

```ts title="src/api/customs/route.ts"
import {
  MedusaRequest,
  MedusaResponse,
} from "@medusajs/framework/http"
import {
  ContainerRegistrationKeys,
} from "@medusajs/framework/utils"

export const GET = async (
  req: MedusaRequest,
  res: MedusaResponse
) => {
  const query = req.scope.resolve(ContainerRegistrationKeys.QUERY)

  const { data: posts } = await query.graph({
    entity: "post",
    ...req.queryConfig,
  })

  res.json({ posts: posts })
}
```

This adds a `GET` API route at `/customs`, which is the API route you added the middleware for.

In the API route, you pass `req.queryConfig` to `query.graph`. `queryConfig` has properties like `fields` and `pagination` to configure the query based on the default values you specified in the middleware, and the query parameters passed in the request.

### Test it Out

To test it out, start your Medusa application and send a `GET` request to the `/customs` API route. A list of records are retrieved with the specified fields in the middleware.

```json title="Returned Data"
{
  "posts": [
    {
      "id": "123",
      "title": "test"
    }
  ]
}
```

Try passing one of the Query configuration parameters, like `fields` or `limit`, and you'll see its impact on the returned result.

Learn more about [specifing fields and relations](https://docs.medusajs.com/api/store#select-fields-and-relations) and [pagination](https://docs.medusajs.com/api/store#pagination) in the API reference.


# Query Context

In this chapter, you'll learn how to pass contexts when retrieving data with [Query](https://docs.medusajs.com/learn/fundamentals/module-links/query/index.html.md).

## What is Query Context?

Query context is a way to pass additional information when retrieving data with Query. This data can be useful when applying custom transformations to the retrieved data based on the current context.

For example, consider you have a Blog Module with posts and authors. You can accept the user's language as a context and return the posts in the user's language. Another example is how Medusa uses Query Context to [retrieve product variants' prices based on the customer's currency](https://docs.medusajs.com/resources/commerce-modules/product/guides/price/index.html.md).

***

## How to Use Query Context

The `query.graph` method accepts an optional `context` parameter that can be used to pass additional context either to the data model you're retrieving (for example, `post`), or its related and linked models (for example, `author`).

You initialize a context using `QueryContext` from the Modules SDK. It accepts an object of contexts as an argument.

For example, to retrieve posts using Query while passing the user's language as a context:

```ts
const { data } = await query.graph({
  entity: "post",
  fields: ["*"],
  context: QueryContext({
    lang: "es",
  }),
})
```

In this example, you pass in the context a `lang` property whose value is `es`.

Then, to handle the context while retrieving records of the data model, in the associated module's service you override the generated `list` method of the data model.

For example, continuing the example above, you can override the `listPosts` method of the Blog Module's service to handle the context:

```ts highlights={highlights2}
import { MedusaContext, MedusaService } from "@medusajs/framework/utils"
import { Context, FindConfig } from "@medusajs/framework/types"
import Post from "./models/post"
import Author from "./models/author"

class BlogModuleService extends MedusaService({
  Post,
  Author,
}){
  // @ts-ignore
  async listPosts(
    filters?: any, 
    config?: FindConfig<any> | undefined, 
    @MedusaContext() sharedContext?: Context | undefined
  ) {
    const context = filters.context ?? {}
    delete filters.context

    let posts = await super.listPosts(filters, config, sharedContext)

    if (context.lang === "es") {
      posts = posts.map((post) => {
        return {
          ...post,
          title: post.title + " en español",
        }
      })
    }

    return posts
  }
}

export default BlogModuleService
```

In the above example, you override the generated `listPosts` method. This method receives as a first parameter the filters passed to the query, but it also includes a `context` property that holds the context passed to the query.

You extract the context from `filters`, then retrieve the posts using the parent's `listPosts` method. After that, if the language is set in the context, you transform the titles of the posts.

All posts returned will now have their titles appended with "en español".

Learn more about the generated `list` method in [this reference](https://docs.medusajs.com/resources/service-factory-reference/methods/list/index.html.md).

### Using Pagination with Query

If you pass pagination fields to `query.graph`, you must also override the `listAndCount` method in the service.

For example, following along with the previous example, you must override the `listAndCountPosts` method of the Blog Module's service:

```ts
import { MedusaContext, MedusaService } from "@medusajs/framework/utils"
import { Context, FindConfig } from "@medusajs/framework/types"
import Post from "./models/post"
import Author from "./models/author"

class BlogModuleService extends MedusaService({
  Post,
  Author,
}){
  // @ts-ignore
  async listAndCountPosts(
    filters?: any, 
    config?: FindConfig<any> | undefined, 
    @MedusaContext() sharedContext?: Context | undefined
  ) {
    const context = filters.context ?? {}
    delete filters.context

    const result = await super.listAndCountPosts(
      filters, 
      config, 
      sharedContext
    )

    if (context.lang === "es") {
      result.posts = posts.map((post) => {
        return {
          ...post,
          title: post.title + " en español",
        }
      })
    }

    return result
  }
}

export default BlogModuleService
```

Now, the `listAndCountPosts` method will handle the context passed to `query.graph` when you pass pagination fields. You can also move the logic to transform the posts' titles to a separate method and call it from both `listPosts` and `listAndCountPosts`.

***

## Passing Query Context to Related Data Models

If you're retrieving a data model and you want to pass context to its associated model in the same module, you can pass them as part of `QueryContext`'s parameter, then handle them in the same `list` method.

For linked data models, check out the [next section](#passing-query-context-to-linked-data-models).

For example, to pass a context for the post's authors:

```ts highlights={highlights3}
const { data } = await query.graph({
  entity: "post",
  fields: ["*"],
  context: QueryContext({
    lang: "es",
    author: QueryContext({
      lang: "es",
    }),
  }),
})
```

Then, in the `listPosts` method, you can handle the context for the post's authors:

```ts highlights={highlights4}
import { MedusaContext, MedusaService } from "@medusajs/framework/utils"
import { Context, FindConfig } from "@medusajs/framework/types"
import Post from "./models/post"
import Author from "./models/author"

class BlogModuleService extends MedusaService({
  Post,
  Author,
}){
  // @ts-ignore
  async listPosts(
    filters?: any, 
    config?: FindConfig<any> | undefined, 
    @MedusaContext() sharedContext?: Context | undefined
  ) {
    const context = filters.context ?? {}
    delete filters.context

    let posts = await super.listPosts(filters, config, sharedContext)

    const isPostLangEs = context.lang === "es"
    const isAuthorLangEs = context.author?.lang === "es"

    if (isPostLangEs || isAuthorLangEs) {
      posts = posts.map((post) => {
        return {
          ...post,
          title: isPostLangEs ? post.title + " en español" : post.title,
          author: {
            ...post.author,
            name: isAuthorLangEs ? post.author.name + " en español" : post.author.name,
          },
        }
      })
    }

    return posts
  }
}

export default BlogModuleService
```

The context in `filters` will also have the context for `author`, which you can use to make transformations to the post's authors.

***

## Passing Query Context to Linked Data Models

If you're retrieving a data model and you want to pass context to a linked model in a different module, pass to the `context` property an object instead, where its keys are the linked model's name and the values are the context for that linked model.

For example, consider the Product Module's `Product` data model is linked to the Blog Module's `Post` data model. You can pass context to the `Post` data model while retrieving products like so:

```ts highlights={highlights5}
const { data } = await query.graph({
  entity: "product",
  fields: ["*", "post.*"],
  context: {
    post: QueryContext({
      lang: "es",
    }),
  },
})
```

In this example, you retrieve products and their associated posts. You also pass a context for `post`, indicating the customer's language.

To handle the context, you override the generated `listPosts` method of the Blog Module as explained [previously](#how-to-use-query-context).


# Read-Only Module Link

In this chapter, you’ll learn what a read-only module link is and how to define one.

## What is a Read-Only Module Link?

Consider a scenario where you need to access related records from another module, but don't want the overhead of managing or storing the links between them. This can include cases where you're working with external data models not stored in your Medusa database, such as third-party systems.

In those cases, instead of defining a [Module Link](https://docs.medusajs.com/learn/fundamentals/module-links/index.html.md) whose linked records must be stored in a link table in the database, you can use a read-only module link. A read-only module link builds a virtual relation from one data model to another in a different module without creating a link table in the database. Instead, the linked record's ID is stored in the first data model's field.

For example, Medusa creates a read-only module link from the `Cart` data model of the [Cart Module](https://docs.medusajs.com/resources/commerce-modules/cart/index.html.md) to the `Customer` data model of the [Customer Module](https://docs.medusajs.com/resources/commerce-modules/customer/index.html.md). This link allows you to access the details of the cart's customer without managing the link. Instead, the customer's ID is stored in the `Cart` data model.

![Diagram illustrating the read-only module link from cart to customer](https://res.cloudinary.com/dza7lstvk/image/upload/v1742212508/Medusa%20Book/cart-customer_w6vk59.jpg)

***

## How to Define a Read-Only Module Link

The `defineLink` function accepts an optional third-parameter object that can hold additional configurations for the module link.

If you're not familiar with the `defineLink` function, refer to the [Module Links chapter](https://docs.medusajs.com/learn/fundamentals/module-links/index.html.md) for more information.

To make the module link read-only, pass the `readOnly` property as `true`. You must also set in the link configuration of the first data model a `field` property that specifies the data model's field where the linked record's ID is stored.

For example:

```ts highlights={highlights}
import BlogModule from "../modules/blog"
import ProductModule from "@medusajs/medusa/product"
import { defineLink } from "@medusajs/framework/utils"

export default defineLink(
  {
    linkable: BlogModule.linkable.post,
    field: "product_id",
  },
  ProductModule.linkable.product,
  {
    readOnly: true,
  }
)
```

In this example, you define a read-only module link from the Blog Module's `post` data model to the Product Module's `product` data model. You do that by:

- Passing an object as a first parameter that accepts the linkable configuration and the field where the linked record's ID is stored.
- Setting the `readOnly` property to `true` in the third parameter.

Unlike the stored module link, Medusa will not create a table in the database for this link. Instead, Medusa uses the ID stored in the specified field of the first data model to retrieve the linked record.

***

## Retrieve Read-Only Linked Record

[Query](https://docs.medusajs.com/learn/fundamentals/module-links/query/index.html.md) allows you to retrieve records linked through a read-only module link.

For example, assuming you have the module link created in [the above section](#how-to-define-a-read-only-module-link), you can retrieve a post and its linked product as follows:

```ts
const { result } = await query.graph({
  entity: "post",
  fields: ["id", "product.*"],
  filters: {
    id: "post_123",
  },
})
```

In the above example, you retrieve a post and its linked product. Medusa will use the ID of the product in the post's `product_id` field to determine which product should be retrieved.

***

## Read-Only Module Link Direction

A read-only module is uni-directional. So, you can only retrieve the linked record from the first data model. If you need to access the linked record from the second data model, you must define another read-only module link in the opposite direction.

In the `blog` -> `product` example, you can access a post's product, but you can't access a product's posts. You would have to define another read-only module link from `product` to `blog` to access a product's posts.

***

## Inverse Read-Only Module Link

An inverse read-only module link is a read-only module link that allows you to access the linked record based on the ID stored in the second data model.

For example, consider you want to access a product's posts. You can define a read-only module link from the Product Module's `product` data model to the Blog Module's `post` data model:

```ts
import BlogModule from "../modules/blog"
import ProductModule from "@medusajs/medusa/product"
import { defineLink } from "@medusajs/framework/utils"

export default defineLink(
  {
    linkable: ProductModule.linkable.product,
    field: "id",
  },
  {
    linkable: BlogModule.linkable.post.id,
    primaryKey: "product_id",
  },
  {
    readOnly: true,
  }
)
```

In the above example, you define a read-only module link from the Product Module's `product` data model to the Blog Module's `post` data model. This link allows you to access a product's posts.

Since you can't add a `post_id` field to the `product` data model, you must:

1. Set the `field` property in the first data model's link configuration to the product's ID field.
2. Spread the `BlogModule.linkable.post.id` object in the second parameter object and set the `primaryKey` property to the field in the `post` data model that holds the product's ID.

You can now retrieve a product and its linked posts:

```ts
const { result } = await query.graph({
  entity: "product",
  fields: ["id", "post.*"],
  filters: {
    id: "prod_123",
  },
})
```

***

## One-to-One or One-to-Many?

When you retrieve the linked record through a read-only module link, the retrieved data may be an object (one-to-one) or an array of objects (one-to-many) based on different criteria.

|Scenario|Relation Type|
|---|---|---|
|The first data model's |One-to-one relation|
|The first data model's |One-to-many relation|
|The read-only module link is inversed.|One-to-many relation if multiple records in the second data model have the same ID of the first data model. Otherwise, one-to-one relation.|

### One-to-One Relation

Consider the first read-only module link you defined in this chapter:

```ts
import BlogModule from "../modules/blog"
import ProductModule from "@medusajs/medusa/product"

export default defineLink(
  {
    linkable: BlogModule.linkable.post,
    field: "product_id",
  },
  ProductModule.linkable.product,
  {
    readOnly: true,
  }
)
```

Since the `product_id` field of a post stores the ID of a single product, the link is a one-to-one relation. When querying a post, you'll get a single product object:

```json title="Example Data"
[
  {
    "id": "post_123",
    "product_id": "prod_123",
    "product": {
      "id": "prod_123",
      // ...
    }
  }
]
```

### One-to-Many Relation

Consider the read-only module link from the `post` data model uses an array of product IDs:

```ts
import BlogModule from "../modules/blog"
import ProductModule from "@medusajs/medusa/product"

export default defineLink(
  {
    linkable: BlogModule.linkable.post,
    field: "product_ids",
  },
  ProductModule.linkable.product,
  {
    readOnly: true,
  }
)
```

Where `product_ids` in the `post` data model is an array of strings. In this case, the link would be a one-to-many relation. So, an array of products would be returned when querying a post:

```json title="Example Data"
[
  {
    "id": "post_123",
    "product_ids": ["prod_123", "prod_124"],
    "product": [
      {
        "id": "prod_123",
        // ...
      },
      {
        "id": "prod_124",
        // ...
      }
    ]
  }
]
```

### Relation with Inversed Read-Only Link

If you define an inversed read-only module link where the ID of the linked record is stored in the second data model, the link can be either one-to-one or one-to-many based on the number of records in the second data model that have the same ID of the first data model.

For example, consider the `product` -> `post` link you defined in an earlier section:

```ts
import BlogModule from "../modules/blog"
import ProductModule from "@medusajs/medusa/product"
import { defineLink } from "@medusajs/framework/utils"

export default defineLink(
  {
    linkable: ProductModule.linkable.product,
    field: "id",
  },
  {
    linkable: BlogModule.linkable.post.id,
    primaryKey: "product_id",
  },
  {
    readOnly: true,
  }
)
```

In the above snippet, the ID of the product is stored in the `post`'s `product_id` string field.

When you retrieve the post of a product, it may be a post object, or an array of post objects if multiple posts are linked to the product:

```json title="Example Data"
[
  {
    "id": "prod_123",
    "post": {
      "id": "post_123",
      "product_id": "prod_123"
      // ...
    }
  },
  {
    "id": "prod_321",
    "post": [
      {
        "id": "post_123",
        "product_id": "prod_321"
        // ...
      },
      {
        "id": "post_124",
        "product_id": "prod_321"
        // ...
      }
    ]
  }
]
```

If, however, you use an array field in `post`, the relation would always be one-to-many:

```json title="Example Data"
[
  {
    "id": "prod_123",
    "post": [
      {
        "id": "post_123",
        "product_id": "prod_123"
        // ...
      }
    ]
  }
]
```

#### Force One-to-Many Relation

Alternatively, you can force a one-to-many relation by setting `isList` to `true` in the first data model's link configuration. For example:

```ts
import BlogModule from "../modules/blog"
import ProductModule from "@medusajs/medusa/product"
import { defineLink } from "@medusajs/framework/utils"

export default defineLink(
  {
    linkable: ProductModule.linkable.product,
    field: "id",
    isList: true,
  },
  {
    linkable: BlogModule.linkable.post.id,
    primaryKey: "product_id",
  },
  {
    readOnly: true,
  }
)
```

In this case, the relation would always be one-to-many, even if only one post is linked to a product:

```json title="Example Data"
[
  {
    "id": "prod_123",
    "post": [
      {
        "id": "post_123",
        "product_id": "prod_123"
        // ...
      }
    ]
  }
]
```

***

## Example: Read-Only Module Link for Virtual Data Models

Read-only module links are most useful when working with data models that aren't stored in your Medusa database. For example, data that is stored in a third-party system. In those cases, you can define a read-only module link between a data model in Medusa and the data model in the external system, facilitating the retrieval of the linked data.

To define the read-only module link to a virtual data model, you must:

1. Create a `list` method in the custom module's service. This method retrieves the linked records filtered by the ID(s) of the first data model.
   - You can also create a `listAndCount` method to retrieve the related records with pagination.
2. Define the read-only module link from the first data model to the virtual data model.
3. Use Query to retrieve the first data model and its linked records from the virtual data model.

For example, consider you have a third-party Content-Management System (CMS) that you're integrating with Medusa, and you want to retrieve the posts in the CMS associated with a product in Medusa.

To do that, first, create a CMS Module having the following service:

Refer to the [Modules chapter](https://docs.medusajs.com/learn/fundamentals/modules/index.html.md) to learn how to create a module and its service.

```ts title="src/modules/cms/service.ts"
import { FindConfig } from "@medusajs/framework/types"

type CmsModuleOptions = {
  apiKey: string
}

export default class CmsModuleService {
  private client

  constructor({}, options: CmsModuleOptions) {
    this.client = new Client(options)
  }

  async list(
    filter: {
      id: string | string[]
    }
  ) {
    return this.client.getPosts(filter)
    /**
     - Example of returned data:
     - 
     - [
     -   {
     -     "id": "post_123",
     -     "product_id": "prod_321"
     -   },
     -   {
     -     "id": "post_456",
     -     "product_id": "prod_654"
     -   }
     - ]
    */
  }

  // To retrieve with pagination
  async listAndCount(
    filter: {
      id: string | string[]
    },
    config?: FindConfig<any> | undefined 
  ) {
    return this.client.getPosts(filter, {
      limit: config?.take,
      offset: config?.skip,
    })
    /**
     - Example of returned data:
     - 
     - {
     -   count: 2,
     -   data: [
     -     {
     -       "id": "post_123",
     -       "product_id": "prod_321"
     -     },
     -     {
     -       "id": "post_456",
     -       "product_id": "prod_654"
     -     }
     -   ]
     - }
    */
  }
}
```

The above service initializes a client, assuming your CMS has an SDK that allows you to retrieve posts.

The service must have a `list` method to be part of the read-only module link. This method accepts the ID(s) of the products to retrieve their associated posts. The posts must include the product's ID in a field, such as `product_id`.

You can also create a `listAndCount` method to retrieve the posts with pagination. This method is called if you pass [pagination parameters to Query](https://docs.medusajs.com/learn/fundamentals/module-links/query#apply-pagination/index.html.md).

Next, define a read-only module link from the Product Module to the CMS Module:

```ts title="src/links/product-cms.ts"
import { defineLink } from "@medusajs/framework/utils"
import ProductModule from "@medusajs/medusa/product"
import { CMS_MODULE } from "../modules/cms"

export default defineLink(
  {
    linkable: ProductModule.linkable.product,
    field: "id",
  },
  {
    linkable: {
      serviceName: CMS_MODULE,
      alias: "cms_post",
      primaryKey: "product_id",
    },
  },
  {
    readOnly: true,
  }
)
```

To define the read-only module link, you must pass to `defineLink`:

1. The first parameter: an object with the linkable configuration of the data model in Medusa, and the fields that will be passed as a filter to the CMS service. For example, if you want to filter by product title instead, you can pass `title` instead of `id`.
2. The second parameter: an object with the linkable configuration of the virtual data model in the CMS. This object must have the following properties:
   - `serviceName`: The name of the service, which is the CMS Module's name. Medusa uses this name to resolve the module's service from the [Medusa container](https://docs.medusajs.com/learn/fundamentals/medusa-container/index.html.md).
   - `alias`: The alias to use when querying the linked records. You'll see how that works in a bit.
   - `primaryKey`: The field in the CMS data model that holds the ID of a product.
3. The third parameter: an object with the `readOnly` property set to `true`.

Now, you can use Query to retrieve a product and its linked post from the CMS:

```ts
const { data } = await query.graph({
  entity: "product",
  fields: ["id", "cms_post.*"],
})
```

In the above example, each product that has a CMS post with the `product_id` field set to the product's ID will be retrieved:

```json title="Example Data"
[
  {
    "id": "prod_123",
    "cms_post": {
      "id": "post_123",
      "product_id": "prod_123",
      // ...
    }
  }
]
```

If multiple posts have their `product_id` set to a product's ID, an array of posts is returned instead:

```json title="Example Data"
[
  {
    "id": "prod_123",
    "cms_post": [
      {
        "id": "post_123",
        "product_id": "prod_123",
        // ...
      },
      {
        "id": "post_124",
        "product_id": "prod_123",
        // ...
      }
    ]
  }
]
```

[Sanity Integration Tutorial](https://docs.medusajs.com/resources/integrations/guides/sanity/index.html.md).


# Commerce Modules

In this chapter, you'll learn about Medusa's Commerce Modules.

## What is a Commerce Module?

Commerce Modules are built-in [modules](https://docs.medusajs.com/learn/fundamentals/modules/index.html.md) of Medusa that provide core commerce logic specific to domains like Products, Orders, Customers, Fulfillment, and much more.

Medusa's Commerce Modules are used to form Medusa's default [workflows](https://docs.medusajs.com/resources/medusa-workflows-reference/index.html.md) and [APIs](https://docs.medusajs.com/api/store). For example, when you call the add to cart endpoint. the add to cart workflow runs which uses the Product Module to check if the product exists, the Inventory Module to ensure the product is available in the inventory, and the Cart Module to finally add the product to the cart.

You'll find the details and steps of the add-to-cart workflow in [this workflow reference](https://docs.medusajs.com/resources/references/medusa-workflows/addToCartWorkflow/index.html.md)

The core commerce logic contained in Commerce Modules is also available directly when you are building customizations. This granular access to commerce functionality is unique and expands what's possible to build with Medusa drastically.

### List of Medusa's Commerce Modules

Refer to [this reference](https://docs.medusajs.com/resources/commerce-modules/index.html.md) for a full list of Commerce Modules in Medusa.

***

## Use Commerce Modules in Custom Flows

Similar to your [custom modules](https://docs.medusajs.com/learn/fundamentals/modules/index.html.md), the Medusa application registers a Commerce Module's service in the [container](https://docs.medusajs.com/learn/fundamentals/medusa-container/index.html.md). So, you can resolve it in your custom flows. This is useful as you build unique requirements extending core commerce features.

For example, consider you have a [workflow](https://docs.medusajs.com/learn/fundamentals/workflows/index.html.md) (a special function that performs a task in a series of steps with rollback mechanism) that needs a step to retrieve the total number of products. You can create a step in the workflow that resolves the Product Module's service from the container to use its methods:

```ts highlights={highlights}
import { createStep, StepResponse } from "@medusajs/framework/workflows-sdk"

export const countProductsStep = createStep(
  "count-products",
  async ({ }, { container }) => {
    const productModuleService = container.resolve("product")

    const [,count] = await productModuleService.listAndCountProducts()

    return new StepResponse(count)
  }
)
```

Your workflow can use services of both custom and Commerce Modules, supporting you in building custom flows without having to re-build core commerce features.


# Module Container

In this chapter, you'll learn about the module's container and how to resolve resources in that container.

Since modules are [isolated](https://docs.medusajs.com/learn/fundamentals/modules/isolation/index.html.md), each module has a local container only used by the resources of that module.

So, resources in the module, such as services or loaders, can only resolve other resources registered in the module's container, and some Framework tools that the Medusa application registers in the module's container.

### List of Registered Resources

Find a list of resources or dependencies registered in a module's container in [the Container Resources reference](https://docs.medusajs.com/resources/medusa-container-resources/index.html.md).

***

## Resolve Resources

### Services

A service's constructor accepts as a first parameter an object used to resolve resources registered in the module's container.

For example:

```ts highlights={[["4"], ["10"]]}
import { Logger } from "@medusajs/framework/types"

type InjectedDependencies = {
  logger: Logger
}

export default class BlogModuleService {
  protected logger_: Logger

  constructor({ logger }: InjectedDependencies) {
    this.logger_ = logger

    this.logger_.info("[BlogModuleService]: Hello World!")
  }

  // ...
}
```

### Loader

A loader function accepts as a parameter an object having the property `container`. Its value is the module's container used to resolve resources.

For example:

```ts highlights={[["9"]]}
import {
  LoaderOptions,
} from "@medusajs/framework/types"
import { 
  ContainerRegistrationKeys,
} from "@medusajs/framework/utils"

export default async function helloWorldLoader({
  container,
}: LoaderOptions) {
  const logger = container.resolve(ContainerRegistrationKeys.LOGGER)

  logger.info("[helloWorldLoader]: Hello, World!")
}
```


# Perform Database Operations in a Service

In this chapter, you'll learn how to perform database operations in a module's service.

This chapter is intended for more advanced database use-cases where you need more control over queries and operations. For basic database operations, such as creating or retrieving data of a model, use the [Service Factory](https://docs.medusajs.com/learn/fundamentals/modules/service-factory/index.html.md) instead.

## Run Queries

[MikroORM's entity manager](https://mikro-orm.io/docs/entity-manager) is a class that has methods to run queries on the database and perform operations.

Medusa provides an `InjectManager` decorator from the Modules SDK that injects a service's method with a [forked entity manager](https://mikro-orm.io/docs/identity-map#forking-entity-manager).

So, to run database queries in a service:

1. Add the `InjectManager` decorator to the method.
2. Add as a last parameter an optional `sharedContext` parameter that has the `MedusaContext` decorator from the Modules SDK. This context holds database-related context, including the manager injected by `InjectManager`

For example, in your service, add the following methods:

```ts highlights={methodsHighlight}
// other imports...
import { 
  InjectManager,
  MedusaContext,
} from "@medusajs/framework/utils"
import { Context } from "@medusajs/framework/types"
import { EntityManager } from "@mikro-orm/knex"

class BlogModuleService {
  // ...

  @InjectManager()
  async getCount(
    @MedusaContext() sharedContext?: Context<EntityManager>
  ): Promise<number | undefined> {
    return await sharedContext?.manager?.count("my_custom")
  }
  
  @InjectManager()
  async getCountSql(
    @MedusaContext() sharedContext?: Context<EntityManager>
  ): Promise<number> {
    const data = await sharedContext?.manager?.execute(
      "SELECT COUNT(*) as num FROM my_custom"
    ) 
    
    return parseInt(data?.[0].num || 0)
  }
}
```

You add two methods `getCount` and `getCountSql` that have the `InjectManager` decorator. Each of the methods also accept the `sharedContext` parameter which has the `MedusaContext` decorator.

The entity manager is injected to the `sharedContext.manager` property, which is an instance of [EntityManager from the @mikro-orm/knex package](https://mikro-orm.io/api/knex/class/EntityManager).

You use the manager in the `getCount` method to retrieve the number of records in a table, and in the `getCountSql` to run a PostgreSQL query that retrieves the count.

Refer to [MikroORM's reference](https://mikro-orm.io/api/knex/class/EntityManager) for a full list of the entity manager's methods.

***

## Perform Database Operations

There are two ways to perform database operations in transactional methods:

1. Using the [data model repositories](#perform-database-operations-with-data-model-repositories) in your module.
2. Using the [transactional entity manager](#perform-database-operations-with-the-transactional-entity-manager) injected into the method's shared context.

For both approaches, you must wrap the method performing the database operations in a transaction.

### Wrap Database Operations in Transactions

When performing database operations without using the [Service Factory](https://docs.medusajs.com/learn/fundamentals/modules/service-factory/index.html.md), you must wrap the method performing the database operations in a transaction.

To wrap database operations in a transaction, you create two methods:

1. A private or protected method that's wrapped in a transaction. To wrap it in a transaction, you use the `InjectTransactionManager` decorator from the Modules SDK.
2. A public method that calls the transactional method. You use on it the `InjectManager` decorator as explained in the previous section.

Both methods must accept as a last parameter an optional `sharedContext` parameter that has the `MedusaContext` decorator from the Modules SDK. It holds database-related contexts passed through the Medusa application.

For example:

```ts highlights={opHighlights}
import { 
  InjectManager,
  InjectTransactionManager,
  MedusaContext,
} from "@medusajs/framework/utils"
import { Context } from "@medusajs/framework/types"
import { EntityManager } from "@mikro-orm/knex"

class BlogModuleService {
  // ...
  @InjectTransactionManager()
  protected async update_(
    input: {
      id: string,
      name: string
    },
    @MedusaContext() sharedContext?: Context<EntityManager>
  ): Promise<any> {
    const transactionManager = sharedContext?.transactionManager
    
    // TODO: update the record
  }

  @InjectManager()
  async update(
    input: {
      id: string,
      name: string
    },
    @MedusaContext() sharedContext?: Context<EntityManager>
  ) {
    return await this.update_(input, sharedContext)
  }
}
```

The `BlogModuleService` has two methods:

- A protected `update_` that performs the database operations inside a transaction.
- A public `update` that executes the transactional protected method.

You can then perform in the transactional method the database operations either using the [data model repository](#perform-database-operations-with-data-model-repositories) or the [transactional entity manager](#perform-database-operations-with-the-transactional-entity-manager).

#### Why Wrap a Transactional Method

The variables in the transactional method (such as `update_`) hold values that are uncommitted to the database. They're only committed once the method finishes execution.

So, if in your method you perform database operations, then use their result to perform other actions, such as connecting to a third-party service, you'll be working with uncommitted data.

By placing only the database operations in a method that has the `InjectTransactionManager` and using it in a wrapper method, the wrapper method receives the committed result of the transactional method.

This is also useful if you perform heavy data normalization outside of the database operations. In that case, you don't hold the transaction for a longer time than needed.

For example, the `update` method may call other methods than `update_` to perform other actions:

```ts
// other imports...
import { 
  InjectManager,
  InjectTransactionManager,
  MedusaContext,
} from "@medusajs/framework/utils"
import { Context } from "@medusajs/framework/types"
import { EntityManager } from "@mikro-orm/knex"

class BlogModuleService {
  // ...
  @InjectTransactionManager()
  protected async update_(
    // ...
  ): Promise<any> {
    // ...
  }
  @InjectManager()
  async update(
    input: {
      id: string,
      name: string
    },
    @MedusaContext() sharedContext?: Context<EntityManager>
  ) {
    const newData = await this.update_(input, sharedContext)

    // example method that sends data to another system
    await this.sendNewDataToSystem(newData)

    return newData
  }
}
```

In this case, only the `update_` method is wrapped in a transaction. The returned value `newData` holds the committed result, which can be used for other operations, such as passed to a `sendNewDataToSystem` method.

#### Using Methods in Transactional Methods

If your protected transactional method uses other methods that accept a Medusa context, pass the shared context to those methods.

For example:

```ts highlights={anotherMethodHighlights}
// other imports...
import { 
  InjectTransactionManager,
  MedusaContext,
} from "@medusajs/framework/utils"
import { Context } from "@medusajs/framework/types"
import { EntityManager } from "@mikro-orm/knex"

class BlogModuleService {
  // ...
  @InjectTransactionManager()
  protected async anotherMethod(
    @MedusaContext() sharedContext?: Context<EntityManager>
  ) {
    // ...
  }
  
  @InjectTransactionManager()
  protected async update_(
    input: {
      id: string,
      name: string
    },
    @MedusaContext() sharedContext?: Context<EntityManager>
  ): Promise<any> {
    this.anotherMethod(sharedContext)
  }
}
```

You use the `anotherMethod` transactional method in the `update_` transactional method, so you pass it the shared context.

The `anotherMethod` now runs in the same transaction as the `update_` method.

### Perform Database Operations with Data Model Repositories

For every data model in your module, Medusa generates a data model repository that has methods to perform database operations.

For example, if your module has a `Post` model, it has a `postRepository` in the container.

The data model repository is a wrapper around the [entity manager](https://mikro-orm.io/api/knex/class/EntityManager) that provides a higher-level API for performing database operations.

To use the low-level entity manager, use the [transactional entity manager](#perform-database-operations-with-the-transactional-entity-manager) instead.

#### Resolve Data Model Repository

When the Medusa application injects a data model repository into a module's container, it formats the registration name by:

- Taking the data model's name that's passed as the first parameter of `model.define`
- Lower-casing the first character
- Suffixing the result with `Repository`.

For example:

- `Post` model: `postRepository`
- `My_Custom` model: `my_CustomRepository`

So, to resolve a data model repository from a module's container, pass the expected registration name of the repository in the first parameter of the module's constructor (the container).

For example:

### Extending Service Factory

```ts highlights={serviceFactoryRepoHighlights}
import { MedusaService } from "@medusajs/framework/utils"
import { InferTypeOf, DAL } from "@medusajs/framework/types"
import Post from "./models/post"

type Post = InferTypeOf<typeof Post>

type InjectedDependencies = {
  postRepository: DAL.RepositoryService<Post>
}

class BlogModuleService extends MedusaService({
  Post,
}){
  protected postRepository_: DAL.RepositoryService<Post>

  constructor({ 
    postRepository, 
  }: InjectedDependencies) {
    super(...arguments)
    this.postRepository_ = postRepository
  }
}

export default BlogModuleService
```

### Without Service Factory

```ts highlights={noServiceFactoryRepoHighlights}
import { InferTypeOf, DAL } from "@medusajs/framework/types"
import Post from "./models/post"

type Post = InferTypeOf<typeof Post>

type InjectedDependencies = {
  postRepository: DAL.RepositoryService<Post>
}

class BlogModuleService {
  protected postRepository_: DAL.RepositoryService<Post>

  constructor({ 
    postRepository, 
  }: InjectedDependencies) {
    super(...arguments)
    this.postRepository_ = postRepository
  }
}

export default BlogModuleService
```

You can then use the data model repository in your service to perform database operations.

#### Data Model Repository Methods

A data model repository has methods that allows you to create, update, and delete records, among other operations.

To learn about the methods available in a data model repository, refer to the [Data Model Repository](https://docs.medusajs.com/resources/data-model-repository-reference/index.html.md) reference.

### Perform Database Operations with the Transactional Entity Manager

Your transactional method can use the transactional entity manager injected into the method's shared context to perform database operations. It's an instance of the [MikroORM EntityManager](https://mikro-orm.io/api/knex/class/EntityManager) class.

To use an easier higher-level API focused on each data model, use the [data model repository](#perform-database-operations-with-data-model-repositories) instead.

For example:

```ts highlights={transactionalEntityManagerHighlights}
import { 
  InjectManager,
  InjectTransactionManager,
  MedusaContext,
} from "@medusajs/framework/utils"
import { Context } from "@medusajs/framework/types"
import { EntityManager } from "@mikro-orm/knex"

class BlogModuleService {
  // ...
  @InjectTransactionManager()
  protected async update_(
    input: {
      id: string,
      name: string
    },
    @MedusaContext() sharedContext?: Context<EntityManager>
  ): Promise<any> {
    const transactionManager = sharedContext?.transactionManager
    await transactionManager?.nativeUpdate(
      "my_custom",
      {
        id: input.id,
      },
      {
        name: input.name,
      }
    )

    // retrieve again
    const updatedRecord = await transactionManager?.execute(
      `SELECT * FROM my_custom WHERE id = '${input.id}'`
    )

    return updatedRecord
  }

  @InjectManager()
  async update(
    input: {
      id: string,
      name: string
    },
    @MedusaContext() sharedContext?: Context<EntityManager>
  ) {
    return await this.update_(input, sharedContext)
  }
}
```

The `update_` method uses the transactional entity manager injected into the `sharedContext.transactionManager` property to perform the database operations.

Find all available methods in the [MikroORM EntityManager](https://mikro-orm.io/api/knex/class/EntityManager) reference.

***

## Configure Transactions with the Base Repository

To configure the transaction, such as its [isolation level](https://www.postgresql.org/docs/current/transaction-iso.html), use the `baseRepository` class registered in your module's container.

The `baseRepository` is an instance of a repository class that provides methods to create transactions, run database operations, and more.

The `baseRepository` has a `transaction` method that allows you to run a function within a transaction and configure that transaction.

For example, resolve the `baseRepository` in your service's constructor:

### Extending Service Factory

```ts highlights={baseRepoHighlights}
import { MedusaService } from "@medusajs/framework/utils"
import Post from "./models/post"
import { DAL } from "@medusajs/framework/types"

type InjectedDependencies = {
  baseRepository: DAL.RepositoryService
}

class BlogModuleService extends MedusaService({
  Post,
}){
  protected baseRepository_: DAL.RepositoryService

  constructor({ baseRepository }: InjectedDependencies) {
    super(...arguments)
    this.baseRepository_ = baseRepository
  }
}

export default BlogModuleService
```

### Without Service Factory

```ts highlights={noServiceFactoryBaseRepoHighlights}
import { DAL } from "@medusajs/framework/types"

type InjectedDependencies = {
  baseRepository: DAL.RepositoryService
}

class BlogModuleService {
  protected baseRepository_: DAL.RepositoryService

  constructor({ baseRepository }: InjectedDependencies) {
    this.baseRepository_ = baseRepository
  }
}

export default BlogModuleService
```

Then, use it in the service's transactional methods:

```ts highlights={repoHighlights}
// ...
import { 
  InjectManager,
  InjectTransactionManager,
  MedusaContext,
} from "@medusajs/framework/utils"
import { Context } from "@medusajs/framework/types"
import { EntityManager } from "@mikro-orm/knex"

class BlogModuleService {
  // ...
  @InjectTransactionManager()
  protected async update_(
    input: {
      id: string,
      name: string
    },
    @MedusaContext() sharedContext?: Context<EntityManager>
  ): Promise<any> {
    return await this.baseRepository_.transaction(
      async (transactionManager) => {
        await transactionManager.nativeUpdate(
          "my_custom",
          {
            id: input.id,
          },
          {
            name: input.name,
          }
        )

        // retrieve again
        const updatedRecord = await transactionManager.execute(
          `SELECT * FROM my_custom WHERE id = '${input.id}'`
        )

        return updatedRecord
      },
      {
        transaction: sharedContext?.transactionManager,
      }
    )
  }

  @InjectManager()
  async update(
    input: {
      id: string,
      name: string
    },
    @MedusaContext() sharedContext?: Context<EntityManager>
  ) {
    return await this.update_(input, sharedContext)
  }
}
```

The `update_` method uses the `baseRepository_.transaction` method to wrap a function in a transaction.

The function parameter receives a transactional entity manager as a parameter. Use it to perform the database operations.

The `baseRepository_.transaction` method also receives as a second parameter an object of options. You must pass in it the `transaction` property and set its value to the `sharedContext.transactionManager` property so that the function wrapped in the transaction uses the injected transaction manager.

Refer to [MikroORM's reference](https://mikro-orm.io/api/knex/class/EntityManager) for a full list of the entity manager's methods.

### Transaction Options

The second parameter of the `baseRepository_.transaction` method is an object of options that accepts the following properties:

1. `transaction`: Set the transactional entity manager passed to the function. You must provide this option as explained in the previous section.

```ts highlights={[["24"]]}
// other imports...
import { EntityManager } from "@mikro-orm/knex"
import { 
  InjectTransactionManager,
  MedusaContext,
} from "@medusajs/framework/utils"
import { Context } from "@medusajs/framework/types"

class BlogModuleService {
  // ...
  @InjectTransactionManager()
  async update_(
    input: {
      id: string,
      name: string
    },
    @MedusaContext() sharedContext?: Context<EntityManager>
  ): Promise<any> {
    return await this.baseRepository_.transaction<EntityManager>(
      async (transactionManager) => {
        // ...
      },
      {
        transaction: sharedContext?.transactionManager,
      }
    )
  }
}
```

2. `isolationLevel`: Sets the transaction's [isolation level](https://www.postgresql.org/docs/current/transaction-iso.html). Its values can be:
   - `read committed`
   - `read uncommitted`
   - `snapshot`
   - `repeatable read`
   - `serializable`

```ts highlights={[["25"]]}
// other imports...
import { 
  InjectTransactionManager,
  MedusaContext,
} from "@medusajs/framework/utils"
import { Context } from "@medusajs/framework/types"
import { EntityManager } from "@mikro-orm/knex"
import { IsolationLevel } from "@mikro-orm/core"

class BlogModuleService {
  // ...
  @InjectTransactionManager()
  async update_(
    input: {
      id: string,
      name: string
    },
    @MedusaContext() sharedContext?: Context<EntityManager>
  ): Promise<any> {
    return await this.baseRepository_.transaction<EntityManager>(
      async (transactionManager) => {
        // ...
      },
      {
        isolationLevel: IsolationLevel.READ_COMMITTED,
      }
    )
  }
}
```

3. `enableNestedTransactions`: (default: `false`) whether to allow using nested transactions.
   - If `transaction` is provided and this is disabled, the manager in `transaction` is re-used.

```ts highlights={[["24"]]}
// other imports...
import { 
  InjectTransactionManager,
  MedusaContext,
} from "@medusajs/framework/utils"
import { Context } from "@medusajs/framework/types"
import { EntityManager } from "@mikro-orm/knex"

class BlogModuleService {
  // ...
  @InjectTransactionManager()
  async update_(
    input: {
      id: string,
      name: string
    },
    @MedusaContext() sharedContext?: Context<EntityManager>
  ): Promise<any> {
    return await this.baseRepository_.transaction<EntityManager>(
      async (transactionManager) => {
        // ...
      },
      {
        enableNestedTransactions: false,
      }
    )
  }
}
```


# Infrastructure Modules

In this chapter, you’ll learn about Infrastructure Modules.

## What is an Infrastructure Module?

An Infrastructure Module implements features and mechanisms related to the Medusa application’s architecture and infrastructure.

Since modules are interchangeable, you have more control over Medusa’s architecture. For example, you can choose to use Memcached for event handling instead of Redis.

***

## Infrastructure Module Types

There are different Infrastructure Module types including:

![Diagram illustrating how the modules connect to third-party services](https://res.cloudinary.com/dza7lstvk/image/upload/v1727095814/Medusa%20Book/architectural-modules_bj9bb9.jpg)

- Analytics Module: Integrates a third-party service to track and analyze user interactions and system events.
- Cache Module: Defines the caching mechanism or logic to cache computational results.
- Event Module: Integrates a pub/sub service to handle subscribing to and emitting events.
- Workflow Engine Module: Integrates a service to store and track workflow executions and steps.
- File Module: Integrates a storage service to handle uploading and managing files.
- Notification Module: Integrates a third-party service or defines custom logic to send notifications to users and customers.
- Locking Module: Integrates a service that manages access to shared resources by multiple processes or threads.

***

## Infrastructure Modules List

Refer to the [Infrastructure Modules reference](https://docs.medusajs.com/resources/infrastructure-modules/index.html.md) for a list of Medusa’s Infrastructure Modules, available modules to install, and how to create an Infrastructure Module.


# Module Isolation

In this chapter, you'll learn how modules are isolated, and what that means for your custom development.

- Modules can't access resources, such as services or data models, from other modules.
- Use [Module Links](https://docs.medusajs.com/learn/fundamentals/module-links/index.html.md) to extend an existing module's data models, and [Query](https://docs.medusajs.com/learn/fundamentals/module-links/query/index.html.md) to retrieve data across modules.
- Use [workflows](https://docs.medusajs.com/learn/fundamentals/workflows/index.html.md) to build features that depend on functionalities from different modules.

## How are Modules Isolated?

A module is unaware of any resources other than its own, such as services or data models. This means it can't access these resources if they're implemented in another module.

For example, your custom module can't resolve the Product Module's main service or have direct relationships from its data model to the Product Module's data models.

A module has its own container, as explained in the [Module Container](https://docs.medusajs.com/learn/fundamentals/modules/container/index.html.md) chapter. This container includes the module's resources, such as services and data models, and some Framework resources that the Medusa application provides.

Refer to the [Module Container Resources](https://docs.medusajs.com/resources/medusa-container-resources/index.html.md) for a list of resources registered in a module's container.

***

## Why are Modules Isolated

Some of the module isolation's benefits include:

- Integrate your module into any Medusa application without side-effects to your setup.
- Replace existing modules with your custom implementation if your use case is drastically different.
- Use modules in other environments, such as Edge functions and Next.js apps.

***

## How to Extend Data Model of Another Module?

To extend the data model of another module, such as the `Product` data model of the Product Module, use [Module Links](https://docs.medusajs.com/learn/fundamentals/module-links/index.html.md). Module Links allow you to build associations between data models of different modules without breaking the module isolation.

Then, you can retrieve data across modules using [Query](https://docs.medusajs.com/learn/fundamentals/module-links/query/index.html.md).

***

## How to Use Services of Other Modules?

You'll often build feature that uses functionalities from different modules. For example, if you may need to retrieve brands, then sync them to a third-party service.

To build functionalities spanning across modules and systems, create a [workflow](https://docs.medusajs.com/learn/fundamentals/workflows/index.html.md) whose steps resolve the modules' services to perform these functionalities.

Workflows ensure data consistency through their roll-back mechanism and tracking of each execution's status, steps, input, and output.

### Example

For example, consider you have two modules:

1. A module that stores and manages brands in your application.
2. A module that integrates a third-party Content Management System (CMS).

To sync brands from your application to the third-party system, create the following steps:

```ts title="Example Steps" highlights={stepsHighlights}
const retrieveBrandsStep = createStep(
  "retrieve-brands",
  async (_, { container }) => {
    const brandModuleService = container.resolve(
      "brand"
    )

    const brands = await brandModuleService.listBrands()

    return new StepResponse(brands)
  }
)

const createBrandsInCmsStep = createStep(
  "create-brands-in-cms",
  async ({ brands }, { container }) => {
    const cmsModuleService = container.resolve(
      "cms"
    )

    const cmsBrands = await cmsModuleService.createBrands(brands)

    return new StepResponse(cmsBrands, cmsBrands)
  },
  async (brands, { container }) => {
    const cmsModuleService = container.resolve(
      "cms"
    )

    await cmsModuleService.deleteBrands(
      brands.map((brand) => brand.id)
    )
  }
)
```

The `retrieveBrandsStep` retrieves the brands from a Brand Module, and the `createBrandsInCmsStep` creates the brands in a third-party system using a CMS Module.

Then, create the following workflow that uses these steps:

```ts title="Example Workflow"
export const syncBrandsWorkflow = createWorkflow(
  "sync-brands",
  () => {
    const brands = retrieveBrandsStep()

    createBrandsInCmsStep({ brands })
  }
)
```

You can then use this workflow in an API route, scheduled job, or other resources that use this functionality.

***

## How to Use Framework APIs and Tools in Module?

### Framework Tools in Module Container

A module has in its container some Framework APIs and tools, such as [Logger](https://docs.medusajs.com/learn/debugging-and-testing/logging/index.html.md). You can refer to the [Module Container Resources](https://docs.medusajs.com/resources/medusa-container-resources/index.html.md) for a list of resources registered in a module's container.

You can resolve those resources in the module's services and loaders.

For example:

```ts title="Example Service"
import { Logger } from "@medusajs/framework/types"

type InjectedDependencies = {
  logger: Logger
}

export default class BlogModuleService {
  protected logger_: Logger

  constructor({ logger }: InjectedDependencies) {
    this.logger_ = logger

    this.logger_.info("[BlogModuleService]: Hello World!")
  }

  // ...
}
```

In this example, the `BlogModuleService` class resolves the `Logger` service from the module's container and uses it to log a message.

### Using Framework Tools in Workflows

Some Framework APIs and tools are not registered in the module's container. For example, [Query](https://docs.medusajs.com/learn/fundamentals/module-links/query/index.html.md) is only registered in the Medusa container.

You should, instead, build workflows that use these APIs and tools along with your module's service.

For example, you can create a workflow that retrieves data using Query, then pass the data to your module's service to perform some action.

```ts title="Example Workflow"
import { createWorkflow, createStep } from "@medusajs/framework/workflows-sdk"
import { useQueryGraphStep } from "@medusajs/medusa/core-flows"

const createBrandsInCmsStep = createStep(
  "create-brands-in-cms",
  async ({ brands }, { container }) => {
    const cmsModuleService = container.resolve(
      "cms"
    )

    const cmsBrands = await cmsModuleService.createBrands(brands)

    return new StepResponse(cmsBrands, cmsBrands)
  },
  async (brands, { container }) => {
    const cmsModuleService = container.resolve(
      "cms"
    )

    await cmsModuleService.deleteBrands(
      brands.map((brand) => brand.id)
    )
  }
)

const syncBrandsWorkflow = createWorkflow(
  "sync-brands",
  () => {
    const { data: brands } = useQueryGraphStep({
      entity: "brand",
      fields: [
        "*",
        "products.*",
      ],
    })

    createBrandsInCmsStep({ brands })
  }
)
```

In this example, you use the `useQueryGraphStep` to retrieve brands with their products, then pass the brands to the `createBrandsInCmsStep` step.

In the `createBrandsInCmsStep`, you resolve the CMS Module's service from the module's container and use it to create the brands in the third-party system. You pass the brands you retrieved using Query to the module's service.

### Injecting Dependencies to Module

Some cases still require you to access external resources, mainly [Infrastructure Modules](https://docs.medusajs.com/resources/infrastructure-modules/index.html.md) or Framework tools, in your module.
For example, you may need the [Event Module](https://docs.medusajs.com/resources/infrastructure-modules/event/index.html.md) to emit events from your module's service.

In those cases, you can inject the dependencies to your module's service in `medusa-config.ts` using the `dependencies` property of the module's configuration.

Use this approach only when absolutely necessary, where workflows aren't sufficient for your use case. By injecting dependencies, you risk breaking your module if the dependency isn't provided, or if the dependency's API changes.

For example:

```ts title="medusa-config.ts"
import { Modules } from "@medusajs/framework/utils"

module.exports = defineConfig({
  // ...
  modules: [
    {
      resolve: "./src/modules/blog",
      dependencies: [
        Modules.EVENT_BUS,
      ],
    },
  ],
})
```

In this example, you inject the Event Module's service to your module's container.

Only the main service will be injected into the module's container.

You can then use the Event Module's service in your module's service:

```ts title="Example Service"
class BlogModuleService {
  protected eventBusService_: AbstractEventBusModuleService

  constructor({ event_bus }) {
    this.eventBusService_ = event_bus
  }

  performAction() {
    // TODO perform action

    this.eventBusService_.emit({
      name: "custom.event",
      data: {
        id: "123",
        // other data payload
      },
    })
  }
}
```


# Loaders

In this chapter, you’ll learn about loaders and how to use them.

## What is a Loader?

When building a commerce application, you'll often need to execute an action the first time the application starts. For example, if your application needs to connect to databases other than Medusa's PostgreSQL database, you might need to establish a connection on application startup.

In Medusa, you can execute an action when the application starts using a loader. A loader is a function exported by a [module](https://docs.medusajs.com/learn/fundamentals/modules/index.html.md), which is a package of business logic for a single domain. When the Medusa application starts, it executes all loaders exported by configured modules.

Loaders are useful to register custom resources, such as database connections, in the [module's container](https://docs.medusajs.com/learn/fundamentals/modules/container/index.html.md), which is similar to the [Medusa container](https://docs.medusajs.com/learn/fundamentals/medusa-container/index.html.md) but includes only [resources available to the module](https://docs.medusajs.com/resources/medusa-container-resources#module-container-resources/index.html.md). Modules are isolated, so they can't access resources outside of them, such as a service in another module.

Medusa isolates modules to ensure that they're re-usable across applications, aren't tightly coupled to other resources, and don't have implications when integrated into the Medusa application. Learn more about why modules are isolated in [this chapter](https://docs.medusajs.com/learn/fundamentals/modules/isolation/index.html.md), and check out [this reference for the list of resources in the module's container](https://docs.medusajs.com/resources/medusa-container-resources#module-container-resources/index.html.md).

***

## How to Create a Loader?

### 1. Implement Loader Function

You create a loader function in a TypeScript or JavaScript file under a module's `loaders` directory.

For example, consider you have a `hello` module, you can create a loader at `src/modules/hello/loaders/hello-world.ts` with the following content:

![Example of loader file in the application's directory structure](https://res.cloudinary.com/dza7lstvk/image/upload/v1732865671/Medusa%20Book/loader-dir-overview_eg6vtu.jpg)

Learn how to create a module in [this chapter](https://docs.medusajs.com/learn/fundamentals/modules/index.html.md).

```ts title="src/modules/hello/loaders/hello-world.ts"
import {
  LoaderOptions,
} from "@medusajs/framework/types"

export default async function helloWorldLoader({
  container,
}: LoaderOptions) {
  const logger = container.resolve("logger")

  logger.info("[HELLO MODULE] Just started the Medusa application!")
}
```

The loader file exports an async function, which is the function executed when the application loads.

The function receives an object parameter that has a `container` property, which is the module's container that you can use to resolve resources from. In this example, you resolve the Logger utility to log a message in the terminal.

Find the list of resources in the module's container in [this reference](https://docs.medusajs.com/resources/medusa-container-resources#module-container-resources/index.html.md).

### 2. Export Loader in Module Definition

After implementing the loader, you must export it in the module's definition in the `index.ts` file at the root of the module's directory. Otherwise, the Medusa application will not run it.

So, to export the loader you implemented above in the `hello` module, add the following to `src/modules/hello/index.ts`:

```ts title="src/modules/hello/index.ts"
// other imports...
import helloWorldLoader from "./loaders/hello-world"

export default Module("hello", {
  // ...
  loaders: [helloWorldLoader],
})
```

The second parameter of the `Module` function accepts a `loaders` property whose value is an array of loader functions. The Medusa application will execute these functions when it starts.

### Test the Loader

Assuming your module is [added to Medusa's configuration](https://docs.medusajs.com/learn/fundamentals/modules#4-add-module-to-medusas-configurations/index.html.md), you can test the loader by starting the Medusa application:

```bash npm2yarn
npm run dev
```

Then, you'll find the following message logged in the terminal:

```plain
info:   [HELLO MODULE] Just started the Medusa application!
```

This indicates that the loader in the `hello` module ran and logged this message.

***

## When are Loaders Executed?

### Loaders Executed on Application Startup

When you start the Medusa application, it executes the loaders of all modules in their registration order.

A loader is executed before the module's main service is instantiated. So, you can use loaders to register in the module's container resources that you want to use in the module's service. For example, you can register a database connection.

Loaders are also useful to only load a module if a certain condition is met. For example, if you try to connect to a database in a loader but the connection fails, you can throw an error in the loader to prevent the module from being loaded. This is useful if your module depends on an external service to work.

### Loaders Executed with Migrations

Loaders are also executed when you run [migrations](https://docs.medusajs.com/learn/fundamentals/data-models/write-migration/index.html.md). This can be useful if you need to run some task before the migrations, or you want to migrate some data to an integrated third-party system as part of the migration process.

***

## Example: Register Custom MongoDB Connection

As mentioned in this chapter's introduction, loaders are most useful when you need to register a custom resource in the module's container to re-use it in other customizations in the module.

Consider your have a MongoDB module that allows you to perform operations on a MongoDB database.

### Prerequisites

- [MongoDB database that you can connect to from a local machine.](https://www.mongodb.com)
- [Install the MongoDB SDK in your Medusa application.](https://www.mongodb.com/docs/drivers/node/current/quick-start/download-and-install/#install-the-node.js-driver)

To connect to the database, you create the following loader in your module:

```ts title="src/modules/mongo/loaders/connection.ts" highlights={loaderHighlights}
import { LoaderOptions } from "@medusajs/framework/types"
import { asValue } from "awilix"
import { MongoClient } from "mongodb"

type ModuleOptions = {
  connection_url?: string
  db_name?: string
}

export default async function mongoConnectionLoader({
  container,
  options,
}: LoaderOptions<ModuleOptions>) {
  if (!options.connection_url) {
    throw new Error(`[MONGO MDOULE]: connection_url option is required.`)
  }
  if (!options.db_name) {
    throw new Error(`[MONGO MDOULE]: db_name option is required.`)
  }
  const logger = container.resolve("logger")
  
  try {
    const clientDb = (
      await (new MongoClient(options.connection_url)).connect()
    ).db(options.db_name)

    logger.info("Connected to MongoDB")

    container.register(
      "mongoClient",
      asValue(clientDb)
    )
  } catch (e) {
    logger.error(
      `[MONGO MDOULE]: An error occurred while connecting to MongoDB: ${e}`
    )
  }
}
```

The loader function accepts in its object parameter an `options` property, which is the options passed to the module in Medusa's configurations. For example:

```ts title="medusa-config.ts" highlights={optionHighlights}
module.exports = defineConfig({
  // ...
  modules: [
    {
      resolve: "./src/modules/mongo",
      options: {
        connection_url: process.env.MONGO_CONNECTION_URL,
        db_name: process.env.MONGO_DB_NAME,
      },
    },
  ],
})
```

Passing options is useful when your module needs informations like connection URLs or API keys, as it ensures your module can be re-usable across applications. For the MongoDB Module, you expect two options:

- `connection_url`: the URL to connect to the MongoDB database.
- `db_name`: The name of the database to connect to.

In the loader, you check first that these options are set before proceeding. Then, you create an instance of the MongoDB client and connect to the database specified in the options.

After creating the client, you register it in the module's container using the container's `register` method. The method accepts two parameters:

1. The key to register the resource under, which in this case is `mongoClient`. You'll use this name later to resolve the client.
2. The resource to register in the container, which is the MongoDB client you created. However, you don't pass the resource as-is. Instead, you need to use an `asValue` function imported from the [awilix package](https://github.com/jeffijoe/awilix), which is the package used to implement the container functionality in Medusa.

### Use Custom Registered Resource in Module's Service

After registering the custom MongoDB client in the module's container, you can now resolve and use it in the module's service.

For example:

```ts title="src/modules/mongo/service.ts"
import type { Db } from "mongodb"

type InjectedDependencies = {
  mongoClient: Db
}

export default class MongoModuleService {
  private mongoClient_: Db

  constructor({ mongoClient }: InjectedDependencies) {
    this.mongoClient_ = mongoClient
  }

  async createMovie({ title }: {
    title: string
  }) {
    const moviesCol = this.mongoClient_.collection("movie")

    const insertedMovie = await moviesCol.insertOne({
      title,
    })

    const movie = await moviesCol.findOne({
      _id: insertedMovie.insertedId,
    })

    return movie
  }

  async deleteMovie(id: string) {
    const moviesCol = this.mongoClient_.collection("movie")

    await moviesCol.deleteOne({
      _id: {
        equals: id,
      },
    })
  }
}
```

The service `MongoModuleService` resolves the `mongoClient` resource you registered in the loader and sets it as a class property. You then use it in the `createMovie` and `deleteMovie` methods, which create and delete a document in a `movie` collection in the MongoDB database, respectively.

Make sure to export the loader in the module's definition in the `index.ts` file at the root directory of the module:

```ts title="src/modules/mongo/index.ts" highlights={[["9"]]}
import { Module } from "@medusajs/framework/utils"
import MongoModuleService from "./service"
import mongoConnectionLoader from "./loaders/connection"

export const MONGO_MODULE = "mongo"

export default Module(MONGO_MODULE, {
  service: MongoModuleService,
  loaders: [mongoConnectionLoader],
})
```

### Test it Out

You can test the connection out by starting the Medusa application. If it's successful, you'll see the following message logged in the terminal:

```bash
info:    Connected to MongoDB
```

You can now resolve the MongoDB Module's main service in your customizations to perform operations on the MongoDB database.


# Modules Directory Structure

In this document, you'll learn about the expected files and directories in your module.

![Module Directory Structure Example](https://res.cloudinary.com/dza7lstvk/image/upload/v1714379976/Medusa%20Book/modules-dir-overview_nqq7ne.jpg)

## index.ts

The `index.ts` file in the root of your module's directory is the only required file. It must export the module's definition as explained in a [previous chapter](https://docs.medusajs.com/learn/fundamentals/modules/index.html.md).

***

## service.ts

A module must have a main service. It's created in the `service.ts` file at the root of your module directory as explained in a [previous chapter](https://docs.medusajs.com/learn/fundamentals/modules/index.html.md).

***

## Other Directories

The following directories are optional and their content are explained more in the following chapters:

- `models`: Holds the data models representing tables in the database.
- `migrations`: Holds the migration files used to reflect changes on the database.
- `loaders`: Holds the scripts to run on the Medusa application's start-up.


# Module Options

In this chapter, you’ll learn about passing options to your module from the Medusa application’s configurations and using them in the module’s resources.

## What are Module Options?

A module can receive options to customize or configure its functionality. For example, if you’re creating a module that integrates a third-party service, you’ll want to receive the integration credentials in the options rather than adding them directly in your code.

***

## How to Pass Options to a Module?

To pass options to a module, add an `options` property to the module’s configuration in `medusa-config.ts`.

For example:

```ts title="medusa-config.ts"
module.exports = defineConfig({
  // ...
  modules: [
    {
      resolve: "./src/modules/blog",
      options: {
        capitalize: true,
      },
    },
  ],
})
```

The `options` property’s value is an object. You can pass any properties you want.

### Pass Options to a Module in a Plugin

If your module is part of a plugin, you can pass options to the module in the plugin’s configuration.

For example:

```ts title="medusa-config.ts"
import { defineConfig } from "@medusajs/framework/utils"
module.exports = defineConfig({
  plugins: [
    {
      resolve: "@myorg/plugin-name",
      options: {
        capitalize: true,
      },
    },
  ],
})
```

The `options` property in the plugin configuration is passed to all modules in a plugin.

***

## Access Module Options in Main Service

The module’s main service receives the module options as a second parameter.

For example:

```ts title="src/modules/blog/service.ts" highlights={[["12"], ["14", "options?: ModuleOptions"], ["17"], ["18"], ["19"]]}
import { MedusaService } from "@medusajs/framework/utils"
import Post from "./models/post"

// recommended to define type in another file
type ModuleOptions = {
  capitalize?: boolean
}

export default class BlogModuleService extends MedusaService({
  Post,
}){
  protected options_: ModuleOptions

  constructor({}, options?: ModuleOptions) {
    super(...arguments)

    this.options_ = options || {
      capitalize: false,
    }
  }

  // ...
}
```

***

## Access Module Options in Loader

The object that a module’s loaders receive as a parameter has an `options` property holding the module's options.

For example:

```ts title="src/modules/blog/loaders/hello-world.ts" highlights={[["11"], ["12", "ModuleOptions", "The type of expected module options."], ["16"]]}
import {
  LoaderOptions,
} from "@medusajs/framework/types"

// recommended to define type in another file
type ModuleOptions = {
  capitalize?: boolean
}

export default async function helloWorldLoader({
  options,
}: LoaderOptions<ModuleOptions>) {
  
  console.log(
    "[BLOG MODULE] Just started the Medusa application!",
    options
  )
}
```

***

## Validate Module Options

If you expect a certain option and want to throw an error if it's not provided or isn't valid, it's recommended to perform the validation in a loader. The module's service is only instantiated when it's used, whereas the loader runs the when the Medusa application starts.

So, by performing the validation in the loader, you ensure you can throw an error at an early point, rather than when the module is used.

For example, to validate that the Hello Module received an `apiKey` option, create the loader `src/modules/loaders/validate.ts`:

```ts title="src/modules/blog/loaders/validate.ts" 
import { LoaderOptions } from "@medusajs/framework/types"
import { MedusaError } from "@medusajs/framework/utils"

// recommended to define type in another file
type ModuleOptions = {
  apiKey?: string
}

export default async function validationLoader({
  options,
}: LoaderOptions<ModuleOptions>) {
  if (!options.apiKey) {
    throw new MedusaError(
      MedusaError.Types.INVALID_DATA,
      "Hello Module requires an apiKey option."
    )
  }
}
```

Then, export the loader in the module's definition file, as explained in [this chapter](https://docs.medusajs.com/learn/fundamentals/modules/loaders/index.html.md):

```ts title="src/modules/blog/index.ts"
// other imports...
import validationLoader from "./loaders/validate"

export const BLOG_MODULE = "blog"

export default Module(BLOG_MODULE, {
  // ...
  loaders: [validationLoader],
})
```

Now, when the Medusa application starts, the loader will run, validating the module's options and throwing an error if the `apiKey` option is missing.


# Multiple Services in a Module

In this chapter, you'll learn how to use multiple services in a module.

## Module's Main and Internal Services

A module has one main service only, which is the service exported in the module's definition.

However, you may use other services in your module to better organize your code or split functionalities. These are called internal services that can be resolved within your module, but not in external resources.

***

## How to Add an Internal Service

### 1. Create Service

To add an internal service, create it in the `services` directory of your module.

For example, create the file `src/modules/blog/services/client.ts` with the following content:

```ts title="src/modules/blog/services/client.ts"
export class ClientService {
  async getMessage(): Promise<string> {
    return "Hello, World!"
  }
}
```

### 2. Export Service in Index

Next, create an `index.ts` file under the `services` directory of the module that exports your internal services.

For example, create the file `src/modules/blog/services/index.ts` with the following content:

```ts title="src/modules/blog/services/index.ts"
export * from "./client"
```

This exports the `ClientService`.

### 3. Resolve Internal Service

Internal services exported in the `services/index.ts` file of your module are now registered in the container and can be resolved in other services in the module as well as loaders.

For example, in your main service:

```ts title="src/modules/blog/service.ts" highlights={[["5"], ["13"]]}
// other imports...
import { ClientService } from "./services"

type InjectedDependencies = {
  clientService: ClientService
}

class BlogModuleService extends MedusaService({
  Post,
}){
  protected clientService_: ClientService

  constructor({ clientService }: InjectedDependencies) {
    super(...arguments)
    this.clientService_ = clientService
  }
}
```

You can now use your internal service in your main service.

***

## Resolve Resources in Internal Service

Resolve dependencies from your module's container in the constructor of your internal service.

For example:

```ts
import { Logger } from "@medusajs/framework/types"

type InjectedDependencies = {
  logger: Logger
}

export class ClientService {
  protected logger_: Logger

  constructor({ logger }: InjectedDependencies) {
    this.logger_ = logger
  }
}
```

***

## Access Module Options

Your internal service can't access the module's options.

To retrieve the module's options, use the `configModule` registered in the module's container, which is the configurations in `medusa-config.ts`.

For example:

```ts
import { ConfigModule } from "@medusajs/framework/types"
import { BLOG_MODULE } from ".."

export type InjectedDependencies = {
  configModule: ConfigModule
}

export class ClientService {
  protected options: Record<string, any>

  constructor({ configModule }: InjectedDependencies) {
    const moduleDef = configModule.modules[BLOG_MODULE]

    if (typeof moduleDef !== "boolean") {
      this.options = moduleDef.options
    }
  }
}
```

The `configModule` has a `modules` property that includes all registered modules. Retrieve the module's configuration using its registration key.

If its value is not a `boolean`, set the service's options to the module configuration's `options` property.


# Service Constraints

This chapter lists constraints to keep in mind when creating a service.

## Use Async Methods

Medusa wraps service method executions to inject useful context or transactions. However, since Medusa can't detect whether the method is asynchronous, it always executes methods in the wrapper with the `await` keyword.

For example, if you have a synchronous `getMessage` method, and you use it in other resources like workflows, Medusa executes it as an async method:

```ts
await blogModuleService.getMessage()
```

So, make sure your service's methods are always async to avoid unexpected errors or behavior.

```ts highlights={[["8", "", "Method must be async."], ["13", "async", "Correct way of defining the method."]]}
import { MedusaService } from "@medusajs/framework/utils"
import Post from "./models/post"

class BlogModuleService extends MedusaService({
  Post,
}){
  // Don't
  getMessage(): string {
    return "Hello, World!"
  }

  // Do
  async getMessage(): Promise<string> {
    return "Hello, World!"
  }
}

export default BlogModuleService
```


# Service Factory

In this chapter, you’ll learn about what the service factory is and how to use it.

## What is the Service Factory?

Medusa provides a service factory that your module’s main service can extend.

The service factory generates data management methods for your data models in the database, so you don't have to implement these methods manually.

Your service provides data-management functionalities of your data models.

***

## How to Extend the Service Factory?

Medusa provides the service factory as a `MedusaService` function your service extends. The function creates and returns a service class with generated data-management methods.

For example, create the file `src/modules/blog/service.ts` with the following content:

```ts title="src/modules/blog/service.ts" highlights={highlights}
import { MedusaService } from "@medusajs/framework/utils"
import Post from "./models/post"

class BlogModuleService extends MedusaService({
  Post,
}){
  // TODO implement custom methods
}

export default BlogModuleService
```

### MedusaService Parameters

The `MedusaService` function accepts one parameter, which is an object of data models to generate data-management methods for.

In the example above, since the `BlogModuleService` extends `MedusaService`, it has methods to manage the `Post` data model, such as `createPosts`.

### Generated Methods

The service factory generates methods to manage the records of each of the data models provided in the first parameter in the database.

The method's names are the operation's name, suffixed by the data model's key in the object parameter passed to `MedusaService`.

For example, the following methods are generated for the service above:

Find a complete reference of each of the methods in [this documentation](https://docs.medusajs.com/resources/service-factory-reference/index.html.md)

### listPosts

### listPosts

This method retrieves an array of records based on filters and pagination configurations.

For example:

```ts
const posts = await blogModuleService
  .listPosts()

// with filters
const posts = await blogModuleService
  .listPosts({
    id: ["123"]
  })
```

### listAndCountPosts

### retrievePost

This method retrieves a record by its ID.

For example:

```ts
const post = await blogModuleService
  .retrievePost("123")
```

### retrievePost

### updatePosts

This method updates and retrieves records of the data model.

For example:

```ts
const post = await blogModuleService
  .updatePosts({
    id: "123",
    title: "test"
  })

// update multiple
const posts = await blogModuleService
  .updatePosts([
    {
      id: "123",
      title: "test"
    },
    {
      id: "321",
      title: "test 2"
    },
  ])

// use filters
const posts = await blogModuleService
  .updatePosts([
    {
      selector: {
        id: ["123", "321"]
      },
      data: {
        title: "test"
      }
    },
  ])
```

### createPosts

### softDeletePosts

This method soft-deletes records using an array of IDs or an object of filters.

For example:

```ts
await blogModuleService.softDeletePosts("123")

// soft-delete multiple
await blogModuleService.softDeletePosts([
  "123", "321"
])

// use filters
await blogModuleService.softDeletePosts({
  id: ["123", "321"]
})
```

### updatePosts

### deletePosts

### softDeletePosts

### restorePosts

### Using a Constructor

If you implement the `constructor` of your service, make sure to call `super` passing it `...arguments`.

For example:

```ts highlights={[["8"]]}
import { MedusaService } from "@medusajs/framework/utils"
import Post from "./models/post"

class BlogModuleService extends MedusaService({
  Post,
}){
  constructor() {
    super(...arguments)
  }
}

export default BlogModuleService
```


# Compensation Function

In this chapter, you'll learn what a compensation function is and how to add it to a step.

## What is a Compensation Function

A compensation function rolls back or undoes changes made by a step when an error occurs in the workflow.

For example, if a step creates a record, the compensation function deletes the record when an error occurs later in the workflow.

By using compensation functions, you provide a mechanism that guarantees data consistency in your application and across systems.

***

## How to add a Compensation Function?

A compensation function is passed as a second parameter to the `createStep` function.

For example, create the file `src/workflows/hello-world.ts` with the following content:

```ts title="src/workflows/hello-world.ts" highlights={[["15"], ["16"], ["17"]]} collapsibleLines="1-5" expandButtonLabel="Show Imports"
import { 
  createStep,
  StepResponse,
} from "@medusajs/framework/workflows-sdk"

const step1 = createStep(
  "step-1",
  async () => {
    const message = `Hello from step one!`

    console.log(message)

    return new StepResponse(message)
  },
  async () => {
    console.log("Oops! Rolling back my changes...")
  }
)
```

Each step can have a compensation function. The compensation function only runs if an error occurs throughout the workflow.

***

## Test the Compensation Function

Create a step in the same `src/workflows/hello-world.ts` file that throws an error:

```ts title="src/workflows/hello-world.ts"
const step2 = createStep(
  "step-2",
  async () => {
    throw new Error("Throwing an error...")
  }
)
```

Then, create a workflow that uses the steps:

```ts title="src/workflows/hello-world.ts" collapsibleLines="1-8" expandButtonLabel="Show Imports"
import {
  createWorkflow,
  WorkflowResponse,
} from "@medusajs/framework/workflows-sdk"
// other imports...

// steps...

const myWorkflow = createWorkflow(
  "hello-world", 
  function (input) {
  const str1 = step1()
  step2()

  return new WorkflowResponse({
    message: str1,
  })
})

export default myWorkflow
```

Finally, execute the workflow from an API route:

```ts title="src/api/workflow/route.ts" collapsibleLines="1-6" expandButtonLabel="Show Imports"
import type {
  MedusaRequest,
  MedusaResponse,
} from "@medusajs/framework/http"
import myWorkflow from "../../../workflows/hello-world"

export async function GET(
  req: MedusaRequest,
  res: MedusaResponse
) {
  const { result } = await myWorkflow(req.scope)
    .run()

  res.send(result)
}
```

Run the Medusa application and send a `GET` request to `/workflow`:

```bash
curl http://localhost:9000/workflow
```

In the console, you'll see:

- `Hello from step one!` logged in the terminal, indicating that the first step ran successfully.
- `Oops! Rolling back my changes...` logged in the terminal, indicating that the second step failed and the compensation function of the first step ran consequently.

***

## Pass Input to Compensation Function

If a step creates a record, the compensation function must receive the ID of the record to remove it.

To pass input to the compensation function, pass a second parameter in the `StepResponse` returned by the step.

For example:

```ts highlights={inputHighlights}
import { 
  createStep,
  StepResponse,
} from "@medusajs/framework/workflows-sdk"

const step1 = createStep(
  "step-1",
  async () => {
    return new StepResponse(
      `Hello from step one!`, 
      { message: "Oops! Rolling back my changes..." }
    )
  },
  async ({ message }) => {
    console.log(message)
  }
)
```

In this example, the step passes an object as a second parameter to `StepResponse`.

The compensation function receives the object and uses its `message` property to log a message.

***

## Resolve Resources from the Medusa Container

The compensation function receives an object second parameter. The object has a `container` property that you use to resolve resources from the Medusa container.

For example:

```ts
import { 
  createStep,
  StepResponse,
} from "@medusajs/framework/workflows-sdk"
import { ContainerRegistrationKeys } from "@medusajs/framework/utils"

const step1 = createStep(
  "step-1",
  async () => {
    return new StepResponse(
      `Hello from step one!`, 
      { message: "Oops! Rolling back my changes..." }
    )
  },
  async ({ message }, { container }) => {
    const logger = container.resolve(
      ContainerRegistrationKeys.LOGGER
    )

    logger.info(message)
  }
)
```

In this example, you use the `container` property in the second object parameter of the compensation function to resolve the logger.

You then use the logger to log a message.

***

## Handle Step Errors in Loops

This feature is only available after [Medusa v2.0.5](https://github.com/medusajs/medusa/releases/tag/v2.0.5).

Consider you have a module that integrates a third-party ERP system, and you're creating a workflow that deletes items in that ERP. You may have the following step:

```ts
// other imports...
import { promiseAll } from "@medusajs/framework/utils"

type StepInput = {
  ids: string[]
}

const step1 = createStep(
  "step-1",
  async ({ ids }: StepInput, { container }) => {
    const erpModuleService = container.resolve(
      ERP_MODULE
    )
    const prevData: unknown[] = []

    await promiseAll(
      ids.map(async (id) => {
        const data = await erpModuleService.retrieve(id)

        await erpModuleService.delete(id)

        prevData.push(id)
      })
    )

    return new StepResponse(ids, prevData)
  }
)
```

In the step, you loop over the IDs to retrieve the item's data, store them in a `prevData` variable, then delete them using the ERP Module's service. You then pass the `prevData` variable to the compensation function.

However, if an error occurs in the loop, the `prevData` variable won't be passed to the compensation function as the execution never reached the return statement.

To handle errors in the loop so that the compensation function receives the last version of `prevData` before the error occurred, you wrap the loop in a try-catch block. Then, in the catch block, you invoke and return the `StepResponse.permanentFailure` function:

```ts highlights={highlights}
try {
  await promiseAll(
    ids.map(async (id) => {
      const data = await erpModuleService.retrieve(id)

      await erpModuleService.delete(id)

      prevData.push(id)
    })
  )
} catch (e) {
  return StepResponse.permanentFailure(
    `An error occurred: ${e}`,
    prevData
  )
}
```

The `StepResponse.permanentFailure` fails the step and its workflow, triggering current and previous steps' compensation functions. The `permanentFailure` function accepts as a first parameter the error message, which is saved in the workflow's error details, and as a second parameter the data to pass to the compensation function.

So, if an error occurs during the loop, the compensation function will still receive the `prevData` variable to undo the changes made before the step failed.

For more details on error handling in workflows and steps, check the [Handling Errors chapter](https://docs.medusajs.com/learn/fundamentals/workflows/errors/index.html.md).


# Scheduled Jobs Number of Executions

In this chapter, you'll learn how to set a limit on the number of times a scheduled job is executed.

## numberOfExecutions Option

The export configuration object of the scheduled job accepts an optional property `numberOfExecutions`. Its value is a number indicating how many times the scheduled job can be executed during the Medusa application's runtime.

For example:

```ts highlights={highlights}
export default async function myCustomJob() {
  console.log("I'll be executed three times only.")
}

export const config = {
  name: "hello-world",
  // execute every minute
  schedule: "* * * * *",
  numberOfExecutions: 3,
}
```

The above scheduled job has the `numberOfExecutions` configuration set to `3`.

So, it'll only execute 3 times, each every minute, then it won't be executed anymore.

If you restart the Medusa application, the scheduled job will be executed again until reaching the number of executions specified.


# Expose a Workflow Hook

In this chapter, you'll learn how to expose a hook in your workflow.

## When to Expose a Hook

Your workflow is reusable in other applications, and you allow performing an external action at some point in your workflow.

Your workflow isn't reusable by other applications. Use a step that performs what a hook handler would instead.

***

## How to Expose a Hook in a Workflow?

To expose a hook in your workflow, use `createHook` from the Workflows SDK.

For example:

```ts title="src/workflows/my-workflow/index.ts" highlights={hookHighlights}
import {
  createStep,
  createHook,
  createWorkflow,
  WorkflowResponse,
} from "@medusajs/framework/workflows-sdk"
import { createProductStep } from "./steps/create-product"

export const myWorkflow = createWorkflow(
  "my-workflow", 
  function (input) {
    const product = createProductStep(input)
    const productCreatedHook = createHook(
      "productCreated", 
      { productId: product.id }
    )

    return new WorkflowResponse(product, {
      hooks: [productCreatedHook],
    })
  }
)
```

The `createHook` function accepts two parameters:

1. The first is a string indicating the hook's name. You use this to consume the hook later.
2. The second is the input to pass to the hook handler.

The workflow must also pass an object having a `hooks` property as a second parameter to the `WorkflowResponse` constructor. Its value is an array of the workflow's hooks.

### How to Consume the Hook?

To consume the hook of the workflow, create the file `src/workflows/hooks/my-workflow.ts` with the following content:

```ts title="src/workflows/hooks/my-workflow.ts" highlights={handlerHighlights}
import { myWorkflow } from "../my-workflow"

myWorkflow.hooks.productCreated(
  async ({ productId }, { container }) => {
    // TODO perform an action
  }
)
```

The hook is available on the workflow's `hooks` property using its name `productCreated`.

You invoke the hook, passing a step function (the hook handler) as a parameter.


# Conditions in Workflows with When-Then

In this chapter, you'll learn how to execute an action based on a condition in a workflow using when-then from the Workflows SDK.

## Why If-Conditions Aren't Allowed in Workflows?

Medusa creates an internal representation of the workflow definition you pass to `createWorkflow` to track and store its steps. At that point, variables in the workflow don't have any values. They only do when you execute the workflow.

So, you can't use an if-condition that checks a variable's value, as the condition will be evaluated when Medusa creates the internal representation of the workflow, rather than during execution.

Instead, use when-then from the Workflows SDK. It allows you to perform steps in a workflow only if a condition that you specify is satisfied.

Restrictions for conditions is only applicable in a workflow's definition. You can still use if-conditions in your step's code.

***

## How to use When-Then?

The Workflows SDK provides a `when` function that is used to check whether a condition is true. You chain a `then` function to `when` that specifies the steps to execute if the condition in `when` is satisfied.

For example:

```ts highlights={highlights}
import { 
  createWorkflow,
  WorkflowResponse,
  when,
} from "@medusajs/framework/workflows-sdk"
// step imports...

const workflow = createWorkflow(
  "workflow", 
  function (input: {
    is_active: boolean
  }) {

    const result = when(
      input, 
      (input) => {
        return input.is_active
      }
    ).then(() => {
      const stepResult = isActiveStep()
      return stepResult
    })

    // executed without condition
    const anotherStepResult = anotherStep(result)

    return new WorkflowResponse(
      anotherStepResult
    )
  }
)
```

In this code snippet, you execute the `isActiveStep` only if the `input.is_active`'s value is `true`.

### When Parameters

`when` accepts the following parameters:

1. The first parameter is either an object or the workflow's input. This data is passed as a parameter to the function in `when`'s second parameter.
2. The second parameter is a function that returns a boolean indicating whether to execute the action in `then`.

### Then Parameters

To specify the action to perform if the condition is satisfied, chain a `then` function to `when` and pass it a callback function.

The callback function is only executed if `when`'s second parameter function returns a `true` value.

***

## Implementing If-Else with When-Then

when-then doesn't support if-else conditions. Instead, use two `when-then` conditions in your workflow.

For example:

```ts highlights={ifElseHighlights}
const workflow = createWorkflow(
  "workflow", 
  function (input: {
    is_active: boolean
  }) {

    const isActiveResult = when(
      input, 
      (input) => {
        return input.is_active
      }
    ).then(() => {
      return isActiveStep()
    })

    const notIsActiveResult = when(
      input,
      (input) => {
        return !input.is_active
      }
    ).then(() => {
      return notIsActiveStep()
    })

    // ...
  }
)
```

In the above workflow, you use two `when-then` blocks. The first one performs a step if `input.is_active` is `true`, and the second performs a step if `input.is_active` is `false`, acting as an else condition.

***

## Specify Name for When-Then

Internally, `when-then` blocks have a unique name similar to a step. When you return a step's result in a `when-then` block, the block's name is derived from the step's name. For example:

```ts
const isActiveResult = when(
  input, 
  (input) => {
    return input.is_active
  }
).then(() => {
  return isActiveStep()
})
```

This `when-then` block's internal name will be `when-then-is-active`, where `is-active` is the step's name.

However, if you need to return in your `when-then` block something other than a step's result, you need to specify a unique step name for that block. Otherwise, Medusa will generate a random name for it which can cause unexpected errors in production.

You pass a name for `when-then` as a first parameter of `when`, whose signature can accept three parameters in this case. For example:

```ts highlights={nameHighlights}
const { isActive } = when(
  "check-is-active",
  input, 
  (input) => {
    return input.is_active
  }
).then(() => {
  const isActive = isActiveStep()

  return {
    isActive,
  }
})
```

Since `then` returns a value different than the step's result, you pass to the `when` function the following parameters:

1. A unique name to be assigned to the `when-then` block.
2. Either an object or the workflow's input. This data is passed as a parameter to the function in `when`'s second parameter.
3. A function that returns a boolean indicating whether to execute the action in `then`.

The second and third parameters are the same as the parameters you previously passed to `when`.


# Workflow Constraints

This chapter lists constraints of defining a workflow or its steps.

Medusa creates an internal representation of the workflow definition you pass to `createWorkflow` to track and store its steps. At that point, variables in the workflow don't have any values. They only do when you execute the workflow.

This creates restrictions related to variable manipulations, using if-conditions, and other constraints. This chapter lists these constraints and provides their alternatives.

## Workflow Constraints

### No Async Functions

The function passed to `createWorkflow` can’t be an async function:

```ts highlights={[["4", "async", "Function can't be async."], ["11", "", "Correct way of defining the function."]]}
// Don't
const myWorkflow = createWorkflow(
  "hello-world", 
  async function (input: WorkflowInput) {
  // ...
})

// Do
const myWorkflow = createWorkflow(
  "hello-world", 
  function (input: WorkflowInput) {
  // ...
})
```

### No Direct Variable Manipulation

You can’t directly manipulate variables within the workflow's constructor function.

Learn more about why you can't manipulate variables [in this chapter](https://docs.medusajs.com/learn/fundamentals/workflows/variable-manipulation/index.html.md)

Instead, use `transform` from the Workflows SDK:

```ts highlights={highlights}
// Don't
const myWorkflow = createWorkflow(
  "hello-world", 
  function (input: WorkflowInput) {
  const str1 = step1(input)
  const str2 = step2(input)

  return new WorkflowResponse({
    message: `${str1}${str2}`,
  })
})

// Do
const myWorkflow = createWorkflow(
  "hello-world", 
  function (input: WorkflowInput) {
  const str1 = step1(input)
  const str2 = step2(input)

  const result = transform(
    {
      str1,
      str2,
    },
    (input) => ({
      message: `${input.str1}${input.str2}`,
    })
  )

  return new WorkflowResponse(result)
})
```

#### Create Dates in transform

When you use `new Date()` in a workflow's constructor function, the date is evaluated when Medusa creates the internal representation of the workflow, not during execution.

Instead, create the date using `transform`.

Learn more about how Medusa creates an internal representation of a workflow [in this chapter](https://docs.medusajs.com/learn/fundamentals/workflows/variable-manipulation/index.html.md).

For example:

```ts highlights={dateHighlights}
// Don't
const myWorkflow = createWorkflow(
  "hello-world", 
  function (input: WorkflowInput) {
  const today = new Date()

  return new WorkflowResponse({
    today,
  })
})

// Do
const myWorkflow = createWorkflow(
  "hello-world", 
  function (input: WorkflowInput) {
  const today = transform({}, () => new Date())

  return new WorkflowResponse({
    today,
  })
})
```

### No If Conditions

You can't use if-conditions in a workflow.

Learn more about why you can't use if-conditions [in this chapter](https://docs.medusajs.com/learn/fundamentals/workflows/conditions#why-if-conditions-arent-allowed-in-workflows/index.html.md)

Instead, use when-then from the Workflows SDK:

```ts
// Don't
const myWorkflow = createWorkflow(
  "hello-world", 
  function (input: WorkflowInput) {
  if (input.is_active) {
    // perform an action
  }
})

// Do (explained in the next chapter)
const myWorkflow = createWorkflow(
  "hello-world", 
  function (input: WorkflowInput) {
  when(input, (input) => {
    return input.is_active
  })
  .then(() => {
    // perform an action
  })
})
```

You can also pair multiple `when-then` blocks to implement an `if-else` condition as explained in [this chapter](https://docs.medusajs.com/learn/fundamentals/workflows/conditions/index.html.md).

### No Conditional Operators

You can't use conditional operators in a workflow, such as `??` or `||`.

Learn more about why you can't use conditional operators [in this chapter](https://docs.medusajs.com/learn/fundamentals/workflows/conditions#why-if-conditions-arent-allowed-in-workflows/index.html.md)

Instead, use `transform` to store the desired value in a variable.

#### Logical Or (||) Alternative

```ts
// Don't
const myWorkflow = createWorkflow(
  "hello-world", 
  function (input: WorkflowInput) {
    const message = input.message || "Hello"
})

// Do
// other imports...
import { transform } from "@medusajs/framework/workflows-sdk"

const myWorkflow = createWorkflow(
  "hello-world", 
  function (input: WorkflowInput) {
    const message = transform(
      {
        input,
      },
      (data) => data.input.message || "hello"
    )
})
```

#### Nullish Coalescing (??) Alternative

```ts
// Don't
const myWorkflow = createWorkflow(
  "hello-world", 
  function (input: WorkflowInput) {
    const message = input.message ?? "Hello"
})

// Do
// other imports...
import { transform } from "@medusajs/framework/workflows-sdk"

const myWorkflow = createWorkflow(
  "hello-world", 
  function (input: WorkflowInput) {
    const message = transform(
      {
        input,
      },
      (data) => data.input.message ?? "hello"
    )
})
```

#### Double Not (!!) Alternative

```ts
// Don't
const myWorkflow = createWorkflow(
  "hello-world", 
  function (input: WorkflowInput) {
    step1({
      isActive: !!input.is_active,
    })
})

// Do
// other imports...
import { transform } from "@medusajs/framework/workflows-sdk"

const myWorkflow = createWorkflow(
  "hello-world", 
  function (input: WorkflowInput) {
    const isActive = transform(
      {
        input,
      },
      (data) => !!data.input.is_active
    )
    
    step1({
      isActive,
    })
})
```

#### Ternary Alternative

```ts
// Don't
const myWorkflow = createWorkflow(
  "hello-world", 
  function (input: WorkflowInput) {
    step1({
      message: input.is_active ? "active" : "inactive",
    })
})

// Do
// other imports...
import { transform } from "@medusajs/framework/workflows-sdk"

const myWorkflow = createWorkflow(
  "hello-world", 
  function (input: WorkflowInput) {
    const message = transform(
      {
        input,
      },
      (data) => {
        return data.input.is_active ? "active" : "inactive"
      }
    )
    
    step1({
      message,
    })
})
```

#### Optional Chaining (?.) Alternative

```ts
// Don't
const myWorkflow = createWorkflow(
  "hello-world", 
  function (input: WorkflowInput) {
    step1({
      name: input.customer?.name,
    })
})

// Do
// other imports...
import { transform } from "@medusajs/framework/workflows-sdk"

const myWorkflow = createWorkflow(
  "hello-world", 
  function (input: WorkflowInput) {
    const name = transform(
      {
        input,
      },
      (data) => data.input.customer?.name
    )
    
    step1({
      name,
    })
})
```

### No Try-Catch

In a workflow, don't use try-catch blocks to handle errors.

Instead, refer to the [Error Handling](https://docs.medusajs.com/learn/fundamentals/workflows/errors/index.html.md) chapter for alternative ways to handle errors.

***

## Step Constraints

### Returned Values

A step must only return serializable values, such as [primitive values](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Data_structures#primitive_values) or an object.

Values of other types, such as Maps, aren't allowed.

```ts
// Don't
import { 
  createStep,
  StepResponse,
} from "@medusajs/framework/workflows-sdk"

const step1 = createStep(
  "step-1",
  (input, { container }) => {
    const myMap = new Map()

    // ...

    return new StepResponse({
      myMap,
    })
  }
)

// Do
import { 
  createStep,
  StepResponse,
} from "@medusajs/framework/workflows-sdk"

const step1 = createStep(
  "step-1",
  (input, { container }) => {
    const myObj: Record<string, unknown> = {}

    // ...

    return new StepResponse({
      myObj,
    })
  }
)
```


# Error Handling in Workflows

In this chapter, you’ll learn about what happens when an error occurs in a workflow, how to disable error throwing in a workflow, and try-catch alternatives in workflow definitions.

## Default Behavior of Errors in Workflows

When an error occurs in a workflow, such as when a step throws an error, the workflow execution stops. Then, [the compensation function](https://docs.medusajs.com/learn/fundamentals/workflows/compensation-function/index.html.md) of every step in the workflow is called to undo the actions performed by their respective steps.

The workflow's caller, such as an API route, subscriber, or scheduled job, will also fail and stop execution. Medusa then logs the error in the console. For API routes, an appropriate error is returned to the client based on the thrown error.

Learn more about error handling in API routes in the [Errors chapter](https://docs.medusajs.com/learn/fundamentals/api-routes/errors/index.html.md).

This is the default behavior of errors in workflows. However, you can configure workflows to not throw errors, or you can configure a step's internal error handling mechanism to change the default behavior.

***

## Disable Error Throwing in Workflow

When an error is thrown in the workflow, that means the caller of the workflow, such as an API route, will fail and stop execution as well.

While this is the common behavior, there are certain cases where you want to handle the error differently. For example, you may want to check the errors thrown by the workflow and return a custom error response to the client.

The object parameter of a workflow's `run` method accepts a `throwOnError` property. When this property is set to `false`, the workflow will stop execution if an error occurs, but the Medusa's workflow engine will catch that error and return it to the caller instead of throwing it.

For example:

```ts title="src/api/workflows/route.ts" highlights={highlights} collapsibleLines="1-6" expandButtonLabel="Show Imports"
import type {
  MedusaRequest,
  MedusaResponse,
} from "@medusajs/framework/http"
import myWorkflow from "../../../workflows/hello-world"

export async function GET(
  req: MedusaRequest,
  res: MedusaResponse
) {
  const { result, errors } = await myWorkflow(req.scope)
    .run({
      // ...
      throwOnError: false,
    })

  if (errors.length) {
    return res.send({
      message: "Something unexpected happened. Please try again.",
    })
  }

  res.send(result)
}
```

You disable throwing errors in the workflow by setting the `throwOnError` property to `false` in the `run` method of the workflow.

The object returned by the `run` method contains an `errors` property. This property is an array of errors that occured during the workflow's execution. You can check this array to see if any errors occurred and handle them accordingly.

An error object has the following properties:

- action: (\`string\`) The ID of the step that threw the error.
- handlerType: (\`invoke\` \\| \`compensate\`) Where the error occurred. If the value is \`invoke\`, it means the error occurred in a step. Otherwise, the error occurred in the compensation function of a step.
- error: (\[Error]\(https://nodejs.org/docs/latest-v20.x/api/errors.html#class-error)) The error object that was thrown.

***

## Try-Catch Alternatives in Workflow Definition

If you want to use try-catch mechanism in a workflow to undo step actions, use a [compensation function](https://docs.medusajs.com/learn/fundamentals/workflows/compensation-function/index.html.md) instead.

### Why You Can't Use Try-Catch in Workflow Definitions

Medusa creates an internal representation of the workflow definition you pass to `createWorkflow` to track and store its steps.

At that point, variables in the workflow don't have any values. They only do when you execute the workflow.

So, try-catch blocks in the workflow definition function won't have an effect, as at that time the workflow is not executed and errors are not thrown.

You can still use try-catch blocks in a workflow's step functions. For cases that require granular control over error handling in a workflow's definition, you can configure the internal error handling mechanism of a step.

### Skip Workflow on Step Failure

A step has a `skipOnPermanentFailure` configuration that allows you to configure what happens when an error occurs in the step. Its value can be a boolean or a string.

By default, `skipOnPermanentFailure` is disabled. When it's enabled, the workflow's status is set to `skipped` instead of `failed`. This means:

- Compensation functions of the workflow's steps are not called.
- The workflow's caller continues executing. You can still [access the error](#disable-error-throwing-in-workflow) that occurred during the workflow's execution as mentioned in the [Disable Error Throwing](#disable-error-throwing-in-workflow) section.

This is useful when you want to perform actions if no error occurs, but you don't care about compensating the workflow's steps or you don't want to stop the caller's execution.

You can think of setting the `skipOnPermanentFailure` to `true` as the equivalent of the following `try-catch` block:

```ts title="Outside a Workflow"
try {
  actionThatThrowsError()

  moreActions()
} catch (e) {
  // don't do anything
}
```

You can do this in a workflow using the step's `skipOnPermanentFailure` configuration:

```ts title="Workflow Equivalent" highlights={skipOnPermanentFailureEnabledHighlights}
import {
  createWorkflow,
} from "@medusajs/framework/workflows-sdk"
import { 
  actionThatThrowsError,
  moreActions,
} from "./steps"

export const myWorkflow = createWorkflow(
  "hello-world", 
  function (input) {
    actionThatThrowsError().config({
      skipOnPermanentFailure: true,
    })

    // This action will not be executed if the previous step throws an error
    moreActions()
  }
)
```

You set the configuration of a step by chaining the `config` method to the step's function call. The `config` method accepts an object similar to the one that can be passed to `createStep`.

In this example, if the `actionThatThrowsError` step throws an error, the rest of the workflow will be skipped, and the `moreActions` step will not be executed.

You can then access the error that occurred in that step as explained in the [Disable Error Throwing](#disable-error-throwing-in-workflow) section.

### Continue Workflow Execution from a Specific Step

In some cases, if an error occurs in a step, you may want to continue the workflow's execution from a specific step instead of stopping the workflow's execution or skipping the rest of the steps.

The `skipOnPermanentFailure` configuration can accept a step's ID as a value. Then, the workflow will continue execution from that step if an error occurs in the step that has the `skipOnPermanentFailure` configuration.

The compensation function of the step that has the `skipOnPermanentFailure` configuration will not be called when an error occurs.

You can think of setting the `skipOnPermanentFailure` to a step's ID as the equivalent of the following `try-catch` block:

```ts title="Outside a Workflow"
try {
  actionThatThrowsError()

  moreActions()
} catch (e) {
  // do nothing
}

continueExecutionFromStep()
```

You can do this in a workflow using the step's `skipOnPermanentFailure` configuration:

```ts title="Workflow Equivalent" highlights={skipOnPermanentFailureStepHighlights}
import {
  createWorkflow,
} from "@medusajs/framework/workflows-sdk"
import { 
  actionThatThrowsError,
  moreActions,
  continueExecutionFromStep,
} from "./steps"

export const myWorkflow = createWorkflow(
  "hello-world", 
  function (input) {
    actionThatThrowsError().config({
      // The `continue-execution-from-step` is the ID passed as a first
      // parameter to `createStep` of `continueExecutionFromStep`.
      skipOnPermanentFailure: "continue-execution-from-step",
    })

    // This action will not be executed if the previous step throws an error
    moreActions()

    // This action will be executed either way
    continueExecutionFromStep()
  }
)
```

In this example, you configure the `actionThatThrowsError` step to continue the workflow's execution from the `continueExecutionFromStep` step if an error occurs in the `actionThatThrowsError` step.

Notice that you pass the ID of the `continueExecutionFromStep` step as it's set in the `createStep` function.

So, the `moreActions` step will not be executed if the `actionThatThrowsError` step throws an error, and the `continueExecutionFromStep` will be executed anyway.

You can then access the error that occurred in that step as explained in the [Disable Error Throwing](#disable-error-throwing-in-workflow) section.

If the specified step ID doesn't exist in the workflow, it will be equivalent to setting the `skipOnPermanentFailure` configuration to `true`. So, the workflow will be skipped, and the rest of the steps will not be executed.

### Set Step as Failed, but Continue Workflow Execution

In some cases, you may want to fail a step, but continue the rest of the workflow's execution.

This is useful when you don't want a step's failure to stop the workflow's execution, but you want to mark that step as failed.

The `continueOnPermanentFailure` configuration allows you to do that. When enabled, the workflow's execution will continue, but the step will be marked as failed if an error occurs in that step.

The compensation function of the step that has the `continueOnPermanentFailure` configuration will not be called when an error occurs.

You can think of setting the `continueOnPermanentFailure` to `true` as the equivalent of the following `try-catch` block:

```ts title="Outside a Workflow"
try {
  actionThatThrowsError()
} catch (e) {
  // do nothing
}

moreActions()
```

You can do this in a workflow using the step's `continueOnPermanentFailure` configuration:

```ts title="Workflow Equivalent" highlights={continueOnPermanentFailureHighlights}
import {
  createWorkflow,
} from "@medusajs/framework/workflows-sdk"
import { 
  actionThatThrowsError,
  moreActions,
} from "./steps"

export const myWorkflow = createWorkflow(
  "hello-world", 
  function (input) {
    actionThatThrowsError().config({
      continueOnPermanentFailure: true,
    })

    // This action will be executed even if the previous step throws an error
    moreActions()
  }
)
```

In this example, if the `actionThatThrowsError` step throws an error, the `moreActions` step will still be executed.

You can then access the error that occurred in that step as explained in the [Disable Error Throwing](#disable-error-throwing-in-workflow) section.


# Execute Another Workflow

In this chapter, you'll learn how to execute a workflow in another.

## Execute in a Workflow

To execute a workflow in another, use the `runAsStep` method that every workflow has.

For example:

```ts highlights={workflowsHighlights} collapsibleLines="1-7" expandMoreButton="Show Imports"
import {
  createWorkflow,
} from "@medusajs/framework/workflows-sdk"
import { 
  createProductsWorkflow,
} from "@medusajs/medusa/core-flows"

const workflow = createWorkflow(
  "hello-world",
  async (input) => {
    const products = createProductsWorkflow.runAsStep({
      input: {
        products: [
          // ...
        ],
      },
    })

    // ...
  }
)
```

Instead of invoking the workflow and passing it the container, you use its `runAsStep` method and pass it an object as a parameter.

The object has an `input` property to pass input to the workflow.

***

## Preparing Input Data

If you need to perform some data manipulation to prepare the other workflow's input data, use `transform` from the Workflows SDK.

Learn about transform in [this chapter](https://docs.medusajs.com/learn/fundamentals/workflows/variable-manipulation/index.html.md).

For example:

```ts highlights={transformHighlights} collapsibleLines="1-12"
import {
  createWorkflow,
  transform,
} from "@medusajs/framework/workflows-sdk"
import { 
  createProductsWorkflow,
} from "@medusajs/medusa/core-flows"

type WorkflowInput = {
  title: string
}

const workflow = createWorkflow(
  "hello-product",
  async (input: WorkflowInput) => {
    const createProductsData = transform({
      input,
    }, (data) => [
      {
        title: `Hello ${data.input.title}`,
      },
    ])

    const products = createProductsWorkflow.runAsStep({
      input: {
        products: createProductsData,
      },
    })

    // ...
  }
)
```

In this example, you use the `transform` function to prepend `Hello` to the title of the product. Then, you pass the result as an input to the `createProductsWorkflow`.

***

## Run Workflow Conditionally

To run a workflow in another based on a condition, use when-then from the Workflows SDK.

Learn about when-then in [this chapter](https://docs.medusajs.com/learn/fundamentals/workflows/conditions/index.html.md).

For example:

```ts highlights={whenHighlights} collapsibleLines="1-16"
import {
  createWorkflow,
  when,
} from "@medusajs/framework/workflows-sdk"
import { 
  createProductsWorkflow,
} from "@medusajs/medusa/core-flows"
import { 
  CreateProductWorkflowInputDTO,
} from "@medusajs/framework/types"

type WorkflowInput = {
  product?: CreateProductWorkflowInputDTO
  should_create?: boolean
}

const workflow = createWorkflow(
  "hello-product",
  async (input: WorkflowInput) => {
    const product = when(input, ({ should_create }) => should_create)
      .then(() => {
        return createProductsWorkflow.runAsStep({
          input: {
            products: [input.product],
          },
        })
      })
  }
)
```

In this example, you use when-then to run the `createProductsWorkflow` only if `should_create` (passed in the `input`) is enabled.


# Long-Running Workflows

In this chapter, you’ll learn what a long-running workflow is and how to configure it.

## What is a Long-Running Workflow?

When you execute a workflow, you wait until the workflow finishes execution to receive the output.

A long-running workflow is a workflow that continues its execution in the background. You don’t receive its output immediately. Instead, you subscribe to the workflow execution to listen to status changes and receive its result once the execution is finished.

### Why use Long-Running Workflows?

Long-running workflows are useful if:

- A task takes too long. For example, you're importing data from a CSV file.
- The workflow's steps wait for an external action to finish before resuming execution. For example, before you import the data from the CSV file, you wait until the import is confirmed by the user.
- You want to retry workflow steps after a long period of time. For example, you want to retry a step that processes a payment every day until the payment is successful.
  - Refer to the [Retry Failed Steps chapter](https://docs.medusajs.com/learn/fundamentals/workflows/retry-failed-steps/index.html.md) for more information.

***

## Configure Long-Running Workflows

A workflow is considered long-running if at least one step has its `async` configuration set to `true` and doesn't return a step response.

For example, consider the following workflow and steps:

```ts title="src/workflows/hello-world.ts" highlights={[["15"]]} collapsibleLines="1-11" expandButtonLabel="Show More"
import { 
  createStep,  
  createWorkflow,
  WorkflowResponse,
  StepResponse,
} from "@medusajs/framework/workflows-sdk"

const step1 = createStep("step-1", async () => {
  return new StepResponse({})
})

const step2 = createStep(
  {
    name: "step-2",
    async: true,
  },
  async () => {
    console.log("Waiting to be successful...")
  }
)

const step3 = createStep("step-3", async () => {
  return new StepResponse("Finished three steps")
})

const myWorkflow = createWorkflow(
  "hello-world", 
  function () {
  step1()
  step2()
  const message = step3()

  return new WorkflowResponse({
    message,
  })
})

export default myWorkflow
```

The second step has in its configuration object `async` set to `true` and it doesn't return a step response. This indicates that this step is an asynchronous step.

So, when you execute the `hello-world` workflow, it continues its execution in the background once it reaches the second step.

### When is a Workflow Considered Long-Running?

A workflow is also considered long-running if:

- One of its steps has its `async` configuration set to `true` and doesn't return a step response.
- One of its steps has its `retryInterval` option set as explained in the [Retry Failed Steps chapter](https://docs.medusajs.com/learn/fundamentals/workflows/retry-failed-steps/index.html.md).

***

## Change Step Status

Once the workflow's execution reaches an async step, it'll wait in the background for the step to succeed or fail before it moves to the next step.

To fail or succeed a step, use the Workflow Engine Module's main service that is registered in the Medusa Container under the `Modules.WORKFLOW_ENGINE` (or `workflowsModuleService`) key.

### Retrieve Transaction ID

Before changing the status of a workflow execution's async step, you must have the execution's transaction ID.

When you execute the workflow, the object returned has a `transaction` property, which is an object that holds the details of the workflow execution's transaction. Use its `transactionId` to later change async steps' statuses:

```ts
const { transaction } = await myWorkflow(req.scope)
  .run()

// use transaction.transactionId later
```

### Change Step Status to Successful

The Workflow Engine Module's main service has a `setStepSuccess` method to set a step's status to successful. If you use it on a workflow execution's async step, the workflow continues execution to the next step.

For example, consider the following step:

```ts highlights={successStatusHighlights} collapsibleLines="1-9" expandButtonLabel="Show Imports"
import {
  Modules,
  TransactionHandlerType,
} from "@medusajs/framework/utils"
import { 
  StepResponse, 
  createStep,
} from "@medusajs/framework/workflows-sdk"

type SetStepSuccessStepInput = {
  transactionId: string
};

export const setStepSuccessStep = createStep(
  "set-step-success-step",
  async function (
    { transactionId }: SetStepSuccessStepInput,
    { container }
  ) {
    const workflowEngineService = container.resolve(
      Modules.WORKFLOW_ENGINE
    )

    await workflowEngineService.setStepSuccess({
      idempotencyKey: {
        action: TransactionHandlerType.INVOKE,
        transactionId,
        stepId: "step-2",
        workflowId: "hello-world",
      },
      stepResponse: new StepResponse("Done!"),
      options: {
        container,
      },
    })
  }
)
```

In this step (which you use in a workflow other than the long-running workflow), you resolve the Workflow Engine Module's main service and set `step-2` of the previous workflow as successful.

The `setStepSuccess` method of the workflow engine's main service accepts as a parameter an object having the following properties:

- idempotencyKey: (\`object\`) The details of the workflow execution.

  - action: (\`invoke\` | \`compensate\`) If the step's compensation function is running, use \`compensate\`. Otherwise, use \`invoke\`.

  - transactionId: (\`string\`) The ID of the workflow execution's transaction.

  - stepId: (\`string\`) The ID of the step to change its status. This is the first parameter passed to \`createStep\` when creating the step.

  - workflowId: (\`string\`) The ID of the workflow. This is the first parameter passed to \`createWorkflow\` when creating the workflow.
- stepResponse: (\`StepResponse\`) Set the response of the step. This is similar to the response you return in a step's definition, but since the \`async\` step doesn't have a response, you set its response when changing its status.
- options: (\`Record\<string, any>\`) Options to pass to the step.

  - container: (\`MedusaContainer\`) An instance of the Medusa Container

### Change Step Status to Failed

The Workflow Engine Module's main service also has a `setStepFailure` method that changes a step's status to failed. It accepts the same parameter as `setStepSuccess`.

After changing the async step's status to failed, the workflow execution fails and the compensation functions of previous steps are executed.

For example:

```ts highlights={failureStatusHighlights} collapsibleLines="1-9" expandButtonLabel="Show Imports"
import {
  Modules,
  TransactionHandlerType,
} from "@medusajs/framework/utils"
import { 
  StepResponse, 
  createStep,
} from "@medusajs/framework/workflows-sdk"

type SetStepFailureStepInput = {
  transactionId: string
};

export const setStepFailureStep = createStep(
  "set-step-failure-step",
  async function (
    { transactionId }: SetStepFailureStepInput,
    { container }
  ) {
    const workflowEngineService = container.resolve(
      Modules.WORKFLOW_ENGINE
    )

    await workflowEngineService.setStepFailure({
      idempotencyKey: {
        action: TransactionHandlerType.INVOKE,
        transactionId,
        stepId: "step-2",
        workflowId: "hello-world",
      },
      stepResponse: new StepResponse("Failed!"),
      options: {
        container,
      },
    })
  }
)
```

You use this step in another workflow that changes the status of an async step in a long-running workflow's execution to failed.

***

## Access Long-Running Workflow Status and Result

To access the status and result of a long-running workflow execution, use the `subscribe` and `unsubscribe` methods of the Workflow Engine Module's main service.

To retrieve the workflow execution's details at a later point, you must enable [storing the workflow's executions](https://docs.medusajs.com/learn/fundamentals/workflows/store-executions/index.html.md).

For example:

```ts title="src/api/workflows/route.ts" highlights={highlights} collapsibleLines="1-11" expandButtonLabel="Show Imports"
import type { MedusaRequest, MedusaResponse } from "@medusajs/framework/http"
import myWorkflow from "../../../workflows/hello-world"
import { 
  IWorkflowEngineService,
} from "@medusajs/framework/types"
import { Modules } from "@medusajs/framework/utils"

export async function GET(req: MedusaRequest, res: MedusaResponse) {
  const { transaction, result } = await myWorkflow(req.scope).run()

  const workflowEngineService = req.scope.resolve<
    IWorkflowEngineService
  >(
    Modules.WORKFLOW_ENGINE
  )

  const subscriptionOptions = {
    workflowId: "hello-world",
    transactionId: transaction.transactionId,
    subscriberId: "hello-world-subscriber",
  }

  await workflowEngineService.subscribe({
    ...subscriptionOptions,
    subscriber: async (data) => {
      if (data.eventType === "onFinish") {
        console.log("Finished execution", data.result)
        // unsubscribe
        await workflowEngineService.unsubscribe({
          ...subscriptionOptions,
          subscriberOrId: subscriptionOptions.subscriberId,
        })
      } else if (data.eventType === "onStepFailure") {
        console.log("Workflow failed", data.step)
      }
    },
  })

  res.send(result)
}
```

In the above example, you execute the long-running workflow `hello-world` and resolve the Workflow Engine Module's main service from the Medusa container.

### subscribe Method

The main service's `subscribe` method allows you to listen to changes in the workflow execution’s status. It accepts an object having three properties:

- workflowId: (\`string\`) The name of the workflow.
- transactionId: (\`string\`) The ID of the workflow exection's transaction. The transaction's details are returned in the response of the workflow execution.
- subscriberId: (\`string\`) The ID of the subscriber.
- subscriber: (\`(data: \{ eventType: string, result?: any }) => Promise\<void>\`) The function executed when the workflow execution's status changes. The function receives a data object. It has an \`eventType\` property, which you use to check the status of the workflow execution.

If the value of `eventType` in the `subscriber` function's first parameter is `onFinish`, the workflow finished executing. The first parameter then also has a `result` property holding the workflow's output.

### unsubscribe Method

You can unsubscribe from the workflow using the workflow engine's `unsubscribe` method, which requires the same object parameter as the `subscribe` method.

However, instead of the `subscriber` property, it requires a `subscriberOrId` property whose value is the same `subscriberId` passed to the `subscribe` method.

***

## Example: Restaurant-Delivery Recipe

To find a full example of a long-running workflow, refer to the [restaurant-delivery recipe](https://docs.medusajs.com/resources/recipes/marketplace/examples/restaurant-delivery/index.html.md).

In the recipe, you use a long-running workflow that moves an order from placed to completed. The workflow waits for the restaurant to accept the order, the driver to pick up the order, and other external actions.


# Run Workflow Steps in Parallel

In this chapter, you’ll learn how to run workflow steps in parallel.

## parallelize Utility Function

If your workflow has steps that don’t rely on one another’s results, run them in parallel using `parallelize` from the Workflows SDK.

The workflow waits until all steps passed to the `parallelize` function finish executing before continuing to the next step.

For example:

```ts highlights={highlights} collapsibleLines="1-12" expandButtonLabel="Show Imports"
import {
  createWorkflow,
  WorkflowResponse,
  parallelize,
} from "@medusajs/framework/workflows-sdk"
import {
  createProductStep,
  getProductStep,
  createPricesStep,
  attachProductToSalesChannelStep,
} from "./steps"

interface WorkflowInput {
  title: string
}

const myWorkflow = createWorkflow(
  "my-workflow", 
  (input: WorkflowInput) => {
   const product = createProductStep(input)

   const [prices, productSalesChannel] = parallelize(
     createPricesStep(product),
     attachProductToSalesChannelStep(product)
   )

   const refetchedProduct = getProductStep(product.id)

   return new WorkflowResponse(refetchedProduct)
 }
)
```

The `parallelize` function accepts the steps to run in parallel as a parameter.

It returns an array of the steps' results in the same order they're passed to the `parallelize` function.

So, `prices` is the result of `createPricesStep`, and `productSalesChannel` is the result of `attachProductToSalesChannelStep`.


# Multiple Step Usage in Workflow

In this chapter, you'll learn how to use a step multiple times in a workflow.

## Problem Reusing a Step in a Workflow

In some cases, you may need to use a step multiple times in the same workflow.

The most common example is using the `useQueryGraphStep` multiple times in a workflow to retrieve multiple unrelated data, such as customers and products.

Each workflow step must have a unique ID, which is the ID passed as a first parameter when creating the step:

```ts
const useQueryGraphStep = createStep(
  "use-query-graph"
  // ...
)
```

This causes an error when you use the same step multiple times in a workflow, as it's registered in the workflow as two steps having the same ID:

```ts
const helloWorkflow = createWorkflow(
  "hello",
  () => {
    const { data: products } = useQueryGraphStep({
      entity: "product",
      fields: ["id"],
    })

    // ERROR OCCURS HERE: A STEP HAS THE SAME ID AS ANOTHER IN THE WORKFLOW
    const { data: customers } = useQueryGraphStep({
      entity: "customer",
      fields: ["id"],
    })
  }
)
```

The next section explains how to fix this issue to use the same step multiple times in a workflow.

***

## How to Use a Step Multiple Times in a Workflow?

When you execute a step in a workflow, you can chain a `config` method to it to change the step's config.

Use the `config` method to change a step's ID for a single execution.

So, this is the correct way to write the example above:

```ts highlights={highlights}
const helloWorkflow = createWorkflow(
  "hello",
  () => {
    const { data: products } = useQueryGraphStep({
      entity: "product",
      fields: ["id"],
    })

    // ✓ No error occurs, the step has a different ID.
    const { data: customers } = useQueryGraphStep({
      entity: "customer",
      fields: ["id"],
    }).config({ name: "fetch-customers" })
  }
)
```

The `config` method accepts an object with a `name` property. Its value is a new ID of the step to use for this execution only.

The first `useQueryGraphStep` usage has the ID `use-query-graph`, and the second `useQueryGraphStep` usage has the ID `fetch-customers`.


# Retry Failed Steps

In this chapter, you’ll learn how to configure steps to allow retrial on failure.

## What is a Step Retrial?

A step retrial is a mechanism that allows a step to be retried automatically when it fails. This is useful for handling transient errors, such as network issues or temporary unavailability of a service.

When a step fails, the workflow engine can automatically retry the step a specified number of times before marking the workflow as failed. This can help improve the reliability and resilience of your workflows.

You can also configure the interval between retries, allowing you to wait for a certain period before attempting the step again. This is useful when the failure is due to a temporary issue that may resolve itself after some time.

For example, if a step captures a payment, you may want to retry it the next day until the payment is successful or the maximum number of retries is reached.

***

## Configure a Step’s Retrial

By default, when an error occurs in a step, the step and the workflow fail, and the execution stops.

You can configure the step to retry on failure. The `createStep` function can accept a configuration object instead of the step’s name as a first parameter.

For example:

```ts title="src/workflows/hello-world.ts" highlights={[["10"]]} collapsibleLines="1-6" expandButtonLabel="Show Imports"
import { 
  createStep, 
  createWorkflow,
  WorkflowResponse,
} from "@medusajs/framework/workflows-sdk"

const step1 = createStep(
  {
    name: "step-1",
    maxRetries: 2,
  },
  async () => {
    console.log("Executing step 1")

    throw new Error("Oops! Something happened.")
  }
)

const myWorkflow = createWorkflow(
  "hello-world", 
  function () {
  const str1 = step1()

  return new WorkflowResponse({
    message: str1,
  })
})

export default myWorkflow
```

The step’s configuration object accepts a `maxRetries` property, which is a number indicating the number of times a step can be retried when it fails.

When you execute the above workflow, you’ll see the following result in the terminal:

```bash
Executing step 1
Executing step 1
Executing step 1
error:   Oops! Something happened.
Error: Oops! Something happened.
```

The first line indicates the first time the step was executed, and the next two lines indicate the times the step was retried. After that, the step and workflow fail.

***

## Step Retry Intervals

By default, a step is retried immediately after it fails. To specify a wait time before a step is retried, pass a `retryInterval` property to the step's configuration object. Its value is a number of seconds to wait before retrying the step.

For example:

```ts title="src/workflows/hello-world.ts" highlights={[["5"]]}
const step1 = createStep(
  {
    name: "step-1",
    maxRetries: 2,
    retryInterval: 2, // 2 seconds
  },
  async () => {
    // ...
  }
)
```

In this example, if the step fails, it will be retried after two seconds.

### Maximum Retry Interval

The `retryInterval` property's maximum value is [Number.MAX\_SAFE\_INTEGER](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/MAX_SAFE_INTEGER). So, you can set a very long wait time before the step is retried, allowing you to retry steps after a long period.

For example, to retry a step after a day:

```ts title="src/workflows/hello-world.ts" highlights={[["5"]]}
const step1 = createStep(
  {
    name: "step-1",
    maxRetries: 2,
    retryInterval: 86400, // 1 day
  },
  async () => {
    // ...
  }
)
```

In this example, if the step fails, it will be retried after `86400` seconds (one day).

### Interval Changes Workflow to Long-Running

By setting `retryInterval` on a step, a workflow that uses that step becomes a [long-running workflow](https://docs.medusajs.com/learn/fundamentals/workflows/long-running-workflow/index.html.md) that runs asynchronously in the background. This is useful when creating workflows that may fail and should run for a long time until they succeed, such as waiting for a payment to be captured or a shipment to be delivered.

However, since the long-running workflow runs in the background, you won't receive its result or errors immediately when you execute the workflow.

Instead, you must subscribe to the workflow's execution using the Workflow Engine Module Service. Learn more about it in [this chapter](https://docs.medusajs.com/learn/fundamentals/workflows/long-running-workflow#access-long-running-workflow-status-and-result/index.html.md).


# Store Workflow Executions

In this chapter, you'll learn how to store workflow executions in the database and access them later.

## Workflow Execution Retention

Medusa doesn't store your workflow's execution details by default. However, you can configure a workflow to keep its execution details stored in the database.

This is useful for auditing and debugging purposes. When you store a workflow's execution, you can view details around its steps, their states and their output. You can also check whether the workflow or any of its steps failed.

You can view stored workflow executions from the Medusa Admin dashboard by going to Settings -> Workflows.

***

## How to Store Workflow's Executions?

### Prerequisites

- [Redis Workflow Engine must be installed and configured.](https://docs.medusajs.com/resources/infrastructure-modules/workflow-engine/redis/index.html.md)

`createWorkflow` from the Workflows SDK can accept an object as a first parameter to set the workflow's configuration. To enable storing a workflow's executions:

- Enable the `store` option. If your workflow is a [Long-Running Workflow](https://docs.medusajs.com/learn/fundamentals/workflows/long-running-workflow/index.html.md), this option is enabled by default.
- Set the `retentionTime` option to the number of seconds that the workflow execution should be stored in the database.

For example:

```ts highlights={highlights}
import { createStep, createWorkflow } from "@medusajs/framework/workflows-sdk"

const step1 = createStep(
  {
    name: "step-1",
  },
  async () => {
    console.log("Hello from step 1")
  }
)

export const helloWorkflow = createWorkflow(
  {
    name: "hello-workflow",
    retentionTime: 99999,
    store: true,
  },
  () => {
    step1()
  }
)
```

Whenever you execute the `helloWorkflow` now, its execution details will be stored in the database.

***

## Retrieve Workflow Executions

You can view stored workflow executions from the Medusa Admin dashboard by going to Settings -> Workflows.

When you execute a workflow, the returned object has a `transaction` property containing the workflow execution's transaction details:

```ts
const { transaction } = await helloWorkflow(container).run()
```

To retrieve a workflow's execution details from the database, resolve the Workflow Engine Module from the container and use its `listWorkflowExecutions` method.

For example, you can create a `GET` API Route at `src/workflows/[id]/route.ts` that retrieves a workflow execution for the specified transaction ID:

```ts title="src/workflows/[id]/route.ts" highlights={retrieveHighlights}
import { MedusaRequest, MedusaResponse } from "@medusajs/framework"
import { Modules } from "@medusajs/framework/utils"

export async function GET(
  req: MedusaRequest,
  res: MedusaResponse
) {
  const { transaction_id } = req.params
  
  const workflowEngineService = req.scope.resolve(
    Modules.WORKFLOW_ENGINE
  )

  const [workflowExecution] = await workflowEngineService.listWorkflowExecutions({
    transaction_id: transaction_id,
  })

  res.json({
    workflowExecution,
  })
}
```

In the above example, you resolve the Workflow Engine Module from the container and use its `listWorkflowExecutions` method, passing the `transaction_id` as a filter to retrieve its workflow execution details.

A workflow execution object will be similar to the following:

```json
{
  "workflow_id": "hello-workflow",
  "transaction_id": "01JJC2T6AVJCQ3N4BRD1EB88SP",
  "id": "wf_exec_01JJC2T6B3P76JD35F12QTTA78",
  "execution": {
    "state": "done",
    "steps": {},
    "modelId": "hello-workflow",
    "options": {},
    "metadata": {},
    "startedAt": 1737719880027,
    "definition": {},
    "timedOutAt": null,
    "hasAsyncSteps": false,
    "transactionId": "01JJC2T6AVJCQ3N4BRD1EB88SP",
    "hasFailedSteps": false,
    "hasSkippedSteps": false,
    "hasWaitingSteps": false,
    "hasRevertedSteps": false,
    "hasSkippedOnFailureSteps": false
  },
  "context": {
    "data": {},
    "errors": []
  },
  "state": "done",
  "created_at": "2025-01-24T09:58:00.036Z",
  "updated_at": "2025-01-24T09:58:00.046Z",
  "deleted_at": null
}
```

### Example: Check if Stored Workflow Execution Failed

To check if a stored workflow execution failed, you can check its `state` property:

```ts
if (workflowExecution.state === "failed") {
  return res.status(500).json({
    error: "Workflow failed",
  })
}
```

Other state values include `done`, `invoking`, and `compensating`.


# Data Manipulation in Workflows with transform

In this chapter, you'll learn how to use `transform` from the Workflows SDK to manipulate data and variables in a workflow.

## Why Variable Manipulation isn't Allowed in Workflows

Medusa creates an internal representation of the workflow definition you pass to `createWorkflow` to track and store its steps.

At that point, variables in the workflow don't have any values. They only do when you execute the workflow.

So, you can only pass variables as parameters to steps. But, in a workflow, you can't change a variable's value or, if the variable is an array, loop over its items.

Instead, use `transform` from the Workflows SDK.

Restrictions for variable manipulation is only applicable in a workflow's definition. You can still manipulate variables in your step's code.

***

## What is the transform Utility?

`transform` creates a new variable as the result of manipulating other variables.

For example, consider you have two strings as the output of two steps:

```ts
const str1 = step1()
const str2 = step2()
```

To concatenate the strings, you create a new variable `str3` using the `transform` function:

```ts highlights={highlights}
import { 
  createWorkflow,
  WorkflowResponse,
  transform,
} from "@medusajs/framework/workflows-sdk"
// step imports...

const myWorkflow = createWorkflow(
  "hello-world", 
  function (input) {
    const str1 = step1(input)
    const str2 = step2(input)

    const str3 = transform(
      { str1, str2 },
      (data) => `${data.str1}${data.str2}`
    )

    return new WorkflowResponse(str3)
  }
)
```

`transform` accepts two parameters:

1. The first parameter is an object of variables to manipulate. The object is passed as a parameter to `transform`'s second parameter function.
2. The second parameter is the function performing the variable manipulation.

The value returned by the second parameter function is returned by `transform`. So, the `str3` variable holds the concatenated string.

You can use the returned value in the rest of the workflow, either to pass it as an input to other steps or to return it in the workflow's response.

***

## Example: Looping Over Array

Use `transform` to loop over arrays to create another variable from the array's items.

For example:

```ts collapsibleLines="1-7" expandButtonLabel="Show Imports"
import { 
  createWorkflow,
  WorkflowResponse,
  transform,
} from "@medusajs/framework/workflows-sdk"
// step imports...

type WorkflowInput = {
  items: {
    id: string
    name: string
  }[]
}

const myWorkflow = createWorkflow(
  "hello-world", 
  function ({ items }: WorkflowInput) {
    const ids = transform(
      { items },
      (data) => data.items.map((item) => item.id)
    )
    
    doSomethingStep(ids)

    // ...
  }
)
```

This workflow receives an `items` array in its input.

You use `transform` to create an `ids` variable, which is an array of strings holding the `id` of each item in the `items` array.

You then pass the `ids` variable as a parameter to the `doSomethingStep`.

***

## Example: Creating a Date

If you create a date with `new Date()` in a workflow's constructor function, Medusa evaluates the date's value when it creates the internal representation of the workflow, not when the workflow is executed.

So, use `transform` instead to create a date variable with `new Date()`.

For example:

```ts
const myWorkflow = createWorkflow(
  "hello-world",
  () => {
    const today = transform({}, () => new Date())

    doSomethingStep(today)
  }
)
```

In this workflow, `today` is only evaluated when the workflow is executed.

***

## Caveats

### Transform Evaluation

`transform`'s value is only evaluated if you pass its output to a step or in the workflow response.

For example, if you have the following workflow:

```ts
const myWorkflow = createWorkflow(
  "hello-world", 
  function (input) {
    const str = transform(
      { input },
      (data) => `${data.input.str1}${data.input.str2}`
    )

    return new WorkflowResponse("done")
  }
)
```

Since `str`'s value isn't used as a step's input or passed to `WorkflowResponse`, its value is never evaluated.

### Data Validation

`transform` should only be used to perform variable or data manipulation.

If you want to perform some validation on the data, use a step or [when-then](https://docs.medusajs.com/learn/fundamentals/workflows/conditions/index.html.md) instead.

For example:

```ts
// DON'T
const myWorkflow = createWorkflow(
  "hello-world", 
  function (input) {
    const str = transform(
      { input },
      (data) => {
        if (!input.str1) {
          throw new Error("Not allowed!")
        }
      }
    )
  }
)

// DO
const validateHasStr1Step = createStep(
  "validate-has-str1",
  ({ input }) => {
    if (!input.str1) {
      throw new Error("Not allowed!")
    }
  }
)

const myWorkflow = createWorkflow(
  "hello-world", 
  function (input) {
    validateHasStr1({
      input,
    })

    // workflow continues its execution only if 
    // the step doesn't throw the error.
  }
)
```


# Workflow Hooks

In this chapter, you'll learn what a workflow hook is and how to consume them.

## What is a Workflow Hook?

A workflow hook is a point in a workflow where you can inject custom functionality as a step function, called a hook handler.

Medusa exposes hooks in many of its workflows that are used in its API routes. You can consume those hooks to add your custom logic.

Refer to the [Workflows Reference](https://docs.medusajs.com/resources/medusa-workflows-reference/index.html.md) to view all workflows and their hooks.

You want to perform a custom action during a workflow's execution, such as when a product is created.

***

## How to Consume a Hook?

A workflow has a special `hooks` property which is an object that holds its hooks.

So, in a TypeScript or JavaScript file created under the `src/workflows/hooks` directory:

- Import the workflow.
- Access its hook using the `hooks` property.
- Pass the hook a step function as a parameter to consume it.

For example, to consume the `productsCreated` hook of Medusa's `createProductsWorkflow`, create the file `src/workflows/hooks/product-created.ts` with the following content:

```ts title="src/workflows/hooks/product-created.ts" highlights={handlerHighlights}
import { createProductsWorkflow } from "@medusajs/medusa/core-flows"

createProductsWorkflow.hooks.productsCreated(
  async ({ products }, { container }) => {
    // TODO perform an action
  }
)
```

The `productsCreated` hook is available on the workflow's `hooks` property by its name.

You invoke the hook, passing a step function (the hook handler) as a parameter.

Now, when a product is created using the [Create Product API route](https://docs.medusajs.com/api/admin#products_postproducts), your hook handler is executed after the product is created.

A hook can have only one handler.

Refer to the [createProductsWorkflow reference](https://docs.medusajs.com/resources/references/medusa-workflows/createProductsWorkflow/index.html.md) to see at which point the hook handler is executed.

### Hook Handler Parameter

Since a hook handler is essentially a step function, it receives the hook's input as a first parameter, and an object holding a `container` property as a second parameter.

Each hook has different input. For example, the `productsCreated` hook receives an object having a `products` property holding the created product.

### Hook Handler Compensation

Since the hook handler is a step function, you can set its compensation function as a second parameter of the hook.

For example:

```ts title="src/workflows/hooks/product-created.ts"
import { createProductsWorkflow } from "@medusajs/medusa/core-flows"

createProductsWorkflow.hooks.productsCreated(
  async ({ products }, { container }) => {
    // TODO perform an action

    return new StepResponse(undefined, { ids })
  },
  async ({ ids }, { container }) => {
    // undo the performed action
  }
)
```

The compensation function is executed if an error occurs in the workflow to undo the actions performed by the hook handler.

The compensation function receives as an input the second parameter passed to the `StepResponse` returned by the step function.

It also accepts as a second parameter an object holding a `container` property to resolve resources from the Medusa container.

### Additional Data Property

Medusa's workflows pass in the hook's input an `additional_data` property:

```ts title="src/workflows/hooks/product-created.ts" highlights={[["4", "additional_data"]]}
import { createProductsWorkflow } from "@medusajs/medusa/core-flows"

createProductsWorkflow.hooks.productsCreated(
  async ({ products, additional_data }, { container }) => {
    // TODO perform an action
  }
)
```

This property is an object that holds additional data passed to the workflow through the request sent to the API route using the workflow.

Learn how to pass `additional_data` in requests to API routes in [this chapter](https://docs.medusajs.com/learn/fundamentals/api-routes/additional-data/index.html.md).

### Pass Additional Data to Workflow

You can also pass that additional data when executing the workflow. Pass it as a parameter to the `.run` method of the workflow:

```ts title="src/workflows/hooks/product-created.ts" highlights={[["10", "additional_data"]]}
import type { MedusaRequest, MedusaResponse } from "@medusajs/framework/http"
import { createProductsWorkflow } from "@medusajs/medusa/core-flows"

export async function POST(req: MedusaRequest, res: MedusaResponse) {
  await createProductsWorkflow(req.scope).run({
    input: { 
      products: [
        // ...
      ], 
      additional_data: {
        custom_field: "test",
      },
    },
  })
}
```

Your hook handler then receives that passed data in the `additional_data` object.


# Workflow Timeout

In this chapter, you’ll learn how to set a timeout for workflows and steps.

## What is a Workflow Timeout?

By default, a workflow doesn’t have a timeout. It continues execution until it’s finished or an error occurs.

You can configure a workflow’s timeout to indicate how long the workflow can execute. If a workflow's execution time passes the configured timeout, it is failed and an error is thrown.

### Timeout Doesn't Stop Step Execution

Configuring a timeout doesn't stop the execution of a step in progress. The timeout only affects the status of the workflow and its result.

***

## Configure Workflow Timeout

The `createWorkflow` function can accept a configuration object instead of the workflow’s name.

In the configuration object, you pass a `timeout` property, whose value is a number indicating the timeout in seconds.

For example:

```ts title="src/workflows/hello-world.ts" highlights={[["16"]]} collapsibleLines="1-13" expandButtonLabel="Show More"
import { 
  createStep,  
  createWorkflow,
  WorkflowResponse,
} from "@medusajs/framework/workflows-sdk"

const step1 = createStep(
  "step-1",
  async () => {
    // ...
  }
)

const myWorkflow = createWorkflow({
  name: "hello-world",
  timeout: 2, // 2 seconds
}, function () {
  const str1 = step1()

  return new WorkflowResponse({
    message: str1,
  })
})

export default myWorkflow

```

This workflow's executions fail if they run longer than two seconds.

A workflow’s timeout error is returned in the `errors` property of the workflow’s execution, as explained in [this chapter](https://docs.medusajs.com/learn/fundamentals/workflows/errors/index.html.md). The error’s name is `TransactionTimeoutError`.

***

## Configure Step Timeout

Alternatively, you can configure the timeout for a step rather than the entire workflow.

As mentioned in the previous section, the timeout doesn't stop the execution of the step. It only affects the step's status and output.

The step’s configuration object accepts a `timeout` property, whose value is a number indicating the timeout in seconds.

For example:

```tsx
const step1 = createStep(
  {
    name: "step-1",
    timeout: 2, // 2 seconds
  },
  async () => {
    // ...
  }
)
```

This step's executions fail if they run longer than two seconds.

A step’s timeout error is returned in the `errors` property of the workflow’s execution, as explained in [this chapter](https://docs.medusajs.com/learn/fundamentals/workflows/errors/index.html.md). The error’s name is `TransactionStepTimeoutError`.


# Translate Medusa Admin

The Medusa Admin supports multiple languages, with the default being English. In this documentation, you'll learn how to contribute to the community by translating the Medusa Admin to a language you're fluent in.

{/* vale docs.We = NO */}

You can contribute either by translating the admin to a new language, or fixing translations for existing languages. As we can't validate every language's translations, some translations may be incorrect. Your contribution is welcome to fix any translation errors you find.

{/* vale docs.We = YES */}

Check out the translated languages either in the admin dashboard's settings or on [GitHub](https://github.com/medusajs/medusa/blob/develop/packages/admin/dashboard/src/i18n/languages.ts).

***

## How to Contribute Translation

1. Clone the [Medusa monorepository](https://github.com/medusajs/medusa) to your local machine:

```bash
git clone https://github.com/medusajs/medusa.git
```

If you already have it cloned, make sure to pull the latest changes from the `develop` branch.

2. Install the monorepository's dependencies. Since it's a Yarn workspace, it's highly recommended to use yarn:

```bash
yarn install
```

3. Create a branch that you'll use to open the pull request later:

```bash
git checkout -b feat/translate-<LANGUAGE>
```

Where `<LANGUAGE>` is your language name. For example, `feat/translate-da`.

4. Translation files are under `packages/admin/dashboard/src/i18n/translations` as JSON files whose names are the ISO-2 name of the language.
   - If you're adding a new language, copy the file `packages/admin/dashboard/src/i18n/translations/en.json` and paste it with the ISO-2 name for your language. For example, if you're adding Danish translations, copy the `en.json` file and paste it as `packages/admin/dashboard/src/i18n/translations/de.json`.
   - If you're fixing a translation, find the JSON file of the language under `packages/admin/dashboard/src/i18n/translations`.

5. Start translating the keys in the JSON file (or updating the targeted ones). All keys in the JSON file must be translated, and your PR tests will fail otherwise.
   - You can check whether the JSON file is valid by running the following command in `packages/admin/dashboard`, replacing `da.json` with the JSON file's name:

```bash title="packages/admin/dashboard"
yarn i18n:validate da.json
```

6. After finishing the translation, if you're adding a new language, import its JSON file in `packages/admin/dashboard/src/i18n/translations/index.ts` and add it to the exported object:

```ts title="packages/admin/dashboard/src/i18n/translations/index.ts" highlights={[["2"], ["6"], ["7"], ["8"]]}
// other imports...
import da from "./da.json"

export default {
  // other languages...
  da: {
    translation: da,
  },
}
```

The language's key in the object is the ISO-2 name of the language.

7. If you're adding a new language, add it to the file `packages/admin/dashboard/src/i18n/languages.ts`:

```ts title="packages/admin/dashboard/src/i18n/languages.ts" highlights={languageHighlights}
import { da } from "date-fns/locale"
// other imports...

export const languages: Language[] = [
  // other languages...
  {
    code: "da",
    display_name: "Danish",
    ltr: true,
    date_locale: da,
  },
]
```

`languages` is an array having the following properties:

- `code`: The ISO-2 name of the language. For example, `da` for Danish.
- `display_name`: The language's name to be displayed in the admin.
- `ltr`: Whether the language supports a left-to-right layout. For example, set this to `false` for languages like Arabic.
- `date_locale`: An instance of the locale imported from the [date-fns/locale](https://date-fns.org/) package.

8. Once you're done, push the changes into your branch and open a pull request on GitHub.

Our team will perform a general review on your PR and merge it if no issues are found. The translation will be available in the admin after the next release.


# Docs Contribution Guidelines

Thank you for your interest in contributing to the documentation! You will be helping the open source community and other developers interested in learning more about Medusa and using it.

This guide is specific to contributing to the documentation. If you’re interested in contributing to Medusa’s codebase, check out the [contributing guidelines in the Medusa GitHub repository](https://github.com/medusajs/medusa/blob/develop/CONTRIBUTING.md).

## What Can You Contribute?

You can contribute to the Medusa documentation in the following ways:

- Fixes to existing content. This includes small fixes like typos, or adding missing information.
- Additions to the documentation. If you think a documentation page can be useful to other developers, you can contribute by adding it.
  - Make sure to open an issue first in the [medusa repository](https://github.com/medusajs/medusa) to confirm that you can add that documentation page.
- Fixes to UI components and tooling. If you find a bug while browsing the documentation, you can contribute by fixing it.

***

## Documentation Workspace

Medusa's documentation projects are all part of the documentation `yarn` workspace, which you can find in the [medusa repository](https://github.com/medusajs/medusa) under the `www` directory.

The workspace has the following two directories:

- `apps`: this directory holds the different documentation websites and projects.
  - `book`: includes the codebase for the [main Medusa documentation](https://docs.medusajs.com//index.html.md). It's built with [Next.js 15](https://nextjs.org/).
  - `resources`: includes the codebase for the resources documentation, which powers different sections of the docs such as the [Integrations](https://docs.medusajs.com/resources/integrations/index.html.md) or [How-to & Tutorials](https://docs.medusajs.com/resources/how-to-tutorials/index.html.md) sections. It's built with [Next.js 15](https://nextjs.org/).
  - `api-reference`: includes the codebase for the API reference website. It's built with [Next.js 15](https://nextjs.org/).
  - `ui`: includes the codebase for the Medusa UI documentation website. It's built with [Next.js 15](https://nextjs.org/).
- `packages`: this directory holds the shared packages and components necessary for the development of the projects in the `apps` directory.
  - `docs-ui` includes the shared React components between the different apps.
  - `remark-rehype-plugins` includes Remark and Rehype plugins used by the documentation projects.

### Setup the Documentation Workspace

### Prerequisites

- [Node.js v20+](https://nodejs.org/en/download)
- [Yarn v3+](https://v3.yarnpkg.com/getting-started/install)

In the `www` directory, run the following command to install the dependencies:

```bash
yarn install
```

Then, run the following command to build packages under the `www/packages` directory:

```bash
yarn build
```

After that, you can change into the directory of any documentation project under the `www/apps` directory and run the `dev` command to start the development server.

***

## Documentation Content

All documentation projects are built with Next.js. The content is writtin in MDX files.

### Medusa Main Docs Content

The content of the Medusa main docs are under the `www/apps/book/app` directory.

### Medusa Resources Content

The content of all pages under the `/resources` path are under the `www/apps/resources/app` directory.

Documentation pages under the `www/apps/resources/references` directory are generated automatically from the source code under the `packages/medusa` directory. So, you can't directly make changes to them. Instead, you'll have to make changes to the comments in the original source code.

### API Reference

The API reference's content is split into two types:

1. Static content, which are the content related to getting started, expanding fields, and more. These are located in the `www/apps/api-reference/markdown` directory. They are MDX files.
2. OpenAPI specs that are shown to developers when checking the reference of an API Route. These are generated from OpenApi Spec comments, which are under the `www/utils/generated/oas-output` directory.

### Medusa UI Documentation

The content of the Medusa UI documentation are located under the `www/apps/ui/src/content/docs` directory. They are MDX files.

The UI documentation also shows code examples, which are under the `www/apps/ui/src/examples` directory.

The UI component props are generated from the source code and placed into the `www/apps/ui/src/specs` directory. To contribute to these props and their comments, check the comments in the source code under the `packages/design-system/ui` directory.

***

## Style Guide

When you contribute to the documentation content, make sure to follow the [documentation style guide](https://www.notion.so/Style-Guide-Docs-fad86dd1c5f84b48b145e959f36628e0).

***

## How to Contribute

If you’re fixing errors in an existing documentation page, you can scroll down to the end of the page and click on the “Edit this page” link. You’ll be redirected to the GitHub edit form of that page and you can make edits directly and submit a pull request (PR).

If you’re adding a new page or contributing to the codebase, fork the repository, create a new branch, and make all changes necessary in your repository. Then, once you’re done,  create a PR in the Medusa repository.

### Base Branch

When you make an edit to an existing documentation page or fork the repository to make changes to the documentation, create a new branch.

Documentation contributions always use `develop` as the base branch. Make sure to also open your PR against the `develop` branch.

### Branch Name

Make sure that the branch name starts with `docs/`. For example, `docs/fix-services`. Vercel deployed previews are only triggered for branches starting with `docs/`.

### Pull Request Conventions

When you create a pull request, prefix the title with `docs:` or `docs(PROJECT_NAME):`, where `PROJECT_NAME` is the name of the documentation project this pull request pertains to. For example, `docs(ui): fix titles`.

In the body of the PR, explain clearly what the PR does. If the PR solves an issue, use [closing keywords](https://docs.github.com/en/issues/tracking-your-work-with-issues/linking-a-pull-request-to-an-issue#linking-a-pull-request-to-an-issue-using-a-keyword) with the issue number. For example, “Closes #1333”.

***

## Images

If you are adding images to a documentation page, you can host the image on [Imgur](https://imgur.com) for free to include it in the PR. Our team will later upload it to our image hosting.

***

## NPM and Yarn Code Blocks

If you’re adding code blocks that use NPM and Yarn, you must add the `npm2yarn` meta field.

For example:

````md
```bash npm2yarn
npm run start
```
````

The code snippet must be written using NPM.

### Global Option

When a command uses the global option `-g`, add it at the end of the NPM command to ensure that it’s transformed to a Yarn command properly. For example:

```bash npm2yarn
npm install @medusajs/cli -g
```

***

## Linting with Vale

Medusa uses [Vale](https://vale.sh/) to lint documentation pages and perform checks on incoming PRs into the repository.

### Result of Vale PR Checks

You can check the result of running the "lint" action on your PR by clicking the Details link next to it. You can find there all errors that you need to fix.

### Run Vale Locally

If you want to check your work locally, you can do that by:

1. [Installing Vale](https://vale.sh/docs/vale-cli/installation/) on your machine.
2. Changing to the `www/vale` directory:

```bash
cd www/vale
```

3\. Running the `run-vale` script:

```bash
# to lint content for the main documentation
./run-vale.sh book/app/learn error resources
# to lint content for the resources documentation
./run-vale.sh resources/app error
# to lint content for the API reference
./run-vale.sh api-reference/markdown error
# to lint content for the Medusa UI documentation
./run-vale.sh ui/src/content/docs error
# to lint content for the user guide
./run-vale.sh user-guide/app error
```

{/* TODO need to enable MDX v1 comments first. */}

{/* ### Linter Exceptions

If it's needed to break some style guide rules in a document, you can wrap the parts that the linter shouldn't scan with the following comments in the `md` or `mdx` files:

```md
<!-- vale off -->

content that shouldn't be scanned for errors here...

<!-- vale on -->
```

You can also disable specific rules. For example:

```md
<!-- vale docs.Numbers = NO -->

Medusa supports Node versions 14 and 16.

<!-- vale docs.Numbers = YES -->
```

If you use this in your PR, you must justify its usage. */}

***

## Linting with ESLint

Medusa uses ESlint to lint code blocks both in the content and the code base of the documentation apps.

### Linting Content with ESLint

Each PR runs through a check that lints the code in the content files using ESLint. The action's name is `content-eslint`.

If you want to check content ESLint errors locally and fix them, you can do that by:

1\. Install the dependencies in the `www` directory:

```bash
yarn install
```

2\. Run the turbo command in the `www` directory:

```bash
turbo run lint:content
```

This will fix any fixable errors, and show errors that require your action.

### Linting Code with ESLint

Each PR runs through a check that lints the code in the content files using ESLint. The action's name is `code-docs-eslint`.

If you want to check code ESLint errors locally and fix them, you can do that by:

1\. Install the dependencies in the `www` directory:

```bash
yarn install
```

2\. Run the turbo command in the `www` directory:

```bash
yarn lint
```

This will fix any fixable errors, and show errors that require your action.

{/* TODO need to enable MDX v1 comments first. */}

{/* ### ESLint Exceptions

If some code blocks have errors that can't or shouldn't be fixed, you can add the following command before the code block:

~~~md
<!-- eslint-skip -->

```js
console.log("This block isn't linted")
```

```js
console.log("This block is linted")
```
~~~

You can also disable specific rules. For example:

~~~md
<!-- eslint-disable semi -->

```js
console.log("This block can use semicolons");
```

```js
console.log("This block can't use semi colons")
```
~~~ */}


# Example: Write Integration Tests for API Routes

In this chapter, you'll learn how to write integration tests for API routes using [medusaIntegrationTestRunner](https://docs.medusajs.com/learn/debugging-and-testing/testing-tools/integration-tests/index.html.md) from Medusa's Testing Framework.

### Prerequisites

- [Testing Tools Setup](https://docs.medusajs.com/learn/debugging-and-testing/testing-tools/index.html.md)

## Test a GET API Route

Consider the following API route created at `src/api/custom/route.ts`:

```ts title="src/api/custom/route.ts"
import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http"

export async function GET(
  req: MedusaRequest,
  res: MedusaResponse
){
  res.json({
    message: "Hello, World!",
  })
}
```

To write an integration test that tests this API route, create the file `integration-tests/http/custom-routes.spec.ts` with the following content:

```ts title="integration-tests/http/custom-routes.spec.ts" highlights={getHighlights}
import { medusaIntegrationTestRunner } from "@medusajs/test-utils"

medusaIntegrationTestRunner({
  testSuite: ({ api, getContainer }) => {
    describe("Custom endpoints", () => {
      describe("GET /custom", () => {
        it("returns correct message", async () => {
          const response = await api.get(
            `/custom`
          )
  
          expect(response.status).toEqual(200)
          expect(response.data).toHaveProperty("message")
          expect(response.data.message).toEqual("Hello, World!")
        })
      })
    })
  },
})

jest.setTimeout(60 * 1000)
```

You use the `medusaIntegrationTestRunner` to write your tests.

You add a single test that sends a `GET` request to `/custom` using the `api.get` method. For the test to pass, the response is expected to:

- Have a code status `200`,
- Have a `message` property in the returned data.
- Have the value of the `message` property equal to `Hello, World!`.

### Run Tests

Run the following command to run your tests:

```bash npm2yarn
npm run test:integration
```

If you don't have a `test:integration` script in `package.json`, refer to the [Medusa Testing Tools chapter](https://docs.medusajs.com/learn/debugging-and-testing/testing-tools#add-test-commands/index.html.md).

This runs your Medusa application and runs the tests available under the `src/integrations/http` directory.

### Jest Timeout

Since your tests connect to the database and perform actions that require more time than the typical tests, make sure to increase the timeout in your test:

```ts title="integration-tests/http/custom-routes.spec.ts"
// in your test's file
jest.setTimeout(60 * 1000)
```

***

## Test a POST API Route

Suppose you have a `blog` module whose main service extends the service factory, and that has the following model:

```ts title="src/modules/blog/models/my-custom.ts"
import { model } from "@medusajs/framework/utils"

const Post = model.define("post", {
  id: model.id().primaryKey(),
  name: model.text(),
})

export default Post
```

And consider that the file `src/api/custom/route.ts` defines another route handler for `POST` requests:

```ts title="src/api/custom/route.ts"
// other imports...
import BlogModuleService from "../../../modules/blog/service"
import { BLOG_MODULE } from "../../../modules/blog"

// ...

export async function POST(
  req: MedusaRequest,
  res: MedusaResponse
) {
  const blogModuleService: BlogModuleService = req.scope.resolve(
    BLOG_MODULE
  )

  const post = await blogModuleService.createPosts(
    req.body
  )

  res.json({
    post,
  })
}
```

This API route creates a new record of `Post`.

To write tests for this API route, add the following at the end of the `testSuite` function in `integration-tests/http/custom-routes.spec.ts`:

```ts title="integration-tests/http/custom-routes.spec.ts" highlights={postHighlights}
// other imports...
import BlogModuleService from "../../src/modules/blog/service"

medusaIntegrationTestRunner({
  testSuite: ({ api, getContainer }) => {
    describe("Custom endpoints", () => {
      // other tests...

      describe("POST /custom", () => {
        const id = "1"

        it("Creates my custom", async () => {
  
          const response = await api.post(
            `/custom`,
            {
              id,
              name: "Test",
            }
          )
  
          expect(response.status).toEqual(200)
          expect(response.data).toHaveProperty("post")
          expect(response.data.post).toEqual({
            id,
            name: "Test",
            created_at: expect.any(String),
            updated_at: expect.any(String),
          })
        })
      })
    })
  },
})
```

This adds a test for the `POST /custom` API route. It uses `api.post` to send the POST request. The `api.post` method accepts as a second parameter the data to pass in the request body.

The test passes if the response has:

- Status code `200`.
- A `post` property in its data.
- Its `id` and `name` match the ones provided to the request.

### Tear Down Created Record

To ensure consistency in the database for the rest of the tests after the above test is executed, utilize [Jest's setup and teardown hooks](https://jestjs.io/docs/setup-teardown) to delete the created record.

Use the `getContainer` function passed as a parameter to the `testSuite` function to resolve a service and use it for setup or teardown purposes

So, add an `afterAll` hook in the `describe` block for `POST /custom`:

```ts title="integration-tests/http/custom-routes.spec.ts"
// other imports...
import BlogModuleService from "../../src/modules/blog/service"
import { BLOG_MODULE } from "../../src/modules/blog"

medusaIntegrationTestRunner({
  testSuite: ({ api, getContainer }) => {
    describe("Custom endpoints", () => {
      // other tests...

      describe("POST /custom", () => {
        // ...
        afterAll(() => async () => {
          const blogModuleService: BlogModuleService = getContainer().resolve(
            BLOG_MODULE
          )

          await blogModuleService.deletePosts(id)
        })
      })
    })
  },
})
```

The `afterAll` hook resolves the `BlogModuleService` and use its `deletePosts` to delete the record created by the test.

***

## Test a DELETE API Route

Consider a `/custom/:id` API route created at `src/api/custom/[id]/route.ts`:

```ts title="src/api/custom/[id]/route.ts"
import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http"
import BlogModuleService from "../../../modules/blog/service"
import { BLOG_MODULE } from "../../../modules/blog"

export async function DELETE(
  req: MedusaRequest,
  res: MedusaResponse
) {
  const blogModuleService: BlogModuleService = req.scope.resolve(
    BLOG_MODULE
  )

  await blogModuleService.deletePosts(req.params.id)

  res.json({
    success: true,
  })
}
```

This API route accepts an ID path parameter, and uses the `BlogModuleService` to delete a `Post` record by that ID.

To add tests for this API route, add the following to `integration-tests/http/custom-routes.spec.ts`:

```ts title="integration-tests/http/custom-routes.spec.ts" highlights={deleteHighlights}
medusaIntegrationTestRunner({
  testSuite: ({ api, getContainer }) => {
    describe("Custom endpoints", () => {
      // ...

      describe("DELETE /custom/:id", () => {
        const id = "1"

        beforeAll(() => async () => {
          const blogModuleService: BlogModuleService = getContainer().resolve(
            BLOG_MODULE
          )

          await blogModuleService.createPosts({
            id,
            name: "Test",
          })
        })

        it("Deletes my custom", async () => {
          const response = await api.delete(
            `/custom/${id}`
          )

          expect(response.status).toEqual(200)
          expect(response.data).toHaveProperty("success")
          expect(response.data.success).toBeTruthy()
        })
      })
    })
  },
})
```

This adds a new test for the `DELETE /custom/:id` API route. You use the `beforeAll` hook to create a `Post` record using the `BlogModuleService`.

In the test, you use the `api.delete` method to send a `DELETE` request to `/custom/:id`. The test passes if the response:

- Has a `200` status code.
- Has a `success` property in its data.
- The `success` property's value is true.

***

## Pass Headers in Test Requests

Some requests require passing headers. For example, all routes prefixed with `/store` must pass a publishable API key in the header.

The `get`, `post`, and `delete` methods accept an optional third parameter that you can pass a `headers` property to, whose value is an object of headers to pass in the request.

### Pass Publishable API Key

For example, to pass a publishable API key in the header for a request to a `/store` route:

```ts title="integration-tests/http/custom-routes.spec.ts" highlights={headersHighlights}
import { medusaIntegrationTestRunner } from "@medusajs/test-utils"
import { ApiKeyDTO } from "@medusajs/framework/types"
import { createApiKeysWorkflow } from "@medusajs/medusa/core-flows"

medusaIntegrationTestRunner({
  testSuite: ({ api, getContainer }) => {
    describe("Custom endpoints", () => {
      let pak: ApiKeyDTO
      beforeAll(async () => {
        pak = (await createApiKeysWorkflow(getContainer()).run({
          input: {
            api_keys: [
              {
                type: "publishable",
                title: "Test Key",
                created_by: "",
              },
            ],
          },
        })).result[0]
      })
      describe("GET /custom", () => {
        it("returns correct message", async () => {
          const response = await api.get(
            `/store/custom`,
            {
              headers: {
                "x-publishable-api-key": pak.token,
              },
            }
          )
  
          expect(response.status).toEqual(200)
          expect(response.data).toHaveProperty("message")
          expect(response.data.message).toEqual("Hello, World!")
        })
      })
    })
  },
})

jest.setTimeout(60 * 1000)
```

In your test suit, you add a `beforeAll` hook to create a publishable API key before the tests run. To create the API key, you can use the `createApiKeysWorkflow` or the [API Key Module's service](https://docs.medusajs.com/resources/commerce-modules/api-key/index.html.md).

Then, in the test, you pass an object as the last parameter to `api.get` with a `headers` property. The `headers` property is an object with the key `x-publishable-api-key` and the value of the API key's token.

### Send Authenticated Requests

If your custom route is accessible by authenticated users only, such as routes prefixed by `/admin` or `/store/customers/me`, you can create a test customer or user, generate a JWT token for them, and pass the token in the request's Authorization header.

For example:

- The `jsonwebtoken` is available in your application by default.
- For custom actor types, you only need to change the `actorType` value in the `jwt.sign` method.

### Admin User

```ts title="integration-tests/http/custom-routes.spec.ts" highlights={adminHighlights}
import { medusaIntegrationTestRunner } from "@medusajs/test-utils"
import jwt from "jsonwebtoken"

medusaIntegrationTestRunner({
  testSuite: ({ api, getContainer }) => {
    describe("Custom endpoints", () => {
      describe("GET /custom", () => {
        const headers: Record<string, string> = {
        }
        beforeEach(async () => {
          const container = getContainer()
          
          const authModuleService = container.resolve("auth")
          const userModuleService = container.resolve("user")
          
          const user = await userModuleService.createUsers({
            email: "admin@medusa.js",
            
          })
          const authIdentity = await authModuleService.createAuthIdentities({
            provider_identities: [
              {
                provider: "emailpass",
                entity_id: "admin@medusa.js",
                provider_metadata: {
                  password: "supersecret",
                },
              },
            ],
            app_metadata: {
              user_id: user.id,
            },
          })
  
          const token = jwt.sign(
            {
              actor_id: user.id,
              actor_type: "user",
              auth_identity_id: authIdentity.id,
            },
            "supersecret",
            {
              expiresIn: "1d",
            }
          )
        
          headers["authorization"] = `Bearer ${token}`
        })
        it("returns correct message", async () => {
          const response = await api.get(
            `/admin/custom`,
            { headers }
          )
  
          expect(response.status).toEqual(200)
        })
      })
    })
  },
})

jest.setTimeout(60 * 1000)
```

### Customer User

```ts title="integration-tests/http/custom-routes.spec.ts" highlights={customerHighlights}
import { medusaIntegrationTestRunner } from "@medusajs/test-utils"
import { ApiKeyDTO } from "@medusajs/framework/types"
import jwt from "jsonwebtoken"
import { createApiKeysWorkflow } from "@medusajs/medusa/core-flows"

medusaIntegrationTestRunner({
  testSuite: ({ api, getContainer }) => {
    describe("Custom endpoints", () => {
      describe("GET /custom", () => {
        const headers: Record<string, string> = {
        }
        beforeEach(async () => {
          const container = getContainer()
          
          const authModuleService = container.resolve("auth")
          const customerModuleService = container.resolve("customer")
          
          const customer = await customerModuleService.createCustomers({
            email: "admin@medusa.js",
            
          })
          const authIdentity = await authModuleService.createAuthIdentities({
            provider_identities: [
              {
                provider: "emailpass",
                entity_id: "customer@medusa.js",
                provider_metadata: {
                  password: "supersecret",
                },
              },
            ],
            app_metadata: {
              user_id: customer.id,
            },
          })
  
          const token = jwt.sign(
            {
              actor_id: customer.id,
              actor_type: "customer",
              auth_identity_id: authIdentity.id,
            },
            "supersecret",
            {
              expiresIn: "1d",
            }
          )
        
          headers["authorization"] = `Bearer ${token}`


          const pak = (await createApiKeysWorkflow(getContainer()).run({
            input: {
              api_keys: [
                {
                  type: "publishable",
                  title: "Test Key",
                  created_by: "",
                },
              ],
            },
          })).result[0]

          headers["x-publishable-api-key"] = pak.token
        })
        it("returns correct message", async () => {
          const response = await api.get(
            `/store/customers/me/custom`,
            { headers }
          )
  
          expect(response.status).toEqual(200)
        })
      })
    })
  },
})

jest.setTimeout(60 * 1000)
```

In the test suite, you add a `beforeEach` hook that creates a user or customer, an auth identity, and generates a JWT token for them. The JWT token is then set in the `Authorization` header of the request.

You also create and pass a publishable API key in the header for the customer as it's required for requests to `/store` routes. Learn more in [this section](#pass-publishable-api-key).

***

## Upload Files in Test Requests

If your API route requires uploading a file, create a `FormData` object imported from the `form-data` object, then pass the form data headers in the request.

For example:

The `form-data` package is available by default.

```ts title="integration-tests/http/custom-routes.spec.ts"
import { medusaIntegrationTestRunner } from "@medusajs/test-utils"
import FormData from "form-data"

medusaIntegrationTestRunner({
  testSuite: ({ api, getContainer }) => {
    describe("Custom endpoints", () => {
      describe("GET /custom", () => {
        it("upload file", async () => {
          const form = new FormData()
          form.append("files", Buffer.from("content 1"), "image1.jpg")
          form.append("files", Buffer.from("content 2"), "image2.jpg")

          const response = await api.post(`/custom`, form, {
            headers: form.getHeaders(),
          })
  
          expect(response.status).toEqual(200)
          expect(response.data).toHaveProperty("files")
          expect(response.data.files).toEqual(
            expect.arrayContaining([
              expect.objectContaining({
                id: expect.any(String),
                url: expect.any(String),
              }),
            ])
          )
        })
      })
    })
  },
})

jest.setTimeout(60 * 1000)
```

You don't have to actually upload a file, you use the `form.append` method to append to a `files` field in the form data object, and you pass random content using the `Buffer.from` method.

Then, you pass to the `api.post` method the form data object as a second parameter, and an object with the `headers` property set to the form data object's headers as a third parameter.

If you're passing authentication or other headers, you can pass both the form data headers and the authentication headers in the same object:

```ts title="integration-tests/http/custom-routes.spec.ts"
const response = await api.post(`/custom`, form, {
  headers: {
    ...form.getHeaders(),
    ...authHeaders,
  },
})
```


# Example: Write Integration Tests for Workflows

In this chapter, you'll learn how to write integration tests for workflows using [medusaIntegrationTestRunner](https://docs.medusajs.com/learn/debugging-and-testing/testing-tools/integration-tests/index.html.md) from Medusa's Testing Framwork.

### Prerequisites

- [Testing Tools Setup](https://docs.medusajs.com/learn/debugging-and-testing/testing-tools/index.html.md)

## Write Integration Test for Workflow

Consider you have the following workflow defined at `src/workflows/hello-world.ts`:

```ts title="src/workflows/hello-world.ts"
import {
  createWorkflow,
  createStep,
  StepResponse,
  WorkflowResponse,
} from "@medusajs/framework/workflows-sdk"

const step1 = createStep("step-1", () => {
  return new StepResponse("Hello, World!")
})

export const helloWorldWorkflow = createWorkflow(
  "hello-world-workflow",
  () => {
    const message = step1()

    return new WorkflowResponse(message)
  }
)
```

To write a test for this workflow, create the file `integration-tests/http/workflow.spec.ts` with the following content:

```ts title="integration-tests/http/workflow.spec.ts"
import { medusaIntegrationTestRunner } from "@medusajs/test-utils"
import { helloWorldWorkflow } from "../../src/workflows/hello-world"

medusaIntegrationTestRunner({
  testSuite: ({ getContainer }) => {
    describe("Test hello-world workflow", () => {
      it("returns message", async () => {
        const { result } = await helloWorldWorkflow(getContainer())
          .run()

        expect(result).toEqual("Hello, World!")
      })
    })
  },
})

jest.setTimeout(60 * 1000)
```

You use the `medusaIntegrationTestRunner` to write an integration test for the workflow. The test pases if the workflow returns the string `"Hello, World!"`.

### Jest Timeout

Since your tests connect to the database and perform actions that require more time than the typical tests, make sure to increase the timeout in your test:

```ts title="integration-tests/http/custom-routes.spec.ts"
// in your test's file
jest.setTimeout(60 * 1000)
```

***

## Run Test

Run the following command to run your tests:

```bash npm2yarn
npm run test:integration
```

If you don't have a `test:integration` script in `package.json`, refer to the [Medusa Testing Tools chapter](https://docs.medusajs.com/learn/debugging-and-testing/testing-tools#add-test-commands/index.html.md).

This runs your Medusa application and runs the tests available under the `integrations/http` directory.

***

## Test That a Workflow Throws an Error

You might want to test that a workflow throws an error in certain cases. To test this:

- Disable the `throwOnError` option when executing the workflow.
- Use the returned `errors` property to check what errors were thrown.

For example, if you have a step that throws this error:

```ts title="src/workflows/hello-world.ts"
import { MedusaError } from "@medusajs/framework/utils"
import { createStep } from "@medusajs/framework/workflows-sdk"

const step1 = createStep("step-1", () => {
  throw new MedusaError(MedusaError.Types.NOT_FOUND, "Item doesn't exist")
})
```

You can write the following test to ensure that the workflow throws that error:

```ts title="integration-tests/http/workflow.spec.ts"
import { medusaIntegrationTestRunner } from "@medusajs/test-utils"
import { helloWorldWorkflow } from "../../src/workflows/hello-world"

medusaIntegrationTestRunner({
  testSuite: ({ getContainer }) => {
    describe("Test hello-world workflow", () => {
      it("returns message", async () => {
        const { errors } = await helloWorldWorkflow(getContainer())
          .run({
            throwOnError: false,
          })

        expect(errors.length).toBeGreaterThan(0)
        expect(errors[0].error.message).toBe("Item doesn't exist")
      })
    })
  },
})

jest.setTimeout(60 * 1000)
```

The `errors` property contains an array of errors thrown during the execution of the workflow. Each error item has an `error` object, being the error thrown.

If you threw a `MedusaError`, then you can check the error message in `errors[0].error.message`.


# Example: Integration Tests for a Module

In this chapter, find an example of writing an integration test for a module using [moduleIntegrationTestRunner](https://docs.medusajs.com/learn/debugging-and-testing/testing-tools/modules-tests/index.html.md) from Medusa's Testing Framework.

### Prerequisites

- [Testing Tools Setup](https://docs.medusajs.com/learn/debugging-and-testing/testing-tools/index.html.md)

## Write Integration Test for Module

Consider a `blog` module with a `BlogModuleService` that has a `getMessage` method:

```ts title="src/modules/blog/service.ts"
import { MedusaService } from "@medusajs/framework/utils"
import MyCustom from "./models/my-custom"

class BlogModuleService extends MedusaService({
  MyCustom,
}){
  getMessage(): string {
    return "Hello, World!"
  }
}

export default BlogModuleService
```

To create an integration test for the method, create the file `src/modules/blog/__tests__/service.spec.ts` with the following content:

```ts title="src/modules/blog/__tests__/service.spec.ts"
import { moduleIntegrationTestRunner } from "@medusajs/test-utils"
import { BLOG_MODULE } from ".."
import BlogModuleService from "../service"
import MyCustom from "../models/my-custom"

moduleIntegrationTestRunner<BlogModuleService>({
  moduleName: BLOG_MODULE,
  moduleModels: [MyCustom],
  resolve: "./src/modules/blog",
  testSuite: ({ service }) => {
    describe("BlogModuleService", () => {
      it("says hello world", () => {
        const message = service.getMessage()

        expect(message).toEqual("Hello, World!")
      })
    })
  },
})

jest.setTimeout(60 * 1000)
```

You use the `moduleIntegrationTestRunner` function to add tests for the `blog` module. You have one test that passes if the `getMessage` method returns the `"Hello, World!"` string.

***

## Run Test

Run the following command to run your module integration tests:

```bash npm2yarn
npm run test:integration:modules
```

If you don't have a `test:integration:modules` script in `package.json`, refer to the [Medusa Testing Tools chapter](https://docs.medusajs.com/learn/debugging-and-testing/testing-tools#add-test-commands/index.html.md).

This runs your Medusa application and runs the tests available in any `__tests__` directory under the `src/modules` directory.


# Commerce Modules

In this section of the documentation, you'll find guides and references related to Medusa's Commerce Modules.

A Commerce Module provides features for a commerce domain within its service. The Medusa application exposes these features in its API routes to clients.

A Commerce Module also defines data models, representing tables in the database. The Medusa Framework and tools allow you to extend these data models to add custom fields.

## Commerce Modules List

- [API Key Module](https://docs.medusajs.com/commerce-modules/api-key/index.html.md)
- [Auth Module](https://docs.medusajs.com/commerce-modules/auth/index.html.md)
- [Cart Module](https://docs.medusajs.com/commerce-modules/cart/index.html.md)
- [Currency Module](https://docs.medusajs.com/commerce-modules/currency/index.html.md)
- [Customer Module](https://docs.medusajs.com/commerce-modules/customer/index.html.md)
- [Fulfillment Module](https://docs.medusajs.com/commerce-modules/fulfillment/index.html.md)
- [Inventory Module](https://docs.medusajs.com/commerce-modules/inventory/index.html.md)
- [Order Module](https://docs.medusajs.com/commerce-modules/order/index.html.md)
- [Payment Module](https://docs.medusajs.com/commerce-modules/payment/index.html.md)
- [Pricing Module](https://docs.medusajs.com/commerce-modules/pricing/index.html.md)
- [Product Module](https://docs.medusajs.com/commerce-modules/product/index.html.md)
- [Promotion Module](https://docs.medusajs.com/commerce-modules/promotion/index.html.md)
- [Region Module](https://docs.medusajs.com/commerce-modules/region/index.html.md)
- [Sales Channel Module](https://docs.medusajs.com/commerce-modules/sales-channel/index.html.md)
- [Stock Location Module](https://docs.medusajs.com/commerce-modules/stock-location/index.html.md)
- [Store Module](https://docs.medusajs.com/commerce-modules/store/index.html.md)
- [Tax Module](https://docs.medusajs.com/commerce-modules/tax/index.html.md)
- [User Module](https://docs.medusajs.com/commerce-modules/user/index.html.md)

***

## How to Use Modules

The Commerce Modules can be used in many use cases, including:

- Medusa Application: The Medusa application uses the Commerce Modules to expose commerce features through the REST APIs.
- Serverless Application: Use the Commerce Modules in a serverless application, such as a Next.js application, without having to manage a fully-fledged ecommerce system. You can use it by installing it in your Node.js project as an NPM package.
- Node.js Application: Use the Commerce Modules in any Node.js application by installing it with NPM.


# API Key Module

In this section of the documentation, you will find resources to learn more about the API Key Module and how to use it in your application.

Refer to the [Medusa Admin User Guide](https://docs.medusajs.com/user-guide/settings/developer/index.html.md) to learn how to manage publishable and secret API keys using the dashboard.

Medusa has API-key related features available out-of-the-box through the API Key Module. A [module](https://docs.medusajs.com/docs/learn/fundamentals/modules/index.html.md) is a standalone package that provides features for a single domain. Each of Medusa's commerce features are placed in Commerce Modules, such as this API Key Module.

Learn more about why modules are isolated in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/modules/isolation/index.html.md).

## API Key Features

- [API Key Types and Management](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/api-key/concepts/index.html.md): Manage API keys in your store. You can create both publishable and secret API keys for different use cases.
- [Token Verification](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/api-key/concepts#token-verification/index.html.md): Verify tokens of secret API keys to authenticate users or actions.
- [Revoke Keys](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/api-key/concepts#api-key-expiration/index.html.md): Revoke keys to disable their use permanently.
- Roll API Keys: Roll API keys by [revoking](https://docs.medusajs.com/references/api-key/revoke/index.html.md) a key then [re-creating it](https://docs.medusajs.com/references/api-key/createApiKeys/index.html.md).

***

## How to Use the API Key Module

In your Medusa application, you build flows around Commerce Modules. A flow is built as a [Workflow](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md), which is a special function composed of a series of steps that guarantees data consistency and reliable roll-back mechanism.

You can build custom workflows and steps. You can also re-use Medusa's workflows and steps, which are provided by the `@medusajs/medusa/core-flows` package.

For example:

```ts title="src/workflows/create-api-key.ts" highlights={highlights}
import { 
  createWorkflow, 
  WorkflowResponse,
  createStep,
  StepResponse,
} from "@medusajs/framework/workflows-sdk"
import { Modules } from "@medusajs/framework/utils"

const createApiKeyStep = createStep(
  "create-api-key",
  async ({}, { container }) => {
    const apiKeyModuleService = container.resolve(Modules.API_KEY)

    const apiKey = await apiKeyModuleService.createApiKeys({
      title: "Publishable API key",
      type: "publishable",
      created_by: "user_123",
    })

    return new StepResponse({ apiKey }, apiKey.id)
  },
  async (apiKeyId, { container }) => {
    const apiKeyModuleService = container.resolve(Modules.API_KEY)

    await apiKeyModuleService.deleteApiKeys([apiKeyId])
  }
)

export const createApiKeyWorkflow = createWorkflow(
  "create-api-key",
  () => {
    const { apiKey } = createApiKeyStep()

    return new WorkflowResponse({
      apiKey,
    })
  }
)
```

You can then execute the workflow in your custom API routes, scheduled jobs, or subscribers:

### API Route

```ts title="src/api/workflow/route.ts" highlights={[["11"], ["12"]]} collapsibleLines="1-6" expandButtonLabel="Show Imports"
import type {
  MedusaRequest,
  MedusaResponse,
} from "@medusajs/framework/http"
import { createApiKeyWorkflow } from "../../workflows/create-api-key"

export async function GET(
  req: MedusaRequest,
  res: MedusaResponse
) {
  const { result } = await createApiKeyWorkflow(req.scope)
    .run()

  res.send(result)
}
```

### Subscriber

```ts title="src/subscribers/user-created.ts" highlights={[["11"], ["12"]]} collapsibleLines="1-6" expandButtonLabel="Show Imports"
import {
  type SubscriberConfig,
  type SubscriberArgs,
} from "@medusajs/framework"
import { createApiKeyWorkflow } from "../workflows/create-api-key"

export default async function handleUserCreated({
  event: { data },
  container,
}: SubscriberArgs<{ id: string }>) {
  const { result } = await createApiKeyWorkflow(container)
    .run()

  console.log(result)
}

export const config: SubscriberConfig = {
  event: "user.created",
}
```

### Scheduled Job

```ts title="src/jobs/run-daily.ts" highlights={[["7"], ["8"]]}
import { MedusaContainer } from "@medusajs/framework/types"
import { createApiKeyWorkflow } from "../workflows/create-api-key"

export default async function myCustomJob(
  container: MedusaContainer
) {
  const { result } = await createApiKeyWorkflow(container)
    .run()

  console.log(result)
}

export const config = {
  name: "run-once-a-day",
  schedule: `0 0 * * *`,
}
```

Learn more about workflows in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md).

***


# Auth Module

In this section of the documentation, you will find resources to learn more about the Auth Module and how to use it in your application.

Medusa has auth related features available out-of-the-box through the Auth Module. A [module](https://docs.medusajs.com/docs/learn/fundamentals/modules/index.html.md) is a standalone package that provides features for a single domain. Each of Medusa's commerce features are placed in Commerce Modules, such as this Auth Module.

Learn more about why modules are isolated in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/modules/isolation/index.html.md).

## Auth Features

- [Basic User Authentication](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/auth/authentication-route#1-basic-authentication-flow/index.html.md): Authenticate users using their email and password credentials.
- [Third-Party and Social Authentication](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/auth/authentication-route#2-third-party-service-authenticate-flow/index.html.md): Authenticate users using third-party services and social platforms, such as [Google](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/auth/auth-providers/google/index.html.md) and [GitHub](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/auth/auth-providers/github/index.html.md).
- [Authenticate Custom Actor Types](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/auth/create-actor-type/index.html.md): Create custom user or actor types, such as managers, authenticate them in your application, and guard routes based on the custom user types.
- [Custom Authentication Providers](https://docs.medusajs.com/references/auth/provider/index.html.md): Integrate third-party services with custom authentication providors.

***

## How to Use the Auth Module

In your Medusa application, you build flows around Commerce Modules. A flow is built as a [Workflow](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md), which is a special function composed of a series of steps that guarantees data consistency and reliable roll-back mechanism.

You can build custom workflows and steps. You can also re-use Medusa's workflows and steps, which are provided by the `@medusajs/medusa/core-flows` package.

For example:

```ts title="src/workflows/authenticate-user.ts" highlights={highlights}
import { 
  createWorkflow, 
  WorkflowResponse,
  createStep,
  StepResponse,
} from "@medusajs/framework/workflows-sdk"
import { Modules, MedusaError } from "@medusajs/framework/utils"
import { MedusaRequest } from "@medusajs/framework/http"
import { AuthenticationInput } from "@medusajs/framework/types"

type Input = {
  req: MedusaRequest
}

const authenticateUserStep = createStep(
  "authenticate-user",
  async ({ req }: Input, { container }) => {
    const authModuleService = container.resolve(Modules.AUTH)

    const { success, authIdentity, error } = await authModuleService
      .authenticate(
        "emailpass",
       {
          url: req.url,
          headers: req.headers,
          query: req.query,
          body: req.body,
          authScope: "admin", // or custom actor type
          protocol: req.protocol,
        } as AuthenticationInput
      )

    if (!success) {
      // incorrect authentication details
      throw new MedusaError(
        MedusaError.Types.UNAUTHORIZED,
        error || "Incorrect authentication details"
      )
    }

    return new StepResponse({ authIdentity }, authIdentity?.id)
  },
  async (authIdentityId, { container }) => {
    if (!authIdentityId) {
      return
    }
    
    const authModuleService = container.resolve(Modules.AUTH)

    await authModuleService.deleteAuthIdentities([authIdentityId])
  }
)

export const authenticateUserWorkflow = createWorkflow(
  "authenticate-user",
  (input: Input) => {
    const { authIdentity } = authenticateUserStep(input)

    return new WorkflowResponse({
      authIdentity,
    })
  }
)
```

You can then execute the workflow in your custom API routes, scheduled jobs, or subscribers:

```ts title="API Route" highlights={[["11"], ["12"]]} collapsibleLines="1-6" expandButtonLabel="Show Imports"
import type {
  MedusaRequest,
  MedusaResponse,
} from "@medusajs/framework/http"
import { authenticateUserWorkflow } from "../../workflows/authenticate-user"

export async function GET(
  req: MedusaRequest,
  res: MedusaResponse
) {
  const { result } = await authenticateUserWorkflow(req.scope)
    .run({
      req,
    })

  res.send(result)
}
```

Learn more about workflows in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md).

***

## Configure Auth Module

The Auth Module accepts options for further configurations. Refer to [this documentation](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/auth/module-options/index.html.md) for details on the module's options.

***

## Providers

Medusa provides the following authentication providers out-of-the-box. You can use them to authenticate admin users, customers, or custom actor types.

***


# Customer Module

In this section of the documentation, you will find resources to learn more about the Customer Module and how to use it in your application.

Refer to the [Medusa Admin User Guide](https://docs.medusajs.com/user-guide/customers/index.html.md) to learn how to manage customers and groups using the dashboard.

Medusa has customer related features available out-of-the-box through the Customer Module. A [module](https://docs.medusajs.com/docs/learn/fundamentals/modules/index.html.md) is a standalone package that provides features for a single domain. Each of Medusa's commerce features are placed in Commerce Modules, such as this Customer Module.

Learn more about why modules are isolated in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/modules/isolation/index.html.md).

## Customer Features

- [Customer Management](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/customer/customer-accounts/index.html.md): Store and manage guest and registered customers in your store.
- [Customer Organization](https://docs.medusajs.com/references/customer/models/index.html.md): Organize customers into groups. This has a lot of benefits and supports many use cases, such as provide discounts for specific customer groups using the [Promotion Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/promotion/index.html.md).

***

## How to Use the Customer Module

In your Medusa application, you build flows around Commerce Modules. A flow is built as a [Workflow](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md), which is a special function composed of a series of steps that guarantees data consistency and reliable roll-back mechanism.

You can build custom workflows and steps. You can also re-use Medusa's workflows and steps, which are provided by the `@medusajs/medusa/core-flows` package.

For example:

```ts title="src/workflows/create-customer.ts" highlights={highlights}
import { 
  createWorkflow, 
  WorkflowResponse,
  createStep,
  StepResponse,
} from "@medusajs/framework/workflows-sdk"
import { Modules } from "@medusajs/framework/utils"

const createCustomerStep = createStep(
  "create-customer",
  async ({}, { container }) => {
    const customerModuleService = container.resolve(Modules.CUSTOMER)

    const customer = await customerModuleService.createCustomers({
      first_name: "Peter",
      last_name: "Hayes",
      email: "peter.hayes@example.com",
    })

    return new StepResponse({ customer }, customer.id)
  },
  async (customerId, { container }) => {
    if (!customerId) {
      return
    }
    const customerModuleService = container.resolve(Modules.CUSTOMER)

    await customerModuleService.deleteCustomers([customerId])
  }
)

export const createCustomerWorkflow = createWorkflow(
  "create-customer",
  () => {
    const { customer } = createCustomerStep()

    return new WorkflowResponse({
      customer,
    })
  }
)
```

You can then execute the workflow in your custom API routes, scheduled jobs, or subscribers:

### API Route

```ts title="src/api/workflow/route.ts" highlights={[["11"], ["12"]]} collapsibleLines="1-6" expandButtonLabel="Show Imports"
import type {
  MedusaRequest,
  MedusaResponse,
} from "@medusajs/framework/http"
import { createCustomerWorkflow } from "../../workflows/create-customer"

export async function GET(
  req: MedusaRequest,
  res: MedusaResponse
) {
  const { result } = await createCustomerWorkflow(req.scope)
    .run()

  res.send(result)
}
```

### Subscriber

```ts title="src/subscribers/user-created.ts" highlights={[["11"], ["12"]]} collapsibleLines="1-6" expandButtonLabel="Show Imports"
import {
  type SubscriberConfig,
  type SubscriberArgs,
} from "@medusajs/framework"
import { createCustomerWorkflow } from "../workflows/create-customer"

export default async function handleUserCreated({
  event: { data },
  container,
}: SubscriberArgs<{ id: string }>) {
  const { result } = await createCustomerWorkflow(container)
    .run()

  console.log(result)
}

export const config: SubscriberConfig = {
  event: "user.created",
}
```

### Scheduled Job

```ts title="src/jobs/run-daily.ts" highlights={[["7"], ["8"]]}
import { MedusaContainer } from "@medusajs/framework/types"
import { createCustomerWorkflow } from "../workflows/create-customer"

export default async function myCustomJob(
  container: MedusaContainer
) {
  const { result } = await createCustomerWorkflow(container)
    .run()

  console.log(result)
}

export const config = {
  name: "run-once-a-day",
  schedule: `0 0 * * *`,
}
```

Learn more about workflows in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md).

***


# Fulfillment Module

In this section of the documentation, you will find resources to learn more about the Fulfillment Module and how to use it in your application.

Refer to the Medusa Admin User Guide to learn how to use the dashboard to:

- [Manage order fulfillments](https://docs.medusajs.com/user-guide/orders/fulfillments/index.html.md).
- [Manage shipping options and profiles](https://docs.medusajs.com/user-guide/settings/locations-and-shipping/index.html.md).

Medusa has fulfillment related features available out-of-the-box through the Fulfillment Module. A [module](https://docs.medusajs.com/docs/learn/fundamentals/modules/index.html.md) is a standalone package that provides features for a single domain. Each of Medusa's commerce features are placed in Commerce Modules, such as this Fulfillment Module.

Learn more about why modules are isolated in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/modules/isolation/index.html.md).

## Fulfillment Features

- [Fulfillment Management](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/fulfillment/item-fulfillment/index.html.md): Create fulfillments and keep track of their status, items, and more.
- [Integrate Third-Party Fulfillment Providers](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/fulfillment/fulfillment-provider/index.html.md): Create third-party fulfillment providers to provide customers with shipping options and fulfill their orders.
- [Restrict By Location and Rules](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/fulfillment/shipping-option/index.html.md): Shipping options can be restricted to specific geographical locations. You can also specify custom rules to restrict shipping options.
- [Support Different Fulfillment Forms](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/fulfillment/concepts/index.html.md): Support various fulfillment forms, such as shipping or pick up.
- [Tiered Pricing and Price Rules](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/pricing/price-rules/index.html.md): Set prices for shipping options with tiers and rules, allowing you to create complex pricing strategies.

***

## How to Use the Fulfillment Module

In your Medusa application, you build flows around Commerce Modules. A flow is built as a [Workflow](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md), which is a special function composed of a series of steps that guarantees data consistency and reliable roll-back mechanism.

You can build custom workflows and steps. You can also re-use Medusa's workflows and steps, which are provided by the `@medusajs/medusa/core-flows` package.

For example:

```ts title="src/workflows/create-fulfillment.ts" highlights={highlights}
import { 
  createWorkflow, 
  WorkflowResponse,
  createStep,
  StepResponse,
} from "@medusajs/framework/workflows-sdk"
import { Modules } from "@medusajs/framework/utils"

const createFulfillmentStep = createStep(
  "create-fulfillment",
  async ({}, { container }) => {
    const fulfillmentModuleService = container.resolve(Modules.FULFILLMENT)

    const fulfillment = await fulfillmentModuleService.createFulfillment({
      location_id: "loc_123",
      provider_id: "webshipper",
      delivery_address: {
        country_code: "us",
        city: "Strongsville",
        address_1: "18290 Royalton Rd",
      },
      items: [
        {
          title: "Shirt",
          sku: "SHIRT",
          quantity: 1,
          barcode: "123456",
        },
      ],
      labels: [],
      order: {},
    })

    return new StepResponse({ fulfillment }, fulfillment.id)
  },
  async (fulfillmentId, { container }) => {
    if (!fulfillmentId) {
      return
    }
    const fulfillmentModuleService = container.resolve(Modules.FULFILLMENT)

    await fulfillmentModuleService.deleteFulfillment(fulfillmentId)
  }
)

export const createFulfillmentWorkflow = createWorkflow(
  "create-fulfillment",
  () => {
    const { fulfillment } = createFulfillmentStep()

    return new WorkflowResponse({
      fulfillment,
    })
  }
)
```

You can then execute the workflow in your custom API routes, scheduled jobs, or subscribers:

### API Route

```ts title="src/api/workflow/route.ts" highlights={[["11"], ["12"]]} collapsibleLines="1-6" expandButtonLabel="Show Imports"
import type {
  MedusaRequest,
  MedusaResponse,
} from "@medusajs/framework/http"
import { createFulfillmentWorkflow } from "../../workflows/create-fuilfillment"

export async function GET(
  req: MedusaRequest,
  res: MedusaResponse
) {
  const { result } = await createFulfillmentWorkflow(req.scope)
    .run()

  res.send(result)
}
```

### Subscriber

```ts title="src/subscribers/user-created.ts" highlights={[["11"], ["12"]]} collapsibleLines="1-6" expandButtonLabel="Show Imports"
import {
  type SubscriberConfig,
  type SubscriberArgs,
} from "@medusajs/framework"
import { createFulfillmentWorkflow } from "../workflows/create-fuilfillment"

export default async function handleUserCreated({
  event: { data },
  container,
}: SubscriberArgs<{ id: string }>) {
  const { result } = await createFulfillmentWorkflow(container)
    .run()

  console.log(result)
}

export const config: SubscriberConfig = {
  event: "user.created",
}
```

### Scheduled Job

```ts title="src/jobs/run-daily.ts" highlights={[["7"], ["8"]]}
import { MedusaContainer } from "@medusajs/framework/types"
import { createFulfillmentWorkflow } from "../workflows/create-fuilfillment"

export default async function myCustomJob(
  container: MedusaContainer
) {
  const { result } = await createFulfillmentWorkflow(container)
    .run()

  console.log(result)
}

export const config = {
  name: "run-once-a-day",
  schedule: `0 0 * * *`,
}
```

Learn more about workflows in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md).

***

## Configure Fulfillment Module

The Fulfillment Module accepts options for further configurations. Refer to [this documentation](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/fulfillment/module-options/index.html.md) for details on the module's options.

***


# Inventory Module

In this section of the documentation, you will find resources to learn more about the Inventory Module and how to use it in your application.

Refer to the [Medusa Admin User Guide](https://docs.medusajs.com/user-guide/inventory/index.html.md) to learn how to manage inventory and related features using the dashboard.

Medusa has inventory related features available out-of-the-box through the Inventory Module. A [module](https://docs.medusajs.com/docs/learn/fundamentals/modules/index.html.md) is a standalone package that provides features for a single domain. Each of Medusa's commerce features are placed in Commerce Modules, such as this Inventory Module.

Learn more about why modules are isolated in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/modules/isolation/index.html.md).

## Inventory Features

- [Inventory Items Management](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/inventory/concepts/index.html.md): Store and manage inventory of any stock-kept item, such as product variants.
- [Inventory Across Locations](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/inventory/concepts#inventorylevel/index.html.md): Manage inventory levels across different locations, such as warehouses.
- [Reservation Management](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/inventory/concepts#reservationitem/index.html.md): Reserve quantities of inventory items at specific locations for orders or other purposes.
- [Check Inventory Availability](https://docs.medusajs.com/references/inventory-next/confirmInventory/index.html.md): Check whether an inventory item has the necessary quantity for purchase.
- [Inventory Kits](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/inventory/inventory-kit/index.html.md): Create and manage inventory kits for a single product, allowing you to implement use cases like bundled or multi-part products.

***

## How to Use the Inventory Module

In your Medusa application, you build flows around Commerce Modules. A flow is built as a [Workflow](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md), which is a special function composed of a series of steps that guarantees data consistency and reliable roll-back mechanism.

You can build custom workflows and steps. You can also re-use Medusa's workflows and steps, which are provided by the `@medusajs/medusa/core-flows` package.

For example:

```ts title="src/workflows/create-inventory-item.ts" highlights={highlights}
import { 
  createWorkflow, 
  WorkflowResponse,
  createStep,
  StepResponse,
} from "@medusajs/framework/workflows-sdk"
import { Modules } from "@medusajs/framework/utils"

const createInventoryItemStep = createStep(
  "create-inventory-item",
  async ({}, { container }) => {
    const inventoryModuleService = container.resolve(Modules.INVENTORY)

    const inventoryItem = await inventoryModuleService.createInventoryItems({
      sku: "SHIRT",
      title: "Green Medusa Shirt",
      requires_shipping: true,
    })

    return new StepResponse({ inventoryItem }, inventoryItem.id)
  },
  async (inventoryItemId, { container }) => {
    if (!inventoryItemId) {
      return
    }
    const inventoryModuleService = container.resolve(Modules.INVENTORY)

    await inventoryModuleService.deleteInventoryItems([inventoryItemId])
  }
)

export const createInventoryItemWorkflow = createWorkflow(
  "create-inventory-item-workflow",
  () => {
    const { inventoryItem } = createInventoryItemStep()

    return new WorkflowResponse({
      inventoryItem,
    })
  }
)
```

You can then execute the workflow in your custom API routes, scheduled jobs, or subscribers:

### API Route

```ts title="src/api/workflow/route.ts" highlights={[["11"], ["12"]]} collapsibleLines="1-6" expandButtonLabel="Show Imports"
import type {
  MedusaRequest,
  MedusaResponse,
} from "@medusajs/framework/http"
import { createInventoryItemWorkflow } from "../../workflows/create-inventory-item"

export async function GET(
  req: MedusaRequest,
  res: MedusaResponse
) {
  const { result } = await createInventoryItemWorkflow(req.scope)
    .run()

  res.send(result)
}
```

### Subscriber

```ts title="src/subscribers/user-created.ts" highlights={[["11"], ["12"]]} collapsibleLines="1-6" expandButtonLabel="Show Imports"
import {
  type SubscriberConfig,
  type SubscriberArgs,
} from "@medusajs/framework"
import { createInventoryItemWorkflow } from "../workflows/create-inventory-item"

export default async function handleUserCreated({
  event: { data },
  container,
}: SubscriberArgs<{ id: string }>) {
  const { result } = await createInventoryItemWorkflow(container)
    .run()

  console.log(result)
}

export const config: SubscriberConfig = {
  event: "user.created",
}
```

### Scheduled Job

```ts title="src/jobs/run-daily.ts" highlights={[["7"], ["8"]]}
import { MedusaContainer } from "@medusajs/framework/types"
import { createInventoryItemWorkflow } from "../workflows/create-inventory-item"

export default async function myCustomJob(
  container: MedusaContainer
) {
  const { result } = await createInventoryItemWorkflow(container)
    .run()

  console.log(result)
}

export const config = {
  name: "run-once-a-day",
  schedule: `0 0 * * *`,
}
```

Learn more about workflows in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md).

***


# Currency Module

In this section of the documentation, you will find resources to learn more about the Currency Module and how to use it in your application.

Refer to the [Medusa Admin User Guide](https://docs.medusajs.com/user-guide/settings/store/index.html.md) to learn how to manage your store's currencies using the dashboard.

Medusa has currency related features available out-of-the-box through the Currency Module. A [module](https://docs.medusajs.com/docs/learn/fundamentals/modules/index.html.md) is a standalone package that provides features for a single domain. Each of Medusa's commerce features are placed in Commerce Modules, such as this Currency Module.

Learn more about why modules are isolated in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/modules/isolation/index.html.md).

## Currency Features

- [Currency Management and Retrieval](https://docs.medusajs.com/references/currency/listAndCountCurrencies/index.html.md): This module adds all common currencies to your application and allows you to retrieve them.
- [Support Currencies in Modules](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/currency/links-to-other-modules/index.html.md): Other Commerce Modules use currency codes in their data models or operations. Use the Currency Module to retrieve a currency code and its details.

***

## How to Use the Currency Module

In your Medusa application, you build flows around Commerce Modules. A flow is built as a [Workflow](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md), which is a special function composed of a series of steps that guarantees data consistency and reliable roll-back mechanism.

You can build custom workflows and steps. You can also re-use Medusa's workflows and steps, which are provided by the `@medusajs/medusa/core-flows` package.

For example:

```ts title="src/workflows/retrieve-price-with-currency.ts" highlights={highlights}
import { 
  createWorkflow, 
  WorkflowResponse,
  createStep,
  StepResponse,
  transform,
} from "@medusajs/framework/workflows-sdk"
import { Modules } from "@medusajs/framework/utils"

const retrieveCurrencyStep = createStep(
  "retrieve-currency",
  async ({}, { container }) => {
    const currencyModuleService = container.resolve(Modules.CURRENCY)

    const currency = await currencyModuleService
      .retrieveCurrency("usd")

    return new StepResponse({ currency })
  }
)

type Input = {
  price: number
}

export const retrievePriceWithCurrency = createWorkflow(
  "create-currency",
  (input: Input) => {
    const { currency } = retrieveCurrencyStep()

    const formattedPrice = transform({
      input,
      currency,
    }, (data) => {
      return `${data.currency.symbol}${data.input.price}`
    })

    return new WorkflowResponse({
      formattedPrice,
    })
  }
)
```

You can then execute the workflow in your custom API routes, scheduled jobs, or subscribers:

### API Route

```ts title="src/api/workflow/route.ts" highlights={[["11"], ["12"], ["13"], ["14"]]} collapsibleLines="1-6" expandButtonLabel="Show Imports"
import type {
  MedusaRequest,
  MedusaResponse,
} from "@medusajs/framework/http"
import { retrievePriceWithCurrency } from "../../workflows/retrieve-price-with-currency"

export async function GET(
  req: MedusaRequest,
  res: MedusaResponse
) {
  const { result } = await retrievePriceWithCurrency(req.scope)
    .run({
      price: 10,
    })

  res.send(result)
}
```

### Subscriber

```ts title="src/subscribers/user-created.ts" highlights={[["11"], ["12"], ["13"], ["14"]]} collapsibleLines="1-6" expandButtonLabel="Show Imports"
import {
  type SubscriberConfig,
  type SubscriberArgs,
} from "@medusajs/framework"
import { retrievePriceWithCurrency } from "../workflows/retrieve-price-with-currency"

export default async function handleUserCreated({
  event: { data },
  container,
}: SubscriberArgs<{ id: string }>) {
  const { result } = await retrievePriceWithCurrency(container)
    .run({
      price: 10,
    })

  console.log(result)
}

export const config: SubscriberConfig = {
  event: "user.created",
}
```

### Scheduled Job

```ts title="src/jobs/run-daily.ts" highlights={[["7"], ["8"], ["9"], ["10"]]}
import { MedusaContainer } from "@medusajs/framework/types"
import { retrievePriceWithCurrency } from "../workflows/retrieve-price-with-currency"

export default async function myCustomJob(
  container: MedusaContainer
) {
  const { result } = await retrievePriceWithCurrency(container)
    .run({
      price: 10,
    })

  console.log(result)
}

export const config = {
  name: "run-once-a-day",
  schedule: `0 0 * * *`,
}
```

Learn more about workflows in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md).

***


# Cart Module

In this section of the documentation, you will find resources to learn more about the Cart Module and how to use it in your application.

Medusa has cart related features available out-of-the-box through the Cart Module. A [module](https://docs.medusajs.com/docs/learn/fundamentals/modules/index.html.md) is a standalone package that provides features for a single domain. Each of Medusa's commerce features are placed in Commerce Modules, such as this Cart Module.

Learn more about why modules are isolated in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/modules/isolation/index.html.md).

## Cart Features

- [Cart Management](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/cart/concepts/index.html.md): Store and manage carts, including their addresses, line items, shipping methods, and more.
- [Apply Promotion Adjustments](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/cart/promotions/index.html.md): Apply promotions or discounts to line items and shipping methods by adding adjustment lines that are factored into their subtotals.
- [Apply Tax Lines](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/cart/tax-lines/index.html.md): Apply tax lines to line items and shipping methods.
- [Cart Scoping](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/cart/links-to-other-modules/index.html.md): When used in the Medusa application, Medusa creates links to other Commerce Modules, scoping a cart to a sales channel, region, and a customer.

***

## How to Use the Cart Module

In your Medusa application, you build flows around Commerce Modules. A flow is built as a [Workflow](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md), which is a special function composed of a series of steps that guarantees data consistency and reliable roll-back mechanism.

You can build custom workflows and steps. You can also re-use Medusa's workflows and steps, which are provided by the `@medusajs/medusa/core-flows` package.

For example:

```ts title="src/workflows/create-cart.ts" highlights={highlights}
import { 
  createWorkflow, 
  WorkflowResponse,
  createStep,
  StepResponse,
} from "@medusajs/framework/workflows-sdk"
import { Modules } from "@medusajs/framework/utils"

const createCartStep = createStep(
  "create-cart",
  async ({}, { container }) => {
    const cartModuleService = container.resolve(Modules.CART)

    const cart = await cartModuleService.createCarts({
      currency_code: "usd",
      shipping_address: {
        address_1: "1512 Barataria Blvd",
        country_code: "us",
      },
      items: [
        {
          title: "Shirt",
          unit_price: 1000,
          quantity: 1,
        },
      ],
    })

    return new StepResponse({ cart }, cart.id)
  },
  async (cartId, { container }) => {
    if (!cartId) {
      return
    }
    const cartModuleService = container.resolve(Modules.CART)

    await cartModuleService.deleteCarts([cartId])
  }
)

export const createCartWorkflow = createWorkflow(
  "create-cart",
  () => {
    const { cart } = createCartStep()

    return new WorkflowResponse({
      cart,
    })
  }
)
```

You can then execute the workflow in your custom API routes, scheduled jobs, or subscribers:

### API Route

```ts title="src/api/workflow/route.ts" highlights={[["11"], ["12"]]} collapsibleLines="1-6" expandButtonLabel="Show Imports"
import type {
  MedusaRequest,
  MedusaResponse,
} from "@medusajs/framework/http"
import { createCartWorkflow } from "../../workflows/create-cart"

export async function GET(
  req: MedusaRequest,
  res: MedusaResponse
) {
  const { result } = await createCartWorkflow(req.scope)
    .run()

  res.send(result)
}
```

### Subscriber

```ts title="src/subscribers/user-created.ts" highlights={[["11"], ["12"]]} collapsibleLines="1-6" expandButtonLabel="Show Imports"
import {
  type SubscriberConfig,
  type SubscriberArgs,
} from "@medusajs/framework"
import { createCartWorkflow } from "../workflows/create-cart"

export default async function handleUserCreated({
  event: { data },
  container,
}: SubscriberArgs<{ id: string }>) {
  const { result } = await createCartWorkflow(container)
    .run()

  console.log(result)
}

export const config: SubscriberConfig = {
  event: "user.created",
}
```

### Scheduled Job

```ts title="src/jobs/run-daily.ts" highlights={[["7"], ["8"]]}
import { MedusaContainer } from "@medusajs/framework/types"
import { createCartWorkflow } from "../workflows/create-cart"

export default async function myCustomJob(
  container: MedusaContainer
) {
  const { result } = await createCartWorkflow(container)
    .run()

  console.log(result)
}

export const config = {
  name: "run-once-a-day",
  schedule: `0 0 * * *`,
}
```

Learn more about workflows in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md).

***


# Order Module

In this section of the documentation, you will find resources to learn more about the Order Module and how to use it in your application.

Refer to the [Medusa Admin User Guide](https://docs.medusajs.com/user-guide/orders/index.html.md) to learn how to manage orders using the dashboard.

Medusa has order related features available out-of-the-box through the Order Module. A [module](https://docs.medusajs.com/docs/learn/fundamentals/modules/index.html.md) is a standalone package that provides features for a single domain. Each of Medusa's commerce features are placed in Commerce Modules, such as this Order Module.

Learn more about why modules are isolated in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/modules/isolation/index.html.md).

## Order Features

- [Order Management](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/order/concepts/index.html.md): Store and manage your orders to retrieve, create, cancel, and perform other operations.
- Draft Orders: Allow merchants to create orders on behalf of their customers as draft orders that later are transformed to regular orders.
- [Apply Promotion Adjustments](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/order/promotion-adjustments/index.html.md): Apply promotions or discounts to the order's items and shipping methods by adding adjustment lines that are factored into their subtotals.
- [Apply Tax Lines](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/order/tax-lines/index.html.md): Apply tax lines to an order's line items and shipping methods.
- [Returns](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/order/return/index.html.md), [Edits](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/order/edit/index.html.md), [Exchanges](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/order/exchange/index.html.md), and [Claims](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/order/claim/index.html.md): Make [changes](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/order/order-change/index.html.md) to an order to edit, return, or exchange its items, with [version-based control](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/order/order-versioning/index.html.md) over the order's timeline.

***

## How to Use the Order Module

In your Medusa application, you build flows around Commerce Modules. A flow is built as a [Workflow](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md), which is a special function composed of a series of steps that guarantees data consistency and reliable roll-back mechanism.

You can build custom workflows and steps. You can also re-use Medusa's workflows and steps, which are provided by the `@medusajs/medusa/core-flows` package.

For example:

```ts title="src/workflows/create-draft-order.ts" highlights={highlights}
import { 
  createWorkflow, 
  WorkflowResponse,
  createStep,
  StepResponse,
} from "@medusajs/framework/workflows-sdk"
import { Modules } from "@medusajs/framework/utils"

const createDraftOrderStep = createStep(
  "create-order",
  async ({}, { container }) => {
    const orderModuleService = container.resolve(Modules.ORDER)

    const draftOrder = await orderModuleService.createOrders({
      currency_code: "usd",
      items: [
        {
          title: "Shirt",
          quantity: 1,
          unit_price: 3000,
        },
      ],
      shipping_methods: [
        {
          name: "Express shipping",
          amount: 3000,
        },
      ],
      status: "draft",
    })

    return new StepResponse({ draftOrder }, draftOrder.id)
  },
  async (draftOrderId, { container }) => {
    if (!draftOrderId) {
      return
    }
    const orderModuleService = container.resolve(Modules.ORDER)

    await orderModuleService.deleteOrders([draftOrderId])
  }
)

export const createDraftOrderWorkflow = createWorkflow(
  "create-draft-order",
  () => {
    const { draftOrder } = createDraftOrderStep()

    return new WorkflowResponse({
      draftOrder,
    })
  }
)
```

You can then execute the workflow in your custom API routes, scheduled jobs, or subscribers:

### API Route

```ts title="src/api/workflow/route.ts" highlights={[["11"], ["12"]]} collapsibleLines="1-6" expandButtonLabel="Show Imports"
import type {
  MedusaRequest,
  MedusaResponse,
} from "@medusajs/framework/http"
import { createDraftOrderWorkflow } from "../../workflows/create-draft-order"

export async function GET(
  req: MedusaRequest,
  res: MedusaResponse
) {
  const { result } = await createDraftOrderWorkflow(req.scope)
    .run()

  res.send(result)
}
```

### Subscriber

```ts title="src/subscribers/user-created.ts" highlights={[["11"], ["12"]]} collapsibleLines="1-6" expandButtonLabel="Show Imports"
import {
  type SubscriberConfig,
  type SubscriberArgs,
} from "@medusajs/framework"
import { createDraftOrderWorkflow } from "../workflows/create-draft-order"

export default async function handleUserCreated({
  event: { data },
  container,
}: SubscriberArgs<{ id: string }>) {
  const { result } = await createDraftOrderWorkflow(container)
    .run()

  console.log(result)
}

export const config: SubscriberConfig = {
  event: "user.created",
}
```

### Scheduled Job

```ts title="src/jobs/run-daily.ts" highlights={[["7"], ["8"]]}
import { MedusaContainer } from "@medusajs/framework/types"
import { createDraftOrderWorkflow } from "../workflows/create-draft-order"

export default async function myCustomJob(
  container: MedusaContainer
) {
  const { result } = await createDraftOrderWorkflow(container)
    .run()

  console.log(result)
}

export const config = {
  name: "run-once-a-day",
  schedule: `0 0 * * *`,
}
```

Learn more about workflows in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md).

***


# Payment Module

In this section of the documentation, you will find resources to learn more about the Payment Module and how to use it in your application.

Refer to the [Medusa Admin User Guide](https://docs.medusajs.com/user-guide/orders/payments/index.html.md) to learn how to manage order payments using the dashboard.

Medusa has payment related features available out-of-the-box through the Payment Module. A [module](https://docs.medusajs.com/docs/learn/fundamentals/modules/index.html.md) is a standalone package that provides features for a single domain. Each of Medusa's commerce features are placed in Commerce Modules, such as this Payment Module.

Learn more about why modules are isolated in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/modules/isolation/index.html.md).

## Payment Features

- [Authorize, Capture, and Refund Payments](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/payment/payment/index.html.md): Authorize, capture, and refund payments for a single resource.
- [Payment Collection Management](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/payment/payment-collection/index.html.md): Store and manage all payments of a single resources, such as a cart, in payment collections.
- [Integrate Third-Party Payment Providers](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/payment/payment-provider/index.html.md): Use payment providers like [Stripe](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/payment/payment-provider/stripe/index.html.md) to handle and process payments, or integrate custom payment providers.
- [Saved Payment Methods](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/payment/account-holder/index.html.md): Save payment methods for customers in third-party payment providers.
- [Handle Webhook Events](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/payment/webhook-events/index.html.md): Handle webhook events from third-party providers and process the associated payment.

***

## How to Use the Payment Module

In your Medusa application, you build flows around Commerce Modules. A flow is built as a [Workflow](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md), which is a special function composed of a series of steps that guarantees data consistency and reliable roll-back mechanism.

You can build custom workflows and steps. You can also re-use Medusa's workflows and steps, which are provided by the `@medusajs/medusa/core-flows` package.

For example:

```ts title="src/workflows/create-payment-collection.ts" highlights={highlights}
import { 
  createWorkflow, 
  WorkflowResponse,
  createStep,
  StepResponse,
} from "@medusajs/framework/workflows-sdk"
import { Modules } from "@medusajs/framework/utils"

const createPaymentCollectionStep = createStep(
  "create-payment-collection",
  async ({}, { container }) => {
    const paymentModuleService = container.resolve(Modules.PAYMENT)

    const paymentCollection = await paymentModuleService.createPaymentCollections({
      currency_code: "usd",
      amount: 5000,
    })

    return new StepResponse({ paymentCollection }, paymentCollection.id)
  },
  async (paymentCollectionId, { container }) => {
    if (!paymentCollectionId) {
      return
    }
    const paymentModuleService = container.resolve(Modules.PAYMENT)

    await paymentModuleService.deletePaymentCollections([paymentCollectionId])
  }
)

export const createPaymentCollectionWorkflow = createWorkflow(
  "create-payment-collection",
  () => {
    const { paymentCollection } = createPaymentCollectionStep()

    return new WorkflowResponse({
      paymentCollection,
    })
  }
)
```

You can then execute the workflow in your custom API routes, scheduled jobs, or subscribers:

### API Route

```ts title="src/api/workflow/route.ts" highlights={[["11"], ["12"]]} collapsibleLines="1-6" expandButtonLabel="Show Imports"
import type {
  MedusaRequest,
  MedusaResponse,
} from "@medusajs/framework/http"
import { createPaymentCollectionWorkflow } from "../../workflows/create-payment-collection"

export async function GET(
  req: MedusaRequest,
  res: MedusaResponse
) {
  const { result } = await createPaymentCollectionWorkflow(req.scope)
    .run()

  res.send(result)
}
```

### Subscriber

```ts title="src/subscribers/user-created.ts" highlights={[["11"], ["12"]]} collapsibleLines="1-6" expandButtonLabel="Show Imports"
import {
  type SubscriberConfig,
  type SubscriberArgs,
} from "@medusajs/framework"
import { createPaymentCollectionWorkflow } from "../workflows/create-payment-collection"

export default async function handleUserCreated({
  event: { data },
  container,
}: SubscriberArgs<{ id: string }>) {
  const { result } = await createPaymentCollectionWorkflow(container)
    .run()

  console.log(result)
}

export const config: SubscriberConfig = {
  event: "user.created",
}
```

### Scheduled Job

```ts title="src/jobs/run-daily.ts" highlights={[["7"], ["8"]]}
import { MedusaContainer } from "@medusajs/framework/types"
import { createPaymentCollectionWorkflow } from "../workflows/create-payment-collection"

export default async function myCustomJob(
  container: MedusaContainer
) {
  const { result } = await createPaymentCollectionWorkflow(container)
    .run()

  console.log(result)
}

export const config = {
  name: "run-once-a-day",
  schedule: `0 0 * * *`,
}
```

Learn more about workflows in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md).

***

## Configure Payment Module

The Payment Module accepts options for further configurations. Refer to [this documentation](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/payment/module-options/index.html.md) for details on the module's options.

***

## Providers

Medusa provides the following payment providers out-of-the-box. You can use them to process payments for orders, returns, and other resources.

***


# Pricing Module

In this section of the documentation, you will find resources to learn more about the Pricing Module and how to use it in your application.

Refer to the [Medusa Admin User Guide](https://docs.medusajs.com/user-guide/price-lists/index.html.md) to learn how to manage price lists using the dashboard.

Medusa has pricing related features available out-of-the-box through the Pricing Module. A [module](https://docs.medusajs.com/docs/learn/fundamentals/modules/index.html.md) is a standalone package that provides features for a single domain. Each of Medusa's commerce features are placed in Commerce Modules, such as this Pricing Module.

Learn more about why modules are isolated in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/modules/isolation/index.html.md).

## Pricing Features

- [Price Management](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/pricing/concepts/index.html.md): Store and manage prices of a resource, such as a product or a variant.
- [Advanced Rule Engine](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/pricing/price-rules/index.html.md): Create prices with tiers and custom rules to condition prices based on different contexts.
- [Price Lists](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/pricing/concepts#price-list/index.html.md): Group prices and apply them only in specific conditions with price lists.
- [Price Calculation Strategy](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/pricing/price-calculation/index.html.md): Retrieve the best price in a given context and for the specified rule values.
- [Tax-Inclusive Pricing](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/pricing/tax-inclusive-pricing/index.html.md): Calculate prices with taxes included in the price, and Medusa will handle calculating the taxes automatically.

***

## How to Use the Pricing Module

In your Medusa application, you build flows around Commerce Modules. A flow is built as a [Workflow](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md), which is a special function composed of a series of steps that guarantees data consistency and reliable roll-back mechanism.

You can build custom workflows and steps. You can also re-use Medusa's workflows and steps, which are provided by the `@medusajs/medusa/core-flows` package.

For example:

```ts title="src/workflows/create-price-set.ts" highlights={highlights}
import { 
  createWorkflow, 
  WorkflowResponse,
  createStep,
  StepResponse,
} from "@medusajs/framework/workflows-sdk"
import { Modules } from "@medusajs/framework/utils"

const createPriceSetStep = createStep(
  "create-price-set",
  async ({}, { container }) => {
    const pricingModuleService = container.resolve(Modules.PRICING)

    const priceSet = await pricingModuleService.createPriceSets({
      prices: [
        {
          amount: 500,
          currency_code: "USD",
        },
        {
          amount: 400,
          currency_code: "EUR",
          min_quantity: 0,
          max_quantity: 4,
          rules: {},
        },
      ],
    })

    return new StepResponse({ priceSet }, priceSet.id)
  },
  async (priceSetId, { container }) => {
    if (!priceSetId) {
      return
    }
    const pricingModuleService = container.resolve(Modules.PRICING)

    await pricingModuleService.deletePriceSets([priceSetId])
  }
)

export const createPriceSetWorkflow = createWorkflow(
  "create-price-set",
  () => {
    const { priceSet } = createPriceSetStep()

    return new WorkflowResponse({
      priceSet,
    })
  }
)
```

You can then execute the workflow in your custom API routes, scheduled jobs, or subscribers:

### API Route

```ts title="src/api/workflow/route.ts" highlights={[["11"], ["12"]]} collapsibleLines="1-6" expandButtonLabel="Show Imports"
import type {
  MedusaRequest,
  MedusaResponse,
} from "@medusajs/framework/http"
import { createPriceSetWorkflow } from "../../workflows/create-price-set"

export async function GET(
  req: MedusaRequest,
  res: MedusaResponse
) {
  const { result } = await createPriceSetWorkflow(req.scope)
    .run()

  res.send(result)
}
```

### Subscriber

```ts title="src/subscribers/user-created.ts" highlights={[["11"], ["12"]]} collapsibleLines="1-6" expandButtonLabel="Show Imports"
import {
  type SubscriberConfig,
  type SubscriberArgs,
} from "@medusajs/framework"
import { createPriceSetWorkflow } from "../workflows/create-price-set"

export default async function handleUserCreated({
  event: { data },
  container,
}: SubscriberArgs<{ id: string }>) {
  const { result } = await createPriceSetWorkflow(container)
    .run()

  console.log(result)
}

export const config: SubscriberConfig = {
  event: "user.created",
}
```

### Scheduled Job

```ts title="src/jobs/run-daily.ts" highlights={[["7"], ["8"]]}
import { MedusaContainer } from "@medusajs/framework/types"
import { createPriceSetWorkflow } from "../workflows/create-price-set"

export default async function myCustomJob(
  container: MedusaContainer
) {
  const { result } = await createPriceSetWorkflow(container)
    .run()

  console.log(result)
}

export const config = {
  name: "run-once-a-day",
  schedule: `0 0 * * *`,
}
```

Learn more about workflows in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md).

***


# Product Module

In this section of the documentation, you will find resources to learn more about the Product Module and how to use it in your application.

Refer to the [Medusa Admin User Guide](https://docs.medusajs.com/user-guide/products/index.html.md) to learn how to manage products using the dashboard.

Medusa has product related features available out-of-the-box through the Product Module. A [module](https://docs.medusajs.com/docs/learn/fundamentals/modules/index.html.md) is a standalone package that provides features for a single domain. Each of Medusa's commerce features are placed in Commerce Modules, such as this Product Module.

Learn more about why modules are isolated in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/modules/isolation/index.html.md).

## Product Features

- [Products Management](https://docs.medusajs.com/references/product/models/Product/index.html.md): Store and manage products. Products have custom options, such as color or size, and each variant in the product sets the value for these options.
- [Product Organization](https://docs.medusajs.com/references/product/models/index.html.md): The Product Module provides different data models used to organize products, including categories, collections, tags, and more.
- [Bundled and Multi-Part Products](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/inventory/inventory-kit/index.html.md): Create and manage inventory kits for a single product, allowing you to implement use cases like bundled or multi-part products.
- [Tiered Pricing and Price Rules](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/pricing/price-rules/index.html.md): Set prices for product variants with tiers and rules, allowing you to create complex pricing strategies.

***

## How to Use the Product Module

In your Medusa application, you build flows around Commerce Modules. A flow is built as a [Workflow](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md), which is a special function composed of a series of steps that guarantees data consistency and reliable roll-back mechanism.

You can build custom workflows and steps. You can also re-use Medusa's workflows and steps, which are provided by the `@medusajs/medusa/core-flows` package.

For example:

```ts title="src/workflows/create-product.ts" highlights={highlights}
import { 
  createWorkflow, 
  WorkflowResponse,
  createStep,
  StepResponse,
} from "@medusajs/framework/workflows-sdk"
import { Modules } from "@medusajs/framework/utils"

const createProductStep = createStep(
  "create-product",
  async ({}, { container }) => {
    const productService = container.resolve(Modules.PRODUCT)

    const product = await productService.createProducts({
      title: "Medusa Shirt",
      options: [
        {
          title: "Color",
          values: ["Black", "White"],
        },
      ],
      variants: [
        {
          title: "Black Shirt",
          options: {
            Color: "Black",
          },
        },
      ],
    })

    return new StepResponse({ product }, product.id)
  },
  async (productId, { container }) => {
    if (!productId) {
      return
    }
    const productService = container.resolve(Modules.PRODUCT)

    await productService.deleteProducts([productId])
  }
)

export const createProductWorkflow = createWorkflow(
  "create-product",
  () => {
    const { product } = createProductStep()

    return new WorkflowResponse({
      product,
    })
  }
)
```

You can then execute the workflow in your custom API routes, scheduled jobs, or subscribers:

### API Route

```ts title="src/api/workflow/route.ts" highlights={[["11"], ["12"]]} collapsibleLines="1-6" expandButtonLabel="Show Imports"
import type {
  MedusaRequest,
  MedusaResponse,
} from "@medusajs/framework/http"
import { createProductWorkflow } from "../../workflows/create-product"

export async function GET(
  req: MedusaRequest,
  res: MedusaResponse
) {
  const { result } = await createProductWorkflow(req.scope)
    .run()

  res.send(result)
}
```

### Subscriber

```ts title="src/subscribers/user-created.ts" highlights={[["11"], ["12"]]} collapsibleLines="1-6" expandButtonLabel="Show Imports"
import {
  type SubscriberConfig,
  type SubscriberArgs,
} from "@medusajs/framework"
import { createProductWorkflow } from "../workflows/create-product"

export default async function handleUserCreated({
  event: { data },
  container,
}: SubscriberArgs<{ id: string }>) {
  const { result } = await createProductWorkflow(container)
    .run()

  console.log(result)
}

export const config: SubscriberConfig = {
  event: "user.created",
}
```

### Scheduled Job

```ts title="src/jobs/run-daily.ts" highlights={[["7"], ["8"]]}
import { MedusaContainer } from "@medusajs/framework/types"
import { createProductWorkflow } from "../workflows/create-product"

export default async function myCustomJob(
  container: MedusaContainer
) {
  const { result } = await createProductWorkflow(container)
    .run()

  console.log(result)
}

export const config = {
  name: "run-once-a-day",
  schedule: `0 0 * * *`,
}
```

Learn more about workflows in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md).

***


# Region Module

In this section of the documentation, you will find resources to learn more about the Region Module and how to use it in your application.

Refer to the [Medusa Admin User Guide](https://docs.medusajs.com/user-guide/settings/regions/index.html.md) to learn how to manage regions using the dashboard.

Medusa has region related features available out-of-the-box through the Region Module. A [module](https://docs.medusajs.com/docs/learn/fundamentals/modules/index.html.md) is a standalone package that provides features for a single domain. Each of Medusa's commerce features are placed in Commerce Modules, such as this Region Module.

Learn more about why modules are isolated in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/modules/isolation/index.html.md).

***

## Region Features

- [Region Management](https://docs.medusajs.com/references/region/models/Region/index.html.md): Manage regions in your store. You can create regions with different currencies and settings.
- [Multi-Currency Support](https://docs.medusajs.com/references/region/models/Region/index.html.md): Each region has a currency. You can support multiple currencies in your store by creating multiple regions.
- [Different Settings Per Region](https://docs.medusajs.com/references/region/models/Region/index.html.md): Each region has its own settings, such as what countries belong to a region or its tax settings.

***

## How to Use Region Module's Service

In your Medusa application, you build flows around Commerce Modules. A flow is built as a [Workflow](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md), which is a special function composed of a series of steps that guarantees data consistency and reliable roll-back mechanism.

You can build custom workflows and steps. You can also re-use Medusa's workflows and steps, which are provided by the `@medusajs/medusa/core-flows` package.

For example:

```ts title="src/workflows/create-region.ts" highlights={highlights}
import { 
  createWorkflow, 
  WorkflowResponse,
  createStep,
  StepResponse,
} from "@medusajs/framework/workflows-sdk"
import { Modules } from "@medusajs/framework/utils"

const createRegionStep = createStep(
  "create-region",
  async ({}, { container }) => {
    const regionModuleService = container.resolve(Modules.REGION)

    const region = await regionModuleService.createRegions({
      name: "Europe",
      currency_code: "eur",
    })

    return new StepResponse({ region }, region.id)
  },
  async (regionId, { container }) => {
    if (!regionId) {
      return
    }
    const regionModuleService = container.resolve(Modules.REGION)

    await regionModuleService.deleteRegions([regionId])
  }
)

export const createRegionWorkflow = createWorkflow(
  "create-region",
  () => {
    const { region } = createRegionStep()

    return new WorkflowResponse({
      region,
    })
  }
)
```

You can then execute the workflow in your custom API routes, scheduled jobs, or subscribers:

### API Route

```ts title="src/api/workflow/route.ts" highlights={[["11"], ["12"]]} collapsibleLines="1-6" expandButtonLabel="Show Imports"
import type {
  MedusaRequest,
  MedusaResponse,
} from "@medusajs/framework/http"
import { createRegionWorkflow } from "../../workflows/create-region"

export async function GET(
  req: MedusaRequest,
  res: MedusaResponse
) {
  const { result } = await createRegionWorkflow(req.scope)
    .run()

  res.send(result)
}
```

### Subscriber

```ts title="src/subscribers/user-created.ts" highlights={[["11"], ["12"]]} collapsibleLines="1-6" expandButtonLabel="Show Imports"
import {
  type SubscriberConfig,
  type SubscriberArgs,
} from "@medusajs/framework"
import { createRegionWorkflow } from "../workflows/create-region"

export default async function handleUserCreated({
  event: { data },
  container,
}: SubscriberArgs<{ id: string }>) {
  const { result } = await createRegionWorkflow(container)
    .run()

  console.log(result)
}

export const config: SubscriberConfig = {
  event: "user.created",
}
```

### Scheduled Job

```ts title="src/jobs/run-daily.ts" highlights={[["7"], ["8"]]}
import { MedusaContainer } from "@medusajs/framework/types"
import { createRegionWorkflow } from "../workflows/create-region"

export default async function myCustomJob(
  container: MedusaContainer
) {
  const { result } = await createRegionWorkflow(container)
    .run()

  console.log(result)
}

export const config = {
  name: "run-once-a-day",
  schedule: `0 0 * * *`,
}
```

Learn more about workflows in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md).

***


# Sales Channel Module

In this section of the documentation, you will find resources to learn more about the Sales Channel Module and how to use it in your application.

Refer to the [Medusa Admin User Guide](https://docs.medusajs.com/user-guide/settings/sales-channels/index.html.md) to learn how to manage sales channels using the dashboard.

Medusa has sales channel related features available out-of-the-box through the Sales Channel Module. A [module](https://docs.medusajs.com/docs/learn/fundamentals/modules/index.html.md) is a standalone package that provides features for a single domain. Each of Medusa's commerce features are placed in Commerce Modules, such as this Sales Channel Module.

Learn more about why modules are isolated in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/modules/isolation/index.html.md).

## What's a Sales Channel?

A sales channel indicates an online or offline channel that you sell products on.

Some use case examples for using a sales channel:

- Implement a B2B Ecommerce Store.
- Specify different products for each channel you sell in.
- Support omnichannel in your ecommerce store.

***

## Sales Channel Features

- [Sales Channel Management](https://docs.medusajs.com/references/sales-channel/models/SalesChannel/index.html.md): Manage sales channels in your store. Each sales channel has different meta information such as name or description, allowing you to easily differentiate between sales channels.
- [Product Availability](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/sales-channel/links-to-other-modules/index.html.md): Medusa uses the Product and Sales Channel modules to allow merchants to specify a product's availability per sales channel.
- [Cart and Order Scoping](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/sales-channel/links-to-other-modules/index.html.md): Carts, available through the Cart Module, are scoped to a sales channel. Paired with the product availability feature, you benefit from more features like allowing only products available in sales channel in a cart.
- [Inventory Availability Per Sales Channel](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/sales-channel/links-to-other-modules/index.html.md): Medusa links sales channels to stock locations, allowing you to retrieve available inventory of products based on the specified sales channel.

***

## How to Use Sales Channel Module's Service

In your Medusa application, you build flows around Commerce Modules. A flow is built as a [Workflow](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md), which is a special function composed of a series of steps that guarantees data consistency and reliable roll-back mechanism.

You can build custom workflows and steps. You can also re-use Medusa's workflows and steps, which are provided by the `@medusajs/medusa/core-flows` package.

For example:

```ts title="src/workflows/create-sales-channel.ts" highlights={highlights}
import { 
  createWorkflow, 
  WorkflowResponse,
  createStep,
  StepResponse,
} from "@medusajs/framework/workflows-sdk"
import { Modules } from "@medusajs/framework/utils"

const createSalesChannelStep = createStep(
  "create-sales-channel",
  async ({}, { container }) => {
    const salesChannelModuleService = container.resolve(Modules.SALES_CHANNEL)

    const salesChannels = await salesChannelModuleService.createSalesChannels([
      {
        name: "B2B",
      },
      {
        name: "Mobile App",
      },
    ])

    return new StepResponse({ salesChannels }, salesChannels.map((sc) => sc.id))
  },
  async (salesChannelIds, { container }) => {
    if (!salesChannelIds) {
      return
    }
    const salesChannelModuleService = container.resolve(Modules.SALES_CHANNEL)

    await salesChannelModuleService.deleteSalesChannels(
      salesChannelIds
    )
  }
)

export const createSalesChannelWorkflow = createWorkflow(
  "create-sales-channel",
  () => {
    const { salesChannels } = createSalesChannelStep()

    return new WorkflowResponse({
      salesChannels,
    })
  }
)
```

You can then execute the workflow in your custom API routes, scheduled jobs, or subscribers:

### API Route

```ts title="src/api/workflow/route.ts" highlights={[["11"], ["12"]]} collapsibleLines="1-6" expandButtonLabel="Show Imports"
import type {
  MedusaRequest,
  MedusaResponse,
} from "@medusajs/framework/http"
import { createSalesChannelWorkflow } from "../../workflows/create-sales-channel"

export async function GET(
  req: MedusaRequest,
  res: MedusaResponse
) {
  const { result } = await createSalesChannelWorkflow(req.scope)
    .run()

  res.send(result)
}
```

### Subscriber

```ts title="src/subscribers/user-created.ts" highlights={[["11"], ["12"]]} collapsibleLines="1-6" expandButtonLabel="Show Imports"
import {
  type SubscriberConfig,
  type SubscriberArgs,
} from "@medusajs/framework"
import { createSalesChannelWorkflow } from "../workflows/create-sales-channel"

export default async function handleUserCreated({
  event: { data },
  container,
}: SubscriberArgs<{ id: string }>) {
  const { result } = await createSalesChannelWorkflow(container)
    .run()

  console.log(result)
}

export const config: SubscriberConfig = {
  event: "user.created",
}
```

### Scheduled Job

```ts title="src/jobs/run-daily.ts" highlights={[["7"], ["8"]]}
import { MedusaContainer } from "@medusajs/framework/types"
import { createSalesChannelWorkflow } from "../workflows/create-sales-channel"

export default async function myCustomJob(
  container: MedusaContainer
) {
  const { result } = await createSalesChannelWorkflow(container)
    .run()

  console.log(result)
}

export const config = {
  name: "run-once-a-day",
  schedule: `0 0 * * *`,
}
```

Learn more about workflows in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md).

***


# Promotion Module

In this section of the documentation, you will find resources to learn more about the Promotion Module and how to use it in your application.

Refer to the [Medusa Admin User Guide](https://docs.medusajs.com/user-guide/promotions/index.html.md) to learn how to manage promotions using the dashboard.

Medusa has promotion related features available out-of-the-box through the Promotion Module. A [module](https://docs.medusajs.com/docs/learn/fundamentals/modules/index.html.md) is a standalone package that provides features for a single domain. Each of Medusa's commerce features are placed in Commerce Modules, such as this Promotion Module.

Learn more about why modules are isolated in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/modules/isolation/index.html.md).

## Promotion Features

- [Discount Functionalities](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/promotion/concepts/index.html.md): A promotion discounts an amount or percentage of a cart's items, shipping methods, or the entire order.
- [Flexible Promotion Rules](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/promotion/concepts#flexible-rules/index.html.md): A promotion has rules that restricts when the promotion is applied.
- [Campaign Management](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/promotion/campaign/index.html.md): A campaign combines promotions under the same conditions, such as start and end dates, and budget configurations.
- [Apply Promotion on Carts and Orders](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/promotion/actions/index.html.md): Apply promotions on carts and orders to discount items, shipping methods, or the entire order.

***

## How to Use the Promotion Module

In your Medusa application, you build flows around Commerce Modules. A flow is built as a [Workflow](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md), which is a special function composed of a series of steps that guarantees data consistency and reliable roll-back mechanism.

You can build custom workflows and steps. You can also re-use Medusa's workflows and steps, which are provided by the `@medusajs/medusa/core-flows` package.

For example:

```ts title="src/workflows/create-promotion.ts" highlights={highlights}
import { 
  createWorkflow, 
  WorkflowResponse,
  createStep,
  StepResponse,
} from "@medusajs/framework/workflows-sdk"
import { Modules } from "@medusajs/framework/utils"

const createPromotionStep = createStep(
  "create-promotion",
  async ({}, { container }) => {
    const promotionModuleService = container.resolve(Modules.PROMOTION)

    const promotion = await promotionModuleService.createPromotions({
      code: "10%OFF",
      type: "standard",
      application_method: {
        type: "percentage",
        target_type: "order",
        value: 10,
        currency_code: "usd",
      },
    })

    return new StepResponse({ promotion }, promotion.id)
  },
  async (promotionId, { container }) => {
    if (!promotionId) {
      return
    }
    const promotionModuleService = container.resolve(Modules.PROMOTION)

    await promotionModuleService.deletePromotions(promotionId)
  }
)

export const createPromotionWorkflow = createWorkflow(
  "create-promotion",
  () => {
    const { promotion } = createPromotionStep()

    return new WorkflowResponse({
      promotion,
    })
  }
)
```

You can then execute the workflow in your custom API routes, scheduled jobs, or subscribers:

### API Route

```ts title="src/api/workflow/route.ts" highlights={[["11"], ["12"]]} collapsibleLines="1-6" expandButtonLabel="Show Imports"
import type {
  MedusaRequest,
  MedusaResponse,
} from "@medusajs/framework/http"
import { createPromotionWorkflow } from "../../workflows/create-cart"

export async function GET(
  req: MedusaRequest,
  res: MedusaResponse
) {
  const { result } = await createPromotionWorkflow(req.scope)
    .run()

  res.send(result)
}
```

### Subscriber

```ts title="src/subscribers/user-created.ts" highlights={[["11"], ["12"]]} collapsibleLines="1-6" expandButtonLabel="Show Imports"
import {
  type SubscriberConfig,
  type SubscriberArgs,
} from "@medusajs/framework"
import { createPromotionWorkflow } from "../workflows/create-cart"

export default async function handleUserCreated({
  event: { data },
  container,
}: SubscriberArgs<{ id: string }>) {
  const { result } = await createPromotionWorkflow(container)
    .run()

  console.log(result)
}

export const config: SubscriberConfig = {
  event: "user.created",
}
```

### Scheduled Job

```ts title="src/jobs/run-daily.ts" highlights={[["7"], ["8"]]}
import { MedusaContainer } from "@medusajs/framework/types"
import { createPromotionWorkflow } from "../workflows/create-cart"

export default async function myCustomJob(
  container: MedusaContainer
) {
  const { result } = await createPromotionWorkflow(container)
    .run()

  console.log(result)
}

export const config = {
  name: "run-once-a-day",
  schedule: `0 0 * * *`,
}
```

Learn more about workflows in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md).

***


# Stock Location Module

In this section of the documentation, you will find resources to learn more about the Stock Location Module and how to use it in your application.

Refer to the [Medusa Admin User Guide](https://docs.medusajs.com/user-guide/settings/locations-and-shipping/index.html.md) to learn how to manage stock locations using the dashboard.

Medusa has stock location related features available out-of-the-box through the Stock Location Module. A [module](https://docs.medusajs.com/docs/learn/fundamentals/modules/index.html.md) is a standalone package that provides features for a single domain. Each of Medusa's commerce features are placed in Commerce Modules, such as this Stock Location Module.

Learn more about why modules are isolated in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/modules/isolation/index.html.md).

## Stock Location Features

- [Stock Location Management](https://docs.medusajs.com/references/stock-location-next/models/index.html.md): Store and manage stock locations. Medusa links stock locations with data models of other modules that require a location, such as the [Inventory Module's InventoryLevel](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/stock-location/links-to-other-modules/index.html.md).
- [Address Management](https://docs.medusajs.com/references/stock-location-next/models/StockLocationAddress/index.html.md): Manage the address of each stock location.

***

## How to Use Stock Location Module's Service

In your Medusa application, you build flows around Commerce Modules. A flow is built as a [Workflow](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md), which is a special function composed of a series of steps that guarantees data consistency and reliable roll-back mechanism.

You can build custom workflows and steps. You can also re-use Medusa's workflows and steps, which are provided by the `@medusajs/medusa/core-flows` package.

For example:

```ts title="src/workflows/create-stock-location.ts" highlights={highlights}
import { 
  createWorkflow, 
  WorkflowResponse,
  createStep,
  StepResponse,
} from "@medusajs/framework/workflows-sdk"
import { Modules } from "@medusajs/framework/utils"

const createStockLocationStep = createStep(
  "create-stock-location",
  async ({}, { container }) => {
    const stockLocationModuleService = container.resolve(Modules.STOCK_LOCATION)

    const stockLocation = await stockLocationModuleService.createStockLocations({
      name: "Warehouse 1",
    })

    return new StepResponse({ stockLocation }, stockLocation.id)
  },
  async (stockLocationId, { container }) => {
    if (!stockLocationId) {
      return
    }
    const stockLocationModuleService = container.resolve(Modules.STOCK_LOCATION)

    await stockLocationModuleService.deleteStockLocations([stockLocationId])
  }
)

export const createStockLocationWorkflow = createWorkflow(
  "create-stock-location",
  () => {
    const { stockLocation } = createStockLocationStep()

    return new WorkflowResponse({ stockLocation })
  }
)
```

You can then execute the workflow in your custom API routes, scheduled jobs, or subscribers:

### API Route

```ts title="src/api/workflow/route.ts" highlights={[["11"], ["12"]]} collapsibleLines="1-6" expandButtonLabel="Show Imports"
import type {
  MedusaRequest,
  MedusaResponse,
} from "@medusajs/framework/http"
import { createStockLocationWorkflow } from "../../workflows/create-stock-location"

export async function GET(
  req: MedusaRequest,
  res: MedusaResponse
) {
  const { result } = await createStockLocationWorkflow(req.scope)
    .run()

  res.send(result)
}
```

### Subscriber

```ts title="src/subscribers/user-created.ts" highlights={[["11"], ["12"]]} collapsibleLines="1-6" expandButtonLabel="Show Imports"
import {
  type SubscriberConfig,
  type SubscriberArgs,
} from "@medusajs/framework"
import { createStockLocationWorkflow } from "../workflows/create-stock-location"

export default async function handleUserCreated({
  event: { data },
  container,
}: SubscriberArgs<{ id: string }>) {
  const { result } = await createStockLocationWorkflow(container)
    .run()

  console.log(result)
}

export const config: SubscriberConfig = {
  event: "user.created",
}
```

### Scheduled Job

```ts title="src/jobs/run-daily.ts" highlights={[["7"], ["8"]]}
import { MedusaContainer } from "@medusajs/framework/types"
import { createStockLocationWorkflow } from "../workflows/create-stock-location"

export default async function myCustomJob(
  container: MedusaContainer
) {
  const { result } = await createStockLocationWorkflow(container)
    .run()

  console.log(result)
}

export const config = {
  name: "run-once-a-day",
  schedule: `0 0 * * *`,
}
```

Learn more about workflows in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md).

***


# Tax Module

In this section of the documentation, you will find resources to learn more about the Tax Module and how to use it in your application.

Refer to the [Medusa Admin User Guide](https://docs.medusajs.com/user-guide/settings/tax-regions/index.html.md) to learn how to manage tax regions using the dashboard.

Medusa has tax related features available out-of-the-box through the Tax Module. A [module](https://docs.medusajs.com/docs/learn/fundamentals/modules/index.html.md) is a standalone package that provides features for a single domain. Each of Medusa's commerce features are placed in Commerce Modules, such as this Tax Module.

Learn more about why modules are isolated in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/modules/isolation/index.html.md).

## Tax Features

- [Tax Settings Per Region](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/tax/tax-region/index.html.md): Set different tax settings for each tax region.
- [Tax Rates and Rules](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/tax/tax-rates-and-rules/index.html.md): Manage each region's default tax rates and override them with conditioned tax rates.
- [Retrieve Tax Lines for carts and orders](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/tax/tax-calculation-with-provider/index.html.md): Calculate and retrieve the tax lines of a cart or order's line items and shipping methods with tax providers.
- [Custom Tax Providers](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/tax/tax-provider/index.html.md): Create custom tax providers to calculate tax lines differently for each tax region.

***

## How to Use Tax Module's Service

In your Medusa application, you build flows around Commerce Modules. A flow is built as a [Workflow](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md), which is a special function composed of a series of steps that guarantees data consistency and reliable roll-back mechanism.

You can build custom workflows and steps. You can also re-use Medusa's workflows and steps, which are provided by the `@medusajs/medusa/core-flows` package.

For example:

```ts title="src/workflows/create-tax-region.ts" highlights={highlights}
import { 
  createWorkflow, 
  WorkflowResponse,
  createStep,
  StepResponse,
} from "@medusajs/framework/workflows-sdk"
import { Modules } from "@medusajs/framework/utils"

const createTaxRegionStep = createStep(
  "create-tax-region",
  async ({}, { container }) => {
    const taxModuleService = container.resolve(Modules.TAX)

    const taxRegion = await taxModuleService.createTaxRegions({
      country_code: "us",
    })

    return new StepResponse({ taxRegion }, taxRegion.id)
  },
  async (taxRegionId, { container }) => {
    if (!taxRegionId) {
      return
    }
    const taxModuleService = container.resolve(Modules.TAX)

    await taxModuleService.deleteTaxRegions([taxRegionId])
  }
)

export const createTaxRegionWorkflow = createWorkflow(
  "create-tax-region",
  () => {
    const { taxRegion } = createTaxRegionStep()

    return new WorkflowResponse({ taxRegion })
  }
)
```

You can then execute the workflow in your custom API routes, scheduled jobs, or subscribers:

### API Route

```ts title="src/api/workflow/route.ts" highlights={[["11"], ["12"]]} collapsibleLines="1-6" expandButtonLabel="Show Imports"
import type {
  MedusaRequest,
  MedusaResponse,
} from "@medusajs/framework/http"
import { createTaxRegionWorkflow } from "../../workflows/create-tax-region"

export async function GET(
  req: MedusaRequest,
  res: MedusaResponse
) {
  const { result } = await createTaxRegionWorkflow(req.scope)
    .run()

  res.send(result)
}
```

### Subscriber

```ts title="src/subscribers/user-created.ts" highlights={[["11"], ["12"]]} collapsibleLines="1-6" expandButtonLabel="Show Imports"
import {
  type SubscriberConfig,
  type SubscriberArgs,
} from "@medusajs/framework"
import { createTaxRegionWorkflow } from "../workflows/create-tax-region"

export default async function handleUserCreated({
  event: { data },
  container,
}: SubscriberArgs<{ id: string }>) {
  const { result } = await createTaxRegionWorkflow(container)
    .run()

  console.log(result)
}

export const config: SubscriberConfig = {
  event: "user.created",
}
```

### Scheduled Job

```ts title="src/jobs/run-daily.ts" highlights={[["7"], ["8"]]}
import { MedusaContainer } from "@medusajs/framework/types"
import { createTaxRegionWorkflow } from "../workflows/create-tax-region"

export default async function myCustomJob(
  container: MedusaContainer
) {
  const { result } = await createTaxRegionWorkflow(container)
    .run()

  console.log(result)
}

export const config = {
  name: "run-once-a-day",
  schedule: `0 0 * * *`,
}
```

Learn more about workflows in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md).

***

## Configure Tax Module

The Tax Module accepts options for further configurations. Refer to [this documentation](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/tax/module-options/index.html.md) for details on the module's options.

***


# Store Module

In this section of the documentation, you will find resources to learn more about the Store Module and how to use it in your application.

Refer to the [Medusa Admin User Guide](https://docs.medusajs.com/user-guide/settings/store/index.html.md) to learn how to manage your store using the dashboard.

Medusa has store related features available out-of-the-box through the Store Module. A [module](https://docs.medusajs.com/docs/learn/fundamentals/modules/index.html.md) is a standalone package that provides features for a single domain. Each of Medusa's commerce features are placed in Commerce Modules, such as this Store Module.

Learn more about why modules are isolated in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/modules/isolation/index.html.md).

## Store Features

- [Store Management](https://docs.medusajs.com/references/store/models/Store/index.html.md): Create and manage stores in your application.
- [Multi-Tenancy Support](https://docs.medusajs.com/references/store/models/Store/index.html.md): Create multiple stores, each having its own configurations.

***

## How to Use Store Module's Service

In your Medusa application, you build flows around Commerce Modules. A flow is built as a [Workflow](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md), which is a special function composed of a series of steps that guarantees data consistency and reliable roll-back mechanism.

You can build custom workflows and steps. You can also re-use Medusa's workflows and steps, which are provided by the `@medusajs/medusa/core-flows` package.

For example:

```ts title="src/workflows/create-store.ts" highlights={highlights}
import { 
  createWorkflow, 
  WorkflowResponse,
  createStep,
  StepResponse,
} from "@medusajs/framework/workflows-sdk"
import { Modules } from "@medusajs/framework/utils"

const createStoreStep = createStep(
  "create-store",
  async ({}, { container }) => {
    const storeModuleService = container.resolve(Modules.STORE)

    const store = await storeModuleService.createStores({
      name: "My Store",
      supported_currencies: [{
        currency_code: "usd",
        is_default: true,
      }],
    })

    return new StepResponse({ store }, store.id)
  },
  async (storeId, { container }) => {
    if(!storeId) {
      return
    }
    const storeModuleService = container.resolve(Modules.STORE)
    
    await storeModuleService.deleteStores([storeId])
  }
)

export const createStoreWorkflow = createWorkflow(
  "create-store",
  () => {
    const { store } = createStoreStep()

    return new WorkflowResponse({ store })
  }
)
```

You can then execute the workflow in your custom API routes, scheduled jobs, or subscribers:

### API Route

```ts title="src/api/workflow/route.ts" highlights={[["11"], ["12"]]} collapsibleLines="1-6" expandButtonLabel="Show Imports"
import type {
  MedusaRequest,
  MedusaResponse,
} from "@medusajs/framework/http"
import { createStoreWorkflow } from "../../workflows/create-store"

export async function GET(
  req: MedusaRequest,
  res: MedusaResponse
) {
  const { result } = await createStoreWorkflow(req.scope)
    .run()

  res.send(result)
}
```

### Subscriber

```ts title="src/subscribers/user-created.ts" highlights={[["11"], ["12"]]} collapsibleLines="1-6" expandButtonLabel="Show Imports"
import {
  type SubscriberConfig,
  type SubscriberArgs,
} from "@medusajs/framework"
import { createStoreWorkflow } from "../workflows/create-store"

export default async function handleUserCreated({
  event: { data },
  container,
}: SubscriberArgs<{ id: string }>) {
  const { result } = await createStoreWorkflow(container)
    .run()

  console.log(result)
}

export const config: SubscriberConfig = {
  event: "user.created",
}
```

### Scheduled Job

```ts title="src/jobs/run-daily.ts" highlights={[["7"], ["8"]]}
import { MedusaContainer } from "@medusajs/framework/types"
import { createStoreWorkflow } from "../workflows/create-store"

export default async function myCustomJob(
  container: MedusaContainer
) {
  const { result } = await createStoreWorkflow(container)
    .run()

  console.log(result)
}

export const config = {
  name: "run-once-a-day",
  schedule: `0 0 * * *`,
}
```

Learn more about workflows in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md).

***


# User Module

In this section of the documentation, you will find resources to learn more about the User Module and how to use it in your application.

Refer to the [Medusa Admin User Guide](https://docs.medusajs.com/user-guide/settings/users/index.html.md) to learn how to manage users using the dashboard.

Medusa has user related features available out-of-the-box through the User Module. A [module](https://docs.medusajs.com/docs/learn/fundamentals/modules/index.html.md) is a standalone package that provides features for a single domain. Each of Medusa's commerce features are placed in Commerce Modules, such as this User Module.

Learn more about why modules are isolated in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/modules/isolation/index.html.md).

## User Features

- [User Management](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/user/user-creation-flows/index.html.md): Store and manage users in your store.
- [Invite Users](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/user/user-creation-flows#invite-users/index.html.md): Invite users to join your store and manage those invites.

***

## How to Use User Module's Service

In your Medusa application, you build flows around Commerce Modules. A flow is built as a [Workflow](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md), which is a special function composed of a series of steps that guarantees data consistency and reliable roll-back mechanism.

You can build custom workflows and steps. You can also re-use Medusa's workflows and steps, which are provided by the `@medusajs/medusa/core-flows` package.

For example:

```ts title="src/workflows/create-user.ts" highlights={highlights}
import { 
  createWorkflow, 
  WorkflowResponse,
  createStep,
  StepResponse,
} from "@medusajs/framework/workflows-sdk"
import { Modules } from "@medusajs/framework/utils"

const createUserStep = createStep(
  "create-user",
  async ({}, { container }) => {
    const userModuleService = container.resolve(Modules.USER)

    const user = await userModuleService.createUsers({
      email: "user@example.com",
      first_name: "John",
      last_name: "Smith",
    })

    return new StepResponse({ user }, user.id)
  },
  async (userId, { container }) => {
    if (!userId) {
      return
    }
    const userModuleService = container.resolve(Modules.USER)

    await userModuleService.deleteUsers([userId])
  }
)

export const createUserWorkflow = createWorkflow(
  "create-user",
  () => {
    const { user } = createUserStep()

    return new WorkflowResponse({
      user,
    })
  }
)
```

You can then execute the workflow in your custom API routes, scheduled jobs, or subscribers:

### API Route

```ts title="src/api/workflow/route.ts" highlights={[["11"], ["12"]]} collapsibleLines="1-6" expandButtonLabel="Show Imports"
import type {
  MedusaRequest,
  MedusaResponse,
} from "@medusajs/framework/http"
import { createUserWorkflow } from "../../workflows/create-user"

export async function GET(
  req: MedusaRequest,
  res: MedusaResponse
) {
  const { result } = await createUserWorkflow(req.scope)
    .run()

  res.send(result)
}
```

### Subscriber

```ts title="src/subscribers/user-created.ts" highlights={[["11"], ["12"]]} collapsibleLines="1-6" expandButtonLabel="Show Imports"
import {
  type SubscriberConfig,
  type SubscriberArgs,
} from "@medusajs/framework"
import { createUserWorkflow } from "../workflows/create-user"

export default async function handleUserCreated({
  event: { data },
  container,
}: SubscriberArgs<{ id: string }>) {
  const { result } = await createUserWorkflow(container)
    .run()

  console.log(result)
}

export const config: SubscriberConfig = {
  event: "user.created",
}
```

### Scheduled Job

```ts title="src/jobs/run-daily.ts" highlights={[["7"], ["8"]]}
import { MedusaContainer } from "@medusajs/framework/types"
import { createUserWorkflow } from "../workflows/create-user"

export default async function myCustomJob(
  container: MedusaContainer
) {
  const { result } = await createUserWorkflow(container)
    .run()

  console.log(result)
}

export const config = {
  name: "run-once-a-day",
  schedule: `0 0 * * *`,
}
```

Learn more about workflows in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md).

***

## Configure User Module

The User Module accepts options for further configurations. Refer to [this documentation](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/user/module-options/index.html.md) for details on the module's options.

***


# API Key Concepts

In this document, you’ll learn about the different types of API keys, their expiration and verification.

## API Key Types

There are two types of API keys:

- `publishable`: A public key used in client applications, such as a storefront.
- `secret`: A secret key used for authentication and verification purposes, such as an admin user’s authentication token or a password reset token.

The API key’s type is stored in the `type` property of the [ApiKey data model](https://docs.medusajs.com/references/api-key/models/ApiKey/index.html.md).

***

## API Key Expiration

An API key expires when it’s revoked using the [revoke method of the module’s main service](https://docs.medusajs.com/references/api-key/revoke/index.html.md).

The associated token is no longer usable or verifiable.

***

## Token Verification

To verify a token received as an input or in a request, use the [authenticate method of the module’s main service](https://docs.medusajs.com/references/api-key/authenticate/index.html.md) which validates the token against all non-expired tokens.


# Links between API Key Module and Other Modules

This document showcases the module links defined between the API Key Module and other Commerce Modules.

## Summary

The API Key Module has the following links to other modules:

|First Data Model|Second Data Model|Type|Description|
|---|---|---|---|
|ApiKey|SalesChannel|Stored - many-to-many|Learn more|

***

## Sales Channel Module

You can create a publishable API key and associate it with a sales channel. Medusa defines a link between the `ApiKey` and the `SalesChannel` data models.

![A diagram showcasing an example of how data models from the API Key and Sales Channel modules are linked](https://res.cloudinary.com/dza7lstvk/image/upload/v1709812064/Medusa%20Resources/sales-channel-api-key_zmqi2l.jpg)

This is useful to avoid passing the sales channel's ID as a parameter of every request, and instead pass the publishable API key in the header of any request to the Store API route.

Learn more about this in the [Sales Channel Module's documentation](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/sales-channel/publishable-api-keys/index.html.md).

### Retrieve with Query

To retrieve the sales channels of an API key with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `sales_channels.*` in `fields`:

### query.graph

```ts
const { data: apiKeys } = await query.graph({
  entity: "api_key",
  fields: [
    "sales_channels.*",
  ],
})

// apiKeys[0].sales_channels
```

### useQueryGraphStep

```ts
import { useQueryGraphStep } from "@medusajs/medusa/core-flows"

// ...

const { data: apiKeys } = useQueryGraphStep({
  entity: "api_key",
  fields: [
    "sales_channels.*",
  ],
})

// apiKeys[0].sales_channels
```

### Manage with Link

To manage the sales channels of an API key, use [Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link/index.html.md):

### link.create

```ts
import { Modules } from "@medusajs/framework/utils"

// ...

await link.create({
  [Modules.API_KEY]: {
    publishable_key_id: "apk_123",
  },
  [Modules.SALES_CHANNEL]: {
    sales_channel_id: "sc_123",
  },
})
```

### createRemoteLinkStep

```ts
import { Modules } from "@medusajs/framework/utils"
import { createRemoteLinkStep } from "@medusajs/medusa/core-flows"

// ...

createRemoteLinkStep({
  [Modules.API_KEY]: {
    publishable_key_id: "apk_123",
  },
  [Modules.SALES_CHANNEL]: {
    sales_channel_id: "sc_123",
  },
})
```


# Auth Module Provider

In this guide, you’ll learn about the Auth Module Provider and how it's used.

## What is an Auth Module Provider?

An Auth Module Provider handles authenticating customers and users, either using custom logic or by integrating a third-party service.

For example, the EmailPass Auth Module Provider authenticates a user using their email and password, whereas the Google Auth Module Provider authenticates users using their Google account.

### Auth Providers List

- [Emailpass](https://docs.medusajs.com/commerce-modules/auth/auth-providers/emailpass/index.html.md)
- [Google](https://docs.medusajs.com/commerce-modules/auth/auth-providers/google/index.html.md)
- [GitHub](https://docs.medusajs.com/commerce-modules/auth/auth-providers/github/index.html.md)

***

## How to Create an Auth Module Provider?

An Auth Module Provider is a module whose service extends the `AbstractAuthModuleProvider` imported from `@medusajs/framework/utils`.

The module can have multiple auth provider services, where each is registered as a separate auth provider.

Refer to the [Create Auth Module Provider](https://docs.medusajs.com/references/auth/provider/index.html.md) guide to learn how to create an Auth Module Provider.

***

## Configure Allowed Auth Providers of Actor Types

By default, users of all actor types can authenticate with all installed Auth Module Providers.

To restrict the auth providers used for actor types, use the [authMethodsPerActor option](https://docs.medusajs.com/docs/learn/configurations/medusa-config#httpauthMethodsPerActor/index.html.md) in Medusa's configurations:

```ts title="medusa-config.ts"
module.exports = defineConfig({
  projectConfig: {
    http: {
      authMethodsPerActor: {
        user: ["google"],
        customer: ["emailpass"],
      },
      // ...
    },
    // ...
  },
})
```

When you specify the `authMethodsPerActor` configuration, it overrides the default. So, if you don't specify any providers for an actor type, users of that actor type can't authenticate with any provider.


# Auth Identity and Actor Types

In this document, you’ll learn about concepts related to identity and actors in the Auth Module.

## What is an Auth Identity?

The [AuthIdentity data model](https://docs.medusajs.com/references/auth/models/AuthIdentity/index.html.md) represents a user registered by an [authentication provider](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/auth/auth-providers/index.html.md). When a user is registered using an authentication provider, the provider creates a record of `AuthIdentity`.

Then, when the user logs-in in the future with the same authentication provider, the associated auth identity is used to validate their credentials.

***

## Actor Types

An actor type is a type of user that can be authenticated. The Auth Module doesn't store or manage any user-like models, such as for customers or users. Instead, the user types are created and managed by other modules. For example, a customer is managed by the [Customer Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/customer/index.html.md).

Then, when an auth identity is created for the actor type, the ID of the user is stored in the `app_metadata` property of the auth identity.

For example, an auth identity of a customer has the following `app_metadata` property:

```json
{
  "app_metadata": {
    "customer_id": "cus_123"
  }
}
```

The ID of the user is stored in the key `{actor_type}_id` of the `app_metadata` property.

***

## Protect Routes by Actor Type

When you protect routes with the `authenticate` middleware, you specify in its first parameter the actor type that must be authenticated to access the specified API routes.

For example:

```ts title="src/api/middlewares.ts" highlights={highlights}
import { 
  defineMiddlewares,
  authenticate,
} from "@medusajs/framework/http"

export default defineMiddlewares({
  routes: [
    {
      matcher: "/custom/admin*",
      middlewares: [
        authenticate("user", ["session", "bearer", "api-key"]),
      ],
    },
  ],
})
```

By specifying `user` as the first parameter of `authenticate`, only authenticated users of actor type `user` (admin users) can access API routes starting with `/custom/admin`.

***

## Custom Actor Types

You can define custom actor types that allows a custom user, managed by your custom module, to authenticate into Medusa.

For example, if you have a custom module with a `Manager` data model, you can authenticate managers with the `manager` actor type.

Learn how to create a custom actor type in [this guide](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/auth/create-actor-type/index.html.md).


# How to Create an Actor Type

In this document, learn how to create an actor type and authenticate its associated data model.

## 0. Create Module with Data Model

Before creating an actor type, you must have a module with a data model representing the actor type.

Learn how to create a module in [this guide](https://docs.medusajs.com/docs/learn/fundamentals/modules/index.html.md).

The rest of this guide uses this `Manager` data model as an example:

```ts title="src/modules/manager/models/manager.ts"
import { model } from "@medusajs/framework/utils"

const Manager = model.define("manager", {
  id: model.id().primaryKey(),
  firstName: model.text(),
  lastName: model.text(),
  email: model.text(),
})

export default Manager
```

***

## 1. Create Workflow

Start by creating a workflow that does two things:

- Creates a record of the `Manager` data model.
- Sets the `app_metadata` property of the associated `AuthIdentity` record based on the new actor type.

For example, create the file `src/workflows/create-manager.ts`. with the following content:

```ts title="src/workflows/create-manager.ts" highlights={workflowHighlights}
import { 
  createWorkflow, 
  createStep,
  StepResponse,
  WorkflowResponse,
} from "@medusajs/framework/workflows-sdk"
import { 
  setAuthAppMetadataStep,
} from "@medusajs/medusa/core-flows"
import ManagerModuleService from "../modules/manager/service"

type CreateManagerWorkflowInput = {
  manager: {
    first_name: string
    last_name: string
    email: string
  }
  authIdentityId: string
}

const createManagerStep = createStep(
  "create-manager-step",
  async ({ 
    manager: managerData,
  }: Pick<CreateManagerWorkflowInput, "manager">, 
  { container }) => {
    const managerModuleService: ManagerModuleService = 
      container.resolve("manager")

    const manager = await managerModuleService.createManager(
      managerData
    )

    return new StepResponse(manager)
  }
)

const createManagerWorkflow = createWorkflow(
  "create-manager",
  function (input: CreateManagerWorkflowInput) {
    const manager = createManagerStep({
      manager: input.manager,
    })

    setAuthAppMetadataStep({
      authIdentityId: input.authIdentityId,
      actorType: "manager",
      value: manager.id,
    })

    return new WorkflowResponse(manager)
  }
)

export default createManagerWorkflow
```

This workflow accepts the manager’s data and the associated auth identity’s ID as inputs. The next sections explain how the auth identity ID is retrieved.

The workflow has two steps:

1. Create the manager using the `createManagerStep`.
2. Set the `app_metadata` property of the associated auth identity using the `setAuthAppMetadataStep` from Medusa's core workflows. You specify the actor type `manager` in the `actorType` property of the step’s input.

***

## 2. Define the Create API Route

Next, you’ll use the workflow defined in the previous section in an API route that creates a manager.

So, create the file `src/api/manager/route.ts` with the following content:

```ts title="src/api/manager/route.ts" highlights={createRouteHighlights}
import type { 
  AuthenticatedMedusaRequest,
  MedusaResponse,
} from "@medusajs/framework/http"
import { MedusaError } from "@medusajs/framework/utils"
import createManagerWorkflow from "../../workflows/create-manager"

type RequestBody = {
  first_name: string
  last_name: string
  email: string
}

export async function POST(
  req: AuthenticatedMedusaRequest<RequestBody>, 
  res: MedusaResponse
) {
  // If `actor_id` is present, the request carries 
  // authentication for an existing manager
  if (req.auth_context.actor_id) {
    throw new MedusaError(
      MedusaError.Types.INVALID_DATA,
      "Request already authenticated as a manager."
    )
  }

  const { result } = await createManagerWorkflow(req.scope)
    .run({
      input: {
        manager: req.body,
        authIdentityId: req.auth_context.auth_identity_id,
      },
    })
  
    res.status(200).json({ manager: result })
}
```

Since the manager must be associated with an `AuthIdentity` record, the request is expected to be authenticated, even if the manager isn’t created yet. This can be achieved by:

1. Obtaining a token usng the [/auth route](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/auth/authentication-route/index.html.md).
2. Passing the token in the bearer header of the request to this route.

In the API route, you create the manager using the workflow from the previous section and return it in the response.

***

## 3. Apply the `authenticate` Middleware

The last step is to apply the `authenticate` middleware on the API routes that require a manager’s authentication.

To do that, create the file `src/api/middlewares.ts` with the following content:

```ts title="src/api/middlewares.ts" highlights={middlewareHighlights}
import { 
  defineMiddlewares,
  authenticate,
} from "@medusajs/framework/http"

export default defineMiddlewares({
  routes: [
    {
      matcher: "/manager",
      method: "POST",
      middlewares: [
        authenticate("manager", ["session", "bearer"], {
          allowUnregistered: true,
        }),
      ],
    },
    {
      matcher: "/manager/me*",
      middlewares: [
        authenticate("manager", ["session", "bearer"]),
      ],
    },
  ],
})
```

This applies middlewares on two route patterns:

1. The `authenticate` middleware is applied on the `/manager` API route for `POST` requests while allowing unregistered managers. This requires that a bearer token be passed in the request to access the manager’s auth identity but doesn’t require the manager to be registered.
2. The `authenticate` middleware is applied on all routes starting with `/manager/me`, restricting these routes to authenticated managers only.

### Retrieve Manager API Route

For example, create the file `src/api/manager/me/route.ts` with the following content:

```ts title="src/api/manager/me/route.ts"
import { 
  AuthenticatedMedusaRequest,
  MedusaResponse,
} from "@medusajs/framework/http"
import ManagerModuleService from "../../../modules/manager/service"

export async function GET(
  req: AuthenticatedMedusaRequest,
  res: MedusaResponse
): Promise<void> {
  const query = req.scope.resolve("query")
  const managerId = req.auth_context?.actor_id

  const { data: [manager] } = await query.graph({
    entity: "manager",
    fields: ["*"],
    filters: {
      id: managerId,
    },
  }, {
    throwIfKeyNotFound: true,
  })

  res.json({ manager })
}
```

This route is only accessible by authenticated managers. You access the manager’s ID using `req.auth_context.actor_id`.

***

## Test Custom Actor Type Authentication Flow

To authenticate managers:

1. Send a `POST` request to `/auth/manager/emailpass/register` to create an auth identity for the manager:

```bash
curl -X POST 'http://localhost:9000/auth/manager/emailpass/register' \
-H 'Content-Type: application/json' \
--data-raw '{
    "email": "manager@gmail.com",
    "password": "supersecret"
}'
```

Copy the returned token to use it in the next request.

2. Send a `POST` request to `/manager` to create a manager:

```bash
curl -X POST 'http://localhost:9000/manager' \
-H 'Content-Type: application/json' \
-H 'Authorization: Bearer {token}' \
--data-raw '{
    "first_name": "John",
    "last_name": "Doe",
    "email": "manager@gmail.com"
}'
```

Replace `{token}` with the token returned in the previous step.

3. Send a `POST` request to `/auth/manager/emailpass` again to retrieve an authenticated token for the manager:

```bash
curl -X POST 'http://localhost:9000/auth/manager/emailpass' \
-H 'Content-Type: application/json' \
--data-raw '{
    "email": "manager@gmail.com",
    "password": "supersecret"
}'
```

4. You can now send authenticated requests as a manager. For example, send a `GET` request to `/manager/me` to retrieve the authenticated manager’s details:

```bash
curl 'http://localhost:9000/manager/me' \
-H 'Authorization: Bearer {token}'
```

Whenever you want to log in as a manager, use the `/auth/manager/emailpass` API route, as explained in step 3.

***

## Delete User of Actor Type

When you delete a user of the actor type, you must update its auth identity to remove the association to the user.

For example, create the following workflow that deletes a manager and updates its auth identity, create the file `src/workflows/delete-manager.ts` with the following content:

```ts title="src/workflows/delete-manager.ts" collapsibleLines="1-6" expandButtonLabel="Show Imports"
import {
  createStep,
  StepResponse,
} from "@medusajs/framework/workflows-sdk"
import ManagerModuleService from "../modules/manager/service"

export type DeleteManagerWorkflow = {
  id: string
}

const deleteManagerStep = createStep(
  "delete-manager-step",
  async (
    { id }: DeleteManagerWorkflow, 
    { container }) => {
      const managerModuleService: ManagerModuleService = 
        container.resolve("manager")

      const manager = await managerModuleService.retrieve(id)

      await managerModuleService.deleteManagers(id)

      return new StepResponse(undefined, { manager })
    },
    async ({ manager }, { container }) => {
      const managerModuleService: ManagerModuleService = 
        container.resolve("manager")

      await managerModuleService.createManagers(manager)
    }
  )
```

You add a step that deletes the manager using the `deleteManagers` method of the module's main service. In the compensation function, you create the manager again.

Next, in the same file, add the workflow that deletes a manager:

```ts title="src/workflows/delete-manager.ts" collapsibleLines="1-15" expandButtonLabel="Show Imports" highlights={deleteHighlights}
// other imports
import { MedusaError } from "@medusajs/framework/utils"
import {
  WorkflowData,
  WorkflowResponse,
  createWorkflow,
  transform,
} from "@medusajs/framework/workflows-sdk"
import { 
  setAuthAppMetadataStep,
  useQueryGraphStep,
} from "@medusajs/medusa/core-flows"

// ...

export const deleteManagerWorkflow = createWorkflow(
  "delete-manager",
  (
    input: WorkflowData<DeleteManagerWorkflow>
  ): WorkflowResponse<string> => {
    deleteManagerStep(input)

    const { data: authIdentities } = useQueryGraphStep({
      entity: "auth_identity",
      fields: ["id"],
      filters: {
        app_metadata: {
          // the ID is of the format `{actor_type}_id`.
          manager_id: input.id,
        },
      },
    })

    const authIdentity = transform(
      { authIdentities },
      ({ authIdentities }) => {
        const authIdentity = authIdentities[0]

        if (!authIdentity) {
          throw new MedusaError(
            MedusaError.Types.NOT_FOUND,
            "Auth identity not found"
          )
        }

        return authIdentity
      }
    )

    setAuthAppMetadataStep({
      authIdentityId: authIdentity.id,
      actorType: "manager",
      value: null,
    })

    return new WorkflowResponse(input.id)
  }
)
```

In the workflow, you:

1. Use the `deleteManagerStep` defined earlier to delete the manager.
2. Retrieve the auth identity of the manager using Query. To do that, you filter the `app_metadata` property of an auth identity, which holds the user's ID under `{actor_type_name}_id`. So, in this case, it's `manager_id`.
3. Check that the auth identity exist, then, update the auth identity to remove the ID of the manager from it.

You can use this workflow when deleting a manager, such as in an API route.


# Auth Module Options

In this document, you'll learn about the options of the Auth Module.

## providers

The `providers` option is an array of auth module providers.

When the Medusa application starts, these providers are registered and can be used to handle authentication.

By default, the `emailpass` provider is registered to authenticate customers and admin users.

For example:

```ts title="medusa-config.ts"
import { Modules, ContainerRegistrationKeys } from "@medusajs/framework/utils"

// ...

module.exports = defineConfig({
  // ...
  modules: [
    {
      resolve: "@medusajs/medusa/auth",
      dependencies: [Modules.CACHE, ContainerRegistrationKeys.LOGGER],
      options: {
        providers: [
          {
            resolve: "@medusajs/medusa/auth-emailpass",
            id: "emailpass",
            options: {
              // provider options...
            },
          },
        ],
      },
    },
  ],
})
```

The `providers` option is an array of objects that accept the following properties:

- `resolve`: A string indicating the package name of the module provider or the path to it relative to the `src` directory.
- `id`: A string indicating the provider's unique name or ID.
- `options`: An optional object of the module provider's options.

***

## Auth CORS

The Medusa application's authentication API routes are defined under the `/auth` prefix that requires setting the `authCors` property of the `http` configuration.

By default, the Medusa application you created will have an `AUTH_CORS` environment variable, which is used as the value of `authCors`.

Refer to [Medusa's configuration guide](https://docs.medusajs.com/docs/learn/configurations/medusa-config#httpauthCors/index.html.md) to learn more about the `authCors` configuration.

***

## authMethodsPerActor Configuration

The Medusa application's configuration accept an `authMethodsPerActor` configuration which restricts the allowed auth providers used with an actor type.

Learn more about the `authMethodsPerActor` configuration in [this guide](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/auth/auth-providers#configure-allowed-auth-providers-of-actor-types/index.html.md).


# How to Handle Password Reset Token Event

In this guide, you'll learn how to handle the `auth.password_reset` event, which is emitted when a request is sent to the [Generate Reset Password Token API route](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/auth/authentication-route#generate-reset-password-token-route/index.html.md).

Refer to this [Medusa Admin User Guide](https://docs.medusajs.com/user-guide/reset-password/index.html.md) to learn how to reset your user admin password using the dashboard.

You'll create a subscriber that listens to the event. When the event is emitted, the subscriber sends an email notification to the user.

### Prerequisites

- [A notification provider module, such as SendGrid](https://docs.medusajs.com/infrastructure-modules/notification/sendgrid/index.html.md)

## 1. Create Subscriber

The first step is to create a subscriber that listens to the `auth.password_reset` and sends the user a notification with instructions to reset their password.

Create the file `src/subscribers/handle-reset.ts` with the following content:

```ts title="src/subscribers/handle-reset.ts" highlights={highlights} collapsibleLines="1-6" expandMoreLabel="Show Imports"
import {
  SubscriberArgs,
  type SubscriberConfig,
} from "@medusajs/medusa"
import { Modules } from "@medusajs/framework/utils"

export default async function resetPasswordTokenHandler({
  event: { data: {
    entity_id: email,
    token,
    actor_type,
  } },
  container,
}: SubscriberArgs<{ entity_id: string, token: string, actor_type: string }>) {
  const notificationModuleService = container.resolve(
    Modules.NOTIFICATION
  )

  const urlPrefix = actor_type === "customer" ? 
    "https://storefront.com" : 
    "https://admin.com/app"

  await notificationModuleService.createNotifications({
    to: email,
    channel: "email",
    template: "reset-password-template",
    data: {
      // a URL to a frontend application
      url: `${urlPrefix}/reset-password?token=${token}&email=${email}`,
    },
  })
}

export const config: SubscriberConfig = {
  event: "auth.password_reset",
}
```

You subscribe to the `auth.password_reset` event. The event has a data payload object with the following properties:

- `entity_id`: The identifier of the user. When using the `emailpass` provider, it's the user's email.
- `token`: The token to reset the user's password.
- `actor_type`: The user's actor type. For example, if the user is a customer, the `actor_type` is `customer`. If it's an admin user, the `actor_type` is `user`.

This event's payload previously had an `actorType` field. It was renamed to `actor_type` after [Medusa v2.0.7](https://github.com/medusajs/medusa/releases/tag/v2.0.7).

In the subscriber, you:

- Decide the frontend URL based on whether the user is a customer or admin user by checking the value of `actor_type`.
- Resolve the Notification Module and use its `createNotifications` method to send the notification.
- You pass to the `createNotifications` method an object having the following properties:
  - `to`: The identifier to send the notification to, which in this case is the email.
  - `channel`: The channel to send the notification through, which in this case is email.
  - `template`: The template ID in the third-party service.
  - `data`: The data payload to pass to the template. You pass the URL to redirect the user to. You must pass the token and email in the URL so that the frontend can send them later to the Medusa application when reseting the password.

***

## 2. Test it Out: Generate Reset Password Token

To test the subscriber out, send a request to the `/auth/{actor_type}/{auth_provider}/reset-password` API route, replacing `{actor_type}` and `{auth_provider}` with the user's actor type and provider used for authentication respectively.

For example, to generate a reset password token for an admin user using the `emailpass` provider, send the following request:

```bash
curl --location 'http://localhost:9000/auth/user/emailpass/reset-password' \
--header 'Content-Type: application/json' \
--data-raw '{
    "identifier": "admin-test@gmail.com"
}'
```

In the request body, you must pass an `identifier` parameter. Its value is the user's identifier, which is the email in this case.

If the token is generated successfully, the request returns a response with `201` status code. In the terminal, you'll find the following message indicating that the `auth.password_reset` event was emitted and your subscriber ran:

```plain
info:    Processing auth.password_reset which has 1 subscribers
```

The notification is sent to the user with the frontend URL to enter a new password.

***

## Next Steps: Implementing Frontend

In your frontend, you must have a page that accepts `token` and `email` query parameters.

The page shows the user password fields to enter their new password, then submits the new password, token, and email to the [Reset Password Route](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/auth/authentication-route#reset-password-route/index.html.md).

### Examples

- [Storefront Guide: Reset Customer Password](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/storefront-development/customers/reset-password/index.html.md)


# Customer Accounts

In this document, you’ll learn how registered and unregistered accounts are distinguished in the Medusa application.

Refer to this [Medusa Admin User Guide](https://docs.medusajs.com/user-guide/customers/index.html.md) to learn how to manage customers using the dashboard.

## `has_account` Property

The [Customer data model](https://docs.medusajs.com/references/customer/models/Customer/index.html.md) has a `has_account` property, which is a boolean that indicates whether a customer is registered.

When a guest customer places an order, a new `Customer` record is created with `has_account` set to `false`.

When this or another guest customer registers an account with the same email, a new `Customer` record is created with `has_account` set to `true`.

***

## Email Uniqueness

The above behavior means that two `Customer` records may exist with the same email. However, the main difference is the `has_account` property's value.

So, there can only be one guest customer (having `has_account=false`) and one registered customer (having `has_account=true`) with the same email.


# Links between Customer Module and Other Modules

This document showcases the module links defined between the Customer Module and other Commerce Modules.

## Summary

The Customer Module has the following links to other modules:

Read-only links are used to query data across modules, but the relations aren't stored in a pivot table in the database.

|First Data Model|Second Data Model|Type|Description|
|---|---|---|---|
|Customer|AccountHolder|Stored - many-to-many|Learn more|
|Cart|Customer|Read-only - has one|Learn more|
|Order|Customer|Read-only - has one|Learn more|

***

## Payment Module

Medusa defines a link between the `Customer` and `AccountHolder` data models, allowing payment providers to save payment methods for a customer, if the payment provider supports it.

This link is available starting from Medusa `v2.5.0`.

### Retrieve with Query

To retrieve the account holder associated with a customer with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `customer.*` in `fields`:

### query.graph

```ts
const { data: customers } = await query.graph({
  entity: "customer",
  fields: [
    "account_holder_link.account_holder.*",
  ],
})

// customers[0].account_holder_link?.[0]?.account_holder
```

### useQueryGraphStep

```ts
import { useQueryGraphStep } from "@medusajs/medusa/core-flows"

// ...

const { data: customers } = useQueryGraphStep({
  entity: "customer",
  fields: [
    "account_holder_link.account_holder.*",
  ],
})

// customers[0].account_holder_link?.[0]?.account_holder
```

### Manage with Link

To manage the account holders of a customer, use [Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link/index.html.md):

### link.create

```ts
import { Modules } from "@medusajs/framework/utils"

// ...

await link.create({
  [Modules.CUSTOMER]: {
    customer_id: "cus_123",
  },
  [Modules.PAYMENT]: {
    account_holder_id: "acchld_123",
  },
})
```

### createRemoteLinkStep

```ts
import { createRemoteLinkStep } from "@medusajs/medusa/core-flows"

// ...

createRemoteLinkStep({
  [Modules.CUSTOMER]: {
    customer_id: "cus_123",
  },
  [Modules.PAYMENT]: {
    account_holder_id: "acchld_123",
  },
})
```

***

## Cart Module

Medusa defines a read-only link between the [Cart Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/cart/index.html.md)'s `Cart` data model and the `Customer` data model. Because the link is read-only from the `Cart`'s side, you can only retrieve the customer of a cart, and not the other way around.

### Retrieve with Query

To retrieve the customer of a cart with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `customer.*` in `fields`:

### query.graph

```ts
const { data: carts } = await query.graph({
  entity: "cart",
  fields: [
    "customer.*",
  ],
})

// carts.customer
```

### useQueryGraphStep

```ts
import { useQueryGraphStep } from "@medusajs/medusa/core-flows"

// ...

const { data: carts } = useQueryGraphStep({
  entity: "cart",
  fields: [
    "customer.*",
  ],
})

// carts.customer
```

***

## Order Module

Medusa defines a read-only link between the [Order Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/order/index.html.md)'s `Order` data model and the `Customer` data model. Because the link is read-only from the `Order`'s side, you can only retrieve the customer of an order, and not the other way around.

### Retrieve with Query

To retrieve the customer of an order with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `customer.*` in `fields`:

### query.graph

```ts
const { data: orders } = await query.graph({
  entity: "order",
  fields: [
    "customer.*",
  ],
})

// orders.customer
```

### useQueryGraphStep

```ts
import { useQueryGraphStep } from "@medusajs/medusa/core-flows"

// ...

const { data: orders } = useQueryGraphStep({
  entity: "order",
  fields: [
    "customer.*",
  ],
})

// orders.customer
```


# Fulfillment Concepts

In this document, you’ll learn about some basic fulfillment concepts.

## Fulfillment Set

A fulfillment set is a general form or way of fulfillment. For example, shipping is a form of fulfillment, and pick-up is another form of fulfillment. Each of these can be created as fulfillment sets.

A fulfillment set is represented by the [FulfillmentSet data model](https://docs.medusajs.com/references/fulfillment/models/FulfillmentSet/index.html.md). All other configurations, options, and management features are related to a fulfillment set, in one way or another.

```ts
const fulfillmentSets = await fulfillmentModuleService.createFulfillmentSets(
  [
    {
      name: "Shipping",
      type: "shipping",
    },
    {
      name: "Pick-up",
      type: "pick-up",
    },
  ]
)
```

***

## Service Zone

A service zone is a collection of geographical zones or areas. It’s used to restrict available shipping options to a defined set of locations.

A service zone is represented by the [ServiceZone data model](https://docs.medusajs.com/references/fulfillment/models/ServiceZone/index.html.md). It’s associated with a fulfillment set, as each service zone is specific to a form of fulfillment. For example, if a customer chooses to pick up items, you can restrict the available shipping options based on their location.

![A diagram showcasing the relation between fulfillment sets, service zones, and geo zones](https://res.cloudinary.com/dza7lstvk/image/upload/v1712329770/Medusa%20Resources/service-zone_awmvfs.jpg)

A service zone can have multiple geographical zones, each represented by the [GeoZone data model](https://docs.medusajs.com/references/fulfillment/models/GeoZone/index.html.md). It holds location-related details to narrow down supported areas, such as country, city, or province code.

The province code is always in lower-case and in [ISO 3166-2 format](https://en.wikipedia.org/wiki/ISO_3166-2).

***

## Shipping Profile

A shipping profile defines a type of items that are shipped in a similar manner. For example, a `default` shipping profile is used for all item types, but the `digital` shipping profile is used for digital items that aren’t shipped and delivered conventionally.

A shipping profile is represented by the [ShippingProfile data model](https://docs.medusajs.com/references/fulfillment/models/ShippingProfile/index.html.md). It only defines the profile’s details, but it’s associated with the shipping options available for the item type.


# Fulfillment Module Provider

In this guide, you’ll learn about the Fulfillment Module Provider and how it's used.

Refer to this [Medusa Admin User Guide](https://docs.medusajs.com/user-guide/settings/locations-and-shipping/locations#manage-fulfillment-providers/index.html.md) to learn how to add a fulfillment provider to a location using the dashboard.

## What is a Fulfillment Module Provider?

A Fulfillment Module Provider handles fulfilling items, typically using a third-party integration.

Fulfillment Module Providers registered in the Fulfillment Module's [options](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/fulfillment/module-options/index.html.md) are stored and represented by the [FulfillmentProvider data model](https://docs.medusajs.com/references/fulfillment/models/FulfillmentProvider/index.html.md).

![Diagram showcasing the communication between Medusa, the Fulfillment Module Provider, and the third-party fulfillment provider.](https://res.cloudinary.com/dza7lstvk/image/upload/v1746794800/Medusa%20Resources/fulfillment-provider-service_ljsqpq.jpg)

***

## Default Fulfillment Provider

Medusa provides a Manual Fulfillment Provider that acts as a placeholder fulfillment provider. It doesn't process fulfillment and delegates that to the merchant.

This provider is installed by default in your application and you can use it to fulfill items manually.

The identifier of the manual fulfillment provider is `fp_manual_manual`.

***

## How to Create a Custom Fulfillment Provider?

A Fulfillment Module Provider is a module whose service implements the `IFulfillmentProvider` imported from `@medusajs/framework/types`.

The module can have multiple fulfillment provider services, where each are registered as separate fulfillment providers.

Refer to the [Create Fulfillment Module Provider](https://docs.medusajs.com/references/fulfillment/provider/index.html.md) guide to learn how to create a Fulfillment Module Provider.

{/* TODO add link to user guide */}

After you create a fulfillment provider, you can choose it as the default Fulfillment Module Provider for a stock location in the Medusa Admin dashboard.

***

## How are Fulfillment Providers Registered?

### Configure Fulfillment Module's Providers

The Fulfillment Module accepts a `providers` option that allows you to configure the providers registered in your application.

Learn more about this option in the [Module Options](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/fulfillment/module-options/index.html.md) guide.

### Registration on Application Start

When the Medusa application starts, it registers the Fulfillment Module Providers defined in the `providers` option of the Fulfillment Module.

For each Fulfillment Module Provider, the Medusa application finds all fulfillment provider services defined in them to register.

### FulfillmentProvider Data Model

A registered fulfillment provider is represented by the [FulfillmentProvider data model](https://docs.medusajs.com/references/fulfillment/models/FulfillmentProvider/index.html.md) in the Medusa application.

This data model is used to reference a service in the Fulfillment Module Provider and determine whether it's installed in the application.

![Diagram showcasing the FulfillmentProvider data model](https://res.cloudinary.com/dza7lstvk/image/upload/v1746794803/Medusa%20Resources/fulfillment-provider-model_wo2ato.jpg)

The `FulfillmentProvider` data model has the following properties:

- `id`: The unique identifier of the fulfillment provider. The ID's format is `fp_{identifier}_{id}`, where:
  - `identifier` is the value of the `identifier` property in the Fulfillment Module Provider's service.
  - `id` is the value of the `id` property of the Fulfillment Module Provider in `medusa-config.ts`.
- `is_enabled`: A boolean indicating whether the fulfillment provider is enabled.

### How to Remove a Fulfillment Provider?

You can remove a registered fulfillment provider from the Medusa application by removing it from the `providers` option in the Fulfillment Module's configuration.

Then, the next time the Medusa application starts, it will set the `is_enabled` property of the `FulfillmentProvider`'s record to `false`. This allows you to re-enable the fulfillment provider later if needed by adding it back to the `providers` option.


# Item Fulfillment

In this document, you’ll learn about the concepts of item fulfillment.

## Fulfillment Data Model

A fulfillment is the shipping and delivery of one or more items to the customer. It’s represented by the [Fulfillment data model](https://docs.medusajs.com/references/fulfillment/models/Fulfillment/index.html.md).

***

## Fulfillment Processing by a Fulfillment Provider

A fulfillment is associated with a fulfillment provider that handles all its processing, such as creating a shipment for the fulfillment’s items.

The fulfillment is also associated with a shipping option of that provider, which determines how the item is shipped.

![A diagram showcasing the relation between a fulfillment, fulfillment provider, and shipping option](https://res.cloudinary.com/dza7lstvk/image/upload/v1712331947/Medusa%20Resources/fulfillment-shipping-option_jk9ndp.jpg)

***

## data Property

The `Fulfillment` data model has a `data` property that holds any necessary data for the third-party fulfillment provider to process the fulfillment.

For example, the `data` property can hold the ID of the fulfillment in the third-party provider. The associated fulfillment provider then uses it whenever it retrieves the fulfillment’s details.

***

## Fulfillment Items

A fulfillment is used to fulfill one or more items. Each item is represented by the `FulfillmentItem` data model.

The fulfillment item holds details relevant to fulfilling the item, such as barcode, SKU, and quantity to fulfill.

![A diagram showcasing the relation between fulfillment and fulfillment items.](https://res.cloudinary.com/dza7lstvk/image/upload/v1712332114/Medusa%20Resources/fulfillment-item_etzxb0.jpg)

***

## Fulfillment Label

Once a shipment is created for the fulfillment, you can store its tracking number, URL, or other related details as a label, represented by the `FulfillmentLabel` data model.

***

## Fulfillment Status

The `Fulfillment` data model has three properties to keep track of the current status of the fulfillment:

- `packed_at`: The date the fulfillment was packed. If set, then the fulfillment has been packed.
- `shipped_at`: The date the fulfillment was shipped. If set, then the fulfillment has been shipped.
- `delivered_at`: The date the fulfillment was delivered. If set, then the fulfillment has been delivered.


# Links between Fulfillment Module and Other Modules

This document showcases the module links defined between the Fulfillment Module and other Commerce Modules.

## Summary

The Fulfillment Module has the following links to other modules:

|First Data Model|Second Data Model|Type|Description|
|---|---|---|---|
|Order|Fulfillment|Stored - one-to-many|Learn more|
|Return|Fulfillment|Stored - one-to-many|Learn more|
|PriceSet|ShippingOption|Stored - many-to-one|Learn more|
|Product|ShippingProfile|Stored - many-to-one|Learn more|
|StockLocation|FulfillmentProvider|Stored - one-to-many|Learn more|
|StockLocation|FulfillmentSet|Stored - one-to-many|Learn more|

***

## Order Module

The [Order Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/order/index.html.md) provides order-management functionalities.

Medusa defines a link between the `Fulfillment` and `Order` data models. A fulfillment is created for an orders' items.

![A diagram showcasing an example of how data models from the Fulfillment and Order modules are linked](https://res.cloudinary.com/dza7lstvk/image/upload/v1716549903/Medusa%20Resources/order-fulfillment_h0vlps.jpg)

A fulfillment is also created for a return's items. So, Medusa defines a link between the `Fulfillment` and `Return` data models.

![A diagram showcasing an example of how data models from the Fulfillment and Order modules are linked](https://res.cloudinary.com/dza7lstvk/image/upload/v1728399052/Medusa%20Resources/Social_Media_Graphics_2024_Order_Return_vetimk.jpg)

### Retrieve with Query

To retrieve the order of a fulfillment with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `order.*` in `fields`:

To retrieve the return, pass `return.*` in `fields`.

### query.graph

```ts
const { data: fulfillments } = await query.graph({
  entity: "fulfillment",
  fields: [
    "order.*",
  ],
})

// fulfillments.order
```

### useQueryGraphStep

```ts
import { useQueryGraphStep } from "@medusajs/medusa/core-flows"

// ...

const { data: fulfillments } = useQueryGraphStep({
  entity: "fulfillment",
  fields: [
    "order.*",
  ],
})

// fulfillments.order
```

### Manage with Link

To manage the order of a cart, use [Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link/index.html.md):

### link.create

```ts
import { Modules } from "@medusajs/framework/utils"

// ...

await link.create({
  [Modules.ORDER]: {
    order_id: "order_123",
  },
  [Modules.FULFILLMENT]: {
    fulfillment_id: "ful_123",
  },
})
```

### createRemoteLinkStep

```ts
import { Modules } from "@medusajs/framework/utils"
import { createRemoteLinkStep } from "@medusajs/medusa/core-flows"

// ...

createRemoteLinkStep({
  [Modules.ORDER]: {
    order_id: "order_123",
  },
  [Modules.FULFILLMENT]: {
    fulfillment_id: "ful_123",
  },
})
```

***

## Pricing Module

The Pricing Module provides features to store, manage, and retrieve the best prices in a specified context.

Medusa defines a link between the `PriceSet` and `ShippingOption` data models. A shipping option's price is stored as a price set.

![A diagram showcasing an example of how data models from the Pricing and Fulfillment modules are linked](https://res.cloudinary.com/dza7lstvk/image/upload/v1716561747/Medusa%20Resources/pricing-fulfillment_spywwa.jpg)

### Retrieve with Query

To retrieve the price set of a shipping option with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `price_set.*` in `fields`:

### query.graph

```ts
const { data: shippingOptions } = await query.graph({
  entity: "shipping_option",
  fields: [
    "price_set_link.*",
  ],
})

// shippingOptions[0].price_set_link?.price_set_id
```

### useQueryGraphStep

```ts
import { useQueryGraphStep } from "@medusajs/medusa/core-flows"

// ...

const { data: shippingOptions } = useQueryGraphStep({
  entity: "shipping_option",
  fields: [
    "price_set_link.*",
  ],
})

// shippingOptions[0].price_set_link?.price_set_id
```

### Manage with Link

To manage the price set of a shipping option, use [Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link/index.html.md):

### link.create

```ts
import { Modules } from "@medusajs/framework/utils"

// ...

await link.create({
  [Modules.FULFILLMENT]: {
    shipping_option_id: "so_123",
  },
  [Modules.PRICING]: {
    price_set_id: "pset_123",
  },
})
```

### createRemoteLinkStep

```ts
import { Modules } from "@medusajs/framework/utils"
import { createRemoteLinkStep } from "@medusajs/medusa/core-flows"

// ...

createRemoteLinkStep({
  [Modules.FULFILLMENT]: {
    shipping_option_id: "so_123",
  },
  [Modules.PRICING]: {
    price_set_id: "pset_123",
  },
})
```

***

## Product Module

Medusa defines a link between the `ShippingProfile` data model and the `Product` data model of the Product Module. Each product must belong to a shipping profile.

This link is introduced in [Medusa v2.5.0](https://github.com/medusajs/medusa/releases/tag/v2.5.0).

### Retrieve with Query

To retrieve the products of a shipping profile with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `products.*` in `fields`:

### query.graph

```ts
const { data: shippingProfiles } = await query.graph({
  entity: "shipping_profile",
  fields: [
    "products.*",
  ],
})

// shippingProfiles[0].products
```

### useQueryGraphStep

```ts
import { useQueryGraphStep } from "@medusajs/medusa/core-flows"

// ...

const { data: shippingProfiles } = useQueryGraphStep({
  entity: "shipping_profile",
  fields: [
    "products.*",
  ],
})

// shippingProfiles[0].products
```

### Manage with Link

To manage the shipping profile of a product, use [Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link/index.html.md):

### link.create

```ts
import { Modules } from "@medusajs/framework/utils"

// ...

await link.create({
  [Modules.PRODUCT]: {
    product_id: "prod_123",
  },
  [Modules.FULFILLMENT]: {
    shipping_profile_id: "sp_123",
  },
})
```

### createRemoteLinkStep

```ts
import { Modules } from "@medusajs/framework/utils"
import { createRemoteLinkStep } from "@medusajs/medusa/core-flows"

// ...

createRemoteLinkStep({
  [Modules.PRODUCT]: {
    product_id: "prod_123",
  },
  [Modules.FULFILLMENT]: {
    shipping_profile_id: "sp_123",
  },
})
```

***

## Stock Location Module

The Stock Location Module provides features to manage stock locations in a store.

Medusa defines a link between the `FulfillmentSet` and `StockLocation` data models. A fulfillment set can be conditioned to a specific stock location.

![A diagram showcasing an example of how data models from the Fulfillment and Stock Location modules are linked](https://res.cloudinary.com/dza7lstvk/image/upload/v1712567101/Medusa%20Resources/fulfillment-stock-location_nlkf7e.jpg)

Medusa also defines a link between the `FulfillmentProvider` and `StockLocation` data models to indicate the providers that can be used in a location.

![A diagram showcasing an example of how data models from the Fulfillment and Stock Location modules are linked](https://res.cloudinary.com/dza7lstvk/image/upload/v1728399492/Medusa%20Resources/fulfillment-provider-stock-location_b0mulo.jpg)

### Retrieve with Query

To retrieve the stock location of a fulfillment set with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `location.*` in `fields`:

To retrieve the stock location of a fulfillment provider, pass `locations.*` in `fields`.

### query.graph

```ts
const { data: fulfillmentSets } = await query.graph({
  entity: "fulfillment_set",
  fields: [
    "location.*",
  ],
})

// fulfillmentSets[0].location
```

### useQueryGraphStep

```ts
import { useQueryGraphStep } from "@medusajs/medusa/core-flows"

// ...

const { data: fulfillmentSets } = useQueryGraphStep({
  entity: "fulfillment_set",
  fields: [
    "location.*",
  ],
})

// fulfillmentSets[0].location
```

### Manage with Link

To manage the stock location of a fulfillment set, use [Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link/index.html.md):

### link.create

```ts
import { Modules } from "@medusajs/framework/utils"

// ...

await link.create({
  [Modules.STOCK_LOCATION]: {
    stock_location_id: "sloc_123",
  },
  [Modules.FULFILLMENT]: {
    fulfillment_set_id: "fset_123",
  },
})
```

### createRemoteLinkStep

```ts
import { Modules } from "@medusajs/framework/utils"
import { createRemoteLinkStep } from "@medusajs/medusa/core-flows"

// ...

createRemoteLinkStep({
  [Modules.STOCK_LOCATION]: {
    stock_location_id: "sloc_123",
  },
  [Modules.FULFILLMENT]: {
    fulfillment_set_id: "fset_123",
  },
})
```


# Fulfillment Module Options

In this document, you'll learn about the options of the Fulfillment Module.

## providers

The `providers` option is an array of fulfillment module providers.

When the Medusa application starts, these providers are registered and can be used to process fulfillments.

For example:

```ts title="medusa-config.ts"
import { Modules } from "@medusajs/framework/utils"

// ...

module.exports = defineConfig({
  // ...
  modules: [
    {
      resolve: "@medusajs/medusa/fulfillment",
      options: {
        providers: [
          {
            resolve: `@medusajs/medusa/fulfillment-manual`,
            id: "manual",
            options: {
              // provider options...
            },
          },
        ],
      },
    },
  ],
})
```

The `providers` option is an array of objects that accept the following properties:

- `resolve`: A string indicating either the package name  of the module provider or the path to it relative to the `src` directory.
- `id`: A string indicating the provider's unique name or ID.
- `options`: An optional object of the module provider's options.


# Shipping Option

In this document, you’ll learn about shipping options and their rules.

## What’s a Shipping Option?

A shipping option is a way of shipping an item. Each fulfillment provider provides a set of shipping options. For example, a provider may provide a shipping option for express shipping and another for standard shipping.

When the customer places their order, they choose a shipping option to be used to fulfill their items.

A shipping option is represented by the [ShippingOption data model](https://docs.medusajs.com/references/fulfillment/models/ShippingOption/index.html.md).

***

## Service Zone Restrictions

A shipping option is restricted by a service zone, limiting the locations a shipping option be used in.

For example, a fulfillment provider may have a shipping option that can be used in the United States, and another in Canada.

![A diagram showcasing the relation between shipping options and service zones.](https://res.cloudinary.com/dza7lstvk/image/upload/v1712330831/Medusa%20Resources/shipping-option-service-zone_pobh6k.jpg)

Service zones can be more restrictive, such as restricting to certain cities or province codes.

The province code is always in lower-case and in [ISO 3166-2 format](https://en.wikipedia.org/wiki/ISO_3166-2).

![A diagram showcasing the relation between shipping options, service zones, and geo zones](https://res.cloudinary.com/dza7lstvk/image/upload/v1712331186/Medusa%20Resources/shipping-option-service-zone-city_m5sxod.jpg)

***

## Shipping Option Rules

You can restrict shipping options by custom rules, such as the item’s weight or the customer’s group.

You can also restrict a shipping option's price based on specific conditions. For example, you can make a shipping option's price free based on the cart's total. Learn more in the Pricing Module's [Price Rules](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/pricing/price-rules#how-to-set-rules-on-a-price/index.html.md) guide.

These rules are represented by the [ShippingOptionRule data model](https://docs.medusajs.com/references/fulfillment/models/ShippingOptionRule/index.html.md). Its properties define the custom rule:

- `attribute`: The name of a property or table that the rule applies to. For example, `customer_group`.
- `operator`: The operator used in the condition. For example:
  - To allow multiple values, use the operator `in`, which validates that the provided values are in the rule’s values.
  - To create a negation condition that considers `value` against the rule, use `nin`, which validates that the provided values aren’t in the rule’s values.
- `value`: One or more values.

![A diagram showcasing the relation between shipping option and shipping option rules.](https://res.cloudinary.com/dza7lstvk/image/upload/v1712331340/Medusa%20Resources/shipping-option-rule_oosopf.jpg)

A shipping option can have multiple rules. For example, you can add rules to a shipping option so that it's available if the customer belongs to the VIP group and the total weight is less than 2000g.

![A diagram showcasing how a shipping option can have multiple rules.](https://res.cloudinary.com/dza7lstvk/image/upload/v1712331462/Medusa%20Resources/shipping-option-rule-2_ylaqdb.jpg)

***

## Shipping Profile and Types

A shipping option belongs to a type. For example, a shipping option’s type may be `express`, while another `standard`. The type is represented by the [ShippingOptionType data model](https://docs.medusajs.com/references/fulfillment/models/ShippingOptionType/index.html.md).

A shipping option also belongs to a shipping profile, as each shipping profile defines the type of items to be shipped in a similar manner.

***

## data Property

When fulfilling an item, you might use a third-party fulfillment provider that requires additional custom data to be passed along from the checkout or order-creation process.

The `ShippingOption` data model has a `data` property. It's an object that stores custom data relevant later when creating and processing a fulfillment.


# Inventory Concepts

In this document, you’ll learn about the main concepts in the Inventory Module, and how data is stored and related.

## InventoryItem

An inventory item, represented by the [InventoryItem data model](https://docs.medusajs.com/references/inventory-next/models/InventoryItem/index.html.md), is a stock-kept item, such as a product, whose inventory can be managed.

The `InventoryItem` data model mainly holds details related to the underlying stock item, but has relations to other data models that include its inventory details.

![A diagram showcasing the relation between data models in the Inventory Module](https://res.cloudinary.com/dza7lstvk/image/upload/v1709658103/Medusa%20Resources/inventory-architecture_kxr2ql.png)

### Inventory Shipping Requirement

An inventory item has a `requires_shipping` field (enabled by default) that indicates whether the item requires shipping. For example, if you're selling a digital license that has limited stock quantity but doesn't require shipping.

When a product variant is purchased in the Medusa application, this field is used to determine whether the item requires shipping. Learn more in [this documentation](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/product/selling-products/index.html.md).

***

## InventoryLevel

An inventory level, represented by the [InventoryLevel data model](https://docs.medusajs.com/references/inventory-next/models/InventoryLevel/index.html.md), holds the inventory and quantity details of an inventory item in a specific location.

It has three quantity-related properties:

- `stocked_quantity`: The available stock quantity of an item in the associated location.
- `reserved_quantity`: The quantity reserved from the available `stocked_quantity`. It indicates the quantity that's still not removed from stock, but considered as unavailable when checking whether an item is in stock.
- `incoming_quantity`: The incoming stock quantity of an item into the associated location. This property doesn't play into the `stocked_quantity` or when checking whether an item is in stock.

### Associated Location

The inventory level's location is determined by the `location_id` property. Medusa links the `InventoryLevel` data model with the `StockLocation` data model from the Stock Location Module.

***

## ReservationItem

A reservation item, represented by the [ReservationItem](https://docs.medusajs.com/references/inventory-next/models/ReservationItem/index.html.md) data model, represents unavailable quantity of an inventory item in a location. It's used when an order is placed but not fulfilled yet.

The reserved quantity is associated with a location, so it has a similar relation to that of the `InventoryLevel` with the Stock Location Module.


# Inventory Kits

In this guide, you'll learn how inventory kits can be used in the Medusa application to support use cases like multi-part products, bundled products, and shared inventory across products.

Refer to the following user guides to learn how to use the Medusa Admin dashboard to:

- [Create Multi-Part Products](https://docs.medusajs.com/user-guide/products/create/multi-part/index.html.md).
- [Create Bundled Products](https://docs.medusajs.com/user-guide/products/create/bundle/index.html.md).

## What is an Inventory Kit?

An inventory kit is a collection of inventory items that are linked to a single product variant. These inventory items can be used to represent different parts of a product, or to represent a bundle of products.

The Medusa application links inventory items from the [Inventory Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/inventory/index.html.md) to product variants in the [Product Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/product/index.html.md). Each variant can have multiple inventory items, and these inventory items can be re-used or shared across variants.

Using inventory kits, you can implement use cases like:

- [Multi-part products](#multi-part-products): A product that consists of multiple parts, each with its own inventory item.
- [Bundled products](#bundled-products): A product that is sold as a bundle, where each variant in the bundle product can re-use the inventory items of another product that should be sold as part of the bundle.

***

## Multi-Part Products

Consider your store sells bicycles that consist of a frame, wheels, and seats, and you want to manage the inventory of these parts separately.

To implement this in Medusa, you can:

- Create inventory items for each of the different parts.
- For each bicycle product, add a variant whose inventory kit consists of the inventory items of each of the parts.

Then, whenever a customer purchases a bicycle, the inventory of each part is updated accordingly. You can also use the `required_quantity` of the variant's inventory items to set how much quantity is consumed of the part's inventory when a bicycle is sold. For example, the bicycle's wheels require 2 wheels inventory items to be sold when a bicycle is sold.

![Diagram showcasing how a variant is linked to multi-part inventory items](https://res.cloudinary.com/dza7lstvk/image/upload/v1736414257/Medusa%20Resources/multi-part-product_kepbnx.jpg)

### Create Multi-Part Product

Using the [Medusa Admin](https://docs.medusajs.com/user-guide/products/create/multi-part/index.html.md), you can create a multi-part product by creating its inventory items first, then assigning these inventory items to the product's variant(s).

Using [workflows](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md), you can implement this by first creating the inventory items:

```ts highlights={multiPartsHighlights1}
import { 
  createInventoryItemsWorkflow, 
  useQueryGraphStep,
} from "@medusajs/medusa/core-flows"
import { createWorkflow } from "@medusajs/framework/workflows-sdk"

export const createMultiPartProductsWorkflow = createWorkflow(
  "create-multi-part-products",
  () => {
    // Alternatively, you can create a stock location
    const { data: stockLocations } = useQueryGraphStep({
      entity: "stock_location",
      fields: ["*"],
      filters: {
        name: "European Warehouse",
      },
    })

    const inventoryItems = createInventoryItemsWorkflow.runAsStep({
      input: {
        items: [
          {
            sku: "FRAME",
            title: "Frame",
            location_levels: [
              {
                stocked_quantity: 100,
                location_id: stockLocations[0].id,
              },
            ],
          },
          {
            sku: "WHEEL",
            title: "Wheel",
            location_levels: [
              {
                stocked_quantity: 100,
                location_id: stockLocations[0].id,
              },
            ],
          },
          {
            sku: "SEAT",
            title: "Seat",
            location_levels: [
              {
                stocked_quantity: 100,
                location_id: stockLocations[0].id,
              },
            ],
          },
        ],
      },
    })

    // TODO create the product
  }
)
```

You start by retrieving the stock location to create the inventory items in. Alternatively, you can [create a stock location](https://docs.medusajs.com/references/medusa-workflows/createStockLocationsWorkflow/index.html.md).

Then, you create the inventory items that the product variant consists of.

Next, create the product and pass the inventory item's IDs to the product's variant:

```ts highlights={multiPartHighlights2}
import { 
  // ...
  transform,
} from "@medusajs/framework/workflows-sdk"
import { 
  // ...
  createProductsWorkflow,
} from "@medusajs/medusa/core-flows"

export const createMultiPartProductsWorkflow = createWorkflow(
  "create-multi-part-products",
  () => {
    // ...

    const inventoryItemIds = transform({
      inventoryItems,
    }, (data) => {
      return data.inventoryItems.map((inventoryItem) => {
        return {
          inventory_item_id: inventoryItem.id,
          // can also specify required_quantity
        }
      })
    })

    const products = createProductsWorkflow.runAsStep({
      input: {
        products: [
          {
            title: "Bicycle",
            variants: [
              {
                title: "Bicycle - Small",
                prices: [
                  {
                    amount: 100,
                    currency_code: "usd",
                  },
                ],
                options: {
                  "Default Option": "Default Variant",
                },
                inventory_items: inventoryItemIds,
              },
            ],
            options: [
              {
                title: "Default Option",
                values: ["Default Variant"],
              },
            ],
            shipping_profile_id: "sp_123",
          },
        ],
      },
    })
  }
)
```

You prepare the inventory item IDs to pass to the variant using [transform](https://docs.medusajs.com/docs/learn/fundamentals/workflows/variable-manipulation/index.html.md) from the Workflows SDK, then pass these IDs to the created product's variant.

You can now [execute the workflow](https://docs.medusajs.com/docs/learn/fundamentals/workflows#3-execute-the-workflow/index.html.md) in [API routes](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/index.html.md), [scheduled jobs](https://docs.medusajs.com/docs/learn/fundamentals/scheduled-jobs/index.html.md), or [subscribers](https://docs.medusajs.com/docs/learn/fundamentals/events-and-subscribers/index.html.md).

***

## Bundled Products

While inventory kits support bundled products, some features like custom pricing for a bundle or separate fulfillment for a bundle's items are not supported. To support those features, follow the [Bundled Products](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/recipes/bundled-products/examples/standard/index.html.md) tutorial to learn how to customize the Medusa application to add bundled products.

Consider you have three products: shirt, pants, and shoes. You sell those products separately, but you also want to offer them as a bundle.

![Diagram showcasing products each having their own variants and inventory](https://res.cloudinary.com/dza7lstvk/image/upload/v1736414787/Medusa%20Resources/bundled-product-1_vmzewk.jpg)

You can do that by creating a product, where each variant re-uses the inventory items of each of the shirt, pants, and shoes products.

Then, when the bundled product's variant is purchased, the inventory quantity of the associated inventory items are updated.

![Diagram showcasing a bundled product using the same inventory as the products part of the bundle](https://res.cloudinary.com/dza7lstvk/image/upload/v1736414780/Medusa%20Resources/bundled-product_x94ca1.jpg)

### Create Bundled Product

You can create a bundled product in the [Medusa Admin](https://docs.medusajs.com/user-guide/products/create/bundle/index.html.md) by creating the products part of the bundle first, each having its own inventory items. Then, you create the bundled product whose variant(s) have inventory kits composed of inventory items from each of the products part of the bundle.

Using [workflows](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md), you can implement this by first creating the products part of the bundle:

```ts highlights={bundledHighlights1}
import { 
  createWorkflow,
} from "@medusajs/framework/workflows-sdk"
import { 
  createProductsWorkflow,
} from "@medusajs/medusa/core-flows"

export const createBundledProducts = createWorkflow(
  "create-bundled-products",
  () => {
    const products = createProductsWorkflow.runAsStep({
      input: {
        products: [
          {
            title: "Shirt",
            shipping_profile_id: "sp_123",
            variants: [
              {
                title: "Shirt",
                prices: [
                  {
                    amount: 10,
                    currency_code: "usd",
                  },
                ],
                options: {
                  "Default Option": "Default Variant",
                },
                manage_inventory: true,
              },
            ],
            options: [
              {
                title: "Default Option",
                values: ["Default Variant"],
              },
            ],
          },
          {
            title: "Pants",
            shipping_profile_id: "sp_123",
            variants: [
              {
                title: "Pants",
                prices: [
                  {
                    amount: 10,
                    currency_code: "usd",
                  },
                ],
                options: {
                  "Default Option": "Default Variant",
                },
                manage_inventory: true,
              },
            ],
            options: [
              {
                title: "Default Option",
                values: ["Default Variant"],
              },
            ],
          },
          {
            title: "Shoes",
            shipping_profile_id: "sp_123",
            variants: [
              {
                title: "Shoes",
                prices: [
                  {
                    amount: 10,
                    currency_code: "usd",
                  },
                ],
                options: {
                  "Default Option": "Default Variant",
                },
                manage_inventory: true,
              },
            ],
            options: [
              {
                title: "Default Option",
                values: ["Default Variant"],
              },
            ],
          },
        ],
      },
    })

    // TODO re-retrieve with inventory
  }
)
```

You create three products and enable `manage_inventory` for their variants, which will create a default inventory item. You can also create the inventory item first for more control over the quantity as explained in [the previous section](#create-multi-part-product).

Next, retrieve the products again but with variant information:

```ts highlights={bundledHighlights2}
import { 
  // ...
  transform,
} from "@medusajs/framework/workflows-sdk"
import { 
  useQueryGraphStep,
} from "@medusajs/medusa/core-flows"

export const createBundledProducts = createWorkflow(
  "create-bundled-products",
  () => {
    // ...
    const productIds = transform({
      products,
    }, (data) => data.products.map((product) => product.id))

    // @ts-ignore
    const { data: productsWithInventory } = useQueryGraphStep({
      entity: "product",
      fields: [
        "variants.*",
        "variants.inventory_items.*",
      ],
      filters: {
        id: productIds,
      },
    })

    const inventoryItemIds = transform({
      productsWithInventory,
    }, (data) => {
      return data.productsWithInventory.map((product) => {
        return {
          inventory_item_id: product.variants[0].inventory_items?.[0]?.inventory_item_id,
        }
      })
    })

    // create bundled product
  }
)
```

Using [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), you retrieve the product again with the inventory items of each variant. Then, you prepare the inventory items to pass to the bundled product's variant.

Finally, create the bundled product:

```ts highlights={bundledProductHighlights3}
export const createBundledProducts = createWorkflow(
  "create-bundled-products",
  () => {
    // ...
    const bundledProduct = createProductsWorkflow.runAsStep({
      input: {
        products: [
          {
            title: "Bundled Clothes",
            shipping_profile_id: "sp_123",
            variants: [
              {
                title: "Bundle",
                prices: [
                  {
                    amount: 30,
                    currency_code: "usd",
                  },
                ],
                options: {
                  "Default Option": "Default Variant",
                },
                inventory_items: inventoryItemIds,
              },
            ],
            options: [
              {
                title: "Default Option",
                values: ["Default Variant"],
              },
            ],
          },
        ],
      },
    }).config({ name: "create-bundled-product" })
  }
)
```

The bundled product has the same inventory items as those of the products part of the bundle.

You can now [execute the workflow](https://docs.medusajs.com/docs/learn/fundamentals/workflows#3-execute-the-workflow/index.html.md) in [API routes](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/index.html.md), [scheduled jobs](https://docs.medusajs.com/docs/learn/fundamentals/scheduled-jobs/index.html.md), or [subscribers](https://docs.medusajs.com/docs/learn/fundamentals/events-and-subscribers/index.html.md).


# Inventory Module in Medusa Flows

This document explains how the Inventory Module is used within the Medusa application's flows.

## Product Variant Creation

When a product variant is created and its `manage_inventory` property's value is `true`, the Medusa application creates an inventory item associated with that product variant.

This flow is implemented within the [createProductVariantsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createProductVariantsWorkflow/index.html.md)

![A diagram showcasing how the Inventory Module is used in the product variant creation form](https://res.cloudinary.com/dza7lstvk/image/upload/v1709661511/Medusa%20Resources/inventory-product-create_khz2hk.jpg)

***

## Add to Cart

When a product variant with `manage_inventory` set to `true` is added to cart, the Medusa application checks whether there's sufficient stocked quantity. If not, an error is thrown and the product variant won't be added to the cart.

This flow is implemented within the [addToCartWorkflow](https://docs.medusajs.com/references/medusa-workflows/addToCartWorkflow/index.html.md)

![A diagram showcasing how the Inventory Module is used in the add to cart flow](https://res.cloudinary.com/dza7lstvk/image/upload/v1709711645/Medusa%20Resources/inventory-cart-flow_achwq9.jpg)

***

## Order Placed

When an order is placed, the Medusa application creates a reservation item for each product variant with `manage_inventory` set to `true`.

This flow is implemented within the [completeCartWorkflow](https://docs.medusajs.com/references/medusa-workflows/completeCartWorkflow/index.html.md)

![A diagram showcasing how the Inventory Module is used in the order placed flow](https://res.cloudinary.com/dza7lstvk/image/upload/v1709712005/Medusa%20Resources/inventory-order-placed_qdxqdn.jpg)

***

## Order Fulfillment

When an item in an order is fulfilled and the associated variant has its `manage_inventory` property set to `true`, the Medusa application:

- Subtracts the `reserved_quantity` from the `stocked_quantity` in the inventory level associated with the variant's inventory item.
- Resets the `reserved_quantity` to `0`.
- Deletes the associated reservation item.

This flow is implemented within the [createOrderFulfillmentWorkflow](https://docs.medusajs.com/references/medusa-workflows/createOrderFulfillmentWorkflow/index.html.md)

![A diagram showcasing how the Inventory Module is used in the order fulfillment flow](https://res.cloudinary.com/dza7lstvk/image/upload/v1709712390/Medusa%20Resources/inventory-order-fulfillment_o9wdxh.jpg)

***

## Order Return

When an item in an order is returned and the associated variant has its `manage_inventory` property set to `true`, the Medusa application increments the `stocked_quantity` of the inventory item's level with the returned quantity.

This flow is implemented within the [confirmReturnReceiveWorkflow](https://docs.medusajs.com/references/medusa-workflows/confirmReturnReceiveWorkflow/index.html.md)

![A diagram showcasing how the Inventory Module is used in the order return flow](https://res.cloudinary.com/dza7lstvk/image/upload/v1709712457/Medusa%20Resources/inventory-order-return_ihftyk.jpg)

### Dismissed Returned Items

If a returned item is considered damaged or is dismissed, its quantity doesn't increment the `stocked_quantity` of the inventory item's level.


# Links between Inventory Module and Other Modules

This document showcases the module links defined between the Inventory Module and other Commerce Modules.

## Summary

The Inventory Module has the following links to other modules:

Read-only links are used to query data across modules, but the relations aren't stored in a pivot table in the database.

|First Data Model|Second Data Model|Type|Description|
|---|---|---|---|
|ProductVariant|InventoryItem|Stored - many-to-many|Learn more|
|InventoryLevel|StockLocation|Read-only - has many|Learn more|

***

## Product Module

Each product variant has different inventory details. Medusa defines a link between the `ProductVariant` and `InventoryItem` data models.

![A diagram showcasing an example of how data models from the Inventory and Product Module are linked.](https://res.cloudinary.com/dza7lstvk/image/upload/v1709658720/Medusa%20Resources/inventory-product_ejnray.jpg)

A product variant whose `manage_inventory` property is enabled has an associated inventory item. Through that inventory's items relations in the Inventory Module, you can manage and check the variant's inventory quantity.

Learn more about product variant's inventory management in [this guide](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/product/variant-inventory/index.html.md).

### Retrieve with Query

To retrieve the product variants of an inventory item with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `variants.*` in `fields`:

### query.graph

```ts
const { data: inventoryItems } = await query.graph({
  entity: "inventory_item",
  fields: [
    "variants.*",
  ],
})

// inventoryItems[0].variants
```

### useQueryGraphStep

```ts
import { useQueryGraphStep } from "@medusajs/medusa/core-flows"

// ...

const { data: inventoryItems } = useQueryGraphStep({
  entity: "inventory_item",
  fields: [
    "variants.*",
  ],
})

// inventoryItems[0].variants
```

### Manage with Link

To manage the variants of an inventory item, use [Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link/index.html.md):

### link.create

```ts
import { Modules } from "@medusajs/framework/utils"

// ...

await link.create({
  [Modules.PRODUCT]: {
    variant_id: "variant_123",
  },
  [Modules.INVENTORY]: {
    inventory_item_id: "iitem_123",
  },
})
```

### createRemoteLinkStep

```ts
import { Modules } from "@medusajs/framework/utils"
import { createRemoteLinkStep } from "@medusajs/medusa/core-flows"

// ...

createRemoteLinkStep({
  [Modules.PRODUCT]: {
    variant_id: "variant_123",
  },
  [Modules.INVENTORY]: {
    inventory_item_id: "iitem_123",
  },
})
```

***

## Stock Location Module

Medusa defines a read-only link between the `InventoryLevel` data model and the [Stock Location Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/stock-location/index.html.md)'s `StockLocation` data model. This means you can retrieve the details of an inventory level's stock locations, but you don't manage the links in a pivot table in the database. The stock location of an inventory level is determined by the `location_id` property of the `InventoryLevel` data model.

### Retrieve with Query

To retrieve the stock locations of an inventory level with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `stock_locations.*` in `fields`:

### query.graph

```ts
const { data: inventoryLevels } = await query.graph({
  entity: "inventory_level",
  fields: [
    "stock_locations.*",
  ],
})

// inventoryLevels[0].stock_locations
```

### useQueryGraphStep

```ts
import { useQueryGraphStep } from "@medusajs/medusa/core-flows"

// ...

const { data: inventoryLevels } = useQueryGraphStep({
  entity: "inventory_level",
  fields: [
    "stock_locations.*",
  ],
})

// inventoryLevels[0].stock_locations
```


# Links between Currency Module and Other Modules

This document showcases the module links defined between the Currency Module and other Commerce Modules.

## Summary

The Currency Module has the following links to other modules:

Read-only links are used to query data across modules, but the relations aren't stored in a pivot table in the database.

|First Data Model|Second Data Model|Type|Description|
|---|---|---|---|
|StoreCurrency|Currency|Read-only - has one|Learn more|

***

## Store Module

The Store Module has a `Currency` data model that stores the supported currencies of a store. However, these currencies don't hold all the details of a currency, such as its name or symbol.

Instead, Medusa defines a read-only link between the [Store Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/store/index.html.md)'s `StoreCurrency` data model and the Currency Module's `Currency` data model. Because the link is read-only from the `Store`'s side, you can only retrieve the details of a store's supported currencies, and not the other way around.

### Retrieve with Query

To retrieve the details of a store's currencies with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `supported_currencies.currency.*` in `fields`:

### query.graph

```ts
const { data: stores } = await query.graph({
  entity: "store",
  fields: [
    "supported_currencies.currency.*",
  ],
})

// stores[0].supported_currencies[0].currency
```

### useQueryGraphStep

```ts
import { useQueryGraphStep } from "@medusajs/medusa/core-flows"

// ...

const { data: stores } = useQueryGraphStep({
  entity: "store",
  fields: [
    "supported_currencies.currency.*",
  ],
})

// stores[0].supported_currencies[0].currency
```


# Cart Concepts

In this document, you’ll get an overview of the main concepts of a cart.

## Shipping and Billing Addresses

A cart has a shipping and billing address. Both of these addresses are represented by the [Address data model](https://docs.medusajs.com/references/cart/models/Address/index.html.md).

![A diagram showcasing the relation between the Cart and Address data models](https://res.cloudinary.com/dza7lstvk/image/upload/v1711532392/Medusa%20Resources/cart-addresses_ls6qmv.jpg)

***

## Line Items

A line item, represented by the [LineItem](https://docs.medusajs.com/references/cart/models/LineItem/index.html.md) data model, is a quantity of a product variant added to the cart. A cart has multiple line items.

A line item stores some of the product variant’s properties, such as the `product_title` and `product_description`. It also stores data related to the item’s quantity and price.

In the Medusa application, a product variant is implemented in the [Product Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/product/index.html.md).

***

## Shipping Methods

A shipping method, represented by the [ShippingMethod data model](https://docs.medusajs.com/references/cart/models/ShippingMethod/index.html.md), is used to fulfill the items in the cart after the order is placed. A cart can have more than one shipping method.

In the Medusa application, the shipping method is created from a shipping option, available through the [Fulfillment Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/fulfillment/index.html.md). Its ID is stored in the `shipping_option_id` property of the method.

### data Property

After an order is placed, you can use a third-party fulfillment provider to fulfill its shipments.

If the fulfillment provider requires additional custom data to be passed along from the checkout process, set this data in the `ShippingMethod`'s `data` property.

The `data` property is an object used to store custom data relevant later for fulfillment.


# Links between Cart Module and Other Modules

This document showcases the module links defined between the Cart Module and other Commerce Modules.

## Summary

The Cart Module has the following links to other modules:

Read-only links are used to query data across modules, but the relations aren't stored in a pivot table in the database.

|First Data Model|Second Data Model|Type|Description|
|---|---|---|---|
|Cart|Customer|Read-only - has one|Learn more|
|Order|Cart|Stored - one-to-one|Learn more|
|Cart|PaymentCollection|Stored - one-to-one|Learn more|
|LineItem|Product|Read-only - has one|Learn more|
|LineItem|ProductVariant|Read-only - has one|Learn more|
|Cart|Promotion|Stored - many-to-many|Learn more|
|Cart|Region|Read-only - has one|Learn more|
|Cart|SalesChannel|Read-only - has one|Learn more|

***

## Customer Module

Medusa defines a read-only link between the `Cart` data model and the [Customer Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/customer/index.html.md)'s `Customer` data model. This means you can retrieve the details of a cart's customer, but you don't manage the links in a pivot table in the database. The customer of a cart is determined by the `customer_id` property of the `Cart` data model.

### Retrieve with Query

To retrieve the customer of a cart with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `customer.*` in `fields`:

### query.graph

```ts
const { data: carts } = await query.graph({
  entity: "cart",
  fields: [
    "customer.*",
  ],
})

// carts[0].customer
```

### useQueryGraphStep

```ts
import { useQueryGraphStep } from "@medusajs/medusa/core-flows"

// ...

const { data: carts } = useQueryGraphStep({
  entity: "cart",
  fields: [
    "customer.*",
  ],
})

// carts[0].customer
```

***

## Order Module

The [Order Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/order/index.html.md) provides order-management features.

Medusa defines a link between the `Cart` and `Order` data models. The cart is linked to the order created once the cart is completed.

![A diagram showcasing an example of how data models from the Cart and Order modules are linked](https://res.cloudinary.com/dza7lstvk/image/upload/v1728375735/Medusa%20Resources/cart-order_ijwmfs.jpg)

### Retrieve with Query

To retrieve the order of a cart with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `order.*` in `fields`:

### query.graph

```ts
const { data: carts } = await query.graph({
  entity: "cart",
  fields: [
    "order.*",
  ],
})

// carts[0].order
```

### useQueryGraphStep

```ts
import { useQueryGraphStep } from "@medusajs/medusa/core-flows"

// ...

const { data: carts } = useQueryGraphStep({
  entity: "cart",
  fields: [
    "order.*",
  ],
})

// carts[0].order
```

### Manage with Link

To manage the order of a cart, use [Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link/index.html.md):

### link.create

```ts
import { Modules } from "@medusajs/framework/utils"

// ...

await link.create({
  [Modules.CART]: {
    cart_id: "cart_123",
  },
  [Modules.ORDER]: {
    order_id: "order_123",
  },
})
```

### createRemoteLinkStep

```ts
import { Modules } from "@medusajs/framework/utils"
import { createRemoteLinkStep } from "@medusajs/medusa/core-flows"

// ...

createRemoteLinkStep({
  [Modules.CART]: {
    cart_id: "cart_123",
  },
  [Modules.ORDER]: {
    order_id: "order_123",
  },
})
```

***

## Payment Module

The [Payment Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/payment/index.html.md) handles payment processing and management.

Medusa defines a link between the `Cart` and `PaymentCollection` data models. A cart has a payment collection which holds all the authorized payment sessions and payments made related to the cart.

![A diagram showcasing an example of how data models from the Cart and Payment modules are linked](https://res.cloudinary.com/dza7lstvk/image/upload/v1711537849/Medusa%20Resources/cart-payment_ixziqm.jpg)

### Retrieve with Query

To retrieve the payment collection of a cart with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `payment_collection.*` in `fields`:

### query.graph

```ts
const { data: carts } = await query.graph({
  entity: "cart",
  fields: [
    "payment_collection.*",
  ],
})

// carts[0].payment_collection
```

### useQueryGraphStep

```ts
import { useQueryGraphStep } from "@medusajs/medusa/core-flows"

// ...

const { data: carts } = useQueryGraphStep({
  entity: "cart",
  fields: [
    "payment_collection.*",
  ],
})

// carts[0].payment_collection
```

### Manage with Link

To manage the payment collection of a cart, use [Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link/index.html.md):

### link.create

```ts
import { Modules } from "@medusajs/framework/utils"

// ...

await link.create({
  [Modules.CART]: {
    cart_id: "cart_123",
  },
  [Modules.PAYMENT]: {
    payment_collection_id: "paycol_123",
  },
})
```

### createRemoteLinkStep

```ts
import { createRemoteLinkStep } from "@medusajs/medusa/core-flows"

// ...

createRemoteLinkStep({
  [Modules.CART]: {
    cart_id: "cart_123",
  },
  [Modules.PAYMENT]: {
    payment_collection_id: "paycol_123",
  },
})
```

***

## Product Module

Medusa defines read-only links between:

- the `LineItem` data model and the [Product Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/product/index.html.md)'s `Product` data model. This means you can retrieve the details of a line item's product, but you don't manage the links in a pivot table in the database. The product of a line item is determined by the `product_id` property of the `LineItem` data model.
- the `LineItem` data model and the [Product Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/product/index.html.md)'s `ProductVariant` data model. This means you can retrieve the details of a line item's variant, but you don't manage the links in a pivot table in the database. The variant of a line item is determined by the `variant_id` property of the `LineItem` data model.

### Retrieve with Query

To retrieve the variant of a line item with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `variant.*` in `fields`:

To retrieve the product, pass `product.*` in `fields`.

### query.graph

```ts
const { data: lineItems } = await query.graph({
  entity: "line_item",
  fields: [
    "variant.*",
  ],
})

// lineItems.variant
```

### useQueryGraphStep

```ts
import { useQueryGraphStep } from "@medusajs/medusa/core-flows"

// ...

const { data: lineItems } = useQueryGraphStep({
  entity: "line_item",
  fields: [
    "variant.*",
  ],
})

// lineItems.variant
```

***

## Promotion Module

The [Promotion Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/promotion/index.html.md) provides discount features.

Medusa defines a link between the `Cart` and `Promotion` data models. This indicates the promotions applied on a cart.

![A diagram showcasing an example of how data models from the Cart and Promotion modules are linked](https://res.cloudinary.com/dza7lstvk/image/upload/v1711538015/Medusa%20Resources/cart-promotion_kuh9vm.jpg)

Medusa also defines a read-only link between the `LineItemAdjustment` and `Promotion` data models. This means you can retrieve the details of the promotion applied on a line item, but you don't manage the links in a pivot table in the database. The promotion of a line item is determined by the `promotion_id` property of the `LineItemAdjustment` data model.

### Retrieve with Query

To retrieve the promotions of a cart with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `promotions.*` in `fields`:

To retrieve the promotion of a line item adjustment, pass `promotion.*` in `fields`.

### query.graph

```ts
const { data: carts } = await query.graph({
  entity: "cart",
  fields: [
    "promotions.*",
  ],
})

// carts[0].promotions
```

### useQueryGraphStep

```ts
import { useQueryGraphStep } from "@medusajs/medusa/core-flows"

// ...

const { data: carts } = useQueryGraphStep({
  entity: "cart",
  fields: [
    "promotions.*",
  ],
})

// carts[0].promotions
```

### Manage with Link

To manage the promotions of a cart, use [Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link/index.html.md):

### link.create

```ts
import { Modules } from "@medusajs/framework/utils"

// ...

await link.create({
  [Modules.CART]: {
    cart_id: "cart_123",
  },
  [Modules.PROMOTION]: {
    promotion_id: "promo_123",
  },
})
```

### createRemoteLinkStep

```ts
import { Modules } from "@medusajs/framework/utils"
import { createRemoteLinkStep } from "@medusajs/medusa/core-flows"

// ...

createRemoteLinkStep({
  [Modules.CART]: {
    cart_id: "cart_123",
  },
  [Modules.PROMOTION]: {
    promotion_id: "promo_123",
  },
})
```

***

## Region Module

Medusa defines a read-only link between the `Cart` data model and the [Region Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/region/index.html.md)'s `Region` data model. This means you can retrieve the details of a cart's region, but you don't manage the links in a pivot table in the database. The region of a cart is determined by the `region_id` property of the `Cart` data model.

### Retrieve with Query

To retrieve the region of a cart with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `region.*` in `fields`:

### query.graph

```ts
const { data: carts } = await query.graph({
  entity: "cart",
  fields: [
    "region.*",
  ],
})

// carts[0].region
```

### useQueryGraphStep

```ts
import { useQueryGraphStep } from "@medusajs/medusa/core-flows"

// ...

const { data: carts } = useQueryGraphStep({
  entity: "cart",
  fields: [
    "region.*",
  ],
})

// carts[0].region
```

***

## Sales Channel Module

Medusa defines a read-only link between the `Cart` data model and the [Sales Channel Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/sales-channel/index.html.md)'s `SalesChannel` data model. This means you can retrieve the details of a cart's sales channel, but you don't manage the links in a pivot table in the database. The sales channel of a cart is determined by the `sales_channel_id` property of the `Cart` data model.

### Retrieve with Query

To retrieve the sales channel of a cart with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `sales_channel.*` in `fields`:

### query.graph

```ts
const { data: carts } = await query.graph({
  entity: "cart",
  fields: [
    "sales_channel.*",
  ],
})

// carts[0].sales_channel
```

### useQueryGraphStep

```ts
import { useQueryGraphStep } from "@medusajs/medusa/core-flows"

// ...

const { data: carts } = useQueryGraphStep({
  entity: "cart",
  fields: [
    "sales_channel.*",
  ],
})

// carts[0].sales_channel
```


# Promotions Adjustments in Carts

In this document, you’ll learn how a promotion is applied to a cart’s line items and shipping methods using adjustment lines.

## What are Adjustment Lines?

An adjustment line indicates a change to an item or a shipping method’s amount. It’s used to apply promotions or discounts on a cart.

The [LineItemAdjustment](https://docs.medusajs.com/references/cart/models/LineItemAdjustment/index.html.md) data model represents changes on a line item, and the [ShippingMethodAdjustment](https://docs.medusajs.com/references/cart/models/ShippingMethodAdjustment/index.html.md) data model represents changes on a shipping method.

![A diagram showcasing the relations between other data models and adjustment line models](https://res.cloudinary.com/dza7lstvk/image/upload/v1711534248/Medusa%20Resources/cart-adjustments_k4sttb.jpg)

The `amount` property of the adjustment line indicates the amount to be discounted from the original amount. Also, the ID of the applied promotion is stored in the `promotion_id` property of the adjustment line.

***

## Discountable Option

The [LineItem](https://docs.medusajs.com/references/cart/models/LineItem/index.html.md) data model has an `is_discountable` property that indicates whether promotions can be applied to the line item. It’s enabled by default.

When disabled, a promotion can’t be applied to a line item. In the context of the Promotion Module, the promotion isn’t applied to the line item even if it matches its rules.

***

## Promotion Actions

When using the Cart and Promotion modules together, such as in the Medusa application, use the [computeActions method of the Promotion Module’s main service](https://docs.medusajs.com/references/promotion/computeActions/index.html.md). It retrieves the actions of line items and shipping methods.

Learn more about actions in the [Promotion Module’s documentation](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/promotion/actions/index.html.md).

For example:

```ts collapsibleLines="1-8" expandButtonLabel="Show Imports"
import {
  ComputeActionAdjustmentLine,
  ComputeActionItemLine,
  ComputeActionShippingLine,
  // ...
} from "@medusajs/framework/types"

// retrieve the cart
const cart = await cartModuleService.retrieveCart("cart_123", {
  relations: [
    "items.adjustments",
    "shipping_methods.adjustments",
  ],
})

// retrieve line item adjustments
const lineItemAdjustments: ComputeActionItemLine[] = []
cart.items.forEach((item) => {
  const filteredAdjustments = item.adjustments?.filter(
    (adjustment) => adjustment.code !== undefined
  ) as unknown as ComputeActionAdjustmentLine[]
  if (filteredAdjustments.length) {
    lineItemAdjustments.push({
      ...item,
      adjustments: filteredAdjustments,
    })
  }
})

// retrieve shipping method adjustments
const shippingMethodAdjustments: ComputeActionShippingLine[] =
  []
cart.shipping_methods.forEach((shippingMethod) => {
  const filteredAdjustments =
    shippingMethod.adjustments?.filter(
      (adjustment) => adjustment.code !== undefined
    ) as unknown as ComputeActionAdjustmentLine[]
  if (filteredAdjustments.length) {
    shippingMethodAdjustments.push({
      ...shippingMethod,
      adjustments: filteredAdjustments,
    })
  }
})

// compute actions
const actions = await promotionModuleService.computeActions(
  ["promo_123"],
  {
    items: lineItemAdjustments,
    shipping_methods: shippingMethodAdjustments,
  }
)
```

The `computeActions` method accepts the existing adjustments of line items and shipping methods to compute the actions accurately.

Then, use the returned `addItemAdjustment` and `addShippingMethodAdjustment` actions to set the cart’s line item and the shipping method’s adjustments.

```ts collapsibleLines="1-8" expandButtonLabel="Show Imports"
import {
  AddItemAdjustmentAction,
  AddShippingMethodAdjustment,
  // ...
} from "@medusajs/framework/types"

// ...

await cartModuleService.setLineItemAdjustments(
  cart.id,
  actions.filter(
    (action) => action.action === "addItemAdjustment"
  ) as AddItemAdjustmentAction[]
)

await cartModuleService.setShippingMethodAdjustments(
  cart.id,
  actions.filter(
    (action) =>
      action.action === "addShippingMethodAdjustment"
  ) as AddShippingMethodAdjustment[]
)
```


# Tax Lines in Cart Module

In this document, you’ll learn about tax lines in a cart and how to retrieve tax lines with the Tax Module.

## What are Tax Lines?

A tax line indicates the tax rate of a line item or a shipping method. The [LineItemTaxLine data model](https://docs.medusajs.com/references/cart/models/LineItemTaxLine/index.html.md) represents a line item’s tax line, and the [ShippingMethodTaxLine data model](https://docs.medusajs.com/references/cart/models/ShippingMethodTaxLine/index.html.md) represents a shipping method’s tax line.

![A diagram showcasing the relation between other data models and the tax line models](https://res.cloudinary.com/dza7lstvk/image/upload/v1711534431/Medusa%20Resources/cart-tax-lines_oheaq6.jpg)

***

## Tax Inclusivity

By default, the tax amount is calculated by taking the tax rate from the line item or shipping method’s amount, and then adding them to the item/method’s subtotal.

However, line items and shipping methods have an `is_tax_inclusive` property that, when enabled, indicates that the item or method’s price already includes taxes.

So, instead of calculating the tax rate and adding it to the item/method’s subtotal, it’s calculated as part of the subtotal.

The following diagram is a simplified showcase of how a subtotal is calculated from the taxes perspective.

![A diagram showing an example of calculating the subtotal of a line item using its taxes](https://res.cloudinary.com/dza7lstvk/image/upload/v1711535295/Medusa%20Resources/cart-tax-inclusive_shpr3t.jpg)

For example, if a line item's amount is `5000`, the tax rate is `10`, and tax inclusivity is enabled, the tax amount is 10% of `5000`, which is `500`, making the unit price of the line item `4500`.

***

## Retrieve Tax Lines

When using the Cart and Tax modules together, you can use the `getTaxLines` method of the Tax Module’s main service. It retrieves the tax lines for a cart’s line items and shipping methods.

```ts
// retrieve the cart
const cart = await cartModuleService.retrieveCart("cart_123", {
  relations: [
    "items.tax_lines",
    "shipping_methods.tax_lines",
    "shipping_address",
  ],
})

// retrieve the tax lines
const taxLines = await taxModuleService.getTaxLines(
  [
    ...(cart.items as TaxableItemDTO[]),
    ...(cart.shipping_methods as TaxableShippingDTO[]),
  ],
  {
    address: {
      ...cart.shipping_address,
      country_code:
        cart.shipping_address.country_code || "us",
    },
  }
)
```

Then, use the returned tax lines to set the line items and shipping methods’ tax lines:

```ts
// set line item tax lines
await cartModuleService.setLineItemTaxLines(
  cart.id,
  taxLines.filter((line) => "line_item_id" in line)
)

// set shipping method tax lines
await cartModuleService.setLineItemTaxLines(
  cart.id,
  taxLines.filter((line) => "shipping_line_id" in line)
)
```


# Order Concepts

In this document, you’ll learn about orders and related concepts

## Order Items

The items purchased in the order are represented by the [OrderItem data model](https://docs.medusajs.com/references/order/models/OrderItem/index.html.md). An order can have multiple items.

![A diagram showcasing the relation between an order and its items.](https://res.cloudinary.com/dza7lstvk/image/upload/v1712304722/Medusa%20Resources/order-order-items_uvckxd.jpg)

### Item’s Product Details

The details of the purchased products are represented by the [LineItem data model](https://docs.medusajs.com/references/order/models/OrderLineItem/index.html.md). Not only does a line item hold the details of the product, but also details related to its price, adjustments due to promotions, and taxes.

***

## Order’s Shipping Method

An order has one or more shipping methods used to handle item shipment.

Each shipping method is represented by the [OrderShippingMethod data model](https://docs.medusajs.com/references/order/models/OrderShippingMethod/index.html.md) that holds its details. The shipping method is linked to the order through the [OrderShipping data model](https://docs.medusajs.com/references/order/models/OrderShipping/index.html.md).

![A diagram showcasing the relation between an order and its items.](https://res.cloudinary.com/dza7lstvk/image/upload/v1719570409/Medusa%20Resources/order-shipping-method_tkggvd.jpg)

### data Property

When fulfilling the order, you can use a third-party fulfillment provider that requires additional custom data to be passed along from the order creation process.

The `OrderShippingMethod` data model has a `data` property. It’s an object used to store custom data relevant later for fulfillment.

The Medusa application passes the `data` property to the Fulfillment Module when fulfilling items.

***

## Order Totals

The order’s total amounts (including tax total, total after an item is returned, etc…) are represented by the [OrderSummary data model](https://docs.medusajs.com/references/order/models/OrderSummary/index.html.md).

***

## Order Payments

Payments made on an order, whether they’re capture or refund payments, are recorded as transactions represented by the [OrderTransaction data model](https://docs.medusajs.com/references/order/models/OrderTransaction/index.html.md).

An order can have multiple transactions. The sum of these transactions must be equal to the order summary’s total. Otherwise, there’s an outstanding amount.

Learn more about transactions in [this guide](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/order/transactions/index.html.md).


# Order Edit

In this document, you'll learn about order edits.

Refer to this [Medusa Admin User Guide](https://docs.medusajs.com/user-guide/orders/edit/index.html.md) to learn how to edit an order's items using the dashboard.

## What is an Order Edit?

A merchant can edit an order to add new items or change the quantity of existing items in the order.

An order edit is represented by the [OrderChange data model](https://docs.medusajs.com/references/order/models/OrderChange/index.html.md).

The `OrderChange` data model is associated with any type of change, including a return or exchange. However, its `change_type` property distinguishes the type of change it's making.

In the case of an order edit, the `OrderChange`'s type is `edit`.

***

## Add Items in an Order Edit

When the merchant adds new items to the order in the order edit, the item is added as an [OrderItem](https://docs.medusajs.com/references/order/models/OrderItem/index.html.md).

Also, an `OrderChangeAction` is created. The [OrderChangeAction data model](https://docs.medusajs.com/references/order/models/OrderChangeAction/index.html.md) represents a change made by an `OrderChange`, such as an item added.

So, when an item is added, an `OrderChangeAction` is created with the type `ITEM_ADD`. In its `details` property, the item's ID, price, and quantity are stored.

***

## Update Items in an Order Edit

A merchant can update an existing item's quantity or price.

This change is added as an `OrderChangeAction` with the type `ITEM_UPDATE`. In its `details` property, the item's ID, new price, and new quantity are stored.

***

## Shipping Methods of New Items in the Edit

Adding new items to the order requires adding shipping methods for those items.

These shipping methods are represented by the [OrderShippingMethod data model](https://docs.medusajs.com/references/order/models/OrderItem/index.html.md). Also, an `OrderChangeAction` is created with the type `SHIPPING_ADD`

***

## How Order Edits Impact an Order’s Version

When an order edit is confirmed, the order’s version is incremented.

***

## Payments and Refunds for Order Edit Changes

Once the Order Edit is confirmed, any additional payment or refund required can be made on the original order.

This is determined by the comparison between the `OrderSummary` and the order's transactions, as mentioned in [this guide](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/order/transactions#checking-outstanding-amount/index.html.md).


# Authentication Flows with the Auth Main Service

In this document, you'll learn how to use the Auth Module's main service's methods to implement authentication flows and reset a user's password.

## Authentication Methods

### Register

The [register method of the Auth Module's main service](https://docs.medusajs.com/references/auth/register/index.html.md) creates an auth identity that can be authenticated later.

For example:

```ts
const data = await authModuleService.register(
  "emailpass",
  // passed to auth provider
  {
    // ...
  }
)
```

This method calls the `register` method of the provider specified in the first parameter and returns its data.

### Authenticate

To authenticate a user, you use the [authenticate method of the Auth Module's main service](https://docs.medusajs.com/references/auth/authenticate/index.html.md). For example:

```ts
const data = await authModuleService.authenticate(
  "emailpass",
  // passed to auth provider
  {
    // ...
  }
)
```

This method calls the `authenticate` method of the provider specified in the first parameter and returns its data.

***

## Auth Flow 1: Basic Authentication

The basic authentication flow requires first using the `register` method, then the `authenticate` method:

```ts
const { success, authIdentity, error } = await authModuleService.register(
  "emailpass",
  // passed to auth provider
  {
    // ...
  }
)

if (error) {
  // registration failed
  // TODO return an error
  return
}

// later (can be another route for log-in)
const { success, authIdentity, location } = await authModuleService.authenticate(
  "emailpass",
  // passed to auth provider
  {
    // ...
  }
)

if (success && !location) {
  // user is authenticated
}
```

If `success` is true and `location` isn't set, the user is authenticated successfully, and their authentication details are available within the `authIdentity` object.

The next section explains the flow if `location` is set.

Check out the [AuthIdentity](https://docs.medusajs.com/references/auth/models/AuthIdentity/index.html.md) reference for the received properties in `authIdentity`.

![Diagram showcasing the basic authentication flow](https://res.cloudinary.com/dza7lstvk/image/upload/v1711373749/Medusa%20Resources/basic-auth_lgpqsj.jpg)

### Auth Identity with Same Identifier

If an auth identity, such as a `customer`, tries to register with an email of another auth identity, the `register` method returns an error. This can happen either if another customer is using the same email, or an admin user has the same email.

There are two ways to handle this:

- Consider the customer authenticated if the `authenticate` method validates that the email and password are correct. This allows admin users, for example, to authenticate as customers.
- Return an error message to the customer, informing them that the email is already in use.

***

## Auth Flow 2: Third-Party Service Authentication

The third-party service authentication method requires using the `authenticate` method first:

```ts
const { success, authIdentity, location } = await authModuleService.authenticate(
  "google",
  // passed to auth provider
  {
    // ...
  }
)

if (location) {
  // return the location for the front-end to redirect to
}

if (!success) {
  // authentication failed
}

// authentication successful
```

If the `authenticate` method returns a `location` property, the authentication process requires the user to perform an action with a third-party service. So, you return the `location` to the front-end or client to redirect to that URL.

For example, when using the `google` provider, the `location` is the URL that the user is navigated to login.

![Diagram showcasing the first part of the third-party authentication flow](https://res.cloudinary.com/dza7lstvk/image/upload/v1711374847/Medusa%20Resources/third-party-auth-1_enyedy.jpg)

### Overriding Callback URL

The Google and GitHub providers allow you to override their `callbackUrl` option during authentication. This is useful when you redirect the user after authentication to a URL based on its actor type. For example, you redirect admin users and customers to different pages.

```ts
const { success, authIdentity, location } = await authModuleService.authenticate(
  "google",
  // passed to auth provider
  {
    // ...
    callback_url: "example.com",
  }
)
```

### validateCallback

Providers handling this authentication flow must implement the `validateCallback` method. It implements the logic to validate the authentication with the third-party service.

So, once the user performs the required action with the third-party service (for example, log-in with Google), the frontend must redirect to an API route that uses the [validateCallback method of the Auth Module's main service](https://docs.medusajs.com/references/auth/validateCallback/index.html.md).

The method calls the specified provider’s `validateCallback` method passing it the authentication details it received in the second parameter:

```ts
const { success, authIdentity } = await authModuleService.validateCallback(
  "google",
  // passed to auth provider
  {
    // request data, such as
    url,
    headers,
    query,
    body,
    protocol,
  }
)

if (success) {
  // authentication succeeded
}
```

For providers like Google, the `query` object contains the query parameters from the original callback URL, such as the `code` and `state` parameters.

If the returned `success` property is `true`, the authentication with the third-party provider was successful.

![Diagram showcasing the second part of the third-party authentication flow](https://res.cloudinary.com/dza7lstvk/image/upload/v1711375123/Medusa%20Resources/third-party-auth-2_kmjxju.jpg)

***

## Reset Password

To update a user's password or other authentication details, use the `updateProvider` method of the Auth Module's main service. It calls the `update` method of the specified authentication provider.

For example:

```ts
const { success } = await authModuleService.updateProvider(
  "emailpass",
  // passed to the auth provider
  {
    entity_id: "user@example.com",
    password: "supersecret",
  }
)

if (success) {
  // password reset successfully
}
```

The method accepts as a first parameter the ID of the provider, and as a second parameter the data necessary to reset the password.

In the example above, you use the `emailpass` provider, so you have to pass an object having an `email` and `password` properties.

If the returned `success` property is `true`, the password has reset successfully.


# How to Use Authentication Routes

In this document, you'll learn about the authentication routes and how to use them to create and log-in users, and reset their password.

These routes are added by Medusa's HTTP layer, not the Auth Module.

## Types of Authentication Flows

### 1. Basic Authentication Flow

This authentication flow doesn't require validation with third-party services.

[How to register customer in storefront using basic authentication flow](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/storefront-development/customers/register/index.html.md).

The steps are:

![Diagram showcasing the basic authentication flow between the frontend and the Medusa application](https://res.cloudinary.com/dza7lstvk/image/upload/v1725539370/Medusa%20Resources/basic-auth-routes_pgpjch.jpg)

1. Register the user with the [Register Route](#register-route).
2. Use the authentication token to create the user with their respective API route.
   - For example, for customers you would use the [Create Customer API route](https://docs.medusajs.com/api/store#customers_postcustomers).
   - For admin users, you accept an invite using the [Accept Invite API route](https://docs.medusajs.com/api/admin#invites_postinvitesaccept)
3. Authenticate the user with the [Auth Route](#login-route).

After registration, you only use the [Auth Route](#login-route) for subsequent authentication.

To handle errors related to existing identities, refer to [this section](#handling-existing-identities).

### 2. Third-Party Service Authenticate Flow

This authentication flow authenticates the user with a third-party service, such as Google.

[How to authenticate customer with a third-party provider in the storefront.](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/storefront-development/customers/third-party-login/index.html.md).

It requires the following steps:

![Diagram showcasing the authentication flow between the frontend, Medusa application, and third-party service](https://res.cloudinary.com/dza7lstvk/image/upload/v1725528159/Medusa%20Resources/Third_Party_Auth_tvf4ng.jpg)

1. Authenticate the user with the [Auth Route](#login-route).
2. The auth route returns a URL to authenticate with third-party service, such as login with Google. The frontend (such as a storefront), when it receives a `location` property in the response, must redirect to the returned location.
3. Once the authentication with the third-party service finishes, it redirects back to the frontend with a `code` query parameter. So, make sure your third-party service is configured to redirect to your frontend page after successful authentication.
4. The frontend sends a request to the [Validate Callback Route](#validate-callback-route) passing it the query parameters received from the third-party service, such as the `code` and `state` query parameters.
5. If the callback validation is successful, the frontend receives the authentication token.
6. Decode the received token in the frontend using tools like [react-jwt](https://www.npmjs.com/package/react-jwt).
   - If the decoded data has an `actor_id` property, then the user is already registered. So, use this token for subsequent authenticated requests.
   - If not, follow the rest of the steps.
7. The frontend uses the authentication token to create the user with their respective API route.
   - For example, for customers you would use the [Create Customer API route](https://docs.medusajs.com/api/store#customers_postcustomers).
   - For admin users, you accept an invite using the [Accept Invite API route](https://docs.medusajs.com/api/admin#invites_postinvitesaccept)
8. The frontend sends a request to the [Refresh Token Route](#refresh-token-route) to retrieve a new token with the user information populated.

***

## Register Route

The Medusa application defines an API route at `/auth/{actor_type}/{provider}/register` that creates an auth identity for an actor type, such as a `customer`. It returns a JWT token that you pass to an API route that creates the user.

```bash
curl -X POST http://localhost:9000/auth/{actor_type}/{providers}/register
-H 'Content-Type: application/json' \
--data-raw '{
  "email": "Whitney_Schultz@gmail.com"
  // ...
}'
```

This API route is useful for providers like `emailpass` that uses custom logic to authenticate a user. For authentication providers that authenticate with third-party services, such as Google, use the [Auth Route](#login-route) instead.

For example, if you're registering a customer, you:

1. Send a request to `/auth/customer/emailpass/register` to retrieve the registration JWT token.
2. Send a request to the [Create Customer API route](https://docs.medusajs.com/api/store#customers_postcustomers) to create the customer, passing the [JWT token in the header](https://docs.medusajs.com/api/store#authentication).

### Path Parameters

Its path parameters are:

- `{actor_type}`: the actor type of the user you're authenticating. For example, `customer`.
- `{provider}`: the auth provider to handle the authentication. For example, `emailpass`.

### Request Body Parameters

This route accepts in the request body the data that the specified authentication provider requires to handle authentication.

For example, the EmailPass provider requires an `email` and `password` fields in the request body.

### Response Fields

If the authentication is successful, you'll receive a `token` field in the response body object:

```json
{
  "token": "..."
}
```

Use that token in the header of subsequent requests to send authenticated requests.

### Handling Existing Identities

An auth identity with the same email may already exist in Medusa. This can happen if:

- Another actor type is using that email. For example, an admin user is trying to register as a customer.
- The same email belongs to a record of the same actor type. For example, another customer has the same email.

In these scenarios, the Register Route will return an error instead of a token:

```json
{
  "type": "unauthorized",
  "message": "Identity with email already exists"
}
```

To handle these scenarios, you can use the [Login Route](#login-route) to validate that the email and password match the existing identity. If so, you can allow the admin user, for example, to register as a customer.

Otherwise, if the email and password don't match the existing identity, such as when the email belongs to another customer, the [Login Route](#login-route) returns an error:

```json
{
  "type": "unauthorized",
  "message": "Invalid email or password"
}
```

You can show that error message to the customer.

***

## Login Route

The Medusa application defines an API route at `/auth/{actor_type}/{provider}` that authenticates a user of an actor type. It returns a JWT token that can be passed in [the header of subsequent requests](https://docs.medusajs.com/api/store#authentication) to send authenticated requests.

```bash
curl -X POST http://localhost:9000/auth/{actor_type}/{providers}
-H 'Content-Type: application/json' \
--data-raw '{
  "email": "Whitney_Schultz@gmail.com"
  // ...
}'
```

For example, if you're authenticating a customer, you send a request to `/auth/customer/emailpass`.

### Path Parameters

Its path parameters are:

- `{actor_type}`: the actor type of the user you're authenticating. For example, `customer`.
- `{provider}`: the auth provider to handle the authentication. For example, `emailpass`.

### Request Body Parameters

This route accepts in the request body the data that the specified authentication provider requires to handle authentication.

For example, the EmailPass provider requires an `email` and `password` fields in the request body.

#### Overriding Callback URL

For the [GitHub](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/auth/auth-providers/github/index.html.md) and [Google](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/auth/auth-providers/google/index.html.md) providers, you can pass a `callback_url` body parameter that overrides the `callbackUrl` set in the provider's configurations.

This is useful if you want to redirect the user to a different URL after authentication based on their actor type. For example, you can set different `callback_url` for admin users and customers.

### Response Fields

If the authentication is successful, you'll receive a `token` field in the response body object:

```json
{
  "token": "..."
}
```

Use that token in the header of subsequent requests to send authenticated requests.

If the authentication requires more action with a third-party service, you'll receive a `location` property:

```json
{
  "location": "https://..."
}
```

Redirect to that URL in the frontend to continue the authentication process with the third-party service.

[How to login Customers using the authentication route](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/storefront-development/customers/login/index.html.md).

***

## Validate Callback Route

The Medusa application defines an API route at `/auth/{actor_type}/{provider}/callback` that's useful for validating the authentication callback or redirect from third-party services like Google.

```bash
curl -X POST http://localhost:9000/auth/{actor_type}/{providers}/callback?code=123&state=456
```

Refer to the [third-party authentication flow](#2-third-party-service-authenticate-flow) section to see how this route fits into the authentication flow.

### Path Parameters

Its path parameters are:

- `{actor_type}`: the actor type of the user you're authenticating. For example, `customer`.
- `{provider}`: the auth provider to handle the authentication. For example, `google`.

### Query Parameters

This route accepts all the query parameters that the third-party service sends to the frontend after the user completes the authentication process, such as the `code` and `state` query parameters.

### Response Fields

If the authentication is successful, you'll receive a `token` field in the response body object:

```json
{
  "token": "..."
}
```

In your frontend, decode the token using tools like [react-jwt](https://www.npmjs.com/package/react-jwt):

- If the decoded data has an `actor_id` property, the user is already registered. So, use this token for subsequent authenticated requests.
- If not, use the token in the header of a request that creates the user, such as the [Create Customer API route](https://docs.medusajs.com/api/store#customers_postcustomers).

***

## Refresh Token Route

The Medusa application defines an API route at `/auth/token/refresh` that's useful after authenticating a user with a third-party service to populate the user's token with their new information.

It requires the user's JWT token that they received from the authentication or callback routes.

```bash
curl -X POST http://localhost:9000/auth/token/refresh \
-H 'Authorization: Bearer {token}'
```

### Response Fields

If the token was refreshed successfully, you'll receive a `token` field in the response body object:

```json
{
  "token": "..."
}
```

Use that token in the header of subsequent requests to send authenticated requests.

***

## Reset Password Routes

To reset a user's password:

1. Generate a token using the [Generate Reset Password Token API route](#generate-reset-password-token-route).
   - The API route emits the `auth.password_reset` event, passing the token in the payload.
   - You can create a subscriber, as seen in [this guide](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/auth/reset-password/index.html.md), that listens to the event and send a notification to the user.
2. Pass the token to the [Reset Password API route](#reset-password-route) to reset the password.
   - The URL in the user's notification should direct them to a frontend URL, which sends a request to this route.

[Storefront Development: How to Reset a Customer's Password.](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/storefront-development/customers/reset-password/index.html.md)

### Generate Reset Password Token Route

The Medusa application defines an API route at `/auth/{actor_type}/{auth_provider}/reset-password` that emits the `auth.password_reset` event, passing the token in the payload.

```bash
curl -X POST http://localhost:9000/auth/{actor_type}/{providers}/reset-password
-H 'Content-Type: application/json' \
--data-raw '{
  "identifier": "Whitney_Schultz@gmail.com"
}'
```

This API route is useful for providers like `emailpass` that store a user's password and use it for authentication.

#### Path Parameters

Its path parameters are:

- `{actor_type}`: the actor type of the user you're authenticating. For example, `customer`.
- `{provider}`: the auth provider to handle the authentication. For example, `emailpass`.

#### Request Body Parameters

This route accepts in the request body an object having the following property:

- `identifier`: The user's identifier in the specified auth provider. For example, for the `emailpass` auth provider, you pass the user's email.

#### Response Fields

If the authentication is successful, the request returns a `201` response code.

### Reset Password Route

The Medusa application defines an API route at `/auth/{actor_type}/{auth_provider}/update` that accepts a token and, if valid, updates the user's password.

```bash
curl -X POST http://localhost:9000/auth/{actor_type}/{providers}/update
-H 'Content-Type: application/json' \
-H 'Authorization: Bearer {token}' \
--data-raw '{
  "email": "Whitney_Schultz@gmail.com",
  "password": "supersecret"
}'
```

This API route is useful for providers like `emailpass` that store a user's password and use it for logging them in.

#### Path Parameters

Its path parameters are:

- `{actor_type}`: the actor type of the user you're authenticating. For example, `customer`.
- `{provider}`: the auth provider to handle the authentication. For example, `emailpass`.

#### Pass Token in Authorization Header

Before [Medusa v2.6](https://github.com/medusajs/medusa/releases/tag/v2.6), you passed the token as a query parameter. Now, you must pass it in the `Authorization` header.

In the request's authorization header, you must pass the token generated using the [Generate Reset Password Token route](#generate-reset-password-token-route). You pass it as a bearer token.

### Request Body Parameters

This route accepts in the request body an object that has the data necessary for the provider to update the user's password.

For the `emailpass` provider, you must pass the following properties:

- `email`: The user's email.
- `password`: The new password.

### Response Fields

If the authentication is successful, the request returns an object with a `success` property set to `true`:

```json
{
  "success": "true"
}
```


# Links between Order Module and Other Modules

This document showcases the module links defined between the Order Module and other Commerce Modules.

## Summary

The Order Module has the following links to other modules:

Read-only links are used to query data across modules, but the relations aren't stored in a pivot table in the database.

|First Data Model|Second Data Model|Type|Description|
|---|---|---|---|
|Order|Customer|Read-only - has one|Learn more|
|Order|Cart|Stored - one-to-one|Learn more|
|Order|Fulfillment|Stored - one-to-many|Learn more|
|Return|Fulfillment|Stored - one-to-many|Learn more|
|Order|PaymentCollection|Stored - one-to-many|Learn more|
|OrderClaim|PaymentCollection|Stored - one-to-many|Learn more|
|OrderExchange|PaymentCollection|Stored - one-to-many|Learn more|
|OrderLineItem|Product|Read-only - has many|Learn more|
|Order|Promotion|Stored - many-to-many|Learn more|
|Order|Region|Read-only - has one|Learn more|
|Order|SalesChannel|Read-only - has one|Learn more|

***

## Customer Module

Medusa defines a read-only link between the `Order` data model and the [Customer Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/customer/index.html.md)'s `Customer` data model. This means you can retrieve the details of an order's customer, but you don't manage the links in a pivot table in the database. The customer of an order is determined by the `customer_id` property of the `Order` data model.

### Retrieve with Query

To retrieve the customer of an order with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `customer.*` in `fields`:

### query.graph

```ts
const { data: orders } = await query.graph({
  entity: "order",
  fields: [
    "customer.*",
  ],
})

// orders[0].customer
```

### useQueryGraphStep

```ts
import { useQueryGraphStep } from "@medusajs/medusa/core-flows"

// ...

const { data: orders } = useQueryGraphStep({
  entity: "order",
  fields: [
    "customer.*",
  ],
})

// orders[0].customer
```

***

## Cart Module

The [Cart Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/cart/index.html.md) provides cart-management features.

Medusa defines a link between the `Order` and `Cart` data models. The order is linked to the cart used for the purchased.

![A diagram showcasing an example of how data models from the Cart and Order modules are linked](https://res.cloudinary.com/dza7lstvk/image/upload/v1728375735/Medusa%20Resources/cart-order_ijwmfs.jpg)

### Retrieve with Query

To retrieve the cart of an order with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `cart.*` in `fields`:

### query.graph

```ts
const { data: orders } = await query.graph({
  entity: "order",
  fields: [
    "cart.*",
  ],
})

// orders[0].cart
```

### useQueryGraphStep

```ts
import { useQueryGraphStep } from "@medusajs/medusa/core-flows"

// ...

const { data: orders } = useQueryGraphStep({
  entity: "order",
  fields: [
    "cart.*",
  ],
})

// orders[0].cart
```

### Manage with Link

To manage the cart of an order, use [Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link/index.html.md):

### link.create

```ts
import { Modules } from "@medusajs/framework/utils"

// ...

await link.create({
  [Modules.ORDER]: {
    order_id: "order_123",
  },
  [Modules.CART]: {
    cart_id: "cart_123",
  },
})
```

### createRemoteLinkStep

```ts
import { Modules } from "@medusajs/framework/utils"
import { createRemoteLinkStep } from "@medusajs/medusa/core-flows"

// ...

createRemoteLinkStep({
  [Modules.ORDER]: {
    order_id: "order_123",
  },
  [Modules.CART]: {
    cart_id: "cart_123",
  },
})
```

***

## Fulfillment Module

A fulfillment is created for an orders' items. Medusa defines a link between the `Fulfillment` and `Order` data models.

![A diagram showcasing an example of how data models from the Fulfillment and Order modules are linked](https://res.cloudinary.com/dza7lstvk/image/upload/v1716549903/Medusa%20Resources/order-fulfillment_h0vlps.jpg)

A fulfillment is also created for a return's items. So, Medusa defines a link between the `Fulfillment` and `Return` data models.

![A diagram showcasing an example of how data models from the Fulfillment and Order modules are linked](https://res.cloudinary.com/dza7lstvk/image/upload/v1728399052/Medusa%20Resources/Social_Media_Graphics_2024_Order_Return_vetimk.jpg)

### Retrieve with Query

To retrieve the fulfillments of an order with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `fulfillments.*` in `fields`:

To retrieve the fulfillments of a return, pass `fulfillments.*` in `fields`.

### query.graph

```ts
const { data: orders } = await query.graph({
  entity: "order",
  fields: [
    "fulfillments.*",
  ],
})

// orders[0].fulfillments
```

### useQueryGraphStep

```ts
import { useQueryGraphStep } from "@medusajs/medusa/core-flows"

// ...

const { data: orders } = useQueryGraphStep({
  entity: "order",
  fields: [
    "fulfillments.*",
  ],
})

// orders[0].fulfillments
```

### Manage with Link

To manage the fulfillments of an order, use [Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link/index.html.md):

### link.create

```ts
import { Modules } from "@medusajs/framework/utils"

// ...

await link.create({
  [Modules.ORDER]: {
    order_id: "order_123",
  },
  [Modules.FULFILLMENT]: {
    fulfillment_id: "ful_123",
  },
})
```

### createRemoteLinkStep

```ts
import { Modules } from "@medusajs/framework/utils"
import { createRemoteLinkStep } from "@medusajs/medusa/core-flows"

// ...

createRemoteLinkStep({
  [Modules.ORDER]: {
    order_id: "order_123",
  },
  [Modules.FULFILLMENT]: {
    fulfillment_id: "ful_123",
  },
})
```

***

## Payment Module

An order's payment details are stored in a payment collection. This also applies for claims and exchanges.

So, Medusa defines links between the `PaymentCollection` data model and the `Order`, `OrderClaim`, and `OrderExchange` data models.

![A diagram showcasing an example of how data models from the Order and Payment modules are linked](https://res.cloudinary.com/dza7lstvk/image/upload/v1716554726/Medusa%20Resources/order-payment_ubdwok.jpg)

### Retrieve with Query

To retrieve the payment collections of an order, order exchange, or order claim with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `payment_collections.*` in `fields`:

### query.graph

```ts
const { data: orders } = await query.graph({
  entity: "order",
  fields: [
    "payment_collections.*",
  ],
})

// orders[0].payment_collections
```

### useQueryGraphStep

```ts
import { useQueryGraphStep } from "@medusajs/medusa/core-flows"

// ...

const { data: orders } = useQueryGraphStep({
  entity: "order",
  fields: [
    "payment_collections.*",
  ],
})

// orders[0].payment_collections
```

### Manage with Link

To manage the payment collections of an order, use [Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link/index.html.md):

### link.create

```ts
import { Modules } from "@medusajs/framework/utils"

// ...

await link.create({
  [Modules.ORDER]: {
    order_id: "order_123",
  },
  [Modules.PAYMENT]: {
    payment_collection_id: "paycol_123",
  },
})
```

### createRemoteLinkStep

```ts
import { Modules } from "@medusajs/framework/utils"
import { createRemoteLinkStep } from "@medusajs/medusa/core-flows"

// ...

createRemoteLinkStep({
  [Modules.ORDER]: {
    order_id: "order_123",
  },
  [Modules.PAYMENT]: {
    payment_collection_id: "paycol_123",
  },
})
```

***

## Product Module

Medusa defines read-only links between:

- the `OrderLineItem` data model and the [Product Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/product/index.html.md)'s `Product` data model. This means you can retrieve the details of a line item's product, but you don't manage the links in a pivot table in the database. The product of a line item is determined by the `product_id` property of the `OrderLineItem` data model.
- the `OrderLineItem` data model and the [Product Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/product/index.html.md)'s `ProductVariant` data model. This means you can retrieve the details of a line item's variant, but you don't manage the links in a pivot table in the database. The variant of a line item is determined by the `variant_id` property of the `OrderLineItem` data model.

### Retrieve with Query

To retrieve the variant of a line item with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `variant.*` in `fields`:

To retrieve the product, pass `product.*` in `fields`.

### query.graph

```ts
const { data: lineItems } = await query.graph({
  entity: "order_line_item",
  fields: [
    "variant.*",
  ],
})

// lineItems.variant
```

### useQueryGraphStep

```ts
import { useQueryGraphStep } from "@medusajs/medusa/core-flows"

// ...

const { data: lineItems } = useQueryGraphStep({
  entity: "order_line_item",
  fields: [
    "variant.*",
  ],
})

// lineItems.variant
```

***

## Promotion Module

An order is associated with the promotion applied on it. Medusa defines a link between the `Order` and `Promotion` data models.

![A diagram showcasing an example of how data models from the Order and Promotion modules are linked](https://res.cloudinary.com/dza7lstvk/image/upload/v1716555015/Medusa%20Resources/order-promotion_dgjzzd.jpg)

### Retrieve with Query

To retrieve the promotion applied on an order with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `promotion.*` in `fields`:

### query.graph

```ts
const { data: orders } = await query.graph({
  entity: "order",
  fields: [
    "promotion.*",
  ],
})

// orders[0].promotion
```

### useQueryGraphStep

```ts
import { useQueryGraphStep } from "@medusajs/medusa/core-flows"

// ...

const { data: orders } = useQueryGraphStep({
  entity: "order",
  fields: [
    "promotion.*",
  ],
})

// orders[0].promotion
```

### Manage with Link

To manage the promotion of an order, use [Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link/index.html.md):

### link.create

```ts
import { Modules } from "@medusajs/framework/utils"

// ...

await link.create({
  [Modules.ORDER]: {
    order_id: "order_123",
  },
  [Modules.PROMOTION]: {
    promotion_id: "promo_123",
  },
})
```

### createRemoteLinkStep

```ts
import { Modules } from "@medusajs/framework/utils"
import { createRemoteLinkStep } from "@medusajs/medusa/core-flows"

// ...

createRemoteLinkStep({
  [Modules.ORDER]: {
    order_id: "order_123",
  },
  [Modules.PROMOTION]: {
    promotion_id: "promo_123",
  },
})
```

***

## Region Module

Medusa defines a read-only link between the `Order` data model and the [Region Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/region/index.html.md)'s `Region` data model. This means you can retrieve the details of an order's region, but you don't manage the links in a pivot table in the database. The region of an order is determined by the `region_id` property of the `Order` data model.

### Retrieve with Query

To retrieve the region of an order with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `region.*` in `fields`:

### query.graph

```ts
const { data: orders } = await query.graph({
  entity: "order",
  fields: [
    "region.*",
  ],
})

// orders[0].region
```

### useQueryGraphStep

```ts
import { useQueryGraphStep } from "@medusajs/medusa/core-flows"

// ...

const { data: orders } = useQueryGraphStep({
  entity: "order",
  fields: [
    "region.*",
  ],
})

// orders[0].region
```

***

## Sales Channel Module

Medusa defines a read-only link between the `Order` data model and the [Sales Channel Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/sales-channel/index.html.md)'s `SalesChannel` data model. This means you can retrieve the details of an order's sales channel, but you don't manage the links in a pivot table in the database. The sales channel of an order is determined by the `sales_channel_id` property of the `Order` data model.

### Retrieve with Query

To retrieve the sales channel of an order with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `sales_channel.*` in `fields`:

### query.graph

```ts
const { data: orders } = await query.graph({
  entity: "order",
  fields: [
    "sales_channel.*",
  ],
})

// orders[0].sales_channel
```

### useQueryGraphStep

```ts
import { useQueryGraphStep } from "@medusajs/medusa/core-flows"

// ...

const { data: orders } = useQueryGraphStep({
  entity: "order",
  fields: [
    "sales_channel.*",
  ],
})

// orders[0].sales_channel
```


# Order Versioning

In this document, you’ll learn how an order and its details are versioned.

## What's Versioning?

Versioning means assigning a version number to a record, such as an order and its items. This is useful to view the different versions of the order following changes in its lifetime.

When changes are made on an order, such as an item is added or returned, the order's version changes.

***

## version Property

The `Order` and `OrderSummary` data models have a `version` property that indicates the current version. By default, its value is `1`.

Other order-related data models, such as `OrderItem`, also has a `version` property, but it indicates the version it belongs to.

***

## How the Version Changes

When the order is changed, such as an item is exchanged, this changes the version of the order and its related data:

1. The version of the order and its summary is incremented.
2. Related order data that have a `version` property, such as the `OrderItem`, are duplicated. The duplicated item has the new version, whereas the original item has the previous version.

When the order is retrieved, only the related data having the same version is retrieved.


# Order Change

In this document, you'll learn about the Order Change data model and possible actions in it.

## OrderChange Data Model

The [OrderChange data model](https://docs.medusajs.com/references/order/models/OrderChange/index.html.md) represents any kind of change to an order, such as a return, exchange, or edit.

Its `change_type` property indicates what the order change is created for:

1. `edit`: The order change is making edits to the order, as explained in [this guide](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/order/edit/index.html.md).
2. `exchange`: The order change is associated with an exchange, which you can learn about in [this guide](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/order/exchange/index.html.md).
3. `claim`: The order change is associated with a claim, which you can learn about in [this guide](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/order/claim/index.html.md).
4. `return_request` or `return_receive`: The order change is associated with a return, which you can learn about in [this guide](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/order/return/index.html.md).

Once the order change is confirmed, its changes are applied on the order.

***

## Order Change Actions

The actions to perform on the original order by a change, such as adding an item, are represented by the [OrderChangeAction data model](https://docs.medusajs.com/references/order/models/OrderChangeAction/index.html.md).

The `OrderChangeAction` has an `action` property that indicates the type of action to perform on the order, and a `details` property that holds more details related to the action.

The following table lists the possible `action` values that Medusa uses and what `details` they carry.

|Action|Description|Details|
|---|---|---|---|---|
|\`ITEM\_ADD\`|Add an item to the order.|\`details\`|
|\`ITEM\_UPDATE\`|Update an item in the order.|\`details\`|
|\`RETURN\_ITEM\`|Set an item to be returned.|\`details\`|
|\`RECEIVE\_RETURN\_ITEM\`|Mark a return item as received.|\`details\`|
|\`RECEIVE\_DAMAGED\_RETURN\_ITEM\`|Mark a return item that's damaged as received.|\`details\`|
|\`SHIPPING\_ADD\`|Add a shipping method for new or returned items.|No details added. The ID to the shipping method is added in the |
|\`SHIPPING\_ADD\`|Add a shipping method for new or returned items.|No details added. The ID to the shipping method is added in the |
|\`WRITE\_OFF\_ITEM\`|Remove an item's quantity as part of the claim, without adding the quantity back to the item variant's inventory.|\`details\`|


# Order Exchange

In this document, you’ll learn about order exchanges.

Refer to this [Medusa Admin User Guide](https://docs.medusajs.com/user-guide/orders/exchanges/index.html.md) to learn how to manage an order's exchanges using the dashboard.

## What is an Exchange?

An exchange is the replacement of an item that the customer ordered with another.

A merchant creates the exchange, specifying the items to be replaced and the new items to be sent.

The [OrderExchange data model](https://docs.medusajs.com/references/order/models/OrderExchange/index.html.md) represents an exchange.

***

## Returned and New Items

When the exchange is created, a return, represented by the [Return data model](https://docs.medusajs.com/references/order/models/Return/index.html.md), is created to handle receiving the items back from the customer.

Learn more about returns in [this guide](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/order/return/index.html.md).

The [OrderExchangeItem data model](https://docs.medusajs.com/references/order/models/OrderExchangeItem/index.html.md) represents the new items to be sent to the customer.

***

## Exchange Shipping Methods

An exchange has shipping methods used to send the new items to the customer. They’re represented by the [OrderShippingMethod data model](https://docs.medusajs.com/references/order/models/OrderShippingMethod/index.html.md).

The shipping methods for the returned items are associated with the exchange's return, as explained in [this guide](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/order/return#return-shipping-methods/index.html.md).

***

## Exchange Payment

The `Exchange` data model has a `difference_due` property that stores the outstanding amount.

|Condition|Result|
|---|---|---|
|\`difference\_due \< 0\`|Merchant owes the customer a refund of the |
|\`difference\_due > 0\`|Merchant requires additional payment from the customer of the |
|\`difference\_due = 0\`|No payment processing is required.|

Any payment or refund made is stored in the [Transaction data model](https://docs.medusajs.com/references/order/models/OrderTransaction/index.html.md).

***

## How Exchanges Impact an Order’s Version

When an exchange is confirmed, the order’s version is incremented.


# Promotions Adjustments in Orders

In this document, you’ll learn how a promotion is applied to an order’s items and shipping methods using adjustment lines.

## What are Adjustment Lines?

An adjustment line indicates a change to a line item or a shipping method’s amount. It’s used to apply promotions or discounts on an order.

The [OrderLineItemAdjustment data model](https://docs.medusajs.com/references/order/models/OrderLineItemAdjustment/index.html.md) represents changes on a line item, and the [OrderShippingMethodAdjustment data model](https://docs.medusajs.com/references/order/models/OrderShippingMethodAdjustment/index.html.md) represents changes on a shipping method.

![A diagram showcasing the relation between an order, its items and shipping methods, and their adjustment lines](https://res.cloudinary.com/dza7lstvk/image/upload/v1712306017/Medusa%20Resources/order-adjustments_myflir.jpg)

The `amount` property of the adjustment line indicates the amount to be discounted from the original amount.

The ID of the applied promotion is stored in the `promotion_id` property of the adjustment line.

***

## Discountable Option

The `OrderLineItem` data model has an `is_discountable` property that indicates whether promotions can be applied to the line item. It’s enabled by default.

When disabled, a promotion can’t be applied to a line item. In the context of the Promotion Module, the promotion isn’t applied to the line item even if it matches its rules.

***

## Promotion Actions

When using the Order and Promotion modules together, use the [computeActions method of the Promotion Module’s main service](https://docs.medusajs.com/references/promotion/computeActions/index.html.md). It retrieves the actions of line items and shipping methods.

Learn more about actions in the [Promotion Module’s documentation](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/promotion/actions/index.html.md).

```ts collapsibleLines="1-10" expandButtonLabel="Show Imports"
import {
  ComputeActionAdjustmentLine,
  ComputeActionItemLine,
  ComputeActionShippingLine,
  // ...
} from "@medusajs/framework/types"

// ...

// retrieve the order
const order = await orderModuleService.retrieveOrder("ord_123", {
  relations: [
    "items.item.adjustments",
    "shipping_methods.shipping_method.adjustments",
  ],
})
// retrieve the line item adjustments
const lineItemAdjustments: ComputeActionItemLine[] = []
order.items.forEach((item) => {
  const filteredAdjustments = item.adjustments?.filter(
    (adjustment) => adjustment.code !== undefined
  ) as unknown as ComputeActionAdjustmentLine[]
  if (filteredAdjustments.length) {
    lineItemAdjustments.push({
      ...item,
      ...item.detail,
      adjustments: filteredAdjustments,
    })
  }
})

//retrieve shipping method adjustments
const shippingMethodAdjustments: ComputeActionShippingLine[] =
  []
order.shipping_methods.forEach((shippingMethod) => {
  const filteredAdjustments =
    shippingMethod.adjustments?.filter(
      (adjustment) => adjustment.code !== undefined
    ) as unknown as ComputeActionAdjustmentLine[]
  if (filteredAdjustments.length) {
    shippingMethodAdjustments.push({
      ...shippingMethod,
      adjustments: filteredAdjustments,
    })
  }
})

// compute actions
const actions = await promotionModuleService.computeActions(
  ["promo_123"],
  {
    items: lineItemAdjustments,
    shipping_methods: shippingMethodAdjustments,
    // TODO infer from cart or region
    currency_code: "usd",
  }
)
```

The `computeActions` method accepts the existing adjustments of line items and shipping methods to compute the actions accurately.

Then, use the returned `addItemAdjustment` and `addShippingMethodAdjustment` actions to set the order’s line items and the shipping method’s adjustments.

```ts collapsibleLines="1-9" expandButtonLabel="Show Imports"
import {
  AddItemAdjustmentAction,
  AddShippingMethodAdjustment,
  // ...
} from "@medusajs/framework/types"

// ...

await orderModuleService.setOrderLineItemAdjustments(
  order.id,
  actions.filter(
    (action) => action.action === "addItemAdjustment"
  ) as AddItemAdjustmentAction[]
)

await orderModuleService.setOrderShippingMethodAdjustments(
  order.id,
  actions.filter(
    (action) =>
      action.action === "addShippingMethodAdjustment"
  ) as AddShippingMethodAdjustment[]
)
```


# Order Claim

In this document, you’ll learn about order claims.

Refer to this [Medusa Admin User Guide](https://docs.medusajs.com/user-guide/orders/claims/index.html.md) to learn how to manage an order's claims using the dashboard.

## What is a Claim?

When a customer receives a defective or incorrect item, the merchant can create a claim to refund or replace the item.

The [OrderClaim data model](https://docs.medusajs.com/references/order/models/OrderClaim/index.html.md) represents a claim.

***

## Claim Type

The `Claim` data model has a `type` property whose value indicates the type of the claim:

- `refund`: the items are returned, and the customer is refunded.
- `replace`: the items are returned, and the customer receives new items.

***

## Old and Replacement Items

When the claim is created, a return, represented by the [Return data model](https://docs.medusajs.com/references/order/models/Return/index.html.md), is also created to handle receiving the old items from the customer.

Learn more about returns in [this guide](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/order/return/index.html.md).

If the claim’s type is `replace`, replacement items are represented by the [ClaimItem data model](https://docs.medusajs.com/references/order/models/OrderClaimItem/index.html.md).

***

## Claim Shipping Methods

A claim uses shipping methods to send the replacement items to the customer. These methods are represented by the [OrderShippingMethod data model](https://docs.medusajs.com/references/order/models/OrderShippingMethod/index.html.md).

The shipping methods for the returned items are associated with the claim's return, as explained in [this guide](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/order/return#return-shipping-methods/index.html.md).

***

## Claim Refund

If the claim’s type is `refund`, the amount to be refunded is stored in the `refund_amount` property.

The [Transaction data model](https://docs.medusajs.com/references/order/models/OrderTransaction/index.html.md) represents the refunds made for the claim.

***

## How Claims Impact an Order’s Version

When a claim is confirmed, the order’s version is incremented.


# Order Return

In this document, you’ll learn about order returns.

Refer to this [Medusa Admin User Guide](https://docs.medusajs.com/user-guide/orders/returns/index.html.md) to learn how to manage an order's returns using the dashboard.

## What is a Return?

A return is the return of items delivered from the customer back to the merchant. It is represented by the [Return data model](https://docs.medusajs.com/references/order/models/Return/index.html.md).

A return is requested either by the customer from the storefront, or the merchant from the admin. Medusa supports an automated Return Merchandise Authorization (RMA) flow.

![Diagram showcasing the automated RMA flow.](https://res.cloudinary.com/dza7lstvk/image/upload/v1719578128/Medusa%20Resources/return-rma_pzprwq.jpg)

Once the merchant receives the returned items, they mark the return as received.

***

## Returned Items

The items to be returned are represented by the [ReturnItem data model](https://docs.medusajs.com/references/order/models/ReturnItem/index.html.md).

The `ReturnItem` model has two properties storing the item's quantity:

1. `received_quantity`: The quantity of the item that's received and can be added to the item's inventory quantity.
2. `damaged_quantity`: The quantity of the item that's damaged, meaning it can't be sold again or added to the item's inventory quantity.

***

## Return Shipping Methods

A return has shipping methods used to return the items to the merchant. The shipping methods are represented by the [OrderShippingMethod data model](https://docs.medusajs.com/references/order/models/OrderShippingMethod/index.html.md).

In the Medusa application, the shipping method for a return is created only from a shipping option, provided by the Fulfillment Module, that has the rule `is_return` enabled.

***

## Refund Payment

The `refund_amount` property of the `Return` data model holds the amount a merchant must refund the customer.

The [OrderTransaction data model](https://docs.medusajs.com/references/order/models/OrderTransaction/index.html.md) represents the refunds made for the return.

***

## Returns in Exchanges and Claims

When a merchant creates an exchange or a claim, it includes returning items from the customer.

The `Return` data model also represents the return of these items. In this case, the return is associated with the exchange or claim it was created for.

***

## How Returns Impact an Order’s Version

The order’s version is incremented when:

1. A return is requested.
2. A return is marked as received.


# Tax Lines in Order Module

In this document, you’ll learn about tax lines in an order.

## What are Tax Lines?

A tax line indicates the tax rate of a line item or a shipping method.

The [OrderLineItemTaxLine data model](https://docs.medusajs.com/references/order/models/OrderLineItemTaxLine/index.html.md) represents a line item’s tax line, and the [OrderShippingMethodTaxLine data model](https://docs.medusajs.com/references/order/models/OrderShippingMethodTaxLine/index.html.md) represents a shipping method’s tax line.

![A diagram showcasing the relation between orders, items and shipping methods, and tax lines](https://res.cloudinary.com/dza7lstvk/image/upload/v1712307225/Medusa%20Resources/order-tax-lines_sixujd.jpg)

***

## Tax Inclusivity

By default, the tax amount is calculated by taking the tax rate from the line item or shipping method’s amount and then adding it to the item/method’s subtotal.

However, line items and shipping methods have an `is_tax_inclusive` property that, when enabled, indicates that the item or method’s price already includes taxes.

So, instead of calculating the tax rate and adding it to the item/method’s subtotal, it’s calculated as part of the subtotal.

The following diagram is a simplified showcase of how a subtotal is calculated from the tax perspective.

![A diagram showcasing how a subtotal is calculated from the tax perspective](https://res.cloudinary.com/dza7lstvk/image/upload/v1712307395/Medusa%20Resources/order-tax-inclusive_oebdnm.jpg)

For example, if a line item's amount is `5000`, the tax rate is `10`, and `is_tax_inclusive` is enabled, the tax amount is 10% of `5000`, which is `500`. The item's unit price becomes `4500`.


# Transactions

In this document, you’ll learn about an order’s transactions and its use.

## What is a Transaction?

A transaction represents any order payment process, such as capturing or refunding an amount. It’s represented by the [OrderTransaction data model](https://docs.medusajs.com/references/order/models/OrderTransaction/index.html.md).

The transaction’s main purpose is to ensure a correct balance between paid and outstanding amounts.

Transactions are also associated with returns, claims, and exchanges if additional payment or refund is required.

***

## Checking Outstanding Amount

The order’s total amounts are stored in the `OrderSummary`'s `totals` property, which is a JSON object holding the total details of the order.

```json
{
  "totals": {
    "total": 30,
    "subtotal": 30,
    // ...
  }
}
```

To check the outstanding amount of the order, its transaction amounts are summed. Then, the following conditions are checked:

|Condition|Result|
|---|---|---|
|summary’s total - transaction amounts total = 0|There’s no outstanding amount.|
|summary’s total - transaction amounts total > 0|The customer owes additional payment to the merchant.|
|summary’s total - transaction amounts total \< 0|The merchant owes the customer a refund.|

***

## Transaction Reference

The Order Module doesn’t provide payment processing functionalities, so it doesn’t store payments that can be processed. Payment functionalities are provided by the [Payment Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/payment/index.html.md).

The `OrderTransaction` data model has two properties that determine which data model and record holds the actual payment’s details:

- `reference`: indicates the table’s name in the database. For example, `payment` from the Payment Module.
- `reference_id`: indicates the ID of the record in the table. For example, `pay_123`.


# Account Holders and Saved Payment Methods

In this documentation, you'll learn about account holders, and how they're used to save payment methods in third-party payment providers.

Account holders are available starting from Medusa `v2.5.0`.

## What's an Account Holder?

An account holder represents a customer that can have saved payment methods in a third-party service. It's represented by the `AccountHolder` data model.

It holds fields retrieved from the third-party provider, such as:

- `external_id`: The ID of the equivalent customer or account holder in the third-party provider.
- `data`: Data returned by the payment provider when the account holder is created.

A payment provider that supports saving payment methods for customers would create the equivalent of an account holder in the third-party provider. Then, whenever a payment method is saved, it would be saved under the account holder in the third-party provider.

### Relation between Account Holder and Customer

The Medusa application creates a link between the [Customer](https://docs.medusajs.com/references/customer/models/Customer/index.html.md) data model of the [Customer Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/customer/index.html.md) and the `AccountHolder` data model of the Payment Module.

This link indicates that a customer can have more than one account holder, each representing saved payment methods in different payment providers.

Learn more about this link in the [Link to Other Modules](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/payment/links-to-other-modules/index.html.md) guide.

***

## Save Payment Methods

If a payment provider supports saving payment methods for a customer, they must implement the following methods:

- `createAccountHolder`: Creates an account holder in the payment provider. The Payment Module uses this method before creating the account holder in Medusa, and uses the returned data to set fields like `external_id` and `data` in the created `AccountHolder` record.
- `deleteAccountHolder`: Deletes an account holder in the payment provider. The Payment Module uses this method when an account holder is deleted in Medusa.
- `savePaymentMethod`: Saves a payment method for an account holder in the payment provider.
- `listPaymentMethods`: Lists saved payment methods in the third-party service for an account holder. This is useful when displaying the customer's saved payment methods in the storefront.

Learn more about implementing these methods in the [Create Payment Provider guide](https://docs.medusajs.com/references/payment/provider/index.html.md).

***

## Account Holder in Medusa Payment Flows

In the Medusa application, when a payment session is created for a registered customer, the Medusa application uses the Payment Module to create an account holder for the customer.

Consequently, the Payment Module uses the payment provider to create an account holder in the third-party service, then creates the account holder in Medusa.

This flow is only supported if the chosen payment provider has implemented the necessary [save payment methods](#save-payment-methods).


# Payment Module Options

In this document, you'll learn about the options of the Payment Module.

## All Module Options

|Option|Description|Required|Default|
|---|---|---|---|---|---|---|
|\`webhook\_delay\`|A number indicating the delay in milliseconds before processing a webhook event.|No|\`5000\`|
|\`webhook\_retries\`|The number of times to retry the webhook event processing in case of an error.|No|\`3\`|
|\`providers\`|An array of payment providers to install and register. Learn more |No|-|

***

## providers Option

The `providers` option is an array of payment module providers.

When the Medusa application starts, these providers are registered and can be used to process payments.

For example:

```ts title="medusa-config.ts"
import { Modules } from "@medusajs/framework/utils"

// ...

module.exports = defineConfig({
  // ...
  modules: [
    {
      resolve: "@medusajs/medusa/payment",
      options: {
        providers: [
          {
            resolve: "@medusajs/medusa/payment-stripe",
            id: "stripe",
            options: {
              // ...
            },
          },
        ],
      },
    },
  ],
})
```

The `providers` option is an array of objects that accept the following properties:

- `resolve`: A string indicating the package name of the module provider or the path to it relative to the `src` directory.
- `id`: A string indicating the provider's unique name or ID.
- `options`: An optional object of the module provider's options.


# Links between Payment Module and Other Modules

This document showcases the module links defined between the Payment Module and other Commerce Modules.

## Summary

The Payment Module has the following links to other modules:

|First Data Model|Second Data Model|Type|Description|
|---|---|---|---|
|Cart|PaymentCollection|Stored - one-to-one|Learn more|
|Customer|AccountHolder|Stored - many-to-many|Learn more|
|Order|PaymentCollection|Stored - one-to-many|Learn more|
|OrderClaim|PaymentCollection|Stored - one-to-many|Learn more|
|OrderExchange|PaymentCollection|Stored - one-to-many|Learn more|
|Region|PaymentProvider|Stored - many-to-many|Learn more|

***

## Cart Module

The Cart Module provides cart-related features, but not payment processing.

Medusa defines a link between the `Cart` and `PaymentCollection` data models. A cart has a payment collection which holds all the authorized payment sessions and payments made related to the cart.

Learn more about this relation in [this documentation](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/payment/payment-collection#usage-with-the-cart-module/index.html.md).

### Retrieve with Query

To retrieve the cart associated with the payment collection with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `cart.*` in `fields`:

### query.graph

```ts
const { data: paymentCollections } = await query.graph({
  entity: "payment_collection",
  fields: [
    "cart.*",
  ],
})

// paymentCollections[0].cart
```

### useQueryGraphStep

```ts
import { useQueryGraphStep } from "@medusajs/medusa/core-flows"

// ...

const { data: paymentCollections } = useQueryGraphStep({
  entity: "payment_collection",
  fields: [
    "cart.*",
  ],
})

// paymentCollections[0].cart
```

### Manage with Link

To manage the payment collection of a cart, use [Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link/index.html.md):

### link.create

```ts
import { Modules } from "@medusajs/framework/utils"

// ...

await link.create({
  [Modules.CART]: {
    cart_id: "cart_123",
  },
  [Modules.PAYMENT]: {
    payment_collection_id: "paycol_123",
  },
})
```

### createRemoteLinkStep

```ts
import { createRemoteLinkStep } from "@medusajs/medusa/core-flows"

// ...

createRemoteLinkStep({
  [Modules.CART]: {
    cart_id: "cart_123",
  },
  [Modules.PAYMENT]: {
    payment_collection_id: "paycol_123",
  },
})
```

***

## Customer Module

Medusa defines a link between the `Customer` and `AccountHolder` data models, allowing payment providers to save payment methods for a customer, if the payment provider supports it.

This link is available starting from Medusa `v2.5.0`.

### Retrieve with Query

To retrieve the customer associated with an account holder with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `customer.*` in `fields`:

### query.graph

```ts
const { data: accountHolders } = await query.graph({
  entity: "account_holder",
  fields: [
    "customer.*",
  ],
})

// accountHolders[0].customer
```

### useQueryGraphStep

```ts
import { useQueryGraphStep } from "@medusajs/medusa/core-flows"

// ...

const { data: accountHolders } = useQueryGraphStep({
  entity: "account_holder",
  fields: [
    "customer.*",
  ],
})

// accountHolders[0].customer
```

### Manage with Link

To manage the account holders of a customer, use [Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link/index.html.md):

### link.create

```ts
import { Modules } from "@medusajs/framework/utils"

// ...

await link.create({
  [Modules.CUSTOMER]: {
    customer_id: "cus_123",
  },
  [Modules.PAYMENT]: {
    account_holder_id: "acchld_123",
  },
})
```

### createRemoteLinkStep

```ts
import { createRemoteLinkStep } from "@medusajs/medusa/core-flows"

// ...

createRemoteLinkStep({
  [Modules.CUSTOMER]: {
    customer_id: "cus_123",
  },
  [Modules.PAYMENT]: {
    account_holder_id: "acchld_123",
  },
})
```

***

## Order Module

An order's payment details are stored in a payment collection. This also applies for claims and exchanges.

So, Medusa defines links between the `PaymentCollection` data model and the `Order`, `OrderClaim`, and `OrderExchange` data models.

![A diagram showcasing an example of how data models from the Order and Payment modules are linked](https://res.cloudinary.com/dza7lstvk/image/upload/v1716554726/Medusa%20Resources/order-payment_ubdwok.jpg)

### Retrieve with Query

To retrieve the order of a payment collection with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `order.*` in `fields`:

### query.graph

```ts
const { data: paymentCollections } = await query.graph({
  entity: "payment_collection",
  fields: [
    "order.*",
  ],
})

// paymentCollections[0].order
```

### useQueryGraphStep

```ts
import { useQueryGraphStep } from "@medusajs/medusa/core-flows"

// ...

const { data: paymentCollections } = useQueryGraphStep({
  entity: "payment_collection",
  fields: [
    "order.*",
  ],
})

// paymentCollections[0].order
```

### Manage with Link

To manage the payment collections of an order, use [Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link/index.html.md):

### link.create

```ts
import { Modules } from "@medusajs/framework/utils"

// ...

await link.create({
  [Modules.ORDER]: {
    order_id: "order_123",
  },
  [Modules.PAYMENT]: {
    payment_collection_id: "paycol_123",
  },
})
```

### createRemoteLinkStep

```ts
import { Modules } from "@medusajs/framework/utils"
import { createRemoteLinkStep } from "@medusajs/medusa/core-flows"

// ...

createRemoteLinkStep({
  [Modules.ORDER]: {
    order_id: "order_123",
  },
  [Modules.PAYMENT]: {
    payment_collection_id: "paycol_123",
  },
})
```

***

## Region Module

You can specify for each region which payment providers are available. The Medusa application defines a link between the `PaymentProvider` and the `Region` data models.

![A diagram showcasing an example of how resources from the Payment and Region modules are linked](https://res.cloudinary.com/dza7lstvk/image/upload/v1711569520/Medusa%20Resources/payment-region_jyo2dz.jpg)

This increases the flexibility of your store. For example, you only show during checkout the payment providers associated with the cart's region.

### Retrieve with Query

To retrieve the regions of a payment provider with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `regions.*` in `fields`:

### query.graph

```ts
const { data: paymentProviders } = await query.graph({
  entity: "payment_provider",
  fields: [
    "regions.*",
  ],
})

// paymentProviders[0].regions
```

### useQueryGraphStep

```ts
import { useQueryGraphStep } from "@medusajs/medusa/core-flows"

// ...

const { data: paymentProviders } = useQueryGraphStep({
  entity: "payment_provider",
  fields: [
    "regions.*",
  ],
})

// paymentProviders[0].regions
```

### Manage with Link

To manage the payment providers in a region, use [Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link/index.html.md):

### link.create

```ts
import { Modules } from "@medusajs/framework/utils"

// ...

await link.create({
  [Modules.REGION]: {
    region_id: "reg_123",
  },
  [Modules.PAYMENT]: {
    payment_provider_id: "pp_stripe_stripe",
  },
})
```

### createRemoteLinkStep

```ts
import { Modules } from "@medusajs/framework/utils"
import { createRemoteLinkStep } from "@medusajs/medusa/core-flows"

// ...

createRemoteLinkStep({
  [Modules.REGION]: {
    region_id: "reg_123",
  },
  [Modules.PAYMENT]: {
    payment_provider_id: "pp_stripe_stripe",
  },
})
```


# Payment

In this document, you’ll learn what a payment is and how it's created, captured, and refunded.

## What's a Payment?

When a payment session is authorized, a payment, represented by the [Payment data model](https://docs.medusajs.com/references/payment/models/Payment/index.html.md), is created. This payment can later be captured or refunded.

A payment carries many of the data and relations of a payment session:

- It belongs to the same payment collection.
- It’s associated with the same payment provider, which handles further payment processing.
- It stores the payment session’s `data` property in its `data` property, as it’s still useful for the payment provider’s processing.

***

## Capture Payments

When a payment is captured, a capture, represented by the [Capture data model](https://docs.medusajs.com/references/payment/models/Capture/index.html.md), is created. It holds details related to the capture, such as the amount, the capture date, and more.

The payment can also be captured incrementally, each time a capture record is created for that amount.

![A diagram showcasing how a payment's multiple captures are stored](https://res.cloudinary.com/dza7lstvk/image/upload/v1711565445/Medusa%20Resources/payment-capture_f5fve1.jpg)

***

## Refund Payments

When a payment is refunded, a refund, represented by the [Refund data model](https://docs.medusajs.com/references/payment/models/Refund/index.html.md), is created. It holds details related to the refund, such as the amount, refund date, and more.

A payment can be refunded multiple times, and each time a refund record is created.

![A diagram showcasing how a payment's multiple refunds are stored](https://res.cloudinary.com/dza7lstvk/image/upload/v1711565555/Medusa%20Resources/payment-refund_lgfvyy.jpg)

***

## data Property

Payment providers may need additional data to process the payment later. For example, the ID of the associated payment in the third-party provider.

The `Payment` data model has a `data` property used to store that data. The first time it's set is when the [payment provider in Medusa](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/payment/payment-provider/index.html.md) authorizes the payment.

Then, the `data` property is passed to the Medusa payment provider when the payment is captured or refunded, allowing the payment provider to utilize the data to process the payment with the third-party provider.

If you're building a custom payment provider, learn more about authorizing and capturing the payments and setting the `data` property in the [Create Payment Provider](https://docs.medusajs.com/references/payment/provider/index.html.md) guide.


# Payment Collection

In this document, you’ll learn what a payment collection is and how the Medusa application uses it with the Cart Module.

## What's a Payment Collection?

A payment collection stores payment details related to a resource, such as a cart or an order. It’s represented by the [PaymentCollection data model](https://docs.medusajs.com/references/payment/models/PaymentCollection/index.html.md).

Every purchase or request for payment starts with a payment collection. The collection holds details necessary to complete the payment, including:

- The [payment sessions](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/payment/payment-session/index.html.md) that represents the payment amount to authorize.
- The [payments](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/payment/payment/index.html.md) that are created when a payment session is authorized. They can be captured and refunded.
- The [payment providers](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/payment/payment-provider/index.html.md) that handle the processing of each payment session, including the authorization, capture, and refund.

***

## Multiple Payments

The payment collection supports multiple payment sessions and payments.

You can use this to accept payments in increments or split payments across payment providers.

![Diagram showcasing how a payment collection can have multiple payment sessions and payments](https://res.cloudinary.com/dza7lstvk/image/upload/v1711554695/Medusa%20Resources/payment-collection-multiple-payments_oi3z3n.jpg)

***

## Usage with the Cart Module

The Cart Module provides cart management features. However, it doesn’t provide any features related to accepting payment.

During checkout, the Medusa application links a cart to a payment collection, which will be used for further payment processing.

It also implements the payment flow during checkout as explained in [this documentation](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/payment/payment-flow/index.html.md).

![Diagram showcasing the relation between the Payment and Cart modules](https://res.cloudinary.com/dza7lstvk/image/upload/v1711537849/Medusa%20Resources/cart-payment_ixziqm.jpg)


# Payment Steps in Checkout Flow

In this guide, you'll learn about Medusa's accept payment flow that's used in checkout.

## Overview of the Payment Flow in Checkout

The Medusa application has a built-in payment flow that allows you to accept payments from customers, typically during checkout.

This flow is designed to be flexible and extensible, allowing you to integrate with various payment providers.

The payment flow consists of the following steps:

![A diagram showcasing the payment flow's steps](https://res.cloudinary.com/dza7lstvk/image/upload/v1711566781/Medusa%20Resources/payment-flow_jblrvw.jpg)

- [Create Payment Collection](https://docs.medusajs.com/api/store#payment-collections_postpaymentcollections): Create a payment collection associated with a cart.
  - This payment collection will hold all details related to the payment operations.
- [Show Payment Providers](https://docs.medusajs.com/api/store#payment-providers_getpaymentproviders): Show the customer the available payment providers to choose from.
  - You can integrate any [payment provider](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/payment/payment-provider/index.html.md), and you can enable them per region.
- [Create and Initialize Payment Session](https://docs.medusajs.com/api/store#payment-collections_postpaymentcollectionsidpaymentsessions): Create a payment session for the selected payment provider in the Medusa application, and initialize the session in the third-party payment provider.
- [Complete Cart](https://docs.medusajs.com/api/store#carts_postcartsidcomplete): Once the customer places the order, complete the cart, which involves:
  - Authorizing the payment session with the third-party payment provider.
  - If the third-party payment provider requires performing additional actions, show them to the customer, then retry cart completion.

***

## Implement Payment Checkout Step in Storefront

If you're using the [Next.js Starter Storefront](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/nextjs-starter/index.html.md), the checkout flow is already implemented with the payment step.

If you're building a custom storefront, or you want to customize the checkout flow, you can follow the [Checkout in Storefront](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/storefront-development/checkout/index.html.md) guide to learn how to build the checkout flow in the storefront, including the payment step.

***

{/* TODO add section on customizng the payment flow */}

## Build a Custom Payment Flow

You can also build a custom payment flow using workflows or the Payment Module's main service.

Refer to the [Accept Payment Flow](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/payment/payment-flow/index.html.md) guide to learn more.


# Accept Payment in Checkout Flow

In this guide, you'll learn how to implement it using workflows or the Payment Module.

## Why Implement the Payment Flow?

Medusa already provides a built-in payment flow that allows you to accept payments from customers, which you can learn about in the [Accept Payment Flow in Checkout](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/payment/payment-checkout-flow/index.html.md) guide.

You may need to implement a custom payment flow if you have a different use case, or you're using the Payment Module separately from the Medusa application.

This guide will help you understand how to implement a payment flow using the Payment Module's main service or workflows.

You can also follow this guide to get a general understanding of how the payment flow works in the Medusa application.

***

## How to Implement the Accept Payment Flow?

For a guide on how to implement this flow in the storefront, check out [this guide](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/storefront-development/checkout/payment/index.html.md).

It's highly recommended to use Medusa's workflows to implement this flow. Use the Payment Module's main service for more complex cases.

### 1. Create a Payment Collection

A payment collection holds all details related to a resource’s payment operations. So, you start off by creating a payment collection.

In the Medusa application, you associate the payment collection with a cart, which is the resource that the customer is trying to pay for.

For example:

### Using Workflow

```ts
import { createPaymentCollectionForCartWorkflow } from "@medusajs/medusa/core-flows"

// ...

await createPaymentCollectionForCartWorkflow(container)
  .run({
    input: {
      cart_id: "cart_123",
    },
  })
```

### Using Service

```ts
const paymentCollection =
  await paymentModuleService.createPaymentCollections({
    currency_code: "usd",
    amount: 5000,
  })
```

### 2. Show Payment Providers

Next, you'll show the customer the available payment providers to choose from.

In the Medusa application, you need to use [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md) to retrieve the available payment providers in a region.

### Using Query

```ts
const query = container.resolve("query")

const { data: regionPaymentProviders } = await query.graph({
  entryPoint: "region_payment_provider",
  variables: {
    filters: {
      region_id: "reg_123",
    },
  },
  fields: ["payment_providers.*"],
})

const paymentProviders = regionPaymentProviders.map(
  (relation) => relation.payment_providers
)
```

### Using Service

```ts
const paymentProviders = await paymentModuleService.listPaymentProviders()
```

### 3. Create Payment Sessions

The payment collection has one or more payment sessions, each being a payment amount to be authorized by a payment provider.

So, once the customer selects a payment provider, create a payment session for the selected payment provider.

This will also initialize the payment session in the third-party payment provider.

For example:

### Using Workflow

```ts
import { createPaymentSessionsWorkflow } from "@medusajs/medusa/core-flows"

// ...

const { result: paymentSesion } = await createPaymentSessionsWorkflow(container)
  .run({
    input: {
      payment_collection_id: "paycol_123",
      provider_id: "pp_stripe_stripe",
    },
  })
```

### Using Service

```ts
const paymentSession =
  await paymentModuleService.createPaymentSession(
    paymentCollection.id,
    {
      provider_id: "pp_stripe_stripe",
      currency_code: "usd",
      amount: 5000,
      data: {
        // any necessary data for the
        // payment provider
      },
    }
  )
```

### 4. Authorize Payment Session

Once the customer places the order, you need to authorize the payment session with the third-party payment provider.

For example:

### Using Step

```ts
import { authorizePaymentSessionStep } from "@medusajs/medusa/core-flows"

// ...

authorizePaymentSessionStep({
  id: "payses_123",
  context: {},
})
```

### Using Service

```ts
const payment = authorizePaymentSessionStep({
  id: "payses_123",
  context: {},
})
```

When the payment authorization is successful, a payment is created and returned.

#### Handling Additional Action

If you used the `authorizePaymentSessionStep`, you don't need to implement this logic as it's implemented in the step.

If the payment authorization isn’t successful, whether because it requires additional action or for another reason, the method updates the payment session with the new status and throws an error.

In that case, you can catch that error and, if the session's `status` property is `requires_more`, handle the additional action, then retry the authorization.

For example:

```ts
try {
  const payment =
    await paymentModuleService.authorizePaymentSession(
      paymentSession.id,
      {}
    )
} catch (e) {
  // retrieve the payment session again
  const updatedPaymentSession = (
    await paymentModuleService.listPaymentSessions({
      id: [paymentSession.id],
    })
  )[0]

  if (updatedPaymentSession.status === "requires_more") {
    // TODO perform required action
    // TODO authorize payment again.
  }
}
```

### 5. Payment Flow Complete

The payment flow is complete once the payment session is authorized and the payment is created.

You can then:

- Complete the cart using the [completeCartWorkflow](https://docs.medusajs.com/references/medusa-workflows/completeCartWorkflow/index.html.md) if you're using the Medusa application.
- Capture the payment either using the [capturePaymentWorkflow](https://docs.medusajs.com/references/medusa-workflows/capturePaymentWorkflow/index.html.md) or [capturePayment method](https://docs.medusajs.com/references/payment/capturePayment/index.html.md).
- Refund captured amounts using the [refundPaymentWorkflow](https://docs.medusajs.com/references/medusa-workflows/refundPaymentWorkflow/index.html.md) or [refundPayment method](https://docs.medusajs.com/references/payment/refundPayment/index.html.md).

Some payment providers allow capturing the payment automatically once it’s authorized. In that case, you don’t need to do it manually.


# Payment Module Provider

In this guide, you’ll learn about the Payment Module Provider and how it's used.

Refer to this [Medusa Admin User Guide](https://docs.medusajs.com/user-guide/settings/regions/index.html.md) to learn how to manage the payment providers available in a region using the dashboard.

***

## What is a Payment Module Provider?

The Payment Module Provider handles payment processing in the Medusa application. It integrates third-party payment services, such as [Stripe](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/payment/payment-provider/stripe/index.html.md).

To authorize a payment amount with a payment provider, a payment session is created and associated with that payment provider. The payment provider is then used to handle the authorization.

After the payment session is authorized, the payment provider is associated with the resulting payment and handles its payment processing, such as to capture or refund payment.

![Diagram showcasing the communication between Medusa, the Payment Module Provider, and the third-party payment provider.](https://res.cloudinary.com/dza7lstvk/image/upload/v1746791374/Medusa%20Resources/payment-provider-service_l4zi6m.jpg)

### List of Payment Module Providers

- [Stripe](https://docs.medusajs.com/commerce-modules/payment/payment-provider/stripe/index.html.md)

### Default Payment Provider

The Payment Module provides a `system` payment provider that acts as a placeholder payment provider.

It doesn’t handle payment processing and delegates that to the merchant. It acts similarly to a cash-on-delivery (COD) payment method.

The identifier of the system payment provider is `pp_system`.

***

## How to Create a Custom Payment Provider?

A payment provider is a module whose main service extends the `AbstractPaymentProvider` imported from `@medusajs/framework/utils`.

The module can have multiple payment provider services, where each is registered as a separate payment provider.

Refer to [this guide](https://docs.medusajs.com/references/payment/provider/index.html.md) on how to create a payment provider for the Payment Module.

After you create a payment provider, you can enable it as a payment provider in a region using the [Medusa Admin dashboard](https://docs.medusajs.com/user-guide/settings/regions/index.html.md).

***

## How are Payment Providers Registered?

### Configure Payment Module's Providers

The Payment Module accepts a `providers` option that allows you to configure the providers registered in your application.

Learn more about this option in the [Module Options](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/payment/module-options/index.html.md) guide.

### Registration on Application Start

When the Medusa application starts, it registers the Payment Module Providers defined in the `providers` option of the Payment Module.

For each Payment Module Provider, the Medusa application finds all payment provider services defined in them to register.

### PaymentProvider Data Model

A registered payment provider is represented by the [PaymentProvider data model](https://docs.medusajs.com/references/payment/models/PaymentProvider/index.html.md) in the Medusa application.

![Diagram showcasing the PaymentProvider data model](https://res.cloudinary.com/dza7lstvk/image/upload/v1746791364/Medusa%20Resources/payment-provider-model_lx91oa.jpg)

This data model is used to reference a service in the Payment Module Provider and determine whether it's installed in the application.

The `PaymentProvider` data model has the following properties:

- `id`: The unique identifier of the Payment Module Provider. The ID's format is `pp_{identifier}_{id}`, where:
  - `identifier` is the value of the `identifier` property in the Payment Module Provider's service.
  - `id` is the value of the `id` property of the Payment Module Provider in `medusa-config.ts`.
- `is_enabled`: A boolean indicating whether the payment provider is enabled.

### How to Remove a Payment Provider?

If you remove a payment provider from the `providers` option, the Medusa application will not remove the associated `PaymentProvider` data model record.

Instead, the Medusa application will set the `is_enabled` property of the `PaymentProvider`'s record to `false`. This allows you to re-enable the payment provider later if needed by adding it back to the `providers` option.


# Payment Session

In this document, you’ll learn what a payment session is.

## What's a Payment Session?

A payment session, represented by the [PaymentSession data model](https://docs.medusajs.com/references/payment/models/PaymentSession/index.html.md), is a payment amount to be authorized. It’s associated with a payment provider that handles authorizing it.

A payment collection can have multiple payment sessions. Using this feature, you can implement payment in installments or payments using multiple providers.

![Diagram showcasing how every payment session has a different payment provider](https://res.cloudinary.com/dza7lstvk/image/upload/v1711565056/Medusa%20Resources/payment-session-provider_guxzqt.jpg)

***

## data Property

Payment providers may need additional data to process the payment later. For example, the ID of the session in the third-party provider.

The `PaymentSession` data model has a `data` property used to store that data. It's set by the [payment provider in Medusa](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/payment/payment-provider/index.html.md) when the payment is initialized.

Then, when the payment session is authorized, the `data` property is used by the payment provider in Medusa to process the payment with the third-party provider.

If you're building a custom payment provider, learn more about initializing the payment session and setting the `data` property in the [Create Payment Provider](https://docs.medusajs.com/references/payment/provider/index.html.md) guide.

### data Property in the Storefront

This `data` property is accessible in the storefront as well. So, only store in it data that can be publicly shared, and data that is useful in the storefront.

For example, you can also store the client token used to initialize the payment session in the storefront with the third-party provider.

***

## Payment Session Status

The `status` property of a payment session indicates its current status. Its value can be:

- `pending`: The payment session is awaiting authorization.
- `requires_more`: The payment session requires an action before it’s authorized. For example, to enter a 3DS code.
- `authorized`: The payment session is authorized.
- `error`: An error occurred while authorizing the payment.
- `canceled`: The authorization of the payment session has been canceled.


# Payment Webhook Events

In this guide, you’ll learn how you can handle payment webhook events in your Medusa application and using the Payment Module.

## What's a Payment Webhook Event?

A payment webhook event is a request sent from a third-party payment provider to your application. It indicates a change in a payment’s status.

This is useful in many cases such as:

- When a payment is processed (authorized or captured) asynchronously.
- When a payment is managed on the third-party payment provider's side.
- When a payment action on the frontend was interrupted, leading the payment to be processed without an order being created in the Medusa application.

So, it's essential to handle webhook events to ensure that your application is aware of updated payment statuses and can take appropriate actions.

***

## How to Handle Payment Webhook Events

### Webhook Listener API Route

The Medusa application has a `/hooks/payment/[identifier]_[provider]` API route out-of-the-box that allows you to listen to webhook events from third-party payment providers, where:

- `[identifier]` is the `identifier` static property defined in the payment provider. For example, `stripe`.
- `[provider]` is the ID of the provider. For example, `stripe`.

For example, when integrating basic Stripe payments with the [Stripe Module Provider](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/payment/payment-provider/stripe/index.html.md), the webhook listener route is `/hooks/payment/stripe_stripe`.

You can use this webhook listener when configuring webhook events in your third-party payment provider.

### getWebhookActionAndData Method

The webhook listener API route executes the [getWebhookActionAndData method](https://docs.medusajs.com/references/payment/getWebhookActionAndData/index.html.md) of the Payment Module's main service. This method delegates handling of incoming webhook events to the relevant payment provider.

Payment providers have a similar [getWebhookActionAndData method](https://docs.medusajs.com/references/payment/provider/index.html.md) to process the webhook event. So, if you're implementing a custom payment provider, make sure to implement it to handle webhook events.

![A diagram showcasing the steps of how the getWebhookActionAndData method words](https://res.cloudinary.com/dza7lstvk/image/upload/v1711567415/Medusa%20Resources/payment-webhook_seaocg.jpg)

If the `getWebhookActionAndData` method returns an `authorized` or `captured` action, the Medusa application will perform one of the following actions:

View the full flow of the webhook event processing in the [processPaymentWorkflow](https://docs.medusajs.com/references/medusa-workflows/processPaymentWorkflow/index.html.md) reference.

- If the method returns an `authorized` action, Medusa will set the associated payment session to `authorized`.
- If the method returns a `captured` action, Medusa will set the associated payment session to `captured`.
- In either cases, if the cart associated with the payment session is not completed yet, Medusa will complete the cart.


# Pricing Concepts

In this document, you’ll learn about the main concepts in the Pricing Module.

## Price Set

A [PriceSet](https://docs.medusajs.com/references/pricing/models/PriceSet/index.html.md) represents a collection of prices that are linked to a resource (for example, a product or a shipping option).

Each of these prices are represented by the [Price data module](https://docs.medusajs.com/references/pricing/models/Price/index.html.md).

![A diagram showcasing the relation between the price set and price](https://res.cloudinary.com/dza7lstvk/image/upload/v1709648650/Medusa%20Resources/price-set-money-amount_xeees0.jpg)

***

## Price List

A [PriceList](https://docs.medusajs.com/references/pricing/models/PriceList/index.html.md) is a group of prices only enabled if their conditions and rules are satisfied.

A price list has optional `start_date` and `end_date` properties that indicate the date range in which a price list can be applied.

Its associated prices are represented by the `Price` data model.


# Links between Pricing Module and Other Modules

This document showcases the module links defined between the Pricing Module and other Commerce Modules.

## Summary

The Pricing Module has the following links to other modules:

|First Data Model|Second Data Model|Type|Description|
|---|---|---|---|
|ShippingOption|PriceSet|Stored - one-to-one|Learn more|
|ProductVariant|PriceSet|Stored - one-to-one|Learn more|

***

## Fulfillment Module

The Fulfillment Module provides fulfillment-related functionalities, including shipping options that the customer chooses from when they place their order. However, it doesn't provide pricing-related functionalities for these options.

Medusa defines a link between the `PriceSet` and `ShippingOption` data models. A shipping option's price is stored as a price set.

![A diagram showcasing an example of how data models from the Pricing and Fulfillment modules are linked](https://res.cloudinary.com/dza7lstvk/image/upload/v1716561747/Medusa%20Resources/pricing-fulfillment_spywwa.jpg)

### Retrieve with Query

To retrieve the shipping option of a price set with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `shipping_option.*` in `fields`:

### query.graph

```ts
const { data: priceSets } = await query.graph({
  entity: "price_set",
  fields: [
    "shipping_option.*",
  ],
})

// priceSets[0].shipping_option
```

### useQueryGraphStep

```ts
import { useQueryGraphStep } from "@medusajs/medusa/core-flows"

// ...

const { data: priceSets } = useQueryGraphStep({
  entity: "price_set",
  fields: [
    "shipping_option.*",
  ],
})

// priceSets[0].shipping_option
```

### Manage with Link

To manage the price set of a shipping option, use [Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link/index.html.md):

### link.create

```ts
import { Modules } from "@medusajs/framework/utils"

// ...

await link.create({
  [Modules.FULFILLMENT]: {
    shipping_option_id: "so_123",
  },
  [Modules.PRICING]: {
    price_set_id: "pset_123",
  },
})
```

### createRemoteLinkStep

```ts
import { Modules } from "@medusajs/framework/utils"
import { createRemoteLinkStep } from "@medusajs/medusa/core-flows"

// ...

createRemoteLinkStep({
  [Modules.FULFILLMENT]: {
    shipping_option_id: "so_123",
  },
  [Modules.PRICING]: {
    price_set_id: "pset_123",
  },
})
```

***

## Product Module

The Product Module doesn't store or manage the prices of product variants.

Medusa defines a link between the `ProductVariant` and the `PriceSet`. A product variant’s prices are stored as prices belonging to a price set.

![A diagram showcasing an example of how data models from the Pricing and Product Module are linked. The PriceSet is linked to the ProductVariant of the Product Module.](https://res.cloudinary.com/dza7lstvk/image/upload/v1709651039/Medusa%20Resources/pricing-product_m4xaut.jpg)

So, when you want to add prices for a product variant, you create a price set and add the prices to it.

You can then benefit from adding rules to prices or using the `calculatePrices` method to retrieve the price of a product variant within a specified context.

### Retrieve with Query

To retrieve the variant of a price set with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `variant.*` in `fields`:

### query.graph

```ts
const { data: priceSets } = await query.graph({
  entity: "price_set",
  fields: [
    "variant.*",
  ],
})

// priceSets[0].variant
```

### useQueryGraphStep

```ts
import { useQueryGraphStep } from "@medusajs/medusa/core-flows"

// ...

const { data: priceSets } = useQueryGraphStep({
  entity: "price_set",
  fields: [
    "variant.*",
  ],
})

// priceSets[0].variant
```

### Manage with Link

To manage the price set of a variant, use [Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link/index.html.md):

### link.create

```ts
import { Modules } from "@medusajs/framework/utils"

// ...

await link.create({
  [Modules.PRODUCT]: {
    variant_id: "variant_123",
  },
  [Modules.PRICING]: {
    price_set_id: "pset_123",
  },
})
```

### createRemoteLinkStep

```ts
import { Modules } from "@medusajs/framework/utils"
import { createRemoteLinkStep } from "@medusajs/medusa/core-flows"

// ...

createRemoteLinkStep({
  [Modules.PRODUCT]: {
    variant_id: "variant_123",
  },
  [Modules.PRICING]: {
    price_set_id: "pset_123",
  },
})
```


# Prices Calculation

In this document, you'll learn how prices are calculated when you use the [calculatePrices method](https://docs.medusajs.com/references/pricing/calculatePrices/index.html.md) of the Pricing Module's main service.

## calculatePrices Method

The [calculatePrices method](https://docs.medusajs.com/references/pricing/calculatePrices/index.html.md) accepts as parameters the ID of one or more price sets and a context.

It returns a price object with the best matching price for each price set.

### Calculation Context

The calculation context is an optional object passed as a second parameter to the `calculatePrices` method. It accepts rules to restrict the selected prices in the price set.

For example:

```ts
const price = await pricingModuleService.calculatePrices(
  { id: [priceSetId] },
  {
    context: {
      currency_code: currencyCode,
      region_id: "reg_123",
    },
  }
)
```

In this example, you retrieve the prices in a price set for the specified currency code and region ID.

### Returned Price Object

For each price set, the `calculatePrices` method selects two prices:

- A calculated price: Either a price that belongs to a price list and best matches the specified context, or the same as the original price.
- An original price, which is either:
  - The same price as the calculated price if the price list it belongs to is of type `override`;
  - Or a price that doesn't belong to a price list and best matches the specified context.

Both prices are returned in an object that has the following properties:

- id: (\`string\`) The ID of the price set from which the price was selected.
- is\_calculated\_price\_price\_list: (\`boolean\`) Whether the calculated price belongs to a price list.
- calculated\_amount: (\`number\`) The amount of the calculated price, or \`null\` if there isn't a calculated price. This is the amount shown to the customer.
- is\_original\_price\_price\_list: (\`boolean\`) Whether the original price belongs to a price list.
- original\_amount: (\`number\`) The amount of the original price, or \`null\` if there isn't an original price. This amount is useful to compare with the \`calculated\_amount\`, such as to check for discounted value.
- currency\_code: (\`string\`) The currency code of the calculated price, or \`null\` if there isn't a calculated price.
- is\_calculated\_price\_tax\_inclusive: (\`boolean\`) Whether the calculated price is tax inclusive. Learn more about tax-inclusivity in \[this document]\(../tax-inclusive-pricing/page.mdx)
- is\_original\_price\_tax\_inclusive: (\`boolean\`) Whether the original price is tax inclusive. Learn more about tax-inclusivity in \[this document]\(../tax-inclusive-pricing/page.mdx)
- calculated\_price: (\`object\`) The calculated price's price details.

  - id: (\`string\`) The ID of the price.

  - price\_list\_id: (\`string\`) The ID of the associated price list.

  - price\_list\_type: (\`string\`) The price list's type. For example, \`sale\`.

  - min\_quantity: (\`number\`) The price's min quantity condition.

  - max\_quantity: (\`number\`) The price's max quantity condition.
- original\_price: (\`object\`) The original price's price details.

  - id: (\`string\`) The ID of the price.

  - price\_list\_id: (\`string\`) The ID of the associated price list.

  - price\_list\_type: (\`string\`) The price list's type. For example, \`sale\`.

  - min\_quantity: (\`number\`) The price's min quantity condition.

  - max\_quantity: (\`number\`) The price's max quantity condition.

***

## Examples

Consider the following price set:

```ts
const priceSet = await pricingModuleService.createPriceSets({
  prices: [
    // default price
    {
      amount: 500,
      currency_code: "EUR",
      rules: {},
    },
    // prices with rules
    {
      amount: 400,
      currency_code: "EUR",
      rules: {
        region_id: "reg_123",
      },
    },
    {
      amount: 450,
      currency_code: "EUR",
      rules: {
        city: "krakow",
      },
    },
    {
      amount: 500,
      currency_code: "EUR",
      rules: {
        city: "warsaw",
        region_id: "reg_123",
      },
    },
    {
      amount: 200,
      currency_code: "EUR",
      min_quantity: 100,
    },
  ],
})
```

### Default Price Selection

### Code

```ts
const price = await pricingModuleService.calculatePrices(
  { id: [priceSet.id] },
  {
    context: {
      currency_code: "EUR"
    }
  }
)
```

### Result

### Calculate Prices with Rules

### Code

```ts
const price = await pricingModuleService.calculatePrices(
  { id: [priceSet.id] },
  {
    context: {
      currency_code: "EUR",
      region_id: "reg_123",
      city: "krakow"
    }
  }
)
```

### Result

### Tiered Pricing Selection

### Code

```ts
const price = await pricingModuleService.calculatePrices(
  { id: [priceSet.id] },
  {
    context: {
      cart: {
        items: [
          {
            id: "item_1",
            quantity: 200,
            // assuming the price set belongs to this variant
            variant_id: "variant_1",
            // ...
          }
        ],
        // ...
      }
    }
  }
)
```

### Result

### Price Selection with Price List

### Code

```ts
const priceList = pricingModuleService.createPriceLists([{
  title: "Summer Price List",
  description: "Price list for summer sale",
  starts_at: Date.parse("01/10/2023").toString(),
  ends_at: Date.parse("31/10/2023").toString(),
  rules: {
    region_id: ['PL']
  },
  type: "sale",
  prices: [
    {
      amount: 400,
      currency_code: "EUR",
      price_set_id: priceSet.id,
    },
    {
      amount: 450,
      currency_code: "EUR",
      price_set_id: priceSet.id,
    },
  ],
}]);

const price = await pricingModuleService.calculatePrices(
  { id: [priceSet.id] },
  {
    context: {
      currency_code: "EUR",
      region_id: "PL",
      city: "krakow"
    }
  }
)
```

### Result


# Price Tiers and Rules

In this Pricing Module guide, you'll learn about tired prices, price rules for price sets and price lists, and how to add rules to a price.

## Tiered Pricing

Each price, represented by the [Price data model](https://docs.medusajs.com/references/pricing/models/Price/index.html.md), has two optional properties that can be used to create tiered prices:

- `min_quantity`: The minimum quantity that must be in the cart for the price to be applied.
- `max_quantity`: The maximum quantity that can be in the cart for the price to be applied.

This is useful to set tiered pricing for resources like product variants and shipping options.

For example, you can set a variant's price to:

- `$10` by default.
- `$8` when the customer adds `10` or more of the variant to the cart.
- `$6` when the customer adds `20` or more of the variant to the cart.

These price definitions would look like this:

```json title="Example Prices"
[
  // default price
  {
    "amount": 10,
    "currency_code": "usd",
  },
  {
    "amount": 8,
    "currency_code": "usd",
    "min_quantity": 10,
    "max_quantity": 19,
  },
  {
    "amount": 6,
    "currency_code": "usd",
    "min_quantity": 20,
  },
],
```

### How to Create Tiered Prices?

When you create prices, you can specify a `min_quantity` and `max_quantity` for each price. This allows you to create tiered pricing, where the price changes based on the quantity of items in the cart.

For example:

For most use cases where you're building customizations in the Medusa application, it's highly recommended to use [Medusa's workflows](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/medusa-workflows-reference/index.html.md) rather than using the Pricing Module directly. Medusa's workflows already implement extensive functionalities that you can re-use in your custom flows, with reliable roll-back mechanism.

### Using Medusa Workflows

```ts highlights={tieredPricingHighlights}
const { result } = await createProductsWorkflow(container)
  .run({
    input: {
      products: [{
        variants: [{
          id: "variant_1",
          prices: [
            // default price
            {
              amount: 10,
              currency_code: "usd",
            },
            {
              amount: 8,
              currency_code: "usd",
              min_quantity: 10,
              max_quantity: 19,
            },
            {
              amount: 6,
              currency_code: "usd",
              min_quantity: 20,
            },
          ],
          // ...
        }],
      }],
      // ...
    },
  })
```

### Using the Pricing Module

```ts
const priceSet = await pricingModule.addPrices({
  priceSetId: "pset_1",
  prices: [
    // default price
    {
      amount: 10,
      currency_code: "usd",
    },
    // tiered prices
    {
      amount: 8,
      currency_code: "usd",
      min_quantity: 10,
      max_quantity: 19,
    },
    {
      amount: 6,
      currency_code: "usd",
      min_quantity: 20,
    },
  ],
})
```

In this example, you create a product with a variant whose default price is `$10`. You also add two tiered prices that set the price to `$8` when the quantity is between `10` and `19`, and to `$6` when the quantity is `20` or more.

### How are Tiered Prices Applied?

The [price calculation](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/pricing/price-calculation/index.html.md) mechanism considers the cart's items as a context when choosing the best price to apply.

For example, consider the customer added the `variant_1` product variant (created in the workflow snippet of the [above section](#how-to-create-tiered-prices)) to their cart with a quantity of `15`.

The price calculation mechanism will choose the second price, which is `$8`, because the quantity of `15` is between `10` and `19`.

If there are other rules applied to the price, they may affect the price calculation. Keep reading to learn about other price rules, and refer to the [Price Calculation](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/pricing/price-calculation/index.html.md) guide for more details on the calculation mechanism.

***

## Price Rule

You can also restrict prices by advanced rules, such as a customer's group, zip code, or a cart's total.

Each rule of a price is represented by the [PriceRule data model](https://docs.medusajs.com/references/pricing/models/PriceRule/index.html.md).

The `Price` data model has a `rules_count` property, which indicates how many rules, represented by `PriceRule`, are applied to the price.

For exmaple, you create a price restricted to `10557` zip codes.

![A diagram showcasing the relation between the PriceRule and Price](https://res.cloudinary.com/dza7lstvk/image/upload/v1709648772/Medusa%20Resources/price-rule-1_vy8bn9.jpg)

A price can have multiple price rules.

For example, a price can be restricted by a region and a zip code.

![A diagram showcasing the relation between the PriceRule and Price with multiple rules.](https://res.cloudinary.com/dza7lstvk/image/upload/v1709649296/Medusa%20Resources/price-rule-3_pwpocz.jpg)

### Price List Rules

Rules applied to a price list are represented by the [PriceListRule data model](https://docs.medusajs.com/references/pricing/models/PriceListRule/index.html.md).

The `rules_count` property of a `PriceList` indicates how many rules are applied to it.

![A diagram showcasing the relation between the PriceSet, PriceList, Price, RuleType, and PriceListRuleValue](https://res.cloudinary.com/dza7lstvk/image/upload/v1709641999/Medusa%20Resources/price-list_zd10yd.jpg)

### How to Create Prices with Rules?

When you create prices, you can specify rules for each price. This allows you to create complex pricing strategies based on different contexts.

For example:

For most use cases where you're building customizations in the Medusa application, it's highly recommended to use [Medusa's workflows](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/medusa-workflows-reference/index.html.md) rather than using the Pricing Module directly. Medusa's workflows already implement extensive functionalities that you can re-use in your custom flows, with reliable roll-back mechanism.

### Using Medusa Workflows

```ts highlights={workflowHighlights}
const { result } = await createShippingOptionsWorkflow(container)
  .run({
    input: [{
      name: "Standard Shipping",
      service_zone_id: "serzo_123",
      shipping_profile_id: "sp_123",
      provider_id: "prov_123",
      type: {
        label: "Standard",
        description: "Standard shipping",
        code: "standard",
      },
      price_type: "flat",
      prices: [
        // default price
        {
          currency_code: "usd",
          amount: 10,
          rules: {},
        },
        // price if cart total >= $100
        {
          currency_code: "usd",
          amount: 0,
          rules: {
            item_total: {
              operator: "gte",
              value: 100,
            },
          },
        },
      ],
    }],
  })
```

### Using the Pricing Module

```ts
const priceSet = await pricingModule.addPrices({
  priceSetId: "pset_1",
  prices: [
    // default price
    {
      currency_code: "usd",
      amount: 10,
      rules: {},
    },
    // price if cart total >= $100
    {
      currency_code: "usd",
      amount: 0,
      rules: {
        item_total: {
          operator: "gte",
          value: 100,
        },
      },
    },
  ],
})
```

In this example, you create a shipping option whose default price is `$10`. When the total of the cart or order using this shipping option is greater than `$100`, the shipping option's price becomes free.

### How is the Price Rule Applied?

The [price calculation](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/pricing/price-calculation/index.html.md) mechanism considers a price applicable when the resource that this price is in matches the specified rules.

For example, a [cart object](https://docs.medusajs.com/api/store#carts_cart_schema) has an `item_total` property. So, if a shipping option has the following price:

```json
{
  "currency_code": "usd",
  "amount": 0,
  "rules": {
    "item_total": {
      "operator": "gte",
      "value": 100,
    }
  }
}
```

The shipping option's price is applied when the cart's `item_total` is greater than or equal to `$100`.

You can also apply the rule on nested relations and properties. For example, to apply a shipping option's price based on the customer's group, you can apply a rule on the `customer.group.id` attribute:

```json
{
  "currency_code": "usd",
  "amount": 0,
  "rules": {
    "customer.group.id": {
      "operator": "eq",
      "value": "cusgrp_123"
    }
  }
}
```

In this example, the price is only applied if a cart's customer belongs to the customer group of ID `cusgrp_123`.

These same rules apply to product variant prices as well, or any other resource that has a price.


# Links between Product Module and Other Modules

This document showcases the module links defined between the Product Module and other Commerce Modules.

## Summary

The Product Module has the following links to other modules:

Read-only links are used to query data across modules, but the relations aren't stored in a pivot table in the database.

|First Data Model|Second Data Model|Type|Description|
|---|---|---|---|
|LineItem|Product|Read-only - has one|Learn more|
|Product|ShippingProfile|Stored - many-to-one|Learn more|
|ProductVariant|InventoryItem|Stored - many-to-many|Learn more|
|OrderLineItem|Product|Read-only - has one|Learn more|
|ProductVariant|PriceSet|Stored - one-to-one|Learn more|
|Product|SalesChannel|Stored - many-to-many|Learn more|

***

## Cart Module

Medusa defines read-only links between:

- The [Cart Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/cart/index.html.md)'s `LineItem` data model and the `Product` data model. Because the link is read-only from the `LineItem`'s side, you can only retrieve the product of a line item, and not the other way around.
- The `ProductVariant` data model and the [Cart Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/cart/index.html.md)'s `LineItem` data model. Because the link is read-only from the `LineItem`'s side, you can only retrieve the variant of a line item, and not the other way around.

### Retrieve with Query

To retrieve the variant of a line item with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `variant.*` in `fields`:

To retrieve the product, pass `product.*` in `fields`.

### query.graph

```ts
const { data: lineItems } = await query.graph({
  entity: "line_item",
  fields: [
    "variant.*",
  ],
})

// lineItems[0].variant
```

### useQueryGraphStep

```ts
import { useQueryGraphStep } from "@medusajs/medusa/core-flows"

// ...

const { data: lineItems } = useQueryGraphStep({
  entity: "line_item",
  fields: [
    "variant.*",
  ],
})

// lineItems[0].variant
```

***

## Fulfillment Module

Medusa defines a link between the `Product` data model and the `ShippingProfile` data model of the Fulfillment Module. Each product must belong to a shipping profile.

This link is introduced in [Medusa v2.5.0](https://github.com/medusajs/medusa/releases/tag/v2.5.0).

### Retrieve with Query

To retrieve the shipping profile of a product with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `shipping_profile.*` in `fields`:

### query.graph

```ts
const { data: products } = await query.graph({
  entity: "product",
  fields: [
    "shipping_profile.*",
  ],
})

// products[0].shipping_profile
```

### useQueryGraphStep

```ts
import { useQueryGraphStep } from "@medusajs/medusa/core-flows"

// ...

const { data: products } = useQueryGraphStep({
  entity: "product",
  fields: [
    "shipping_profile.*",
  ],
})

// products[0].shipping_profile
```

### Manage with Link

To manage the shipping profile of a product, use [Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link/index.html.md):

### link.create

```ts
import { Modules } from "@medusajs/framework/utils"

// ...

await link.create({
  [Modules.PRODUCT]: {
    product_id: "prod_123",
  },
  [Modules.FULFILLMENT]: {
    shipping_profile_id: "sp_123",
  },
})
```

### createRemoteLinkStep

```ts
import { Modules } from "@medusajs/framework/utils"
import { createRemoteLinkStep } from "@medusajs/medusa/core-flows"

// ...

createRemoteLinkStep({
  [Modules.PRODUCT]: {
    product_id: "prod_123",
  },
  [Modules.FULFILLMENT]: {
    shipping_profile_id: "sp_123",
  },
})
```

***

## Inventory Module

The Inventory Module provides inventory-management features for any stock-kept item.

Medusa defines a link between the `ProductVariant` and `InventoryItem` data models. Each product variant has different inventory details.

![A diagram showcasing an example of how data models from the Product and Inventory modules are linked.](https://res.cloudinary.com/dza7lstvk/image/upload/v1709652779/Medusa%20Resources/product-inventory_kmjnud.jpg)

When the `manage_inventory` property of a product variant is enabled, you can manage the variant's inventory in different locations through this relation.

Learn more about product variant's inventory management in [this guide](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/product/variant-inventory/index.html.md).

### Retrieve with Query

To retrieve the inventory items of a product variant with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `inventory_items.*` in `fields`:

### query.graph

```ts
const { data: variants } = await query.graph({
  entity: "variant",
  fields: [
    "inventory_items.*",
  ],
})

// variants[0].inventory_items
```

### useQueryGraphStep

```ts
import { useQueryGraphStep } from "@medusajs/medusa/core-flows"

// ...

const { data: variants } = useQueryGraphStep({
  entity: "variant",
  fields: [
    "inventory_items.*",
  ],
})

// variants[0].inventory_items
```

### Manage with Link

To manage the inventory items of a variant, use [Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link/index.html.md):

### link.create

```ts
import { Modules } from "@medusajs/framework/utils"

// ...

await link.create({
  [Modules.PRODUCT]: {
    variant_id: "variant_123",
  },
  [Modules.INVENTORY]: {
    inventory_item_id: "iitem_123",
  },
})
```

### createRemoteLinkStep

```ts
import { Modules } from "@medusajs/framework/utils"
import { createRemoteLinkStep } from "@medusajs/medusa/core-flows"

// ...

createRemoteLinkStep({
  [Modules.PRODUCT]: {
    variant_id: "variant_123",
  },
  [Modules.INVENTORY]: {
    inventory_item_id: "iitem_123",
  },
})
```

***

## Order Module

Medusa defines read-only links between:

- the [Order Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/order/index.html.md)'s `OrderLineItem` data model and the `Product` data model. Because the link is read-only from the `OrderLineItem`'s side, you can only retrieve the product of an order line item, and not the other way around.
- the [Order Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/order/index.html.md)'s `OrderLineItem` data model and the `ProductVariant` data model. Because the link is read-only from the `OrderLineItem`'s side, you can only retrieve the variant of an order line item, and not the other way around.

### Retrieve with Query

To retrieve the variant of a line item with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `variant.*` in `fields`:

To retrieve the product, pass `product.*` in `fields`.

### query.graph

```ts
const { data: lineItems } = await query.graph({
  entity: "order_line_item",
  fields: [
    "variant.*",
  ],
})

// lineItems[0].variant
```

### useQueryGraphStep

```ts
import { useQueryGraphStep } from "@medusajs/medusa/core-flows"

// ...

const { data: lineItems } = useQueryGraphStep({
  entity: "order_line_item",
  fields: [
    "variant.*",
  ],
})

// lineItems[0].variant
```

***

## Pricing Module

The Product Module doesn't provide pricing-related features.

Instead, Medusa defines a link between the `ProductVariant` and the `PriceSet` data models. A product variant’s prices are stored belonging to a price set.

![A diagram showcasing an example of how data models from the Pricing and Product Module are linked.](https://res.cloudinary.com/dza7lstvk/image/upload/v1709651464/Medusa%20Resources/product-pricing_vlxsiq.jpg)

So, to add prices for a product variant, create a price set and add the prices to it.

### Retrieve with Query

To retrieve the price set of a variant with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `price_set.*` in `fields`:

### query.graph

```ts
const { data: variants } = await query.graph({
  entity: "variant",
  fields: [
    "price_set.*",
  ],
})

// variants[0].price_set
```

### useQueryGraphStep

```ts
import { useQueryGraphStep } from "@medusajs/medusa/core-flows"

// ...

const { data: variants } = useQueryGraphStep({
  entity: "variant",
  fields: [
    "price_set.*",
  ],
})

// variants[0].price_set
```

### Manage with Link

To manage the price set of a variant, use [Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link/index.html.md):

### link.create

```ts
import { Modules } from "@medusajs/framework/utils"

// ...

await link.create({
  [Modules.PRODUCT]: {
    variant_id: "variant_123",
  },
  [Modules.PRICING]: {
    price_set_id: "pset_123",
  },
})
```

### createRemoteLinkStep

```ts
import { Modules } from "@medusajs/framework/utils"
import { createRemoteLinkStep } from "@medusajs/medusa/core-flows"

// ...

createRemoteLinkStep({
  [Modules.PRODUCT]: {
    variant_id: "variant_123",
  },
  [Modules.PRICING]: {
    price_set_id: "pset_123",
  },
})
```

***

## Sales Channel Module

The Sales Channel Module provides functionalities to manage multiple selling channels in your store.

Medusa defines a link between the `Product` and `SalesChannel` data models. A product can have different availability in different sales channels.

![A diagram showcasing an example of how data models from the Product and Sales Channel modules are linked.](https://res.cloudinary.com/dza7lstvk/image/upload/v1709651840/Medusa%20Resources/product-sales-channel_t848ik.jpg)

### Retrieve with Query

To retrieve the sales channels of a product with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `sales_channels.*` in `fields`:

### query.graph

```ts
const { data: products } = await query.graph({
  entity: "product",
  fields: [
    "sales_channels.*",
  ],
})

// products[0].sales_channels
```

### useQueryGraphStep

```ts
import { useQueryGraphStep } from "@medusajs/medusa/core-flows"

// ...

const { data: products } = useQueryGraphStep({
  entity: "product",
  fields: [
    "sales_channels.*",
  ],
})

// products[0].sales_channels
```

### Manage with Link

To manage the sales channels of a product, use [Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link/index.html.md):

### link.create

```ts
import { Modules } from "@medusajs/framework/utils"

// ...

await link.create({
  [Modules.PRODUCT]: {
    product_id: "prod_123",
  },
  [Modules.SALES_CHANNEL]: {
    sales_channel_id: "sc_123",
  },
})
```

### createRemoteLinkStep

```ts
import { Modules } from "@medusajs/framework/utils"
import { createRemoteLinkStep } from "@medusajs/medusa/core-flows"

// ...

createRemoteLinkStep({
  [Modules.PRODUCT]: {
    product_id: "prod_123",
  },
  [Modules.SALES_CHANNEL]: {
    sales_channel_id: "sc_123",
  },
})
```


# Product Variant Inventory

# Product Variant Inventory

In this guide, you'll learn about the inventory management features related to product variants.

Refer to this [Medusa Admin User Guide](https://docs.medusajs.com/user-guide/products/variants#manage-product-variant-inventory/index.html.md) to learn how to manage inventory of product variants.

## Configure Inventory Management of Product Variants

A product variant, represented by the [ProductVariant](https://docs.medusajs.com/references/product/models/ProductVariant/index.html.md) data model, has a `manage_inventory` field that's disabled by default. This field indicates whether you'll manage the inventory quantity of the product variant in the Medusa application. You can also keep `manage_inventory` disabled if you manage the product's inventory in an external system, such as an ERP.

The Product Module doesn't provide inventory-management features. Instead, the Medusa application uses the [Inventory Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/inventory/index.html.md) to manage inventory for products and variants. When `manage_inventory` is disabled, the Medusa application always considers the product variant to be in stock. This is useful if your product's variants aren't items that can be stocked, such as digital products, or they don't have a limited stock quantity.

When `manage_inventory` is enabled, the Medusa application tracks the inventory of the product variant using the [Inventory Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/inventory/index.html.md). For example, when a customer purchases a product variant, the Medusa application decrements the stocked quantity of the product variant.

***

## How the Medusa Application Manages Inventory

When a product variant has `manage_inventory` enabled, the Medusa application creates an inventory item using the [Inventory Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/inventory/index.html.md) and links it to the product variant.

![Diagram showcasing the link between a product variant and its inventory item](https://res.cloudinary.com/dza7lstvk/image/upload/v1709652779/Medusa%20Resources/product-inventory_kmjnud.jpg)

The inventory item has one or more locations, called inventory levels, that represent the stock quantity of the product variant at a specific location. This allows you to manage inventory across multiple warehouses, such as a warehouse in the US and another in Europe.

![Diagram showcasing the link between a variant and its inventory item, and the inventory item's level.](https://res.cloudinary.com/dza7lstvk/image/upload/v1738580390/Medusa%20Resources/variant-inventory-level_bbee2t.jpg)

Learn more about inventory concepts in the [Inventory Module's documentation](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/inventory/concepts/index.html.md).

The Medusa application represents and manages stock locations using the [Stock Location Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/stock-location/index.html.md). It creates a read-only link between the `InventoryLevel` and `StockLocation` data models so that it can retrieve the stock location of an inventory level.

![Diagram showcasing the read-only link between an inventory level and a stock location](https://res.cloudinary.com/dza7lstvk/image/upload/v1738582163/Medusa%20Resources/inventory-level-stock_amxfg5.jpg)

Learn more about the Stock Location Module in the [Stock Location Module's documentation](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/stock-location/concepts/index.html.md).

### Product Inventory in Storefronts

When a storefront sends a request to the Medusa application, it must always pass a [publishable API key](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/sales-channel/publishable-api-keys/index.html.md) in the request header. This API key specifies the sales channels, available through the [Sales Channel Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/sales-channel/index.html.md), of the storefront.

The Medusa application links sales channels to stock locations, indicating the locations available for a specific sales channel. So, all inventory-related operations are scoped by the sales channel and its associated stock locations.

For example, the availability of a product variant is determined by the `stocked_quantity` of its inventory level at the stock location linked to the storefront's sales channel.

![Diagram showcasing the overall relations between inventory, stock location, and sales channel concepts](https://res.cloudinary.com/dza7lstvk/image/upload/v1738582163/Medusa%20Resources/inventory-stock-sales_fknoxw.jpg)

***

## Variant Back Orders

Product variants have an `allow_backorder` field that's disabled by default. When enabled, the Medusa application allows customers to purchase the product variant even when it's out of stock. Use this when your product variant is available through on-demand or pre-order purchase.

You can also allow customers to subscribe to restock notifications of a product variant as explained in [this guide](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/recipes/commerce-automation/restock-notification/index.html.md).

***

## Additional Resources

The following guides provide more details on inventory management in the Medusa application:

- [Inventory Kits in the Inventory Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/inventory/inventory-kit/index.html.md): Learn how you can implement bundled or multi-part products through the Inventory Module.
- [Retrieve Product Variant Inventory Quantity](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/product/guides/variant-inventory/index.html.md): Learn how to retrieve the available inventory quantity of a product variant.
- [Configure Selling Products](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/product/selling-products/index.html.md): Learn how to use inventory management to support different use cases when selling products.
- [Inventory in Flows](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/inventory/inventory-in-flows/index.html.md): Learn how Medusa utilizes inventory management in different flows.
- [Storefront guide: how to retrieve a product variant's inventory details](https://docs.medusajs.com/resources/storefront-development/products/inventory/index.html.md).


# Configure Selling Products

In this guide, you'll learn how to set up and configure your products based on their shipping and inventory requirements, the product type, how you want to sell them, or your commerce ecosystem.

The concepts in this guide are applicable starting from Medusa v2.5.1.

## Scenario

Businesses can have different selling requirements:

1. They may sell physical or digital items.
2. They may sell items that don't require shipping or inventory management, such as selling digital products, services, or booking appointments.
3. They may sell items whose inventory is managed by an external system, such as an ERP.

Medusa supports these different selling requirements by allowing you to configure shipping and inventory requirements for products and their variants. This guide explains how these configurations work, then provides examples of setting up different use cases.

***

## Configuring Shipping Requirements

The Medusa application defines a link between the `Product` data model and a [ShippingProfile](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/fulfillment/concepts#shipping-profile/index.html.md) in the [Fulfillment Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/fulfillment/index.html.md), allowing you to associate a product with a shipping profile.

When a product is associated with a shipping profile, its variants require shipping and fulfillment when purchased. This is useful for physical products or digital products that require custom fulfillment.

If a product doesn't have an associated shipping profile, its variants don't require shipping and fulfillment when purchased. This is useful for digital products, for example, that don't require shipping.

### Overriding Shipping Requirements for Variants

A product variant whose inventory is managed by Medusa (its `manage_inventory` property is enabled) has an [inventory item](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/inventory/concepts#inventoryitem/index.html.md). The inventory item has a `requires_shipping` property that can be used to override its shipping requirement. This is useful if the product has an associated shipping profile but you want to disable shipping for a specific variant, or vice versa.

Learn more about product variant's inventory in [this guide](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/product/variant-inventory/index.html.md).

When a product variant is purchased, the Medusa application decides whether the purchased item requires shipping in the following order:

1. The product variant has an inventory item. In this case, the Medusa application uses the inventory item's `requires_shipping` property to determine if the item requires shipping.
2. If the product variant doesn't have an inventory item, the Medusa application checks whether the product has an associated shipping profile to determine if the item requires shipping.

***

## Use Case Examples

By combining configurations of shipment requirements and inventory management, you can set up your products to support your use case:

|Use Case|Configurations|Example|
|---|---|---|---|---|
|Item that's shipped on purchase, and its variant inventory is managed by the Medusa application.||Any stock-kept item (clothing, for example), whose inventory is managed in the Medusa application.|
|Item that's shipped on purchase, but its variant inventory is managed externally (not by Medusa) or it has infinite stock.||Any stock-kept item (clothing, for example), whose inventory is managed in an ERP or has infinite stock.|
|Item that's not shipped on purchase, but its variant inventory is managed by Medusa.||Digital products, such as licenses, that don't require shipping but have a limited quantity.|
|Item that doesn't require shipping and its variant inventory isn't managed by Medusa.|||


# Links between Region Module and Other Modules

This document showcases the module links defined between the Region Module and other Commerce Modules.

## Summary

The Region Module has the following links to other modules:

Read-only links are used to query data across modules, but the relations aren't stored in a pivot table in the database.

|First Data Model|Second Data Model|Type|Description|
|---|---|---|---|
|Cart|Region|Read-only - has one|Learn more|
|Order|Region|Read-only - has one|Learn more|
|Region|PaymentProvider|Stored - many-to-many|Learn more|

***

## Cart Module

Medusa defines a read-only link between the [Cart Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/cart/index.html.md)'s `Cart` data model and the `Region` data model. Because the link is read-only from the `Cart`'s side, you can only retrieve the region of a cart, and not the other way around.

### Retrieve with Query

To retrieve the region of a cart with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `region.*` in `fields`:

### query.graph

```ts
const { data: carts } = await query.graph({
  entity: "cart",
  fields: [
    "region.*",
  ],
})

// carts[0].region
```

### useQueryGraphStep

```ts
import { useQueryGraphStep } from "@medusajs/medusa/core-flows"

// ...

const { data: carts } = useQueryGraphStep({
  entity: "cart",
  fields: [
    "region.*",
  ],
})

// carts[0].region
```

***

## Order Module

Medusa defines a read-only link between the [Order Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/order/index.html.md)'s `Order` data model and the `Region` data model. Because the link is read-only from the `Order`'s side, you can only retrieve the region of an order, and not the other way around.

### Retrieve with Query

To retrieve the region of an order with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `region.*` in `fields`:

### query.graph

```ts
const { data: orders } = await query.graph({
  entity: "order",
  fields: [
    "region.*",
  ],
})

// orders[0].region
```

### useQueryGraphStep

```ts
import { useQueryGraphStep } from "@medusajs/medusa/core-flows"

// ...

const { data: orders } = useQueryGraphStep({
  entity: "order",
  fields: [
    "region.*",
  ],
})

// orders[0].region
```

***

## Payment Module

You can specify for each region which payment providers are available for use.

Medusa defines a module link between the `PaymentProvider` and the `Region` data models.

![A diagram showcasing an example of how resources from the Payment and Region modules are linked](https://res.cloudinary.com/dza7lstvk/image/upload/v1711569520/Medusa%20Resources/payment-region_jyo2dz.jpg)

### Retrieve with Query

To retrieve the payment providers of a region with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `payment_providers.*` in `fields`:

### query.graph

```ts
const { data: regions } = await query.graph({
  entity: "region",
  fields: [
    "payment_providers.*",
  ],
})

// regions[0].payment_providers
```

### useQueryGraphStep

```ts
import { useQueryGraphStep } from "@medusajs/medusa/core-flows"

// ...

const { data: regions } = useQueryGraphStep({
  entity: "region",
  fields: [
    "payment_providers.*",
  ],
})

// regions[0].payment_providers
```

### Manage with Link

To manage the payment providers in a region, use [Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link/index.html.md):

### link.create

```ts
import { Modules } from "@medusajs/framework/utils"

// ...

await link.create({
  [Modules.REGION]: {
    region_id: "reg_123",
  },
  [Modules.PAYMENT]: {
    payment_provider_id: "pp_stripe_stripe",
  },
})
```

### createRemoteLinkStep

```ts
import { Modules } from "@medusajs/framework/utils"
import { createRemoteLinkStep } from "@medusajs/medusa/core-flows"

// ...

createRemoteLinkStep({
  [Modules.REGION]: {
    region_id: "reg_123",
  },
  [Modules.PAYMENT]: {
    payment_provider_id: "pp_stripe_stripe",
  },
})
```


# Links between Sales Channel Module and Other Modules

This document showcases the module links defined between the Sales Channel Module and other Commerce Modules.

## Summary

The Sales Channel Module has the following links to other modules:

Read-only links are used to query data across modules, but the relations aren't stored in a pivot table in the database.

|First Data Model|Second Data Model|Type|Description|
|---|---|---|---|
|ApiKey|SalesChannel|Stored - many-to-many|Learn more|
|Cart|SalesChannel|Read-only - has one|Learn more|
|Order|SalesChannel|Read-only - has one|Learn more|
|Product|SalesChannel|Stored - many-to-many|Learn more|
|SalesChannel|StockLocation|Stored - many-to-many|Learn more|

***

## API Key Module

A publishable API key allows you to easily specify the sales channel scope in a client request.

Medusa defines a link between the `ApiKey` and the `SalesChannel` data models.

![A diagram showcasing an example of how resources from the Sales Channel and API Key modules are linked](https://res.cloudinary.com/dza7lstvk/image/upload/v1709812064/Medusa%20Resources/sales-channel-api-key_zmqi2l.jpg)

### Retrieve with Query

To retrieve the API keys associated with a sales channel with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `publishable_api_keys.*` in `fields`:

### query.graph

```ts
const { data: salesChannels } = await query.graph({
  entity: "sales_channel",
  fields: [
    "publishable_api_keys.*",
  ],
})

// salesChannels[0].publishable_api_keys
```

### useQueryGraphStep

```ts
import { useQueryGraphStep } from "@medusajs/medusa/core-flows"

// ...

const { data: salesChannels } = useQueryGraphStep({
  entity: "sales_channel",
  fields: [
    "publishable_api_keys.*",
  ],
})

// salesChannels[0].publishable_api_keys
```

### Manage with Link

To manage the sales channels of an API key, use [Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link/index.html.md):

### link.create

```ts
import { Modules } from "@medusajs/framework/utils"

// ...

await link.create({
  [Modules.API_KEY]: {
    publishable_key_id: "apk_123",
  },
  [Modules.SALES_CHANNEL]: {
    sales_channel_id: "sc_123",
  },
})
```

### createRemoteLinkStep

```ts
import { Modules } from "@medusajs/framework/utils"
import { createRemoteLinkStep } from "@medusajs/medusa/core-flows"

// ...

createRemoteLinkStep({
  [Modules.API_KEY]: {
    publishable_key_id: "apk_123",
  },
  [Modules.SALES_CHANNEL]: {
    sales_channel_id: "sc_123",
  },
})
```

***

## Cart Module

Medusa defines a read-only link between the [Cart Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/cart/index.html.md)'s `Cart` data model and the `SalesChannel` data model. Because the link is read-only from the `Cart`'s side, you can only retrieve the sales channel of a cart, and not the other way around.

### Retrieve with Query

To retrieve the sales channel of a cart with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `sales_channel.*` in `fields`:

### query.graph

```ts
const { data: carts } = await query.graph({
  entity: "cart",
  fields: [
    "sales_channel.*",
  ],
})

// carts[0].sales_channel
```

### useQueryGraphStep

```ts
import { useQueryGraphStep } from "@medusajs/medusa/core-flows"

// ...

const { data: carts } = useQueryGraphStep({
  entity: "cart",
  fields: [
    "sales_channel.*",
  ],
})

// carts[0].sales_channel
```

***

## Order Module

Medusa defines a read-only link between the [Order Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/order/index.html.md)'s `Order` data model and the `SalesChannel` data model. Because the link is read-only from the `Order`'s side, you can only retrieve the sales channel of an order, and not the other way around.

### Retrieve with Query

To retrieve the sales channel of an order with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `sales_channel.*` in `fields`:

### query.graph

```ts
const { data: orders } = await query.graph({
  entity: "order",
  fields: [
    "sales_channel.*",
  ],
})

// orders.sales_channel
```

### useQueryGraphStep

```ts
import { useQueryGraphStep } from "@medusajs/medusa/core-flows"

// ...

const { data: orders } = useQueryGraphStep({
  entity: "order",
  fields: [
    "sales_channel.*",
  ],
})

// orders.sales_channel
```

***

## Product Module

A product has different availability for different sales channels. Medusa defines a link between the `Product` and the `SalesChannel` data models.

![A diagram showcasing an example of how resources from the Sales Channel and Product modules are linked](https://res.cloudinary.com/dza7lstvk/image/upload/v1709809833/Medusa%20Resources/product-sales-channel_t848ik.jpg)

A product can be available in more than one sales channel. You can retrieve only the products of a sales channel.

### Retrieve with Query

To retrieve the products of a sales channel with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `products.*` in `fields`:

### query.graph

```ts
const { data: salesChannels } = await query.graph({
  entity: "sales_channel",
  fields: [
    "products.*",
  ],
})

// salesChannels[0].products
```

### useQueryGraphStep

```ts
import { useQueryGraphStep } from "@medusajs/medusa/core-flows"

// ...

const { data: salesChannels } = useQueryGraphStep({
  entity: "sales_channel",
  fields: [
    "products.*",
  ],
})

// salesChannels[0].products
```

### Manage with Link

To manage the sales channels of a product, use [Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link/index.html.md):

### link.create

```ts
import { Modules } from "@medusajs/framework/utils"

// ...

await link.create({
  [Modules.PRODUCT]: {
    product_id: "prod_123",
  },
  [Modules.SALES_CHANNEL]: {
    sales_channel_id: "sc_123",
  },
})
```

### createRemoteLinkStep

```ts
import { Modules } from "@medusajs/framework/utils"
import { createRemoteLinkStep } from "@medusajs/medusa/core-flows"

// ...

createRemoteLinkStep({
  [Modules.PRODUCT]: {
    product_id: "prod_123",
  },
  [Modules.SALES_CHANNEL]: {
    sales_channel_id: "sc_123",
  },
})
```

***

## Stock Location Module

A stock location is associated with a sales channel. This scopes inventory quantities associated with that stock location by the associated sales channel.

Medusa defines a link between the `SalesChannel` and `StockLocation` data models.

![A diagram showcasing an example of how resources from the Sales Channel and Stock Location modules are linked](https://res.cloudinary.com/dza7lstvk/image/upload/v1716796872/Medusa%20Resources/sales-channel-location_cqrih1.jpg)

### Retrieve with Query

To retrieve the stock locations of a sales channel with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `stock_locations.*` in `fields`:

### query.graph

```ts
const { data: salesChannels } = await query.graph({
  entity: "sales_channel",
  fields: [
    "stock_locations.*",
  ],
})

// salesChannels[0].stock_locations
```

### useQueryGraphStep

```ts
import { useQueryGraphStep } from "@medusajs/medusa/core-flows"

// ...

const { data: salesChannels } = useQueryGraphStep({
  entity: "sales_channel",
  fields: [
    "stock_locations.*",
  ],
})

// salesChannels[0].stock_locations
```

### Manage with Link

To manage the stock locations of a sales channel, use [Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link/index.html.md):

### link.create

```ts
import { Modules } from "@medusajs/framework/utils"

// ...

await link.create({
  [Modules.SALES_CHANNEL]: {
    sales_channel_id: "sc_123",
  },
  [Modules.STOCK_LOCATION]: {
    sales_channel_id: "sloc_123",
  },
})
```

### createRemoteLinkStep

```ts
import { Modules } from "@medusajs/framework/utils"
import { createRemoteLinkStep } from "@medusajs/medusa/core-flows"

// ...

createRemoteLinkStep({
  [Modules.SALES_CHANNEL]: {
    sales_channel_id: "sc_123",
  },
  [Modules.STOCK_LOCATION]: {
    sales_channel_id: "sloc_123",
  },
})
```


# Publishable API Keys with Sales Channels

In this document, you’ll learn what publishable API keys are and how to use them with sales channels.

## Publishable API Keys with Sales Channels

A publishable API key, provided by the API Key Module, is a client key scoped to one or more sales channels.

When sending a request to a Store API route, you must pass a publishable API key in the header of the request:

```bash
curl http://localhost:9000/store/products \
  x-publishable-api-key: {your_publishable_api_key}
```

The Medusa application infers the associated sales channels and ensures that only data relevant to the sales channel are used.

***

## How to Create a Publishable API Key?

To create a publishable API key, either use the [Medusa Admin](https://docs.medusajs.com/user-guide/settings/developer/publishable-api-keys/index.html.md) or the [Admin API Routes](https://docs.medusajs.com/api/admin#publishable-api-keys).

***

## Access Sales Channels in Custom Store API Routes

If you create an API route under the `/store` prefix, you can access the sales channels associated with the request's publishable API key using the `publishable_key_context` property of the request object.

For example:

```ts
import { MedusaStoreRequest, MedusaResponse } from "@medusajs/framework/http"
import { getVariantAvailability } from "@medusajs/framework/utils"

export async function GET(
  req: MedusaStoreRequest,
  res: MedusaResponse
) {
  const query = req.scope.resolve("query")
  const sales_channel_ids = req.publishable_key_context.sales_channel_ids

  res.json({
    sales_channel_id: sales_channel_ids[0],
  })
}
```

In this example, you retrieve the scope's sales channel IDs using `req.publishable_key_context.sales_channel_ids`, whose value is an array of IDs.

You can then use these IDs based on your business logic. For example, you can retrieve the sales channels' details using [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md).

Notice that the request object's type is `MedusaStoreRequest` instead of `MedusaRequest` to ensure the availability of the `publishable_key_context` property.


# Tax-Inclusive Pricing

In this document, you’ll learn about tax-inclusive pricing and how it's used when calculating prices.

## What is Tax-Inclusive Pricing?

A tax-inclusive price is a price of a resource that includes taxes. Medusa calculates the tax amount from the price rather than adds the amount to it.

For example, if a product’s price is $50, the tax rate is 2%, and tax-inclusive pricing is enabled, then the product's price is $49, and the applied tax amount is $1.

***

## How is Tax-Inclusive Pricing Set?

The [PricePreference data model](https://docs.medusajs.com/references/pricing/models/PricePreference/index.html.md) holds the tax-inclusive setting for a context. It has two properties that indicate the context:

- `attribute`: The name of the attribute to compare against. For example, `region_id` or `currency_code`.
- `value`: The attribute’s value. For example, `reg_123` or `usd`.

Only `region_id` and `currency_code` are supported as an `attribute` at the moment.

The `is_tax_inclusive` property indicates whether tax-inclusivity is enabled in the specified context.

For example:

```json
{
  "attribute": "currency_code",
  "value": "USD",
  "is_tax_inclusive": true,
}
```

In this example, tax-inclusivity is enabled for the `USD` currency code.

***

## Tax-Inclusive Pricing in Price Calculation

### Tax Context

As mentioned in the [Price Calculation documentation](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/pricing/price-calculation#calculation-context/index.html.md), The `calculatePrices` method accepts as a parameter a calculation context.

To get accurate tax results, pass the `region_id` and / or `currency_code` in the calculation context.

### Returned Tax Properties

The `calculatePrices` method returns two properties related to tax-inclusivity:

Learn more about the returned properties in [this guide](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/pricing/price-calculation#returned-price-object/index.html.md).

- `is_calculated_price_tax_inclusive`: Whether the selected `calculated_price` is tax-inclusive.
- `is_original_price_tax_inclusive` : Whether the selected `original_price` is tax-inclusive.

A price is considered tax-inclusive if:

1. It belongs to the region or currency code specified in the calculation context;
2. and the region or currency code has a price preference with `is_tax_inclusive` enabled.

### Tax Context Precedence

A region’s price preference’s `is_tax_inclusive`'s value takes higher precedence in determining whether a price is tax-inclusive if:

- both the `region_id` and `currency_code` are provided in the calculation context;
- the selected price belongs to the region;
- and the region has a price preference


# Promotion Actions

In this document, you’ll learn about promotion actions and how they’re computed using the [computeActions method](https://docs.medusajs.com/references/promotion/computeActions/index.html.md).

## computeActions Method

The Promotion Module's main service has a [computeActions method](https://docs.medusajs.com/references/promotion/computeActions/index.html.md) that returns an array of actions to perform on a cart when one or more promotions are applied.

Actions inform you what adjustment must be made to a cart item or shipping method. Each action is an object having the `action` property indicating the type of action.

***

## Action Types

### `addItemAdjustment` Action

The `addItemAdjustment` action indicates that an adjustment must be made to an item. For example, removing $5 off its amount.

This action has the following format:

```ts
export interface AddItemAdjustmentAction {
  action: "addItemAdjustment"
  item_id: string
  amount: number
  code: string
  description?: string
}
```

This action means that a new record should be created of the `LineItemAdjustment` data model in the Cart Module, or `OrderLineItemAdjustment` data model in the Order Module.

Refer to [this reference](https://docs.medusajs.com/references/promotion/interfaces/promotion.AddItemAdjustmentAction/index.html.md) for details on the object’s properties.

### `removeItemAdjustment` Action

The `removeItemAdjustment` action indicates that an adjustment must be removed from a line item. For example, remove the $5 discount.

The `computeActions` method accepts any previous item adjustments in the `items` property of the second parameter.

This action has the following format:

```ts
export interface RemoveItemAdjustmentAction {
  action: "removeItemAdjustment"
  adjustment_id: string
  description?: string
  code: string
}
```

This action means that a new record should be removed of the `LineItemAdjustment` (or `OrderLineItemAdjustment`) with the specified ID in the `adjustment_id` property.

Refer to [this reference](https://docs.medusajs.com/references/promotion/interfaces/promotion.RemoveItemAdjustmentAction/index.html.md) for details on the object’s properties.

### `addShippingMethodAdjustment` Action

The `addShippingMethodAdjustment` action indicates that an adjustment must be made on a shipping method. For example, make the shipping method free.

This action has the following format:

```ts
export interface AddShippingMethodAdjustment {
  action: "addShippingMethodAdjustment"
  shipping_method_id: string
  amount: number
  code: string
  description?: string
}
```

This action means that a new record should be created of the `ShippingMethodAdjustment` data model in the Cart Module, or `OrderShippingMethodAdjustment` data model in the Order Module.

Refer to [this reference](https://docs.medusajs.com/references/promotion/interfaces/promotion.AddShippingMethodAdjustment/index.html.md) for details on the object’s properties.

### `removeShippingMethodAdjustment` Action

The `removeShippingMethodAdjustment` action indicates that an adjustment must be removed from a shipping method. For example, remove the free shipping discount.

The `computeActions` method accepts any previous shipping method adjustments in the `shipping_methods` property of the second parameter.

This action has the following format:

```ts
export interface RemoveShippingMethodAdjustment {
  action: "removeShippingMethodAdjustment"
  adjustment_id: string
  code: string
}
```

When the Medusa application receives this action type, it removes the `ShippingMethodAdjustment` (or `OrderShippingMethodAdjustment`) with the specified ID in the `adjustment_id` property.

Refer to [this reference](https://docs.medusajs.com/references/promotion/interfaces/promotion.RemoveShippingMethodAdjustment/index.html.md) for details on the object’s properties.

### `campaignBudgetExceeded` Action

When the `campaignBudgetExceeded` action is returned, the promotions within a campaign can no longer be used as the campaign budget has been exceeded.

This action has the following format:

```ts
export interface CampaignBudgetExceededAction {
  action: "campaignBudgetExceeded"
  code: string
}
```

Refer to [this reference](https://docs.medusajs.com/references/promotion/interfaces/promotion.CampaignBudgetExceededAction/index.html.md) for details on the object’s properties.


# Application Method

In this document, you'll learn what an application method is.

## What is an Application Method?

The [ApplicationMethod data model](https://docs.medusajs.com/references/promotion/models/ApplicationMethod/index.html.md) defines how a promotion is applied:

|Property|Purpose|
|---|---|
|\`type\`|Does the promotion discount a fixed amount or a percentage?|
|\`target\_type\`|Is the promotion applied on a cart item, shipping method, or the entire order?|
|\`allocation\`|Is the discounted amount applied on each item or split between the applicable items?|

## Target Promotion Rules

When the promotion is applied to a cart item or a shipping method, you can restrict which items/shipping methods the promotion is applied to.

The `ApplicationMethod` data model has a collection of `PromotionRule` records to restrict which items or shipping methods the promotion applies to. The `target_rules` property represents this relation.

![A diagram showcasing the target\_rules relation between the ApplicationMethod and PromotionRule data models](https://res.cloudinary.com/dza7lstvk/image/upload/v1709898273/Medusa%20Resources/application-method-target-rules_hqaymz.jpg)

In this example, the promotion is only applied on products in the cart having the SKU `SHIRT`.

***

## Buy Promotion Rules

When the promotion’s type is `buyget`, you must specify the “buy X” condition. For example, a cart must have two shirts before the promotion can be applied.

The application method has a collection of `PromotionRule` items to define the “buy X” rule. The `buy_rules` property represents this relation.

![A diagram showcasing the buy\_rules relation between the ApplicationMethod and PromotionRule data models](https://res.cloudinary.com/dza7lstvk/image/upload/v1709898453/Medusa%20Resources/application-method-buy-rules_djjuhw.jpg)

In this example, the cart must have two products with the SKU `SHIRT` for the promotion to be applied.


# Campaign

In this document, you'll learn about campaigns.

Refer to this [Medusa Admin User Guide](https://docs.medusajs.com/user-guide/promotions/campaigns/index.html.md) to learn how to manage campaigns using the dashboard.

## What is a Campaign?

A [Campaign](https://docs.medusajs.com/references/promotion/models/Campaign/index.html.md) combines promotions under the same conditions, such as start and end dates.

![A diagram showcasing the relation between the Campaign and Promotion data models](https://res.cloudinary.com/dza7lstvk/image/upload/v1709899225/Medusa%20Resources/campagin-promotion_hh3qsi.jpg)

***

## Campaign Limits

Each campaign has a budget represented by the [CampaignBudget data model](https://docs.medusajs.com/references/promotion/models/CampaignBudget/index.html.md). The budget limits how many times the promotion can be used.

There are two types of budgets:

- `spend`: An amount that, when crossed, the promotion becomes unusable. For example, if the amount limit is set to `$100`, and the total amount of usage of this promotion crosses that threshold, the promotion can no longer be applied.
- `usage`: The number of times that a promotion can be used. For example, if the usage limit is set to `10`, the promotion can be used only 10 times by customers. After that, it can no longer be applied.

![A diagram showcasing the relation between the Campaign and CampaignBudget data models](https://res.cloudinary.com/dza7lstvk/image/upload/v1709899463/Medusa%20Resources/campagin-budget_rvqlmi.jpg)


# Promotion Concepts

In this guide, you’ll learn about the main promotion and rule concepts in the Promotion Module.

Refer to this [Medusa Admin User Guide](https://docs.medusajs.com/user-guide/promotions/index.html.md) to learn how to manage promotions using the dashboard.

## What is a Promotion?

A promotion, represented by the [Promotion data model](https://docs.medusajs.com/references/promotion/models/Promotion/index.html.md), is a discount that can be applied on cart items, shipping methods, or entire orders.

A promotion has two types:

- `standard`: A standard promotion with rules.
- `buyget`: “A buy X get Y” promotion with rules.

|\`standard\`|\`buyget\`|
|---|---|
|A coupon code that gives customers 10% off their entire order.|Buy two shirts and get another for free.|
|A coupon code that gives customers $15 off any shirt in their order.|Buy two shirts and get 10% off the entire order.|
|A discount applied automatically for VIP customers that removes 10% off their shipping method’s amount.|Spend $100 and get free shipping.|

The Medusa Admin UI may not provide a way to create each of these promotion examples. However, they are supported by the Promotion Module and Medusa's workflows and API routes.

***

## Promotion Rules

A promotion can be restricted by a set of rules, each rule is represented by the [PromotionRule data model](https://docs.medusajs.com/references/promotion/models/PromotionRule/index.html.md).

For example, you can create a promotion that only customers of the `VIP` customer group can use.

![A diagram showcasing the relation between Promotion and PromotionRule](https://res.cloudinary.com/dza7lstvk/image/upload/v1709833196/Medusa%20Resources/promotion-promotion-rule_msbx0w.jpg)

A `PromotionRule`'s `attribute` property indicates the property's name to which this rule is applied. For example, `customer_group_id`.

The expected value for the attribute is stored in the `PromotionRuleValue` data model. So, a rule can have multiple values.

When testing whether a promotion can be applied to a cart, the rule's `attribute` property and its values are tested on the cart itself.

For example, the cart's customer must be part of the customer group(s) indicated in the promotion rule's value.

### Flexible Rules

The `PromotionRule`'s `operator` property adds more flexibility to the rule’s condition rather than simple equality (`eq`).

For example, to restrict the promotion to only `VIP` and `B2B` customer groups:

- Add a `PromotionRule` record with its `attribute` property set to `customer_group_id` and `operator` property to `in`.
- Add two `PromotionRuleValue` records associated with the rule: one with the value `VIP` and the other `B2B`.

![A diagram showcasing the relation between PromotionRule and PromotionRuleValue when a rule has multiple values](https://res.cloudinary.com/dza7lstvk/image/upload/v1709897383/Medusa%20Resources/promotion-promotion-rule-multiple_hctpmt.jpg)

In this case, a customer’s group must be in the `VIP` and `B2B` set of values to use the promotion.

***

## How to Apply Rules on a Promotion?

### Using Workflows

If you're managing promotions using [Medusa's workflows](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/medusa-workflows-reference/index.html.md) or the API routes that use them, you can specify rules for the promotion or its [application method](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/promotion/application-method/index.html.md).

For example, if you're creating a promotion using the [createPromotionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createPromotionsWorkflow/index.html.md):

```ts
const { result } = await createPromotionsWorkflow(container)
  .run({
    input: {
      promotionsData: [{
        code: "10OFF",
        type: "standard",
        status: "active",
        application_method: {
          type: "percentage",
          target_type: "items",
          allocation: "across",
          value: 10,
          currency_code: "usd",
        },
        rules: [
          {
            attribute: "customer.group.id",
            operator: "eq",
            values: [
              "cusgrp_123",
            ],
          },
        ],
      }],
    },
  })
```

In this example, the promotion is restricted to customers with the `cusgrp_123` customer group.

### Using Promotion Module's Service

For most use cases, it's recommended to use [workflows](#using-workflows) instead of directly using the module's service.

If you're managing promotions using the Promotion Module's service, you can specify rules for the promotion or its [application method](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/promotion/application-method/index.html.md) in its methods.

For example, if you're creating a promotion with the [createPromotions](https://docs.medusajs.com/references/promotion/createPromotions/index.html.md) method:

```ts
const promotions = await promotionModuleService.createPromotions([
  {
    code: "50OFF",
    type: "standard",
    status: "active",
    application_method: {
      type: "percentage",
      target_type: "items",
      value: 50,
    },
    rules: [
      {
        attribute: "customer.group.id",
        operator: "eq",
        values: [
          "cusgrp_123",
        ],
      },
    ],
  },
])
```

In this example, the promotion is restricted to customers with the `cusgrp_123` customer group.

### How is the Promotion Rule Applied?

A promotion is applied on a resource if its attributes match the promotion's rules.

For example, consider you have the following promotion with a rule that restricts the promotion to a specific customer:

```json
{
  "code": "10OFF",
  "type": "standard",
  "status": "active",
  "application_method": {
    "type": "percentage",
    "target_type": "items",
    "allocation": "across",
    "value": 10,
    "currency_code": "usd"
  },
  "rules": [
    {
      "attribute": "customer_id",
      "operator": "eq",
      "values": [
        "cus_123"
      ]
    }
  ]
}
```

When you try to apply this promotion on a cart, the cart's `customer_id` is compared to the promotion rule's value based on the specified operator. So, the promotion will only be applied if the cart's `customer_id` is equal to `cus_123`.


# Links between Promotion Module and Other Modules

This document showcases the module links defined between the Promotion Module and other Commerce Modules.

## Summary

The Promotion Module has the following links to other modules:

Read-only links are used to query data across modules, but the relations aren't stored in a pivot table in the database.

|First Data Model|Second Data Model|Type|Description|
|---|---|---|---|
|Cart|Promotion|Stored - many-to-many|Learn more|
|LineItemAdjustment|Promotion|Read-only - has one|Learn more|
|Order|Promotion|Stored - many-to-many|Learn more|

***

## Cart Module

A promotion can be applied on line items and shipping methods of a cart. Medusa defines a link between the `Cart` and `Promotion` data models.

![A diagram showcasing an example of how data models from the Cart and Promotion modules are linked](https://res.cloudinary.com/dza7lstvk/image/upload/v1711538015/Medusa%20Resources/cart-promotion_kuh9vm.jpg)

Medusa also defines a read-only link between the [Cart Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/cart/index.html.md)'s `LineItemAdjustment` data model and the `Promotion` data model. Because the link is read-only from the `LineItemAdjustment`'s side, you can only retrieve the promotion applied on a line item, and not the other way around.

### Retrieve with Query

To retrieve the carts that a promotion is applied on with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `carts.*` in `fields`:

To retrieve the promotion of a line item adjustment, pass `promotion.*` in `fields`.

### query.graph

```ts
const { data: promotions } = await query.graph({
  entity: "promotion",
  fields: [
    "carts.*",
  ],
})

// promotions[0].carts
```

### useQueryGraphStep

```ts
import { useQueryGraphStep } from "@medusajs/medusa/core-flows"

// ...

const { data: promotions } = useQueryGraphStep({
  entity: "promotion",
  fields: [
    "carts.*",
  ],
})

// promotions[0].carts
```

### Manage with Link

To manage the promotions of a cart, use [Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link/index.html.md):

### link.create

```ts
import { Modules } from "@medusajs/framework/utils"

// ...

await link.create({
  [Modules.CART]: {
    cart_id: "cart_123",
  },
  [Modules.PROMOTION]: {
    promotion_id: "promo_123",
  },
})
```

### createRemoteLinkStep

```ts
import { Modules } from "@medusajs/framework/utils"
import { createRemoteLinkStep } from "@medusajs/medusa/core-flows"

// ...

createRemoteLinkStep({
  [Modules.CART]: {
    cart_id: "cart_123",
  },
  [Modules.PROMOTION]: {
    promotion_id: "promo_123",
  },
})
```

***

## Order Module

An order is associated with the promotion applied on it. Medusa defines a link between the `Order` and `Promotion` data models.

![A diagram showcasing an example of how data models from the Order and Promotion modules are linked](https://res.cloudinary.com/dza7lstvk/image/upload/v1716555015/Medusa%20Resources/order-promotion_dgjzzd.jpg)

### Retrieve with Query

To retrieve the orders a promotion is applied on with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `orders.*` in `fields`:

### query.graph

```ts
const { data: promotions } = await query.graph({
  entity: "promotion",
  fields: [
    "orders.*",
  ],
})

// promotions[0].orders
```

### useQueryGraphStep

```ts
import { useQueryGraphStep } from "@medusajs/medusa/core-flows"

// ...

const { data: promotions } = useQueryGraphStep({
  entity: "promotion",
  fields: [
    "orders.*",
  ],
})

// promotions[0].orders
```

### Manage with Link

To manage the promotion of an order, use [Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link/index.html.md):

### link.create

```ts
import { Modules } from "@medusajs/framework/utils"

// ...

await link.create({
  [Modules.ORDER]: {
    order_id: "order_123",
  },
  [Modules.PROMOTION]: {
    promotion_id: "promo_123",
  },
})
```

### createRemoteLinkStep

```ts
import { Modules } from "@medusajs/framework/utils"
import { createRemoteLinkStep } from "@medusajs/medusa/core-flows"

// ...

createRemoteLinkStep({
  [Modules.ORDER]: {
    order_id: "order_123",
  },
  [Modules.PROMOTION]: {
    promotion_id: "promo_123",
  },
})
```


# Stock Location Concepts

In this document, you’ll learn about the main concepts in the Stock Location Module.

## Stock Location

A stock location, represented by the `StockLocation` data model, represents a location where stock items are kept. For example, a warehouse.

Medusa uses stock locations to provide inventory details, from the Inventory Module, per location.

***

## StockLocationAddress

The `StockLocationAddress` data model belongs to the `StockLocation` data model. It provides more detailed information of the location, such as country code or street address.


# Links between Stock Location Module and Other Modules

This document showcases the module links defined between the Stock Location Module and other Commerce Modules.

## Summary

The Stock Location Module has the following links to other modules:

Read-only links are used to query data across modules, but the relations aren't stored in a pivot table in the database.

|First Data Model|Second Data Model|Type|Description|
|---|---|---|---|
|FulfillmentSet|StockLocation|Stored - many-to-one|Learn more|
|FulfillmentProvider|StockLocation|Stored - many-to-many|Learn more|
|InventoryLevel|StockLocation|Read-only - has many|Learn more|
|SalesChannel|StockLocation|Stored - many-to-many|Learn more|

***

## Fulfillment Module

A fulfillment set can be conditioned to a specific stock location.

Medusa defines a link between the `FulfillmentSet` and `StockLocation` data models.

![A diagram showcasing an example of how data models from the Fulfillment and Stock Location modules are linked](https://res.cloudinary.com/dza7lstvk/image/upload/v1712567101/Medusa%20Resources/fulfillment-stock-location_nlkf7e.jpg)

Medusa also defines a link between the `FulfillmentProvider` and `StockLocation` data models to indicate the providers that can be used in a location.

![A diagram showcasing an example of how data models from the Fulfillment and Stock Location modules are linked](https://res.cloudinary.com/dza7lstvk/image/upload/v1728399492/Medusa%20Resources/fulfillment-provider-stock-location_b0mulo.jpg)

### Retrieve with Query

To retrieve the fulfillment sets of a stock location with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `fulfillment_sets.*` in `fields`:

To retrieve the fulfillment providers, pass `fulfillment_providers.*` in `fields`.

### query.graph

```ts
const { data: stockLocations } = await query.graph({
  entity: "stock_location",
  fields: [
    "fulfillment_sets.*",
  ],
})

// stockLocations[0].fulfillment_sets
```

### useQueryGraphStep

```ts
import { useQueryGraphStep } from "@medusajs/medusa/core-flows"

// ...

const { data: stockLocations } = useQueryGraphStep({
  entity: "stock_location",
  fields: [
    "fulfillment_sets.*",
  ],
})

// stockLocations[0].fulfillment_sets
```

### Manage with Link

To manage the stock location of a fulfillment set, use [Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link/index.html.md):

### link.create

```ts
import { Modules } from "@medusajs/framework/utils"

// ...

await link.create({
  [Modules.STOCK_LOCATION]: {
    stock_location_id: "sloc_123",
  },
  [Modules.FULFILLMENT]: {
    fulfillment_set_id: "fset_123",
  },
})
```

### createRemoteLinkStep

```ts
import { Modules } from "@medusajs/framework/utils"
import { createRemoteLinkStep } from "@medusajs/medusa/core-flows"

// ...

createRemoteLinkStep({
  [Modules.STOCK_LOCATION]: {
    stock_location_id: "sloc_123",
  },
  [Modules.FULFILLMENT]: {
    fulfillment_set_id: "fset_123",
  },
})
```

***

## Inventory Module

Medusa defines a read-only link between the [Inventory Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/inventory/index.html.md)'s `InventoryLevel` data model and the `StockLocation` data model. Because the link is read-only from the `InventoryLevel`'s side, you can only retrieve the stock location of an inventory level, and not the other way around.

### Retrieve with Query

To retrieve the stock locations of an inventory level with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `stock_locations.*` in `fields`:

### query.graph

```ts
const { data: inventoryLevels } = await query.graph({
  entity: "inventory_level",
  fields: [
    "stock_locations.*",
  ],
})

// inventoryLevels[0].stock_locations
```

### useQueryGraphStep

```ts
import { useQueryGraphStep } from "@medusajs/medusa/core-flows"

// ...

const { data: inventoryLevels } = useQueryGraphStep({
  entity: "inventory_level",
  fields: [
    "stock_locations.*",
  ],
})

// inventoryLevels[0].stock_locations
```

***

## Sales Channel Module

A stock location is associated with a sales channel. This scopes inventory quantities in a stock location by the associated sales channel.

Medusa defines a link between the `SalesChannel` and `StockLocation` data models.

![A diagram showcasing an example of how resources from the Sales Channel and Stock Location modules are linked](https://res.cloudinary.com/dza7lstvk/image/upload/v1716796872/Medusa%20Resources/sales-channel-location_cqrih1.jpg)

### Retrieve with Query

To retrieve the sales channels of a stock location with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `sales_channels.*` in `fields`:

### query.graph

```ts
const { data: stockLocations } = await query.graph({
  entity: "stock_location",
  fields: [
    "sales_channels.*",
  ],
})

// stockLocations[0].sales_channels
```

### useQueryGraphStep

```ts
import { useQueryGraphStep } from "@medusajs/medusa/core-flows"

// ...

const { data: stockLocations } = useQueryGraphStep({
  entity: "stock_location",
  fields: [
    "sales_channels.*",
  ],
})

// stockLocations[0].sales_channels
```

### Manage with Link

To manage the stock locations of a sales channel, use [Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link/index.html.md):

### link.create

```ts
import { Modules } from "@medusajs/framework/utils"

// ...

await link.create({
  [Modules.SALES_CHANNEL]: {
    sales_channel_id: "sc_123",
  },
  [Modules.STOCK_LOCATION]: {
    sales_channel_id: "sloc_123",
  },
})
```

### createRemoteLinkStep

```ts
import { Modules } from "@medusajs/framework/utils"
import { createRemoteLinkStep } from "@medusajs/medusa/core-flows"

// ...

createRemoteLinkStep({
  [Modules.SALES_CHANNEL]: {
    sales_channel_id: "sc_123",
  },
  [Modules.STOCK_LOCATION]: {
    sales_channel_id: "sloc_123",
  },
})
```


# Tax Module Options

In this guide, you'll learn about the options of the Tax Module.

## providers

The `providers` option is an array of either [tax module providers](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/tax/tax-provider/index.html.md) or path to a file that defines a tax provider.

When the Medusa application starts, these providers are registered and can be used to retrieve tax lines.

```ts title="medusa-config.ts"
import { Modules } from "@medusajs/framework/utils"

// ...

module.exports = defineConfig({
  // ...
  modules: [
    {
      resolve: "@medusajs/tax",
      options: {
        providers: [
          {
            resolve: "./path/to/my-provider",
            id: "my-provider",
            options: {
              // ...
            },
          },
        ],
      },
    },
  ],
})
```

The objects in the array accept the following properties:

- `resolve`: A string indicating the package name of the module provider or the path to it.
- `id`: A string indicating the provider's unique name or ID.
- `options`: An optional object of the module provider's options.


# Tax Calculation with the Tax Provider

In this guide, you’ll learn how tax lines are calculated using the tax provider.

## Tax Lines Calculation

Tax lines are calculated and retrieved using the [getTaxLines method of the Tax Module’s main service](https://docs.medusajs.com/references/tax/getTaxLines/index.html.md). It accepts an array of line items and shipping methods, and the context of the calculation.

For example:

```ts
const taxLines = await taxModuleService.getTaxLines(
  [
    {
      id: "cali_123",
      product_id: "prod_123",
      unit_price: 1000,
      quantity: 1,
    },
    {
      id: "casm_123",
      shipping_option_id: "so_123",
      unit_price: 2000,
    },
  ],
  {
    address: {
      country_code: "us",
    },
  }
)
```

The context object is used to determine which tax regions and rates to use in the calculation. It includes properties related to the address and customer.

The example above retrieves the tax lines based on the tax region for the United States.

The method returns tax lines for the line item and shipping methods. For example:

```json
[
  {
    "line_item_id": "cali_123",
    "rate_id": "txr_1",
    "rate": 10,
    "code": "XXX",
    "name": "Tax Rate 1"
  },
  {
    "shipping_line_id": "casm_123",
    "rate_id": "txr_2",
    "rate": 5,
    "code": "YYY",
    "name": "Tax Rate 2"
  }
]
```

***

## Using the Tax Provider in the Calculation

The tax lines retrieved by the `getTaxLines` method are actually retrieved from the tax region’s [Tax Module Provider](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/tax/tax-provider/index.html.md).

A tax module implements the logic to shape tax lines. Each tax region uses a tax provider.

Learn more about tax providers, configuring, and creating them in the [Tax Module Provider](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/tax/tax-provider/index.html.md) guide.


# Tax Module Provider

In this guide, you’ll learn about the Tax Module Provider and how it's used.

Refer to this [Medusa Admin User Guide](https://docs.medusajs.com/user-guide/settings/tax-regions/index.html.md) to learn how to manage tax provider of a tax region using the dashboard.

## What is a Tax Module Provider?

The Tax Module Provider handles tax line calculations in the Medusa application. It integrates third-party tax services, such as TaxJar, or implements custom tax calculation logic.

The Medusa application uses the Tax Module Provider whenever it needs to calculate tax lines for a cart or order, or when you [calculate the tax lines using the Tax Module's service](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/tax/tax-calculation-with-provider/index.html.md).

![Diagram showcasing the communication between Medusa the Tax Module Provider, and the third-party tax provider.](https://res.cloudinary.com/dza7lstvk/image/upload/v1746790996/Medusa%20Resources/tax-provider-service_kcgpne.jpg)

***

## Default Tax Provider

The Tax Module provides a `system` tax provider that acts as a placeholder tax provider. It performs basic tax calculation, as you can see in the [Create Tax Module Provider](https://docs.medusajs.com/references/tax/provider#gettaxlines/index.html.md) guide.

This provider is installed by default in your application and you can use it with tax regions.

The identifier of the system tax provider is `tp_system`.

***

## How to Create a Custom Tax Provider?

A Tax Module Provider is a module whose service implements the `ITaxProvider` imported from `@medusajs/framework/types`.

The module can have multiple tax provider services, where each are registered as separate tax providers.

Refer to the [Create Tax Module Provider](https://docs.medusajs.com/references/tax/provider/index.html.md) guide to learn how to create a Tax Module Provider.

After you create a tax provider, you can choose it as the default Tax Module Provider for a region in the [Medusa Admin dashboard](https://docs.medusajs.com/user-guide/settings/tax-regions/index.html.md).

***

## How are Tax Providers Registered?

### Configure Tax Module's Providers

The Tax Module accepts a `providers` option that allows you to configure the providers registered in your application.

Learn more about this option in the [Module Options](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/tax/module-options/index.html.md) guide.

### Registration on Application Start

When the Medusa application starts, it registers the Tax Module Providers defined in the `providers` option of the Tax Module.

For each Tax Module Provider, the Medusa application finds all tax provider services defined in them to register.

### TaxProvider Data Model

A registered tax provider is represented by the [TaxProvider data model](https://docs.medusajs.com/references/tax/models/TaxProvider/index.html.md) in the Medusa application.

This data model is used to reference a service in the Tax Module Provider and determine whether it's installed in the application.

![Diagram showcasing the TaxProvider data model](https://res.cloudinary.com/dza7lstvk/image/upload/v1746791254/Medusa%20Resources/tax-provider-model_r6ktjw.jpg)

The `TaxProvider` data model has the following properties:

- `id`: The unique identifier of the tax provider. The ID's format is `tp_{identifier}_{id}`, where:
  - `identifier` is the value of the `identifier` property in the Tax Module Provider's service.
  - `id` is the value of the `id` property of the Tax Module Provider in `medusa-config.ts`.
- `is_enabled`: A boolean indicating whether the tax provider is enabled.

### How to Remove a Tax Provider?

You can remove a registered tax provider from the Medusa application by removing it from the `providers` option in the Tax Module's configuration.

Then, the next time the Medusa application starts, it will set the `is_enabled` property of the `TaxProvider`'s record to `false`. This allows you to re-enable the tax provider later if needed by adding it back to the `providers` option.


# Tax Rates and Rules

In this document, you’ll learn about tax rates and rules.

Refer to this [Medusa Admin User Guide](https://docs.medusajs.com/user-guide/settings/tax-regions#manage-tax-rate-overrides/index.html.md) to learn how to manage tax rates using the dashboard.

## What are Tax Rates?

A tax rate is a percentage amount used to calculate the tax amount for each taxable item’s price, such as line items or shipping methods, in a cart. The sum of all calculated tax amounts are then added to the cart’s total as a tax total.

Each tax region has a default tax rate. This tax rate is applied to all taxable items of a cart in that region.

### Combinable Tax Rates

Tax regions can have parent tax regions. To inherit the tax rates of the parent tax region, set the `is_combinable` of the child’s tax rates to `true`.

Then, when tax rates are retrieved for a taxable item in the child region, both the child and the parent tax regions’ applicable rates are returned.

***

## Override Tax Rates with Rules

You can create tax rates that override the default for specific conditions or rules.

For example, you can have a default tax rate is 10%, but for products of type “Shirt” is %15.

A tax region can have multiple tax rates, and each tax rate can have multiple tax rules. The [TaxRateRule data model](https://docs.medusajs.com/references/tax/models/TaxRateRule/index.html.md) represents a tax rate’s rule.

![A diagram showcasing the relation between TaxRegion, TaxRate, and TaxRateRule](https://res.cloudinary.com/dza7lstvk/image/upload/v1711462167/Medusa%20Resources/tax-rate-rule_enzbp2.jpg)

These two properties of the data model identify the rule’s target:

- `reference`: the name of the table in the database that this rule points to. For example, `product_type`.
- `reference_id`: the ID of the data model’s record that this points to. For example, a product type’s ID.

So, to override the default tax rate for product types “Shirt”, you create a tax rate and associate with it a tax rule whose `reference` is `product_type` and `reference_id` the ID of the “Shirt” product type.


# Tax Region

In this document, you’ll learn about tax regions and how to use them with the Region Module.

Refer to this [Medusa Admin User Guide](https://docs.medusajs.com/user-guide/settings/tax-regions/index.html.md) to learn how to manage tax regions using the dashboard.

## What is a Tax Region?

A tax region, represented by the [TaxRegion data model](https://docs.medusajs.com/references/tax/models/TaxRegion/index.html.md), stores tax settings related to a region that your store serves.

Tax regions can inherit settings and rules from a parent tax region.

***

## Tax Rules in a Tax Region

Tax rules define the tax rates and behavior within a tax region. They specify:

- The tax rate percentage.
- Which products the tax applies to.
- Other custom rules to determine tax applicability.

Learn more about tax rules in the [Tax Rates and Rules](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/tax/tax-rates-and-rules/index.html.md) guide.

***

## Tax Provider

Each tax region can have a default tax provider. The tax provider is responsible for calculating the tax lines for carts and orders in that region.

You can use Medusa's default tax provider or create a custom one, allowing you to integrate with third-party tax services or implement your own tax calculation logic.

Learn more about tax providers in the [Tax Provider](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/tax/tax-provider/index.html.md) guide.


# Links between Store Module and Other Modules

This document showcases the module links defined between the Store Module and other Commerce Modules.

## Summary

The Store Module has the following links to other modules:

Read-only links are used to query data across modules, but the relations aren't stored in a pivot table in the database.

|First Data Model|Second Data Model|Type|Description|
|---|---|---|---|
|StoreCurrency|Currency|Read-only - has many|Learn more|

***

## Currency Module

The Store Module has a `Currency` data model that stores the supported currencies of a store. However, these currencies don't hold all the details of a currency, such as its name or symbol.

Instead, Medusa defines a read-only link between the [Currency Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/currency/index.html.md)'s `Currency` data model and the Store Module's `StoreCurrency` data model. This means you can retrieve the details of a store's supported currencies, but you don't manage the links in a pivot table in the database. The currencies of a store are determined by the `currency_code` of the [Currency](https://docs.medusajs.com/references/store/models/StoreCurrency/index.html.md) data model in the Store Module (not in the Currency Module).

### Retrieve with Query

To retrieve the details of a store's currencies with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `supported_currencies.currency.*` in `fields`:

### query.graph

```ts
const { data: stores } = await query.graph({
  entity: "store",
  fields: [
    "supported_currencies.currency.*",
  ],
})

// stores[0].supported_currencies
```

### useQueryGraphStep

```ts
import { useQueryGraphStep } from "@medusajs/medusa/core-flows"

// ...

const { data: stores } = useQueryGraphStep({
  entity: "store",
  fields: [
    "supported_currencies.currency.*",
  ],
})

// stores[0].supported_currencies
```


# User Module Options

In this document, you'll learn about the options of the User Module.

## Module Options

```ts title="medusa-config.ts"
import { Modules } from "@medusajs/framework/utils"

// ...

module.exports = defineConfig({
  // ...
  modules: [
    {
      resolve: "@medusajs/user",
      options: {
        jwt_secret: process.env.JWT_SECRET,
      },
    },
  ],
})
```

|Option|Description|Required|
|---|---|---|---|---|
|\`jwt\_secret\`|A string indicating the secret used to sign the invite tokens.|Yes|

### Environment Variables

Make sure to add the necessary environment variables for the above options in `.env`:

```bash
JWT_SECRET=supersecret
```


# User Creation Flows

In this document, learn the different ways to create a user using the User Module.

Refer to this [Medusa Admin User Guide](https://docs.medusajs.com/user-guide/settings/users/index.html.md) to learn how to manage users using the dashboard.

## Straightforward User Creation

To create a user, use the [createUsers method of the User Module’s main service](https://docs.medusajs.com/references/user/createUsers/index.html.md):

```ts
const user = await userModuleService.createUsers({
  email: "user@example.com",
})
```

You can pair this with the Auth Module to allow the user to authenticate, as explained in a [later section](#create-identity-with-the-auth-module).

***

## Invite Users

To create a user, you can create an invite for them using the [createInvites method](https://docs.medusajs.com/references/user/createInvites/index.html.md) of the User Module's main service:

```ts
const invite = await userModuleService.createInvites({
  email: "user@example.com",
})
```

Later, you can accept the invite and create a new user for them:

```ts
const invite =
  await userModuleService.validateInviteToken("secret_123")

await userModuleService.updateInvites({
  id: invite.id,
  accepted: true,
})

const user = await userModuleService.createUsers({
  email: invite.email,
})
```

### Invite Expiry

An invite has an expiry date. You can renew the expiry date and refresh the token using the [refreshInviteTokens method](https://docs.medusajs.com/references/user/refreshInviteTokens/index.html.md):

```ts
await userModuleService.refreshInviteTokens(["invite_123"])
```

***

## Create Identity with the Auth Module

By combining the User and Auth Modules, you can use the Auth Module for authenticating users, and the User Module to manage those users.

So, when a user is authenticated, and you receive the `AuthIdentity` object, you can use it to create a user if it doesn’t exist:

```ts
const { success, authIdentity } =
  await authModuleService.authenticate("emailpass", {
    // ...
  })

const [, count] = await userModuleService.listAndCountUsers({
  email: authIdentity.entity_id,
})

if (!count) {
  const user = await userModuleService.createUsers({
    email: authIdentity.entity_id,
  })
}
```


# Emailpass Auth Module Provider

In this document, you’ll learn about the Emailpass auth module provider and how to install and use it in the Auth Module.

Using the Emailpass auth module provider, you allow users to register and login with an email and password.

***

## Register the Emailpass Auth Module Provider

The Emailpass auth provider is registered by default with the Auth Module.

If you want to pass options to the provider, add the provider to the `providers` option of the Auth Module:

```ts title="medusa-config.ts"
import { Modules, ContainerRegistrationKeys } from "@medusajs/framework/utils"

// ...

module.exports = defineConfig({
  // ...
  modules: [
    {
      resolve: "@medusajs/medusa/auth",
      dependencies: [Modules.CACHE, ContainerRegistrationKeys.LOGGER],
      options: {
        providers: [
          // other providers...
          {
            resolve: "@medusajs/medusa/auth-emailpass",
            id: "emailpass",
            options: {
              // options...
            },
          },
        ],
      },
    },
  ],
})
```

### Module Options

|Configuration|Description|Required|Default|
|---|---|---|---|---|---|---|
|\`hashConfig\`|An object of configurations to use when hashing the user's
password. Refer to |No|\`\`\`ts
const hashConfig = \{
&#x20; logN: 15,
&#x20; r: 8,
&#x20; p: 1
}
\`\`\`|

***

## Related Guides

- [How to register a customer using email and password](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/storefront-development/customers/register/index.html.md)


# Stripe Module Provider

In this document, you’ll learn about the Stripe Module Provider and how to configure it in the Payment Module.

Your technical team must install the Stripe Module Provider in your Medusa application first. Then, refer to [this user guide](https://docs.medusajs.com/user-guide/settings/regions#edit-region-details/index.html.md) to learn how to enable the Stripe payment provider in a region using the Medusa Admin dashboard.

## Register the Stripe Module Provider

### Prerequisites

- [Stripe account](https://stripe.com/)
- [Stripe Secret API Key](https://support.stripe.com/questions/locate-api-keys-in-the-dashboard)
- [For deployed Medusa applications, a Stripe webhook secret. Refer to the end of this guide for details on the URL and events.](https://docs.stripe.com/webhooks#add-a-webhook-endpoint)

The Stripe Module Provider is installed by default in your application. To use it, add it to the array of providers passed to the Payment Module in `medusa-config.ts`:

```ts title="medusa-config.ts"
module.exports = defineConfig({
  // ...
  modules: [
    {
      resolve: "@medusajs/medusa/payment",
      options: {
        providers: [
          {
            resolve: "@medusajs/medusa/payment-stripe",
            id: "stripe",
            options: {
              apiKey: process.env.STRIPE_API_KEY,
            },
          },
        ],
      },
    },
  ],
})
```

### Environment Variables

Make sure to add the necessary environment variables for the above options in `.env`:

```bash
STRIPE_API_KEY=<YOUR_STRIPE_API_KEY>
```

### Module Options

|Option|Description|Required|Default|
|---|---|---|---|---|---|---|
|\`apiKey\`|A string indicating the Stripe Secret API key.|Yes|-|
|\`webhookSecret\`|A string indicating the Stripe webhook secret. This is only useful for deployed Medusa applications.|Yes|-|
|\`capture\`|Whether to automatically capture payment after authorization.|No|\`false\`|
|\`automatic\_payment\_methods\`|A boolean value indicating whether to enable Stripe's automatic payment methods. This is useful if you integrate services like Apple pay or Google pay.|No|\`false\`|
|\`payment\_description\`|A string used as the default description of a payment if none is available in cart.context.payment\_description.|No|-|

***

## Enable Stripe Providers in a Region

Before customers can use Stripe to complete their purchases, you must enable the Stripe payment provider(s) in the region where you want to offer this payment method.

Refer to the [user guide](https://docs.medusajs.com/user-guide/settings/regions#edit-region-details/index.html.md) to learn how to edit a region and enable the Stripe payment provider.

***

## Stripe Payment Provider IDs

When you register the Stripe Module Provider, it registers different providers, such as basic Stripe payment, Bancontact, and more.

Each provider is registered and referenced by a unique ID made up of the format `pp_{identifier}_{id}`, where:

- `{identifier}` is the ID of the payment provider as defined in the Stripe Module Provider.
- `{id}` is the ID of the Stripe Module Provider as set in the `medusa-config.ts` file. For example, `stripe`.

Assuming you set the ID of the Stripe Module Provider to `stripe` in `medusa-config.ts`, the Medusa application will register the following payment providers:

|Provider Name|Provider ID|
|---|---|---|
|Basic Stripe Payment|\`pp\_stripe\_stripe\`|
|Bancontact Payments|\`pp\_stripe-bancontact\_stripe\`|
|BLIK Payments|\`pp\_stripe-blik\_stripe\`|
|giropay Payments|\`pp\_stripe-giropay\_stripe\`|
|iDEAL Payments|\`pp\_stripe-ideal\_stripe\`|
|Przelewy24 Payments|\`pp\_stripe-przelewy24\_stripe\`|
|PromptPay Payments|\`pp\_stripe-promptpay\_stripe\`|

***

## Setup Stripe Webhooks

For production applications, you must set up webhooks in Stripe that inform Medusa of changes and updates to payments. Refer to [Stripe's documentation](https://docs.stripe.com/webhooks#add-a-webhook-endpoint) on how to setup webhooks.

### Webhook URL

Medusa has a `{server_url}/hooks/payment/{provider_id}` API route that you can use to register webhooks in Stripe, where:

- `{server_url}` is the URL to your deployed Medusa application in server mode.
- `{provider_id}` is the ID of the provider as explained in the [Stripe Payment Provider IDs](#stripe-payment-provider-ids) section, without the `pp_` prefix.

The Stripe Module Provider supports the following payment types, and the webhook endpoint URL is different for each:

|Stripe Payment Type|Webhook Endpoint URL|
|---|---|---|
|Basic Stripe Payment|\`\{server\_url}/hooks/payment/stripe\_stripe\`|
|Bancontact Payments|\`\{server\_url}/hooks/payment/stripe-bancontact\_stripe\`|
|BLIK Payments|\`\{server\_url}/hooks/payment/stripe-blik\_stripe\`|
|giropay Payments|\`\{server\_url}/hooks/payment/stripe-giropay\_stripe\`|
|iDEAL Payments|\`\{server\_url}/hooks/payment/stripe-ideal\_stripe\`|
|Przelewy24 Payments|\`\{server\_url}/hooks/payment/stripe-przelewy24\_stripe\`|
|PromptPay Payments|\`\{server\_url}/hooks/payment/stripe-promptpay\_stripe\`|

### Webhook Events

When you set up the webhook in Stripe, choose the following events to listen to:

- `payment_intent.amount_capturable_updated`
- `payment_intent.succeeded`
- `payment_intent.payment_failed`

***

## Useful Guides

- [Storefront guide: Add Stripe payment method during checkout](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/storefront-development/checkout/payment/stripe/index.html.md).
- [Integrate in Next.js Starter](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/nextjs-starter#stripe-integration/index.html.md).
- [Customize Stripe Integration in Next.js Starter](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/nextjs-starter/guides/customize-stripe/index.html.md).
- [Add Saved Payment Methods with Stripe](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/how-to-tutorials/tutorials/saved-payment-methods/index.html.md).


# Get Product Variant Prices using Query

In this document, you'll learn how to retrieve product variant prices in the Medusa application using [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md).

The Product Module doesn't provide pricing functionalities. The Medusa application links the Product Module's `ProductVariant` data model to the Pricing Module's `PriceSet` data model.

So, to retrieve data across the linked records of the two modules, you use Query.

## Retrieve All Product Variant Prices

To retrieve all product variant prices, retrieve the product using Query and include among its fields `variants.prices.*`.

For example:

```ts highlights={[["6"]]}
const { data: products } = await query.graph({
  entity: "product",
  fields: [
    "*",
    "variants.*",
    "variants.prices.*",
  ],
  filters: {
    id: [
      "prod_123",
    ],
  },
})
```

Each variant in the retrieved products has a `prices` array property with all the product variant prices. Each price object has the properties of the [Pricing Module's Price data model](https://docs.medusajs.com/references/pricing/models/Price/index.html.md).

***

## Retrieve Calculated Price for a Context

The Pricing Module can calculate prices of a variant based on a [context](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/pricing/price-calculation#calculation-context/index.html.md), such as the region ID or the currency code.

Learn more about prices calculation in [this Pricing Module documentation](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/pricing/price-calculation/index.html.md).

To retrieve calculated prices of variants based on a context, retrieve the products using Query and:

- Pass `variants.calculated_price.*` in the `fields` property.
- Pass a `context` property in the object parameter. Its value is an object of objects that sets the context for the retrieved fields.

For example:

```ts highlights={[["10"], ["15"], ["16"], ["17"], ["18"], ["19"], ["20"], ["21"], ["22"]]}
import { QueryContext } from "@medusajs/framework/utils"

// ...

const { data: products } = await query.graph({
  entity: "product",
  fields: [
    "*",
    "variants.*",
    "variants.calculated_price.*",
  ],
  filters: {
    id: "prod_123",
  },
  context: {
    variants: {
      calculated_price: QueryContext({
        region_id: "reg_01J3MRPDNXXXDSCC76Y6YCZARS",
        currency_code: "eur",
      }),
    },
  },
})
```

For the context of the product variant's calculated price, you pass an object to `context` with the property `variants`, whose value is another object with the property `calculated_price`.

`calculated_price`'s value is created using `QueryContext` from the Modules SDK, passing it a [calculation context object](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/pricing/price-calculation#calculation-context/index.html.md).

Each variant in the retrieved products has a `calculated_price` object. Learn more about its properties in [this Pricing Module guide](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/pricing/price-calculation#returned-price-object/index.html.md).


# Calculate Product Variant Price with Taxes

In this document, you'll learn how to calculate a product variant's price with taxes.

## Step 0: Resolve Resources

You'll need the following resources for the taxes calculation:

1. [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md) to retrieve the product's variants' prices for a context. Learn more about that in [this guide](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/product/guides/price/index.html.md).
2. The Tax Module's main service to get the tax lines for each product.

```ts
// other imports...
import {
  Modules,
  ContainerRegistrationKeys,
} from "@medusajs/framework/utils"

// In an API route, workflow step, etc...
const query = container.resolve(ContainerRegistrationKeys.QUERY)
const taxModuleService = container.resolve(
  Modules.TAX
)
```

***

## Step 1: Retrieve Prices for a Context

After resolving the resources, use Query to retrieve the products with the variants' prices for a context:

Learn more about retrieving product variants' prices for a context in [this guide](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/product/guides/price/index.html.md).

```ts
import { QueryContext } from "@medusajs/framework/utils"

// ...

const { data: products } = await query.graph({
  entity: "product",
  fields: [
    "*",
    "variants.*",
    "variants.calculated_price.*",
  ],
  filters: {
    id: "prod_123",
  },
  context: {
    variants: {
      calculated_price: QueryContext({
        region_id: "region_123",
        currency_code: "usd",
      }),
    },
  },
})
```

***

## Step 2: Get Tax Lines for Products

To retrieve the tax line of each product, first, add the following utility method:

```ts
// other imports...
import {
  HttpTypes,
  TaxableItemDTO,
} from "@medusajs/framework/types"

// ...
const asTaxItem = (product: HttpTypes.StoreProduct): TaxableItemDTO[] => {
  return product.variants
    ?.map((variant) => {
      if (!variant.calculated_price) {
        return
      }

      return {
        id: variant.id,
        product_id: product.id,
        product_name: product.title,
        product_categories: product.categories?.map((c) => c.name),
        product_category_id: product.categories?.[0]?.id,
        product_sku: variant.sku,
        product_type: product.type,
        product_type_id: product.type_id,
        quantity: 1,
        unit_price: variant.calculated_price.calculated_amount,
        currency_code: variant.calculated_price.currency_code,
      }
    })
    .filter((v) => !!v) as unknown as TaxableItemDTO[]
}
```

This formats the products as items to calculate tax lines for.

Then, use it when retrieving the tax lines of the products retrieved earlier:

```ts
// other imports...
import {
  ItemTaxLineDTO,
} from "@medusajs/framework/types"

// ...
const taxLines = (await taxModuleService.getTaxLines(
  products.map(asTaxItem).flat(),
  {
    // example of context properties. You can pass other ones.
    address: {
      country_code,
    },
  }
)) as unknown as ItemTaxLineDTO[]
```

You use the Tax Module's main service's [getTaxLines method](https://docs.medusajs.com/references/tax/getTaxLines/index.html.md) to retrieve the tax line.

For the first parameter, you use the `asTaxItem` function to format the products as expected by the `getTaxLines` method.

For the second parameter, you pass the current context. You can pass other details such as the customer's ID.

Learn about the other context properties to pass in [the getTaxLines method's reference](https://docs.medusajs.com/references/tax/getTaxLines/index.html.md).

***

## Step 3: Calculate Price with Tax for Variant

To calculate the price with and without taxes for a variant, first, group the tax lines retrieved in the previous step by variant IDs:

```ts highlights={taxLineHighlights}
const taxLinesMap = new Map<string, ItemTaxLineDTO[]>()
taxLines.forEach((taxLine) => {
  const variantId = taxLine.line_item_id
  if (!taxLinesMap.has(variantId)) {
    taxLinesMap.set(variantId, [])
  }

  taxLinesMap.get(variantId)?.push(taxLine)
})
```

Notice that the variant's ID is stored in the `line_item_id` property of a tax line since tax lines are used for line items in a cart.

Then, loop over the products and their variants to retrieve the prices with and without taxes:

```ts highlights={calculateTaxHighlights}
// other imports...
import {
  calculateAmountsWithTax,
} from "@medusajs/framework/utils"

// ...
products.forEach((product) => {
  product.variants?.forEach((variant) => {
    if (!variant.calculated_price) {
      return
    }

    const taxLinesForVariant = taxLinesMap.get(variant.id) || []
    const { priceWithTax, priceWithoutTax } = calculateAmountsWithTax({
      taxLines: taxLinesForVariant,
      amount: variant.calculated_price!.calculated_amount!,
      includesTax:
        variant.calculated_price!.is_calculated_price_tax_inclusive!,
    })

    // do something with prices...
  })
})
```

For each product variant, you:

1. Retrieve its tax lines from the `taxLinesMap`.
2. Calculate its prices with and without taxes using the `calculateAmountsWithTax` from the Medusa Framework.
3. The `calculateAmountsWithTax` function returns an object having two properties:
   - `priceWithTax`: The variant's price with the taxes applied.
   - `priceWithoutTax`: The variant's price without taxes applied.


# Get Product Variant Inventory Quantity

In this guide, you'll learn how to retrieve the available inventory quantity of a product variant in your Medusa application customizations. That includes API routes, workflows, subscribers, scheduled jobs, and any resource that can access the [Medusa container](https://docs.medusajs.com/docs/learn/fundamentals/medusa-container/index.html.md).

Refer to the [Retrieve Product Variant Inventory](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/storefront-development/products/inventory/index.html.md) storefront guide.

## Understanding Product Variant Inventory Availability

Product variants have a `manage_inventory` boolean field that indicates whether the Medusa application manages the inventory of the product variant.

When `manage_inventory` is disabled, the Medusa application always considers the product variant to be in stock. So, you can't retrieve the inventory quantity for those products.

When `manage_inventory` is enabled, the Medusa application tracks the inventory of the product variant using the [Inventory Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/inventory/index.html.md). For example, when a customer purchases a product variant, the Medusa application decrements the stocked quantity of the product variant.

This guide explains how to retrieve the inventory quantity of a product variant when `manage_inventory` is enabled.

***

## Retrieve Product Variant Inventory

To retrieve the inventory quantity of a product variant, use the `getVariantAvailability` utility function imported from `@medusajs/framework/utils`. It returns the available quantity of the product variant.

For example:

```ts highlights={variantAvailabilityHighlights}
import { getVariantAvailability } from "@medusajs/framework/utils"

// ...

// use req.scope instead of container in API routes
const query = container.resolve("query")

const availability = await getVariantAvailability(query, {
  variant_ids: ["variant_123"],
  sales_channel_id: "sc_123",
})
```

A product variant's inventory quantity is set per [stock location](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/stock-location/index.html.md). This stock location is linked to a [sales channel](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/sales-channel/index.html.md).

So, to retrieve the inventory quantity of a product variant using `getVariantAvailability`, you need to also provide the ID of the sales channel to retrieve the inventory quantity in.

Refer to the [Retrieve Sales Channel to Use](#retrieve-sales-channel-to-use) section to learn how to retrieve the sales channel ID to use in the `getVariantAvailability` function.

### Parameters

The `getVariantAvailability` function accepts the following parameters:

- query: (Query) Instance of Query to retrieve the necessary data.
- options: (\`object\`) The options to retrieve the variant availability.

  - variant\_ids: (\`string\[]\`) The IDs of the product variants to retrieve their inventory availability.

  - sales\_channel\_id: (\`string\`) The ID of the sales channel to retrieve the variant availability in.

### Returns

The `getVariantAvailability` function resolves to an object whose keys are the IDs of each product variant passed in the `variant_ids` parameter.

The value of each key is an object with the following properties:

- availability: (\`number\`) The available quantity of the product variant in the stock location linked to the sales channel. If \`manage\_inventory\` is disabled, this value is \`0\`.
- sales\_channel\_id: (\`string\`) The ID of the sales channel that the availability is scoped to.

For example, the object may look like this:

```json title="Example result"
{
  "variant_123": {
    "availability": 10,
    "sales_channel_id": "sc_123"
  }
}
```

***

## Retrieve Sales Channel to Use

To retrieve the sales channel ID to use in the `getVariantAvailability` function, you can either:

- Use the sales channel of the request's scope.
- Use the sales channel that the variant's product is available in.

### Method 1: Use Sales Channel Scope in Store Routes

Requests sent to API routes starting with `/store` must include a [publishable API key in the request header](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/sales-channel/publishable-api-keys/index.html.md). This scopes the request to one or more sales channels associated with the publishable API key.

So, if you're retrieving the variant inventory availability in an API route starting with `/store`, you can access the sales channel using the `publishable_key_context.sales_channel_ids` property of the request object:

```ts highlights={salesChannelScopeHighlights}
import { MedusaStoreRequest, MedusaResponse } from "@medusajs/framework/http"
import { getVariantAvailability } from "@medusajs/framework/utils"

export async function GET(
  req: MedusaStoreRequest,
  res: MedusaResponse
) {
  const query = req.scope.resolve("query")
  const sales_channel_ids = req.publishable_key_context.sales_channel_ids

  const availability = await getVariantAvailability(query, {
    variant_ids: ["variant_123"],
    sales_channel_id: sales_channel_ids[0],
  })

  res.json({
    availability,
  })
}
```

In this example, you retrieve the scope's sales channel IDs using `req.publishable_key_context.sales_channel_ids`, whose value is an array of IDs.

Then, you pass the first sales channel ID to the `getVariantAvailability` function to retrieve the inventory availability of the product variant in that sales channel.

Notice that the request object's type is `MedusaStoreRequest` instead of `MedusaRequest` to ensure the availability of the `publishable_key_context` property.

### Method 2: Use Product's Sales Channel

A product is linked to the sales channels it's available in. So, you can retrieve the details of the variant's product, including its sales channels.

For example:

```ts highlights={productSalesChannelHighlights}
import { getVariantAvailability } from "@medusajs/framework/utils"

// ...

// use req.scope instead of container in API routes
const query = container.resolve("query")

const { data: variants } = await query.graph({
  entity: "variant",
  fields: ["id", "product.sales_channels.*"],
  filters: {
    id: "variant_123",
  },
})

const availability = await getVariantAvailability(query, {
  variant_ids: ["variant_123"],
  sales_channel_id: variants[0].product!.sales_channels![0]!.id,
})
```

In this example, you retrieve the sales channels of the variant's product using [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md).

You pass the ID of the variant as a filter, and you specify `product.sales_channels.*` as the fields to retrieve. This retrieves the sales channels linked to the variant's product.

Then, you pass the first sales channel ID to the `getVariantAvailability` function to retrieve the inventory availability of the product variant in that sales channel.


# GitHub Auth Module Provider

In this document, you’ll learn about the GitHub Auth Module Provider and how to install and use it in the Auth Module.

The Github Auth Module Provider authenticates users with their GitHub account.

Learn about the authentication flow in [this guide](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/auth/authentication-route/index.html.md).

***

## Register the Github Auth Module Provider

### Prerequisites

- [Register GitHub App. When setting the Callback URL, set it to a URL in your frontend that later uses Medusa's callback route to validate the authentication.](https://docs.github.com/en/apps/creating-github-apps/setting-up-a-github-app/creating-a-github-app)
- [Retrieve the client ID and client secret of your GitHub App](https://docs.github.com/en/rest/authentication/authenticating-to-the-rest-api?apiVersion=2022-11-28#using-basic-authentication)

Add the module to the array of providers passed to the Auth Module:

```ts title="medusa-config.ts"
import { Modules, ContainerRegistrationKeys } from "@medusajs/framework/utils"

// ...

module.exports = defineConfig({
  // ...
  modules: [
    {
      resolve: "@medusajs/medusa/auth",
      dependencies: [Modules.CACHE, ContainerRegistrationKeys.LOGGER],
      options: {
        providers: [
          // other providers...
          {
            resolve: "@medusajs/medusa/auth-github",
            id: "github",
            options: {
              clientId: process.env.GITHUB_CLIENT_ID,
              clientSecret: process.env.GITHUB_CLIENT_SECRET,
              callbackUrl: process.env.GITHUB_CALLBACK_URL,
            },
          },
        ],
      },
    },
  ],
})
```

### Environment Variables

Make sure to add the necessary environment variables for the above options in `.env`:

```plain
GITHUB_CLIENT_ID=<YOUR_GITHUB_CLIENT_ID>
GITHUB_CLIENT_SECRET=<YOUR_GITHUB_CLIENT_SECRET>
GITHUB_CALLBACK_URL=<YOUR_GITHUB_CALLBACK_URL>
```

### Module Options

|Configuration|Description|Required|
|---|---|---|---|---|
|\`clientId\`|A string indicating the client ID of your GitHub app.|Yes|
|\`clientSecret\`|A string indicating the client secret of your GitHub app.|Yes|
|\`callbackUrl\`|A string indicating the URL to redirect to in your frontend after the user completes their authentication in GitHub.|Yes|

***

## Override Callback URL During Authentication

In many cases, you may have different callback URL for actor types. For example, you may redirect admin users to a different URL than customers after authentication.

The [Authenticate or Login API Route](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/auth/authentication-route#login-route/index.html.md) can accept a `callback_url` body parameter to override the provider's `callbackUrl` option. Learn more in [this documentation](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/auth/authentication-route#login-route/index.html.md).

***

## Examples

- [How to implement third-party / social login in the storefront.](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/storefront-development/customers/third-party-login/index.html.md).


# Google Auth Module Provider

In this document, you’ll learn about the Google Auth Module Provider and how to install and use it in the Auth Module.

The Google Auth Module Provider authenticates users with their Google account.

Learn about the authentication flow for third-party providers in [this guide](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/auth/authentication-route#2-third-party-service-authenticate-flow/index.html.md).

***

## Register the Google Auth Module Provider

### Prerequisites

- [Create a project in Google Cloud.](https://cloud.google.com/resource-manager/docs/creating-managing-projects)
- [Create authorization credentials. When setting the Redirect Uri, set it to a URL in your frontend that later uses Medusa's callback route to validate the authentication.](https://developers.google.com/identity/protocols/oauth2/web-server#creatingcred)

Add the module to the array of providers passed to the Auth Module:

```ts title="medusa-config.ts"
import { Modules, ContainerRegistrationKeys } from "@medusajs/framework/utils"

// ...

module.exports = defineConfig({
  // ...
  modules: [
    {
      // ...
      [Modules.AUTH]: {
        resolve: "@medusajs/medusa/auth",
        dependencies: [Modules.CACHE, ContainerRegistrationKeys.LOGGER],
        options: {
          providers: [
            // other providers...
            {
              resolve: "@medusajs/medusa/auth-google",
              id: "google",
              options: {
                clientId: process.env.GOOGLE_CLIENT_ID,
                clientSecret: process.env.GOOGLE_CLIENT_SECRET,
                callbackUrl: process.env.GOOGLE_CALLBACK_URL,
              },
            },
          ],
        },
      },
    },
  ],
})
```

### Environment Variables

Make sure to add the necessary environment variables for the above options in `.env`:

```plain
GOOGLE_CLIENT_ID=<YOUR_GOOGLE_CLIENT_ID>
GOOGLE_CLIENT_SECRET=<YOUR_GOOGLE_CLIENT_SECRET>
GOOGLE_CALLBACK_URL=<YOUR_GOOGLE_CALLBACK_URL>
```

### Module Options

|Configuration|Description|Required|
|---|---|---|---|---|
|\`clientId\`|A string indicating the |Yes|
|\`clientSecret\`|A string indicating the |Yes|
|\`callbackUrl\`|A string indicating the URL to redirect to in your frontend after the user completes their authentication in Google.|Yes|

***

***

## Override Callback URL During Authentication

In many cases, you may have different callback URL for actor types. For example, you may redirect admin users to a different URL than customers after authentication.

The [Authenticate or Login API Route](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/auth/authentication-route#login-route/index.html.md) can accept a `callback_url` body parameter to override the provider's `callbackUrl` option. Learn more in [this documentation](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/auth/authentication-route#login-route/index.html.md).

***

## Examples

- [How to implement Google social login in the storefront](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/storefront-development/customers/third-party-login/index.html.md).


# Infrastructure Modules

Medusa's architectural functionalities, such as emitting and subscribing to events or caching data, are all implemented in Infrastructure Modules. An Infrastructure Module is a package that can be installed and used in any Medusa application. These modules allow you to choose and integrate custom services for architectural purposes.

For example, you can use our [Redis Event Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/infrastructure-modules/event/redis/index.html.md) to handle event functionalities, or create a custom module that implements these functionalities with Memcached. Learn more in [the Architecture documentation](https://docs.medusajs.com/docs/learn/introduction/architecture/index.html.md).

This section of the documentation showcases Medusa's Infrastructure Modules, how they work, and how to use them in your Medusa application.

## Analytics Module

The Analytics Module is available starting [Medusa v2.8.3](https://github.com/medusajs/medusa/releases/tag/v2.8.3).

The Analytics Module exposes functionalities to track and analyze user interactions and system events. For example, tracking cart updates or completed orders. Learn more in the [Analytics Module documentation](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/infrastructure-modules/analytics/index.html.md).

{/* The Analytics Module has module providers that implement the underlying logic of integrating third-party services for tracking analytics. The following Analytics Module Providers are provided by Medusa. You can also create a custom provider as explained in the [Create Analytics Module Provider guide](/references/analytics/provider). */}

- [Local](https://docs.medusajs.com/infrastructure-modules/analytics/local/index.html.md)
- [PostHog](https://docs.medusajs.com/infrastructure-modules/analytics/posthog/index.html.md)

## Cache Module

A Cache Module is used to cache the results of computations such as price selection or various tax calculations. Learn more in [this documentation](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/infrastructure-modules/cache/index.html.md).

The following Cache modules are provided by Medusa. You can also create your own cache module as explained in [this guide](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/infrastructure-modules/cache/create/index.html.md).

- [In-Memory](https://docs.medusajs.com/infrastructure-modules/cache/in-memory/index.html.md)
- [Redis](https://docs.medusajs.com/infrastructure-modules/cache/redis/index.html.md)

***

## Event Module

An Event Module implements the underlying publish/subscribe system that handles queueing events, emitting them, and executing their subscribers.  Learn more in [this documentation](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/infrastructure-modules/event/index.html.md).

The following Event modules are provided by Medusa. You can also create your own event module as explained in [this guide](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/infrastructure-modules/event/create/index.html.md).

- [Local](https://docs.medusajs.com/infrastructure-modules/event/local/index.html.md)
- [Redis](https://docs.medusajs.com/infrastructure-modules/event/redis/index.html.md)

***

## File Module

The File Module handles file upload and storage of assets, such as product images. Refer to the [File Module documentation](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/infrastructure-modules/file/index.html.md) to learn more about it.

The File Module has module providers that implement the underlying logic of handling uploads and downloads of assets, such as integrating third-party services. The following File Module Providers are provided by Medusa. You can also create a custom provider as explained in the [Create File Module Provider guide](https://docs.medusajs.com/references/file-provider-module/index.html.md).

- [Local](https://docs.medusajs.com/infrastructure-modules/file/local/index.html.md)
- [AWS S3 (and Compatible APIs)](https://docs.medusajs.com/infrastructure-modules/file/s3/index.html.md)

***

## Locking Module

The Locking Module manages access to shared resources by multiple processes or threads. It prevents conflicts between processes and ensures data consistency. Refer to the [Locking Module documentation](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/infrastructure-modules/locking/index.html.md) to learn more about it.

The Locking Module uses module providers that implement the underlying logic of the locking mechanism. The following Locking Module Providers are provided by Medusa. You can also create a custom provider as explained in the [Create Locking Module Provider guide](https://docs.medusajs.com/references/locking-module-provider/index.html.md).

- [Redis](https://docs.medusajs.com/infrastructure-modules/locking/redis/index.html.md)
- [PostgreSQL](https://docs.medusajs.com/infrastructure-modules/locking/postgres/index.html.md)

***

## Notification Module

The Notification Module handles sending notifications to users or customers, such as reset password instructions or newsletters. Refer to the [Notifcation Module documentation](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/infrastructure-modules/notification/index.html.md) to learn more about it.

The Notification Module has module providers that implement the underlying logic of sending notifications, typically through integrating a third-party service. The following modules are provided by Medusa. You can also create a custom provider as explained in the [Create Notification Module Provider guide](https://docs.medusajs.com/references/notification-provider-module/index.html.md).

- [Local](https://docs.medusajs.com/infrastructure-modules/notification/local/index.html.md)
- [SendGrid](https://docs.medusajs.com/infrastructure-modules/notification/sendgrid/index.html.md)

### Notification Module Provider Guides

- [Send Notification](https://docs.medusajs.com/infrastructure-modules/notification/send-notification/index.html.md)
- [Create Notification Provider](https://docs.medusajs.com/references/notification-provider-module/index.html.md)
- [Resend](https://docs.medusajs.com/integrations/guides/resend/index.html.md)

***

## Workflow Engine Module

A Workflow Engine Module handles tracking and recording the transactions and statuses of workflows and their steps. Learn more about it in the [Worklow Engine Module documentation](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/infrastructure-modules/workflow-engine/index.html.md).

The following Workflow Engine modules are provided by Medusa.

- [In-Memory](https://docs.medusajs.com/infrastructure-modules/workflow-engine/in-memory/index.html.md)
- [Redis](https://docs.medusajs.com/infrastructure-modules/workflow-engine/redis/index.html.md)


# Analytics Module

In this document, you'll learn about the Analytics Module and its providers.

The Analytics Module is available starting [Medusa v2.8.3](https://github.com/medusajs/medusa/releases/tag/v2.8.3).

## What is the Analytics Module?

The Analytics Module exposes functionalities to track and analyze user interactions and system events with third-party services. For example, you can track cart updates or completed orders.

In your Medusa application, you can use the Analytics Module to send data to third-party analytics services like PostHog or Segment, enabling you to gain insights into user behavior and system performance.

![Diagram showcasing the flow of tracking an event like order.placed](https://res.cloudinary.com/dza7lstvk/image/upload/v1747832107/Medusa%20Resources/analytics-module-overview_egz7xg.jpg)

***

## How to Use the Analytics Module?

### Configure Analytics Module Provider

To use the Analytics Module, you need to configure it along with an Analytics Module Provider.

An Analytics Module Provider implements the underlying logic of sending analytics data. It integrates with a third-party analytics service to send the data collected through the Analytics Module.

Medusa provides two Analytics Module Providers: Local and PostHog module providers.

You can also [create a custom Analytics Module Provider](https://docs.medusajs.com/references/analytics/provider/index.html.md) that integrates with a third-party service, like Segment.

- [Local](https://docs.medusajs.com/infrastructure-modules/analytics/local/index.html.md)
- [PostHog](https://docs.medusajs.com/infrastructure-modules/analytics/posthog/index.html.md)

[Segment](https://docs.medusajs.com/integrations/guides/segment/index.html.md): undefined

To configure the Analytics Module and its provider, add it to the list of modules in your `medusa-config.ts` file. For example:

```ts title="medusa-config.ts"
module.exports = defineConfig({
  // ...
  modules: [
    {
      resolve: "@medusajs/medusa/analytics",
      options: {
        providers: [
          {
            resolve: "@medusajs/medusa/analytics-local",
            id: "local",
          },
        ],
      },
    },
  ],
})
```

Refer to the documentation of each provider for specific configuration options.

### Track Events

To track an event, you can use the Analytics Module as part of the [workflows](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md) you build for your custom features. A workflow is a special function composed of a series of steps that guarantees data consistency and reliable roll-back mechanism.

In a step of your workflow, you can resolve the Analytics Module's service and use its methods to track events or identify users.

For example, create a workflow at `src/workflows/track-order-placed.ts` with the following content:

```ts title="src/workflows/track-order-created.ts" highlights={workflowHighlights}
import { createWorkflow } from "@medusajs/framework/workflows-sdk"
import { createStep } from "@medusajs/framework/workflows-sdk"
import { Modules } from "@medusajs/framework/utils"
import { OrderDTO } from "@medusajs/framework/types"

type StepInput = {
  order: OrderDTO
}

const trackOrderCreatedStep = createStep(
  "track-order-created-step",
  async ({ order }: StepInput, { container }) => {
    const analyticsModuleService = container.resolve(Modules.ANALYTICS)

    await analyticsModuleService.track({
      event: "order_created",
      userId: order.customer_id,
      properties: {
        order_id: order.id,
        total: order.total,
        items: order.items.map((item) => ({
          variant_id: item.variant_id,
          product_id: item.product_id,
          quantity: item.quantity,
        })),
        customer_id: order.customer_id,
      },
    })
  }
)

type WorkflowInput = {
  order_id: string
}

export const trackOrderCreatedWorkflow = createWorkflow(
  "track-order-created-workflow",
  ({ order_id }: WorkflowInput) => {
    const { data: orders } = useQueryGraphStep({
      entity: "order",
      fields: [
        "*",
        "customer.*",
        "items.*",
      ],
      filters: {
        id: order_id,
      },
    })
    trackOrderCreatedStep({
      order: orders[0],
    })
  }
)
```

This workflow retrieves the order details using the `useQueryGraphStep` and then tracks the order creation event using the `trackOrderCreatedStep`.

In the step, you resolve the service of the Analytics Module from the [Medusa container](https://docs.medusajs.com/docs/learn/fundamentals/medusa-container/index.html.md) and use its `track` method to track the event. This method will use the underlying provider configured in `medusa-config.ts` to track the event.

### Execute Analytics Workflow

After that, you can execute this workflow in a subscriber that runs when a product is created.

create a subscriber at `src/subscribers/order-placed.ts` with the following content:

```ts title="src/subscribers/order-placed.ts"
import type {
  SubscriberArgs,
  SubscriberConfig,
} from "@medusajs/framework"
import { trackOrderCreatedWorkflow } from "../workflows/track-order-created"

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

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

This subscriber listens to the `order.placed` event and executes the `trackOrderCreatedWorkflow` workflow, passing the order ID as input.

You'll now track the order creation event whenever an order is placed in your Medusa application. You can test this out by placing an order and checking the provider you integrated with (for example, PostHog) for the tracked event.


# Cache Module

In this document, you'll learn what a Cache Module is and how to use it in your Medusa application.

## What is a Cache Module?

A Cache Module is used to cache the results of computations such as price selection or various tax calculations.

The underlying database, third-party service, or caching logic is flexible since it's implemented in a module. You can choose from Medusa’s cache modules or create your own to support something more suitable for your architecture.

### Default Cache Module

By default, Medusa uses the [In-Memory Cache Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/infrastructure-modules/cache/in-memory/index.html.md). This module uses a plain JavaScript Map object to store the cache data. While this is suitable for development, it's recommended to use other Cache Modules, such as the [Redis Cache Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/infrastructure-modules/cache/redis/index.html.md), for production. You can also [Create a Cache Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/infrastructure-modules/cache/create/index.html.md).

***

## How to Use the Cache Module?

You can use the registered Cache Module as part of the [workflows](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md) you build for your custom features. A workflow is a special function composed of a series of steps that guarantees data consistency and reliable roll-back mechanism.

In a step of your workflow, you can resolve the Cache Module's service and use its methods to cache data, retrieve cached data, or clear the cache.

For example:

```ts
import { Modules } from "@medusajs/framework/utils"
import { 
  createStep,
  createWorkflow,
} from "@medusajs/framework/workflows-sdk"

const step1 = createStep(
  "step-1",
  async ({}, { container }) => {
    const cacheModuleService = container.resolve(
      Modules.CACHE
    )

    await cacheModuleService.set("key", "value")
  } 
)

export const workflow = createWorkflow(
  "workflow-1",
  () => {
    step1()
  }
)
```

In the example above, you create a workflow that has a step. In the step, you resolve the service of the Cache Module from the [Medusa container](https://docs.medusajs.com/docs/learn/fundamentals/medusa-container/index.html.md).

Then, you use the `set` method of the Cache Module to cache the value `"value"` with the key `"key"`.

***

## List of Cache Modules

Medusa provides the following Cache Modules. You can use one of them, or [Create a Cache Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/infrastructure-modules/cache/create/index.html.md).

- [In-Memory](https://docs.medusajs.com/infrastructure-modules/cache/in-memory/index.html.md)
- [Redis](https://docs.medusajs.com/infrastructure-modules/cache/redis/index.html.md)


# Notification Module

In this document, you'll learn about the Notification Module and its providers.

## What is the Notification Module?

The Notification Module exposes the functionalities to send a notification to a customer or user. For example, sending an order confirmation email. Medusa uses the Notification Module in its core commerce features for notification operations, and you an use it in your custom features as well.

***

## How to Use the Notification Module?

You can use the Notification Module as part of the [workflows](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md) you build for your custom features. A workflow is a special function composed of a series of steps that guarantees data consistency and reliable roll-back mechanism.

In a step of your workflow, you can resolve the Notification Module's service and use its methods to send notifications.

For example:

```ts
import { Modules } from "@medusajs/framework/utils"
import { 
  createStep,
  createWorkflow,
} from "@medusajs/framework/workflows-sdk"

const step1 = createStep(
  "step-1",
  async ({}, { container }) => {
    const notificationModuleService = container.resolve(
      Modules.NOTIFICATION
    )

    await notificationModuleService.createNotifications({
      to: "customer@gmail.com",
      channel: "email",
      template: "product-created",
      data,
    })
  } 
)

export const workflow = createWorkflow(
  "workflow-1",
  () => {
    step1()
  }
)
```

In the example above, you create a workflow that has a step. In the step, you resolve the service of the Notification Module from the [Medusa container](https://docs.medusajs.com/docs/learn/fundamentals/medusa-container/index.html.md).

Then, you use the `createNotifications` method of the Notification Module to send an email notification.

Find a full example of sending a notification in the [Send Notification guide](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/infrastructure-modules/notification/send-notification/index.html.md).

***

## What is a Notification Module Provider?

A Notification Module Provider implements the underlying logic of sending notification. It either integrates a third-party service or uses custom logic to send the notification.

By default, Medusa uses the [Local Notification Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/infrastructure-modules/notification/local/index.html.md) which only simulates sending the notification by logging a message in the terminal.

Medusa provides other Notification Modules that actually send notifications, such as the [SendGrid Notification Module Provider](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/infrastructure-modules/notification/send-notification/index.html.md). You can also [Create a Notification Module Provider](https://docs.medusajs.com/references/notification-provider-module/index.html.md).

- [Local](https://docs.medusajs.com/infrastructure-modules/notification/local/index.html.md)
- [SendGrid](https://docs.medusajs.com/infrastructure-modules/notification/sendgrid/index.html.md)

***

## Notification Module Provider Channels

When you send a notification, you specify the channel to send it through, such as `email` or `sms`.

You register providers of the Notification Module in `medusa-config.ts`. For each provider, you pass a `channels` option specifying which channels the provider can be used in. Only one provider can be setup for each channel.

For example:

```ts title="medusa-config.ts" highlights={[["19"]]}
import { Modules } from "@medusajs/framework/utils"

// ...

module.exports = {
  // ...
  modules: [
    // ...
    {
      resolve: "@medusajs/medusa/notification",
      options: {
        providers: [
          // ...
          {
            resolve: "@medusajs/medusa/notification-local",
            id: "notification",
            options: {
              channels: ["email"],
            },
          },
        ],
      },
    },
  ],
}
```

The `channels` option is an array of strings indicating the channels this provider is used for.


# Event Module

In this document, you'll learn what an Event Module is and how to use it in your Medusa application.

## What is an Event Module?

An Event Module implements the underlying publish/subscribe system that handles queueing events, emitting them, and executing their subscribers.

This makes the event architecture customizable, as you can either choose one of Medusa’s event modules or create your own.

Learn more about Medusa's event systems in the [Events and Subscribers documentation](https://docs.medusajs.com/docs/learn/fundamentals/events-and-subscribers/index.html.md).

### Default Event Module

By default, Medusa uses the [Local Event Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/infrastructure-modules/event/local/index.html.md). This module uses Node’s EventEmitter to implement the publish/subscribe system. While this is suitable for development, it's recommended to use other Event Modules, such as the [Redis Event Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/infrastructure-modules/event/redis/index.html.md), for production. You can also [Create an Event Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/infrastructure-modules/event/create/index.html.md).

***

## How to Use the Event Module?

You can use the registered Event Module as part of the [workflows](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md) you build for your custom features. A workflow is a special function composed of a series of steps that guarantees data consistency and reliable roll-back mechanism.

Medusa provides the helper step [emitEventStep](https://docs.medusajs.com/references/helper-steps/emitEventStep/index.html.md) that you can use in your workflow. You can also resolve the Event Module's service in a step of your workflow and use its methods to emit events.

For example:

```ts
import { Modules } from "@medusajs/framework/utils"
import { 
  createStep,
  createWorkflow,
} from "@medusajs/framework/workflows-sdk"

const step1 = createStep(
  "step-1",
  async ({}, { container }) => {
    const eventModuleService = container.resolve(
      Modules.EVENT
    )

    await eventModuleService.emit({
      name: "custom.event",
      data: {
        id: "123",
        // other data payload
      },
    })
  } 
)

export const workflow = createWorkflow(
  "workflow-1",
  () => {
    step1()
  }
)
```

In the example above, you create a workflow that has a step. In the step, you resolve the service of the Event Module from the [Medusa container](https://docs.medusajs.com/docs/learn/fundamentals/medusa-container/index.html.md).

Then, you use the `emit` method of the Event Module to emit an event with the name `"custom.event"` and the data payload `{ id: "123" }`.

***

## List of Event Modules

Medusa provides the following Event Modules. You can use one of them, or [Create an Event Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/infrastructure-modules/event/create/index.html.md).

- [Local](https://docs.medusajs.com/infrastructure-modules/event/local/index.html.md)
- [Redis](https://docs.medusajs.com/infrastructure-modules/event/redis/index.html.md)


# Locking Module

In this document, you'll learn about the Locking Module and its providers.

## What is the Locking Module?

The Locking Module manages access to shared resources by multiple processes or threads. It prevents conflicts between processes that are trying to access the same resource at the same time, and ensures data consistency.

Medusa uses the Locking Module to control concurrency, avoid race conditions, and protect parts of code that should not be executed by more than one process at a time. This is especially essential in distributed or multi-threaded environments.

For example, Medusa uses the Locking Module in inventory management to ensure that only one transaction can update the stock levels at a time. By using the Locking Module in this scenario, Medusa prevents overselling an inventory item and keeps its quantity amounts accurate, even during high traffic periods or when receiving concurrent requests.

***

## How to Use the Locking Module?

You can use the Locking Module as part of the [workflows](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md) you build for your custom features. A workflow is a special function composed of a series of steps that guarantees data consistency and reliable roll-back mechanism.

In a step of your workflow, you can resolve the Locking Module's service and use its methods to execute an asynchronous job, acquire a lock, or release locks.

For example:

```ts
import { Modules } from "@medusajs/framework/utils"
import { 
  createStep,
  createWorkflow,
} from "@medusajs/framework/workflows-sdk"

const step1 = createStep(
  "step-1",
  async ({}, { container }) => {
    const lockingModuleService = container.resolve(
      Modules.LOCKING
    )
    const productModuleService = container.resolve(
      Modules.PRODUCT
    )

    await lockingModuleService.execute("prod_123", async () => {
      await productModuleService.deleteProduct("prod_123")
    })
  } 
)

export const workflow = createWorkflow(
  "workflow-1",
  () => {
    step1()
  }
)
```

In the example above, you create a workflow that has a step. In the step, you resolve the services of the Locking and Product modules from the [Medusa container](https://docs.medusajs.com/docs/learn/fundamentals/medusa-container/index.html.md).

Then, you use the `execute` method of the Locking Module to acquire a lock for the product with the ID `prod_123` and execute an asynchronous function, which deletes the product.

***

## When to Use the Locking Module?

You should use the Locking Module when you need to ensure that only one process can access a shared resource at a time. As mentioned in the inventory example previously, you don't want customers to order quantities of inventory that are not available, or to update the stock levels of an item concurrently.

In those scenarios, you can use the Locking Module to acquire a lock for a resource and execute a critical section of code that should not be accessed by multiple processes simultaneously.

***

## What is a Locking Module Provider?

A Locking Module Provider implements the underlying logic of the Locking Module. It manages the locking mechanisms and ensures that only one process can access a shared resource at a time.

Medusa provides [multiple Locking Module Providers](#list-of-locking-module-providers) that are suitable for development and production. You can also create a [custom Locking Module Provider](https://docs.medusajs.com/references/locking-module-provider/index.html.md) to implement custom locking mechanisms or integrate with third-party services.

### Default Locking Module Provider

By default, Medusa uses the In-Memory Locking Module Provider. This provider uses a plain JavaScript map to store the locks. While this is useful for development, it is not recommended for production environments as it is only intended for use in a single-instance environment.

To add more providers, you can register them in the `medusa-config.ts` file. For example:

```ts
module.exports = defineConfig({
  // ...
  modules: [
    {
      resolve: "@medusajs/medusa/locking",
      options: {
        providers: [
          // add providers here...
        ],
      },
    },
  ],
})
```

When you register other providers in `medusa-config.ts`, Medusa will set the default provider based on the following scenarios:

|Scenario|Default Provider|
|---|---|---|
|One provider is registered.|The registered provider.|
|Multiple providers are registered and none of them has an |In-Memory Locking Module Provider.|
|Multiple providers and one of them has an |The provider with the |

***

## List of Locking Module Providers

Medusa provides the following Locking Module Providers. You can use one of them, or [Create a Locking Module Provider](https://docs.medusajs.com/references/locking-module-provider/index.html.md).

- [Redis](https://docs.medusajs.com/infrastructure-modules/locking/redis/index.html.md)
- [PostgreSQL](https://docs.medusajs.com/infrastructure-modules/locking/postgres/index.html.md)


# File Module

In this document, you'll learn about the File Module and its providers.

## What is the File Module?

The File Module exposes the functionalities to upload assets, such as product images, to the Medusa application. Medusa uses the File Module in its core commerce features for all file operations, and you can use it in your custom features as well.

***

## How to Use the File Module?

You can use the File Module as part of the [workflows](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md) you build for your custom features. A workflow is a special function composed of a series of steps that guarantees data consistency and reliable roll-back mechanism.

In a step of your workflow, you can resolve the File Module's service and use its methods to upload files, retrieve files, or delete files.

For example:

```ts
import { Modules } from "@medusajs/framework/utils"
import { 
  createStep,
  createWorkflow,
  StepResponse,
  WorkflowResponse,
} from "@medusajs/framework/workflows-sdk"

const step1 = createStep(
  "step-1",
  async ({}, { container }) => {
    const fileModuleService = container.resolve(
      Modules.FILE
    )

    const { url } = await fileModuleService.retrieveFile("image.png")

    return new StepResponse(url)
  } 
)

export const workflow = createWorkflow(
  "workflow-1",
  () => {
    const url = step1()

    return new WorkflowResponse(url)
  }
)
```

In the example above, you create a workflow that has a step. In the step, you resolve the service of the File Module from the [Medusa container](https://docs.medusajs.com/docs/learn/fundamentals/medusa-container/index.html.md).

Then, you use the `retrieveFile` method of the File Module to retrieve the URL of the file with the name `"image.png"`. The URL is then returned as a response from the step and the workflow.

***

### What is a File Module Provider?

A File Module Provider implements the underlying logic of handling uploads and downloads of assets, such as integrating third-party services. The File Module then uses the registered File Module Provider to handle file operations.

Only one File Module Provider can be registered at a time. If you register multiple providers, the File Module will throw an error.

By default, Medusa uses the [Local File Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/infrastructure-modules/file/local/index.html.md). This module uploads files to the `uploads` directory of your Medusa application.

This is useful for development. However, for production, it’s highly recommended to use other File Module Providers, such as the S3 File Module Provider. You can also [Create a File Provider](https://docs.medusajs.com/references/file-provider-module/index.html.md).

- [Local](https://docs.medusajs.com/infrastructure-modules/file/local/index.html.md)
- [AWS S3 (and Compatible APIs)](https://docs.medusajs.com/infrastructure-modules/file/s3/index.html.md)


# Workflow Engine Module

In this document, you'll learn what a Workflow Engine Module is and how to use it in your Medusa application.

## What is a Workflow Engine Module?

A Workflow Engine Module handles tracking and recording the transactions and statuses of workflows and their steps. It can use custom mechanism or integrate a third-party service.

### Default Workflow Engine Module

Medusa uses the [In-Memory Workflow Engine Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/infrastructure-modules/workflow-engine/in-memory/index.html.md) by default. For production purposes, it's recommended to use the [Redis Workflow Engine Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/infrastructure-modules/workflow-engine/redis/index.html.md) instead.

***

## How to Use the Workflow Engine Module?

You can use the registered Workflow Engine Module as part of the [workflows](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md) you build for your custom features. A workflow is a special function composed of a series of steps that guarantees data consistency and reliable roll-back mechanism.

In a step of your workflow, you can resolve the Workflow Engine Module's service and use its methods to track and record the transactions and statuses of workflows and their steps.

For example:

```ts
import { Modules } from "@medusajs/framework/utils"
import { 
  createStep,
  createWorkflow,
  StepResponse,
  WorkflowResponse,
} from "@medusajs/framework/workflows-sdk"

const step1 = createStep(
  "step-1",
  async ({}, { container }) => {
    const workflowEngineService = container.resolve(
      Modules.WORKFLOW_ENGINE
    )

    const [workflowExecution] = await workflowEngineService.listWorkflowExecutions({
      transaction_id: transaction_id,
    })

    return new StepResponse(workflowExecution)
  } 
)

export const workflow = createWorkflow(
  "workflow-1",
  () => {
    const workflowExecution = step1()

    return new WorkflowResponse(workflowExecution)
  }
)
```

In the example above, you create a workflow that has a step. In the step, you resolve the service of the Workflow Engine Module from the [Medusa container](https://docs.medusajs.com/docs/learn/fundamentals/medusa-container/index.html.md).

Then, you use the `listWorkflowExecutions` method of the Workflow Engine Module to list the workflow executions with the transaction ID `transaction_id`. The workflow execution is then returned as a response from the step and the workflow.

***

## List of Workflow Engine Modules

Medusa provides the following Workflow Engine Modules.

- [In-Memory](https://docs.medusajs.com/infrastructure-modules/workflow-engine/in-memory/index.html.md)
- [Redis](https://docs.medusajs.com/infrastructure-modules/workflow-engine/redis/index.html.md)


# Local Analytics Module Provider

The Local Analytics Module Provider is a simple analytics provider for Medusa that logs analytics events to the console. It's useful for development and debugging purposes.

The Analytics Module and its providers are available starting [Medusa v2.8.3](https://github.com/medusajs/medusa/releases/tag/v2.8.3).

***

## Register the Local Analytics Module

Add the module into the `provider` object of the Analytics Module:

You can use only one Analytics Module Provider in your Medusa application.

```ts title="medusa-config.ts"
module.exports = defineConfig({
  // ...
  modules: [
    {
      resolve: "@medusajs/medusa/analytics",
      options: {
        providers: [
          {
            resolve: "@medusajs/analytics-local",
            id: "local",
          }
        ],
      },
    },
  ],
})
```

***

## Test out the Module

To test the module out, you'll track in the console when an order is placed.

You'll first create a [workflow](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md) that tracks the order completion event. Then, you can execute the workflow in a [subscriber](https://docs.medusajs.com/docs/learn/fundamentals/events-and-subscribers/index.html.md) that listens to the `order.placed` event.

For example, create a workflow at `src/workflows/track-order-placed.ts` with the following content:

```ts title="src/workflows/track-order-created.ts" highlights={workflowHighlights}
import { createWorkflow } from "@medusajs/framework/workflows-sdk"
import { createStep } from "@medusajs/framework/workflows-sdk"
import { Modules } from "@medusajs/framework/utils"
import { OrderDTO } from "@medusajs/framework/types"

type StepInput = {
  order: OrderDTO
}

const trackOrderCreatedStep = createStep(
  "track-order-created-step",
  async ({ order }: StepInput, { container }) => {
    const analyticsModuleService = container.resolve(Modules.ANALYTICS)

    await analyticsModuleService.track({
      event: "order_created",
      userId: order.customer_id,
      properties: {
        order_id: order.id,
        total: order.total,
        items: order.items.map((item) => ({
          variant_id: item.variant_id,
          product_id: item.product_id,
          quantity: item.quantity,
        })),
        customer_id: order.customer_id,
      },
    })
  }
)

type WorkflowInput = {
  order_id: string
}

export const trackOrderCreatedWorkflow = createWorkflow(
  "track-order-created-workflow",
  ({ order_id }: WorkflowInput) => {
    const { data: orders } = useQueryGraphStep({
      entity: "order",
      fields: [
        "*",
        "customer.*",
        "items.*",
      ],
      filters: {
        id: order_id,
      },
    })
    trackOrderCreatedStep({
      order: orders[0],
    })
  }
)
```

This workflow retrieves the order details using the `useQueryGraphStep` and then tracks the order creation event using the `trackOrderCreatedStep`.

In the step, you resolve the service of the Analytics Module from the [Medusa container](https://docs.medusajs.com/docs/learn/fundamentals/medusa-container/index.html.md) and use its `track` method to track the event. This method will use the underlying provider configured (which is the Local Analytics Module Provider, in this case) to track the event.

Next, create a subscriber at `src/subscribers/order-placed.ts` with the following content:

```ts title="src/subscribers/order-placed.ts"
import type {
  SubscriberArgs,
  SubscriberConfig,
} from "@medusajs/framework"
import { trackOrderCreatedWorkflow } from "../workflows/track-order-created"

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

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

This subscriber listens to the `order.placed` event and executes the `trackOrderCreatedWorkflow` workflow, passing the order ID as input.

You'll now track the order creation event whenever an order is placed in your Medusa application. You can test this out by placing an order and checking the console for the tracked event.

***

## Additional Resources

- [How to Use the Analytics Module](https://docs.medusajs.com/references/analytics/service/index.html.md)


# PostHog Analytics Module Provider

The PostHog Analytics Module Provider allows you to integrate [PostHog](https://posthog.com/) with Medusa.

PostHog is an open-source product analytics platform that helps you track user interactions and analyze user behavior in your commerce application.

By integrating PostHog with Medusa, you can track events such as cart additions, order completions, and user sign-ups, enabling you to gain insights into user behavior and optimize your application accordingly.

The Analytics Module and its providers are available starting [Medusa v2.8.3](https://github.com/medusajs/medusa/releases/tag/v2.8.3).

***

## Register the PostHog Analytics Module

### Prerequisites

- [PostHog account](https://app.posthog.com/signup)
- [PostHog API Key](https://posthog.com/docs/getting-started/api-key)

Add the module into the `provider` object of the Analytics Module:

You can use only one provider in your Medusa application.

```ts title="medusa-config.ts"
module.exports = defineConfig({
  // ...
  modules: [
    {
      resolve: "@medusajs/medusa/analytics",
      options: {
        providers: [
          {
            resolve: "@medusajs/analytics-posthog",
            id: "posthog",
            options: {
              posthogEventsKey: process.env.POSTHOG_EVENTS_API_KEY,
              posthogHost: process.env.POSTHOG_HOST,
            },
          }
        ],
      },
    },
  ],
})
```

### Environment Variables

Make sure to add the following environment variables:

```bash
POSTHOG_EVENTS_API_KEY=<YOUR_POSTHOG_EVENTS_API_KEY>
POSTHOG_HOST=<YOUR_POSTHOG_HOST>
```

### PostHog Analytics Module Options

|Option|Description|Default|
|---|---|---|
|\`eventsKey\`|The PostHog API key for tracking events. This is required to authenticate your requests to the PostHog API.|-|
|\`posthogHost\`|The PostHog API host URL.|\`https://eu.i.posthog.com\`|

***

## Test out the Module

To test the module out, you'll track in PostHog when an order is placed.

You'll first create a [workflow](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md) that tracks the order completion event. Then, you can execute the workflow in a [subscriber](https://docs.medusajs.com/docs/learn/fundamentals/events-and-subscribers/index.html.md) that listens to the `order.placed` event.

For example, create a workflow at `src/workflows/track-order-placed.ts` with the following content:

```ts title="src/workflows/track-order-created.ts" highlights={workflowHighlights}
import { createWorkflow } from "@medusajs/framework/workflows-sdk"
import { createStep } from "@medusajs/framework/workflows-sdk"
import { Modules } from "@medusajs/framework/utils"
import { OrderDTO } from "@medusajs/framework/types"

type StepInput = {
  order: OrderDTO
}

const trackOrderCreatedStep = createStep(
  "track-order-created-step",
  async ({ order }: StepInput, { container }) => {
    const analyticsModuleService = container.resolve(Modules.ANALYTICS)

    await analyticsModuleService.track({
      event: "order_created",
      userId: order.customer_id,
      properties: {
        order_id: order.id,
        total: order.total,
        items: order.items.map((item) => ({
          variant_id: item.variant_id,
          product_id: item.product_id,
          quantity: item.quantity,
        })),
        customer_id: order.customer_id,
      },
    })
  }
)

type WorkflowInput = {
  order_id: string
}

export const trackOrderCreatedWorkflow = createWorkflow(
  "track-order-created-workflow",
  ({ order_id }: WorkflowInput) => {
    const { data: orders } = useQueryGraphStep({
      entity: "order",
      fields: [
        "*",
        "customer.*",
        "items.*",
      ],
      filters: {
        id: order_id,
      },
    })
    trackOrderCreatedStep({
      order: orders[0],
    })
  }
)
```

This workflow retrieves the order details using the `useQueryGraphStep` and then tracks the order creation event using the `trackOrderCreatedStep`.

In the step, you resolve the service of the Analytics Module from the [Medusa container](https://docs.medusajs.com/docs/learn/fundamentals/medusa-container/index.html.md) and use its `track` method to track the event. This method will use the underlying provider configured (which is the PostHog Analytics Module Provider, in this case) to track the event.

Next, create a subscriber at `src/subscribers/order-placed.ts` with the following content:

```ts title="src/subscribers/order-placed.ts"
import type {
  SubscriberArgs,
  SubscriberConfig,
} from "@medusajs/framework"
import { trackOrderCreatedWorkflow } from "../workflows/track-order-created"

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

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

This subscriber listens to the `order.placed` event and executes the `trackOrderCreatedWorkflow` workflow, passing the order ID as input.

You'll now track the order creation event whenever an order is placed in your Medusa application. You can test this out by placing an order and checking your PostHog dashboard for the tracked event.

***

## Additional Resources

- [How to Use the Analytics Module](https://docs.medusajs.com/references/analytics/service/index.html.md)


# In-Memory Cache Module

The In-Memory Cache Module uses a plain JavaScript Map object to store the cached data. This module is used by default in your Medusa application.

This module is helpful for development or when you’re testing out Medusa, but it’s not recommended to be used in production.

For production, it’s recommended to use modules like [Redis Cache Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/infrastructure-modules/cache/redis/index.html.md).

***

## Register the In-Memory Cache Module

The In-Memory Cache Module is registered by default in your application.

Add the module into the `modules` property of the exported object in `medusa-config.ts`:

```ts title="medusa-config.ts"
import { Modules } from "@medusajs/framework/utils"
// ...

module.exports = defineConfig({
  // ...
  modules: [
    {
      resolve: "@medusajs/medusa/cache-inmemory",
      options: {
        // optional options
      },
    },
  ],
})
```

### In-Memory Cache Module Options

|Option|Description|Default|
|---|---|---|---|---|
|\`ttl\`|The number of seconds an item can live in the cache before it’s removed.|\`30\`|


# How to Create a Cache Module

In this guide, you’ll learn how to create a Cache Module.

## 1. Create Module Directory

Start by creating a new directory for your module. For example, `src/modules/my-cache`.

***

## 2. Create the Cache Service

Create the file `src/modules/my-cache/service.ts` that holds the implementation of the cache service.

The Cache Module's main service must implement the `ICacheService` interface imported from `@medusajs/framework/types`:

```ts title="src/modules/my-cache/service.ts"
import { ICacheService } from "@medusajs/framework/types"

class MyCacheService implements ICacheService {
  get<T>(key: string): Promise<T> {
    throw new Error("Method not implemented.")
  }
  set(key: string, data: unknown, ttl?: number): Promise<void> {
    throw new Error("Method not implemented.")
  }
  invalidate(key: string): Promise<void> {
    throw new Error("Method not implemented.")
  }
}

export default MyCacheService
```

The service implements the required methods based on the desired caching mechanism.

### Implement get Method

The `get` method retrieves the value of a cached item based on its key.

The method accepts a string as a first parameter, which is the key in the cache. It either returns the cached item or `null` if it doesn’t exist.

For example, to implement this method using Memcached:

```ts title="src/modules/my-cache/service.ts"
class MyCacheService implements ICacheService {
  // ...
  async get<T>(cacheKey: string): Promise<T | null> {
    return new Promise((res, rej) => {
      this.memcached.get(cacheKey, (err, data) => {
        if (err) {
          res(null)
        } else {
          if (data) {
            res(JSON.parse(data))
          } else {
            res(null)
          }
        }
      })
    })
  }
}
```

### Implement set Method

The `set` method is used to set an item in the cache. It accepts three parameters:

1. The first parameter is a string indicating the key of the data being added to the cache. This key can be used later to get or invalidate the cached item.
2. The second parameter is the data to be added to the cache. The data can be of any type.
3. The third parameter is optional. It’s a number indicating how long (in seconds) the data should be kept in the cache.

For example, to implement this method using Memcached:

```ts title="src/modules/my-cache/service.ts"
class MyCacheService implements ICacheService {
  protected TTL = 60
  // ...
  async set(
    key: string,
    data: Record<string, unknown>,
    ttl: number = this.TTL // or any value
  ): Promise<void> {
    return new Promise((res, rej) =>
      this.memcached.set(
        key, JSON.stringify(data), ttl, (err) => {
        if (err) {
          rej(err)
        } else {
          res()
        }
      })
    )
  }
}
```

### Implement invalidate Method

The `invalidate` method removes an item from the cache using its key.

By default, items are removed from the cache when their time-to-live (ttl) expires. The `invalidate` method can be used to remove the item beforehand.

The method accepts a string as a first parameter, which is the key of the item to invalidate and remove from the cache.

For example, to implement this method using Memcached:

```ts title="src/modules/my-cache/service.ts"
class MyCacheService implements ICacheService {
  // ...
  async invalidate(key: string): Promise<void> {
    return new Promise((res, rej) => {
      this.memcached.del(key, (err) => {
        if (err) {
          rej(err)
        } else {
          res()
        }
      })
    })
  }
}
```

***

## 3. Create Module Definition File

Create the file `src/modules/my-cache/index.ts` with the following content:

```ts title="src/modules/my-cache/index.ts"
import MyCacheService from "./service"
import { Module } from "@medusajs/framework/utils"

export default Module("my-cache", {
  service: MyCacheService,
})
```

This exports the module's definition, indicating that the `MyCacheService` is the main service of the module.

***

## 4. Use Module

To use your Cache Module, add it to the `modules` object exported as part of the configurations in `medusa-config.ts`. A Cache Module is added under the `cacheService` key.

For example:

```ts title="medusa-config.ts"
import { Modules } from "@medusajs/framework/utils"

// ...

module.exports = defineConfig({
  // ...
  modules: [
    {
      resolve: "./src/modules/my-cache",
      options: {
        // any options
        ttl: 30,
      },
    },
  ],
})
```


# Local Notification Module Provider

The Local Notification Module Provider simulates sending a notification, but only logs the notification's details in the terminal. This is useful for development.

***

## Register the Local Notification Module

The Local Notification Module Provider is registered by default in your application. It's configured to run on the `feed` channel.

Add the module into the `providers` array of the Notification Module:

Only one provider can be defined for a channel.

```ts title="medusa-config.ts"
import { Modules } from "@medusajs/framework/utils"

// ...

module.exports = defineConfig({
  // ...
  modules: [
    {
      resolve: "@medusajs/medusa/notification",
      options: {
        providers: [
          // ...
          {
            resolve: "@medusajs/medusa/notification-local",
            id: "local",
            options: {
              channels: ["email"],
            },
          },
        ],
      },
    },
  ],
})
```

### Local Notification Module Options

|Option|Description|
|---|---|---|
|\`channels\`|The channels this notification module is used to send notifications for. While the local notification module doesn't actually send the notification,
it's important to specify its channels to make sure it's used when a notification for that channel is created.|


# Redis Cache Module

The Redis Cache Module uses Redis to cache data in your store. In production, it's recommended to use this module.

***

## Register the Redis Cache Module

### Prerequisites

- [Redis installed and Redis server running](https://redis.io/docs/getting-started/installation/)

Add the module into the `modules` property of the exported object in `medusa-config.ts`:

```ts title="medusa-config.ts" highlights={highlights}
import { Modules } from "@medusajs/framework/utils"

// ...

module.exports = defineConfig({
  // ...
  modules: [
    {
      resolve: "@medusajs/medusa/cache-redis",
      options: { 
        redisUrl: process.env.CACHE_REDIS_URL,
      },
    },
  ],
})
```

### Environment Variables

Make sure to add the following environment variables:

```bash
CACHE_REDIS_URL=<YOUR_REDIS_URL>
```

### Redis Cache Module Options

|Option|Description|Required|Default|
|---|---|---|---|---|---|---|
|\`redisUrl\`|A string indicating the Redis connection URL.|Yes|-|
|\`redisOptions\`|An object of Redis options. Refer to the |No|-|
|\`ttl\`|The number of seconds an item can live in the cache before it’s removed.|No|\`30\`|
|\`namespace\`|A string used to prefix all cached keys with |No|\`medusa\`|

***

## Test the Module

To test the module, start the Medusa application:

```bash npm2yarn
npm run dev
```

You'll see the following message in the terminal's logs:

```bash noCopy noReport
Connection to Redis in module 'cache-redis' established
```


# Send Notification with the Notification Module

In this guide, you'll learn about the different ways to send notifications using the Notification Module.

## Using the Create Method

In your resource, such as a [subscriber](https://docs.medusajs.com/docs/learn/fundamentals/events-and-subscribers/index.html.md), resolve the Notification Module's main service and use its `create` method:

```ts title="src/subscribers/product-created.ts" highlights={highlights} collapsibleLines="1-7" expandButtonLabel="Show Imports"
import type {
  SubscriberArgs,
  SubscriberConfig,
} from "@medusajs/framework"
import { Modules } from "@medusajs/framework/utils"
import { INotificationModuleService } from "@medusajs/framework/types"

export default async function productCreateHandler({
  event: { data },
  container,
}: SubscriberArgs<{ id: string }>) {
  const notificationModuleService: INotificationModuleService =
    container.resolve(Modules.NOTIFICATION)

  await notificationModuleService.createNotifications({
    to: "user@gmail.com",
    channel: "email",
    template: "product-created",
    data,
  })
}

export const config: SubscriberConfig = {
  event: "product.created",
}
```

The `create` method accepts an object or an array of objects having the following properties:

- to: (\`string\`) The destination to send the notification to. When sending an email, it'll be the email address. When sending an SMS, it'll be the phone number.
- channel: (\`string\`) The channel to send the notification through. For example, \`email\` or \`sms\`. The module provider defined for that channel will be used to send the notification.
- template: (\`string\`) The ID of the template used for the notification. This is useful for providers like SendGrid, where you define templates within SendGrid and use their IDs here.
- data: (\`Record\<string, unknown>\`) The data to pass along to the template, if necessary.

For a full list of properties accepted, refer to [this guide](https://docs.medusajs.com/references/notification-provider-module#create/index.html.md).

***

## Using the sendNotificationsStep

If you want to send a notification as part of a workflow, You can use the [sendNotificationsStep](https://docs.medusajs.com/references/medusa-workflows/steps/sendNotificationsStep/index.html.md) in your workflow.

For example:

```ts title="src/workflows/send-email.ts"
import { createWorkflow } from "@medusajs/framework/workflows-sdk"
import { 
  sendNotificationsStep, 
  useQueryGraphStep,
} from "@medusajs/medusa/core-flows"

type WorkflowInput = {
  id: string
}

export const sendEmailWorkflow = createWorkflow(
  "send-email-workflow",
  ({ id }: WorkflowInput) => {
    const { data: products } = useQueryGraphStep({
      entity: "product",
      fields: [
        "*",
        "variants.*",
      ],
      filters: {
        id,
      },
    })

    sendNotificationsStep({
      to: "user@gmail.com",
      channel: "email",
      template: "product-created",
      data: {
        product_title: product[0].title,
        product_image: product[0].images[0]?.url,
      },
    })
  }
)
```

For a full list of input properties accepted, refer to the [sendNotificationsStep](https://docs.medusajs.com/references/medusa-workflows/steps/sendNotificationsStep/index.html.md) reference.

You can then execute this workflow in a subscriber, API route, or scheduled job.

For example, you can execute it when a product is created:

```ts title="src/subscribers/product-created.ts"
import type {
  SubscriberArgs,
  SubscriberConfig,
} from "@medusajs/framework"
import { sendEmailWorkflow } from "../workflows/send-email"

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

export const config: SubscriberConfig = {
  event: "product.created",
}
```


# SendGrid Notification Module Provider

The SendGrid Notification Module Provider integrates [SendGrid](https://sendgrid.com) to send emails to users and customers.

## Register the SendGrid Notification Module

### Prerequisites

- [SendGrid account](https://signup.sendgrid.com)
- [Setup SendGrid single sender](https://docs.sendgrid.com/ui/sending-email/sender-verification)
- [SendGrid API Key](https://docs.sendgrid.com/ui/account-and-settings/api-keys)

Add the module into the `providers` array of the Notification Module:

Only one provider can be defined for a channel.

```ts title="medusa-config.ts"
import { Modules } from "@medusajs/framework/utils"

// ...

module.exports = defineConfig({
  // ...
  modules: [
    {
      resolve: "@medusajs/medusa/notification",
      options: {
        providers: [
          // ...
          {
            resolve: "@medusajs/medusa/notification-sendgrid",
            id: "sendgrid",
            options: {
              channels: ["email"],
              api_key: process.env.SENDGRID_API_KEY,
              from: process.env.SENDGRID_FROM,
            },
          },
        ],
      },
    },
  ],
})
```

### Environment Variables

Make sure to add the following environment variables:

```bash
SENDGRID_API_KEY=<YOUR_SENDGRID_API_KEY>
SENDGRID_FROM=<YOUR_SENDGRID_FROM>
```

### SendGrid Notification Module Options

|Option|Description|
|---|---|---|
||The channels this notification module is used to send notifications for.
Only one provider can be defined for a channel.|
|
|
|
|

## SendGrid Templates

When you send a notification, you must specify the ID of the template to use in SendGrid.

Refer to [this SendGrid documentation guide](https://docs.sendgrid.com/ui/sending-email/how-to-send-an-email-with-dynamic-templates) on how to create templates for your different email types.

***

## Test out the Module

To test the module out, you'll listen to the `product.created` event and send an email when a product is created.

Create a [subscriber](https://docs.medusajs.com/docs/learn/fundamentals/events-and-subscribers/index.html.md) at `src/subscribers/product-created.ts` with the following content:

```ts title="src/subscribers/product-created.ts" highlights={highlights} collapsibleLines="1-7" expandButtonLabel="Show Imports"
import type {
  SubscriberArgs,
  SubscriberConfig,
} from "@medusajs/framework"
import { Modules } from "@medusajs/framework/utils"

export default async function productCreateHandler({
  event: { data },
  container,
}: SubscriberArgs<{ id: string }>) {
  const notificationModuleService = container.resolve(Modules.NOTIFICATION)
  const query = container.resolve("query")

  const { data: [product] } = await query.graph({
    entity: "product",
    fields: ["*"],
    filters: {
      id: data.id,
    },
  })

  await notificationModuleService.createNotifications({
    to: "test@gmail.com",
    channel: "email",
    template: "product-created",
    data: {
      product_title: product.title,
      product_image: product.images[0]?.url,
    },
  })
}

export const config: SubscriberConfig = {
  event: "product.created",
}
```

In this subscriber, you:

- Resolve the Notification Module's main service and [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md) from the [Medusa container](https://docs.medusajs.com/docs/learn/fundamentals/medusa-container/index.html.md).
- Retrieve the product's details using Query to pass them to the template in SendGrid.
- Use the `createNotifications` method of the Notification Module's main service to create a notification to be sent to the specified email. By specifying the `email` channel, the SendGrid Notification Module Provider is used to send the notification.
- The `template` property of the `createNotifications` method's parameter specifies the ID of the template defined in SendGrid.
- The `data` property allows you to pass data to the template in SendGrid. For example, the product's title and image.

Then, start the Medusa application:

```bash npm2yarn
npm run dev
```

And create a product either using the [API route](https://docs.medusajs.com/api/admin#products_postproducts) or the [Medusa Admin](https://docs.medusajs.com/user-guide/products/create/index.html.md). This runs the subscriber and sends an email using SendGrid.

### Other Events to Handle

Medusa emits other events that you can handle to send notifications using the SendGrid Notification Module Provider, such as `order.placed` when an order is placed.

Refer to the [Events Reference](https://docs.medusajs.com/references/events/index.html.md) for a complete list of events emitted by Medusa.

### Sending Emails with SendGrid in Workflows

You can also send an email using SendGrid in any [workflow](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md). This allows you to send emails within your custom flows.

You can use the [sendNotifcationStep](https://docs.medusajs.com/references/medusa-workflows/steps/sendNotificationsStep/index.html.md) in your workflow to send an email using SendGrid.

For example:

```ts title="src/workflows/send-email.ts"
import { createWorkflow } from "@medusajs/framework/workflows-sdk"
import { 
  sendNotificationsStep, 
  useQueryGraphStep,
} from "@medusajs/medusa/core-flows"

type WorkflowInput = {
  id: string
}

export const sendEmailWorkflow = createWorkflow(
  "send-email-workflow",
  ({ id }: WorkflowInput) => {
    const { data: products } = useQueryGraphStep({
      entity: "product",
      fields: [
        "*",
        "variants.*",
      ],
      filters: {
        id,
      },
    })

    sendNotificationsStep({
      to: "test@gmail.com",
      channel: "email",
      template: "product-created",
      data: {
        product_title: product[0].title,
        product_image: product[0].images[0]?.url,
      },
    })
  }
)
```

This workflow works similarly to the subscriber. It retrieves the product's details using Query and sends an email using SendGrid (by specifying the `email` channel) to the `test@gmail.com` email.

You can also execute this workflow in a subscriber. For example, you can execute it when a product is created:

```ts title="src/subscribers/product-created.ts"
import type {
  SubscriberArgs,
  SubscriberConfig,
} from "@medusajs/framework"
import { Modules } from "@medusajs/framework/utils"
import { sendEmailWorkflow } from "../workflows/send-email"

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

export const config: SubscriberConfig = {
  event: "product.created",
}
```

This subscriber will run every time a product is created, and it will execute the `sendEmailWorkflow` to send an email using SendGrid.


# How to Create an Event Module

In this guide, you’ll learn how to create an Event Module.

## 1. Create Module Directory

Start by creating a new directory for your module. For example, `src/modules/my-event`.

***

## 2. Create the Event Service

Create the file `src/modules/my-event/service.ts` that holds the implementation of the event service.

The Event Module's main service must extend the `AbstractEventBusModuleService` class from the Medusa Framework:

```ts title="src/modules/my-event/service.ts"
import { AbstractEventBusModuleService } from "@medusajs/framework/utils"
import { Message } from "@medusajs/types"

class MyEventService extends AbstractEventBusModuleService {
  async emit<T>(data: Message<T> | Message<T>[], options: Record<string, unknown>): Promise<void> {
    throw new Error("Method not implemented.")
  }
  async releaseGroupedEvents(eventGroupId: string): Promise<void> {
    throw new Error("Method not implemented.")
  }
  async clearGroupedEvents(eventGroupId: string): Promise<void> {
    throw new Error("Method not implemented.")
  }
}

export default MyEventService
```

The service implements the required methods based on the desired publish/subscribe logic.

### eventToSubscribersMap\_ Property

The `AbstractEventBusModuleService` has a field `eventToSubscribersMap_`, which is a JavaScript Map. The map's keys are the event names, whereas the value of each key is an array of subscribed handler functions.

In your custom implementation, you can use this property to manage the subscribed handler functions:

```ts
const eventSubscribers = 
  this.eventToSubscribersMap_.get(eventName) || []
```

### emit Method

The `emit` method is used to push an event from the Medusa application into your messaging system. The subscribers to that event would then pick up the message and execute their asynchronous tasks.

An example implementation:

```ts title="src/modules/my-event/service.ts"
class MyEventService extends AbstractEventBusModuleService {
  async emit<T>(data: Message<T> | Message<T>[], options: Record<string, unknown>): Promise<void> {
    const events = Array.isArray(data) ? data : [data]

    for (const event of events) {
      console.log(`Received the event ${event.name} with data ${event.data}`)

      // TODO push the event somewhere
    }
  }
  // ...
}
```

The `emit` method receives the following parameters:

- data: (\`object or array of objects\`) The emitted event(s).

  - name: (\`string\`) The name of the emitted event.

  - data: (\`object\`) The data payload of the event.

  - metadata: (\`object\`) Additional details of the emitted event.

    - eventGroupId: (string) A group ID that the event belongs to.

  - options: (\`object\`) Additional options relevant for the event service.

### releaseGroupedEvents Method

Grouped events are useful when you have distributed transactions where you need to explicitly group, release, and clear events upon lifecycle transaction events.

If your Event Module supports grouped events, this method is used to emit all events in a group, then clear that group.

For example:

```ts title="src/modules/my-event/service.ts"
class MyEventService extends AbstractEventBusModuleService {
  protected groupedEventsMap_: Map<string, Message[]>

  constructor() {
    // @ts-ignore
    super(...arguments)

    this.groupedEventsMap_ = new Map()
  }

  async releaseGroupedEvents(eventGroupId: string): Promise<void> {
    const groupedEvents = this.groupedEventsMap_.get(eventGroupId) || []

    for (const event of groupedEvents) {
      const { options, ...eventBody } = event

      // TODO emit event
    }

    await this.clearGroupedEvents(eventGroupId)
  }

  // ...
}
```

The `releaseGroupedEvents` receives the group ID as a parameter.

In the example above, you add a `groupedEventsMap_` property to store grouped events. Then, in the method, you emit the events in the group, then clear the grouped events using the `clearGroupedEvents` which you'll learn about next.

To add events to the grouped events map, you can do it in the `emit` method:

```ts title="src/modules/my-event/service.ts"
class MyEventService extends AbstractEventBusModuleService {
  // ...
  async emit<T>(data: Message<T> | Message<T>[], options: Record<string, unknown>): Promise<void> {
    const events = Array.isArray(data) ? data : [data]

    for (const event of events) {
      console.log(`Received the event ${event.name} with data ${event.data}`)

      if (event.metadata.eventGroupId) {
        const groupedEvents = this.groupedEventsMap_.get(
          event.metadata.eventGroupId
        ) || []

        groupedEvents.push(event)

        this.groupedEventsMap_.set(event.metadata.eventGroupId, groupedEvents)
        continue
      }

      // TODO push the event somewhere
    }
  }
}
```

### clearGroupedEvents Method

If your Event Module supports grouped events, this method is used to remove the events of a group.

For example:

```ts title="src/modules/my-event/service.ts"
class MyEventService extends AbstractEventBusModuleService {
  // from previous section
  protected groupedEventsMap_: Map<string, Message[]>

  async clearGroupedEvents(eventGroupId: string): Promise<void> {
    this.groupedEventsMap_.delete(eventGroupId)
  }

  // ...
}
```

The method accepts the group's name as a parameter.

In the method, you delete the group from the `groupedEventsMap_` property (added in the previous section), deleting the stored events of it as well.

***

## 3. Create Module Definition File

Create the file `src/modules/my-event/index.ts` with the following content:

```ts title="src/modules/my-event/index.ts"
import MyEventService from "./service"
import { Module } from "@medusajs/framework/utils"

export default Module("my-event", {
  service: MyEventService,
})
```

This exports the module's definition, indicating that the `MyEventService` is the main service of the module.

***

## 4. Use Module

To use your Event Module, add it to the `modules` object exported as part of the configurations in `medusa-config.ts`. An Event Module is added under the `eventBus` key.

For example:

```ts title="medusa-config.ts"
import { Modules } from "@medusajs/framework/utils"

// ...

module.exports = defineConfig({
  // ...
  modules: [
    {
      resolve: "./src/modules/my-event",
      options: { 
        // any options
      },
    },
  ],
})
```


# Local Event Module

The Local Event Module uses Node EventEmitter to implement Medusa's pub/sub events system. The Node EventEmitter is limited to a single process environment.

This module is useful for development and testing, but it’s not recommended to be used in production.

For production, it’s recommended to use modules like [Redis Event Bus Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/infrastructure-modules/event/redis/index.html.md).

***

## Register the Local Event Module

The Local Event Module is registered by default in your application.

Add the module into the `modules` property of the exported object in `medusa-config.ts`:

```ts title="medusa-config.ts"
import { Modules } from "@medusajs/framework/utils"

// ...

module.exports = defineConfig({
  // ...
  modules: [
    {
      resolve: "@medusajs/medusa/event-bus-local",
    },
  ],
})
```

***

## Test the Module

To test the module, start the Medusa application:

```bash npm2yarn
npm run dev
```

You'll see the following message in the terminal's logs:

```bash noCopy noReport
Local Event Bus installed. This is not recommended for production.
```


# Redis Event Module

The Redis Event Module uses Redis to implement Medusa's pub/sub events system.

It's powered by BullMQ and `io-redis`. BullMQ is responsible for the message queue and worker, and `io-redis` is the underlying Redis client that BullMQ connects to for events storage.

In production, it's recommended to use this module.

***

## Register the Redis Event Module

### Prerequisites

- [Redis installed and Redis server running](https://redis.io/docs/getting-started/installation/)

Add the module into the `modules` property of the exported object in `medusa-config.ts`:

```ts title="medusa-config.ts"
import { Modules } from "@medusajs/framework/utils"

// ...

module.exports = defineConfig({
  // ...
  modules: [
    {
      resolve: "@medusajs/medusa/event-bus-redis",
      options: { 
        redisUrl: process.env.EVENTS_REDIS_URL,
      },
    },
  ],
})
```

### Environment Variables

Make sure to add the following environment variables:

```bash
EVENTS_REDIS_URL=<YOUR_REDIS_URL>
```

### Redis Event Module Options

|Option|Description|Required|Default|
|---|---|---|---|---|---|---|
|\`redisUrl\`|A string indicating the Redis connection URL.|Yes|-|
|\`redisOptions\`|An object of Redis options. Refer to the |No|-|
|\`queueName\`|A string indicating BullMQ's queue name.|No|\`events-queue\`|
|\`queueOptions\`|An object of options to pass to the BullMQ constructor. Refer to |No|-|
|\`workerOptions\`|An object of options to pass to the BullMQ Worker constructor. Refer to |No|-|
|\`jobOptions\`|An object of options to pass to jobs added to the BullMQ queue. Refer to |No|-|

## Test the Module

To test the module, start the Medusa application:

```bash npm2yarn
npm run dev
```

You'll see the following message in the terminal's logs:

```bash noCopy noReport
Connection to Redis in module 'event-redis' established
```


# PostgreSQL Locking Module Provider

The PostgreSQL Locking Module Provider uses PostgreSQL's advisory locks to control and manage locks across multiple instances of Medusa. Advisory locks are lightweight locks that do not interfere with other database transactions. By using PostgreSQL's advisory locks, Medusa can create distributed locks directly through the database.

The provider uses the existing PostgreSQL database in your application to manage locks, so you don't need to set up a separate database or service to manage locks.

While this provider is suitable for production environments, it's recommended to use the [Redis Locking Module Provider](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/infrastructure-modules/locking/redis/index.html.md) if possible.

***

## Register the PostgreSQL Locking Module Provider

To register the PostgreSQL Locking Module Provider, add it to the list of providers of the Locking Module in `medusa-config.ts`:

```ts title="medusa-config.ts"
module.exports = defineConfig({
  // ...
  modules: [
    {
      resolve: "@medusajs/medusa/locking",
      options: {
        providers: [
          {
            resolve: "@medusajs/medusa/locking-postgres",
            id: "locking-postgres",
            // set this if you want this provider to be used by default
            // and you have other Locking Module Providers registered.
            is_default: true,
          },
        ],
      },
    },
  ],
})
```

### Run Migrations

The PostgreSQL Locking Module Provider requires a new `locking` table in the database to store the locks. So, you must run the migrations after registering the provider:

```bash
npx medusa db:migrate
```

This will run the migration in the PostgreSQL Locking Module Provider and create the necessary table in the database.

***

## Use Provider with Locking Module

The PostgreSQL Locking Module Provider will be the default provider if you don't register any other providers, or if you set the `is_default` flag to `true`:

```ts title="medusa-config.ts" highlights={defaultHighlights}
module.exports = defineConfig({
  // ...
  modules: [
    {
      resolve: "@medusajs/medusa/locking",
      options: {
        providers: [
          {
            resolve: "@medusajs/medusa/locking-postgres",
            id: "locking-postgres",
            is_default: true,
          },
        ],
      },
    },
  ],
})
```

If you use the Locking Module in your customizations, the PostgreSQL Locking Module Provider will be used by default in this case. You can also explicitly use this provider by passing its identifier `lp_locking-postgres` to the Locking Module's service methods.

For example, when using the `acquire` method in a [workflow step](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md):

```ts
import { Modules } from "@medusajs/framework/utils"
import { createStep } from "@medusajs/framework/workflows-sdk"

const step1 = createStep(
  "step-1",
  async ({}, { container }) => {
    const lockingModuleService = container.resolve(
      Modules.LOCKING
    )

    await lockingModuleService.acquire("prod_123", {
      provider: "lp_locking-postgres",
    })
  } 
)
```


# Redis Locking Module Provider

The Redis Locking Module Provider uses Redis to manage locks across multiple instances of Medusa. Redis ensures that locks are globally available, which is ideal for distributed environments.

This provider is recommended for production environments where Medusa is running in a multi-instance setup.

***

## Register the Redis Locking Module Provider

### Prerequisites

- [A redis server set up locally or a database in your deployed application.](https://redis.io/download)

To register the Redis Locking Module Provider, add it to the list of providers of the Locking Module in `medusa-config.ts`:

```ts title="medusa-config.ts"
module.exports = defineConfig({
  // ...
  modules: [
    {
      resolve: "@medusajs/medusa/locking",
      options: {
        providers: [
          {
            resolve: "@medusajs/medusa/locking-redis",
            id: "locking-redis",
            // set this if you want this provider to be used by default
            // and you have other Locking Module Providers registered.
            is_default: true,
            options: {
              redisUrl: process.env.LOCKING_REDIS_URL,
            },
          },
        ],
      },
    },
  ],
})
```

### Environment Variables

Make sure to add the following environment variable:

```bash
LOCKING_REDIS_URL=<YOUR_LOCKING_REDIS_URL>
```

Where `<YOUR_LOCKING_REDIS_URL>` is the URL of your Redis server, either locally or in the deployed environment.

The default Redis URL in a local environment is `redis://localhost:6379`.

### Redis Locking Module Provider Options

|Option|Description|Required|Default|
|---|---|---|---|---|---|---|
|\`redisUrl\`|A string indicating the Redis connection URL.|Yes|-|
|\`redisOptions\`|An object of Redis options. Refer to the |No|-|
|\`namespace\`|A string used to prefix all locked keys with |No|\`medusa\_lock:\`|
|\`waitLockingTimeout\`|A number indicating the default timeout (in seconds) to wait while acquiring a lock. This timeout is used when no timeout is specified when executing an asynchronous job or acquiring a lock.|No|\`5\`|
|\`defaultRetryInterval\`|A number indicating the time (in milliseconds) to wait before retrying to acquire a lock.|No|\`5\`|
|\`maximumRetryInterval\`|A number indicating the maximum time (in milliseconds) to wait before retrying to acquire a lock.|No|\`200\`|

***

## Test out the Module

To test out the Redis Locking Module Provider, start the Medusa application:

```bash npm2yarn
npm run dev
```

You'll see the following message logged in the terminal:

```bash
info:    Connection to Redis in "locking-redis" provider established
```

This message indicates that the Redis Locking Module Provider has successfully connected to the Redis server.

If you set the `is_default` flag to `true` in the provider options or you only registered the Redis Locking Module Provider, the Locking Module will use it by default for all locking operations.

***

## Use Provider with Locking Module

The Redis Locking Module Provider will be the default provider if you don't register any other providers, or if you set the `is_default` flag to `true`:

```ts title="medusa-config.ts" highlights={defaultHighlights}
module.exports = defineConfig({
  // ...
  modules: [
    {
      resolve: "@medusajs/medusa/locking",
      options: {
        providers: [
          {
            resolve: "@medusajs/medusa/locking-redis",
            id: "locking-redis",
            is_default: true,
            options: {
              // ...
            },
          },
        ],
      },
    },
  ],
})
```

If you use the Locking Module in your customizations, the Redis Locking Module Provider will be used by default in this case. You can also explicitly use this provider by passing its identifier `lp_locking-redis` to the Locking Module's service methods.

For example, when using the `acquire` method in a [workflow step](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md):

```ts
import { Modules } from "@medusajs/framework/utils"
import { createStep } from "@medusajs/framework/workflows-sdk"

const step1 = createStep(
  "step-1",
  async ({}, { container }) => {
    const lockingModuleService = container.resolve(
      Modules.LOCKING
    )

    await lockingModuleService.acquire("prod_123", {
      provider: "lp_locking-redis",
    })
  } 
)
```


# Local File Module Provider

The Local File Module Provider stores files uploaded to your Medusa application in the `/uploads` directory.

- The Local File Module Provider is only for development purposes. Use the [S3 File Module Provider](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/infrastructure-modules/file/s3/index.html.md) in production instead.
- The Local File Module Provider will only read files uploaded through Medusa. It will not read files uploaded manually to the `static` (or other configured) directory.

***

## Register the Local File Module

The Local File Module Provider is registered by default in your application.

Add the module into the `providers` array of the File Module:

The File Module accepts one provider only.

```ts title="medusa-config.ts"
import { Modules } from "@medusajs/framework/utils"

// ...

module.exports = {
  // ...
  modules: [
    {
      resolve: "@medusajs/medusa/file",
      options: {
        providers: [
          {
            resolve: "@medusajs/medusa/file-local",
            id: "local",
            options: {
              // provider options...
            },
          },
        ],
      },
    },
  ],
}
```

### Local File Module Options

|Option|Description|Default|
|---|---|---|---|---|
|\`upload\_dir\`|The directory to upload files to. Medusa exposes the content of the |\`static\`|
|\`backend\_url\`|The URL that serves the files.|\`http://localhost:9000/static\`|


# S3 File Module Provider

The S3 File Module Provider integrates Amazon S3 and services following a compatible API (such as MinIO or DigitalOcean Spaces) to store files uploaded to your Medusa application.

Medusa Cloud offers a managed file storage solution with AWS S3 for your Medusa application. Contact the [sales team](https://medusajs.com/pricing/) to learn more.

## Prerequisites

### AWS S3

- [AWS account](https://console.aws.amazon.com/console/home?nc2=h_ct\&src=header-signin).
- Create [AWS user with AmazonS3FullAccess permissions](https://docs.aws.amazon.com/apigateway/latest/developerguide/api-gateway-create-and-attach-iam-policy.html).
- Create [AWS user access key ID and secret access key](https://docs.aws.amazon.com/IAM/latest/UserGuide/id_credentials_access-keys.html#Using_CreateAccessKey).
- Create [S3 bucket](https://docs.aws.amazon.com/AmazonS3/latest/userguide/create-bucket-overview.html) with the "Public Access setting" enabled:
  1. On your bucket's dashboard, click on the Permissions tab.
  2. Click on the Edit button of the Block public access (bucket settings) section.
  3. In the form that opens, don't toggle any checkboxes and click the "Save changes" button.
  4. Confirm saving the changes by entering `confirm` in the pop-up that shows.
  5. Back on the Permissions page, scroll to the Object Ownership section and click the Edit button.
  6. In the form that opens:
     - Choose the "ACLs enabled" card.
     - Click on the "Save changes" button.
  7. Back on the Permissions page, scroll to the "Access Control List (ACL)" section and click on the Edit button.
  8. In the form that opens, enable the Read permission for "Everyone (public access)".
  9. Check the "I understand the effects of these changes on my objects and buckets." checkbox.
  10. Click on the "Save changes" button.

### MinIO

- Create [DigitalOcean account](https://cloud.digitalocean.com/registrations/new).
- Create [DigitalOcean Spaces bucket](https://docs.digitalocean.com/products/spaces/how-to/create/).
- Create [DigitalOcean Spaces access and secret access keys](https://docs.digitalocean.com/products/spaces/how-to/manage-access/#access-keys).

### DigitalOcean Spaces

1. Create a [Cloudflare account](https://dash.cloudflare.com/sign-up).
2. Set up your R2 bucket:
   - Navigate to R2 Object Storage in your dashboard. You may need to provide your credit-card information.
   - Click "Create bucket"
   - Enter a unique bucket name
   - Select "Automatic" for location
   - Choose "Standard" for storage class
   - Confirm by clicking "Create bucket"
3. Configure public access:
   - On your bucket's dashboard, click on the Settings tab.
   - Scroll down to the Public Access section, and click on "Allow Access" in the "R2.dev subdomain" card.
   - Type 'allow' to confirm
   - Copy the Public R2.dev Bucket URL for your `S3_FILE_URL`
4. Retrieve credentials:
   - [Go to API tokens page](https://dash.cloudflare.com/?to=/:account/r2/api-tokens):
     - Select "Create API token"
     - Edit the "R2 Token" name
     - Under Permissions, select Object Read & Write permission types
     - You can optionally specify the buckets that this API token has access to under the "Specify bucket(s)" section.
     - Once done, click the "Create API Token" button.
     - You'll receive an access key ID and a secret access key. Save them to use them later for the `S3_ACCESS_KEY_ID` and `S3_SECRET_ACCESS_KEY` environment variables.

### Supabase S3 Storage

### Cloudflare R2

***

## Register the S3 File Module

Add the module into the `providers` array of the File Module:

The File Module accepts one provider only.

```ts title="medusa-config.ts"
import { Modules } from "@medusajs/framework/utils"

// ...

module.exports = {
  // ...
  modules: [
    // ...
    {
      resolve: "@medusajs/medusa/file",
      options: {
        providers: [
          {
            resolve: "@medusajs/medusa/file-s3",
            id: "s3",
            options: {
              file_url: process.env.S3_FILE_URL,
              access_key_id: process.env.S3_ACCESS_KEY_ID,
              secret_access_key: process.env.S3_SECRET_ACCESS_KEY,
              region: process.env.S3_REGION,
              bucket: process.env.S3_BUCKET,
              endpoint: process.env.S3_ENDPOINT,
              // other options...
            },
          },
        ],
      },
    },
  ],
}
```

### Additional Configuration for MinIO and Supabase

If you're using MinIO or Supabase, set `forcePathStyle` to `true` in the `additional_client_config` object.

For example:

```ts title="medusa-config.ts"
module.exports = defineConfig({
  // ...
  modules: [
    {
      resolve: "@medusajs/medusa/file",
      options: {
        providers: [
          {
            resolve: "@medusajs/medusa/file-s3",
            id: "s3",
            options: {
              // ...
              additional_client_config: {
                forcePathStyle: true,
              },
            },
          },
        ],
      },
    },
  ],
})
```

### S3 File Module Options

|Option|Description|Default|
|---|---|---|---|---|
|\`file\_url\`|The base URL to upload files to.|-|
|\`access\_key\_id\`|The AWS or (S3 compatible) user's access key ID.|-|
|\`secret\_access\_key\`|The AWS or (S3 compatible) user's secret access key.|-|
|\`region\`|The bucket's region code.|-|
|\`bucket\`|The bucket's name.|-|
|\`endpoint\`|The URL to the AWS S3 (or compatible S3 API) server.|-|
|\`prefix\`|A string to prefix each uploaded file's name.|-|
|\`cache\_control\`|A string indicating how long objects remain in the AWS S3 (or compatible S3 API) cache.|\`public, max-age=31536000\`|
|\`download\_file\_duration\`|A number indicating the expiry time of presigned URLs in seconds.|\`3600\`|
|\`additional\_client\_config\`|Any additional configurations to pass to the S3 client.|-|

***

## Troubleshooting


# How to Use the Workflow Engine Module

In this document, you’ll learn about the different methods in the Workflow Engine Module's service and how to use them.

***

## Resolve Workflow Engine Module's Service

In your workflow's step, you can resolve the Workflow Engine Module's service from the Medusa container:

```ts
import { Modules } from "@medusajs/framework/utils"
import { createStep } from "@medusajs/framework/workflows-sdk"

const step1 = createStep(
  "step-1",
  async ({}, { container }) => {
    const workflowEngineModuleService = container.resolve(
      Modules.WORKFLOW_ENGINE
    )
    
    // TODO use workflowEngineModuleService
  } 
)
```

This will resolve the service of the configured Workflow Engine Module, which is the [In-Memory Workflow Engine Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/infrastructure-modules/workflow-engine/in-memory/index.html.md) by default.

You can then use the Workflow Engine Module's service's methods in the step. The rest of this guide details these methods.

***

## setStepSuccess

This method sets an async step in a currently-executing [long-running workflow](https://docs.medusajs.com/docs/learn/fundamentals/workflows/long-running-workflow/index.html.md) as successful. The workflow will then continue to the next step.

### Example

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

await workflowEngineModuleService.setStepSuccess({
  idempotencyKey: {
    action: TransactionHandlerType.INVOKE,
    transactionId,
    stepId: "step-2",
    workflowId: "hello-world",
  },
  stepResponse: new StepResponse("Done!"),
  options: {
    container,
  },
})
```

### Parameters

- idempotencyKey: (\`object\`) The details of the step to set as successful.

  - action: (\`invoke\` | \`compensate\`) If the step's compensation function is running, use \`compensate\`. Otherwise, use \`invoke\`.

  - transactionId: (\`string\`) The ID of the workflow execution's transaction.

  - stepId: (\`string\`) The ID of the step to change its status. This is the first parameter passed to \`createStep\` when creating the step.

  - workflowId: (\`string\`) The ID of the workflow. This is the first parameter passed to \`createWorkflow\` when creating the workflow.
- stepResponse: (\`StepResponse\`) Set the response of the step. This is similar to the response you return in a step's definition, but since the async step doesn't have a response, you set its response when changing its status.
- options: (\`object\`) Options to pass to the step.

  - container: (\`Container\`) An instance of the Medusa container.

***

## setStepFailure

This method sets an async step in a currently-executing [long-running workflow](https://docs.medusajs.com/docs/learn/fundamentals/workflows/long-running-workflow/index.html.md) as failed. The workflow will then stop executing and the compensation functions of the workflow's steps will be executed.

### Example

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

await workflowEngineModuleService.setStepFailure({
  idempotencyKey: {
    action: TransactionHandlerType.INVOKE,
    transactionId,
    stepId: "step-2",
    workflowId: "hello-world",
  },
  stepResponse: new StepResponse("Failed!"),
  options: {
    container,
  },
})
```

### Parameters

- idempotencyKey: (\`object\`) The details of the step to set as failed.

  - action: (\`invoke\` | \`compensate\`) If the step's compensation function is running, use \`compensate\`. Otherwise, use \`invoke\`.

  - transactionId: (\`string\`) The ID of the workflow execution's transaction.

  - stepId: (\`string\`) The ID of the step to change its status. This is the first parameter passed to \`createStep\` when creating the step.

  - workflowId: (\`string\`) The ID of the workflow. This is the first parameter passed to \`createWorkflow\` when creating the workflow.
- stepResponse: (\`StepResponse\`) Set the response of the step. This is similar to the response you return in a step's definition, but since the async step doesn't have a response, you set its response when changing its status.
- options: (\`object\`) Options to pass to the step.

  - container: (\`Container\`) An instance of the Medusa container.

***

## subscribe

This method subscribes to a workflow's events. You can use this method to listen to a [long-running workflow](https://docs.medusajs.com/docs/learn/fundamentals/workflows/long-running-workflow/index.html.md)'s events and retrieve its result once it's done executing.

Refer to the [Long-Running Workflows](https://docs.medusajs.com/docs/learn/fundamentals/workflows/long-running-workflow#access-long-running-workflow-status-and-result/index.html.md) documentation to learn more.

### Example

```ts
const { transaction } = await helloWorldWorkflow(container).run()

const subscriptionOptions = {
  workflowId: "hello-world",
  transactionId: transaction.transactionId,
  subscriberId: "hello-world-subscriber",
}

await workflowEngineModuleService.subscribe({
  ...subscriptionOptions,
  subscriber: async (data) => {
    if (data.eventType === "onFinish") {
      console.log("Finished execution", data.result)
      // unsubscribe
      await workflowEngineModuleService.unsubscribe({
        ...subscriptionOptions,
        subscriberOrId: subscriptionOptions.subscriberId,
      })
    } else if (data.eventType === "onStepFailure") {
      console.log("Workflow failed", data.step)
    }
  },
})
```

### Parameters

- subscriptionOptions: (\`object\`) The options for the subscription.

  - workflowId: (\`string\`) The ID of the workflow to subscribe to. This is the first parameter passed to \`createWorkflow\` when creating the workflow.

  - transactionId: (\`string\`) The ID of the workflow execution's transaction. This is returned when you execute a workflow.

  - subscriberId: (\`string\`) A unique ID for the subscriber. It's used to unsubscribe from the workflow's events.

  - subscriber: (\`(data: WorkflowEvent) => void\`) The subscriber function that will be called when the workflow emits an event.

***

## unsubscribe

This method unsubscribes from a workflow's events. You can use this method to stop listening to a [long-running workflow](https://docs.medusajs.com/docs/learn/fundamentals/workflows/long-running-workflow/index.html.md)'s events after you've received the result.

### Example

```ts
await workflowEngineModuleService.unsubscribe({
  workflowId: "hello-world",
  transactionId: "transaction-id",
  subscriberOrId: "hello-world-subscriber",
})
```

### Parameters

- workflowId: (\`string\`) The ID of the workflow to unsubscribe from. This is the first parameter passed to \`createWorkflow\` when creating the workflow.
- transactionId: (\`string\`) The ID of the workflow execution's transaction. This is returned when you execute a workflow.
- subscriberOrId: (\`string\`) The subscriber ID or the subscriber function to unsubscribe from the workflow's events.


# In-Memory Workflow Engine Module

The In-Memory Workflow Engine Module uses a plain JavaScript Map object to store the workflow executions.

This module is helpful for development or when you’re testing out Medusa, but it’s not recommended to be used in production.

For production, it’s recommended to use modules like [Redis Workflow Engine Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/infrastructure-modules/workflow-engine/redis/index.html.md).

***

## Register the In-Memory Workflow Engine Module

The In-Memory Workflow Engine Module is registered by default in your application.

Add the module into the `modules` property of the exported object in `medusa-config.ts`:

```ts title="medusa-config.ts"
import { Modules } from "@medusajs/framework/utils"

// ...

module.exports = defineConfig({
  // ...
  modules: [
    {
      resolve: "@medusajs/medusa/workflow-engine-inmemory",
    },
  ],
})
```


# Redis Workflow Engine Module

The Redis Workflow Engine Module uses Redis to track workflow executions and handle their subscribers. In production, it's recommended to use this module.

***

## Register the Redis Workflow Engine Module

### Prerequisites

- [Redis installed and Redis server running](https://redis.io/docs/getting-started/installation/)

Add the module into the `modules` property of the exported object in `medusa-config.ts`:

```ts title="medusa-config.ts" highlights={highlights}
import { Modules } from "@medusajs/framework/utils"

// ...

module.exports = defineConfig({
  // ...
  modules: [
    {
      resolve: "@medusajs/medusa/workflow-engine-redis",
      options: {
        redis: {
          url: process.env.WE_REDIS_URL,
        },
      },
    },
  ],
})
```

### Environment Variables

Make sure to add the following environment variables:

```bash
WE_REDIS_URL=<YOUR_REDIS_URL>
```

### Redis Workflow Engine Module Options

|Option|Description|Required|Default|
|---|---|---|---|---|---|---|
|\`url\`|A string indicating the Redis connection URL.|No. If not provided, you must provide the |-|
|\`options\`|An object of Redis options. Refer to the |No|-|
|\`queueName\`|The name of the queue used to keep track of retries and timeouts.|No|\`medusa-workflows\`|
|\`pubsub\`|A connection object having the following properties:|No. If not provided, you must provide the |-|

## Test the Module

To test the module, start the Medusa application:

```bash npm2yarn
npm run dev
```

You'll see the following message in the terminal's logs:

```bash noCopy noReport
Connection to Redis in module 'workflow-engine-redis' established
```


## Workflows

- [linkSalesChannelsToApiKeyWorkflow](https://docs.medusajs.com/references/medusa-workflows/linkSalesChannelsToApiKeyWorkflow/index.html.md)
- [createApiKeysWorkflow](https://docs.medusajs.com/references/medusa-workflows/createApiKeysWorkflow/index.html.md)
- [revokeApiKeysWorkflow](https://docs.medusajs.com/references/medusa-workflows/revokeApiKeysWorkflow/index.html.md)
- [deleteApiKeysWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteApiKeysWorkflow/index.html.md)
- [updateApiKeysWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateApiKeysWorkflow/index.html.md)
- [addShippingMethodToCartWorkflow](https://docs.medusajs.com/references/medusa-workflows/addShippingMethodToCartWorkflow/index.html.md)
- [completeCartWorkflow](https://docs.medusajs.com/references/medusa-workflows/completeCartWorkflow/index.html.md)
- [confirmVariantInventoryWorkflow](https://docs.medusajs.com/references/medusa-workflows/confirmVariantInventoryWorkflow/index.html.md)
- [createCartCreditLinesWorkflow](https://docs.medusajs.com/references/medusa-workflows/createCartCreditLinesWorkflow/index.html.md)
- [createCartWorkflow](https://docs.medusajs.com/references/medusa-workflows/createCartWorkflow/index.html.md)
- [deleteCartCreditLinesWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteCartCreditLinesWorkflow/index.html.md)
- [createPaymentCollectionForCartWorkflow](https://docs.medusajs.com/references/medusa-workflows/createPaymentCollectionForCartWorkflow/index.html.md)
- [addToCartWorkflow](https://docs.medusajs.com/references/medusa-workflows/addToCartWorkflow/index.html.md)
- [listShippingOptionsForCartWithPricingWorkflow](https://docs.medusajs.com/references/medusa-workflows/listShippingOptionsForCartWithPricingWorkflow/index.html.md)
- [listShippingOptionsForCartWorkflow](https://docs.medusajs.com/references/medusa-workflows/listShippingOptionsForCartWorkflow/index.html.md)
- [refreshCartItemsWorkflow](https://docs.medusajs.com/references/medusa-workflows/refreshCartItemsWorkflow/index.html.md)
- [refreshPaymentCollectionForCartWorkflow](https://docs.medusajs.com/references/medusa-workflows/refreshPaymentCollectionForCartWorkflow/index.html.md)
- [transferCartCustomerWorkflow](https://docs.medusajs.com/references/medusa-workflows/transferCartCustomerWorkflow/index.html.md)
- [refundPaymentAndRecreatePaymentSessionWorkflow](https://docs.medusajs.com/references/medusa-workflows/refundPaymentAndRecreatePaymentSessionWorkflow/index.html.md)
- [updateCartWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateCartWorkflow/index.html.md)
- [updateLineItemInCartWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateLineItemInCartWorkflow/index.html.md)
- [refreshCartShippingMethodsWorkflow](https://docs.medusajs.com/references/medusa-workflows/refreshCartShippingMethodsWorkflow/index.html.md)
- [updateCartPromotionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateCartPromotionsWorkflow/index.html.md)
- [updateTaxLinesWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateTaxLinesWorkflow/index.html.md)
- [validateExistingPaymentCollectionStep](https://docs.medusajs.com/references/medusa-workflows/validateExistingPaymentCollectionStep/index.html.md)
- [batchLinksWorkflow](https://docs.medusajs.com/references/medusa-workflows/batchLinksWorkflow/index.html.md)
- [createLinksWorkflow](https://docs.medusajs.com/references/medusa-workflows/createLinksWorkflow/index.html.md)
- [dismissLinksWorkflow](https://docs.medusajs.com/references/medusa-workflows/dismissLinksWorkflow/index.html.md)
- [updateLinksWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateLinksWorkflow/index.html.md)
- [createDefaultsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createDefaultsWorkflow/index.html.md)
- [createCustomerGroupsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createCustomerGroupsWorkflow/index.html.md)
- [deleteCustomerGroupsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteCustomerGroupsWorkflow/index.html.md)
- [linkCustomerGroupsToCustomerWorkflow](https://docs.medusajs.com/references/medusa-workflows/linkCustomerGroupsToCustomerWorkflow/index.html.md)
- [linkCustomersToCustomerGroupWorkflow](https://docs.medusajs.com/references/medusa-workflows/linkCustomersToCustomerGroupWorkflow/index.html.md)
- [updateCustomerGroupsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateCustomerGroupsWorkflow/index.html.md)
- [deleteFilesWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteFilesWorkflow/index.html.md)
- [uploadFilesWorkflow](https://docs.medusajs.com/references/medusa-workflows/uploadFilesWorkflow/index.html.md)
- [addDraftOrderItemsWorkflow](https://docs.medusajs.com/references/medusa-workflows/addDraftOrderItemsWorkflow/index.html.md)
- [addDraftOrderPromotionWorkflow](https://docs.medusajs.com/references/medusa-workflows/addDraftOrderPromotionWorkflow/index.html.md)
- [cancelDraftOrderEditWorkflow](https://docs.medusajs.com/references/medusa-workflows/cancelDraftOrderEditWorkflow/index.html.md)
- [confirmDraftOrderEditWorkflow](https://docs.medusajs.com/references/medusa-workflows/confirmDraftOrderEditWorkflow/index.html.md)
- [addDraftOrderShippingMethodsWorkflow](https://docs.medusajs.com/references/medusa-workflows/addDraftOrderShippingMethodsWorkflow/index.html.md)
- [beginDraftOrderEditWorkflow](https://docs.medusajs.com/references/medusa-workflows/beginDraftOrderEditWorkflow/index.html.md)
- [convertDraftOrderWorkflow](https://docs.medusajs.com/references/medusa-workflows/convertDraftOrderWorkflow/index.html.md)
- [removeDraftOrderActionItemWorkflow](https://docs.medusajs.com/references/medusa-workflows/removeDraftOrderActionItemWorkflow/index.html.md)
- [convertDraftOrderStep](https://docs.medusajs.com/references/medusa-workflows/convertDraftOrderStep/index.html.md)
- [removeDraftOrderActionShippingMethodWorkflow](https://docs.medusajs.com/references/medusa-workflows/removeDraftOrderActionShippingMethodWorkflow/index.html.md)
- [removeDraftOrderPromotionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/removeDraftOrderPromotionsWorkflow/index.html.md)
- [removeDraftOrderShippingMethodWorkflow](https://docs.medusajs.com/references/medusa-workflows/removeDraftOrderShippingMethodWorkflow/index.html.md)
- [updateDraftOrderActionItemWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateDraftOrderActionItemWorkflow/index.html.md)
- [updateDraftOrderActionShippingMethodWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateDraftOrderActionShippingMethodWorkflow/index.html.md)
- [updateDraftOrderItemWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateDraftOrderItemWorkflow/index.html.md)
- [updateDraftOrderShippingMethodWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateDraftOrderShippingMethodWorkflow/index.html.md)
- [updateDraftOrderStep](https://docs.medusajs.com/references/medusa-workflows/updateDraftOrderStep/index.html.md)
- [updateDraftOrderWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateDraftOrderWorkflow/index.html.md)
- [requestDraftOrderEditWorkflow](https://docs.medusajs.com/references/medusa-workflows/requestDraftOrderEditWorkflow/index.html.md)
- [cancelFulfillmentWorkflow](https://docs.medusajs.com/references/medusa-workflows/cancelFulfillmentWorkflow/index.html.md)
- [batchShippingOptionRulesWorkflow](https://docs.medusajs.com/references/medusa-workflows/batchShippingOptionRulesWorkflow/index.html.md)
- [calculateShippingOptionsPricesWorkflow](https://docs.medusajs.com/references/medusa-workflows/calculateShippingOptionsPricesWorkflow/index.html.md)
- [createFulfillmentWorkflow](https://docs.medusajs.com/references/medusa-workflows/createFulfillmentWorkflow/index.html.md)
- [createReturnFulfillmentWorkflow](https://docs.medusajs.com/references/medusa-workflows/createReturnFulfillmentWorkflow/index.html.md)
- [createShippingOptionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createShippingOptionsWorkflow/index.html.md)
- [createShipmentWorkflow](https://docs.medusajs.com/references/medusa-workflows/createShipmentWorkflow/index.html.md)
- [createServiceZonesWorkflow](https://docs.medusajs.com/references/medusa-workflows/createServiceZonesWorkflow/index.html.md)
- [createShippingProfilesWorkflow](https://docs.medusajs.com/references/medusa-workflows/createShippingProfilesWorkflow/index.html.md)
- [deleteServiceZonesWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteServiceZonesWorkflow/index.html.md)
- [deleteFulfillmentSetsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteFulfillmentSetsWorkflow/index.html.md)
- [deleteShippingOptionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteShippingOptionsWorkflow/index.html.md)
- [updateServiceZonesWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateServiceZonesWorkflow/index.html.md)
- [markFulfillmentAsDeliveredWorkflow](https://docs.medusajs.com/references/medusa-workflows/markFulfillmentAsDeliveredWorkflow/index.html.md)
- [updateFulfillmentWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateFulfillmentWorkflow/index.html.md)
- [updateShippingOptionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateShippingOptionsWorkflow/index.html.md)
- [updateShippingProfilesWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateShippingProfilesWorkflow/index.html.md)
- [validateFulfillmentDeliverabilityStep](https://docs.medusajs.com/references/medusa-workflows/validateFulfillmentDeliverabilityStep/index.html.md)
- [acceptInviteWorkflow](https://docs.medusajs.com/references/medusa-workflows/acceptInviteWorkflow/index.html.md)
- [deleteInvitesWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteInvitesWorkflow/index.html.md)
- [createInvitesWorkflow](https://docs.medusajs.com/references/medusa-workflows/createInvitesWorkflow/index.html.md)
- [refreshInviteTokensWorkflow](https://docs.medusajs.com/references/medusa-workflows/refreshInviteTokensWorkflow/index.html.md)
- [deleteLineItemsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteLineItemsWorkflow/index.html.md)
- [generateResetPasswordTokenWorkflow](https://docs.medusajs.com/references/medusa-workflows/generateResetPasswordTokenWorkflow/index.html.md)
- [processPaymentWorkflow](https://docs.medusajs.com/references/medusa-workflows/processPaymentWorkflow/index.html.md)
- [refundPaymentWorkflow](https://docs.medusajs.com/references/medusa-workflows/refundPaymentWorkflow/index.html.md)
- [capturePaymentWorkflow](https://docs.medusajs.com/references/medusa-workflows/capturePaymentWorkflow/index.html.md)
- [refundPaymentsWorkflow](https://docs.medusajs.com/references/medusa-workflows/refundPaymentsWorkflow/index.html.md)
- [validatePaymentsRefundStep](https://docs.medusajs.com/references/medusa-workflows/validatePaymentsRefundStep/index.html.md)
- [validateRefundStep](https://docs.medusajs.com/references/medusa-workflows/validateRefundStep/index.html.md)
- [createCustomerAccountWorkflow](https://docs.medusajs.com/references/medusa-workflows/createCustomerAccountWorkflow/index.html.md)
- [createCustomersWorkflow](https://docs.medusajs.com/references/medusa-workflows/createCustomersWorkflow/index.html.md)
- [createCustomerAddressesWorkflow](https://docs.medusajs.com/references/medusa-workflows/createCustomerAddressesWorkflow/index.html.md)
- [deleteCustomerAddressesWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteCustomerAddressesWorkflow/index.html.md)
- [deleteCustomersWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteCustomersWorkflow/index.html.md)
- [updateCustomerAddressesWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateCustomerAddressesWorkflow/index.html.md)
- [updateCustomersWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateCustomersWorkflow/index.html.md)
- [removeCustomerAccountWorkflow](https://docs.medusajs.com/references/medusa-workflows/removeCustomerAccountWorkflow/index.html.md)
- [batchProductVariantsWorkflow](https://docs.medusajs.com/references/medusa-workflows/batchProductVariantsWorkflow/index.html.md)
- [batchLinkProductsToCollectionWorkflow](https://docs.medusajs.com/references/medusa-workflows/batchLinkProductsToCollectionWorkflow/index.html.md)
- [batchLinkProductsToCategoryWorkflow](https://docs.medusajs.com/references/medusa-workflows/batchLinkProductsToCategoryWorkflow/index.html.md)
- [createCollectionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createCollectionsWorkflow/index.html.md)
- [createProductTagsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createProductTagsWorkflow/index.html.md)
- [batchProductsWorkflow](https://docs.medusajs.com/references/medusa-workflows/batchProductsWorkflow/index.html.md)
- [createProductOptionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createProductOptionsWorkflow/index.html.md)
- [createProductTypesWorkflow](https://docs.medusajs.com/references/medusa-workflows/createProductTypesWorkflow/index.html.md)
- [createProductVariantsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createProductVariantsWorkflow/index.html.md)
- [createProductsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createProductsWorkflow/index.html.md)
- [deleteProductTagsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteProductTagsWorkflow/index.html.md)
- [deleteProductOptionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteProductOptionsWorkflow/index.html.md)
- [deleteCollectionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteCollectionsWorkflow/index.html.md)
- [deleteProductTypesWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteProductTypesWorkflow/index.html.md)
- [deleteProductVariantsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteProductVariantsWorkflow/index.html.md)
- [exportProductsWorkflow](https://docs.medusajs.com/references/medusa-workflows/exportProductsWorkflow/index.html.md)
- [deleteProductsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteProductsWorkflow/index.html.md)
- [importProductsAsChunksWorkflow](https://docs.medusajs.com/references/medusa-workflows/importProductsAsChunksWorkflow/index.html.md)
- [importProductsWorkflow](https://docs.medusajs.com/references/medusa-workflows/importProductsWorkflow/index.html.md)
- [updateCollectionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateCollectionsWorkflow/index.html.md)
- [updateProductOptionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateProductOptionsWorkflow/index.html.md)
- [updateProductTagsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateProductTagsWorkflow/index.html.md)
- [updateProductsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateProductsWorkflow/index.html.md)
- [updateProductVariantsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateProductVariantsWorkflow/index.html.md)
- [updateProductTypesWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateProductTypesWorkflow/index.html.md)
- [upsertVariantPricesWorkflow](https://docs.medusajs.com/references/medusa-workflows/upsertVariantPricesWorkflow/index.html.md)
- [validateProductInputStep](https://docs.medusajs.com/references/medusa-workflows/validateProductInputStep/index.html.md)
- [createPaymentSessionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createPaymentSessionsWorkflow/index.html.md)
- [createRefundReasonsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createRefundReasonsWorkflow/index.html.md)
- [deletePaymentSessionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deletePaymentSessionsWorkflow/index.html.md)
- [updateRefundReasonsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateRefundReasonsWorkflow/index.html.md)
- [deleteRefundReasonsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteRefundReasonsWorkflow/index.html.md)
- [acceptOrderTransferValidationStep](https://docs.medusajs.com/references/medusa-workflows/acceptOrderTransferValidationStep/index.html.md)
- [acceptOrderTransferWorkflow](https://docs.medusajs.com/references/medusa-workflows/acceptOrderTransferWorkflow/index.html.md)
- [addOrderLineItemsWorkflow](https://docs.medusajs.com/references/medusa-workflows/addOrderLineItemsWorkflow/index.html.md)
- [archiveOrderWorkflow](https://docs.medusajs.com/references/medusa-workflows/archiveOrderWorkflow/index.html.md)
- [beginClaimOrderValidationStep](https://docs.medusajs.com/references/medusa-workflows/beginClaimOrderValidationStep/index.html.md)
- [beginClaimOrderWorkflow](https://docs.medusajs.com/references/medusa-workflows/beginClaimOrderWorkflow/index.html.md)
- [beginExchangeOrderWorkflow](https://docs.medusajs.com/references/medusa-workflows/beginExchangeOrderWorkflow/index.html.md)
- [beginOrderExchangeValidationStep](https://docs.medusajs.com/references/medusa-workflows/beginOrderExchangeValidationStep/index.html.md)
- [beginOrderEditOrderWorkflow](https://docs.medusajs.com/references/medusa-workflows/beginOrderEditOrderWorkflow/index.html.md)
- [beginOrderEditValidationStep](https://docs.medusajs.com/references/medusa-workflows/beginOrderEditValidationStep/index.html.md)
- [beginReceiveReturnValidationStep](https://docs.medusajs.com/references/medusa-workflows/beginReceiveReturnValidationStep/index.html.md)
- [beginReceiveReturnWorkflow](https://docs.medusajs.com/references/medusa-workflows/beginReceiveReturnWorkflow/index.html.md)
- [beginReturnOrderValidationStep](https://docs.medusajs.com/references/medusa-workflows/beginReturnOrderValidationStep/index.html.md)
- [cancelBeginOrderClaimWorkflow](https://docs.medusajs.com/references/medusa-workflows/cancelBeginOrderClaimWorkflow/index.html.md)
- [cancelBeginOrderEditValidationStep](https://docs.medusajs.com/references/medusa-workflows/cancelBeginOrderEditValidationStep/index.html.md)
- [beginReturnOrderWorkflow](https://docs.medusajs.com/references/medusa-workflows/beginReturnOrderWorkflow/index.html.md)
- [cancelBeginOrderClaimValidationStep](https://docs.medusajs.com/references/medusa-workflows/cancelBeginOrderClaimValidationStep/index.html.md)
- [cancelBeginOrderExchangeValidationStep](https://docs.medusajs.com/references/medusa-workflows/cancelBeginOrderExchangeValidationStep/index.html.md)
- [cancelBeginOrderEditWorkflow](https://docs.medusajs.com/references/medusa-workflows/cancelBeginOrderEditWorkflow/index.html.md)
- [cancelOrderChangeWorkflow](https://docs.medusajs.com/references/medusa-workflows/cancelOrderChangeWorkflow/index.html.md)
- [cancelClaimValidateOrderStep](https://docs.medusajs.com/references/medusa-workflows/cancelClaimValidateOrderStep/index.html.md)
- [cancelExchangeValidateOrder](https://docs.medusajs.com/references/medusa-workflows/cancelExchangeValidateOrder/index.html.md)
- [cancelBeginOrderExchangeWorkflow](https://docs.medusajs.com/references/medusa-workflows/cancelBeginOrderExchangeWorkflow/index.html.md)
- [cancelOrderClaimWorkflow](https://docs.medusajs.com/references/medusa-workflows/cancelOrderClaimWorkflow/index.html.md)
- [cancelOrderFulfillmentValidateOrder](https://docs.medusajs.com/references/medusa-workflows/cancelOrderFulfillmentValidateOrder/index.html.md)
- [cancelOrderExchangeWorkflow](https://docs.medusajs.com/references/medusa-workflows/cancelOrderExchangeWorkflow/index.html.md)
- [cancelOrderTransferRequestWorkflow](https://docs.medusajs.com/references/medusa-workflows/cancelOrderTransferRequestWorkflow/index.html.md)
- [cancelReceiveReturnValidationStep](https://docs.medusajs.com/references/medusa-workflows/cancelReceiveReturnValidationStep/index.html.md)
- [cancelOrderWorkflow](https://docs.medusajs.com/references/medusa-workflows/cancelOrderWorkflow/index.html.md)
- [cancelOrderFulfillmentWorkflow](https://docs.medusajs.com/references/medusa-workflows/cancelOrderFulfillmentWorkflow/index.html.md)
- [cancelRequestReturnValidationStep](https://docs.medusajs.com/references/medusa-workflows/cancelRequestReturnValidationStep/index.html.md)
- [cancelReturnReceiveWorkflow](https://docs.medusajs.com/references/medusa-workflows/cancelReturnReceiveWorkflow/index.html.md)
- [cancelReturnRequestWorkflow](https://docs.medusajs.com/references/medusa-workflows/cancelReturnRequestWorkflow/index.html.md)
- [cancelReturnValidateOrder](https://docs.medusajs.com/references/medusa-workflows/cancelReturnValidateOrder/index.html.md)
- [cancelValidateOrder](https://docs.medusajs.com/references/medusa-workflows/cancelValidateOrder/index.html.md)
- [cancelTransferOrderRequestValidationStep](https://docs.medusajs.com/references/medusa-workflows/cancelTransferOrderRequestValidationStep/index.html.md)
- [cancelReturnWorkflow](https://docs.medusajs.com/references/medusa-workflows/cancelReturnWorkflow/index.html.md)
- [completeOrderWorkflow](https://docs.medusajs.com/references/medusa-workflows/completeOrderWorkflow/index.html.md)
- [confirmClaimRequestWorkflow](https://docs.medusajs.com/references/medusa-workflows/confirmClaimRequestWorkflow/index.html.md)
- [confirmClaimRequestValidationStep](https://docs.medusajs.com/references/medusa-workflows/confirmClaimRequestValidationStep/index.html.md)
- [confirmExchangeRequestValidationStep](https://docs.medusajs.com/references/medusa-workflows/confirmExchangeRequestValidationStep/index.html.md)
- [confirmExchangeRequestWorkflow](https://docs.medusajs.com/references/medusa-workflows/confirmExchangeRequestWorkflow/index.html.md)
- [confirmOrderEditRequestWorkflow](https://docs.medusajs.com/references/medusa-workflows/confirmOrderEditRequestWorkflow/index.html.md)
- [confirmReceiveReturnValidationStep](https://docs.medusajs.com/references/medusa-workflows/confirmReceiveReturnValidationStep/index.html.md)
- [confirmOrderEditRequestValidationStep](https://docs.medusajs.com/references/medusa-workflows/confirmOrderEditRequestValidationStep/index.html.md)
- [confirmReturnReceiveWorkflow](https://docs.medusajs.com/references/medusa-workflows/confirmReturnReceiveWorkflow/index.html.md)
- [createAndCompleteReturnOrderWorkflow](https://docs.medusajs.com/references/medusa-workflows/createAndCompleteReturnOrderWorkflow/index.html.md)
- [confirmReturnRequestValidationStep](https://docs.medusajs.com/references/medusa-workflows/confirmReturnRequestValidationStep/index.html.md)
- [confirmReturnRequestWorkflow](https://docs.medusajs.com/references/medusa-workflows/confirmReturnRequestWorkflow/index.html.md)
- [createCompleteReturnValidationStep](https://docs.medusajs.com/references/medusa-workflows/createCompleteReturnValidationStep/index.html.md)
- [createClaimShippingMethodWorkflow](https://docs.medusajs.com/references/medusa-workflows/createClaimShippingMethodWorkflow/index.html.md)
- [createExchangeShippingMethodValidationStep](https://docs.medusajs.com/references/medusa-workflows/createExchangeShippingMethodValidationStep/index.html.md)
- [createClaimShippingMethodValidationStep](https://docs.medusajs.com/references/medusa-workflows/createClaimShippingMethodValidationStep/index.html.md)
- [createExchangeShippingMethodWorkflow](https://docs.medusajs.com/references/medusa-workflows/createExchangeShippingMethodWorkflow/index.html.md)
- [createOrUpdateOrderPaymentCollectionWorkflow](https://docs.medusajs.com/references/medusa-workflows/createOrUpdateOrderPaymentCollectionWorkflow/index.html.md)
- [createFulfillmentValidateOrder](https://docs.medusajs.com/references/medusa-workflows/createFulfillmentValidateOrder/index.html.md)
- [createOrderChangeActionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createOrderChangeActionsWorkflow/index.html.md)
- [createOrderEditShippingMethodValidationStep](https://docs.medusajs.com/references/medusa-workflows/createOrderEditShippingMethodValidationStep/index.html.md)
- [createOrderChangeWorkflow](https://docs.medusajs.com/references/medusa-workflows/createOrderChangeWorkflow/index.html.md)
- [createOrderCreditLinesWorkflow](https://docs.medusajs.com/references/medusa-workflows/createOrderCreditLinesWorkflow/index.html.md)
- [createOrderEditShippingMethodWorkflow](https://docs.medusajs.com/references/medusa-workflows/createOrderEditShippingMethodWorkflow/index.html.md)
- [createOrderFulfillmentWorkflow](https://docs.medusajs.com/references/medusa-workflows/createOrderFulfillmentWorkflow/index.html.md)
- [createOrderPaymentCollectionWorkflow](https://docs.medusajs.com/references/medusa-workflows/createOrderPaymentCollectionWorkflow/index.html.md)
- [createOrderShipmentWorkflow](https://docs.medusajs.com/references/medusa-workflows/createOrderShipmentWorkflow/index.html.md)
- [createOrderWorkflow](https://docs.medusajs.com/references/medusa-workflows/createOrderWorkflow/index.html.md)
- [createReturnShippingMethodWorkflow](https://docs.medusajs.com/references/medusa-workflows/createReturnShippingMethodWorkflow/index.html.md)
- [createReturnShippingMethodValidationStep](https://docs.medusajs.com/references/medusa-workflows/createReturnShippingMethodValidationStep/index.html.md)
- [createOrdersWorkflow](https://docs.medusajs.com/references/medusa-workflows/createOrdersWorkflow/index.html.md)
- [createShipmentValidateOrder](https://docs.medusajs.com/references/medusa-workflows/createShipmentValidateOrder/index.html.md)
- [declineOrderTransferRequestWorkflow](https://docs.medusajs.com/references/medusa-workflows/declineOrderTransferRequestWorkflow/index.html.md)
- [declineOrderChangeWorkflow](https://docs.medusajs.com/references/medusa-workflows/declineOrderChangeWorkflow/index.html.md)
- [declineTransferOrderRequestValidationStep](https://docs.medusajs.com/references/medusa-workflows/declineTransferOrderRequestValidationStep/index.html.md)
- [deleteOrderChangeActionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteOrderChangeActionsWorkflow/index.html.md)
- [dismissItemReturnRequestValidationStep](https://docs.medusajs.com/references/medusa-workflows/dismissItemReturnRequestValidationStep/index.html.md)
- [deleteOrderPaymentCollections](https://docs.medusajs.com/references/medusa-workflows/deleteOrderPaymentCollections/index.html.md)
- [deleteOrderChangeWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteOrderChangeWorkflow/index.html.md)
- [dismissItemReturnRequestWorkflow](https://docs.medusajs.com/references/medusa-workflows/dismissItemReturnRequestWorkflow/index.html.md)
- [exchangeAddNewItemValidationStep](https://docs.medusajs.com/references/medusa-workflows/exchangeAddNewItemValidationStep/index.html.md)
- [exchangeRequestItemReturnValidationStep](https://docs.medusajs.com/references/medusa-workflows/exchangeRequestItemReturnValidationStep/index.html.md)
- [fetchShippingOptionForOrderWorkflow](https://docs.medusajs.com/references/medusa-workflows/fetchShippingOptionForOrderWorkflow/index.html.md)
- [getOrderDetailWorkflow](https://docs.medusajs.com/references/medusa-workflows/getOrderDetailWorkflow/index.html.md)
- [markPaymentCollectionAsPaid](https://docs.medusajs.com/references/medusa-workflows/markPaymentCollectionAsPaid/index.html.md)
- [getOrdersListWorkflow](https://docs.medusajs.com/references/medusa-workflows/getOrdersListWorkflow/index.html.md)
- [markOrderFulfillmentAsDeliveredWorkflow](https://docs.medusajs.com/references/medusa-workflows/markOrderFulfillmentAsDeliveredWorkflow/index.html.md)
- [maybeRefreshShippingMethodsWorkflow](https://docs.medusajs.com/references/medusa-workflows/maybeRefreshShippingMethodsWorkflow/index.html.md)
- [orderClaimItemValidationStep](https://docs.medusajs.com/references/medusa-workflows/orderClaimItemValidationStep/index.html.md)
- [orderClaimAddNewItemValidationStep](https://docs.medusajs.com/references/medusa-workflows/orderClaimAddNewItemValidationStep/index.html.md)
- [orderClaimAddNewItemWorkflow](https://docs.medusajs.com/references/medusa-workflows/orderClaimAddNewItemWorkflow/index.html.md)
- [orderClaimItemWorkflow](https://docs.medusajs.com/references/medusa-workflows/orderClaimItemWorkflow/index.html.md)
- [orderClaimRequestItemReturnWorkflow](https://docs.medusajs.com/references/medusa-workflows/orderClaimRequestItemReturnWorkflow/index.html.md)
- [orderClaimRequestItemReturnValidationStep](https://docs.medusajs.com/references/medusa-workflows/orderClaimRequestItemReturnValidationStep/index.html.md)
- [orderEditAddNewItemValidationStep](https://docs.medusajs.com/references/medusa-workflows/orderEditAddNewItemValidationStep/index.html.md)
- [orderEditAddNewItemWorkflow](https://docs.medusajs.com/references/medusa-workflows/orderEditAddNewItemWorkflow/index.html.md)
- [orderEditUpdateItemQuantityValidationStep](https://docs.medusajs.com/references/medusa-workflows/orderEditUpdateItemQuantityValidationStep/index.html.md)
- [orderEditUpdateItemQuantityWorkflow](https://docs.medusajs.com/references/medusa-workflows/orderEditUpdateItemQuantityWorkflow/index.html.md)
- [orderExchangeAddNewItemWorkflow](https://docs.medusajs.com/references/medusa-workflows/orderExchangeAddNewItemWorkflow/index.html.md)
- [orderExchangeRequestItemReturnWorkflow](https://docs.medusajs.com/references/medusa-workflows/orderExchangeRequestItemReturnWorkflow/index.html.md)
- [orderFulfillmentDeliverablilityValidationStep](https://docs.medusajs.com/references/medusa-workflows/orderFulfillmentDeliverablilityValidationStep/index.html.md)
- [receiveCompleteReturnValidationStep](https://docs.medusajs.com/references/medusa-workflows/receiveCompleteReturnValidationStep/index.html.md)
- [receiveAndCompleteReturnOrderWorkflow](https://docs.medusajs.com/references/medusa-workflows/receiveAndCompleteReturnOrderWorkflow/index.html.md)
- [receiveItemReturnRequestValidationStep](https://docs.medusajs.com/references/medusa-workflows/receiveItemReturnRequestValidationStep/index.html.md)
- [removeClaimAddItemActionValidationStep](https://docs.medusajs.com/references/medusa-workflows/removeClaimAddItemActionValidationStep/index.html.md)
- [receiveItemReturnRequestWorkflow](https://docs.medusajs.com/references/medusa-workflows/receiveItemReturnRequestWorkflow/index.html.md)
- [removeAddItemClaimActionWorkflow](https://docs.medusajs.com/references/medusa-workflows/removeAddItemClaimActionWorkflow/index.html.md)
- [removeClaimItemActionValidationStep](https://docs.medusajs.com/references/medusa-workflows/removeClaimItemActionValidationStep/index.html.md)
- [removeClaimShippingMethodValidationStep](https://docs.medusajs.com/references/medusa-workflows/removeClaimShippingMethodValidationStep/index.html.md)
- [removeExchangeItemActionValidationStep](https://docs.medusajs.com/references/medusa-workflows/removeExchangeItemActionValidationStep/index.html.md)
- [removeClaimShippingMethodWorkflow](https://docs.medusajs.com/references/medusa-workflows/removeClaimShippingMethodWorkflow/index.html.md)
- [removeItemExchangeActionWorkflow](https://docs.medusajs.com/references/medusa-workflows/removeItemExchangeActionWorkflow/index.html.md)
- [removeItemClaimActionWorkflow](https://docs.medusajs.com/references/medusa-workflows/removeItemClaimActionWorkflow/index.html.md)
- [removeExchangeShippingMethodWorkflow](https://docs.medusajs.com/references/medusa-workflows/removeExchangeShippingMethodWorkflow/index.html.md)
- [removeExchangeShippingMethodValidationStep](https://docs.medusajs.com/references/medusa-workflows/removeExchangeShippingMethodValidationStep/index.html.md)
- [removeItemReceiveReturnActionWorkflow](https://docs.medusajs.com/references/medusa-workflows/removeItemReceiveReturnActionWorkflow/index.html.md)
- [removeItemOrderEditActionWorkflow](https://docs.medusajs.com/references/medusa-workflows/removeItemOrderEditActionWorkflow/index.html.md)
- [removeItemReceiveReturnActionValidationStep](https://docs.medusajs.com/references/medusa-workflows/removeItemReceiveReturnActionValidationStep/index.html.md)
- [removeItemReturnActionWorkflow](https://docs.medusajs.com/references/medusa-workflows/removeItemReturnActionWorkflow/index.html.md)
- [removeOrderEditItemActionValidationStep](https://docs.medusajs.com/references/medusa-workflows/removeOrderEditItemActionValidationStep/index.html.md)
- [removeOrderEditShippingMethodValidationStep](https://docs.medusajs.com/references/medusa-workflows/removeOrderEditShippingMethodValidationStep/index.html.md)
- [removeOrderEditShippingMethodWorkflow](https://docs.medusajs.com/references/medusa-workflows/removeOrderEditShippingMethodWorkflow/index.html.md)
- [removeReturnItemActionValidationStep](https://docs.medusajs.com/references/medusa-workflows/removeReturnItemActionValidationStep/index.html.md)
- [requestItemReturnValidationStep](https://docs.medusajs.com/references/medusa-workflows/requestItemReturnValidationStep/index.html.md)
- [removeReturnShippingMethodWorkflow](https://docs.medusajs.com/references/medusa-workflows/removeReturnShippingMethodWorkflow/index.html.md)
- [removeReturnShippingMethodValidationStep](https://docs.medusajs.com/references/medusa-workflows/removeReturnShippingMethodValidationStep/index.html.md)
- [requestOrderEditRequestValidationStep](https://docs.medusajs.com/references/medusa-workflows/requestOrderEditRequestValidationStep/index.html.md)
- [requestItemReturnWorkflow](https://docs.medusajs.com/references/medusa-workflows/requestItemReturnWorkflow/index.html.md)
- [requestOrderEditRequestWorkflow](https://docs.medusajs.com/references/medusa-workflows/requestOrderEditRequestWorkflow/index.html.md)
- [requestOrderTransferValidationStep](https://docs.medusajs.com/references/medusa-workflows/requestOrderTransferValidationStep/index.html.md)
- [requestOrderTransferWorkflow](https://docs.medusajs.com/references/medusa-workflows/requestOrderTransferWorkflow/index.html.md)
- [throwUnlessPaymentCollectionNotPaid](https://docs.medusajs.com/references/medusa-workflows/throwUnlessPaymentCollectionNotPaid/index.html.md)
- [updateClaimAddItemValidationStep](https://docs.medusajs.com/references/medusa-workflows/updateClaimAddItemValidationStep/index.html.md)
- [updateClaimAddItemWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateClaimAddItemWorkflow/index.html.md)
- [throwUnlessStatusIsNotPaid](https://docs.medusajs.com/references/medusa-workflows/throwUnlessStatusIsNotPaid/index.html.md)
- [updateClaimItemValidationStep](https://docs.medusajs.com/references/medusa-workflows/updateClaimItemValidationStep/index.html.md)
- [updateClaimItemWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateClaimItemWorkflow/index.html.md)
- [updateClaimShippingMethodValidationStep](https://docs.medusajs.com/references/medusa-workflows/updateClaimShippingMethodValidationStep/index.html.md)
- [updateClaimShippingMethodWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateClaimShippingMethodWorkflow/index.html.md)
- [updateExchangeAddItemValidationStep](https://docs.medusajs.com/references/medusa-workflows/updateExchangeAddItemValidationStep/index.html.md)
- [updateExchangeShippingMethodWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateExchangeShippingMethodWorkflow/index.html.md)
- [updateExchangeShippingMethodValidationStep](https://docs.medusajs.com/references/medusa-workflows/updateExchangeShippingMethodValidationStep/index.html.md)
- [updateExchangeAddItemWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateExchangeAddItemWorkflow/index.html.md)
- [updateOrderChangeActionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateOrderChangeActionsWorkflow/index.html.md)
- [updateOrderEditAddItemValidationStep](https://docs.medusajs.com/references/medusa-workflows/updateOrderEditAddItemValidationStep/index.html.md)
- [updateOrderChangesWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateOrderChangesWorkflow/index.html.md)
- [updateOrderEditAddItemWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateOrderEditAddItemWorkflow/index.html.md)
- [updateOrderEditItemQuantityValidationStep](https://docs.medusajs.com/references/medusa-workflows/updateOrderEditItemQuantityValidationStep/index.html.md)
- [updateOrderEditShippingMethodValidationStep](https://docs.medusajs.com/references/medusa-workflows/updateOrderEditShippingMethodValidationStep/index.html.md)
- [updateOrderEditItemQuantityWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateOrderEditItemQuantityWorkflow/index.html.md)
- [updateOrderEditShippingMethodWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateOrderEditShippingMethodWorkflow/index.html.md)
- [updateOrderTaxLinesWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateOrderTaxLinesWorkflow/index.html.md)
- [updateOrderValidationStep](https://docs.medusajs.com/references/medusa-workflows/updateOrderValidationStep/index.html.md)
- [updateOrderWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateOrderWorkflow/index.html.md)
- [updateReceiveItemReturnRequestValidationStep](https://docs.medusajs.com/references/medusa-workflows/updateReceiveItemReturnRequestValidationStep/index.html.md)
- [updateReceiveItemReturnRequestWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateReceiveItemReturnRequestWorkflow/index.html.md)
- [updateRequestItemReturnValidationStep](https://docs.medusajs.com/references/medusa-workflows/updateRequestItemReturnValidationStep/index.html.md)
- [updateRequestItemReturnWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateRequestItemReturnWorkflow/index.html.md)
- [updateReturnShippingMethodValidationStep](https://docs.medusajs.com/references/medusa-workflows/updateReturnShippingMethodValidationStep/index.html.md)
- [updateReturnShippingMethodWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateReturnShippingMethodWorkflow/index.html.md)
- [updateReturnValidationStep](https://docs.medusajs.com/references/medusa-workflows/updateReturnValidationStep/index.html.md)
- [updateReturnWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateReturnWorkflow/index.html.md)
- [validateOrderCreditLinesStep](https://docs.medusajs.com/references/medusa-workflows/validateOrderCreditLinesStep/index.html.md)
- [createPriceListPricesWorkflow](https://docs.medusajs.com/references/medusa-workflows/createPriceListPricesWorkflow/index.html.md)
- [batchPriceListPricesWorkflow](https://docs.medusajs.com/references/medusa-workflows/batchPriceListPricesWorkflow/index.html.md)
- [createPriceListsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createPriceListsWorkflow/index.html.md)
- [deletePriceListsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deletePriceListsWorkflow/index.html.md)
- [removePriceListPricesWorkflow](https://docs.medusajs.com/references/medusa-workflows/removePriceListPricesWorkflow/index.html.md)
- [updatePriceListPricesWorkflow](https://docs.medusajs.com/references/medusa-workflows/updatePriceListPricesWorkflow/index.html.md)
- [updatePriceListsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updatePriceListsWorkflow/index.html.md)
- [addOrRemoveCampaignPromotionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/addOrRemoveCampaignPromotionsWorkflow/index.html.md)
- [batchPromotionRulesWorkflow](https://docs.medusajs.com/references/medusa-workflows/batchPromotionRulesWorkflow/index.html.md)
- [createCampaignsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createCampaignsWorkflow/index.html.md)
- [createPromotionRulesWorkflow](https://docs.medusajs.com/references/medusa-workflows/createPromotionRulesWorkflow/index.html.md)
- [createPromotionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createPromotionsWorkflow/index.html.md)
- [deletePromotionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deletePromotionsWorkflow/index.html.md)
- [deletePromotionRulesWorkflow](https://docs.medusajs.com/references/medusa-workflows/deletePromotionRulesWorkflow/index.html.md)
- [deleteCampaignsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteCampaignsWorkflow/index.html.md)
- [updateCampaignsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateCampaignsWorkflow/index.html.md)
- [updatePromotionRulesWorkflow](https://docs.medusajs.com/references/medusa-workflows/updatePromotionRulesWorkflow/index.html.md)
- [updatePromotionsStatusWorkflow](https://docs.medusajs.com/references/medusa-workflows/updatePromotionsStatusWorkflow/index.html.md)
- [updatePromotionsValidationStep](https://docs.medusajs.com/references/medusa-workflows/updatePromotionsValidationStep/index.html.md)
- [updatePromotionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updatePromotionsWorkflow/index.html.md)
- [batchInventoryItemLevelsWorkflow](https://docs.medusajs.com/references/medusa-workflows/batchInventoryItemLevelsWorkflow/index.html.md)
- [bulkCreateDeleteLevelsWorkflow](https://docs.medusajs.com/references/medusa-workflows/bulkCreateDeleteLevelsWorkflow/index.html.md)
- [createInventoryLevelsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createInventoryLevelsWorkflow/index.html.md)
- [deleteInventoryItemWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteInventoryItemWorkflow/index.html.md)
- [createInventoryItemsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createInventoryItemsWorkflow/index.html.md)
- [deleteInventoryLevelsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteInventoryLevelsWorkflow/index.html.md)
- [validateInventoryLevelsDelete](https://docs.medusajs.com/references/medusa-workflows/validateInventoryLevelsDelete/index.html.md)
- [updateInventoryItemsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateInventoryItemsWorkflow/index.html.md)
- [updateInventoryLevelsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateInventoryLevelsWorkflow/index.html.md)
- [createReservationsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createReservationsWorkflow/index.html.md)
- [deleteReservationsByLineItemsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteReservationsByLineItemsWorkflow/index.html.md)
- [updateReservationsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateReservationsWorkflow/index.html.md)
- [deleteReservationsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteReservationsWorkflow/index.html.md)
- [deleteRegionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteRegionsWorkflow/index.html.md)
- [updateRegionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateRegionsWorkflow/index.html.md)
- [createRegionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createRegionsWorkflow/index.html.md)
- [deleteReturnReasonsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteReturnReasonsWorkflow/index.html.md)
- [createReturnReasonsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createReturnReasonsWorkflow/index.html.md)
- [updateReturnReasonsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateReturnReasonsWorkflow/index.html.md)
- [createSalesChannelsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createSalesChannelsWorkflow/index.html.md)
- [deleteSalesChannelsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteSalesChannelsWorkflow/index.html.md)
- [linkProductsToSalesChannelWorkflow](https://docs.medusajs.com/references/medusa-workflows/linkProductsToSalesChannelWorkflow/index.html.md)
- [updateSalesChannelsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateSalesChannelsWorkflow/index.html.md)
- [validateStepShippingProfileDelete](https://docs.medusajs.com/references/medusa-workflows/validateStepShippingProfileDelete/index.html.md)
- [deleteShippingProfileWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteShippingProfileWorkflow/index.html.md)
- [updatePricePreferencesWorkflow](https://docs.medusajs.com/references/medusa-workflows/updatePricePreferencesWorkflow/index.html.md)
- [createPricePreferencesWorkflow](https://docs.medusajs.com/references/medusa-workflows/createPricePreferencesWorkflow/index.html.md)
- [deletePricePreferencesWorkflow](https://docs.medusajs.com/references/medusa-workflows/deletePricePreferencesWorkflow/index.html.md)
- [updateProductCategoriesWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateProductCategoriesWorkflow/index.html.md)
- [deleteProductCategoriesWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteProductCategoriesWorkflow/index.html.md)
- [createProductCategoriesWorkflow](https://docs.medusajs.com/references/medusa-workflows/createProductCategoriesWorkflow/index.html.md)
- [createStockLocationsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createStockLocationsWorkflow/index.html.md)
- [createLocationFulfillmentSetWorkflow](https://docs.medusajs.com/references/medusa-workflows/createLocationFulfillmentSetWorkflow/index.html.md)
- [deleteStockLocationsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteStockLocationsWorkflow/index.html.md)
- [updateStockLocationsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateStockLocationsWorkflow/index.html.md)
- [linkSalesChannelsToStockLocationWorkflow](https://docs.medusajs.com/references/medusa-workflows/linkSalesChannelsToStockLocationWorkflow/index.html.md)
- [createTaxRatesWorkflow](https://docs.medusajs.com/references/medusa-workflows/createTaxRatesWorkflow/index.html.md)
- [createTaxRegionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createTaxRegionsWorkflow/index.html.md)
- [createTaxRateRulesWorkflow](https://docs.medusajs.com/references/medusa-workflows/createTaxRateRulesWorkflow/index.html.md)
- [deleteTaxRateRulesWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteTaxRateRulesWorkflow/index.html.md)
- [deleteTaxRegionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteTaxRegionsWorkflow/index.html.md)
- [setTaxRateRulesWorkflow](https://docs.medusajs.com/references/medusa-workflows/setTaxRateRulesWorkflow/index.html.md)
- [maybeListTaxRateRuleIdsStep](https://docs.medusajs.com/references/medusa-workflows/maybeListTaxRateRuleIdsStep/index.html.md)
- [deleteTaxRatesWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteTaxRatesWorkflow/index.html.md)
- [updateTaxRatesWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateTaxRatesWorkflow/index.html.md)
- [updateTaxRegionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateTaxRegionsWorkflow/index.html.md)
- [createUsersWorkflow](https://docs.medusajs.com/references/medusa-workflows/createUsersWorkflow/index.html.md)
- [createUserAccountWorkflow](https://docs.medusajs.com/references/medusa-workflows/createUserAccountWorkflow/index.html.md)
- [deleteUsersWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteUsersWorkflow/index.html.md)
- [updateUsersWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateUsersWorkflow/index.html.md)
- [removeUserAccountWorkflow](https://docs.medusajs.com/references/medusa-workflows/removeUserAccountWorkflow/index.html.md)
- [createStoresWorkflow](https://docs.medusajs.com/references/medusa-workflows/createStoresWorkflow/index.html.md)
- [deleteStoresWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteStoresWorkflow/index.html.md)
- [updateStoresWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateStoresWorkflow/index.html.md)


## Steps

- [createApiKeysStep](https://docs.medusajs.com/references/medusa-workflows/steps/createApiKeysStep/index.html.md)
- [deleteApiKeysStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteApiKeysStep/index.html.md)
- [linkSalesChannelsToApiKeyStep](https://docs.medusajs.com/references/medusa-workflows/steps/linkSalesChannelsToApiKeyStep/index.html.md)
- [revokeApiKeysStep](https://docs.medusajs.com/references/medusa-workflows/steps/revokeApiKeysStep/index.html.md)
- [updateApiKeysStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateApiKeysStep/index.html.md)
- [validateSalesChannelsExistStep](https://docs.medusajs.com/references/medusa-workflows/steps/validateSalesChannelsExistStep/index.html.md)
- [setAuthAppMetadataStep](https://docs.medusajs.com/references/medusa-workflows/steps/setAuthAppMetadataStep/index.html.md)
- [createCustomerGroupsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createCustomerGroupsStep/index.html.md)
- [deleteCustomerGroupStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteCustomerGroupStep/index.html.md)
- [linkCustomerGroupsToCustomerStep](https://docs.medusajs.com/references/medusa-workflows/steps/linkCustomerGroupsToCustomerStep/index.html.md)
- [linkCustomersToCustomerGroupStep](https://docs.medusajs.com/references/medusa-workflows/steps/linkCustomersToCustomerGroupStep/index.html.md)
- [updateCustomerGroupsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateCustomerGroupsStep/index.html.md)
- [createCustomerAddressesStep](https://docs.medusajs.com/references/medusa-workflows/steps/createCustomerAddressesStep/index.html.md)
- [createCustomersStep](https://docs.medusajs.com/references/medusa-workflows/steps/createCustomersStep/index.html.md)
- [deleteCustomerAddressesStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteCustomerAddressesStep/index.html.md)
- [deleteCustomersStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteCustomersStep/index.html.md)
- [maybeUnsetDefaultBillingAddressesStep](https://docs.medusajs.com/references/medusa-workflows/steps/maybeUnsetDefaultBillingAddressesStep/index.html.md)
- [maybeUnsetDefaultShippingAddressesStep](https://docs.medusajs.com/references/medusa-workflows/steps/maybeUnsetDefaultShippingAddressesStep/index.html.md)
- [updateCustomerAddressesStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateCustomerAddressesStep/index.html.md)
- [updateCustomersStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateCustomersStep/index.html.md)
- [validateCustomerAccountCreation](https://docs.medusajs.com/references/medusa-workflows/steps/validateCustomerAccountCreation/index.html.md)
- [validateDraftOrderStep](https://docs.medusajs.com/references/medusa-workflows/steps/validateDraftOrderStep/index.html.md)
- [createDefaultStoreStep](https://docs.medusajs.com/references/medusa-workflows/steps/createDefaultStoreStep/index.html.md)
- [createEntitiesStep](https://docs.medusajs.com/references/medusa-workflows/steps/createEntitiesStep/index.html.md)
- [createRemoteLinkStep](https://docs.medusajs.com/references/medusa-workflows/steps/createRemoteLinkStep/index.html.md)
- [dismissRemoteLinkStep](https://docs.medusajs.com/references/medusa-workflows/steps/dismissRemoteLinkStep/index.html.md)
- [emitEventStep](https://docs.medusajs.com/references/medusa-workflows/steps/emitEventStep/index.html.md)
- [deleteEntitiesStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteEntitiesStep/index.html.md)
- [updateRemoteLinksStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateRemoteLinksStep/index.html.md)
- [useQueryGraphStep](https://docs.medusajs.com/references/medusa-workflows/steps/useQueryGraphStep/index.html.md)
- [removeRemoteLinkStep](https://docs.medusajs.com/references/medusa-workflows/steps/removeRemoteLinkStep/index.html.md)
- [useRemoteQueryStep](https://docs.medusajs.com/references/medusa-workflows/steps/useRemoteQueryStep/index.html.md)
- [validatePresenceOfStep](https://docs.medusajs.com/references/medusa-workflows/steps/validatePresenceOfStep/index.html.md)
- [deleteFilesStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteFilesStep/index.html.md)
- [uploadFilesStep](https://docs.medusajs.com/references/medusa-workflows/steps/uploadFilesStep/index.html.md)
- [confirmInventoryStep](https://docs.medusajs.com/references/medusa-workflows/steps/confirmInventoryStep/index.html.md)
- [addShippingMethodToCartStep](https://docs.medusajs.com/references/medusa-workflows/steps/addShippingMethodToCartStep/index.html.md)
- [createCartsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createCartsStep/index.html.md)
- [createLineItemAdjustmentsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createLineItemAdjustmentsStep/index.html.md)
- [createLineItemsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createLineItemsStep/index.html.md)
- [createPaymentCollectionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createPaymentCollectionsStep/index.html.md)
- [createShippingMethodAdjustmentsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createShippingMethodAdjustmentsStep/index.html.md)
- [findOneOrAnyRegionStep](https://docs.medusajs.com/references/medusa-workflows/steps/findOneOrAnyRegionStep/index.html.md)
- [findOrCreateCustomerStep](https://docs.medusajs.com/references/medusa-workflows/steps/findOrCreateCustomerStep/index.html.md)
- [findSalesChannelStep](https://docs.medusajs.com/references/medusa-workflows/steps/findSalesChannelStep/index.html.md)
- [getActionsToComputeFromPromotionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/getActionsToComputeFromPromotionsStep/index.html.md)
- [getVariantPriceSetsStep](https://docs.medusajs.com/references/medusa-workflows/steps/getVariantPriceSetsStep/index.html.md)
- [getVariantsStep](https://docs.medusajs.com/references/medusa-workflows/steps/getVariantsStep/index.html.md)
- [getPromotionCodesToApply](https://docs.medusajs.com/references/medusa-workflows/steps/getPromotionCodesToApply/index.html.md)
- [prepareAdjustmentsFromPromotionActionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/prepareAdjustmentsFromPromotionActionsStep/index.html.md)
- [removeLineItemAdjustmentsStep](https://docs.medusajs.com/references/medusa-workflows/steps/removeLineItemAdjustmentsStep/index.html.md)
- [removeShippingMethodAdjustmentsStep](https://docs.medusajs.com/references/medusa-workflows/steps/removeShippingMethodAdjustmentsStep/index.html.md)
- [getLineItemActionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/getLineItemActionsStep/index.html.md)
- [removeShippingMethodFromCartStep](https://docs.medusajs.com/references/medusa-workflows/steps/removeShippingMethodFromCartStep/index.html.md)
- [reserveInventoryStep](https://docs.medusajs.com/references/medusa-workflows/steps/reserveInventoryStep/index.html.md)
- [retrieveCartStep](https://docs.medusajs.com/references/medusa-workflows/steps/retrieveCartStep/index.html.md)
- [setTaxLinesForItemsStep](https://docs.medusajs.com/references/medusa-workflows/steps/setTaxLinesForItemsStep/index.html.md)
- [updateCartPromotionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateCartPromotionsStep/index.html.md)
- [updateCartsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateCartsStep/index.html.md)
- [updateLineItemsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateLineItemsStep/index.html.md)
- [updateShippingMethodsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateShippingMethodsStep/index.html.md)
- [validateAndReturnShippingMethodsDataStep](https://docs.medusajs.com/references/medusa-workflows/steps/validateAndReturnShippingMethodsDataStep/index.html.md)
- [validateCartPaymentsStep](https://docs.medusajs.com/references/medusa-workflows/steps/validateCartPaymentsStep/index.html.md)
- [validateCartShippingOptionsPriceStep](https://docs.medusajs.com/references/medusa-workflows/steps/validateCartShippingOptionsPriceStep/index.html.md)
- [validateCartShippingOptionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/validateCartShippingOptionsStep/index.html.md)
- [validateCartStep](https://docs.medusajs.com/references/medusa-workflows/steps/validateCartStep/index.html.md)
- [validateLineItemPricesStep](https://docs.medusajs.com/references/medusa-workflows/steps/validateLineItemPricesStep/index.html.md)
- [validateShippingStep](https://docs.medusajs.com/references/medusa-workflows/steps/validateShippingStep/index.html.md)
- [validateVariantPricesStep](https://docs.medusajs.com/references/medusa-workflows/steps/validateVariantPricesStep/index.html.md)
- [buildPriceSet](https://docs.medusajs.com/references/medusa-workflows/steps/buildPriceSet/index.html.md)
- [calculateShippingOptionsPricesStep](https://docs.medusajs.com/references/medusa-workflows/steps/calculateShippingOptionsPricesStep/index.html.md)
- [createFulfillmentSets](https://docs.medusajs.com/references/medusa-workflows/steps/createFulfillmentSets/index.html.md)
- [cancelFulfillmentStep](https://docs.medusajs.com/references/medusa-workflows/steps/cancelFulfillmentStep/index.html.md)
- [createFulfillmentStep](https://docs.medusajs.com/references/medusa-workflows/steps/createFulfillmentStep/index.html.md)
- [createReturnFulfillmentStep](https://docs.medusajs.com/references/medusa-workflows/steps/createReturnFulfillmentStep/index.html.md)
- [createShippingOptionRulesStep](https://docs.medusajs.com/references/medusa-workflows/steps/createShippingOptionRulesStep/index.html.md)
- [createServiceZonesStep](https://docs.medusajs.com/references/medusa-workflows/steps/createServiceZonesStep/index.html.md)
- [createShippingOptionsPriceSetsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createShippingOptionsPriceSetsStep/index.html.md)
- [deleteFulfillmentSetsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteFulfillmentSetsStep/index.html.md)
- [createShippingProfilesStep](https://docs.medusajs.com/references/medusa-workflows/steps/createShippingProfilesStep/index.html.md)
- [deleteServiceZonesStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteServiceZonesStep/index.html.md)
- [deleteShippingOptionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteShippingOptionsStep/index.html.md)
- [deleteShippingOptionRulesStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteShippingOptionRulesStep/index.html.md)
- [setShippingOptionsPricesStep](https://docs.medusajs.com/references/medusa-workflows/steps/setShippingOptionsPricesStep/index.html.md)
- [updateFulfillmentStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateFulfillmentStep/index.html.md)
- [updateShippingProfilesStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateShippingProfilesStep/index.html.md)
- [updateServiceZonesStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateServiceZonesStep/index.html.md)
- [updateShippingOptionRulesStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateShippingOptionRulesStep/index.html.md)
- [upsertShippingOptionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/upsertShippingOptionsStep/index.html.md)
- [validateShipmentStep](https://docs.medusajs.com/references/medusa-workflows/steps/validateShipmentStep/index.html.md)
- [validateShippingOptionPricesStep](https://docs.medusajs.com/references/medusa-workflows/steps/validateShippingOptionPricesStep/index.html.md)
- [adjustInventoryLevelsStep](https://docs.medusajs.com/references/medusa-workflows/steps/adjustInventoryLevelsStep/index.html.md)
- [createInventoryItemsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createInventoryItemsStep/index.html.md)
- [attachInventoryItemToVariants](https://docs.medusajs.com/references/medusa-workflows/steps/attachInventoryItemToVariants/index.html.md)
- [createInventoryLevelsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createInventoryLevelsStep/index.html.md)
- [deleteInventoryItemStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteInventoryItemStep/index.html.md)
- [deleteInventoryLevelsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteInventoryLevelsStep/index.html.md)
- [updateInventoryItemsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateInventoryItemsStep/index.html.md)
- [validateInventoryDeleteStep](https://docs.medusajs.com/references/medusa-workflows/steps/validateInventoryDeleteStep/index.html.md)
- [updateInventoryLevelsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateInventoryLevelsStep/index.html.md)
- [validateInventoryItemsForCreate](https://docs.medusajs.com/references/medusa-workflows/steps/validateInventoryItemsForCreate/index.html.md)
- [validateInventoryLocationsStep](https://docs.medusajs.com/references/medusa-workflows/steps/validateInventoryLocationsStep/index.html.md)
- [createInviteStep](https://docs.medusajs.com/references/medusa-workflows/steps/createInviteStep/index.html.md)
- [refreshInviteTokensStep](https://docs.medusajs.com/references/medusa-workflows/steps/refreshInviteTokensStep/index.html.md)
- [validateTokenStep](https://docs.medusajs.com/references/medusa-workflows/steps/validateTokenStep/index.html.md)
- [deleteInvitesStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteInvitesStep/index.html.md)
- [notifyOnFailureStep](https://docs.medusajs.com/references/medusa-workflows/steps/notifyOnFailureStep/index.html.md)
- [sendNotificationsStep](https://docs.medusajs.com/references/medusa-workflows/steps/sendNotificationsStep/index.html.md)
- [deleteLineItemsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteLineItemsStep/index.html.md)
- [updateLineItemsStepWithSelector](https://docs.medusajs.com/references/medusa-workflows/steps/updateLineItemsStepWithSelector/index.html.md)
- [listLineItemsStep](https://docs.medusajs.com/references/medusa-workflows/steps/listLineItemsStep/index.html.md)
- [addOrderTransactionStep](https://docs.medusajs.com/references/medusa-workflows/steps/addOrderTransactionStep/index.html.md)
- [archiveOrdersStep](https://docs.medusajs.com/references/medusa-workflows/steps/archiveOrdersStep/index.html.md)
- [cancelOrderClaimStep](https://docs.medusajs.com/references/medusa-workflows/steps/cancelOrderClaimStep/index.html.md)
- [cancelOrderChangeStep](https://docs.medusajs.com/references/medusa-workflows/steps/cancelOrderChangeStep/index.html.md)
- [cancelOrderExchangeStep](https://docs.medusajs.com/references/medusa-workflows/steps/cancelOrderExchangeStep/index.html.md)
- [cancelOrderFulfillmentStep](https://docs.medusajs.com/references/medusa-workflows/steps/cancelOrderFulfillmentStep/index.html.md)
- [cancelOrderReturnStep](https://docs.medusajs.com/references/medusa-workflows/steps/cancelOrderReturnStep/index.html.md)
- [cancelOrdersStep](https://docs.medusajs.com/references/medusa-workflows/steps/cancelOrdersStep/index.html.md)
- [completeOrdersStep](https://docs.medusajs.com/references/medusa-workflows/steps/completeOrdersStep/index.html.md)
- [createCompleteReturnStep](https://docs.medusajs.com/references/medusa-workflows/steps/createCompleteReturnStep/index.html.md)
- [createOrderChangeStep](https://docs.medusajs.com/references/medusa-workflows/steps/createOrderChangeStep/index.html.md)
- [createOrderClaimItemsFromActionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createOrderClaimItemsFromActionsStep/index.html.md)
- [createOrderClaimsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createOrderClaimsStep/index.html.md)
- [createOrderExchangeItemsFromActionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createOrderExchangeItemsFromActionsStep/index.html.md)
- [createOrderExchangesStep](https://docs.medusajs.com/references/medusa-workflows/steps/createOrderExchangesStep/index.html.md)
- [createOrdersStep](https://docs.medusajs.com/references/medusa-workflows/steps/createOrdersStep/index.html.md)
- [createOrderLineItemsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createOrderLineItemsStep/index.html.md)
- [createReturnsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createReturnsStep/index.html.md)
- [declineOrderChangeStep](https://docs.medusajs.com/references/medusa-workflows/steps/declineOrderChangeStep/index.html.md)
- [deleteExchangesStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteExchangesStep/index.html.md)
- [deleteOrderChangeActionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteOrderChangeActionsStep/index.html.md)
- [deleteClaimsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteClaimsStep/index.html.md)
- [deleteOrderChangesStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteOrderChangesStep/index.html.md)
- [deleteOrderLineItems](https://docs.medusajs.com/references/medusa-workflows/steps/deleteOrderLineItems/index.html.md)
- [deleteReturnsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteReturnsStep/index.html.md)
- [deleteOrderShippingMethods](https://docs.medusajs.com/references/medusa-workflows/steps/deleteOrderShippingMethods/index.html.md)
- [previewOrderChangeStep](https://docs.medusajs.com/references/medusa-workflows/steps/previewOrderChangeStep/index.html.md)
- [registerOrderChangesStep](https://docs.medusajs.com/references/medusa-workflows/steps/registerOrderChangesStep/index.html.md)
- [registerOrderDeliveryStep](https://docs.medusajs.com/references/medusa-workflows/steps/registerOrderDeliveryStep/index.html.md)
- [registerOrderFulfillmentStep](https://docs.medusajs.com/references/medusa-workflows/steps/registerOrderFulfillmentStep/index.html.md)
- [registerOrderShipmentStep](https://docs.medusajs.com/references/medusa-workflows/steps/registerOrderShipmentStep/index.html.md)
- [updateOrderChangeActionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateOrderChangeActionsStep/index.html.md)
- [setOrderTaxLinesForItemsStep](https://docs.medusajs.com/references/medusa-workflows/steps/setOrderTaxLinesForItemsStep/index.html.md)
- [updateOrderChangesStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateOrderChangesStep/index.html.md)
- [updateOrderShippingMethodsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateOrderShippingMethodsStep/index.html.md)
- [updateOrdersStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateOrdersStep/index.html.md)
- [updateReturnsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateReturnsStep/index.html.md)
- [updateReturnItemsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateReturnItemsStep/index.html.md)
- [authorizePaymentSessionStep](https://docs.medusajs.com/references/medusa-workflows/steps/authorizePaymentSessionStep/index.html.md)
- [cancelPaymentStep](https://docs.medusajs.com/references/medusa-workflows/steps/cancelPaymentStep/index.html.md)
- [refundPaymentsStep](https://docs.medusajs.com/references/medusa-workflows/steps/refundPaymentsStep/index.html.md)
- [refundPaymentStep](https://docs.medusajs.com/references/medusa-workflows/steps/refundPaymentStep/index.html.md)
- [capturePaymentStep](https://docs.medusajs.com/references/medusa-workflows/steps/capturePaymentStep/index.html.md)
- [createPaymentAccountHolderStep](https://docs.medusajs.com/references/medusa-workflows/steps/createPaymentAccountHolderStep/index.html.md)
- [createPaymentSessionStep](https://docs.medusajs.com/references/medusa-workflows/steps/createPaymentSessionStep/index.html.md)
- [createRefundReasonStep](https://docs.medusajs.com/references/medusa-workflows/steps/createRefundReasonStep/index.html.md)
- [deleteRefundReasonsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteRefundReasonsStep/index.html.md)
- [deletePaymentSessionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deletePaymentSessionsStep/index.html.md)
- [updatePaymentCollectionStep](https://docs.medusajs.com/references/medusa-workflows/steps/updatePaymentCollectionStep/index.html.md)
- [updateRefundReasonsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateRefundReasonsStep/index.html.md)
- [validateDeletedPaymentSessionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/validateDeletedPaymentSessionsStep/index.html.md)
- [createPricePreferencesStep](https://docs.medusajs.com/references/medusa-workflows/steps/createPricePreferencesStep/index.html.md)
- [createPriceSetsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createPriceSetsStep/index.html.md)
- [updatePricePreferencesAsArrayStep](https://docs.medusajs.com/references/medusa-workflows/steps/updatePricePreferencesAsArrayStep/index.html.md)
- [deletePricePreferencesStep](https://docs.medusajs.com/references/medusa-workflows/steps/deletePricePreferencesStep/index.html.md)
- [updatePricePreferencesStep](https://docs.medusajs.com/references/medusa-workflows/steps/updatePricePreferencesStep/index.html.md)
- [updatePriceSetsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updatePriceSetsStep/index.html.md)
- [createPriceListPricesStep](https://docs.medusajs.com/references/medusa-workflows/steps/createPriceListPricesStep/index.html.md)
- [createPriceListsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createPriceListsStep/index.html.md)
- [deletePriceListsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deletePriceListsStep/index.html.md)
- [getExistingPriceListsPriceIdsStep](https://docs.medusajs.com/references/medusa-workflows/steps/getExistingPriceListsPriceIdsStep/index.html.md)
- [removePriceListPricesStep](https://docs.medusajs.com/references/medusa-workflows/steps/removePriceListPricesStep/index.html.md)
- [updatePriceListPricesStep](https://docs.medusajs.com/references/medusa-workflows/steps/updatePriceListPricesStep/index.html.md)
- [validateVariantPriceLinksStep](https://docs.medusajs.com/references/medusa-workflows/steps/validateVariantPriceLinksStep/index.html.md)
- [updatePriceListsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updatePriceListsStep/index.html.md)
- [validatePriceListsStep](https://docs.medusajs.com/references/medusa-workflows/steps/validatePriceListsStep/index.html.md)
- [batchLinkProductsToCategoryStep](https://docs.medusajs.com/references/medusa-workflows/steps/batchLinkProductsToCategoryStep/index.html.md)
- [batchLinkProductsToCollectionStep](https://docs.medusajs.com/references/medusa-workflows/steps/batchLinkProductsToCollectionStep/index.html.md)
- [createCollectionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createCollectionsStep/index.html.md)
- [createProductOptionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createProductOptionsStep/index.html.md)
- [createProductTagsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createProductTagsStep/index.html.md)
- [createProductVariantsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createProductVariantsStep/index.html.md)
- [createProductTypesStep](https://docs.medusajs.com/references/medusa-workflows/steps/createProductTypesStep/index.html.md)
- [createProductsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createProductsStep/index.html.md)
- [createVariantPricingLinkStep](https://docs.medusajs.com/references/medusa-workflows/steps/createVariantPricingLinkStep/index.html.md)
- [deleteCollectionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteCollectionsStep/index.html.md)
- [deleteProductOptionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteProductOptionsStep/index.html.md)
- [deleteProductTagsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteProductTagsStep/index.html.md)
- [deleteProductTypesStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteProductTypesStep/index.html.md)
- [deleteProductsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteProductsStep/index.html.md)
- [deleteProductVariantsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteProductVariantsStep/index.html.md)
- [getAllProductsStep](https://docs.medusajs.com/references/medusa-workflows/steps/getAllProductsStep/index.html.md)
- [generateProductCsvStep](https://docs.medusajs.com/references/medusa-workflows/steps/generateProductCsvStep/index.html.md)
- [getProductsStep](https://docs.medusajs.com/references/medusa-workflows/steps/getProductsStep/index.html.md)
- [getVariantAvailabilityStep](https://docs.medusajs.com/references/medusa-workflows/steps/getVariantAvailabilityStep/index.html.md)
- [parseProductCsvStep](https://docs.medusajs.com/references/medusa-workflows/steps/parseProductCsvStep/index.html.md)
- [processImportChunksStep](https://docs.medusajs.com/references/medusa-workflows/steps/processImportChunksStep/index.html.md)
- [normalizeCsvToChunksStep](https://docs.medusajs.com/references/medusa-workflows/steps/normalizeCsvToChunksStep/index.html.md)
- [normalizeCsvStep](https://docs.medusajs.com/references/medusa-workflows/steps/normalizeCsvStep/index.html.md)
- [updateCollectionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateCollectionsStep/index.html.md)
- [updateProductTagsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateProductTagsStep/index.html.md)
- [updateProductOptionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateProductOptionsStep/index.html.md)
- [updateProductTypesStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateProductTypesStep/index.html.md)
- [updateProductVariantsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateProductVariantsStep/index.html.md)
- [updateProductsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateProductsStep/index.html.md)
- [waitConfirmationProductImportStep](https://docs.medusajs.com/references/medusa-workflows/steps/waitConfirmationProductImportStep/index.html.md)
- [createProductCategoriesStep](https://docs.medusajs.com/references/medusa-workflows/steps/createProductCategoriesStep/index.html.md)
- [deleteProductCategoriesStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteProductCategoriesStep/index.html.md)
- [updateProductCategoriesStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateProductCategoriesStep/index.html.md)
- [createRegionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createRegionsStep/index.html.md)
- [deleteRegionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteRegionsStep/index.html.md)
- [setRegionsPaymentProvidersStep](https://docs.medusajs.com/references/medusa-workflows/steps/setRegionsPaymentProvidersStep/index.html.md)
- [updateRegionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateRegionsStep/index.html.md)
- [addCampaignPromotionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/addCampaignPromotionsStep/index.html.md)
- [addRulesToPromotionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/addRulesToPromotionsStep/index.html.md)
- [createPromotionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createPromotionsStep/index.html.md)
- [createCampaignsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createCampaignsStep/index.html.md)
- [deleteCampaignsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteCampaignsStep/index.html.md)
- [removeRulesFromPromotionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/removeRulesFromPromotionsStep/index.html.md)
- [removeCampaignPromotionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/removeCampaignPromotionsStep/index.html.md)
- [updateCampaignsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateCampaignsStep/index.html.md)
- [deletePromotionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deletePromotionsStep/index.html.md)
- [updatePromotionRulesStep](https://docs.medusajs.com/references/medusa-workflows/steps/updatePromotionRulesStep/index.html.md)
- [updatePromotionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updatePromotionsStep/index.html.md)
- [createReturnReasonsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createReturnReasonsStep/index.html.md)
- [deleteReturnReasonStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteReturnReasonStep/index.html.md)
- [updateReturnReasonsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateReturnReasonsStep/index.html.md)
- [createReservationsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createReservationsStep/index.html.md)
- [deleteReservationsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteReservationsStep/index.html.md)
- [deleteReservationsByLineItemsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteReservationsByLineItemsStep/index.html.md)
- [updateReservationsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateReservationsStep/index.html.md)
- [listShippingOptionsForContextStep](https://docs.medusajs.com/references/medusa-workflows/steps/listShippingOptionsForContextStep/index.html.md)
- [canDeleteSalesChannelsOrThrowStep](https://docs.medusajs.com/references/medusa-workflows/steps/canDeleteSalesChannelsOrThrowStep/index.html.md)
- [associateProductsWithSalesChannelsStep](https://docs.medusajs.com/references/medusa-workflows/steps/associateProductsWithSalesChannelsStep/index.html.md)
- [associateLocationsWithSalesChannelsStep](https://docs.medusajs.com/references/medusa-workflows/steps/associateLocationsWithSalesChannelsStep/index.html.md)
- [createDefaultSalesChannelStep](https://docs.medusajs.com/references/medusa-workflows/steps/createDefaultSalesChannelStep/index.html.md)
- [createSalesChannelsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createSalesChannelsStep/index.html.md)
- [deleteSalesChannelsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteSalesChannelsStep/index.html.md)
- [detachLocationsFromSalesChannelsStep](https://docs.medusajs.com/references/medusa-workflows/steps/detachLocationsFromSalesChannelsStep/index.html.md)
- [deleteShippingProfilesStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteShippingProfilesStep/index.html.md)
- [detachProductsFromSalesChannelsStep](https://docs.medusajs.com/references/medusa-workflows/steps/detachProductsFromSalesChannelsStep/index.html.md)
- [updateSalesChannelsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateSalesChannelsStep/index.html.md)
- [createStockLocations](https://docs.medusajs.com/references/medusa-workflows/steps/createStockLocations/index.html.md)
- [deleteStockLocationsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteStockLocationsStep/index.html.md)
- [updateStockLocationsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateStockLocationsStep/index.html.md)
- [createTaxRateRulesStep](https://docs.medusajs.com/references/medusa-workflows/steps/createTaxRateRulesStep/index.html.md)
- [createTaxRegionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createTaxRegionsStep/index.html.md)
- [createTaxRatesStep](https://docs.medusajs.com/references/medusa-workflows/steps/createTaxRatesStep/index.html.md)
- [deleteTaxRateRulesStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteTaxRateRulesStep/index.html.md)
- [deleteTaxRatesStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteTaxRatesStep/index.html.md)
- [getItemTaxLinesStep](https://docs.medusajs.com/references/medusa-workflows/steps/getItemTaxLinesStep/index.html.md)
- [deleteTaxRegionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteTaxRegionsStep/index.html.md)
- [listTaxRateIdsStep](https://docs.medusajs.com/references/medusa-workflows/steps/listTaxRateIdsStep/index.html.md)
- [listTaxRateRuleIdsStep](https://docs.medusajs.com/references/medusa-workflows/steps/listTaxRateRuleIdsStep/index.html.md)
- [updateTaxRatesStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateTaxRatesStep/index.html.md)
- [updateTaxRegionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateTaxRegionsStep/index.html.md)
- [createStoresStep](https://docs.medusajs.com/references/medusa-workflows/steps/createStoresStep/index.html.md)
- [deleteStoresStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteStoresStep/index.html.md)
- [updateStoresStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateStoresStep/index.html.md)
- [createUsersStep](https://docs.medusajs.com/references/medusa-workflows/steps/createUsersStep/index.html.md)
- [deleteUsersStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteUsersStep/index.html.md)
- [updateUsersStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateUsersStep/index.html.md)


# Events Reference

This documentation page includes the list of all events emitted by [Medusa's workflows](https://docs.medusajs.com/resources/medusa-workflows-reference/index.html.md).

## Cart Events

### Summary

|Event|Description|
|---|---|
|cart.created|Emitted when a cart is created.|
|cart.updated|Emitted when a cart's details are updated.|
|cart.region\_updated|Emitted when the cart's region is updated. This
event is emitted alongside the |
|cart.customer\_transferred|Emitted when the customer in the cart is transferred.|

### cart.created

Emitted when a cart is created.

#### Payload

```ts
{
  id, // The ID of the cart
}
```

#### Workflows Emitting this Event

The following workflows emit this event when they're executed. These workflows are executed by Medusa's API routes. You can also view the events emitted by API routes in the [Store](https://docs.medusajs.com/api/store) and [Admin](https://docs.medusajs.com/api/admin) API references.

- [createCartWorkflow](https://docs.medusajs.com/references/medusa-workflows/createCartWorkflow/index.html.md)

***

### cart.updated

Emitted when a cart's details are updated.

#### Payload

```ts
{
  id, // The ID of the cart
}
```

#### Workflows Emitting this Event

The following workflows emit this event when they're executed. These workflows are executed by Medusa's API routes. You can also view the events emitted by API routes in the [Store](https://docs.medusajs.com/api/store) and [Admin](https://docs.medusajs.com/api/admin) API references.

- [updateLineItemInCartWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateLineItemInCartWorkflow/index.html.md)
- [updateCartWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateCartWorkflow/index.html.md)
- [addToCartWorkflow](https://docs.medusajs.com/references/medusa-workflows/addToCartWorkflow/index.html.md)
- [addShippingMethodToCartWorkflow](https://docs.medusajs.com/references/medusa-workflows/addShippingMethodToCartWorkflow/index.html.md)

***

### cart.region\_updated

Emitted when the cart's region is updated. This
event is emitted alongside the `cart.updated` event.

#### Payload

```ts
{
  id, // The ID of the cart
}
```

#### Workflows Emitting this Event

The following workflows emit this event when they're executed. These workflows are executed by Medusa's API routes. You can also view the events emitted by API routes in the [Store](https://docs.medusajs.com/api/store) and [Admin](https://docs.medusajs.com/api/admin) API references.

- [updateCartWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateCartWorkflow/index.html.md)

***

### cart.customer\_transferred&#xA;

Emitted when the customer in the cart is transferred.

#### Payload

```ts
{
  id, // The ID of the cart
  customer_id, // The ID of the customer
}
```

#### Workflows Emitting this Event

The following workflows emit this event when they're executed. These workflows are executed by Medusa's API routes. You can also view the events emitted by API routes in the [Store](https://docs.medusajs.com/api/store) and [Admin](https://docs.medusajs.com/api/admin) API references.

- [transferCartCustomerWorkflow](https://docs.medusajs.com/references/medusa-workflows/transferCartCustomerWorkflow/index.html.md)

***

## Customer Events

### Summary

|Event|Description|
|---|---|
|customer.created|Emitted when a customer is created.|
|customer.updated|Emitted when a customer is updated.|
|customer.deleted|Emitted when a customer is deleted.|

### customer.created

Emitted when a customer is created.

#### Payload

```ts
[{
  id, // The ID of the customer
}]
```

#### Workflows Emitting this Event

The following workflows emit this event when they're executed. These workflows are executed by Medusa's API routes. You can also view the events emitted by API routes in the [Store](https://docs.medusajs.com/api/store) and [Admin](https://docs.medusajs.com/api/admin) API references.

- [createCustomersWorkflow](https://docs.medusajs.com/references/medusa-workflows/createCustomersWorkflow/index.html.md)
- [createCustomerAccountWorkflow](https://docs.medusajs.com/references/medusa-workflows/createCustomerAccountWorkflow/index.html.md)

***

### customer.updated

Emitted when a customer is updated.

#### Payload

```ts
[{
  id, // The ID of the customer
}]
```

#### Workflows Emitting this Event

The following workflows emit this event when they're executed. These workflows are executed by Medusa's API routes. You can also view the events emitted by API routes in the [Store](https://docs.medusajs.com/api/store) and [Admin](https://docs.medusajs.com/api/admin) API references.

- [updateCustomersWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateCustomersWorkflow/index.html.md)

***

### customer.deleted

Emitted when a customer is deleted.

#### Payload

```ts
[{
  id, // The ID of the customer
}]
```

#### Workflows Emitting this Event

The following workflows emit this event when they're executed. These workflows are executed by Medusa's API routes. You can also view the events emitted by API routes in the [Store](https://docs.medusajs.com/api/store) and [Admin](https://docs.medusajs.com/api/admin) API references.

- [deleteCustomersWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteCustomersWorkflow/index.html.md)
- [removeCustomerAccountWorkflow](https://docs.medusajs.com/references/medusa-workflows/removeCustomerAccountWorkflow/index.html.md)

***

## Order Events

### Summary

|Event|Description|
|---|---|
|order.updated|Emitted when the details of an order or draft order is updated. This
doesn't include updates made by an edit.|
|order.placed|Emitted when an order is placed, or when a draft order is converted to an
order.|
|order.canceled|Emitted when an order is canceld.|
|order.completed|Emitted when orders are completed.|
|order.archived|Emitted when an order is archived.|
|order.fulfillment\_created|Emitted when a fulfillment is created for an order.|
|order.fulfillment\_canceled|Emitted when an order's fulfillment is canceled.|
|order.return\_requested|Emitted when a return request is confirmed.|
|order.return\_received|Emitted when a return is marked as received.|
|order.claim\_created|Emitted when a claim is created for an order.|
|order.exchange\_created|Emitted when an exchange is created for an order.|
|order.transfer\_requested|Emitted when an order is requested to be transferred to
another customer.|

### order.updated

Emitted when the details of an order or draft order is updated. This
doesn't include updates made by an edit.

#### Payload

```ts
{
  id, // The ID of the order
}
```

#### Workflows Emitting this Event

The following workflows emit this event when they're executed. These workflows are executed by Medusa's API routes. You can also view the events emitted by API routes in the [Store](https://docs.medusajs.com/api/store) and [Admin](https://docs.medusajs.com/api/admin) API references.

- [updateOrderWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateOrderWorkflow/index.html.md)
- [updateDraftOrderWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateDraftOrderWorkflow/index.html.md)

***

### order.placed

Emitted when an order is placed, or when a draft order is converted to an
order.

#### Payload

```ts
{
  id, // The ID of the order
}
```

#### Workflows Emitting this Event

The following workflows emit this event when they're executed. These workflows are executed by Medusa's API routes. You can also view the events emitted by API routes in the [Store](https://docs.medusajs.com/api/store) and [Admin](https://docs.medusajs.com/api/admin) API references.

- [convertDraftOrderWorkflow](https://docs.medusajs.com/references/medusa-workflows/convertDraftOrderWorkflow/index.html.md)
- [completeCartWorkflow](https://docs.medusajs.com/references/medusa-workflows/completeCartWorkflow/index.html.md)
- [processPaymentWorkflow](https://docs.medusajs.com/references/medusa-workflows/processPaymentWorkflow/index.html.md)

***

### order.canceled

Emitted when an order is canceld.

#### Payload

```ts
{
  id, // The ID of the order
}
```

#### Workflows Emitting this Event

The following workflows emit this event when they're executed. These workflows are executed by Medusa's API routes. You can also view the events emitted by API routes in the [Store](https://docs.medusajs.com/api/store) and [Admin](https://docs.medusajs.com/api/admin) API references.

- [cancelOrderWorkflow](https://docs.medusajs.com/references/medusa-workflows/cancelOrderWorkflow/index.html.md)

***

### order.completed

Emitted when orders are completed.

#### Payload

```ts
[{
  id, // The ID of the order
}]
```

#### Workflows Emitting this Event

The following workflows emit this event when they're executed. These workflows are executed by Medusa's API routes. You can also view the events emitted by API routes in the [Store](https://docs.medusajs.com/api/store) and [Admin](https://docs.medusajs.com/api/admin) API references.

- [completeOrderWorkflow](https://docs.medusajs.com/references/medusa-workflows/completeOrderWorkflow/index.html.md)

***

### order.archived

Emitted when an order is archived.

#### Payload

```ts
[{
  id, // The ID of the order
}]
```

#### Workflows Emitting this Event

The following workflows emit this event when they're executed. These workflows are executed by Medusa's API routes. You can also view the events emitted by API routes in the [Store](https://docs.medusajs.com/api/store) and [Admin](https://docs.medusajs.com/api/admin) API references.

- [archiveOrderWorkflow](https://docs.medusajs.com/references/medusa-workflows/archiveOrderWorkflow/index.html.md)

***

### order.fulfillment\_created

Emitted when a fulfillment is created for an order.

#### Payload

```ts
{
  order_id, // The ID of the order
  fulfillment_id, // The ID of the fulfillment
  no_notification, // (boolean) Whether to notify the customer
}
```

#### Workflows Emitting this Event

The following workflows emit this event when they're executed. These workflows are executed by Medusa's API routes. You can also view the events emitted by API routes in the [Store](https://docs.medusajs.com/api/store) and [Admin](https://docs.medusajs.com/api/admin) API references.

- [createOrderFulfillmentWorkflow](https://docs.medusajs.com/references/medusa-workflows/createOrderFulfillmentWorkflow/index.html.md)

***

### order.fulfillment\_canceled

Emitted when an order's fulfillment is canceled.

#### Payload

```ts
{
  order_id, // The ID of the order
  fulfillment_id, // The ID of the fulfillment
  no_notification, // (boolean) Whether to notify the customer
}
```

#### Workflows Emitting this Event

The following workflows emit this event when they're executed. These workflows are executed by Medusa's API routes. You can also view the events emitted by API routes in the [Store](https://docs.medusajs.com/api/store) and [Admin](https://docs.medusajs.com/api/admin) API references.

- [cancelOrderFulfillmentWorkflow](https://docs.medusajs.com/references/medusa-workflows/cancelOrderFulfillmentWorkflow/index.html.md)

***

### order.return\_requested

Emitted when a return request is confirmed.

#### Payload

```ts
{
  order_id, // The ID of the order
  return_id, // The ID of the return
}
```

#### Workflows Emitting this Event

The following workflows emit this event when they're executed. These workflows are executed by Medusa's API routes. You can also view the events emitted by API routes in the [Store](https://docs.medusajs.com/api/store) and [Admin](https://docs.medusajs.com/api/admin) API references.

- [createAndCompleteReturnOrderWorkflow](https://docs.medusajs.com/references/medusa-workflows/createAndCompleteReturnOrderWorkflow/index.html.md)
- [confirmReturnRequestWorkflow](https://docs.medusajs.com/references/medusa-workflows/confirmReturnRequestWorkflow/index.html.md)

***

### order.return\_received

Emitted when a return is marked as received.

#### Payload

```ts
{
  order_id, // The ID of the order
  return_id, // The ID of the return
}
```

#### Workflows Emitting this Event

The following workflows emit this event when they're executed. These workflows are executed by Medusa's API routes. You can also view the events emitted by API routes in the [Store](https://docs.medusajs.com/api/store) and [Admin](https://docs.medusajs.com/api/admin) API references.

- [createAndCompleteReturnOrderWorkflow](https://docs.medusajs.com/references/medusa-workflows/createAndCompleteReturnOrderWorkflow/index.html.md)
- [confirmReturnReceiveWorkflow](https://docs.medusajs.com/references/medusa-workflows/confirmReturnReceiveWorkflow/index.html.md)

***

### order.claim\_created

Emitted when a claim is created for an order.

#### Payload

```ts
{
  order_id, // The ID of the order
  claim_id, // The ID of the claim
}
```

#### Workflows Emitting this Event

The following workflows emit this event when they're executed. These workflows are executed by Medusa's API routes. You can also view the events emitted by API routes in the [Store](https://docs.medusajs.com/api/store) and [Admin](https://docs.medusajs.com/api/admin) API references.

- [confirmClaimRequestWorkflow](https://docs.medusajs.com/references/medusa-workflows/confirmClaimRequestWorkflow/index.html.md)

***

### order.exchange\_created

Emitted when an exchange is created for an order.

#### Payload

```ts
{
  order_id, // The ID of the order
  exchange_id, // The ID of the exchange
}
```

#### Workflows Emitting this Event

The following workflows emit this event when they're executed. These workflows are executed by Medusa's API routes. You can also view the events emitted by API routes in the [Store](https://docs.medusajs.com/api/store) and [Admin](https://docs.medusajs.com/api/admin) API references.

- [confirmExchangeRequestWorkflow](https://docs.medusajs.com/references/medusa-workflows/confirmExchangeRequestWorkflow/index.html.md)

***

### order.transfer\_requested

Emitted when an order is requested to be transferred to
another customer.

#### Payload

```ts
{
  id, // The ID of the order
  order_change_id, // The ID of the order change created for the transfer
}
```

#### Workflows Emitting this Event

The following workflows emit this event when they're executed. These workflows are executed by Medusa's API routes. You can also view the events emitted by API routes in the [Store](https://docs.medusajs.com/api/store) and [Admin](https://docs.medusajs.com/api/admin) API references.

- [requestOrderTransferWorkflow](https://docs.medusajs.com/references/medusa-workflows/requestOrderTransferWorkflow/index.html.md)

***

## Order Edit Events

### Summary

|Event|Description|
|---|---|
|order-edit.requested|Emitted when an order edit is requested.|
|order-edit.confirmed|Emitted when an order edit request is confirmed.|
|order-edit.canceled|Emitted when an order edit request is canceled.|

### order-edit.requested&#xA;

Emitted when an order edit is requested.

#### Payload

```ts
{
  order_id, // The ID of the order
  actions, // (array) The [actions](https://docs.medusajs.com/resources/references/fulfillment/interfaces/fulfillment.OrderChangeActionDTO) to edit the order
}
```

#### Workflows Emitting this Event

The following workflows emit this event when they're executed. These workflows are executed by Medusa's API routes. You can also view the events emitted by API routes in the [Store](https://docs.medusajs.com/api/store) and [Admin](https://docs.medusajs.com/api/admin) API references.

- [requestOrderEditRequestWorkflow](https://docs.medusajs.com/references/medusa-workflows/requestOrderEditRequestWorkflow/index.html.md)

***

### order-edit.confirmed&#xA;

Emitted when an order edit request is confirmed.

#### Payload

```ts
{
  order_id, // The ID of the order
  actions, // (array) The [actions](https://docs.medusajs.com/resources/references/fulfillment/interfaces/fulfillment.OrderChangeActionDTO) to edit the order
}
```

#### Workflows Emitting this Event

The following workflows emit this event when they're executed. These workflows are executed by Medusa's API routes. You can also view the events emitted by API routes in the [Store](https://docs.medusajs.com/api/store) and [Admin](https://docs.medusajs.com/api/admin) API references.

- [confirmOrderEditRequestWorkflow](https://docs.medusajs.com/references/medusa-workflows/confirmOrderEditRequestWorkflow/index.html.md)

***

### order-edit.canceled&#xA;

Emitted when an order edit request is canceled.

#### Payload

```ts
{
  order_id, // The ID of the order
  actions, // (array) The [actions](https://docs.medusajs.com/resources/references/fulfillment/interfaces/fulfillment.OrderChangeActionDTO) to edit the order
}
```

#### Workflows Emitting this Event

The following workflows emit this event when they're executed. These workflows are executed by Medusa's API routes. You can also view the events emitted by API routes in the [Store](https://docs.medusajs.com/api/store) and [Admin](https://docs.medusajs.com/api/admin) API references.

- [cancelBeginOrderEditWorkflow](https://docs.medusajs.com/references/medusa-workflows/cancelBeginOrderEditWorkflow/index.html.md)

***

## User Events

### Summary

|Event|Description|
|---|---|
|user.created|Emitted when users are created.|
|user.updated|Emitted when users are updated.|
|user.deleted|Emitted when users are deleted.|

### user.created

Emitted when users are created.

#### Payload

```ts
[{
  id, // The ID of the user
}]
```

#### Workflows Emitting this Event

The following workflows emit this event when they're executed. These workflows are executed by Medusa's API routes. You can also view the events emitted by API routes in the [Store](https://docs.medusajs.com/api/store) and [Admin](https://docs.medusajs.com/api/admin) API references.

- [createUsersWorkflow](https://docs.medusajs.com/references/medusa-workflows/createUsersWorkflow/index.html.md)
- [createUserAccountWorkflow](https://docs.medusajs.com/references/medusa-workflows/createUserAccountWorkflow/index.html.md)
- [acceptInviteWorkflow](https://docs.medusajs.com/references/medusa-workflows/acceptInviteWorkflow/index.html.md)

***

### user.updated

Emitted when users are updated.

#### Payload

```ts
[{
  id, // The ID of the user
}]
```

#### Workflows Emitting this Event

The following workflows emit this event when they're executed. These workflows are executed by Medusa's API routes. You can also view the events emitted by API routes in the [Store](https://docs.medusajs.com/api/store) and [Admin](https://docs.medusajs.com/api/admin) API references.

- [updateUsersWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateUsersWorkflow/index.html.md)

***

### user.deleted

Emitted when users are deleted.

#### Payload

```ts
[{
  id, // The ID of the user
}]
```

#### Workflows Emitting this Event

The following workflows emit this event when they're executed. These workflows are executed by Medusa's API routes. You can also view the events emitted by API routes in the [Store](https://docs.medusajs.com/api/store) and [Admin](https://docs.medusajs.com/api/admin) API references.

- [deleteUsersWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteUsersWorkflow/index.html.md)
- [removeUserAccountWorkflow](https://docs.medusajs.com/references/medusa-workflows/removeUserAccountWorkflow/index.html.md)

***

## Invite Events

### Summary

|Event|Description|
|---|---|
|invite.accepted|Emitted when an invite is accepted.|
|invite.created|Emitted when invites are created. You can listen to this event
to send an email to the invited users, for example.|
|invite.deleted|Emitted when invites are deleted.|
|invite.resent|Emitted when invites should be resent because their token was
refreshed. You can listen to this event to send an email to the invited users,
for example.|

### invite.accepted

Emitted when an invite is accepted.

#### Payload

```ts
{
  id, // The ID of the invite
}
```

#### Workflows Emitting this Event

The following workflows emit this event when they're executed. These workflows are executed by Medusa's API routes. You can also view the events emitted by API routes in the [Store](https://docs.medusajs.com/api/store) and [Admin](https://docs.medusajs.com/api/admin) API references.

- [acceptInviteWorkflow](https://docs.medusajs.com/references/medusa-workflows/acceptInviteWorkflow/index.html.md)

***

### invite.created

Emitted when invites are created. You can listen to this event
to send an email to the invited users, for example.

#### Payload

```ts
[{
  id, // The ID of the invite
}]
```

#### Workflows Emitting this Event

The following workflows emit this event when they're executed. These workflows are executed by Medusa's API routes. You can also view the events emitted by API routes in the [Store](https://docs.medusajs.com/api/store) and [Admin](https://docs.medusajs.com/api/admin) API references.

- [createInvitesWorkflow](https://docs.medusajs.com/references/medusa-workflows/createInvitesWorkflow/index.html.md)

***

### invite.deleted

Emitted when invites are deleted.

#### Payload

```ts
[{
  id, // The ID of the invite
}]
```

#### Workflows Emitting this Event

The following workflows emit this event when they're executed. These workflows are executed by Medusa's API routes. You can also view the events emitted by API routes in the [Store](https://docs.medusajs.com/api/store) and [Admin](https://docs.medusajs.com/api/admin) API references.

- [deleteInvitesWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteInvitesWorkflow/index.html.md)

***

### invite.resent

Emitted when invites should be resent because their token was
refreshed. You can listen to this event to send an email to the invited users,
for example.

#### Payload

```ts
[{
  id, // The ID of the invite
}]
```

#### Workflows Emitting this Event

The following workflows emit this event when they're executed. These workflows are executed by Medusa's API routes. You can also view the events emitted by API routes in the [Store](https://docs.medusajs.com/api/store) and [Admin](https://docs.medusajs.com/api/admin) API references.

- [refreshInviteTokensWorkflow](https://docs.medusajs.com/references/medusa-workflows/refreshInviteTokensWorkflow/index.html.md)

***

## Auth Events

### Summary

|Event|Description|
|---|---|
|auth.password\_reset|Emitted when a reset password token is generated. You can listen to this event
to send a reset password email to the user or customer, for example.|

### auth.password\_reset

Emitted when a reset password token is generated. You can listen to this event
to send a reset password email to the user or customer, for example.

#### Payload

```ts
{
  entity_id, // The identifier of the user or customer. For example, an email address.
  actor_type, // The type of actor. For example, "customer", "user", or custom.
  token, // The generated token.
}
```

#### Workflows Emitting this Event

The following workflows emit this event when they're executed. These workflows are executed by Medusa's API routes. You can also view the events emitted by API routes in the [Store](https://docs.medusajs.com/api/store) and [Admin](https://docs.medusajs.com/api/admin) API references.

- [generateResetPasswordTokenWorkflow](https://docs.medusajs.com/references/medusa-workflows/generateResetPasswordTokenWorkflow/index.html.md)

***

## Sales Channel Events

### Summary

|Event|Description|
|---|---|
|sales-channel.created|Emitted when sales channels are created.|
|sales-channel.updated|Emitted when sales channels are updated.|
|sales-channel.deleted|Emitted when sales channels are deleted.|

### sales-channel.created

Emitted when sales channels are created.

#### Payload

```ts
[{
  id, // The ID of the sales channel
}]
```

#### Workflows Emitting this Event

The following workflows emit this event when they're executed. These workflows are executed by Medusa's API routes. You can also view the events emitted by API routes in the [Store](https://docs.medusajs.com/api/store) and [Admin](https://docs.medusajs.com/api/admin) API references.

- [createSalesChannelsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createSalesChannelsWorkflow/index.html.md)

***

### sales-channel.updated

Emitted when sales channels are updated.

#### Payload

```ts
[{
  id, // The ID of the sales channel
}]
```

#### Workflows Emitting this Event

The following workflows emit this event when they're executed. These workflows are executed by Medusa's API routes. You can also view the events emitted by API routes in the [Store](https://docs.medusajs.com/api/store) and [Admin](https://docs.medusajs.com/api/admin) API references.

- [updateSalesChannelsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateSalesChannelsWorkflow/index.html.md)

***

### sales-channel.deleted

Emitted when sales channels are deleted.

#### Payload

```ts
[{
  id, // The ID of the sales channel
}]
```

#### Workflows Emitting this Event

The following workflows emit this event when they're executed. These workflows are executed by Medusa's API routes. You can also view the events emitted by API routes in the [Store](https://docs.medusajs.com/api/store) and [Admin](https://docs.medusajs.com/api/admin) API references.

- [deleteSalesChannelsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteSalesChannelsWorkflow/index.html.md)

***

## Product Category Events

### Summary

|Event|Description|
|---|---|
|product-category.created|Emitted when product categories are created.|
|product-category.updated|Emitted when product categories are updated.|
|product-category.deleted|Emitted when product categories are deleted.|

### product-category.created

Emitted when product categories are created.

#### Payload

```ts
[{
  id, // The ID of the product category
}]
```

#### Workflows Emitting this Event

The following workflows emit this event when they're executed. These workflows are executed by Medusa's API routes. You can also view the events emitted by API routes in the [Store](https://docs.medusajs.com/api/store) and [Admin](https://docs.medusajs.com/api/admin) API references.

- [createProductCategoriesWorkflow](https://docs.medusajs.com/references/medusa-workflows/createProductCategoriesWorkflow/index.html.md)

***

### product-category.updated

Emitted when product categories are updated.

#### Payload

```ts
[{
  id, // The ID of the product category
}]
```

#### Workflows Emitting this Event

The following workflows emit this event when they're executed. These workflows are executed by Medusa's API routes. You can also view the events emitted by API routes in the [Store](https://docs.medusajs.com/api/store) and [Admin](https://docs.medusajs.com/api/admin) API references.

- [updateProductCategoriesWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateProductCategoriesWorkflow/index.html.md)

***

### product-category.deleted

Emitted when product categories are deleted.

#### Payload

```ts
[{
  id, // The ID of the product category
}]
```

#### Workflows Emitting this Event

The following workflows emit this event when they're executed. These workflows are executed by Medusa's API routes. You can also view the events emitted by API routes in the [Store](https://docs.medusajs.com/api/store) and [Admin](https://docs.medusajs.com/api/admin) API references.

- [deleteProductCategoriesWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteProductCategoriesWorkflow/index.html.md)

***

## Product Collection Events

### Summary

|Event|Description|
|---|---|
|product-collection.created|Emitted when product collections are created.|
|product-collection.updated|Emitted when product collections are updated.|
|product-collection.deleted|Emitted when product collections are deleted.|

### product-collection.created

Emitted when product collections are created.

#### Payload

```ts
[{
  id, // The ID of the product collection
}]
```

#### Workflows Emitting this Event

The following workflows emit this event when they're executed. These workflows are executed by Medusa's API routes. You can also view the events emitted by API routes in the [Store](https://docs.medusajs.com/api/store) and [Admin](https://docs.medusajs.com/api/admin) API references.

- [createCollectionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createCollectionsWorkflow/index.html.md)

***

### product-collection.updated

Emitted when product collections are updated.

#### Payload

```ts
[{
  id, // The ID of the product collection
}]
```

#### Workflows Emitting this Event

The following workflows emit this event when they're executed. These workflows are executed by Medusa's API routes. You can also view the events emitted by API routes in the [Store](https://docs.medusajs.com/api/store) and [Admin](https://docs.medusajs.com/api/admin) API references.

- [updateCollectionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateCollectionsWorkflow/index.html.md)

***

### product-collection.deleted

Emitted when product collections are deleted.

#### Payload

```ts
[{
  id, // The ID of the product collection
}]
```

#### Workflows Emitting this Event

The following workflows emit this event when they're executed. These workflows are executed by Medusa's API routes. You can also view the events emitted by API routes in the [Store](https://docs.medusajs.com/api/store) and [Admin](https://docs.medusajs.com/api/admin) API references.

- [deleteCollectionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteCollectionsWorkflow/index.html.md)

***

## Product Variant Events

### Summary

|Event|Description|
|---|---|
|product-variant.updated|Emitted when product variants are updated.|
|product-variant.created|Emitted when product variants are created.|
|product-variant.deleted|Emitted when product variants are deleted.|

### product-variant.updated

Emitted when product variants are updated.

#### Payload

```ts
[{
  id, // The ID of the product variant
}]
```

#### Workflows Emitting this Event

The following workflows emit this event when they're executed. These workflows are executed by Medusa's API routes. You can also view the events emitted by API routes in the [Store](https://docs.medusajs.com/api/store) and [Admin](https://docs.medusajs.com/api/admin) API references.

- [updateProductVariantsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateProductVariantsWorkflow/index.html.md)
- [batchProductVariantsWorkflow](https://docs.medusajs.com/references/medusa-workflows/batchProductVariantsWorkflow/index.html.md)

***

### product-variant.created

Emitted when product variants are created.

#### Payload

```ts
[{
  id, // The ID of the product variant
}]
```

#### Workflows Emitting this Event

The following workflows emit this event when they're executed. These workflows are executed by Medusa's API routes. You can also view the events emitted by API routes in the [Store](https://docs.medusajs.com/api/store) and [Admin](https://docs.medusajs.com/api/admin) API references.

- [createProductVariantsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createProductVariantsWorkflow/index.html.md)
- [createProductsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createProductsWorkflow/index.html.md)
- [batchProductVariantsWorkflow](https://docs.medusajs.com/references/medusa-workflows/batchProductVariantsWorkflow/index.html.md)
- [batchProductsWorkflow](https://docs.medusajs.com/references/medusa-workflows/batchProductsWorkflow/index.html.md)
- [importProductsWorkflow](https://docs.medusajs.com/references/medusa-workflows/importProductsWorkflow/index.html.md)

***

### product-variant.deleted

Emitted when product variants are deleted.

#### Payload

```ts
[{
  id, // The ID of the product variant
}]
```

#### Workflows Emitting this Event

The following workflows emit this event when they're executed. These workflows are executed by Medusa's API routes. You can also view the events emitted by API routes in the [Store](https://docs.medusajs.com/api/store) and [Admin](https://docs.medusajs.com/api/admin) API references.

- [deleteProductVariantsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteProductVariantsWorkflow/index.html.md)
- [batchProductVariantsWorkflow](https://docs.medusajs.com/references/medusa-workflows/batchProductVariantsWorkflow/index.html.md)

***

## Product Events

### Summary

|Event|Description|
|---|---|
|product.updated|Emitted when products are updated.|
|product.created|Emitted when products are created.|
|product.deleted|Emitted when products are deleted.|

### product.updated

Emitted when products are updated.

#### Payload

```ts
[{
  id, // The ID of the product
}]
```

#### Workflows Emitting this Event

The following workflows emit this event when they're executed. These workflows are executed by Medusa's API routes. You can also view the events emitted by API routes in the [Store](https://docs.medusajs.com/api/store) and [Admin](https://docs.medusajs.com/api/admin) API references.

- [updateProductsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateProductsWorkflow/index.html.md)
- [batchProductsWorkflow](https://docs.medusajs.com/references/medusa-workflows/batchProductsWorkflow/index.html.md)
- [importProductsWorkflow](https://docs.medusajs.com/references/medusa-workflows/importProductsWorkflow/index.html.md)

***

### product.created

Emitted when products are created.

#### Payload

```ts
[{
  id, // The ID of the product
}]
```

#### Workflows Emitting this Event

The following workflows emit this event when they're executed. These workflows are executed by Medusa's API routes. You can also view the events emitted by API routes in the [Store](https://docs.medusajs.com/api/store) and [Admin](https://docs.medusajs.com/api/admin) API references.

- [createProductsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createProductsWorkflow/index.html.md)
- [batchProductsWorkflow](https://docs.medusajs.com/references/medusa-workflows/batchProductsWorkflow/index.html.md)
- [importProductsWorkflow](https://docs.medusajs.com/references/medusa-workflows/importProductsWorkflow/index.html.md)

***

### product.deleted

Emitted when products are deleted.

#### Payload

```ts
[{
  id, // The ID of the product
}]
```

#### Workflows Emitting this Event

The following workflows emit this event when they're executed. These workflows are executed by Medusa's API routes. You can also view the events emitted by API routes in the [Store](https://docs.medusajs.com/api/store) and [Admin](https://docs.medusajs.com/api/admin) API references.

- [deleteProductsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteProductsWorkflow/index.html.md)
- [batchProductsWorkflow](https://docs.medusajs.com/references/medusa-workflows/batchProductsWorkflow/index.html.md)
- [importProductsWorkflow](https://docs.medusajs.com/references/medusa-workflows/importProductsWorkflow/index.html.md)

***

## Product Type Events

### Summary

|Event|Description|
|---|---|
|product-type.updated|Emitted when product types are updated.|
|product-type.created|Emitted when product types are created.|
|product-type.deleted|Emitted when product types are deleted.|

### product-type.updated

Emitted when product types are updated.

#### Payload

```ts
[{
  id, // The ID of the product type
}]
```

#### Workflows Emitting this Event

The following workflows emit this event when they're executed. These workflows are executed by Medusa's API routes. You can also view the events emitted by API routes in the [Store](https://docs.medusajs.com/api/store) and [Admin](https://docs.medusajs.com/api/admin) API references.

- [updateProductTypesWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateProductTypesWorkflow/index.html.md)

***

### product-type.created

Emitted when product types are created.

#### Payload

```ts
[{
  id, // The ID of the product type
}]
```

#### Workflows Emitting this Event

The following workflows emit this event when they're executed. These workflows are executed by Medusa's API routes. You can also view the events emitted by API routes in the [Store](https://docs.medusajs.com/api/store) and [Admin](https://docs.medusajs.com/api/admin) API references.

- [createProductTypesWorkflow](https://docs.medusajs.com/references/medusa-workflows/createProductTypesWorkflow/index.html.md)

***

### product-type.deleted

Emitted when product types are deleted.

#### Payload

```ts
[{
  id, // The ID of the product type
}]
```

#### Workflows Emitting this Event

The following workflows emit this event when they're executed. These workflows are executed by Medusa's API routes. You can also view the events emitted by API routes in the [Store](https://docs.medusajs.com/api/store) and [Admin](https://docs.medusajs.com/api/admin) API references.

- [deleteProductTypesWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteProductTypesWorkflow/index.html.md)

***

## Product Tag Events

### Summary

|Event|Description|
|---|---|
|product-tag.updated|Emitted when product tags are updated.|
|product-tag.created|Emitted when product tags are created.|
|product-tag.deleted|Emitted when product tags are deleted.|

### product-tag.updated

Emitted when product tags are updated.

#### Payload

```ts
[{
  id, // The ID of the product tag
}]
```

#### Workflows Emitting this Event

The following workflows emit this event when they're executed. These workflows are executed by Medusa's API routes. You can also view the events emitted by API routes in the [Store](https://docs.medusajs.com/api/store) and [Admin](https://docs.medusajs.com/api/admin) API references.

- [updateProductTagsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateProductTagsWorkflow/index.html.md)

***

### product-tag.created

Emitted when product tags are created.

#### Payload

```ts
[{
  id, // The ID of the product tag
}]
```

#### Workflows Emitting this Event

The following workflows emit this event when they're executed. These workflows are executed by Medusa's API routes. You can also view the events emitted by API routes in the [Store](https://docs.medusajs.com/api/store) and [Admin](https://docs.medusajs.com/api/admin) API references.

- [createProductTagsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createProductTagsWorkflow/index.html.md)

***

### product-tag.deleted

Emitted when product tags are deleted.

#### Payload

```ts
[{
  id, // The ID of the product tag
}]
```

#### Workflows Emitting this Event

The following workflows emit this event when they're executed. These workflows are executed by Medusa's API routes. You can also view the events emitted by API routes in the [Store](https://docs.medusajs.com/api/store) and [Admin](https://docs.medusajs.com/api/admin) API references.

- [deleteProductTagsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteProductTagsWorkflow/index.html.md)

***

## Product Option Events

### Summary

|Event|Description|
|---|---|
|product-option.updated|Emitted when product options are updated.|
|product-option.created|Emitted when product options are created.|
|product-option.deleted|Emitted when product options are deleted.|

### product-option.updated

Emitted when product options are updated.

#### Payload

```ts
[{
  id, // The ID of the product option
}]
```

#### Workflows Emitting this Event

The following workflows emit this event when they're executed. These workflows are executed by Medusa's API routes. You can also view the events emitted by API routes in the [Store](https://docs.medusajs.com/api/store) and [Admin](https://docs.medusajs.com/api/admin) API references.

- [updateProductOptionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateProductOptionsWorkflow/index.html.md)

***

### product-option.created

Emitted when product options are created.

#### Payload

```ts
[{
  id, // The ID of the product option
}]
```

#### Workflows Emitting this Event

The following workflows emit this event when they're executed. These workflows are executed by Medusa's API routes. You can also view the events emitted by API routes in the [Store](https://docs.medusajs.com/api/store) and [Admin](https://docs.medusajs.com/api/admin) API references.

- [createProductOptionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createProductOptionsWorkflow/index.html.md)

***

### product-option.deleted

Emitted when product options are deleted.

#### Payload

```ts
[{
  id, // The ID of the product option
}]
```

#### Workflows Emitting this Event

The following workflows emit this event when they're executed. These workflows are executed by Medusa's API routes. You can also view the events emitted by API routes in the [Store](https://docs.medusajs.com/api/store) and [Admin](https://docs.medusajs.com/api/admin) API references.

- [deleteProductOptionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteProductOptionsWorkflow/index.html.md)

***

## Region Events

### Summary

|Event|Description|
|---|---|
|region.updated|Emitted when regions are updated.|
|region.created|Emitted when regions are created.|
|region.deleted|Emitted when regions are deleted.|

### region.updated

Emitted when regions are updated.

#### Payload

```ts
[{
  id, // The ID of the region
}]
```

#### Workflows Emitting this Event

The following workflows emit this event when they're executed. These workflows are executed by Medusa's API routes. You can also view the events emitted by API routes in the [Store](https://docs.medusajs.com/api/store) and [Admin](https://docs.medusajs.com/api/admin) API references.

- [updateRegionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateRegionsWorkflow/index.html.md)

***

### region.created

Emitted when regions are created.

#### Payload

```ts
[{
  id, // The ID of the region
}]
```

#### Workflows Emitting this Event

The following workflows emit this event when they're executed. These workflows are executed by Medusa's API routes. You can also view the events emitted by API routes in the [Store](https://docs.medusajs.com/api/store) and [Admin](https://docs.medusajs.com/api/admin) API references.

- [createRegionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createRegionsWorkflow/index.html.md)

***

### region.deleted

Emitted when regions are deleted.

#### Payload

```ts
[{
  id, // The ID of the region
}]
```

#### Workflows Emitting this Event

The following workflows emit this event when they're executed. These workflows are executed by Medusa's API routes. You can also view the events emitted by API routes in the [Store](https://docs.medusajs.com/api/store) and [Admin](https://docs.medusajs.com/api/admin) API references.

- [deleteRegionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteRegionsWorkflow/index.html.md)

***

## Fulfillment Events

### Summary

|Event|Description|
|---|---|
|shipment.created|Emitted when a shipment is created for an order.|
|delivery.created|Emitted when a fulfillment is marked as delivered.|

### shipment.created

Emitted when a shipment is created for an order.

#### Payload

```ts
{
  id, // the ID of the shipment
  no_notification, // (boolean) whether to notify the customer
}
```

#### Workflows Emitting this Event

The following workflows emit this event when they're executed. These workflows are executed by Medusa's API routes. You can also view the events emitted by API routes in the [Store](https://docs.medusajs.com/api/store) and [Admin](https://docs.medusajs.com/api/admin) API references.

- [createOrderShipmentWorkflow](https://docs.medusajs.com/references/medusa-workflows/createOrderShipmentWorkflow/index.html.md)

***

### delivery.created

Emitted when a fulfillment is marked as delivered.

#### Payload

```ts
{
  id, // the ID of the fulfillment
}
```

#### Workflows Emitting this Event

The following workflows emit this event when they're executed. These workflows are executed by Medusa's API routes. You can also view the events emitted by API routes in the [Store](https://docs.medusajs.com/api/store) and [Admin](https://docs.medusajs.com/api/admin) API references.

- [markOrderFulfillmentAsDeliveredWorkflow](https://docs.medusajs.com/references/medusa-workflows/markOrderFulfillmentAsDeliveredWorkflow/index.html.md)

***

## Payment Events Events

### Summary

|Event|Description|
|---|---|
|payment.captured|Emitted when a payment is captured.|
|payment.refunded|Emitted when a payment is refunded.|

### payment.captured

Emitted when a payment is captured.

#### Payload

```ts
{
  id, // the ID of the payment
}
```

#### Workflows Emitting this Event

The following workflows emit this event when they're executed. These workflows are executed by Medusa's API routes. You can also view the events emitted by API routes in the [Store](https://docs.medusajs.com/api/store) and [Admin](https://docs.medusajs.com/api/admin) API references.

- [capturePaymentWorkflow](https://docs.medusajs.com/references/medusa-workflows/capturePaymentWorkflow/index.html.md)
- [processPaymentWorkflow](https://docs.medusajs.com/references/medusa-workflows/processPaymentWorkflow/index.html.md)
- [markPaymentCollectionAsPaid](https://docs.medusajs.com/references/medusa-workflows/markPaymentCollectionAsPaid/index.html.md)

***

### payment.refunded

Emitted when a payment is refunded.

#### Payload

```ts
{
  id, // the ID of the payment
}
```

#### Workflows Emitting this Event

The following workflows emit this event when they're executed. These workflows are executed by Medusa's API routes. You can also view the events emitted by API routes in the [Store](https://docs.medusajs.com/api/store) and [Admin](https://docs.medusajs.com/api/admin) API references.

- [refundPaymentWorkflow](https://docs.medusajs.com/references/medusa-workflows/refundPaymentWorkflow/index.html.md)


# Medusa CLI Reference

The Medusa CLI tool provides commands that facilitate your development.

### Prerequisites

- [Node.js v20+](https://nodejs.org/en/download)
- [Git CLI tool](https://git-scm.com/downloads)
- [PostgreSQL](https://www.postgresql.org/download/)

## Usage

In your Medusa application's directory, you can use the Medusa CLI tool using NPX.

For example:

```bash
npx medusa --help
```

***


# build Command - Medusa CLI Reference

Create a standalone build of the Medusa application.

This creates a build that:

- Doesn't rely on the source TypeScript files.
- Can be copied to a production server reliably.

The build is outputted to a new `.medusa/server` directory.

```bash
npx medusa build
```

Refer to [this section](#run-built-medusa-application) for next steps.

## Options

|Option|Description|
|---|---|---|
|\`--admin-only\`|Whether to only build the admin to host it separately. If this option is not passed, the admin is built to the |

***

## Run Built Medusa Application

After running the `build` command, use the following step to run the built Medusa application:

- Change to the `.medusa/server` directory and install the dependencies:

```bash npm2yarn
cd .medusa/server && npm install
```

- When running the application locally, make sure to copy the `.env` file from the root project's directory. In production, use system environment variables instead.

```bash npm2yarn
cp .env .medusa/server/.env.production
```

- In the system environment variables, set `NODE_ENV` to `production`:

```bash
NODE_ENV=production
```

- Use the `start` command to run the application:

```bash npm2yarn
cd .medusa/server && npm run start
```

***

## Build Medusa Admin

By default, the Medusa Admin is built to the `.medusa/server/public/admin` directory.

If you want a separate build to host the admin standalone, such as on Vercel, pass the `--admin-only` option as explained in the [Options](#options) section. This outputs the admin to the `.medusa/admin` directory instead.


# db Commands - Medusa CLI Reference

Commands starting with `db:` perform actions on the database.

## db:setup

Creates a database for the Medusa application with the specified name, if it doesn't exit. Then, it runs migrations and syncs links.

It also updates your `.env` file with the database name.

```bash
npx medusa db:setup --db <name>
```

Use this command if you're setting up a Medusa project or database manually.

### Options

|Option|Description|Required|Default|
|---|---|---|---|---|---|---|
|\`--db \<name>\`|The database name.|Yes|-|
|\`--skip-links\`|Skip syncing links to the database.|No|Links are synced by default.|
|\`--execute-safe-links\`|Skip prompts when syncing links and execute only safe actions.|No|Prompts are shown for unsafe actions, by default.|
|\`--execute-all-links\`|Skip prompts when syncing links and execute all (including unsafe) actions.|No|Prompts are shown for unsafe actions, by default.|
|\`--no-interactive\`|Disable the command's prompts.|No|-|

***

## db:create

Creates a database for the Medusa application with the specified name, if it doesn't exit.

It also updates your `.env` file with the database name.

```bash
npx medusa db:create --db <name>
```

Use this command if you want to only create a database.

### Options

|Option|Description|Required|Default|
|---|---|---|---|---|---|---|
|\`--db \<name>\`|The database name.|Yes|-|
|\`--no-interactive\`|Disable the command's prompts.|No|-|

***

## db:generate

Generate a migration file for the latest changes in one or more modules.

```bash
npx medusa db:generate <module_names...>
```

### Arguments

|Argument|Description|Required|
|---|---|---|---|---|
|\`module\_names\`|The name of one or more module (separated by spaces) to generate migrations for. For example, |Yes|

***

## db:migrate

Run the latest migrations to reflect changes on the database, sync link definitions with the database, and run migration data scripts.

```bash
npx medusa db:migrate
```

Use this command if you've updated the Medusa packages, or you've created customizations and want to reflect them in the database.

### Options

|Option|Description|Required|Default|
|---|---|---|---|---|---|---|
|\`--skip-links\`|Skip syncing links to the database.|No|Links are synced by default.|
|\`--skip-scripts\`|Skip running data migration scripts. This option is added starting from
|No|Data migration scripts are run by default starting from
|
|\`--execute-safe-links\`|Skip prompts when syncing links and execute only safe actions.|No|Prompts are shown for unsafe actions, by default.|
|\`--execute-all-links\`|Skip prompts when syncing links and execute all (including unsafe) actions.|No|Prompts are shown for unsafe actions, by default.|

***

## db:rollback

Revert the last migrations ran on one or more modules.

```bash
npx medusa db:rollback <module_names...>
```

### Arguments

|Argument|Description|Required|
|---|---|---|---|---|
|\`module\_names\`|The name of one or more module (separated by spaces) to rollback their migrations for. For example, |Yes|

***

## db:sync-links

Sync the database with the link definitions in your application, including the definitions in Medusa's modules.

```bash
npx medusa db:sync-links
```

### Options

|Option|Description|Required|Default|
|---|---|---|---|---|---|---|
|\`--execute-safe\`|Skip prompts when syncing links and execute only safe actions.|No|Prompts are shown for unsafe actions, by default.|
|\`--execute-all\`|Skip prompts when syncing links and execute all (including unsafe) actions.|No|Prompts are shown for unsafe actions, by default.|


# exec Command - Medusa CLI Reference

Run a custom CLI script. Learn more about it in [this guide](https://docs.medusajs.com/docs/learn/fundamentals/custom-cli-scripts/index.html.md).

```bash
npx medusa exec [file] [args...]
```

## Arguments

|Argument|Description|Required|
|---|---|---|---|---|
|\`file\`|The path to the TypeScript or JavaScript file holding the function to execute.|Yes|
|\`args\`|A list of arguments to pass to the function. These arguments are passed in the |No|


# develop Command - Medusa CLI Reference

Start Medusa application in development. This command watches files for any changes, then rebuilds the files and restarts the Medusa application.

```bash
npx medusa develop
```

## Options

|Option|Description|Default|
|---|---|---|---|---|
|\`-H \<host>\`|Set host of the Medusa server.|\`localhost\`|
|\`-p \<port>\`|Set port of the Medusa server.|\`9000\`|


# new Command - Medusa CLI Reference

Create a new Medusa application. Unlike the `create-medusa-app` CLI tool, this command provides more flexibility for experienced Medusa developers in creating and configuring their project.

```bash
medusa new [<dir_name> [<starter_url>]]
```

## Arguments

|Argument|Description|Required|Default|
|---|---|---|---|---|---|---|
|\`dir\_name\`|The name of the directory to create the Medusa application in.|Yes|-|
|\`starter\_url\`|The URL of the starter repository to create the project from.|No|\`https://github.com/medusajs/medusa-starter-default\`|

## Options

|Option|Description|
|---|---|---|
|\`-y\`|Skip all prompts, such as databaes prompts. A database might not be created if default PostgreSQL credentials don't work.|
|\`--skip-db\`|Skip database creation.|
|\`--skip-env\`|Skip populating |
|\`--db-user \<user>\`|The database user to use for database setup.|
|\`--db-database \<database>\`|The name of the database used for database setup.|
|\`--db-pass \<password>\`|The database password to use for database setup.|
|\`--db-port \<port>\`|The database port to use for database setup.|
|\`--db-host \<host>\`|The database host to use for database setup.|


# plugin Commands - Medusa CLI Reference

Commands starting with `plugin:` perform actions related to [plugin](https://docs.medusajs.com/docs/learn/fundamentals/plugins/index.html.md) development.

These commands are available starting from [Medusa v2.3.0](https://github.com/medusajs/medusa/releases/tag/v2.3.0).

## plugin:publish

Publish a plugin into the local packages registry. The command uses [Yalc](https://github.com/wclr/yalc) under the hood to publish the plugin to a local package registry. You can then install the plugin in a local Medusa project using the [plugin:add](#pluginadd) command.

```bash
npx medusa plugin:publish
```

***

## plugin:add

Install the specified plugins from the local package registry into a local Medusa application. Plugins can be added to the local package registry using the [plugin:publish](#pluginpublish) command.

```bash
npx medusa plugin:add [names...]
```

### Arguments

|Argument|Description|Required|
|---|---|---|---|---|
|\`names\`|The names of one or more plugins to install from the local package registry. A plugin's name is as specified in its |Yes|

***

## plugin:develop

Start a development server for a plugin. The command will watch for changes in the plugin's source code and automatically re-publish the changes into the local package registry.

```bash
npx medusa plugin:develop
```

***

## plugin:db:generate

Generate migrations for all modules in a plugin.

```bash
npx medusa plugin:db:generate
```

***

## plugin:build

Build a plugin before publishing it to NPM. The command will compile an output in the `.medusa/server` directory.

```bash
npx medusa plugin:build
```


# start Command - Medusa CLI Reference

Start the Medusa application in production.

```bash
npx medusa start
```

## Options

|Option|Description|Default|
|---|---|---|---|---|
|\`-H \<host>\`|Set host of the Medusa server.|\`localhost\`|
|\`-p \<port>\`|Set port of the Medusa server.|\`9000\`|
|\`--cluster \<number>\`|Start Medusa's Node.js server in |Cluster mode is disabled by default. If the option is passed but no number is passed, Medusa will try to consume all available CPU cores.|


# user Command - Medusa CLI Reference

Create a new admin user.

```bash
npx medusa user --email <email> [--password <password>]
```

## Options

|Option|Description|Required|Default|
|---|---|---|---|---|---|---|
|\`-e \<email>\`|The user's email.|Yes|-|
|\`-p \<password>\`|The user's password.|No|-|
|\`-i \<id>\`|The user's ID.|No|An automatically generated ID.|
|\`--invite\`|Whether to create an invite instead of a user. When using this option, you don't need to specify a password.
If ran successfully, you'll receive the invite token in the output.|No|\`false\`|


# telemetry Command - Medusa CLI Reference

Enable or disable the collection of anonymous data usage. If no option is provided, the command enables the collection of anonymous data usage.

```bash
npx medusa telemetry
```

#### Options

|Option|Description|
|---|---|---|
|\`--enable\`|Enable telemetry (default).|
|\`--disable\`|Disable telemetry.|


# Medusa CLI Reference

The Medusa CLI tool provides commands that facilitate your development.

### Prerequisites

- [Node.js v20+](https://nodejs.org/en/download)
- [Git CLI tool](https://git-scm.com/downloads)
- [PostgreSQL](https://www.postgresql.org/download/)

## Usage

In your Medusa application's directory, you can use the Medusa CLI tool using NPX.

For example:

```bash
npx medusa --help
```

***


# build Command - Medusa CLI Reference

Create a standalone build of the Medusa application.

This creates a build that:

- Doesn't rely on the source TypeScript files.
- Can be copied to a production server reliably.

The build is outputted to a new `.medusa/server` directory.

```bash
npx medusa build
```

Refer to [this section](#run-built-medusa-application) for next steps.

## Options

|Option|Description|
|---|---|---|
|\`--admin-only\`|Whether to only build the admin to host it separately. If this option is not passed, the admin is built to the |

***

## Run Built Medusa Application

After running the `build` command, use the following step to run the built Medusa application:

- Change to the `.medusa/server` directory and install the dependencies:

```bash npm2yarn
cd .medusa/server && npm install
```

- When running the application locally, make sure to copy the `.env` file from the root project's directory. In production, use system environment variables instead.

```bash npm2yarn
cp .env .medusa/server/.env.production
```

- In the system environment variables, set `NODE_ENV` to `production`:

```bash
NODE_ENV=production
```

- Use the `start` command to run the application:

```bash npm2yarn
cd .medusa/server && npm run start
```

***

## Build Medusa Admin

By default, the Medusa Admin is built to the `.medusa/server/public/admin` directory.

If you want a separate build to host the admin standalone, such as on Vercel, pass the `--admin-only` option as explained in the [Options](#options) section. This outputs the admin to the `.medusa/admin` directory instead.


# develop Command - Medusa CLI Reference

Start Medusa application in development. This command watches files for any changes, then rebuilds the files and restarts the Medusa application.

```bash
npx medusa develop
```

## Options

|Option|Description|Default|
|---|---|---|---|---|
|\`-H \<host>\`|Set host of the Medusa server.|\`localhost\`|
|\`-p \<port>\`|Set port of the Medusa server.|\`9000\`|


# exec Command - Medusa CLI Reference

Run a custom CLI script. Learn more about it in [this guide](https://docs.medusajs.com/docs/learn/fundamentals/custom-cli-scripts/index.html.md).

```bash
npx medusa exec [file] [args...]
```

## Arguments

|Argument|Description|Required|
|---|---|---|---|---|
|\`file\`|The path to the TypeScript or JavaScript file holding the function to execute.|Yes|
|\`args\`|A list of arguments to pass to the function. These arguments are passed in the |No|


# new Command - Medusa CLI Reference

Create a new Medusa application. Unlike the `create-medusa-app` CLI tool, this command provides more flexibility for experienced Medusa developers in creating and configuring their project.

```bash
medusa new [<dir_name> [<starter_url>]]
```

## Arguments

|Argument|Description|Required|Default|
|---|---|---|---|---|---|---|
|\`dir\_name\`|The name of the directory to create the Medusa application in.|Yes|-|
|\`starter\_url\`|The URL of the starter repository to create the project from.|No|\`https://github.com/medusajs/medusa-starter-default\`|

## Options

|Option|Description|
|---|---|---|
|\`-y\`|Skip all prompts, such as databaes prompts. A database might not be created if default PostgreSQL credentials don't work.|
|\`--skip-db\`|Skip database creation.|
|\`--skip-env\`|Skip populating |
|\`--db-user \<user>\`|The database user to use for database setup.|
|\`--db-database \<database>\`|The name of the database used for database setup.|
|\`--db-pass \<password>\`|The database password to use for database setup.|
|\`--db-port \<port>\`|The database port to use for database setup.|
|\`--db-host \<host>\`|The database host to use for database setup.|


# start Command - Medusa CLI Reference

Start the Medusa application in production.

```bash
npx medusa start
```

## Options

|Option|Description|Default|
|---|---|---|---|---|
|\`-H \<host>\`|Set host of the Medusa server.|\`localhost\`|
|\`-p \<port>\`|Set port of the Medusa server.|\`9000\`|
|\`--cluster \<number>\`|Start Medusa's Node.js server in |Cluster mode is disabled by default. If the option is passed but no number is passed, Medusa will try to consume all available CPU cores.|


# telemetry Command - Medusa CLI Reference

Enable or disable the collection of anonymous data usage. If no option is provided, the command enables the collection of anonymous data usage.

```bash
npx medusa telemetry
```

#### Options

|Option|Description|
|---|---|---|
|\`--enable\`|Enable telemetry (default).|
|\`--disable\`|Disable telemetry.|


# user Command - Medusa CLI Reference

Create a new admin user.

```bash
npx medusa user --email <email> [--password <password>]
```

## Options

|Option|Description|Required|Default|
|---|---|---|---|---|---|---|
|\`-e \<email>\`|The user's email.|Yes|-|
|\`-p \<password>\`|The user's password.|No|-|
|\`-i \<id>\`|The user's ID.|No|An automatically generated ID.|
|\`--invite\`|Whether to create an invite instead of a user. When using this option, you don't need to specify a password.
If ran successfully, you'll receive the invite token in the output.|No|\`false\`|


# plugin Commands - Medusa CLI Reference

Commands starting with `plugin:` perform actions related to [plugin](https://docs.medusajs.com/docs/learn/fundamentals/plugins/index.html.md) development.

These commands are available starting from [Medusa v2.3.0](https://github.com/medusajs/medusa/releases/tag/v2.3.0).

## plugin:publish

Publish a plugin into the local packages registry. The command uses [Yalc](https://github.com/wclr/yalc) under the hood to publish the plugin to a local package registry. You can then install the plugin in a local Medusa project using the [plugin:add](#pluginadd) command.

```bash
npx medusa plugin:publish
```

***

## plugin:add

Install the specified plugins from the local package registry into a local Medusa application. Plugins can be added to the local package registry using the [plugin:publish](#pluginpublish) command.

```bash
npx medusa plugin:add [names...]
```

### Arguments

|Argument|Description|Required|
|---|---|---|---|---|
|\`names\`|The names of one or more plugins to install from the local package registry. A plugin's name is as specified in its |Yes|

***

## plugin:develop

Start a development server for a plugin. The command will watch for changes in the plugin's source code and automatically re-publish the changes into the local package registry.

```bash
npx medusa plugin:develop
```

***

## plugin:db:generate

Generate migrations for all modules in a plugin.

```bash
npx medusa plugin:db:generate
```

***

## plugin:build

Build a plugin before publishing it to NPM. The command will compile an output in the `.medusa/server` directory.

```bash
npx medusa plugin:build
```


# db Commands - Medusa CLI Reference

Commands starting with `db:` perform actions on the database.

## db:setup

Creates a database for the Medusa application with the specified name, if it doesn't exit. Then, it runs migrations and syncs links.

It also updates your `.env` file with the database name.

```bash
npx medusa db:setup --db <name>
```

Use this command if you're setting up a Medusa project or database manually.

### Options

|Option|Description|Required|Default|
|---|---|---|---|---|---|---|
|\`--db \<name>\`|The database name.|Yes|-|
|\`--skip-links\`|Skip syncing links to the database.|No|Links are synced by default.|
|\`--execute-safe-links\`|Skip prompts when syncing links and execute only safe actions.|No|Prompts are shown for unsafe actions, by default.|
|\`--execute-all-links\`|Skip prompts when syncing links and execute all (including unsafe) actions.|No|Prompts are shown for unsafe actions, by default.|
|\`--no-interactive\`|Disable the command's prompts.|No|-|

***

## db:create

Creates a database for the Medusa application with the specified name, if it doesn't exit.

It also updates your `.env` file with the database name.

```bash
npx medusa db:create --db <name>
```

Use this command if you want to only create a database.

### Options

|Option|Description|Required|Default|
|---|---|---|---|---|---|---|
|\`--db \<name>\`|The database name.|Yes|-|
|\`--no-interactive\`|Disable the command's prompts.|No|-|

***

## db:generate

Generate a migration file for the latest changes in one or more modules.

```bash
npx medusa db:generate <module_names...>
```

### Arguments

|Argument|Description|Required|
|---|---|---|---|---|
|\`module\_names\`|The name of one or more module (separated by spaces) to generate migrations for. For example, |Yes|

***

## db:migrate

Run the latest migrations to reflect changes on the database, sync link definitions with the database, and run migration data scripts.

```bash
npx medusa db:migrate
```

Use this command if you've updated the Medusa packages, or you've created customizations and want to reflect them in the database.

### Options

|Option|Description|Required|Default|
|---|---|---|---|---|---|---|
|\`--skip-links\`|Skip syncing links to the database.|No|Links are synced by default.|
|\`--skip-scripts\`|Skip running data migration scripts. This option is added starting from
|No|Data migration scripts are run by default starting from
|
|\`--execute-safe-links\`|Skip prompts when syncing links and execute only safe actions.|No|Prompts are shown for unsafe actions, by default.|
|\`--execute-all-links\`|Skip prompts when syncing links and execute all (including unsafe) actions.|No|Prompts are shown for unsafe actions, by default.|

***

## db:rollback

Revert the last migrations ran on one or more modules.

```bash
npx medusa db:rollback <module_names...>
```

### Arguments

|Argument|Description|Required|
|---|---|---|---|---|
|\`module\_names\`|The name of one or more module (separated by spaces) to rollback their migrations for. For example, |Yes|

***

## db:sync-links

Sync the database with the link definitions in your application, including the definitions in Medusa's modules.

```bash
npx medusa db:sync-links
```

### Options

|Option|Description|Required|Default|
|---|---|---|---|---|---|---|
|\`--execute-safe\`|Skip prompts when syncing links and execute only safe actions.|No|Prompts are shown for unsafe actions, by default.|
|\`--execute-all\`|Skip prompts when syncing links and execute all (including unsafe) actions.|No|Prompts are shown for unsafe actions, by default.|


# Medusa JS SDK

In this documentation, you'll learn how to install and use Medusa's JS SDK.

## What is Medusa JS SDK?

Medusa's JS SDK is a library to easily send requests to your Medusa application. You can use it in your admin customizations or custom storefronts.

***

## How to Install Medusa JS SDK?

The Medusa JS SDK is available in your Medusa application by default. So, you don't need to install it before using it in your admin customizations.

To install the Medusa JS SDK in other projects, such as a custom storefront, run the following command:

```bash npm2yarn
npm install @medusajs/js-sdk@latest @medusajs/types@latest
```

You install two libraries:

- `@medusajs/js-sdk`: the Medusa JS SDK.
- `@medusajs/types`: Medusa's types library, which is useful if you're using TypeScript in your development.

***

## Setup JS SDK

In your project, create the following `config.ts` file:

For admin customizations, create this file at `src/admin/lib/config.ts`.

### Admin (Medusa project)

```ts title="src/admin/lib/config.ts"
import Medusa from "@medusajs/js-sdk"

export const sdk = new Medusa({
  baseUrl: import.meta.env.VITE_BACKEND_URL || "/",
  debug: import.meta.env.DEV,
  auth: {
    type: "session",
  },
})
```

### Admin (Medusa Plugin)

```ts title="src/admin/lib/config.ts"
import Medusa from "@medusajs/js-sdk"

export const sdk = new Medusa({
  baseUrl: __BACKEND_URL__ || "/",
  debug: import.meta.env.DEV,
  auth: {
    type: "session",
  },
})
```

### Storefront

```ts title="config.ts"
import Medusa from "@medusajs/js-sdk"

let MEDUSA_BACKEND_URL = "http://localhost:9000"

if (process.env.NEXT_PUBLIC_MEDUSA_BACKEND_URL) {
  MEDUSA_BACKEND_URL = process.env.NEXT_PUBLIC_MEDUSA_BACKEND_URL
}

export const sdk = new Medusa({
  baseUrl: MEDUSA_BACKEND_URL,
  debug: process.env.NODE_ENV === "development",
  publishableKey: process.env.NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY,
})
```

In Medusa Admin customizations that are created in a Medusa project, you use `import.meta.env` to access environment variables, whereas in customizations built in a Medusa plugin, you use the global variable `__BACKEND_URL__` to access the backend URL. You can learn more in the [Admin Environment Variables](https://docs.medusajs.com/docs/learn/fundamentals/admin/environment-variables/index.html.md) chapter.

### JS SDK Configurations

The `Medusa` initializer accepts as a parameter an object with the following properties:

|Property|Description|Default|
|---|---|---|---|---|
|\`baseUrl\`|A required string indicating the URL to the Medusa backend.|-|
|\`publishableKey\`|A string indicating the publishable API key to use in the storefront. You can retrieve it from the Medusa Admin.|-|
|\`auth.type\`|A string that specifies the user authentication method to use.|-|
|\`auth.jwtTokenStorageKey\`|A string that, when |\`medusa\_auth\_token\`|
|\`auth.jwtTokenStorageMethod\`|A string that, when |\`local\`|
|\`auth.storage\`|This option is only available after Medusa v2.5.1. It's an object or class that's used when |-|
|\`auth.fetchCredentials\`|By default, if |\`include\`|
|\`globalHeaders\`|An object of key-value pairs indicating headers to pass in all requests, where the key indicates the name of the header field.|-|
|\`apiKey\`|A string indicating the admin user's API key. If specified, it's used to send authenticated requests.|-|
|\`debug\`|A boolean indicating whether to show debug messages of requests sent in the console. This is useful during development.|\`false\`|
|\`logger\`|Replace the logger used by the JS SDK to log messages. The logger must be a class or object having the following methods:|JavaScript's |

***

## Manage Authentication in JS SDK

The JS SDK supports different types of authentication methods and allow you to flexibly configure them.

To learn more about configuring authentication in the JS SDK and sending authenticated requests, refer to the [Authentication](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/js-sdk/auth/overview/index.html.md) guide.

***

## Send Requests to Custom Routes

The sidebar shows the different methods that you can use to send requests to Medusa's API routes.

To send requests to custom routes, the JS SDK has a `client.fetch` method that wraps the [JavaScript Fetch API](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API/Using_Fetch) that you can use. The method automatically appends configurations and headers, such as authentication headers, to your request.

For example, to send a request to a custom route at `http://localhost:9000/custom`:

### GET

```ts
sdk.client.fetch(`/custom`)
.then((data) => {
  console.log(data)
})
```

### POST

```ts
sdk.client.fetch(`/custom`, {
  method: "post",
  body: {
    id: "123",
  },
}).then((data) => {
  console.log(data)
})
```

### DELETE

```ts
sdk.client.fetch(`/custom`, {
  method: "delete",
}).then(() => {
  console.log("success")
})
```

The `fetch` method accepts as a first parameter the route's path relative to the `baseUrl` configuration you passed when you initialized the SDK.

In the second parameter, you can pass an object of [request configurations](https://developer.mozilla.org/en-US/docs/Web/API/RequestInit). You don't need to configure the content-type to be JSON, or stringify the `body` or `query` value, as that's handled by the method.

The method returns a Promise that, when resolved, has the data returned by the request. If the request returns a JSON object, it'll be automatically parsed to a JavaScript object and returned.

***

## Handle Errors

If an error occurs in a request, the JS SDK throws a `FetchError` object. This object has the following properties:

- `status`: The HTTP status code of the response.
- `statusText`: The error code. For example, `Unauthorized`.
- `message`: The error message. For example, `Invalid credentials`.

You can use these properties to handle errors in your application.

For example:

### Promise

```ts
sdk.store.customer.listAddress()
.then(({ addresses, count, offset, limit }) => {
  // no errors occurred
  // do something with the data
  console.log(addresses)
})
.catch((error) => {
  const fetchError = error as FetchError

  if (fetchError.statusText === "Unauthorized") {
    // redirect to login page
  } else {
    // handle other errors
  }
})
```

### Async/Await

```ts
try {
  const { 
    addresses, 
    count, 
    offset, 
    limit,
  } = await sdk.store.customer.listAddress()
  // no errors occurred
  // do something with the data
  console.log(addresses)
} catch (error) {
  const fetchError = error as FetchError

  if (fetchError.statusText === "Unauthorized") {
    // redirect to login page
  } else {
    // handle other errors
  }
}
```

In the example above, you handle errors in two ways:

- Since the JS SDK's methods return a Promise, you can use the `catch` method to handle errors.
- You can use the `try...catch` statement to handle errors when using `async/await`. This is useful when you're executing the methods as part of a larger function.

In the `catch` method or statement, you have access to the error object of type `FetchError`.

An example of handling the error is to check if the error's `statusText` is `Unauthorized`. If so, you can redirect the customer to the login page. Otherwise, you can handle other errors by showing an alert, for example.

***

## Pass Headers in Requests

There are two ways to pass custom headers in requests when using the JS SDK:

1. Using the `globalHeaders` configuration: This is useful when you want to pass the same headers in all requests. For example, if you want to pass a custom header for tracking purposes:

```ts
const sdk = new Medusa({
  // ...
  globalHeaders: {
    "x-tracking-id": "123456789",
  },
})
```

2. Using the headers parameter of a specific method. Every method has as a last parameter a headers parameter, which is an object of headers to pass in the request. This is useful when you want to pass a custom header in specific requests. For example, to disable HTTP compression for specific requests:

```ts
sdk.store.product.list({
  limit,
  offset,
}, {
  "x-no-compression": "false",
})
```

In the example above, you pass the `x-no-compression` header in the request to disable HTTP compression. You pass it as the last parameter of the `sdk.store.product.list` method.

The JS SDK appends request-specific headers to authentication headers and headers configured in the `globalHeaders` configuration. So, in the example above, the `x-no-compression` header is passed in the request along with the authentication headers and any headers configured in the `globalHeaders` configuration.

***

## Medusa JS SDK Tips

### Use Tanstack (React) Query in Admin Customizations

In admin customizations, use [Tanstack Query](https://tanstack.com/query/latest) with the JS SDK to send requests to custom or existing API routes.

Tanstack Query is installed by default in your Medusa application.

Do not install Tanstack Query as that will cause unexpected errors in your development. If you prefer installing it for better auto-completion in your code editor, make sure to install `v5.64.2` as a development dependency.

Use the [configured SDK](#setup-js-sdk) with the [useQuery](https://tanstack.com/query/latest/docs/framework/react/reference/useQuery#usequery) Tanstack Query hook to send `GET` requests, and [useMutation](https://tanstack.com/query/latest/docs/framework/react/reference/useMutation#usemutation) hook to send `POST` or `DELETE` requests.

For example:

### Query

```tsx title="src/admin/widgets/product-widget.ts"
import { defineWidgetConfig } from "@medusajs/admin-sdk"
import { Button, Container } from "@medusajs/ui"
import { useQuery } from "@tanstack/react-query"
import { sdk } from "../lib/config"
import { DetailWidgetProps, HttpTypes } from "@medusajs/framework/types"

const ProductWidget = () => {
  const { data, isLoading } = useQuery({
    queryFn: () => sdk.admin.product.list(),
    queryKey: ["products"],
  })
  
  return (
    <Container className="divide-y p-0">
      {isLoading && <span>Loading...</span>}
      {data?.products && (
        <ul>
          {data.products.map((product) => (
            <li key={product.id}>{product.title}</li>
          ))}
        </ul>
      )}
    </Container>
  )
}

export const config = defineWidgetConfig({
  zone: "product.list.before",
})

export default ProductWidget
```

### Mutation

```tsx title="src/admin/widgets/product-widget.ts"
import { defineWidgetConfig } from "@medusajs/admin-sdk"
import { Button, Container } from "@medusajs/ui"
import { useMutation } from "@tanstack/react-query"
import { sdk } from "../lib/config"
import { DetailWidgetProps, HttpTypes } from "@medusajs/framework/types"

const ProductWidget = ({ 
  data: productData,
}: DetailWidgetProps<HttpTypes.AdminProduct>) => {
  const { mutateAsync } = useMutation({
    mutationFn: (payload: HttpTypes.AdminUpdateProduct) => 
      sdk.admin.product.update(productData.id, payload),
    onSuccess: () => alert("updated product"),
  })

  const handleUpdate = () => {
    mutateAsync({
      title: "New Product Title",
    })
  }
  
  return (
    <Container className="divide-y p-0">
      <Button onClick={handleUpdate}>Update Title</Button>
    </Container>
  )
}

export const config = defineWidgetConfig({
  zone: "product.details.before",
})

export default ProductWidget
```

Refer to Tanstack Query's documentation to learn more about sending [Queries](https://tanstack.com/query/latest/docs/framework/react/reference/useQuery#usequery) and [Mutations](https://tanstack.com/query/latest/docs/framework/react/reference/useMutation#usemutation).

### Cache in Next.js Projects

Every method of the SDK that sends requests accepts as a last parameter an object of key-value headers to pass in the request.

In Next.js storefronts or projects, pass the `next.tags` header in the last parameter for data caching.

For example:

```ts highlights={[["2", "next"], ["3", "tags", "An array of tags to cache the data under."]]}
sdk.store.product.list({}, {
  next: {
    tags: ["products"],
  },
})
```

The `tags` property accepts an array of tags that the data is cached under.

Then, to purge the cache later, use Next.js's `revalidateTag` utility:

```ts
import { revalidateTag } from "next/cache"

// ...

revalidateTag("products")
```

Learn more in the [Next.js documentation](https://nextjs.org/docs/app/building-your-application/caching#fetch-optionsnexttags-and-revalidatetag).


# Authentication in JS SDK

In this guide, you'll learn about the default authentication setup when using the JS SDK, how to customize it, and how to send authenticated requests to Medusa's APIs.

## Default Authentication Settings in JS SDK

The JS SDK facilitates authentication by storing and managing the necessary authorization headers or sessions for you.

There are three types of authentication:

|Method|Description|When to use|
|---|---|---|
|JWT token (default)|When you log in a user, the JS SDK stores the JWT for you and automatically includes it in the headers of all requests to the Medusa API. This means you don't have to manually set the authorization header for each request. When the user logs out, the SDK clears the stored JWT.||
|Cookie session|When you log in a user, the JS SDK stores the session cookie for you and automatically includes it in the headers of all requests to the Medusa API. This means you don't have to manually set the authorization header for each request. When the user logs out, the SDK destroys the session cookie using Medusa's API.||
|Secret API Key|Only available for admin users. You pass the API key in the JS SDK configurations, and it's always passed in the headers of all requests to the Medusa API.||

***

## JS SDK Authentication Configurations

The JS SDK provides a set of configurations to customize the authentication method and storage. You can set these configurations when initializing the SDK.

For a full list of JS SDK configurations and their possible values, check out the [JS SDK Overview](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/js-sdk#js-sdk-configurations/index.html.md) documentation.

### Authentication Type

By default, the JS SDK uses JWT token (`jwt`) authentication. You can change the authentication method or type by setting the `auth.type` configuration to `session`.

For example:

```ts
import Medusa from "@medusajs/js-sdk"

export const sdk = new Medusa({
  // ...
  auth: {
    type: "session",
  },
})
```

To use a secret API key instead, pass it in the `apiKey` configuration instead:

```ts
import Medusa from "@medusajs/js-sdk"

export const sdk = new Medusa({
  // ...
  apiKey: "your-api-key",
})
```

The provided API key will be passed in the headers of all requests to the Medusa API.

### Change JWT Authentication Storage

By default, the JS SDK stores the JWT token in the `localStorage` under the `medusa_auth_token` key.

Some environments or use cases may require a different storage method or `localStorage` may not be available. For example, if you're building a mobile app with React Native, you might want to use `AsyncStorage` instead of `localStorage`.

You can change the storage method by setting the `auth.jwtTokenStorageMethod` configuration to one of the following values:

|Value|Description|
|---|---|
|\`local\`|Uses |
|\`session\`|Uses |
|\`memory\`|Uses a memory storage method. This means the token will be cleared when the user refreshes the page or closes the browser tab or window. This is also useful when using the JS SDK in a server-side environment.|
|\`custom\`|Uses a custom storage method. This means you can provide your own implementation of the storage method. For example, you can use |
|\`nostore\`|Does not store the JWT token. This means you have to manually set the authorization header for each request. This is useful when you want to use a different authentication method or when you're using the JS SDK in a server-side environment.|

#### Custom Authentication Storage in JS SDK

To use a custom storage method, you need to set the `auth.jwtTokenStorageMethod` configuration to `custom` and provide your own implementation of the storage method in the `auth.storage` configuration.

The object or class passed to `auth.storage` configuration must have the following methods:

- `setItem`: A function that accepts a key and value to store the JWT token.
- `getItem`: A function that accepts a key to retrieve the JWT token.
- `removeItem`: A function that accepts a key to remove the JWT token from storage.

For example, to use `AsyncStorage` in React Native:

```ts
import AsyncStorage from "@react-native-async-storage/async-storage"
import Medusa from "@medusajs/js-sdk"

let MEDUSA_BACKEND_URL = "http://localhost:9000"

if (process.env.NEXT_PUBLIC_MEDUSA_BACKEND_URL) {
  MEDUSA_BACKEND_URL = process.env.NEXT_PUBLIC_MEDUSA_BACKEND_URL
}

export const sdk = new Medusa({
  baseUrl: MEDUSA_BACKEND_URL,
  debug: process.env.NODE_ENV === "development",
  publishableKey: process.env.NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY,
  auth: {
    type: "jwt",
    jwtTokenStorageMethod: "custom",
    storge: AsyncStorage,
  },
})
```

In this example, you specify the `jwtTokenStorageMethod` as `custom` and set the `storage` configuration to `AsyncStorage`. This way, the JS SDK will use `AsyncStorage` to store and manage the JWT token instead of `localStorage`.

### Change Cookie Session Credentials Options

By default, if you set the `auth.type` configuration in the JS SDK to `session`, the JS SDK will pass the `credentials: include` option in the underlying [fetch requests](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API/Using_Fetch#including_credentials).

However, some platforms or environments may not support passing this option. For example, if you're using the JS SDK in a server-side environment or a mobile app, you might want to set the `credentials` option to `same-origin` or `omit`.

You can change the `credentials` option by setting the `auth.fetchCredentials` configuration to one of the following values:

|Value|Description|
|---|---|
|\`include\`|Passes the |
|\`same-origin\`|Passes the |
|\`omit\`|Passes the |

For example:

```ts
import Medusa from "@medusajs/js-sdk"

export const sdk = new Medusa({
  // ...
  auth: {
    type: "session",
    fetchCredentials: "same-origin",
  },
})
```

In this example, you set the `fetchCredentials` configuration to `same-origin`, which means the JS SDK will include cookies and authorization headers in the requests to the Medusa API only if the request is made to the same origin as the current page.

***

## Sending Authenticated Requests in JS SDK

If you're using an API key for authentication, you don't need to log in the user.

The JS SDK has an `auth.login` method that allows you to login admin users, customers, or any [actor type](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/auth/auth-identity-and-actor-types/index.html.md) with any [auth provider](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/auth/auth-providers/index.html.md).

Not only does this method log in the user, but it also stores the JWT token or session cookie for you and automatically includes it in the headers of all requests to the Medusa API. This means you don't have to manually set the authorization header for each request.

For example:

### Admin User

```ts
sdk.auth.login("user", "emailpass", {
  email,
  password,
})
.then((data) => {
  if (typeof data === "object" && data.location){
    // authentication requires more actions
  }
  // user is authenticated
})
.catch((error) => {
  // authentication failed
})
```

### Customer

```ts
sdk.auth.login("customer", "emailpass", {
  email,
  password,
})
.then((data) => {
  if (typeof data === "object" && data.location){
    // authentication requires more actions
  }
  // customer is authenticated
})
.catch((error) => {
  // authentication failed
})
```

### Custom

```ts
sdk.auth.login("manager", "emailpass", {
  email,
  password,
})
.then((data) => {
  if (typeof data === "object" && data.location){
    // authentication requires more actions
  }
  // manager is authenticated
})
.catch((error) => {
  // authentication failed
})
```

In this example, you call the `sdk.auth.login` method passing it the actor type (for example, `user`), the provider (`emailpass`), and the credentials.

If the authentication is successful, there are two types of returned data:

- An object with a `location` property: This means the authentication requires more actions, which happens when using third-party authentication providers, such as [Google](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/auth/auth-providers/google/index.html.md). In that case, you need to redirect the customer to the location to complete their authentication.
  - Refer to the [Third-Party Login in Storefront](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/storefront-development/customers/third-party-login/index.html.md) guide for an example implementation.
- A string: This means the authentication was successful, and the user is logged in. The JS SDK automatically stores the JWT token or session cookie for you and includes it in the headers of all requests to the Medusa API. All requests you send afterwards will be authenticated with the stored token or session cookie.

If the authentication fails, the `catch` block will be executed, and you can handle the error accordingly.

You can learn more about this method in the [auth.login reference](https://docs.medusajs.com/references/js-sdk/auth/login/index.html.md).

### Manually Set JWT Token

If you need to set the JWT token manually, you can use the `sdk.client.setToken` method. All subsequent requests will be authenticated with the provided token.

For example:

```ts
sdk.client.setToken("your-jwt-token")

// all requests sent after this will be authenticated with the provided token
```

You can also clear the token manually as explained in the [Manually Clearing JWT Token](#manually-clearing-jwt-token) section.

***

## Logout in JS SDK

If you're using an API key for authentication, you can't log out the user. You'll have to unset the API key in the JS SDK configurations.

The JS SDK has an `auth.logout` method that allows you to log out the currently authenticated user.

If the JS SDK's authentication type is `jwt`, the method will only clear the stored JWT token from the local storage. If the authentication type is `session`, the method will destroy the session cookie using Medusa's `/auth/session` API route.

Any request sent after logging out will not be authenticated, and you will need to log in again to authenticate the user.

For example:

```ts
sdk.auth.logout()
.then(() => {
  // user is logged out
})
```

You can learn more about this method in the [auth.logout reference](https://docs.medusajs.com/references/js-sdk/auth/logout/index.html.md).

### Manually Clearing JWT Token

If you need to clear the JWT token manually, you can use the `sdk.client.clearToken` method. This will remove the token from the local storage and all subsequent requests will not be authenticated.

For example:

```ts
sdk.client.clearToken()

// all requests sent after this will not be authenticated
```


# Medusa Examples

This documentation page has examples of customizations useful for your custom development in the Medusa application.

Each section links to the associated documentation page to learn more about it.

## API Routes

An API route is a REST API endpoint that exposes commerce features to external applications, such as storefronts, the admin dashboard, or third-party systems.

### Create API Route

Create the file `src/api/hello-world/route.ts` with the following content:

```ts title="src/api/hello-world/route.ts"
import type {
  MedusaRequest,
  MedusaResponse,
} from "@medusajs/framework/http"

export const GET = (
  req: MedusaRequest,
  res: MedusaResponse
) => {
  res.json({
    message: "[GET] Hello world!",
  })
}
```

This creates a `GET` API route at `/hello-world`.

Learn more in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/index.html.md).

### Resolve Resources in API Route

To resolve resources from the Medusa container in an API route:

```ts highlights={[["8", "resolve", "Resolve the Product Module's\nmain service from the Medusa container."]]}
import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http"
import { Modules } from "@medusajs/framework/utils"

export const GET = async (
  req: MedusaRequest, 
  res: MedusaResponse
) => {
  const productModuleService = req.scope.resolve(
    Modules.PRODUCT
  )

  const [, count] = await productModuleService
    .listAndCountProducts()

  res.json({
    count,
  })
}
```

This resolves the Product Module's main service.

Learn more in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/medusa-container/index.html.md).

### Use Path Parameters

API routes can accept path parameters.

To do that, create the file `src/api/hello-world/[id]/route.ts` with the following content:

```ts title="src/api/hello-world/[id]/route.ts" highlights={singlePathHighlights}
import type {
  MedusaRequest,
  MedusaResponse,
} from "@medusajs/framework/http"

export const GET = async (
  req: MedusaRequest,
  res: MedusaResponse
) => {
  res.json({
    message: `[GET] Hello ${req.params.id}!`,
  })
}
```

Learn more about path parameters in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/parameters#path-parameters/index.html.md).

### Use Query Parameters

API routes can accept query parameters:

```ts highlights={queryHighlights}
import type {
  MedusaRequest,
  MedusaResponse,
} from "@medusajs/framework/http"

export const GET = async (
  req: MedusaRequest,
  res: MedusaResponse
) => {
  res.json({
    message: `Hello ${req.query.name}`,
  })
}
```

Learn more about query parameters in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/parameters#query-parameters/index.html.md).

### Use Body Parameters

API routes can accept request body parameters:

```ts highlights={bodyHighlights}
import type {
  MedusaRequest,
  MedusaResponse,
} from "@medusajs/framework/http"

type HelloWorldReq = {
  name: string
}

export const POST = async (
  req: MedusaRequest<HelloWorldReq>,
  res: MedusaResponse
) => {
  res.json({
    message: `[POST] Hello ${req.body.name}!`,
  })
}
```

Learn more about request body parameters in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/parameters#request-body-parameters/index.html.md).

### Set Response Code

You can change the response code of an API route:

```ts highlights={[["7", "status", "Change the response's status."]]}
import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http"

export const GET = async (
  req: MedusaRequest,
  res: MedusaResponse
) => {
  res.status(201).json({
    message: "Hello, World!",
  })
}
```

Learn more about setting the response code in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/responses#set-response-status-code/index.html.md).

### Execute a Workflow in an API Route

To execute a workflow in an API route:

```ts
import type {
  MedusaRequest,
  MedusaResponse,
} from "@medusajs/framework/http"
import myWorkflow from "../../workflows/hello-world"

export async function GET(
  req: MedusaRequest,
  res: MedusaResponse
) {
  const { result } = await myWorkflow(req.scope)
    .run({
      input: {
        name: req.query.name as string,
      },
    })

  res.send(result)
}
```

Learn more in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/workflows#3-execute-the-workflow/index.html.md).

### Change Response Content Type

By default, an API route's response has the content type `application/json`.

To change it to another content type, use the `writeHead` method of `MedusaResponse`:

```ts highlights={responseContentTypeHighlights}
import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http"

export const GET = async (
  req: MedusaRequest,
  res: MedusaResponse
) => {
  res.writeHead(200, {
    "Content-Type": "text/event-stream",
    "Cache-Control": "no-cache",
    Connection: "keep-alive",
  })

  const interval = setInterval(() => {
    res.write("Streaming data...\n")
  }, 3000)

  req.on("end", () => {
    clearInterval(interval)
    res.end()
  })
}
```

This changes the response type to return an event stream.

Learn more in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/responses#change-response-content-type/index.html.md).

### Create Middleware

A middleware is a function executed when a request is sent to an API Route.

Create the file `src/api/middlewares.ts` with the following content:

```ts title="src/api/middlewares.ts"
import type { 
  MedusaNextFunction, 
  MedusaRequest, 
  MedusaResponse, 
  defineMiddlewares,
} from "@medusajs/framework/http"

export default defineMiddlewares({
  routes: [
    {
      matcher: "/custom*",
      middlewares: [
        (
          req: MedusaRequest, 
          res: MedusaResponse, 
          next: MedusaNextFunction
        ) => {
          console.log("Received a request!")

          next()
        },
      ],
    },
    {
      matcher: "/custom/:id",
      middlewares: [
        (
          req: MedusaRequest, 
          res: MedusaResponse, 
          next: MedusaNextFunction
        ) => {
          console.log("With Path Parameter")

          next()
        },
      ],
    },
  ],
})
```

Learn more about middlewares in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/middlewares/index.html.md).

### Restrict HTTP Methods in Middleware

To restrict a middleware to an HTTP method:

```ts title="src/api/middlewares.ts" highlights={middlewareMethodHighlights}
import type { 
  MedusaNextFunction, 
  MedusaRequest, 
  MedusaResponse, 
  defineMiddlewares,
} from "@medusajs/framework/http"

export default defineMiddlewares({
  routes: [
    {
      matcher: "/custom*",
      method: ["POST", "PUT"],
      middlewares: [
        // ...
      ],
    },
  ],
})
```

### Add Validation for Custom Routes

1. Create a [Zod](https://zod.dev/) schema in the file `src/api/custom/validators.ts`:

```ts title="src/api/custom/validators.ts"
import { z } from "zod"

export const PostStoreCustomSchema = z.object({
  a: z.number(),
  b: z.number(),
})
```

2. Add a validation middleware to the custom route in `src/api/middlewares.ts`:

```ts title="src/api/middlewares.ts" highlights={[["13", "validateAndTransformBody"]]}
import { 
  validateAndTransformBody,
  defineMiddlewares,
} from "@medusajs/framework/http"
import { PostStoreCustomSchema } from "./custom/validators"

export default defineMiddlewares({
  routes: [
    {
      matcher: "/custom",
      method: "POST",
      middlewares: [
        validateAndTransformBody(PostStoreCustomSchema),
      ],
    },
  ],
})
```

3. Use the validated body in the `/custom` API route:

```ts title="src/api/custom/route.ts" highlights={[["14", "validatedBody"]]}
import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http"
import { z } from "zod"
import { PostStoreCustomSchema } from "./validators"

type PostStoreCustomSchemaType = z.infer<
  typeof PostStoreCustomSchema
>

export const POST = async (
  req: MedusaRequest<PostStoreCustomSchemaType>,
  res: MedusaResponse
) => {
  res.json({
    sum: req.validatedBody.a + req.validatedBody.b,
  })
}
```

Learn more about request body validation in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/validation/index.html.md).

### Pass Additional Data to API Route

In this example, you'll pass additional data to the Create Product API route, then consume its hook:

Find this example in details in [this documentation](https://docs.medusajs.com/docs/learn/customization/extend-features/extend-create-product/index.html.md).

1. Create the file `src/api/middlewares.ts` with the following content:

```ts title="src/api/middlewares.ts" highlights={[["10", "brand_id", "Replace with your custom field."]]}
import { defineMiddlewares } from "@medusajs/framework/http"
import { z } from "zod"

export default defineMiddlewares({
  routes: [
    {
      matcher: "/admin/products",
      method: ["POST"],
      additionalDataValidator: {
        brand_id: z.string().optional(),
      },
    },
  ],
})
```

Learn more about additional data in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/additional-data/index.html.md).

2. Create the file `src/workflows/hooks/created-product.ts` with the following content:

```ts
import { createProductsWorkflow } from "@medusajs/medusa/core-flows"
import { StepResponse } from "@medusajs/framework/workflows-sdk"

createProductsWorkflow.hooks.productsCreated(
  (async ({ products, additional_data }, { container }) => {
    if (!additional_data.brand_id) {
      return new StepResponse([], [])
    }

    // TODO perform custom action
  }),
  (async (links, { container }) => {
    // TODO undo the action in the compensation
  })

)
```

Learn more about workflow hooks in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/workflows/workflow-hooks/index.html.md).

### Restrict an API Route to Admin Users

You can protect API routes by restricting access to authenticated admin users only.

Add the following middleware in `src/api/middlewares.ts`:

```ts title="src/api/middlewares.ts" highlights={[["11", "authenticate"]]}
import { 
  defineMiddlewares,
  authenticate,
} from "@medusajs/framework/http"

export default defineMiddlewares({
  routes: [
    {
      matcher: "/custom/admin*",
      middlewares: [
        authenticate(
          "user", 
          ["session", "bearer", "api-key"]
        ),
      ],
    },
  ],
})
```

Learn more in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/protected-routes/index.html.md).

### Restrict an API Route to Logged-In Customers

You can protect API routes by restricting access to authenticated customers only.

Add the following middleware in `src/api/middlewares.ts`:

```ts title="src/api/middlewares.ts" highlights={[["11", "authenticate"]]}
import { 
  defineMiddlewares,
  authenticate,
} from "@medusajs/framework/http"

export default defineMiddlewares({
  routes: [
    {
      matcher: "/custom/customer*",
      middlewares: [
        authenticate("customer", ["session", "bearer"]),
      ],
    },
  ],
})
```

Learn more in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/protected-routes/index.html.md).

### Retrieve Logged-In Admin User

To retrieve the currently logged-in user in an API route:

Requires setting up the authentication middleware as explained in [this example](#restrict-an-api-route-to-admin-users).

```ts highlights={[["16", "req.auth_context.actor_id", "Access the user's ID."]]}
import type {
  AuthenticatedMedusaRequest,
  MedusaResponse,
} from "@medusajs/framework/http"
import { Modules } from "@medusajs/framework/utils"

export const GET = async (
  req: AuthenticatedMedusaRequest,
  res: MedusaResponse
) => {
  const userModuleService = req.scope.resolve(
    Modules.USER
  )

  const user = await userModuleService.retrieveUser(
    req.auth_context.actor_id
  )

  // ...
}
```

Learn more in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/protected-routes#retrieve-logged-in-admin-users-details/index.html.md).

### Retrieve Logged-In Customer

To retrieve the currently logged-in customer in an API route:

Requires setting up the authentication middleware as explained in [this example](#restrict-an-api-route-to-logged-in-customers).

```ts highlights={[["18", "req.auth_context.actor_id", "Access the customer's ID."]]}
import type {
  AuthenticatedMedusaRequest,
  MedusaResponse,
} from "@medusajs/framework/http"
import { Modules } from "@medusajs/framework/utils"

export const GET = async (
  req: AuthenticatedMedusaRequest,
  res: MedusaResponse
) => {
  if (req.auth_context?.actor_id) {
    // retrieve customer
    const customerModuleService = req.scope.resolve(
      Modules.CUSTOMER
    )

    const customer = await customerModuleService.retrieveCustomer(
      req.auth_context.actor_id
    )
  }

  // ...
}
```

Learn more in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/protected-routes#retrieve-logged-in-customers-details/index.html.md).

### Throw Errors in API Route

To throw errors in an API route, use `MedusaError` from the Medusa Framework:

```ts highlights={[["9", "MedusaError"]]}
import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http"
import { MedusaError } from "@medusajs/framework/utils"

export const GET = async (
  req: MedusaRequest,
  res: MedusaResponse
) => {
  if (!req.query.q) {
    throw new MedusaError(
      MedusaError.Types.INVALID_DATA,
      "The `q` query parameter is required."
    )
  }

  // ...
}
```

Learn more in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/errors/index.html.md).

### Override Error Handler of API Routes

To override the error handler of API routes, create the file `src/api/middlewares.ts` with the following content:

```ts title="src/api/middlewares.ts" highlights={[["10", "errorHandler"]]}
import { 
  defineMiddlewares, 
  MedusaNextFunction, 
  MedusaRequest, 
  MedusaResponse,
} from "@medusajs/framework/http"
import { MedusaError } from "@medusajs/framework/utils"

export default defineMiddlewares({
  errorHandler: (
    error: MedusaError | any, 
    req: MedusaRequest, 
    res: MedusaResponse, 
    next: MedusaNextFunction
  ) => {
    res.status(400).json({
      error: "Something happened.",
    })
  },
})
```

Learn more in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/errors#override-error-handler/index.html.md),

### Setting up CORS for Custom API Routes

By default, Medusa configures CORS for all routes starting with `/admin`, `/store`, and `/auth`.

To configure CORS for routes under other prefixes, create the file `src/api/middlewares.ts` with the following content:

```ts title="src/api/middlewares.ts"
import type { 
  MedusaNextFunction, 
  MedusaRequest, 
  MedusaResponse, 
  defineMiddlewares,
} from "@medusajs/framework/http"
import { ConfigModule } from "@medusajs/framework/types"
import { parseCorsOrigins } from "@medusajs/framework/utils"
import cors from "cors"

export default defineMiddlewares({
  routes: [
    {
      matcher: "/custom*",
      middlewares: [
        (
          req: MedusaRequest, 
          res: MedusaResponse, 
          next: MedusaNextFunction
        ) => {
          const configModule: ConfigModule =
            req.scope.resolve("configModule")

          return cors({
            origin: parseCorsOrigins(
              configModule.projectConfig.http.storeCors
            ),
            credentials: true,
          })(req, res, next)
        },
      ],
    },
  ],
})
```

### Parse Webhook Body

By default, the Medusa application parses a request's body using JSON.

To parse a webhook's body, create the file `src/api/middlewares.ts` with the following content:

```ts title="src/api/middlewares.ts" highlights={[["9"]]}
import { 
  defineMiddlewares, 
} from "@medusajs/framework/http"

export default defineMiddlewares({
  routes: [
    {
      matcher: "/webhooks/*",
      bodyParser: { preserveRawBody: true },
      method: ["POST"],
    },
  ],
})
```

To access the raw body data in your route, use the `req.rawBody` property:

```ts title="src/api/webhooks/route.ts"
import type {
  MedusaRequest,
  MedusaResponse,
} from "@medusajs/framework/http"

export const POST = (
  req: MedusaRequest,
  res: MedusaResponse
) => {
  console.log(req.rawBody)
}
```

***

## Modules

A module is a package of reusable commerce or architectural functionalities. They handle business logic in a class called a service, and define and manage data models that represent tables in the database.

### Create Module

Find this example explained in details in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/modules/index.html.md).

1. Create the directory `src/modules/blog`.
2. Create the file `src/modules/blog/models/post.ts` with the following data model:

```ts title="src/modules/blog/models/post.ts"
import { model } from "@medusajs/framework/utils"

const Post = model.define("post", {
  id: model.id().primaryKey(),
  title: model.text(),
})

export default Post
```

3. Create the file `src/modules/blog/service.ts` with the following service:

```ts title="src/modules/blog/service.ts"
import { MedusaService } from "@medusajs/framework/utils"
import Post from "./models/post"

class BlogModuleService extends MedusaService({
  Post,
}){
}

export default BlogModuleService
```

4. Create the file `src/modules/blog/index.ts` that exports the module definition:

```ts title="src/modules/blog/index.ts"
import BlogModuleService from "./service"
import { Module } from "@medusajs/framework/utils"

export const BLOG_MODULE = "blog"

export default Module(BLOG_MODULE, {
  service: BlogModuleService,
})
```

5. Add the module to the configurations in `medusa-config.ts`:

```ts title="medusa-config.ts"
module.exports = defineConfig({
  projectConfig: {
    // ...
  },
  modules: [
    {
      resolve: "./modules/blog",
    },
  ],
})
```

6. Generate and run migrations:

```bash
npx medusa db:generate blog
npx medusa db:migrate
```

7. Use the module's main service in an API route:

```ts title="src/api/custom/route.ts"
import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http"
import BlogModuleService from "../../modules/blog/service"
import { BLOG_MODULE } from "../../modules/blog"

export async function GET(
  req: MedusaRequest,
  res: MedusaResponse
): Promise<void> {
  const blogModuleService: BlogModuleService = req.scope.resolve(
    BLOG_MODULE
  )

  const post = await blogModuleService.createPosts({
    title: "test",
  })

  res.json({
    post,
  })
}
```

### Module with Multiple Services

To add services in your module other than the main one, create them in the `services` directory of the module.

For example, create the file `src/modules/blog/services/category.ts` with the following content:

```ts title="src/modules/blog/services/category.ts"
export class CategoryService {
  // TODO add methods
}
```

Then, export the service in the file `src/modules/blog/services/index.ts`:

```ts title="src/modules/blog/services/index.ts"
export * from "./category"
```

Finally, resolve the service in your module's main service or loader:

```ts title="src/modules/blog/service.ts"
import { MedusaService } from "@medusajs/framework/utils"
import Post from "./models/post"
import { CategoryService } from "./services"

type InjectedDependencies = {
  categoryService: CategoryService
}

class BlogModuleService extends MedusaService({
  Post,
}){
  private categoryService: CategoryService

  constructor({ categoryService }: InjectedDependencies) {
    super(...arguments)

    this.categoryService = categoryService
  }
}

export default BlogModuleService
```

Learn more in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/modules/multiple-services/index.html.md).

### Accept Module Options

A module can accept options for configurations and secrets.

To accept options in your module:

1. Pass options to the module in `medusa-config.ts`:

```ts title="medusa-config.ts" highlights={[["6", "options"]]}
module.exports = defineConfig({
  // ...
  modules: [
    {
      resolve: "./modules/blog",
      options: {
        apiKey: true,
      },
    },
  ],
})
```

2. Access the options in the module's main service:

```ts title="src/modules/blog/service.ts" highlights={[["14", "options"]]}
import { MedusaService } from "@medusajs/framework/utils"
import Post from "./models/post"

// recommended to define type in another file
type ModuleOptions = {
  apiKey?: boolean
}

export default class BlogModuleService extends MedusaService({
  Post,
}){
  protected options_: ModuleOptions

  constructor({}, options?: ModuleOptions) {
    super(...arguments)

    this.options_ = options || {
      apiKey: false,
    }
  }

  // ...
}
```

Learn more in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/modules/options/index.html.md).

### Integrate Third-Party System in Module

An example of integrating a dummy third-party system in a module's service:

```ts title="src/modules/blog/service.ts"
import { Logger } from "@medusajs/framework/types"
import { BLOG_MODULE } from ".."

export type ModuleOptions = {
  apiKey: string
}

type InjectedDependencies = {
  logger: Logger
}

export class BlogClient {
  private options_: ModuleOptions
  private logger_: Logger

  constructor(
    { logger }: InjectedDependencies, 
    options: ModuleOptions
  ) {
    this.logger_ = logger
    this.options_ = options
  }

  private async sendRequest(url: string, method: string, data?: any) {
    this.logger_.info(`Sending a ${
      method
    } request to ${url}. data: ${JSON.stringify(data, null, 2)}`)
    this.logger_.info(`Client Options: ${
      JSON.stringify(this.options_, null, 2)
    }`)
  }
}
```

Find a longer example of integrating a third-party service in [this documentation](https://docs.medusajs.com/docs/learn/customization/integrate-systems/service/index.html.md).

***

## Data Models

A data model represents a table in the database. Medusa provides a data model language to intuitively create data models.

### Create Data Model

To create a data model in a module:

This assumes you already have a module. If not, follow [this example](#create-module).

1. Create the file `src/modules/blog/models/post.ts` with the following data model:

```ts title="src/modules/blog/models/post.ts"
import { model } from "@medusajs/framework/utils"

const Post = model.define("post", {
  id: model.id().primaryKey(),
  title: model.text(),
})

export default Post
```

2. Generate and run migrations:

```bash
npx medusa db:generate blog
npx medusa db:migrate
```

Learn more in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/modules#1-create-data-model/index.html.md).

### Data Model Property Types

A data model can have properties of the following types:

1. ID property:

```ts
const Post = model.define("post", {
  id: model.id(),
  // ...
})
```

2. Text property:

```ts
const Post = model.define("post", {
  title: model.text(),
  // ...
})
```

3. Number property:

```ts
const Post = model.define("post", {
  views: model.number(),
  // ...
})
```

4. Big Number property:

```ts
const Post = model.define("post", {
  price: model.bigNumber(),
  // ...
})
```

5. Boolean property:

```ts
const Post = model.define("post", {
  isPublished: model.boolean(),
  // ...
})
```

6. Enum property:

```ts
const Post = model.define("post", {
  status: model.enum(["draft", "published"]),
  // ...
})
```

7. Date-Time property:

```ts
const Post = model.define("post", {
  publishedAt: model.dateTime(),
  // ...
})
```

8. JSON property:

```ts
const Post = model.define("post", {
  metadata: model.json(),
  // ...
})
```

9. Array property:

```ts
const Post = model.define("post", {
  tags: model.array(),
  // ...
})
```

Learn more in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/data-models/properties/index.html.md).

### Set Primary Key

To set an `id` property as the primary key of a data model:

```ts highlights={[["4", "primaryKey"]]}
import { model } from "@medusajs/framework/utils"

const Post = model.define("post", {
  id: model.id().primaryKey(),
  // ...
})

export default Post
```

To set a `text` property as the primary key:

```ts highlights={[["4", "primaryKey"]]}
import { model } from "@medusajs/framework/utils"

const Post = model.define("post", {
  title: model.text().primaryKey(),
  // ...
})

export default Post
```

To set a `number` property as the primary key:

```ts highlights={[["4", "primaryKey"]]}
import { model } from "@medusajs/framework/utils"

const Post = model.define("post", {
  views: model.number().primaryKey(),
  // ...
})

export default Post
```

Learn more in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/data-models/properties#set-primary-key-property/index.html.md).

### Default Property Value

To set the default value of a property:

```ts highlights={[["6"], ["9"]]}
import { model } from "@medusajs/framework/utils"

const Post = model.define("post", {
  status: model
    .enum(["draft", "published"])
    .default("draft"),
  views: model
    .number()
    .default(0),
  // ...
})

export default Post
```

Learn more in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/data-models/properties#property-default-value/index.html.md).

### Nullable Property

To allow `null` values for a property:

```ts highlights={[["4", "nullable"]]}
import { model } from "@medusajs/framework/utils"

const Post = model.define("post", {
  price: model.bigNumber().nullable(),
  // ...
})

export default Post
```

Learn more in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/data-models/properties#make-property-optional/index.html.md).

### Unique Property

To create a unique index on a property:

```ts highlights={[["4", "unique"]]}
import { model } from "@medusajs/framework/utils"

const Post = model.define("post", {
  title: model.text().unique(),
  // ...
})

export default Post
```

Learn more in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/data-models/properties#unique-property/index.html.md).

### Define Database Index on Property

To define a database index on a property:

```ts highlights={[["5", "index"]]}
import { model } from "@medusajs/framework/utils"

const MyCustom = model.define("my_custom", {
  id: model.id().primaryKey(),
  title: model.text().index(
    "IDX_POST_TITLE"
  ),
})

export default MyCustom
```

Learn more in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/data-models/properties#define-database-index-on-property/index.html.md).

### Define Composite Index on Data Model

To define a composite index on a data model:

```ts highlights={[["7", "indexes"]]}
import { model } from "@medusajs/framework/utils"

const MyCustom = model.define("my_custom", {
  id: model.id().primaryKey(),
  name: model.text(),
  age: model.number().nullable(),
}).indexes([
  {
    on: ["name", "age"],
    where: {
      age: {
        $ne: null,
      },
    },
  },
])

export default MyCustom
```

Learn more in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/data-models/index/index.html.md).

### Make a Property Searchable

To make a property searchable using terms or keywords:

```ts highlights={[["4", "searchable"]]}
import { model } from "@medusajs/framework/utils"

const Post = model.define("post", {
  title: model.text().searchable(),
  // ...
})

export default Post
```

Then, to search by that property, pass the `q` filter to the `list` or `listAndCount` generated methods of the module's main service:

`blogModuleService` is the main service that manages the `Post` data model.

```ts
const posts = await blogModuleService.listPosts({
  q: "John",
})
```

Learn more in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/data-models/properties#searchable-property/index.html.md).

### Create One-to-One Relationship

The following creates a one-to-one relationship between the `User` and `Email` data models:

```ts highlights={[["5", "hasOne"], ["10", "belongsTo"]]}
import { model } from "@medusajs/framework/utils"

const User = model.define("user", {
  id: model.id().primaryKey(),
  email: model.hasOne(() => Email),
})

const Email = model.define("email", {
  id: model.id().primaryKey(),
  user: model.belongsTo(() => User, {
    mappedBy: "email",
  }),
})
```

Learn more in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/data-models/relationships#one-to-one-relationship/index.html.md).

### Create One-to-Many Relationship

The following creates a one-to-many relationship between the `Store` and `Product` data models:

```ts highlights={[["5", "hasMany"], ["10", "belongsTo"]]}
import { model } from "@medusajs/framework/utils"

const Store = model.define("store", {
  id: model.id().primaryKey(),
  products: model.hasMany(() => Product),
})

const Product = model.define("product", {
  id: model.id().primaryKey(),
  store: model.belongsTo(() => Store, {
    mappedBy: "products",
  }),
})
```

Learn more in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/data-models/relationships#one-to-many-relationship/index.html.md).

### Create Many-to-Many Relationship

The following creates a many-to-many relationship between the `Order` and `Product` data models:

```ts highlights={[["5", "manyToMany"], ["12", "manyToMany"]]}
import { model } from "@medusajs/framework/utils"

const Order = model.define("order", {
  id: model.id().primaryKey(),
  products: model.manyToMany(() => Product, {
    mappedBy: "orders",
  }),
})

const Product = model.define("product", {
  id: model.id().primaryKey(),
  orders: model.manyToMany(() => Order, {
    mappedBy: "products",
  }),
})
```

Learn more in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/data-models/relationships#many-to-many-relationship/index.html.md).

### Configure Cascades of Data Model

To configure cascade on a data model:

```ts highlights={[["7", "cascades"]]}
import { model } from "@medusajs/framework/utils"
// Product import

const Store = model.define("store", {
  id: model.id().primaryKey(),
  products: model.hasMany(() => Product),
})
.cascades({
  delete: ["products"],
})
```

This configures the delete cascade on the `Store` data model so that, when a store is delete, its products are also deleted.

Learn more in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/data-models/relationships#cascades/index.html.md).

### Manage One-to-One Relationship

Consider you have a one-to-one relationship between `Email` and `User` data models, where an email belongs to a user.

To set the ID of the user that an email belongs to:

`blogModuleService` is the main service that manages the `Email` and `User` data models.

```ts
// when creating an email
const email = await blogModuleService.createEmails({
  // other properties...
  user: "123",
})

// when updating an email
const email = await blogModuleService.updateEmails({
  id: "321",
  // other properties...
  user: "123",
})
```

And to set the ID of a user's email when creating or updating it:

```ts
// when creating a user
const user = await blogModuleService.createUsers({
  // other properties...
  email: "123",
})

// when updating a user
const user = await blogModuleService.updateUsers({
  id: "321",
  // other properties...
  email: "123",
})
```

Learn more in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/data-models/manage-relationships#manage-one-to-one-relationship/index.html.md).

### Manage One-to-Many Relationship

Consider you have a one-to-many relationship between `Product` and `Store` data models, where a store has many products.

To set the ID of the store that a product belongs to:

`blogModuleService` is the main service that manages the `Product` and `Store` data models.

```ts
// when creating a product
const product = await blogModuleService.createProducts({
  // other properties...
  store_id: "123",
})

// when updating a product
const product = await blogModuleService.updateProducts({
  id: "321",
  // other properties...
  store_id: "123",
})
```

Learn more in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/data-models/manage-relationships#manage-one-to-many-relationship/index.html.md)

### Manage Many-to-Many Relationship

Consider you have a many-to-many relationship between `Order` and `Product` data models.

To set the orders a product has when creating it:

`blogModuleService` is the main service that manages the `Product` and `Order` data models.

```ts
const product = await blogModuleService.createProducts({
  // other properties...
  orders: ["123", "321"],
})
```

To add new orders to a product without removing the previous associations:

```ts
const product = await blogModuleService.retrieveProduct(
  "123",
  {
    relations: ["orders"],
  }
)

const updatedProduct = await blogModuleService.updateProducts({
  id: product.id,
  // other properties...
  orders: [
    ...product.orders.map((order) => order.id),
    "321",
  ],
})
```

Learn more in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/data-models/manage-relationships#manage-many-to-many-relationship/index.html.md).

### Retrieve Related Records

To retrieve records related to a data model's records through a relation, pass the `relations` field to the `list`, `listAndCount`, or `retrieve` generated methods:

`blogModuleService` is the main service that manages the `Product` and `Order` data models.

```ts highlights={[["4", "relations"]]}
const product = await blogModuleService.retrieveProducts(
  "123",
  {
    relations: ["orders"],
  }
)
```

Learn more in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/data-models/manage-relationships#retrieve-records-of-relation/index.html.md).

***

## Services

A service is the main resource in a module. It manages the records of your custom data models in the database, or integrate third-party systems.

### Extend Service Factory

The service factory `MedusaService` generates data-management methods for your data models.

To extend the service factory in your module's service:

```ts highlights={[["4", "MedusaService"]]}
import { MedusaService } from "@medusajs/framework/utils"
import Post from "./models/post"

class BlogModuleService extends MedusaService({
  Post,
}){
  // TODO implement custom methods
}

export default BlogModuleService
```

The `BlogModuleService` will now have data-management methods for `Post`.

Refer to [this reference](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/service-factory-reference/index.html.md) for details on the generated methods.

Learn more about the service factory in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/modules/service-factory/index.html.md).

### Resolve Resources in the Service

To resolve resources from the module's container in a service:

### With Service Factory

```ts highlights={[["14"]]}
import { Logger } from "@medusajs/framework/types"
import { MedusaService } from "@medusajs/framework/utils"
import Post from "./models/post"

type InjectedDependencies = {
  logger: Logger
}

class BlogModuleService extends MedusaService({
  Post,
}){
  protected logger_: Logger

  constructor({ logger }: InjectedDependencies) {
    super(...arguments)
    this.logger_ = logger

    this.logger_.info("[BlogModuleService]: Hello World!")
  }

  // ...
}

export default BlogModuleService
```

### Without Service Factory

```ts highlights={[["10"]]}
import { Logger } from "@medusajs/framework/types"

type InjectedDependencies = {
  logger: Logger
}

export default class BlogModuleService {
  protected logger_: Logger

  constructor({ logger }: InjectedDependencies) {
    this.logger_ = logger

    this.logger_.info("[BlogModuleService]: Hello World!")
  }

  // ...
}
```

Learn more in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/modules/container/index.html.md).

### Access Module Options in Service

To access options passed to a module in its service:

```ts highlights={[["14", "options"]]}
import { MedusaService } from "@medusajs/framework/utils"
import Post from "./models/post"

// recommended to define type in another file
type ModuleOptions = {
  apiKey?: boolean
}

export default class BlogModuleService extends MedusaService({
  Post,
}){
  protected options_: ModuleOptions

  constructor({}, options?: ModuleOptions) {
    super(...arguments)

    this.options_ = options || {
      apiKey: "",
    }
  }

  // ...
}
```

Learn more in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/modules/options/index.html.md).

### Run Database Query in Service

To run database query in your service:

```ts highlights={[["14", "count"], ["21", "execute"]]}
// other imports...
import { 
  InjectManager,
  MedusaContext,
} from "@medusajs/framework/utils"

class BlogModuleService {
  // ...

  @InjectManager()
  async getCount(
    @MedusaContext() sharedContext?: Context<EntityManager>
  ): Promise<number> {
    return await sharedContext.manager.count("post")
  }
  
  @InjectManager()
  async getCountSql(
    @MedusaContext() sharedContext?: Context<EntityManager>
  ): Promise<number> {
    const data = await sharedContext.manager.execute(
      "SELECT COUNT(*) as num FROM post"
    ) 
    
    return parseInt(data[0].num)
  }
}
```

Learn more in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/modules/db-operations#run-queries/index.html.md)

### Execute Database Operations in Transactions

To execute database operations within a transaction in your service:

```ts
import { 
  InjectManager,
  InjectTransactionManager,
  MedusaContext,
} from "@medusajs/framework/utils"
import { Context } from "@medusajs/framework/types"
import { EntityManager } from "@mikro-orm/knex"

class BlogModuleService {
  // ...
  @InjectTransactionManager()
  protected async update_(
    input: {
      id: string,
      name: string
    },
    @MedusaContext() sharedContext?: Context<EntityManager>
  ): Promise<any> {
    const transactionManager = sharedContext.transactionManager
    await transactionManager.nativeUpdate(
      "post",
      {
        id: input.id,
      },
      {
        name: input.name,
      }
    )

    // retrieve again
    const updatedRecord = await transactionManager.execute(
      `SELECT * FROM post WHERE id = '${input.id}'`
    )

    return updatedRecord
  }

  @InjectManager()
  async update(
    input: {
      id: string,
      name: string
    },
    @MedusaContext() sharedContext?: Context<EntityManager>
  ) {
    return await this.update_(input, sharedContext)
  }
}
```

Learn more in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/modules/db-operations#execute-operations-in-transactions/index.html.md).

***

## Module Links

A module link forms an association between two data models of different modules, while maintaining module isolation.

### Define a Link

To define a link between your custom module and a Commerce Module, such as the Product Module:

1. Create the file `src/links/blog-product.ts` with the following content:

```ts title="src/links/blog-product.ts"
import BlogModule from "../modules/blog"
import ProductModule from "@medusajs/medusa/product"
import { defineLink } from "@medusajs/framework/utils"

export default defineLink(
  ProductModule.linkable.product,
  BlogModule.linkable.post
)
```

2. Run the following command to sync the links:

```bash
npx medusa db:migrate
```

Learn more in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/module-links/index.html.md).

### Define a List Link

To define a list link, where multiple records of a model can be linked to a record in another:

```ts highlights={[["9", "isList"]]}
import BlogModule from "../modules/blog"
import ProductModule from "@medusajs/medusa/product"
import { defineLink } from "@medusajs/framework/utils"

export default defineLink(
  ProductModule.linkable.product,
  {
    linkable: BlogModule.linkable.post,
    isList: true,
  }
)
```

Learn more about list links in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/module-links#define-a-list-link/index.html.md).

### Set Delete Cascade on Link Definition

To ensure a model's records linked to another model are deleted when the linked model is deleted:

```ts highlights={[["9", "deleteCascades"]]}
import BlogModule from "../modules/blog"
import ProductModule from "@medusajs/medusa/product"
import { defineLink } from "@medusajs/framework/utils"

export default defineLink(
  ProductModule.linkable.product,
  {
    linkable: BlogModule.linkable.post,
    deleteCascades: true,
  }
)
```

Learn more in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/module-links#define-a-list-link/index.html.md).

### Add Custom Columns to Module Link

To add a custom column to the table that stores the linked records of two data models:

```ts highlights={[["9", "database"]]}
import BlogModule from "../modules/blog"
import ProductModule from "@medusajs/medusa/product"
import { defineLink } from "@medusajs/framework/utils"

export default defineLink(
  ProductModule.linkable.product,
  BlogModule.linkable.post,
  {
    database: {
      extraColumns: {
        metadata: {
          type: "json",
        },
      },
    },
  }
)
```

Then, to set the custom column when creating or updating a link between records:

```ts
await link.create({
  [Modules.PRODUCT]: {
    product_id: "123",
  },
  HELLO_MODULE: {
    my_custom_id: "321",
  },
  data: {
    metadata: {
      test: true,
    },
  },
})
```

To retrieve the custom column when retrieving linked records using Query:

```ts
import productBlogLink from "../links/product-blog"

// ...

const { data } = await query.graph({
  entity: productBlogLink.entryPoint,
  fields: ["metadata", "product.*", "post.*"],
  filters: {
    product_id: "prod_123",
  },
})
```

Learn more in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/module-links/custom-columns/index.html.md).

### Create Link Between Records

To create a link between two records using Link:

```ts
import { Modules } from "@medusajs/framework/utils"
import { BLOG_MODULE } from "../../modules/blog"

// ...

await link.create({
  [Modules.PRODUCT]: {
    product_id: "prod_123",
  },
  [HELLO_MODULE]: {
    my_custom_id: "mc_123",
  },
})
```

Learn more in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link#create-link/index.html.md).

### Dismiss Link Between Records

To dismiss links between records using Link:

```ts
import { Modules } from "@medusajs/framework/utils"
import { BLOG_MODULE } from "../../modules/blog"

// ...

await link.dismiss({
  [Modules.PRODUCT]: {
    product_id: "prod_123",
  },
  [BLOG_MODULE]: {
    post_id: "mc_123",
  },
})
```

Learn more in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link#dismiss-link/index.html.md).

### Cascade Delete Linked Records

To cascade delete records linked to a deleted record:

```ts
import { Modules } from "@medusajs/framework/utils"

// ...

await productModuleService.deleteVariants([variant.id])

await link.delete({
  [Modules.PRODUCT]: {
    product_id: "prod_123",
  },
})
```

Learn more in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link#cascade-delete-linked-records/index.html.md).

### Restore Linked Records

To restore records that were soft-deleted because they were linked to a soft-deleted record:

```ts
import { Modules } from "@medusajs/framework/utils"

// ...

await productModuleService.restoreProducts(["prod_123"])

await link.restore({
  [Modules.PRODUCT]: {
    product_id: "prod_123",
  },
})
```

Learn more in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link#restore-linked-records/index.html.md).

***

## Query

Query fetches data across modules. It’s a set of methods registered in the Medusa container under the `query` key.

### Retrieve Records of Data Model

To retrieve records using Query in an API route:

```ts highlights={[["15", "graph"]]}
import {
  MedusaRequest,
  MedusaResponse,
} from "@medusajs/framework/http"
import {
  ContainerRegistrationKeys,
} from "@medusajs/framework/utils"

export const GET = async (
  req: MedusaRequest,
  res: MedusaResponse
) => {
  const query = req.scope.resolve(ContainerRegistrationKeys.QUERY)

  const { data: myCustoms } = await query.graph({
    entity: "my_custom",
    fields: ["id", "name"],
  })

  res.json({ my_customs: myCustoms })
}
```

Learn more in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md).

### Retrieve Linked Records of Data Model

To retrieve records linked to a data model:

```ts highlights={[["20"]]}
import {
  MedusaRequest,
  MedusaResponse,
} from "@medusajs/framework/http"
import {
  ContainerRegistrationKeys,
} from "@medusajs/framework/utils"

export const GET = async (
  req: MedusaRequest,
  res: MedusaResponse
) => {
  const query = req.scope.resolve(ContainerRegistrationKeys.QUERY)

  const { data: myCustoms } = await query.graph({
    entity: "my_custom",
    fields: [
      "id", 
      "name",
      "product.*",
    ],
  })

  res.json({ my_customs: myCustoms })
}
```

Learn more in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query#retrieve-linked-records/index.html.md).

### Apply Filters to Retrieved Records

To filter the retrieved records:

```ts highlights={[["18", "filters"]]}
import {
  MedusaRequest,
  MedusaResponse,
} from "@medusajs/framework/http"
import {
  ContainerRegistrationKeys,
} from "@medusajs/framework/utils"

export const GET = async (
  req: MedusaRequest,
  res: MedusaResponse
) => {
  const query = req.scope.resolve(ContainerRegistrationKeys.QUERY)

  const { data: myCustoms } = await query.graph({
    entity: "my_custom",
    fields: ["id", "name"],
    filters: {
      id: [
        "mc_01HWSVWR4D2XVPQ06DQ8X9K7AX",
        "mc_01HWSVWK3KYHKQEE6QGS2JC3FX",
      ],
    },
  })

  res.json({ my_customs: myCustoms })
}
```

Learn more in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query#apply-filters/index.html.md).

### Apply Pagination and Sort Records

To paginate and sort retrieved records:

```ts highlights={[["21", "pagination"]]}
import {
  MedusaRequest,
  MedusaResponse,
} from "@medusajs/framework/http"
import {
  ContainerRegistrationKeys,
} from "@medusajs/framework/utils"

export const GET = async (
  req: MedusaRequest,
  res: MedusaResponse
) => {
  const query = req.scope.resolve(ContainerRegistrationKeys.QUERY)

  const { 
    data: myCustoms,
    metadata: { count, take, skip } = {},
  } = await query.graph({
    entity: "my_custom",
    fields: ["id", "name"],
    pagination: {
      skip: 0,
      take: 10,
      order: {
        name: "DESC",
      },
    },
  })

  res.json({ 
    my_customs: myCustoms,
    count,
    take,
    skip,
  })
}
```

Learn more in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query#sort-records/index.html.md).

***

## Workflows

A workflow is a series of queries and actions that complete a task.

A workflow allows you to track its execution's progress, provide roll-back logic for each step to mitigate data inconsistency when errors occur, automatically retry failing steps, and more.

### Create a Workflow

To create a workflow:

1. Create the first step at `src/workflows/hello-world/steps/step-1.ts` with the following content:

```ts title="src/workflows/hello-world/steps/step-1.ts"
import { createStep, StepResponse } from "@medusajs/framework/workflows-sdk"

export const step1 = createStep("step-1", async () => {
  return new StepResponse(`Hello from step one!`)
})
```

2. Create the second step at `src/workflows/hello-world/steps/step-2.ts` with the following content:

```ts title="src/workflows/hello-world/steps/step-2.ts"
import { createStep, StepResponse } from "@medusajs/framework/workflows-sdk"

type StepInput = {
  name: string
}

export const step2 = createStep(
  "step-2", 
  async ({ name }: StepInput) => {
    return new StepResponse(`Hello ${name} from step two!`)
  }
)
```

3. Create the workflow at `src/workflows/hello-world/index.ts` with the following content:

```ts title="src/workflows/hello-world/index.ts"
import {
  createWorkflow,
  WorkflowResponse,
} from "@medusajs/framework/workflows-sdk"
import { step1 } from "./steps/step-1"
import { step2 } from "./steps/step-2"

const myWorkflow = createWorkflow(
  "hello-world",
  function (input: WorkflowInput) {
    const str1 = step1()
    // to pass input
    const str2 = step2(input)

    return new WorkflowResponse({
      message: str1,
    })
  }
)

export default myWorkflow
```

Learn more in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md).

### Execute a Workflow

### API Route

```ts title="src/api/workflow/route.ts" highlights={[["11"], ["12"], ["13"], ["14"], ["15"], ["16"]]} collapsibleLines="1-6" expandButtonLabel="Show Imports"
import type {
  MedusaRequest,
  MedusaResponse,
} from "@medusajs/framework/http"
import myWorkflow from "../../workflows/hello-world"

export async function GET(
  req: MedusaRequest,
  res: MedusaResponse
) {
  const { result } = await myWorkflow(req.scope)
    .run({
      input: {
        name: req.query.name as string,
      },
    })

  res.send(result)
}
```

### Subscriber

```ts title="src/subscribers/customer-created.ts" highlights={[["20"], ["21"], ["22"], ["23"], ["24"], ["25"]]} collapsibleLines="1-9" expandButtonLabel="Show Imports"
import {
  type SubscriberConfig,
  type SubscriberArgs,
} from "@medusajs/framework"
import myWorkflow from "../workflows/hello-world"
import { Modules } from "@medusajs/framework/utils"
import { IUserModuleService } from "@medusajs/framework/types"

export default async function handleCustomerCreate({
  event: { data },
  container,
}: SubscriberArgs<{ id: string }>) {
  const userId = data.id
  const userModuleService: IUserModuleService = container.resolve(
    Modules.USER
  )

  const user = await userModuleService.retrieveUser(userId)

  const { result } = await myWorkflow(container)
    .run({
      input: {
        name: user.first_name,
      },
    })

  console.log(result)
}

export const config: SubscriberConfig = {
  event: "user.created",
}
```

### Scheduled Job

```ts title="src/jobs/message-daily.ts" highlights={[["7"], ["8"], ["9"], ["10"], ["11"], ["12"]]}
import { MedusaContainer } from "@medusajs/framework/types"
import myWorkflow from "../workflows/hello-world"

export default async function myCustomJob(
  container: MedusaContainer
) {
  const { result } = await myWorkflow(container)
    .run({
      input: {
        name: "John",
      },
    })

  console.log(result.message)
}

export const config = {
  name: "run-once-a-day",
  schedule: `0 0 * * *`,
};
```

Learn more in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/workflows#3-execute-the-workflow/index.html.md).

### Step with a Compensation Function

Pass a compensation function that undoes what a step did as a second parameter to `createStep`:

```ts highlights={[["15"]]}
import { 
  createStep,
  StepResponse,
} from "@medusajs/framework/workflows-sdk"

const step1 = createStep(
  "step-1",
  async () => {
    const message = `Hello from step one!`

    console.log(message)

    return new StepResponse(message)
  },
  async () => {
    console.log("Oops! Rolling back my changes...")
  }
)
```

Learn more in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/workflows/compensation-function/index.html.md).

### Manipulate Variables in Workflow

To manipulate variables within a workflow's constructor function, use `transform` from the Workflows SDK:

```ts highlights={[["14", "transform"]]}
import { 
  createWorkflow,
  WorkflowResponse,
  transform,
} from "@medusajs/framework/workflows-sdk"
// step imports...

const myWorkflow = createWorkflow(
  "hello-world", 
  function (input) {
    const str1 = step1(input)
    const str2 = step2(input)

    const str3 = transform(
      { str1, str2 },
      (data) => `${data.str1}${data.str2}`
    )

    return new WorkflowResponse(str3)
  }
)
```

Learn more in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/workflows/variable-manipulation/index.html.md)

### Using Conditions in Workflow

To perform steps or set a variable's value based on a condition, use `when-then` from the Workflows SDK:

```ts highlights={[["14", "when"]]}
import { 
  createWorkflow,
  WorkflowResponse,
  when,
} from "@medusajs/framework/workflows-sdk"
// step imports...

const workflow = createWorkflow(
  "workflow", 
  function (input: {
    is_active: boolean
  }) {

    const result = when(
      input, 
      (input) => {
        return input.is_active
      }
    ).then(() => {
      return isActiveStep()
    })

    // executed without condition
    const anotherStepResult = anotherStep(result)

    return new WorkflowResponse(
      anotherStepResult
    )
  }
)
```

### Run Workflow in Another

To run a workflow in another, use the workflow's `runAsStep` special method:

```ts highlights={[["11", "runAsStep"]]}
import {
  createWorkflow,
} from "@medusajs/framework/workflows-sdk"
import { 
  createProductsWorkflow,
} from "@medusajs/medusa/core-flows"

const workflow = createWorkflow(
  "hello-world",
  async (input) => {
    const products = createProductsWorkflow.runAsStep({
      input: {
        products: [
          // ...
        ],
      },
    })

    // ...
  }
)
```

Learn more in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/workflows/execute-another-workflow/index.html.md).

### Consume a Workflow Hook

To consume a workflow hook, create a file under `src/workflows/hooks`:

```ts title="src/workflows/hooks/product-created.ts"
import { createProductsWorkflow } from "@medusajs/medusa/core-flows"

createProductsWorkflow.hooks.productsCreated(
  async ({ products, additional_data }, { container }) => {
    // TODO perform an action
  },
  async (dataFromStep, { container }) => {
    // undo the performed action
  }
)
```

This executes a custom step at the hook's designated point in the workflow.

Learn more in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/workflows/workflow-hooks/index.html.md).

### Expose a Hook

To expose a hook in a workflow, pass it in the second parameter of the returned `WorkflowResponse`:

```ts highlights={[["19", "hooks"]]}
import {
  createStep,
  createHook,
  createWorkflow,
  WorkflowResponse,
} from "@medusajs/framework/workflows-sdk"
import { createProductStep } from "./steps/create-product"

export const myWorkflow = createWorkflow(
  "my-workflow", 
  function (input) {
    const product = createProductStep(input)
    const productCreatedHook = createHook(
      "productCreated", 
      { productId: product.id }
    )

    return new WorkflowResponse(product, {
      hooks: [productCreatedHook],
    })
  }
)
```

Learn more in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/workflows/add-workflow-hook/index.html.md).

### Retry Steps

To configure steps to retry in case of errors, pass the `maxRetries` step option:

```ts highlights={[["10"]]}
import { 
  createStep, 
} from "@medusajs/framework/workflows-sdk"

export const step1 = createStep(
  {
    name: "step-1",
    maxRetries: 2,
  },
  async () => {
    console.log("Executing step 1")

    throw new Error("Oops! Something happened.")
  }
)
```

Learn more in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/workflows/retry-failed-steps/index.html.md).

### Run Steps in Parallel

If steps in a workflow don't depend on one another, run them in parallel using `parallel` from the Workflows SDK:

```ts highlights={[["22", "parallelize"]]}
import {
  createWorkflow,
  WorkflowResponse,
  parallelize,
} from "@medusajs/framework/workflows-sdk"
import {
  createProductStep,
  getProductStep,
  createPricesStep,
  attachProductToSalesChannelStep,
} from "./steps"

interface WorkflowInput {
  title: string
}

const myWorkflow = createWorkflow(
  "my-workflow", 
  (input: WorkflowInput) => {
   const product = createProductStep(input)

   const [prices, productSalesChannel] = parallelize(
     createPricesStep(product),
     attachProductToSalesChannelStep(product)
   )

   const id = product.id
   const refetchedProduct = getProductStep(product.id)

   return new WorkflowResponse(refetchedProduct)
 }
)
```

Learn more in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/workflows/parallel-steps/index.html.md).

### Configure Workflow Timeout

To configure the timeout of a workflow, at which the workflow's status is changed, but its execution isn't stopped, use the `timeout` configuration:

```ts highlights={[["10"]]}
import { 
  createStep,  
  createWorkflow,
  WorkflowResponse,
} from "@medusajs/framework/workflows-sdk"
// step import...

const myWorkflow = createWorkflow({
  name: "hello-world",
  timeout: 2, // 2 seconds
}, function () {
  const str1 = step1()

  return new WorkflowResponse({
    message: str1,
  })
})

export default myWorkflow
```

Learn more in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/workflows/workflow-timeout/index.html.md).

### Configure Step Timeout

To configure a step's timeout, at which its state changes but its execution isn't stopped, use the `timeout` property:

```ts highlights={[["4"]]}
const step1 = createStep(
  {
    name: "step-1",
    timeout: 2, // 2 seconds
  },
  async () => {
    // ...
  }
)
```

Learn more in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/workflows/workflow-timeout#configure-step-timeout/index.html.md).

### Long-Running Workflow

A long-running workflow is a workflow that runs in the background. You can wait before executing some of its steps until another external or separate action occurs.

To create a long-running workflow, configure any of its steps to be `async` without returning any data:

```ts highlights={[["4"]]}
const step2 = createStep(
  {
    name: "step-2",
    async: true,
  },
  async () => {
    console.log("Waiting to be successful...")
  }
)
```

Learn more in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/workflows/long-running-workflow/index.html.md).

### Change Step Status in Long-Running Workflow

To change a step's status:

1. Grab the workflow's transaction ID when you run it:

```ts
const { transaction } = await myLongRunningWorkflow(req.scope)
  .run()
```

2. In an API route, workflow, or other resource, change a step's status to successful using the [Worfklow Engine Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/infrastructure-modules/workflow-engine/index.html.md):

```ts highlights={stepSuccessHighlights}
const workflowEngineService = container.resolve(
  Modules.WORKFLOW_ENGINE
)

await workflowEngineService.setStepSuccess({
  idempotencyKey: {
    action: TransactionHandlerType.INVOKE,
    transactionId,
    stepId: "step-2",
    workflowId: "hello-world",
  },
  stepResponse: new StepResponse("Done!"),
  options: {
    container,
  },
})
```

3. In an API route, workflow, or other resource, change a step's status to failure using the [Worfklow Engine Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/infrastructure-modules/workflow-engine/index.html.md):

```ts highlights={stepFailureHighlights}
const workflowEngineService = container.resolve(
  Modules.WORKFLOW_ENGINE
)

await workflowEngineService.setStepFailure({
  idempotencyKey: {
    action: TransactionHandlerType.INVOKE,
    transactionId,
    stepId: "step-2",
    workflowId: "hello-world",
  },
  stepResponse: new StepResponse("Failed!"),
  options: {
    container,
  },
})
```

Learn more in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/workflows/long-running-workflow/index.html.md).

### Access Long-Running Workflow's Result

Use the Workflow Engine Module's `subscribe` and `unsubscribe` methods to access the status of a long-running workflow.

For example, in an API route:

```ts highlights={[["18", "subscribe", "Subscribe to the workflow's status changes."]]}
import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http"
import myWorkflow from "../../../workflows/hello-world"
import { Modules } from "@medusajs/framework/utils"

export async function GET(req: MedusaRequest, res: MedusaResponse) {
  const { transaction, result } = await myWorkflow(req.scope).run()

  const workflowEngineService = req.scope.resolve(
    Modules.WORKFLOW_ENGINE
  )

  const subscriptionOptions = {
    workflowId: "hello-world",
    transactionId: transaction.transactionId,
    subscriberId: "hello-world-subscriber",
  }

  await workflowEngineService.subscribe({
    ...subscriptionOptions,
    subscriber: async (data) => {
      if (data.eventType === "onFinish") {
        console.log("Finished execution", data.result)
        // unsubscribe
        await workflowEngineService.unsubscribe({
          ...subscriptionOptions,
          subscriberOrId: subscriptionOptions.subscriberId,
        })
      } else if (data.eventType === "onStepFailure") {
        console.log("Workflow failed", data.step)
      }
    },
  })

  res.send(result)
}
```

Learn more in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/workflows/long-running-workflow#access-long-running-workflow-status-and-result/index.html.md).

***

## Subscribers

A subscriber is a function executed whenever the event it listens to is emitted.

### Create a Subscriber

To create a subscriber that listens to the `product.created` event, create the file `src/subscribers/product-created.ts` with the following content:

```ts title="src/subscribers/product-created.ts"
import type {
  SubscriberArgs,
  SubscriberConfig,
} from "@medusajs/framework"

export default async function productCreateHandler({
  event,
}: SubscriberArgs<{ id: string }>) {
  const productId = event.data.id
  console.log(`The product ${productId} was created`)
}

export const config: SubscriberConfig = {
  event: "product.created",
}
```

Learn more in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/events-and-subscribers/index.html.md).

### Resolve Resources in Subscriber

To resolve resources from the Medusa container in a subscriber, use the `container` property of its parameter:

```ts highlights={[["6", "container"], ["8", "resolve", "Resolve the Product Module's main service."]]}
import { SubscriberArgs, SubscriberConfig } from "@medusajs/framework"
import { Modules } from "@medusajs/framework/utils"

export default async function productCreateHandler({
  event: { data },
  container,
}: SubscriberArgs<{ id: string }>) {
  const productModuleService = container.resolve(Modules.PRODUCT)

  const productId = data.id

  const product = await productModuleService.retrieveProduct(
    productId
  )

  console.log(`The product ${product.title} was created`)
}

export const config: SubscriberConfig = {
  event: `product.created`,
}
```

Learn more in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/events-and-subscribers#resolve-resources/index.html.md).

### Send a Notification to Reset Password

To send a notification, such as an email when a user requests to reset their password, create a subscriber at `src/subscribers/handle-reset.ts` with the following content:

```ts title="src/subscribers/handle-reset.ts"
import {
  SubscriberArgs,
  type SubscriberConfig,
} from "@medusajs/medusa"
import { Modules } from "@medusajs/framework/utils"

export default async function resetPasswordTokenHandler({
  event: { data: {
    entity_id: email,
    token,
    actor_type,
  } },
  container,
}: SubscriberArgs<{ entity_id: string, token: string, actor_type: string }>) {
  const notificationModuleService = container.resolve(
    Modules.NOTIFICATION
  )

  const urlPrefix = actor_type === "customer" ? 
    "https://storefront.com" : 
    "https://admin.com"

  await notificationModuleService.createNotifications({
    to: email,
    channel: "email",
    template: "reset-password-template",
    data: {
      // a URL to a frontend application
      url: `${urlPrefix}/reset-password?token=${token}&email=${email}`,
    },
  })
}

export const config: SubscriberConfig = {
  event: "auth.password_reset",
}
```

Learn more in [this documentation](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/auth/reset-password/index.html.md).

### Execute a Workflow in a Subscriber

To execute a workflow in a subscriber:

```ts
import {
  type SubscriberConfig,
  type SubscriberArgs,
} from "@medusajs/framework"
import myWorkflow from "../workflows/hello-world"
import { Modules } from "@medusajs/framework/utils"

export default async function handleCustomerCreate({
  event: { data },
  container,
}: SubscriberArgs<{ id: string }>) {
  const userId = data.id
  const userModuleService = container.resolve(
    Modules.USER
  )

  const user = await userModuleService.retrieveUser(userId)

  const { result } = await myWorkflow(container)
    .run({
      input: {
        name: user.first_name,
      },
    })

  console.log(result)
}

export const config: SubscriberConfig = {
  event: "user.created",
}
```

Learn more in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/workflows#3-execute-the-workflow/index.html.md)

***

## Scheduled Jobs

A scheduled job is a function executed at a specified interval of time in the background of your Medusa application.

### Create a Scheduled Job

To create a scheduled job, create the file `src/jobs/hello-world.ts` with the following content:

```ts title="src/jobs/hello-world.ts"
// the scheduled-job function
export default function () {
  console.log("Time to say hello world!")
}

// the job's configurations
export const config = {
  name: "every-minute-message",
  // execute every minute
  schedule: "* * * * *",
}
```

Learn more in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/scheduled-jobs/index.html.md).

### Resolve Resources in Scheduled Job

To resolve resources in a scheduled job, use the `container` accepted as a first parameter:

```ts highlights={[["5", "container"], ["7", "resolve", "Resolve the Product Module's main service."]]}
import { MedusaContainer } from "@medusajs/framework/types"
import { Modules } from "@medusajs/framework/utils"

export default async function myCustomJob(
  container: MedusaContainer
) {
  const productModuleService = container.resolve(Modules.PRODUCT)

  const [, count] = await productModuleService.listAndCountProducts()

  console.log(
    `Time to check products! You have ${count} product(s)`
  )
}

export const config = {
  name: "every-minute-message",
  // execute every minute
  schedule: "* * * * *",
}
```

Learn more in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/scheduled-jobs#resolve-resources/index.html.md)

### Specify a Job's Execution Number

To limit the scheduled job's execution to a number of times during the Medusa application's runtime, use the `numberOfExecutions` configuration:

```ts highlights={[["9", "numberOfExecutions"]]}
export default async function myCustomJob() {
  console.log("I'll be executed three times only.")
}

export const config = {
  name: "hello-world",
  // execute every minute
  schedule: "* * * * *",
  numberOfExecutions: 3,
}
```

Learn more in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/scheduled-jobs/execution-number/index.html.md).

### Execute a Workflow in a Scheduled Job

To execute a workflow in a scheduled job:

```ts
import { MedusaContainer } from "@medusajs/framework/types"
import myWorkflow from "../workflows/hello-world"

export default async function myCustomJob(
  container: MedusaContainer
) {
  const { result } = await myWorkflow(container)
    .run({
      input: {
        name: "John",
      },
    })

  console.log(result.message)
}

export const config = {
  name: "run-once-a-day",
  schedule: `0 0 * * *`,
}
```

Learn more in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/workflows#3-execute-the-workflow/index.html.md)

***

## Loaders

A loader is a function defined in a module that's executed when the Medusa application starts.

### Create a Loader

To create a loader, add it to a module's `loaders` directory.

For example, create the file `src/modules/hello/loaders/hello-world.ts` with the following content:

```ts title="src/modules/hello/loaders/hello-world.ts"
export default async function helloWorldLoader() {
  console.log(
    "[HELLO MODULE] Just started the Medusa application!"
  )
}
```

Learn more in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/modules/loaders/index.html.md).

### Resolve Resources in Loader

To resolve resources in a loader, use the `container` property of its first parameter:

```ts highlights={[["9", "container"], ["11", "resolve", "Resolve the Logger from the module's container."]]}
import {
  LoaderOptions,
} from "@medusajs/framework/types"
import { 
  ContainerRegistrationKeys,
} from "@medusajs/framework/utils"

export default async function helloWorldLoader({
  container,
}: LoaderOptions) {
  const logger = container.resolve(ContainerRegistrationKeys.LOGGER)

  logger.info("[helloWorldLoader]: Hello, World!")
}
```

Learn more in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/modules/container/index.html.md).

### Access Module Options

To access a module's options in its loader, use the `options` property of its first parameter:

```ts highlights={[["11", "options"]]}
import {
  LoaderOptions,
} from "@medusajs/framework/types"

// recommended to define type in another file
type ModuleOptions = {
  apiKey?: boolean
}

export default async function helloWorldLoader({
  options,
}: LoaderOptions<ModuleOptions>) {
  
  console.log(
    "[HELLO MODULE] Just started the Medusa application!",
    options
  )
}
```

Learn more in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/modules/options/index.html.md).

### Register Resources in the Module's Container

To register a resource in the Module's container using a loader, use the `container`'s `registerAdd` method:

```ts highlights={[["9", "registerAdd"]]}
import {
  LoaderOptions,
} from "@medusajs/framework/types"
import { asValue } from "awilix"

export default async function helloWorldLoader({
  container,
}: LoaderOptions) {
  container.registerAdd(
    "custom_data",
    asValue({
      test: true,
    })
  )
}
```

Where the first parameter of `registerAdd` is the name to register the resource under, and the second parameter is the resource to register.

***

## Admin Customizations

You can customize the Medusa Admin to inject widgets in existing pages, or create new pages using UI routes.

For a list of components to use in the admin dashboard, refere to [this documentation](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/admin-components/index.html.md).

### Create Widget

A widget is a React component that can be injected into an existing page in the admin dashboard.

To create a widget in the admin dashboard, create the file `src/admin/widgets/products-widget.tsx` with the following content:

```tsx title="src/admin/widgets/products-widget.tsx"
import { defineWidgetConfig } from "@medusajs/admin-sdk"
import { Container, Heading } from "@medusajs/ui"

const ProductWidget = () => {
  return (
    <Container className="divide-y p-0">
      <div className="flex items-center justify-between px-6 py-4">
        <Heading level="h2">Product Widget</Heading>
      </div>
    </Container>
  )
}

export const config = defineWidgetConfig({
  zone: "product.list.before",
})

export default ProductWidget
```

Learn more about widgets in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/admin/widgets/index.html.md).

### Receive Details Props in Widgets

Widgets created in a details page, such as widgets in the `product.details.before` injection zone, receive a prop of the data of the details page (for example, the product):

```tsx highlights={[["10", "data"]]}
import { defineWidgetConfig } from "@medusajs/admin-sdk"
import { Container, Heading } from "@medusajs/ui"
import { 
  DetailWidgetProps, 
  AdminProduct,
} from "@medusajs/framework/types"

// The widget
const ProductWidget = ({ 
  data,
}: DetailWidgetProps<AdminProduct>) => {
  return (
    <Container className="divide-y p-0">
      <div className="flex items-center justify-between px-6 py-4">
        <Heading level="h2">
          Product Widget {data.title}
        </Heading>
      </div>
    </Container>
  )
}

// The widget's configurations
export const config = defineWidgetConfig({
  zone: "product.details.before",
})

export default ProductWidget
```

Learn more in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/admin/widgets#detail-widget-props/index.html.md).

### Create a UI Route

A UI route is a React Component that adds a new page to your admin dashboard. The UI Route can be shown in the sidebar or added as a nested page.

To create a UI route in the admin dashboard, create the file `src/admin/routes/custom/page.tsx` with the following content:

```tsx title="src/admin/routes/custom/page.tsx"
import { defineRouteConfig } from "@medusajs/admin-sdk"
import { ChatBubbleLeftRight } from "@medusajs/icons"
import { Container, Heading } from "@medusajs/ui"

const CustomPage = () => {
  return (
    <Container className="divide-y p-0">
      <div className="flex items-center justify-between px-6 py-4">
        <Heading level="h2">This is my custom route</Heading>
      </div>
    </Container>
  )
}

export const config = defineRouteConfig({
  label: "Custom Route",
  icon: ChatBubbleLeftRight,
})

export default CustomPage
```

This adds a new page at `localhost:9000/app/custom`.

Learn more in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/admin/ui-routes/index.html.md).

### Create Settings Page

To create a settings page, create a UI route under the `src/admin/routes/settings` directory.

For example, create the file `src/admin/routes/settings/custom/page.tsx` with the following content:

```tsx title="src/admin/routes/settings/custom/page.tsx"
import { defineRouteConfig } from "@medusajs/admin-sdk"
import { Container, Heading } from "@medusajs/ui"

const CustomSettingPage = () => {
  return (
    <Container className="divide-y p-0">
      <div className="flex items-center justify-between px-6 py-4">
        <Heading level="h1">Custom Setting Page</Heading>
      </div>
    </Container>
  )
}

export const config = defineRouteConfig({
  label: "Custom",
})

export default CustomSettingPage
```

This adds a setting page at `localhost:9000/app/settings/custom`.

Learn more in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/admin/ui-routes#create-settings-page/index.html.md)

### Accept Path Parameters in UI Routes

To accept a path parameter in a UI route, name one of the directories in its path in the format `[param]`.

For example, create the file `src/admin/routes/custom/[id]/page.tsx` with the following content:

```tsx title="src/admin/routes/custom/[id]/page.tsx"
import { useParams } from "react-router-dom"
import { Container } from "@medusajs/ui"

const CustomPage = () => {
  const { id } = useParams()

  return (
    <Container className="divide-y p-0">
      <div className="flex items-center justify-between px-6 py-4">
        <Heading level="h1">Passed ID: {id}</Heading>
      </div>
    </Container>
  )
}

export default CustomPage
```

This creates a UI route at `localhost:9000/app/custom/:id`, where `:id` is a path parameter.

Learn more in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/admin/ui-routes#path-parameters/index.html.md)

### Send Request to API Route

To send a request to custom API routes from the admin dashboard, use the Fetch API.

For example:

```tsx
import { defineWidgetConfig } from "@medusajs/admin-sdk"
import { Container } from "@medusajs/ui"
import { useEffect, useState } from "react"

const ProductWidget = () => {
  const [productsCount, setProductsCount] = useState(0)
  const [loading, setLoading] = useState(true)

  useEffect(() => {
    if (!loading) {
      return
    }

    fetch(`/admin/products`, {
      credentials: "include",
    })
    .then((res) => res.json())
    .then(({ count }) => {
      setProductsCount(count)
      setLoading(false)
    })
  }, [loading])

  return (
    <Container className="divide-y p-0">
      {loading && <span>Loading...</span>}
      {!loading && <span>You have {productsCount} Product(s).</span>}
    </Container>
  )
}

export const config = defineWidgetConfig({
  zone: "product.list.before",
})

export default ProductWidget
```

Learn more in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/admin/tips#send-requests-to-api-routes/index.html.md)

### Add Link to Another Page

To add a link to another page in a UI route or a widget, use `react-router-dom`'s `Link` component:

```tsx
import { defineWidgetConfig } from "@medusajs/admin-sdk"
import { Container } from "@medusajs/ui"
import { Link } from "react-router-dom"

// The widget
const ProductWidget = () => {
  return (
    <Container className="divide-y p-0">
      <Link to={"/orders"}>View Orders</Link>
    </Container>
  )
}

// The widget's configurations
export const config = defineWidgetConfig({
  zone: "product.details.before",
})

export default ProductWidget
```

Learn more in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/admin/tips#routing-functionalities/index.html.md).

***

## Integration Tests

Medusa provides a `@medusajs/test-utils` package with utility tools to create integration tests for your custom API routes, modules, or other Medusa customizations.

For details on setting up your project for integration tests, refer to [this documentation](https://docs.medusajs.com/docs/learn/debugging-and-testing/testing-tools/index.html.md).

### Test Custom API Route

To create a test for a custom API route, create the file `integration-tests/http/custom-routes.spec.ts` with the following content:

```ts title="integration-tests/http/custom-routes.spec.ts"
import { medusaIntegrationTestRunner } from "@medusajs/test-utils"

medusaIntegrationTestRunner({
  testSuite: ({ api, getContainer }) => {
    describe("Custom endpoints", () => {
      describe("GET /custom", () => {
        it("returns correct message", async () => {
          const response = await api.get(
            `/custom`
          )
  
          expect(response.status).toEqual(200)
          expect(response.data).toHaveProperty("message")
          expect(response.data.message).toEqual("Hello, World!")
        })
      })
    })
  },
})
```

Then, run the test with the following command:

```bash npm2yarn
npm run test:integration
```

Learn more in [this documentation](https://docs.medusajs.com/docs/learn/debugging-and-testing/testing-tools/integration-tests/api-routes/index.html.md).

### Test Workflow

To create a test for a workflow, create the file `integration-tests/http/workflow.spec.ts` with the following content:

```ts title="integration-tests/http/workflow.spec.ts"
import { medusaIntegrationTestRunner } from "@medusajs/test-utils"
import { helloWorldWorkflow } from "../../src/workflows/hello-world"

medusaIntegrationTestRunner({
  testSuite: ({ getContainer }) => {
    describe("Test hello-world workflow", () => {
      it("returns message", async () => {
        const { result } = await helloWorldWorkflow(getContainer())
          .run()

        expect(result).toEqual("Hello, World!")
      })
    })
  },
})
```

Then, run the test with the following command:

```bash npm2yarn
npm run test:integration
```

Learn more in [this documentation](https://docs.medusajs.com/docs/learn/debugging-and-testing/testing-tools/integration-tests/workflows/index.html.md).

### Test Module's Service

To create a test for a module's service, create the test under the `__tests__` directory of the module.

For example, create the file `src/modules/blog/__tests__/service.spec.ts` with the following content:

```ts title="src/modules/blog/__tests__/service.spec.ts"
import { moduleIntegrationTestRunner } from "@medusajs/test-utils"
import { BLOG_MODULE } from ".."
import BlogModuleService from "../service"
import Post from "../models/post"

moduleIntegrationTestRunner<BlogModuleService>({
  moduleName: BLOG_MODULE,
  moduleModels: [Post],
  resolve: "./modules/blog",
  testSuite: ({ service }) => {
    describe("BlogModuleService", () => {
      it("says hello world", () => {
        const message = service.getMessage()

        expect(message).toEqual("Hello, World!")
      })
    })
  },
})
```

Then, run the test with the following command:

```bash npm2yarn
npm run test:modules
```

***

## Commerce Modules

Medusa provides all its commerce features as separate Commerce Modules, such as the Product or Order modules.

Refer to the [Commerce Modules](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/index.html.md) documentation for concepts and reference of every module's main service.

### Create an Actor Type to Authenticate

To create an actor type that can authenticate to the Medusa application, such as a `manager`:

1. Create the data model in a module:

```ts
import { model } from "@medusajs/framework/utils"

const Manager = model.define("manager", {
  id: model.id().primaryKey(),
  firstName: model.text(),
  lastName: model.text(),
  email: model.text(),
})

export default Manager
```

2. Use the `setAuthAppMetadataStep` as a step in a workflow that creates a manager:

```ts
import { 
  createWorkflow, 
  WorkflowResponse,
} from "@medusajs/framework/workflows-sdk"
import { 
  setAuthAppMetadataStep,
} from "@medusajs/medusa/core-flows"
// other imports...

const createManagerWorkflow = createWorkflow(
  "create-manager",
  function (input: CreateManagerWorkflowInput) {
    const manager = createManagerStep({
      manager: input.manager,
    })

    setAuthAppMetadataStep({
      authIdentityId: input.authIdentityId,
      actorType: "manager",
      value: manager.id,
    })

    return new WorkflowResponse(manager)
  }
)
```

3. Use the workflow in an API route that creates a user (manager) of the actor type:

```ts
import type { 
  AuthenticatedMedusaRequest,
  MedusaResponse,
} from "@medusajs/framework/http"
import { MedusaError } from "@medusajs/framework/utils"
import createManagerWorkflow from "../../workflows/create-manager"

type RequestBody = {
  first_name: string
  last_name: string
  email: string
}

export async function POST(
  req: AuthenticatedMedusaRequest<RequestBody>, 
  res: MedusaResponse
) {
  // If `actor_id` is present, the request carries 
  // authentication for an existing manager
  if (req.auth_context.actor_id) {
    throw new MedusaError(
      MedusaError.Types.INVALID_DATA,
      "Request already authenticated as a manager."
    )
  }

  const { result } = await createManagerWorkflow(req.scope)
    .run({
      input: {
        manager: req.body,
        authIdentityId: req.auth_context.auth_identity_id,
      },
    })
  
    res.status(200).json({ manager: result })
}
```

4. Apply the `authenticate` middleware on the new route in `src/api/middlewares.ts`:

```ts title="src/api/middlewares.ts"
import { 
  defineMiddlewares,
  authenticate,
} from "@medusajs/framework/http"

export default defineMiddlewares({
  routes: [
    {
      matcher: "/manager",
      method: "POST",
      middlewares: [
        authenticate("manager", ["session", "bearer"], {
          allowUnregistered: true,
        }),
      ],
    },
    {
      matcher: "/manager/me*",
      middlewares: [
        authenticate("manager", ["session", "bearer"]),
      ],
    },
  ],
})
```

Now, manager users can use the `/manager` API route to register, and all routes starting with `/manager/me` are only accessible by authenticated managers.

Find an elaborate example and learn more in [this documentation](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/auth/create-actor-type/index.html.md).

### Apply Promotion on Cart Items and Shipping

To apply a promotion on a cart's items and shipping methods using the [Cart](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/cart/index.html.md) and [Promotion](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/promotion/index.html.md) modules:

```ts
import {
  ComputeActionAdjustmentLine,
  ComputeActionItemLine,
  ComputeActionShippingLine,
  AddItemAdjustmentAction,
  AddShippingMethodAdjustment,
  // ...
} from "@medusajs/framework/types"

// retrieve the cart
const cart = await cartModuleService.retrieveCart("cart_123", {
  relations: [
    "items.adjustments",
    "shipping_methods.adjustments",
  ],
})

// retrieve line item adjustments
const lineItemAdjustments: ComputeActionItemLine[] = []
cart.items.forEach((item) => {
  const filteredAdjustments = item.adjustments?.filter(
    (adjustment) => adjustment.code !== undefined
  ) as unknown as ComputeActionAdjustmentLine[]
  if (filteredAdjustments.length) {
    lineItemAdjustments.push({
      ...item,
      adjustments: filteredAdjustments,
    })
  }
})

// retrieve shipping method adjustments
const shippingMethodAdjustments: ComputeActionShippingLine[] =
  []
cart.shipping_methods.forEach((shippingMethod) => {
  const filteredAdjustments =
    shippingMethod.adjustments?.filter(
      (adjustment) => adjustment.code !== undefined
    ) as unknown as ComputeActionAdjustmentLine[]
  if (filteredAdjustments.length) {
    shippingMethodAdjustments.push({
      ...shippingMethod,
      adjustments: filteredAdjustments,
    })
  }
})

// compute actions
const actions = await promotionModuleService.computeActions(
  ["promo_123"],
  {
    items: lineItemAdjustments,
    shipping_methods: shippingMethodAdjustments,
  }
)

// set the adjustments on the line item
await cartModuleService.setLineItemAdjustments(
  cart.id,
  actions.filter(
    (action) => action.action === "addItemAdjustment"
  ) as AddItemAdjustmentAction[]
)

// set the adjustments on the shipping method
await cartModuleService.setShippingMethodAdjustments(
  cart.id,
  actions.filter(
    (action) =>
      action.action === "addShippingMethodAdjustment"
  ) as AddShippingMethodAdjustment[]
)
```

Learn more in [this documentation](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/cart/tax-lines/index.html.md).

### Retrieve Tax Lines of a Cart's Items and Shipping

To retrieve the tax lines of a cart's items and shipping methods using the [Cart](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/cart/index.html.md) and [Tax](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/tax/index.html.md) modules:

```ts
// retrieve the cart
const cart = await cartModuleService.retrieveCart("cart_123", {
  relations: [
    "items.tax_lines",
    "shipping_methods.tax_lines",
    "shipping_address",
  ],
})

// retrieve the tax lines
const taxLines = await taxModuleService.getTaxLines(
  [
    ...(cart.items as TaxableItemDTO[]),
    ...(cart.shipping_methods as TaxableShippingDTO[]),
  ],
  {
    address: {
      ...cart.shipping_address,
      country_code:
        cart.shipping_address.country_code || "us",
    },
  }
)

// set line item tax lines
await cartModuleService.setLineItemTaxLines(
  cart.id,
  taxLines.filter((line) => "line_item_id" in line)
)

// set shipping method tax lines
await cartModuleService.setLineItemTaxLines(
  cart.id,
  taxLines.filter((line) => "shipping_line_id" in line)
)
```

Learn more in [this documentation](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/cart/tax-lines/index.html.md)

### Apply Promotion on an Order's Items and Shipping

To apply a promotion on an order's items and shipping methods using the [Order](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/order/index.html.md) and [Promotion](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/promotion/index.html.md) modules:

```ts
import {
  ComputeActionAdjustmentLine,
  ComputeActionItemLine,
  ComputeActionShippingLine,
  AddItemAdjustmentAction,
  AddShippingMethodAdjustment,
  // ...
} from "@medusajs/framework/types"

// ...

// retrieve the order
const order = await orderModuleService.retrieveOrder("ord_123", {
  relations: [
    "items.item.adjustments",
    "shipping_methods.shipping_method.adjustments",
  ],
})
// retrieve the line item adjustments
const lineItemAdjustments: ComputeActionItemLine[] = []
order.items.forEach((item) => {
  const filteredAdjustments = item.adjustments?.filter(
    (adjustment) => adjustment.code !== undefined
  ) as unknown as ComputeActionAdjustmentLine[]
  if (filteredAdjustments.length) {
    lineItemAdjustments.push({
      ...item,
      ...item.detail,
      adjustments: filteredAdjustments,
    })
  }
})

//retrieve shipping method adjustments
const shippingMethodAdjustments: ComputeActionShippingLine[] =
  []
order.shipping_methods.forEach((shippingMethod) => {
  const filteredAdjustments =
    shippingMethod.adjustments?.filter(
      (adjustment) => adjustment.code !== undefined
    ) as unknown as ComputeActionAdjustmentLine[]
  if (filteredAdjustments.length) {
    shippingMethodAdjustments.push({
      ...shippingMethod,
      adjustments: filteredAdjustments,
    })
  }
})

// compute actions
const actions = await promotionModuleService.computeActions(
  ["promo_123"],
  {
    items: lineItemAdjustments,
    shipping_methods: shippingMethodAdjustments,
    // TODO infer from cart or region
    currency_code: "usd",
  }
)

// set the adjustments on the line items
await orderModuleService.setOrderLineItemAdjustments(
  order.id,
  actions.filter(
    (action) => action.action === "addItemAdjustment"
  ) as AddItemAdjustmentAction[]
)

// set the adjustments on the shipping methods
await orderModuleService.setOrderShippingMethodAdjustments(
  order.id,
  actions.filter(
    (action) =>
      action.action === "addShippingMethodAdjustment"
  ) as AddShippingMethodAdjustment[]
)
```

Learn more in [this documentation](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/order/promotion-adjustments/index.html.md)

### Accept Payment using Module

To accept payment using the Payment Module's main service:

1. Create a payment collection and link it to the cart:

```ts
import { 
  ContainerRegistrationKeys,
  Modules,
} from "@medusajs/framework/utils"

// ...

const paymentCollection =
  await paymentModuleService.createPaymentCollections({
    region_id: "reg_123",
    currency_code: "usd",
    amount: 5000,
  })

// resolve Link
const link = container.resolve(
  ContainerRegistrationKeys.LINK
)

// create a link between the cart and payment collection
link.create({
  [Modules.CART]: {
    cart_id: "cart_123",
  },
  [Modules.PAYMENT]: {
    payment_collection_id: paymentCollection.id,
  },
})
```

2. Create a payment session in the collection:

```ts
const paymentSession =
  await paymentModuleService.createPaymentSession(
    paymentCollection.id,
    {
      provider_id: "stripe",
      currency_code: "usd",
      amount: 5000,
      data: {
        // any necessary data for the
        // payment provider
      },
    }
  )
```

3. Authorize the payment session:

```ts
const payment =
  await paymentModuleService.authorizePaymentSession(
    paymentSession.id,
    {}
  )
```

Learn more in [this documentation](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/payment/payment-flow/index.html.md).

### Get Variant's Prices for Region and Currency

To get prices of a product variant for a region and currency using [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md):

```ts
import { QueryContext } from "@medusajs/framework/utils"

// ...

const { data: products } = await query.graph({
  entity: "product",
  fields: [
    "*",
    "variants.*",
    "variants.calculated_price.*",
  ],
  filters: {
    id: "prod_123",
  },
  context: {
    variants: {
      calculated_price: QueryContext({
        region_id: "reg_01J3MRPDNXXXDSCC76Y6YCZARS",
        currency_code: "eur",
      }),
    },
  },
})
```

Learn more in [this documentation](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/product/guides/price#retrieve-calculated-price-for-a-context/index.html.md).

### Get All Variant's Prices

To get all prices of a product variant using [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md):

```ts
const { data: products } = await query.graph({
  entity: "product",
  fields: [
    "*",
    "variants.*",
    "variants.prices.*",
  ],
  filters: {
    id: [
      "prod_123",
    ],
  },
})
```

Learn more in [this documentation](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/product/guides/price/index.html.md).

### Get Variant Prices with Taxes

To get a variant's prices with taxes using [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md) and the [Tax Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/tax/index.html.md)

```ts
import {
  HttpTypes,
  TaxableItemDTO,
  ItemTaxLineDTO,
} from "@medusajs/framework/types"
import { 
  QueryContext,
  calculateAmountsWithTax,
} from "@medusajs/framework/utils"
// other imports...

// ...
const asTaxItem = (product: HttpTypes.StoreProduct): TaxableItemDTO[] => {
  return product.variants
    ?.map((variant) => {
      if (!variant.calculated_price) {
        return
      }

      return {
        id: variant.id,
        product_id: product.id,
        product_name: product.title,
        product_categories: product.categories?.map((c) => c.name),
        product_category_id: product.categories?.[0]?.id,
        product_sku: variant.sku,
        product_type: product.type,
        product_type_id: product.type_id,
        quantity: 1,
        unit_price: variant.calculated_price.calculated_amount,
        currency_code: variant.calculated_price.currency_code,
      }
    })
    .filter((v) => !!v) as unknown as TaxableItemDTO[]
}

const { data: products } = await query.graph({
  entity: "product",
  fields: [
    "*",
    "variants.*",
    "variants.calculated_price.*",
  ],
  filters: {
    id: "prod_123",
  },
  context: {
    variants: {
      calculated_price: QueryContext({
        region_id: "region_123",
        currency_code: "usd",
      }),
    },
  },
})

const taxLines = (await taxModuleService.getTaxLines(
  products.map(asTaxItem).flat(),
  {
    // example of context properties. You can pass other ones.
    address: {
      country_code,
    },
  }
)) as unknown as ItemTaxLineDTO[]

const taxLinesMap = new Map<string, ItemTaxLineDTO[]>()
taxLines.forEach((taxLine) => {
  const variantId = taxLine.line_item_id
  if (!taxLinesMap.has(variantId)) {
    taxLinesMap.set(variantId, [])
  }

  taxLinesMap.get(variantId)?.push(taxLine)
})

products.forEach((product) => {
  product.variants?.forEach((variant) => {
    if (!variant.calculated_price) {
      return
    }

    const taxLinesForVariant = taxLinesMap.get(variant.id) || []
    const { priceWithTax, priceWithoutTax } = calculateAmountsWithTax({
      taxLines: taxLinesForVariant,
      amount: variant.calculated_price!.calculated_amount!,
      includesTax:
        variant.calculated_price!.is_calculated_price_tax_inclusive!,
    })

    // do something with prices...
  })
})
```

Learn more in [this documentation](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/product/guides/price-with-taxes/index.html.md).

### Invite Users

To invite a user using the [User Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/user/index.html.md):

```ts
const invite = await userModuleService.createInvites({
  email: "user@example.com",
})
```

### Accept User Invite

To accept an invite and create a user using the [User Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/user/index.html.md):

```ts
const invite =
  await userModuleService.validateInviteToken(inviteToken)

await userModuleService.updateInvites({
  id: invite.id,
  accepted: true,
})

const user = await userModuleService.createUsers({
  email: invite.email,
})
```


# Implement Custom Line Item Pricing in Medusa

In this guide, you'll learn how to add line items with custom prices to a cart in Medusa.

When you install a Medusa application, you get a fully-fledged commerce platform with a Framework for customization. The Medusa application's commerce features are built around [Commerce Modules](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/index.html.md) which are available out-of-the-box. These features include managing carts and adding line items to them.

By default, you can add product variants to the cart, where the price of its associated line item is based on the product variant's price. However, you can build customizations to add line items with custom prices to the cart. This is useful when integrating an Enterprise Resource Planning (ERP), Product Information Management (PIM), or other third-party services that provide real-time prices for your products.

To showcase how to add line items with custom prices to the cart, this guide uses [GoldAPI.io](https://www.goldapi.io) as an example of a third-party system that you can integrate for real-time prices. You can follow the same approach for other third-party integrations that provide custom pricing.

You can follow this guide whether you're new to Medusa or an advanced Medusa developer.

### Summary

This guide will teach you how to:

- Install and set up Medusa.
- Integrate the third-party service [GoldAPI.io](https://www.goldapi.io) that retrieves real-time prices for metals like Gold and Silver.
- Add an API route to add a product variant that has metals, such as a gold ring, to the cart with the real-time price retrieved from the third-party service.

![Diagram showcasing overview of implementation for adding an item to cart from storefront.](https://res.cloudinary.com/dza7lstvk/image/upload/v1738920014/Medusa%20Resources/custom-line-item-3_zu3qh2.jpg)

- [Custom Item Price Repository](https://github.com/medusajs/examples/tree/main/custom-item-price): Find the full code for this guide in this repository.
- [OpenApi Specs for Postman](https://res.cloudinary.com/dza7lstvk/raw/upload/v1738246728/OpenApi/Custom_Item_Price_gdfnl3.yaml): Import this OpenApi Specs file into tools like Postman.

***

## Step 1: Install a Medusa Application

### Prerequisites

- [Node.js v20+](https://nodejs.org/en/download)
- [Git CLI tool](https://git-scm.com/downloads)
- [PostgreSQL](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. You can also optionally choose to install the [Next.js Starter Storefront](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/nextjs-starter/index.html.md).

Afterwards, the installation process will start, which will install the Medusa application in a directory with your project's name. If you chose to install the Next.js starter, it'll be installed in a separate directory with the `{project-name}-storefront` name.

The Medusa application is composed of a headless Node.js server and an admin dashboard. The storefront is installed or custom-built separately and connects to the Medusa application through its REST endpoints, called [API routes](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/index.html.md). Learn more about Medusa's architecture in [this documentation](https://docs.medusajs.com/docs/learn/introduction/architecture/index.html.md).

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.

Check out the [troubleshooting guides](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/troubleshooting/create-medusa-app-errors/index.html.md) for help.

***

## Step 2: Integrate GoldAPI.io

### Prerequisites

- [GoldAPI.io Account. You can create a free account.](https://www.goldapi.io)

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 integrates the module into your application without implications or side effects on your setup.

In this step, you'll create a Metal Price Module that uses the GoldAPI.io service to retrieve real-time prices for metals like Gold and Silver. You'll use this module later to retrieve the real-time price of a product variant based on the metals in it, and add it to the cart with that custom price.

Learn more about modules in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/modules/index.html.md).

### Create Module Directory

A module is created under the `src/modules` directory of your Medusa application. So, create the directory `src/modules/metal-prices`.

![Diagram showcasing the module directory to create](https://res.cloudinary.com/dza7lstvk/image/upload/v1738247192/Medusa%20Resources/custom-item-price-1_q16evr.jpg)

### Create Module's 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 Metal Prices Module's service that connects to the GoldAPI.io service to retrieve real-time prices for metals.

Start by creating the file `src/modules/metal-prices/service.ts` with the following content:

![Diagram showcasing the service file to create](https://res.cloudinary.com/dza7lstvk/image/upload/v1738247303/Medusa%20Resources/custom-item-price-2_eaefis.jpg)

```ts title="src/modules/metal-prices/service.ts"
type Options = {
  accessToken: string
  sandbox?: boolean
}

export default class MetalPricesModuleService {
  protected options_: Options

  constructor({}, options: Options) {
    this.options_ = options
  }
}
```

A module can accept options that are passed to its service. You define an `Options` type that indicates the options the module accepts. It accepts two options:

- `accessToken`: The access token for the GoldAPI.io service.
- `sandbox`: A boolean that indicates whether to simulate sending requests to the GoldAPI.io service. This is useful when running in a test environment.

The service's constructor receives the module's options as a second parameter. You store the options in the service's `options_` property.

A module has a container of Medusa Framework tools and local resources in the module that you can access in the service constructor's first parameter. Learn more in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/modules/container/index.html.md).

#### Add Method to Retrieve Metal Prices

Next, you'll add the method to retrieve the metal prices from the third-party service.

First, add the following types at the beginning of `src/modules/metal-prices/service.ts`:

```ts title="src/modules/metal-prices/service.ts"
export enum MetalSymbols {
  Gold = "XAU",
  Silver = "XAG",
  Platinum = "XPT",
  Palladium = "XPD"
}

export type PriceResponse = {
  metal: MetalSymbols
  currency: string
  exchange: string
  symbol: string
  price: number
  [key: string]: unknown
}

```

The `MetalSymbols` enum defines the symbols for metals like Gold, Silver, Platinum, and Palladium. The `PriceResponse` type defines the structure of the response from the GoldAPI.io's endpoint.

Next, add the method `getMetalPrices` to the `MetalPricesModuleService` class:

```ts title="src/modules/metal-prices/service.ts"
import { MedusaError } from "@medusajs/framework/utils"

// ...

export default class MetalPricesModuleService {
  // ...
  async getMetalPrice(
    symbol: MetalSymbols, 
    currency: string
  ): Promise<PriceResponse> {
    const upperCaseSymbol = symbol.toUpperCase()
    const upperCaseCurrency = currency.toUpperCase()

    return fetch(`https://www.goldapi.io/api/${upperCaseSymbol}/${upperCaseCurrency}`, {
      headers: {
        "x-access-token": this.options_.accessToken,
        "Content-Type": "application/json",
      },
      redirect: "follow",
    }).then((response) => response.json())
    .then((response) => {
      if (response.error) {
        throw new MedusaError(
          MedusaError.Types.INVALID_DATA,
          response.error
        )
      }

      return response
    })
  }
}
```

The `getMetalPrice` method accepts the metal symbol and currency as parameters. You send a request to GoldAPI.io's `/api/{symbol}/{currency}` endpoint to retrieve the metal's price, also passing the access token in the request's headers.

If the response contains an error, you throw a `MedusaError` with the error message. Otherwise, you return the response, which is of type `PriceResponse`.

#### Add Helper Methods

You'll also add two helper methods to the `MetalPricesModuleService`. The first one is `getMetalSymbols` that returns the metal symbols as an array of strings:

```ts title="src/modules/metal-prices/service.ts"
export default class MetalPricesModuleService {
  // ...
  async getMetalSymbols(): Promise<string[]> {
    return Object.values(MetalSymbols)
  }
}
```

The second is `getMetalSymbol` that receives a name like `gold` and returns the corresponding metal symbol:

```ts title="src/modules/metal-prices/service.ts"
export default class MetalPricesModuleService {
  // ...
  async getMetalSymbol(name: string): Promise<MetalSymbols | undefined> {
    const formattedName = name.charAt(0).toUpperCase() + name.slice(1).toLowerCase()
    return MetalSymbols[formattedName as keyof typeof MetalSymbols]
  }
}
```

You'll use these methods in later steps.

### Export Module Definition

The final piece to a module is its definition, which you export in an `index.ts` file at its root directory. This definition tells Medusa the name of the module and its service.

So, create the file `src/modules/metal-prices/index.ts` with the following content:

![The directory structure of the Metal Prices Module after adding the definition file.](https://res.cloudinary.com/dza7lstvk/image/upload/v1738248049/Medusa%20Resources/custom-item-price-3_imtbuw.jpg)

```ts title="src/modules/metal-prices/index.ts"
import { Module } from "@medusajs/framework/utils"
import MetalPricesModuleService from "./service"

export const METAL_PRICES_MODULE = "metal-prices"

export default Module(METAL_PRICES_MODULE, {
  service: MetalPricesModuleService,
})
```

You use the `Module` function from the Modules SDK to create the module's definition. It accepts two parameters:

1. The module's name, which is `metal-prices`.
2. An object with a required property `service` indicating the module's service.

### Add Module 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 and pass an array with your custom module:

```ts title="medusa-config.ts"
module.exports = defineConfig({
  // ...
  modules: [
    {
      resolve: "./src/modules/metal-prices",
      options: {
        accessToken: process.env.GOLD_API_TOKEN,
        sandbox: process.env.GOLD_API_SANDBOX === "true",
      },
    },
  ],
})
```

Each object in the `modules` array has a `resolve` property, whose value is either a path to the module's directory, or an `npm` package’s name.

The object also has an `options` property that accepts the module's options. You set the `accessToken` and `sandbox` options based on environment variables.

You'll find the access token at the top of your GoldAPI.io dashboard.

![The access token is below the "API Token" header of your GoldAPI.io dashboard.](https://res.cloudinary.com/dza7lstvk/image/upload/v1738248335/Medusa%20Resources/Screenshot_2025-01-30_at_4.44.07_PM_xht3j4.png)

Set the access token as an environment variable in `.env`:

```bash
GOLD_API_TOKEN=
```

You'll start using the module in the next steps.

***

## Step 3: Add Custom Item to Cart Workflow

In this section, you'll implement the logic to retrieve the real-time price of a variant based on the metals in it, then add the variant to the cart with the custom price. You'll implement this logic in 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 an endpoint.

Learn more about workflows in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md)

The workflow you'll implement in this section has the following steps:

- [useQueryGraphStep (Retrieve Cart)](https://docs.medusajs.com/references/helper-steps/useQueryGraphStep/index.html.md): Retrieve the cart's ID and currency using Query.
- [useQueryGraphStep (Retrieve Variant)](https://docs.medusajs.com/references/helper-steps/useQueryGraphStep/index.html.md): Retrieve the variant's details using Query
- [getVariantMetalPricesStep](#getvariantmetalpricesstep): Retrieve the variant's price using the third-party service.
- [addToCartWorkflow](https://docs.medusajs.com/references/medusa-workflows/addToCartWorkflow/index.html.md): Add the item with the custom price to the cart.
- [useQueryGraphStep (Retrieve Cart)](https://docs.medusajs.com/references/helper-steps/useQueryGraphStep/index.html.md): Retrieve the updated cart's details using Query.

`useQueryGraphStep` and `addToCartWorkflow` are available through Medusa's core workflows package. You'll only implement the `getVariantMetalPricesStep`.

### getVariantMetalPricesStep

The `getVariantMetalPricesStep` will retrieve the real-time metal price of a variant received as an input.

To create the step, create the file `src/workflows/steps/get-variant-metal-prices.ts` with the following content:

![The directory structure after adding the step file.](https://res.cloudinary.com/dza7lstvk/image/upload/v1738249036/Medusa%20Resources/custom-item-price-4_kumzdc.jpg)

```ts title="src/workflows/steps/get-variant-metal-prices.ts"
import { createStep } from "@medusajs/framework/workflows-sdk"
import { ProductVariantDTO } from "@medusajs/framework/types"
import { METAL_PRICES_MODULE } from "../../modules/metal-prices"
import MetalPricesModuleService from "../../modules/metal-prices/service"

export type GetVariantMetalPricesStepInput = {
  variant: ProductVariantDTO & {
    calculated_price?: {
      calculated_amount: number
    }
  }
  currencyCode: string
  quantity?: number
}

export const getVariantMetalPricesStep = createStep(
  "get-variant-metal-prices",
  async ({
    variant,
    currencyCode,
    quantity = 1,
  }: GetVariantMetalPricesStepInput, { container }) => {
    const metalPricesModuleService: MetalPricesModuleService = 
      container.resolve(METAL_PRICES_MODULE)

    // TODO 
  }
)
```

You create a step with `createStep` from the Workflows SDK. It accepts two parameters:

1. The step's unique name, which is `get-variant-metal-prices`.
2. An async function that receives two parameters:
   - An input object with the variant, currency code, and quantity. The variant has a `calculated_price` property that holds the variant's fixed price in the Medusa application. This is useful when you want to add a fixed price to the real-time custom price, such as handling fees.
   - The [Medusa container](https://docs.medusajs.com/docs/learn/fundamentals/medusa-container/index.html.md), which is a registry of Framework and commerce tools that you can access in the step.

In the step function, so far you only resolve the Metal Prices Module's service from the Medusa container.

Next, you'll validate that the specified variant can have its price calculated. Add the following import at the top of the file:

```ts title="src/workflows/steps/get-variant-metal-prices.ts"
import { MedusaError } from "@medusajs/framework/utils"
```

And replace the `TODO` in the step function with the following:

```ts title="src/workflows/steps/get-variant-metal-prices.ts"
const variantMetal = variant.options.find(
  (option) => option.option?.title === "Metal"
)?.value
const metalSymbol = await metalPricesModuleService
  .getMetalSymbol(variantMetal || "")

if (!metalSymbol) {
  throw new MedusaError(
    MedusaError.Types.INVALID_DATA,
    "Variant doesn't have metal. Make sure the variant's SKU matches a metal symbol."
  )
}

if (!variant.weight) {
  throw new MedusaError(
    MedusaError.Types.INVALID_DATA,
    "Variant doesn't have weight. Make sure the variant has weight to calculate its price."
  )
}

// TODO retrieve custom price
```

In the code above, you first retrieve the metal option's value from the variant's options, assuming that a variant has metals if it has a `Metal` option. Then, you retrieve the metal symbol of the option's value using the `getMetalSymbol` method of the Metal Prices Module's service.

If the variant doesn't have a metal in its options, the option's value is not valid, or the variant doesn't have a weight, you throw an error. The weight is necessary to calculate the price based on the metal's price per weight.

Next, you'll retrieve the real-time price of the metal using the third-party service. Replace the `TODO` with the following:

```ts title="src/workflows/steps/get-variant-metal-prices.ts"
let price = variant.calculated_price?.calculated_amount || 0
const weight = variant.weight
const { price: metalPrice } = await metalPricesModuleService.getMetalPrice(
  metalSymbol as MetalSymbols, currencyCode
)
price += (metalPrice * weight * quantity)

return new StepResponse(price)
```

In the code above, you first set the price to the variant's fixed price, if it has one. Then, you retrieve the metal's price using the `getMetalPrice` method of the Metal Prices Module's service.

Finally, you calculate the price by multiplying the metal's price by the variant's weight and the quantity to add to the cart, then add the fixed price to it.

Every step must return a `StepResponse` instance. The `StepResponse` constructor accepts the step's output as a parameter, which in this case is the variant's price.

### Create addCustomToCartWorkflow

Now that you have the `getVariantMetalPricesStep`, you can create the workflow that adds the item with custom pricing to the cart.

Create the file `src/workflows/add-custom-to-cart.ts` with the following content:

![The directory structure after adding the workflow file.](https://res.cloudinary.com/dza7lstvk/image/upload/v1738251380/Medusa%20Resources/custom-item-price-5_zorahv.jpg)

```ts title="src/workflows/add-custom-to-cart.ts" highlights={workflowHighlights}
import { createWorkflow } from "@medusajs/framework/workflows-sdk"
import { useQueryGraphStep } from "@medusajs/medusa/core-flows"
import { QueryContext } from "@medusajs/framework/utils"

type AddCustomToCartWorkflowInput = {
  cart_id: string
  item: {
    variant_id: string
    quantity: number
    metadata?: Record<string, unknown>
  }
}

export const addCustomToCartWorkflow = createWorkflow(
  "add-custom-to-cart",
  ({ cart_id, item }: AddCustomToCartWorkflowInput) => {
    // @ts-ignore
    const { data: carts } = useQueryGraphStep({
      entity: "cart",
      filters: { id: cart_id },
      fields: ["id", "currency_code"],
    })

    const { data: variants } = useQueryGraphStep({
      entity: "variant",
      fields: [
        "*",
        "options.*",
        "options.option.*",
        "calculated_price.*",
      ],
      filters: {
        id: item.variant_id,
      },
      options: {
        throwIfKeyNotFound: true,
      },
      context: {
        calculated_price: QueryContext({
          currency_code: carts[0].currency_code,
        }),
      },
    }).config({ name: "retrieve-variant" })

    // TODO add more steps
  }
)
```

You create a workflow with `createWorkflow` from the Workflows SDK. It accepts two parameters:

1. The workflow's unique name, which is `add-custom-to-cart`.
2. A function that receives an input object with the cart's ID and the item to add to the cart. The item has the variant's ID, quantity, and optional metadata.

In the function, you first retrieve the cart's details using the `useQueryGraphStep` helper step. This step uses [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md) which is a Modules SDK tool that retrieves data across modules. You use it to retrieve the cart's ID and currency code.

You also retrieve the variant's details using the `useQueryGraphStep` helper step. You pass the variant's ID to the step's filters and specify the fields to retrieve. To retrieve the variant's price based on the cart's context, you pass the cart's currency code to the `calculated_price` context.

Next, you'll retrieve the variant's real-time price using the `getVariantMetalPricesStep` you created earlier. First, add the following import:

```ts title="src/workflows/add-custom-to-cart.ts"
import { 
  getVariantMetalPricesStep, 
  GetVariantMetalPricesStepInput,
} from "./steps/get-variant-metal-prices"
```

Then, replace the `TODO` in the workflow with the following:

```ts title="src/workflows/add-custom-to-cart.ts"
const price = getVariantMetalPricesStep({
  variant: variants[0],
  currencyCode: carts[0].currency_code,
  quantity: item.quantity,
} as unknown as GetVariantMetalPricesStepInput)

// TODO add item with custom price to cart
```

You execute the `getVariantMetalPricesStep` passing it the variant's details, the cart's currency code, and the quantity of the item to add to the cart. The step returns the variant's custom price.

Next, you'll add the item with the custom price to the cart. First, add the following imports at the top of the file:

```ts title="src/workflows/add-custom-to-cart.ts"
import { transform } from "@medusajs/framework/workflows-sdk"
import { addToCartWorkflow } from "@medusajs/medusa/core-flows"
```

Then, replace the `TODO` in the workflow with the following:

```ts title="src/workflows/add-custom-to-cart.ts"
const itemToAdd = transform({
  item,
  price,
}, (data) => {
  return [{
    ...data.item,
    unit_price: data.price,
  }]
})

addToCartWorkflow.runAsStep({
  input: {
    items: itemToAdd,
    cart_id,
  },
})

// TODO retrieve and return cart
```

You prepare the item to add to the cart using `transform` from the Workflows SDK. It allows you to manipulate and create variables in a workflow. After that, you use Medusa's `addToCartWorkflow` to add the item with the custom price to the cart.

A workflow's constructor function has some constraints in implementation, which is why you need to use `transform` for variable manipulation. Learn more about these constraints in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/workflows/constructor-constraints/index.html.md).

Lastly, you'll retrieve the cart's details again and return them. Add the following import at the beginning of the file:

```ts title="src/workflows/add-custom-to-cart.ts"
import { WorkflowResponse } from "@medusajs/framework/workflows-sdk"
```

And replace the last `TODO` in the workflow with the following:

```ts title="src/workflows/add-custom-to-cart.ts"
// @ts-ignore
const { data: updatedCarts } = useQueryGraphStep({
  entity: "cart",
  filters: { id: cart_id },
  fields: ["id", "items.*"],
}).config({ name: "refetch-cart" })

return new WorkflowResponse({
  cart: updatedCarts[0],
})
```

In the code above, you retrieve the updated cart's details using the `useQueryGraphStep` helper step. To return data from the workflow, you create and return a `WorkflowResponse` instance. It accepts as a parameter the data to return, which is the updated cart.

In the next step, you'll use the workflow in a custom route to add an item with a custom price to the cart.

***

## Step 4: Create Add Custom Item to Cart API Route

Now that you've implemented the logic to add an item with a custom price to the cart, you'll expose this functionality in an API route.

An API Route is an endpoint that exposes commerce features to external applications and clients, such as storefronts. You'll create an API route at the path `/store/carts/:id/line-items-metals` that executes the workflow from the previous step to add a product variant with custom price to the cart.

Learn more about API routes in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/index.html.md).

### Create API Route

An API route is created in a `route.ts` file under a sub-directory of the `src/api` directory.

The path of the API route is the file's path relative to `src/api`. So, to create the `/store/carts/:id/line-items-metals` API route, create the file `src/api/store/carts/[id]/line-items-metals/route.ts` with the following content:

![The directory structure after adding the API route file.](https://res.cloudinary.com/dza7lstvk/image/upload/v1738252712/Medusa%20Resources/custom-item-price-6_deecbu.jpg)

```ts title="src/api/store/carts/[id]/line-items-metals/route.ts"
import { MedusaRequest, MedusaResponse } from "@medusajs/framework"
import { HttpTypes } from "@medusajs/framework/types"
import { addCustomToCartWorkflow } from "../../../../../workflows/add-custom-to-cart"

export const POST = async (
  req: MedusaRequest<HttpTypes.StoreAddCartLineItem>, 
  res: MedusaResponse
) => {
  const { id } = req.params
  const item = req.validatedBody

  const { result } = await addCustomToCartWorkflow(req.scope)
    .run({
      input: {
        cart_id: id,
        item,
      },
    })

  res.status(200).json({ cart: result.cart })
}
```

Since you export a `POST` function in this file, you're exposing a `POST` API route at `/store/carts/:id/line-items-metals`. The route handler function accepts two parameters:

1. A request object with details and context on the request, such as path and body parameters.
2. A response object to manipulate and send the response.

In the function, you retrieve the cart's ID from the path parameter, and the item's details from the request body. This API route will accept the same request body parameters as Medusa's [Add Item to Cart API Route](https://docs.medusajs.com/api/store#carts_postcartsidlineitems).

Then, you execute the `addCustomToCartWorkflow` by invoking it, passing it the Medusa container, which is available in the request's `scope` property, then executing its `run` method. You pass the workflow's input object with the cart's ID and the item to add to the cart.

Finally, you return a response with the updated cart's details.

### Add Request Body Validation Middleware

To ensure that the request body contains the required parameters, you'll add a middleware that validates the incoming request's body based on a defined schema.

A middleware is a function executed before the API route when a request is sent to it. You define middlewares in Medusa in the `src/api/middlewares.ts` directory.

Learn more about middlewares in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/middlewares/index.html.md).

To add a validation middleware to the custom API route, create the file `src/api/middlewares.ts` with the following content:

![The directory structure after adding the middleware file.](https://res.cloudinary.com/dza7lstvk/image/upload/v1738253099/Medusa%20Resources/custom-item-price-7_l7iw2a.jpg)

```ts title="src/api/middlewares.ts"
import { 
  defineMiddlewares,
  validateAndTransformBody,
} from "@medusajs/framework/http"
import { 
  StoreAddCartLineItem,
} from "@medusajs/medusa/api/store/carts/validators"

export default defineMiddlewares({
  routes: [
    {
      matcher: "/store/carts/:id/line-items-metals",
      method: "POST",
      middlewares: [
        validateAndTransformBody(
          StoreAddCartLineItem
        ),
      ],
    },
  ],
})
```

In this file, you export the middlewares definition using `defineMiddlewares` from the Medusa Framework. This function accepts an object having a `routes` property, which is an array of middleware configurations to apply on routes.

You pass in the `routes` array an object having the following properties:

- `matcher`: The route to apply the middleware on.
- `method`: The HTTP method to apply the middleware on for the specified API route.
- `middlewares`: An array of the middlewares to apply. You apply the `validateAndTransformBody` middleware, which validates the request body based on the `StoreAddCartLineItem` schema. This validation schema is the same schema used for Medusa's [Add Item to Cart API Route](https://docs.medusajs.com/api/store#carts_postcartsidlineitems).

Any request sent to the `/store/carts/:id/line-items-metals` API route will now fail if it doesn't have the required parameters.

Learn more about API route validation in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/validation/index.html.md).

### Prepare to Test API Route

Before you test the API route, you'll prepare and retrieve the necessary data to add a product variant with a custom price to the cart.

#### Create Product with Metal Variant

You'll first create a product that has a `Metal` option, and variant(s) with values for this option.

Start the Medusa application with the following command:

```bash npm2yarn
npm run dev
```

Then, open the Medusa Admin dashboard at `localhost:9000/app` and log in with the email and password you created when you installed the Medusa application in the first step.

Once you log in, click on Products in the sidebar, then click the Create button at the top right.

![Click on Products in the sidebar at the left, then click on the Create button at the top right of the content](https://res.cloudinary.com/dza7lstvk/image/upload/v1738253415/Medusa%20Resources/Screenshot_2025-01-30_at_6.09.36_PM_ee0jr2.png)

Then, in the Create Product form:

1. Enter a name for the product, and optionally enter other details like description.
2. Enable the "Yes, this is a product with variants" toggle.
3. Under Product Options, enter "Metal" for the title, and enter "Gold" for the values.

Once you're done, click the Continue button.

![Fill in the product details, enable the "Yes, this is a product with variants" toggle, and add the "Metal" option with "Gold" value](https://res.cloudinary.com/dza7lstvk/image/upload/v1738253520/Medusa%20Resources/Screenshot_2025-01-30_at_6.11.29_PM_lqxth9.png)

You can skip the next two steps by clicking the Continue button again, then the Publish button.

Once you're done, the product's page will open. You'll now add weight to the product's Gold variant. To do that:

- Scroll to the Variants section and find the Gold variant.
- Click on the three-dots icon at its right.
- Choose "Edit" from the dropdown.

![Find the Gold variant in the Variants section, click on the three-dots icon, and choose "Edit"](https://res.cloudinary.com/dza7lstvk/image/upload/v1738254038/Medusa%20Resources/Screenshot_2025-01-30_at_6.19.52_PM_j3hjcx.png)

In the side window that opens, find the Weight field, enter the weight, and click the Save button.

![Enter the weight in the Weight field, then click the Save button](https://res.cloudinary.com/dza7lstvk/image/upload/v1738254165/Medusa%20Resources/Screenshot_2025-01-30_at_6.22.15_PM_yplzdp.png)

Finally, you need to set fixed prices for the variant, even if they're just `0`. To do that:

1. Click on the three-dots icon at the top right of the Variants section.
2. Choose "Edit Prices" from the dropdown.

![Click on the three-dots icon at the top right of the Variants section, then choose "Edit Prices"](https://res.cloudinary.com/dza7lstvk/image/upload/v1738255203/Medusa%20Resources/Screenshot_2025-01-30_at_6.39.35_PM_s3jpxh.png)

For each cell in the table, either enter a fixed price for the specified currency or leave it as `0`. Once you're done, click the Save button.

![Enter fixed prices for the variant in the table, then click the Save button](https://res.cloudinary.com/dza7lstvk/image/upload/v1738255272/Medusa%20Resources/Screenshot_2025-01-30_at_6.40.45_PM_zw1l59.png)

You'll use this variant to add it to the cart later. You can find its ID by clicking on the variant, opening its details page. Then, on the details page, click on the icon at the right of the JSON section, and copy the ID from the JSON data.

![Click on the icon at the right of the JSON section to copy the variant's ID](https://res.cloudinary.com/dza7lstvk/image/upload/v1738254314/Medusa%20Resources/Screenshot_2025-01-30_at_6.24.49_PM_ka7xew.png)

#### Retrieve Publishable API Key

All requests sent to API routes starting with `/store` must have a publishable API key in the header. This ensures the request's operations are scoped to the publishable API key's associated sales channels. For example, products that aren't available in a cart's sales channel can't be added to it.

To retrieve the publishable API key, on the Medusa Admin:

1. Click on Settings in the sidebar at the bottom left.
2. Click on Publishable API Keys from the sidebar, then click on a publishable API key in the list.

![Click on publishable API keys in the Settings sidebar, then click on a publishable API key in the list](https://res.cloudinary.com/dza7lstvk/image/upload/v1738254523/Medusa%20Resources/Screenshot_2025-01-30_at_6.28.17_PM_mldscc.png)

3. Click on the publishable API key to copy it.

![Click on the publishable API key to copy it](https://res.cloudinary.com/dza7lstvk/image/upload/v1738254601/Medusa%20Resources/Screenshot_2025-01-30_at_6.29.26_PM_vvatki.png)

You'll use this key when you test the API route.

### Test API Route

To test out the API route, you need to create a cart. A cart must be associated with a region. So, to retrieve the ID of a region in your store, send a `GET` request to the `/store/regions` API route:

```bash
curl 'localhost:9000/store/regions' \
-H 'x-publishable-api-key: {api_key}'
```

Make sure to replace `{api_key}` with the publishable API key you copied earlier.

This will return a list of regions. Copy the ID of one of the regions.

Then, send a `POST` request to the `/store/carts` API route to create a cart:

```bash
curl -X POST 'localhost:9000/store/carts' \
-H 'x-publishable-api-key: {api_key}' \
-H 'Content-Type: application/json' \
--data '{
    "region_id": "{region_id}"
}'
```

Make sure to replace `{api_key}` with the publishable API key you copied earlier, and `{region_id}` with the ID of a region from the previous request.

This will return the created cart. Copy the ID of the cart to use it next.

Finally, to add the Gold variant to the cart with a custom price, send a `POST` request to the `/store/carts/:id/line-items-metals` API route:

```bash
curl -X POST 'localhost:9000/store/carts/{cart_id}/line-items-metals' \
-H 'x-publishable-api-key: {api_key}' \
-H 'Content-Type: application/json' \
--data '{
    "variant_id": "{variant_id}",
    "quantity": 1
}'
```

Make sure to replace:

- `{api_key}` with the publishable API key you copied earlier.
- `{cart_id}` with the ID of the cart you created.
- `{variant_id}` with the ID of the Gold variant you created.

This will return the cart's details, where you can see in its `items` array the item with the custom price:

```json title="Example Response"
{
  "cart": {
    "items": [
      {
        "variant_id": "{variant_id}",
        "quantity": 1,
        "is_custom_price": true,
        // example custom price
        "unit_price": 2000
      }
    ]
  }
}
```

The price will be the result of the calculation you've implemented earlier, which is the fixed price of the variant plus the real-time price of the metal, multiplied by the weight of the variant and the quantity added to the cart.

This price will be reflected in the cart's total price, and you can proceed to checkout with the custom-priced item.

***

## Next Steps

You've now implemented custom item pricing in Medusa. You can also customize the [storefront](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/nextjs-starter/index.html.md) to use the new API route to add custom-priced items to the cart.

If you're new to Medusa, check out the [main documentation](https://docs.medusajs.com/docs/learn/index.html.md), 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](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/index.html.md).


# Implement Quote Management in Medusa

In this guide, you'll learn how to implement quote management in Medusa.

When you install a Medusa application, you get a fully-fledged commerce platform with a Framework for customization. The Medusa application's commerce features are built around [Commerce Modules](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/index.html.md) which are available out-of-the-box.

By default, the Medusa application provides standard commerce features for orders and carts. However, Medusa's customization capabilities facilitate extending existing features to implement quote-management features.

By building quote management features, you allow customers to request a quote for a set of products and, once the merchant and customer reach an agreement, you create an order for that quote. Quote management is useful in many use cases, including B2B stores.

This guide is based on the [B2B starter](https://github.com/medusajs/b2b-starter-medusa) explaining how to implement some of its quote management features. You can refer to the B2B starter for other features not covered in this guide.

## Summary

By following this guide, you'll add the following features to Medusa:

1. Customers can request a quote for a set of products.
2. Merchants can manage quotes in the Medusa Admin dashboard. They can reject a quote or send a counter-offer, and they can make edits to item prices and quantities.
3. Customers can accept or reject a quote once it's been sent by the merchant.
4. Once the customer accepts a quote, it's converted to an order in Medusa.

![Diagram showcasing the features summary](https://res.cloudinary.com/dza7lstvk/image/upload/v1741173690/Medusa%20Resources/quote-management-summary_xd319j.jpg)

To implement these features, you'll be customizing the Medusa server and the Medusa Admin dashboard.

You can follow this guide whether you're new to Medusa or an advanced Medusa developer.

- [Quote Management Repository](https://github.com/medusajs/examples/tree/main/quote-management): Find the full code for this guide in this repository.
- [OpenApi Specs for Postman](https://res.cloudinary.com/dza7lstvk/raw/upload/v1741171875/OpenApi/quote-management_tbk552.yml): Import this OpenApi Specs file into tools like Postman.

***

## Step 1: Install a Medusa Application

### Prerequisites

- [Node.js v20+](https://nodejs.org/en/download)
- [Git CLI tool](https://git-scm.com/downloads)
- [PostgreSQL](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. You can also optionally choose to install the [Next.js Starter Storefront](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/nextjs-starter/index.html.md).

Afterwards, the installation process will start, which will install the Medusa application in a directory with your project's name. If you chose to install the Next.js starter, it'll be installed in a separate directory with the `{project-name}-storefront` name.

The Medusa application is composed of a headless Node.js server and an admin dashboard. The storefront is installed or custom-built separately and connects to the Medusa application through its REST endpoints, called [API routes](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/index.html.md). Learn more about Medusa's architecture in [this documentation](https://docs.medusajs.com/docs/learn/introduction/architecture/index.html.md).

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.

Check out the [troubleshooting guides](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/troubleshooting/create-medusa-app-errors/index.html.md) for help.

***

## Step 2: Add Quote Module

In Medusa, you can build custom features in a [module](https://docs.medusajs.com/docs/learn/fundamentals/modules/index.html.md). A module is a reusable 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.

In the module, you define the data models necessary for a feature and the logic to manage these data models. Later, you can build commerce flows around your module and link its data models to other modules' data models, such as orders and carts.

In this step, you'll build a Quote Module that defines the necessary data model to store quotes.

Learn more about modules in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/modules/index.html.md).

### Create Module Directory

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

![Diagram showcasing the directory structure after adding the Quote Module's directory](https://res.cloudinary.com/dza7lstvk/image/upload/v1741074268/Medusa%20Resources/quote-1_lxgyyg.jpg)

### Create Data Models

A data model represents a table in the database. You create data models using Medusa's Data Model Language (DML). It simplifies defining a table's columns, relations, and indexes with straightforward methods and configurations.

Learn more about data models in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/modules#1-create-data-model/index.html.md).

For the Quote Module, you need to define a `Quote` data model that represents a quote requested by a customer.

So, start by creating the `Quote` data model. Create the file `src/modules/quote/models/quote.ts` with the following content:

![Diagram showcasing the directory structure after adding the quote model](https://res.cloudinary.com/dza7lstvk/image/upload/v1741074453/Medusa%20Resources/quote-2_lh012l.jpg)

```ts title="src/modules/quote/models/quote.ts" highlights={quoteModelHighlights}
import { model } from "@medusajs/framework/utils"

export enum QuoteStatus {
  PENDING_MERCHANT = "pending_merchant",
  PENDING_CUSTOMER = "pending_customer",
  ACCEPTED = "accepted",
  CUSTOMER_REJECTED = "customer_rejected",
  MERCHANT_REJECTED = "merchant_rejected",
}

export const Quote = model.define("quote", {
  id: model.id().primaryKey(),
  status: model
    .enum(Object.values(QuoteStatus))
    .default(QuoteStatus.PENDING_MERCHANT),
  customer_id: model.text(),
  draft_order_id: model.text(),
  order_change_id: model.text(),
  cart_id: model.text(),
})
```

You define the `Quote` data model using the `model.define` method of the DML. It accepts the data model's table name as a first parameter, and the model's schema object as a second parameter.

`Quote` has the following properties:

- `id`: A unique identifier for the quote.
- `status`: The status of the quote, which can be one of the following:
  - `pending_merchant`: The quote is pending the merchant's approval or rejection.
  - `pending_customer`: The quote is pending the customer's acceptance or rejection.
  - `accepted`: The quote has been accepted by the customer and converted to an order.
  - `customer_rejected`: The customer has rejected the quote.
  - `merchant_rejected`: The merchant has rejected the quote.
- `customer_id`: The ID of the customer who requested the quote. You'll later learn how to link this to a customer record.
- `draft_order_id`: The ID of the draft order created for the quote. You'll later learn how to link this to an order record.
- `order_change_id`: The ID of the order change created for the quote. An [order change](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/order/order-change/index.html.md) is a record of changes made to an order, such as price or quantity updates of the order's items. These changes are later applied to the order. You'll later learn how to link this to an order change record.
- `cart_id`: The ID of the cart that the quote was created from. The cart will hold the items that the customer wants a quote for. You'll later learn how to link this to a cart record.

Learn more about defining data model properties in the [Property Types documentation](https://docs.medusajs.com/docs/learn/fundamentals/data-models/properties/index.html.md).

### Create Module's Service

You now have the necessary data model in the Quote Module, but you need to define the logic to manage it. You do this by creating a service in the module.

A service is a TypeScript or JavaScript class that the module exports. In the service's methods, you can connect to the database, allowing you to manage your data models, or connect to a third-party service, which is useful if you're integrating with external services.

Learn more about services in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/modules#2-create-service/index.html.md).

To create the Quote Module's service, create the file `src/modules/quote/service.ts` with the following content:

![Directory structure after adding the service](https://res.cloudinary.com/dza7lstvk/image/upload/v1741075946/Medusa%20Resources/quote-4_hg4bnr.jpg)

```ts title="src/modules/quote/service.ts"
import { MedusaService } from "@medusajs/framework/utils"
import { Quote } from "./models/quote"

class QuoteModuleService extends MedusaService({ 
  Quote, 
}) {}

export default QuoteModuleService
```

The `QuoteModuleService` extends `MedusaService` from the Modules SDK which generates a class with data-management methods for your module's data models. This saves you time on implementing Create, Read, Update, and Delete (CRUD) methods.

So, the `QuoteModuleService` class now has methods like `createQuotes` and `retrieveQuote`.

Find all methods generated by the `MedusaService` in [this reference](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/service-factory-reference/index.html.md).

You'll use this service later when you implement custom flows for quote management.

### Export Module Definition

The final piece to a module is its definition, which you export in an `index.ts` file at its root directory. This definition tells Medusa the name of the module and its service.

So, create the file `src/modules/quote/index.ts` with the following content:

![Directory structure after adding the module definition](https://res.cloudinary.com/dza7lstvk/image/upload/v1741076106/Medusa%20Resources/quote-5_ngitn1.jpg)

```ts title="src/modules/quote/index.ts"
import { Module } from "@medusajs/framework/utils"
import QuoteModuleService from "./service"

export const QUOTE_MODULE = "quote"

export default Module(QUOTE_MODULE, { 
  service: QuoteModuleService,
})
```

You use the `Module` function from the Modules SDK to create the module's definition. It accepts two parameters:

1. The module's name, which is `quote`.
2. An object with a required property `service` indicating the module's service.

You also export the module's name as `QUOTE_MODULE` so you can reference it later.

### Add Module 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 and pass an array with your custom module:

```ts title="medusa-config.ts"
module.exports = defineConfig({
  // ...
  modules: [
    {
      resolve: "./src/modules/quote",
    },
  ],
})
```

Each object in the `modules` array has a `resolve` property, whose value is either a path to the module's directory, or an `npm` package’s name.

### Generate Migrations

Since data models represent tables in the database, you define how they're created in the database with migrations. A migration is a TypeScript or JavaScript file that defines database changes made by a module.

Learn more about migrations in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/modules#5-generate-migrations/index.html.md).

Medusa's CLI tool generates the migrations for you. To generate a migration for the Quote Module, run the following command in your Medusa application's directory:

```bash
npx medusa db:generate quote
```

The `db:generate` command of the Medusa CLI accepts the name of the module to generate the migration for. You'll now have a `migrations` directory under `src/modules/quote` that holds the generated migration.

![The directory structure of the Quote Module after generating the migration](https://res.cloudinary.com/dza7lstvk/image/upload/v1741076301/Medusa%20Resources/quote-6_adzf76.jpg)

Then, to reflect these migrations on the database, run the following command:

```bash
npx medusa db:migrate
```

The table for the `Quote` data model is now created in the database.

***

## Step 3: Define Links to Other Modules

When you defined the `Quote` data model, you added properties that store the ID of records managed by other modules. For example, the `customer_id` property stores the ID of the customer that requested the quote, but customers are managed by the [Customer Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/customer/index.html.md).

Medusa integrates modules into your application without implications or side effects by isolating modules from one another. This means you can't directly create relationships between data models in your module and data models in other modules.

Instead, Medusa provides the mechanism to define links between data models, and retrieve and manage linked records while maintaining module isolation. Links are useful to define associations between data models in different modules, or extend a model in another module to associate custom properties with it.

To learn more about module isolation, refer to the [Module Isolation documentation](https://docs.medusajs.com/docs/learn/fundamentals/modules/isolation/index.html.md).

In this step, you'll define the following links between the Quote Module's data model and data models in other modules:

1. `Quote` \<> `Cart` data model of the [Cart Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/cart/index.html.md): link quotes to the carts they were created from.
2. `Quote` \<> `Customer` data model of the [Customer Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/customer/index.html.md): link quotes to the customers who requested them.
3. `Quote` \<> `OrderChange` data model of the [Order Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/order/index.html.md): link quotes to the order changes that record adjustments made to the quote's draft order.
4. `Quote` \<> `Order` data model of the [Order Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/order/index.html.md): link quotes to their draft orders that are later converted to orders.

### Define Quote \<> Cart Link

You can define links between data models in a TypeScript or JavaScript file under the `src/links` directory. So, to define the link between the `Quote` and `Cart` data models, create the file `src/links/quote-cart.ts` with the following content:

![Directory structure after adding the quote-cart link](https://res.cloudinary.com/dza7lstvk/image/upload/v1741077395/Medusa%20Resources/quote-7_xrvodi.jpg)

```ts title="src/links/quote-cart.ts" highlights={quoteCartHighlights}
import { defineLink } from "@medusajs/framework/utils"
import QuoteModule from "../modules/quote"
import CartModule from "@medusajs/medusa/cart"

export default defineLink(
  {
    linkable: QuoteModule.linkable.quote.id,
    field: "cart_id",
  },
  CartModule.linkable.cart,
  {
    readOnly: true,
  }
)
```

You define a link using the `defineLink` function from the Modules SDK. It accepts three parameters:

1. An object indicating the first data model part of the link. A module has a special `linkable` property that contains link configurations for its data models. So, you can pass the link configurations for the `Quote` data model from the `QuoteModule` module, specifying that its `cart_id` property holds the ID of the linked record.
2. An object indicating the second data model part of the link. You pass the link configurations for the `Cart` data model from the `CartModule` module.
3. An optional object with additional configurations for the link. By default, Medusa creates a table in the database to represent the link you define. However, when you only want to retrieve the linked records without managing and storing the links, you can set the `readOnly` option to `true`.

You'll now be able to retrieve the cart that a quote was created from, as you'll see in later steps.

### Define Quote \<> Customer Link

Next, you'll define the link between the `Quote` and `Customer` data model of the Customer Module. So, create the file `src/links/quote-customer.ts` with the following content:

![Directory structure after adding the quote-customer link](https://res.cloudinary.com/dza7lstvk/image/upload/v1741078047/Medusa%20Resources/quote-8_bbngmh.jpg)

```ts title="src/links/quote-customer.ts"
import { defineLink } from "@medusajs/framework/utils"
import QuoteModule from "../modules/quote"
import CustomerModule from "@medusajs/medusa/customer"

export default defineLink(
  {
    linkable: QuoteModule.linkable.quote.id,
    field: "customer_id",
  },
  CustomerModule.linkable.customer,
  {
    readOnly: true,
  }
)
```

You define the link between the `Quote` and `Customer` data models in the same way as the `Quote` and `Cart` link. In the first object parameter of `defineLink`, you pass the linkable configurations of the `Quote` data model, specifying the `customer_id` property as the link field. In the second object parameter, you pass the linkable configurations of the `Customer` data model from the Customer Module. You also configure the link to be read-only.

### Define Quote \<> OrderChange Link

Next, you'll define the link between the `Quote` and `OrderChange` data model of the Order Module. So, create the file `src/links/quote-order-change.ts` with the following content:

![Directory structure after adding the quote-order-change link](https://res.cloudinary.com/dza7lstvk/image/upload/v1741078511/Medusa%20Resources/quote-11_faac5m.jpg)

```ts title="src/links/quote-order-change.ts"
import { defineLink } from "@medusajs/framework/utils"
import QuoteModule from "../modules/quote"
import OrderModule from "@medusajs/medusa/order"

export default defineLink(
  {
    linkable: QuoteModule.linkable.quote.id,
    field: "order_change_id",
  },
  OrderModule.linkable.orderChange,
  {
    readOnly: true,
  }
)
```

You define the link between the `Quote` and `OrderChange` data models in the same way as the previous links. You pass the linkable configurations of the `Quote` data model, specifying the `order_change_id` property as the link field. In the second object parameter, you pass the linkable configurations of the `OrderChange` data model from the Order Module. You also configure the link to be read-only.

### Define Quote \<> Order Link

Finally, you'll define the link between the `Quote` and `Order` data model of the Order Module. So, create the file `src/links/quote-order.ts` with the following content:

![Directory structure after adding the quote-order link](https://res.cloudinary.com/dza7lstvk/image/upload/v1741078607/Medusa%20Resources/quote-12_ixr2f7.jpg)

```ts title="src/links/quote-order.ts"
import { defineLink } from "@medusajs/framework/utils"
import QuoteModule from "../modules/quote"
import OrderModule from "@medusajs/medusa/order"

export default defineLink(
  {
    linkable: QuoteModule.linkable.quote.id,
    field: "draft_order_id",
  },
  {
    linkable: OrderModule.linkable.order.id,
    alias: "draft_order",
  },
  {
    readOnly: true,
  }
)
```

You define the link between the `Quote` and `Order` data models similar to the previous links. You pass the linkable configurations of the `Quote` data model, specifying the `draft_order_id` property as the link field.

In the second object parameter, you pass the linkable configurations of the `Order` data model from the Order Module. You also set an `alias` property to `draft_order`. This allows you later to retrieve the draft order of a quote with the `draft_order` alias rather than the default `order` alias. Finally, you configure the link to be read-only.

You've finished creating the links that allow you to retrieve data related to quotes. You'll see how to use these links in later steps.

***

## Step 4: Implement Create Quote Workflow

You're now ready to start implementing quote-management features. The first one you'll implement is the ability for customers to request a quote for a set of items in their cart.

To build custom commerce features in Medusa, you create a [workflow](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md). 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 an endpoint.

So, in this section, you'll learn how to create a workflow that creates a quote for a customer.

Learn more about workflows in the [Workflows documentation](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md).

The workflow will have the following steps:

- [useQueryGraphStep](https://docs.medusajs.com/references/helper-steps/useQueryGraphStep/index.html.md): Retrieve the cart that the customer wants a quote for.
- [useQueryGraphStep](https://docs.medusajs.com/references/helper-steps/useQueryGraphStep/index.html.md): Retrieve the customer requesting the quote.
- [createOrderWorkflow](https://docs.medusajs.com/references/medusa-workflows/createOrderWorkflow/index.html.md): Create the draft order for the quote.
- [beginOrderEditOrderWorkflow](https://docs.medusajs.com/references/medusa-workflows/beginOrderEditOrderWorkflow/index.html.md): Create the order change for the draft order.
- [createQuotesStep](#createQuotesStep): Create the quote for the customer.

The first four steps are provided by Medusa in its `@medusajs/medusa/core-flows` package. So, you only need to implement the `createQuotesStep` step.

### createQuotesStep

In the last step of the workflow, you'll create a quote for the customer using the Quote Module's service.

To create a step, create the file `src/workflows/steps/create-quotes.ts` with the following content:

![Directory structure after adding the create-quotes step](https://res.cloudinary.com/dza7lstvk/image/upload/v1741085446/Medusa%20Resources/quote-13_tv9i23.jpg)

```ts title="src/workflows/steps/create-quotes.ts" highlights={createQuotesStepHighlights}
import { createStep, StepResponse } from "@medusajs/framework/workflows-sdk"
import { QUOTE_MODULE } from "../../modules/quote"
import QueryModuleService from "../../modules/quote/service"

type StepInput = {
  draft_order_id: string;
  order_change_id: string;
  cart_id: string;
  customer_id: string;
}[]

export const createQuotesStep = createStep(
  "create-quotes",
  async (input: StepInput, { container }) => {
    const quoteModuleService: QueryModuleService = container.resolve(
      QUOTE_MODULE
    )

    const quotes = await quoteModuleService.createQuotes(input)

    return new StepResponse(
      quotes,
      quotes.map((quote) => quote.id)
    )
  }
)
```

You create a step with `createStep` from the Workflows SDK. It accepts two parameters:

1. The step's unique name, which is `create-quotes`.
2. An async function that receives two parameters:
   - The step's input, which is in this case an array of quotes to create.
   - An object that has properties including the [Medusa container](https://docs.medusajs.com/docs/learn/fundamentals/medusa-container/index.html.md), which is a registry of Framework and commerce tools that you can access in the step.

In the step function, you resolve the Quote Module's service from the Medusa container using the `resolve` method of the container, passing it the module's name as a parameter.

Then, you create the quotes using the `createQuotes` method. As you remember, the Quote Module's service extends the `MedusaService` which generates data-management methods for you.

A step function must return a `StepResponse` instance. The `StepResponse` constructor accepts two parameters:

1. The step's output, which is the quotes created.
2. Data to pass to the step's compensation function, which you'll add next.

#### Add Compensation to Step

A step can have a compensation function that undoes the actions performed in a step. Then, if an error occurs during the workflow's execution, the compensation functions of executed steps are called to roll back the changes. This mechanism ensures data consistency in your application, especially as you integrate external systems.

To add a compensation function to a step, pass it as a third-parameter to `createStep`:

```ts title="src/workflows/steps/create-quotes.ts"
export const createQuotesStep = createStep(
  // ...
  async (quoteIds, { container }) => {
    if (!quoteIds) {
      return
    }
    
    const quoteModuleService: QueryModuleService = container.resolve(
      QUOTE_MODULE
    )

    await quoteModuleService.deleteQuotes(quoteIds)
  }
)
```

The compensation function accepts two parameters:

1. The data passed from the step in the second parameter of `StepResponse`, which in this case is an array of quote IDs.
2. An object that has properties including the [Medusa container](https://docs.medusajs.com/docs/learn/fundamentals/medusa-container/index.html.md).

In the compensation function, you resolve the Quote Module's service from the Medusa container and call the `deleteQuotes` method to delete the quotes created in the step.

### createRequestForQuoteWorkflow

You can now create the workflow using the steps provided by Medusa and your custom step.

To create the workflow, create the file `src/workflows/create-request-for-quote.ts` with the following content:

```ts title="src/workflows/create-request-for-quote.ts" highlights={createRequestForQuoteHighlights} collapsibleLines="1-20" expandButtonLabel="Show Imports"
import {
  beginOrderEditOrderWorkflow,
  createOrderWorkflow,
  CreateOrderWorkflowInput,
  useQueryGraphStep,
} from "@medusajs/medusa/core-flows"
import { OrderStatus } from "@medusajs/framework/utils"
import {
  createWorkflow,
  transform,
  WorkflowResponse,
} from "@medusajs/workflows-sdk"
import { CreateOrderLineItemDTO } from "@medusajs/framework/types"
import { createQuotesStep } from "./steps/create-quotes"

type WorkflowInput = {
  cart_id: string;
  customer_id: string;
};

export const createRequestForQuoteWorkflow = createWorkflow(
  "create-request-for-quote",
  (input: WorkflowInput) => {
    // @ts-ignore
    const { data: carts } = useQueryGraphStep({
      entity: "cart",
      fields: [
        "id",
        "sales_channel_id",
        "currency_code",
        "region_id",
        "customer.id",
        "customer.email",
        "shipping_address.*",
        "billing_address.*",
        "items.*",
        "shipping_methods.*",
        "promotions.code",
      ],
      filters: { id: input.cart_id },
      options: {
        throwIfKeyNotFound: true,
      },
    })

    const { data: customers } = useQueryGraphStep({
      entity: "customer",
      fields: ["id", "customer"],
      filters: { id: input.customer_id },
      options: {
        throwIfKeyNotFound: true,
      },
    }).config({ name: "customer-query" })

    // TODO create order
  }
)
```

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 having the ID of the customer requesting the quote, and the ID of their cart.

In the workflow's constructor function, you use `useQueryGraphStep` to retrieve the cart and customer details using the IDs passed as an input to the workflow.

`useQueryGraphStep` uses [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), whic allows you to retrieve data across modules. For example, in the above snippet you're retrieving the cart's promotions, which are managed in the [Promotion Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/promotion/index.html.md), by passing `promotions.code` to the `fields` array.

Next, you want to create the draft order for the quote. Replace the `TODO` in the workflow with the following:

```ts title="src/workflows/create-request-for-quote.ts"
const orderInput = transform({ carts, customers }, ({ carts, customers }) => {
  return {
    is_draft_order: true,
    status: OrderStatus.DRAFT,
    sales_channel_id: carts[0].sales_channel_id || undefined,
    email: customers[0].email || undefined,
    customer_id: customers[0].id || undefined,
    billing_address: carts[0].billing_address,
    shipping_address: carts[0].shipping_address,
    items: carts[0].items as CreateOrderLineItemDTO[] || [],
    region_id: carts[0].region_id || undefined,
    promo_codes: carts[0].promotions?.map((promo) => promo?.code),
    currency_code: carts[0].currency_code,
    shipping_methods: carts[0].shipping_methods || [],
  } as CreateOrderWorkflowInput
})

const draftOrder = createOrderWorkflow.runAsStep({
  input: orderInput,
})

// TODO create order change
```

You first prepare the order's details using `transform` from the Workflows SDK. Since Medusa creates an internal representation of the workflow's constructor before any data actually has a value, you can't manipulate data directly in the function. So, Medusa provides utilities like `transform` to manipulate data instead. You can learn more in the [transform variables](https://docs.medusajs.com/docs/learn/fundamentals/workflows/variable-manipulation/index.html.md) documentation.

Then, you create the draft order using the `createOrderWorkflow` workflow which you imported from `@medusajs/medusa/core-flows`. The workflow creates and returns the created order.

After that, you want to create an order change for the draft order. This will allow the admin later to make edits to the draft order, such as updating the prices or quantities of the items in the order.

Replace the `TODO` with the following:

```ts title="src/workflows/create-request-for-quote.ts"
const orderEditInput = transform({ draftOrder }, ({ draftOrder }) => {
  return {
    order_id: draftOrder.id,
    description: "",
    internal_note: "",
    metadata: {},
  }
})

const changeOrder = beginOrderEditOrderWorkflow.runAsStep({
  input: orderEditInput,
})

// TODO create quote
```

You prepare the order change's details using `transform` and then create the order change using the `beginOrderEditOrderWorkflow` workflow which is provided by Medusa.

Finally, you want to create the quote for the customer and return it. Replace the last `TODO` with the following:

```ts title="src/workflows/create-request-for-quote.ts"
const quoteData = transform({
  draftOrder,
  carts,
  customers,
  changeOrder,
}, ({ draftOrder, carts, customers, changeOrder }) => {
  return {
    draft_order_id: draftOrder.id,
    cart_id: carts[0].id,
    customer_id: customers[0].id,
    order_change_id: changeOrder.id,
  }
})

const quotes = createQuotesStep([
  quoteData,
])

return new WorkflowResponse({ quote: quotes[0] })
```

Similar to before, you prepare the quote's details using `transform`. Then, you create the quote using the `createQuotesStep` you implemented earlier.

A workflow must return an instance of `WorkflowResponse`. The `WorkflowResponse` constructor accepts the workflow's output as a parameter, which is an object holding the created quote in this case.

In the next step, you'll learn how to execute the workflow when a customer requests a quote.

***

## Step 5: Create Quote API Route

Now that you have the logic to create a quote for a customer, you need to expose it so that frontend clients, such as a storefront, can use it. You do this by creating an [API route](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/index.html.md).

An API Route is an endpoint that exposes commerce features to external applications and clients, such as storefronts. You'll create an API route at the path `/store/customers/me/quotes` that executes the workflow from the previous step.

Learn more about API routes in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/index.html.md).

### Implement API Route

An API route is created in a `route.ts` file under a sub-directory of the `src/api` directory. The path of the API route is the file's path relative to `src/api`.

By default, all routes starting with `/store/customers/me` require the customer to be authenticated. So, you'll be creating the API route at `/store/customers/me/quotes`.

To create the API route, create the file `src/api/store/customers/me/quotes/route.ts` with the following content:

![Directory structure after adding the store/quotes route](https://res.cloudinary.com/dza7lstvk/image/upload/v1741086995/Medusa%20Resources/quote-14_meo0yo.jpg)

```ts title="src/api/store/customers/me/quotes/route.ts" highlights={createQuoteApiHighlights} collapsibleLines="1-9" expandButtonLabel="Show Imports"
import {
  AuthenticatedMedusaRequest,
  MedusaResponse,
} from "@medusajs/framework/http"
import { ContainerRegistrationKeys } from "@medusajs/framework/utils"
import { 
  createRequestForQuoteWorkflow,
} from "../../../../../workflows/create-request-for-quote"

type CreateQuoteType = {
  cart_id: string;
}

export const POST = async (
  req: AuthenticatedMedusaRequest<CreateQuoteType>,
  res: MedusaResponse
) => {
  const {
    result: { quote: createdQuote },
  } = await createRequestForQuoteWorkflow(req.scope).run({
    input: {
      ...req.validatedBody,
      customer_id: req.auth_context.actor_id,
    },
  })

  const query = req.scope.resolve(
    ContainerRegistrationKeys.QUERY
  )

  const {
    data: [quote],
  } = await query.graph(
    {
      entity: "quote",
      fields: req.queryConfig.fields,
      filters: { id: createdQuote.id },
    },
    { throwIfKeyNotFound: true }
  )

  return res.json({ quote })
}
```

Since you export a `POST` function in this file, you're exposing a `POST` API route at `/store/customers/me/quotes`. The route handler function accepts two parameters:

1. A request object with details and context on the request, such as body parameters or authenticated customer details.
2. A response object to manipulate and send the response.

`AuthenticatedMedusaRequest` accepts the request body's type as a type argument.

In the route handler function, you create the quote using the [createRequestForQuoteWorkflow](#createrequestforquoteworkflow) from the previous step. Then, you resolve Query from the Medusa container, which is available in the request object's `req.scope` property.

You use Query to retrieve the Quote with its fields and linked records, which you'll learn how to specify soon. Finally, you send the quote as a response.

### Add Validation Schema

The API route accepts the cart ID as a request body parameter. So, it's important to validate the body of a request before executing the route's handler. You can do this by specifying a validation schema in a middleware for the API route.

In Medusa, you create validation schemas using [Zod](https://zod.dev/) in a TypeScript file under the `src/api` directory. So, create the file `src/api/store/validators.ts` with the following content:

![Directory structure after adding the validators file](https://res.cloudinary.com/dza7lstvk/image/upload/v1741089363/Medusa%20Resources/quote-15_iy6jem.jpg)

```ts title="src/api/store/validators.ts"
import { z } from "zod"

export type CreateQuoteType = z.infer<typeof CreateQuote>;
export const CreateQuote = z
  .object({
    cart_id: z.string().min(1),
  })
  .strict()
```

You define a `CreateQuote` schema using Zod that specifies the `cart_id` parameter as a required string.

You also export a type inferred from the schema. So, go back to `src/api/store/customers/me/quotes/route.ts` and replace the implementation of `CreateQuoteType` to import the type from the `validators.ts` file instead:

```ts title="src/api/store/customers/me/quotes/route.ts"
// other imports...
// add the following import
import { CreateQuoteType } from "../../../validators"

// remove CreateQuoteType definition

export const POST = async (
  // keep type argument the same
  req: AuthenticatedMedusaRequest<CreateQuoteType>,
  res: MedusaResponse
) => {
  // ...
}
```

### Apply Validation Schema Middleware

Now that you have the validation schema, you need to add the middleware that ensures the request body is validated before the route handler is executed. A middleware is a function executed when a request is sent to an API Route. It's executed before the route handler.

Learn more about middleware in the [Middlewares documentation](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/middlewares/index.html.md).

Middlewares are created in the `src/api/middlewares.ts` file. So create the file `src/api/middlewares.ts` with the following content:

![Directory structure after adding the store middlewares file](https://res.cloudinary.com/dza7lstvk/image/upload/v1741089625/Medusa%20Resources/quote-16_oryolz.jpg)

```ts title="src/api/middlewares.ts"
import { 
  defineMiddlewares, 
  validateAndTransformBody,
} from "@medusajs/framework/http"
import { CreateQuote } from "./store/validators"

export default defineMiddlewares({
  routes: [
    {
      method: ["POST"],
      matcher: "/store/customers/me/quotes",
      middlewares: [
        validateAndTransformBody(CreateQuote),
      ],
    },
  ],
})
```

To export the middlewares, you use the `defineMiddlewares` function. It accepts an object having a `routes` property, whose value is an array of middleware route objects. Each middleware route object has the following properties:

- `method`: The HTTP methods the middleware applies to, which is in this case `POST`.
- `matcher`: The path of the route the middleware applies to.
- `middlewares`: An array of middleware functions to apply to the route. In this case, you apply the `validateAndTransformBody` middleware, which accepts a Zod schema as a parameter and validates that a request's body matches the schema. If not, it throws and returns an error.

### Specify Quote Fields to Retrieve

In the route handler you just created, you specified what fields to retrieve in a quote using the `req.queryConfig.fields` property. The `req.queryConfig` field holds query configurations indicating the default fields to retrieve when using Query to return data in a request. This is useful to unify the returned data structure across different routes, or to allow clients to specify the fields they want to retrieve.

To add the Query configurations, you'll first create a file that exports the default fields to retrieve for a quote, then apply them in a `validateAndTransformQuery` middleware.

Learn more about configuring Query for requests in the [Request Query Configurations documentation](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query#request-query-configurations/index.html.md).

Create the file `src/api/store/customers/me/quotes/query-config.ts` with the following content:

![Directory structure after adding the query-config file](https://res.cloudinary.com/dza7lstvk/image/upload/v1741090067/Medusa%20Resources/quote-17_n6xsdb.jpg)

```ts title="src/api/store/customers/me/quotes/query-config.ts"
export const quoteFields = [
  "id",
  "status",
  "*customer",
  "cart.id",
  "draft_order.id",
  "draft_order.currency_code",
  "draft_order.display_id",
  "draft_order.region_id",
  "draft_order.status",
  "draft_order.version",
  "draft_order.summary",
  "draft_order.total",
  "draft_order.subtotal",
  "draft_order.tax_total",
  "draft_order.order_change",
  "draft_order.discount_total",
  "draft_order.discount_tax_total",
  "draft_order.original_total",
  "draft_order.original_tax_total",
  "draft_order.item_total",
  "draft_order.item_subtotal",
  "draft_order.item_tax_total",
  "draft_order.original_item_total",
  "draft_order.original_item_subtotal",
  "draft_order.original_item_tax_total",
  "draft_order.shipping_total",
  "draft_order.shipping_subtotal",
  "draft_order.shipping_tax_total",
  "draft_order.original_shipping_tax_total",
  "draft_order.original_shipping_subtotal",
  "draft_order.original_shipping_total",
  "draft_order.created_at",
  "draft_order.updated_at",
  "*draft_order.items",
  "*draft_order.items.tax_lines",
  "*draft_order.items.adjustments",
  "*draft_order.items.variant",
  "*draft_order.items.variant.product",
  "*draft_order.items.detail",
  "*draft_order.payment_collections",
  "*order_change.actions",
]

export const retrieveStoreQuoteQueryConfig = {
  defaults: quoteFields,
  isList: false,
}

export const listStoreQuoteQueryConfig = {
  defaults: quoteFields,
  isList: true,
}
```

You export two objects:

- `retrieveStoreQuoteQueryConfig`: Specifies the default fields to retrieve for a single quote.
- `listStoreQuoteQueryConfig`: Specifies the default fields to retrieve for a list of quotes, which you'll use later.

Notice that in the fields retrieved, you specify linked records such as `customer` and `draft_order`. You can do this because you've defined links between the `Quote` data model and these data models previously.

For simplicity, this guide will apply the `listStoreQuoteQueryConfig` to all routes starting with `/store/customers/me/quotes`. However, you should instead apply `retrieveStoreQuoteQueryConfig` to routes that retrieve a single quote, and `listStoreQuoteQueryConfig` to routes that retrieve a list of quotes.

Next, you'll define a Zod schema that allows client applications to specify the fields they want to retrieve in a quote as a query parameter. In `src/api/store/validators.ts`, add the following schema:

```ts title="src/api/store/validators.ts"
// other imports...
import { createFindParams } from "@medusajs/medusa/api/utils/validators"

// ...

export type GetQuoteParamsType = z.infer<typeof GetQuoteParams>;
export const GetQuoteParams = createFindParams({
  limit: 15,
  offset: 0,
})
```

You create a `GetQuoteParams` schema using the `createFindParams` utility from Medusa. This utility creates a schema that allows clients to specify query parameters such as:

- `fields`: The fields to retrieve in a quote.
- `limit`: The maximum number of quotes to retrieve. This is useful for routes that return a list of quotes.
- `offset`: The number of quotes to skip before retrieving the next set of quotes. This is useful for routes that return a list of quotes.
- `order`: The fields to sort the quotes by either in ascending or descending order. This is useful for routes that return a list of quotes.

Finally, you'll apply these Query configurations in a middleware. So, add the following middleware in `src/api/middlewares.ts`:

```ts title="src/api/store/middlewares.ts"
// other imports...
import { GetQuoteParams } from "./store/validators"
import { validateAndTransformQuery } from "@medusajs/framework/http"
import { listStoreQuoteQueryConfig } from "./store/customers/me/quotes/query-config"

export default defineMiddlewares({
  routes: [
    // ...
    {
      matcher: "/store/customers/me/quotes*",
      middlewares: [
        validateAndTransformQuery(
          GetQuoteParams, 
          listStoreQuoteQueryConfig
        ),
      ],
    },
  ],
})
```

You apply the `validateAndTransformQuery` middleware on all routes starting with `/store/customers/me/quotes`. The `validateAndTransformQuery` middleware that Medusa provides accepts two parameters:

1. A Zod schema that specifies how to validate the query parameters of incoming requests.
2. A Query configuration object that specifies the default fields to retrieve in the response, which you defined in the `query-config.ts` file.

The create quote route is now ready to be used by clients to create quotes for customers.

### Test the API Route

To test out the API route, start the Medusa application:

```bash npm2yarn
npm run dev
```

Then, open the Medusa Admin dashboard at `http://localhost:9000/app` and login using the credentials you set up earlier.

#### Retrieve Publishable API Key

All requests sent to routes starting with `/store` must have a publishable API key in their header. This ensures that the request is scoped to a specific sales channel of your storefront.

To learn more about publishable API keys, refer to the [Publishable API Key documentation](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/sales-channel/publishable-api-keys/index.html.md).

To retrieve the publishable API key from the Medusa Admin, refer to [this user guide](https://docs.medusajs.com/user-guide/settings/developer/publishable-api-keys/index.html.md).

#### Retrieve Customer Authentication Token

As mentioned before, the API route you added requires the customer to be authenticated. So, you'll first create a customer, then retrieve their authentication token to use in the request.

Before creating the customer, retrieve a registration token using the [Retrieve Registration JWT Token API route](https://docs.medusajs.com/api/store#auth_postactor_typeauth_provider_register):

```bash
curl -X POST 'http://localhost:9000/auth/customer/emailpass/register' \
-H 'Content-Type: application/json' \
--data-raw '{
  "email": "customer@gmail.com",
  "password": "supersecret"
}'
```

Make sure to replace the email and password with the credentials you want.

Then, register the customer using the [Create Customer API route](https://docs.medusajs.com/api/store#customers_postcustomers):

```bash
curl -X POST 'http://localhost:9000/store/customers' \
-H 'Authorization: Bearer {token}' \
-H 'Content-Type: application/json' \
-H 'x-publishable-api-key: {your_publishable_api_key}' \
--data-raw '{
  "email": "customer@gmail.com"
}'
```

Make sure to replace:

- `{token}` with the registration token you received from the previous request.
- `{your_publishable_api_key}` with the publishable API key you retrieved from the Medusa Admin.

Also, if you changed the email in the first request, make sure to change it here as well.

The customer is now registered. Lastly, you need to retrieve its authenticated token by sending a request to the [Authenticate Customer API route](https://docs.medusajs.com/api/store#auth_postactor_typeauth_provider):

```bash
curl -X POST 'http://localhost:9000/auth/customer/emailpass' \
-H 'Content-Type: application/json' \
--data-raw '{
  "email": "customer@gmail.com",
  "password": "supersecret"
}'
```

Copy the returned token to use it in the next requests.

#### Create Cart

The customer needs a cart with an item before creating the quote.

A cart requires a region ID. You can retrieve a region ID using the [List Regions API route](https://docs.medusajs.com/api/store#regions_getregions):

```bash
curl 'http://localhost:9000/store/regions' \
-H 'x-publishable-api-key: {your_publishable_api_key}'
```

Make sure to replace the `{your_publishable_api_key}` with the publishable API key you retrieved from the Medusa Admin.

Then, create a cart for the customer using the [Create Cart API route](https://docs.medusajs.com/api/store#carts_postcarts):

```bash
curl -X POST 'http://localhost:9000/store/carts' \
-H 'Authorization: Bearer {token}' \
-H 'Content-Type: application/json' \
-H 'x-publishable-api-key: {your_publishable_api_key}' \
--data '{
    "region_id": "{region_id}"
}'
```

Make sure to replace:

- `{token}` with the authentication token you received from the previous request.
- `{your_publishable_api_key}` with the publishable API key you retrieved from the Medusa Admin.
- `{region_id}` with the region ID you retrieved from the previous request.

This will create and return a cart. Copy its ID for the next request.

You now need to add a product variant to the cart. You can retrieve a product variant ID using the [List Products API route](https://docs.medusajs.com/api/store#products_getproducts):

```bash
curl 'http://localhost:9000/store/products' \
-H 'x-publishable-api-key: {your_publishable_api_key}'
```

Make sure to replace the `{your_publishable_api_key}` with the publishable API key you retrieved from the Medusa Admin.

Copy the ID of a variant in a product from the response.

Finally, to add the product variant to the cart, use the [Add Item to Cart API route](https://docs.medusajs.com/api/store#carts_postcartsidlineitems):

```bash
curl -X POST 'http://localhost:9000/store/carts/{id}/line-items' \
-H 'Authorization: Bearer {token}' \
-H 'Content-Type: application/json' \
-H 'x-publishable-api-key: {your_publishable_api_key}' \
--data-raw '{
  "variant_id": "{variant_id}",
  "quantity": 1,
}'
```

Make sure to replace:

- `{id}` with the cart ID you retrieved previously.
- `{token}` with the authentication token you retrieved previously.
- `{your_publishable_api_key}` with the publishable API key you retrieved from the Medusa Admin.
- `{variant_id}` with the product variant ID you retrieved in the previous request.

This adds the product variant to the cart. You can now use the cart to create a quote.

For more accurate totals and processing of the quote's draft order, you should:

- Add shipping and billing addresses by [updating the cart](https://docs.medusajs.com/api/store#carts_postcartsid).
- [Choose a shipping method](https://docs.medusajs.com/api/store#carts_postcartsidshippingmethods) for the cart.
- [Create a payment collection](https://docs.medusajs.com/api/store#payment-collections_postpaymentcollections) for the cart.
- [Initialize payment session](https://docs.medusajs.com/api/store#payment-collections_postpaymentcollectionsidpaymentsessions) in the payment collection.

You can also learn how to build a checkout experience in a storefront by following [this storefront development guide](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/storefront-development/checkout/index.html.md). It's not specific to quote management, so you'll need to change the last step to create a quote instead of an order.

#### Create Quote

To create a quote for the customer, send a request to the `/store/customers/me/quotes` route you created:

```bash
curl -X POST 'http://localhost:9000/store/customers/me/quotes' \
-H 'Authorization: Bearer {token}' \
-H 'Content-Type: application/json' \
-H 'x-publishable-api-key: {your_publishable_api_key}' \
--data-raw '{
  "cart_id": "{cart_id}"
}'
```

Make sure to replace:

- `{token}` with the authentication token you retrieved previously.
- `{your_publishable_api_key}` with the publishable API key you retrieved from the Medusa Admin.
- `{cart_id}` with the ID of the customer's cart.

This will create a quote for the customer and you'll receive its details in the response.

***

## Step 6: List Quotes API Route

After the customer creates a quote, the admin user needs to view these quotes to manage them. In this step, you'll create the API route to list quotes for the admin user. Then, in the next step, you'll customize the Medusa Admin dashboard to display these quotes.

The process of creating this API route will be somewhat similar to the previous route you created. You'll create the route, define the query configurations, and apply them in a middleware.

### Implement API Route

To create the API route, create the file `src/api/admin/quotes/route.ts` with the following content:

![Directory structure after adding the admin quotes route](https://res.cloudinary.com/dza7lstvk/image/upload/v1741094735/Medusa%20Resources/quote-18_uvwqt6.jpg)

```ts title="src/api/admin/quotes/route.ts" highlights={listQuotesHighlights}
import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http"
import { ContainerRegistrationKeys } from "@medusajs/framework/utils"

export const GET = async (
  req: MedusaRequest,
  res: MedusaResponse
) => {
  const query = req.scope.resolve(ContainerRegistrationKeys.QUERY)

  const { data: quotes, metadata } = await query.graph({
    entity: "quote",
    ...req.queryConfig,
  })

  res.json({
    quotes,
    count: metadata!.count,
    offset: metadata!.skip,
    limit: metadata!.take,
  })
}
```

You export a `GET` function in this file, which exposes a `GET` API route at `/admin/quotes`.

In the route handler function, you resolve Query from the Medusa container and use it to retrieve the list of quotes. Similar to before, you use `req.queryConfig` to specify the fields to retrieve in the response.

`req.queryConfig` also includes pagination parameters, such as `limit`, `offset`, and `count`, and they're returned in the `metadata` property of Query's result. You return the pagination details and the list of quotes in the response.

Learn more about paginating Query results in the [Query documentation](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query#apply-pagination/index.html.md).

### Add Query Configurations

Similar to before, you need to specify the default fields to retrieve in a quote and apply them in a middleware for this new route.

Since this is an admin route, create the file `src/api/admin/quotes/query-config.ts` with the following content:

![Directory structure after adding the query-config file](https://res.cloudinary.com/dza7lstvk/image/upload/v1741095492/Medusa%20Resources/quote-19_xca6aq.jpg)

```ts title="src/api/admin/quotes/query-config.ts"
export const quoteFields = [
  "id",
  "status",
  "created_at",
  "updated_at",
  "*customer",
  "cart.id",
  "draft_order.id",
  "draft_order.currency_code",
  "draft_order.display_id",
  "draft_order.region_id",
  "draft_order.status",
  "draft_order.version",
  "draft_order.summary",
  "draft_order.total",
  "draft_order.subtotal",
  "draft_order.tax_total",
  "draft_order.order_change",
  "draft_order.discount_total",
  "draft_order.discount_tax_total",
  "draft_order.original_total",
  "draft_order.original_tax_total",
  "draft_order.item_total",
  "draft_order.item_subtotal",
  "draft_order.item_tax_total",
  "draft_order.original_item_total",
  "draft_order.original_item_subtotal",
  "draft_order.original_item_tax_total",
  "draft_order.shipping_total",
  "draft_order.shipping_subtotal",
  "draft_order.shipping_tax_total",
  "draft_order.original_shipping_tax_total",
  "draft_order.original_shipping_subtotal",
  "draft_order.original_shipping_total",
  "draft_order.created_at",
  "draft_order.updated_at",
  "*draft_order.items",
  "*draft_order.items.tax_lines",
  "*draft_order.items.adjustments",
  "*draft_order.items.variant",
  "*draft_order.items.variant.product",
  "*draft_order.items.detail",
  "*order_change.actions",
]

export const retrieveAdminQuoteQueryConfig = {
  defaults: quoteFields,
  isList: false,
}

export const listAdminQuoteQueryConfig = {
  defaults: quoteFields,
  isList: true,
}
```

You export two objects: `retrieveAdminQuoteQueryConfig` and `listAdminQuoteQueryConfig`, which specify the default fields to retrieve for a single quote and a list of quotes, respectively.

For simplicity, this guide will apply the `listAdminQuoteQueryConfig` to all routes starting with `/admin/quotes`. However, you should instead apply `retrieveAdminQuoteQueryConfig` to routes that retrieve a single quote, and `listAdminQuoteQueryConfig` to routes that retrieve a list of quotes.

Next, you'll define a Zod schema that allows client applications to specify the fields to retrieve and pagination fields as a query parameter. Create the file `src/api/admin/validators.ts` with the following content:

![Directory structure after adding the admin validators file](https://res.cloudinary.com/dza7lstvk/image/upload/v1741095771/Medusa%20Resources/quote-20_iygrip.jpg)

```ts title="src/api/admin/validators.ts"
import {
  createFindParams,
} from "@medusajs/medusa/api/utils/validators"

export const AdminGetQuoteParams = createFindParams({
  limit: 15,
  offset: 0,
})
  .strict()
```

You define the `AdminGetQuoteParams` schema using the `createFindParams` utility from Medusa. The schema allows clients to specify query parameters such as:

- `fields`: The fields to retrieve in a quote.
- `limit`: The maximum number of quotes to retrieve.
- `offset`: The number of quotes to skip before retrieving the next set of quotes.
- `order`: The fields to sort the quotes by either in ascending or descending order.

Finally, you need to apply the `validateAndTransformQuery` middleware on this route. So, add the following to `src/api/middlewares.ts`:

```ts title="src/api/middlewares.ts"
// other imports...
import { AdminGetQuoteParams } from "./admin/quotes/validators"
import { listAdminQuoteQueryConfig } from "./admin/quotes/query-config"

export default defineMiddlewares({
  routes: [
    // ...
    {
      matcher: "/admin/quotes*",
      middlewares: [
        validateAndTransformQuery(
          AdminGetQuoteParams,
          listAdminQuoteQueryConfig
        ),
      ],
    },
  ],
})
```

You add the `validateAndTransformQuery` middleware to all routes starting with `/admin/quotes`. It validates the query parameters and sets the Query configurations based on the defaults you defined and the passed query parameters.

Your API route is now ready for use. You'll test it in the next step by customizing the Medusa Admin dashboard to display the quotes.

***

## Step 7: List Quotes Route in Medusa Admin

Now that you have the API route to retrieve the list of quotes, you want to show these quotes to the admin user in the Medusa Admin dashboard. The Medusa Admin is customizable, allowing you to add new pages as UI routes.

A UI route is a React component that specifies the content to be shown in a new page in the Medusa Admin dashboard. You'll create a UI route to display the list of quotes in the Medusa Admin.

Learn more about UI routes in the [UI Routes documentation](https://docs.medusajs.com/docs/learn/fundamentals/admin/ui-routes/index.html.md).

### Configure JS SDK

Medusa provides a [JS SDK](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/js-sdk/index.html.md) that you can use to send requests to the Medusa server from any client application, including your Medusa Admin customizations.

The JS SDK is installed by default in your Medusa application. To configure it, create the file `src/admin/lib/sdk.ts` with the following content:

![Directory structure after adding the sdk file](https://res.cloudinary.com/dza7lstvk/image/upload/v1741098137/Medusa%20Resources/quote-23_plm90s.jpg)

```ts title="src/admin/lib/sdk.ts"
import Medusa from "@medusajs/js-sdk"

export const sdk = new Medusa({
  baseUrl: import.meta.env.VITE_BACKEND_URL || "/",
  debug: import.meta.env.DEV,
  auth: {
    type: "session",
  },
})
```

You create an instance of the JS SDK using the `Medusa` class from the `@medusajs/js-sdk` package. You pass it an object having the following properties:

- `baseUrl`: The base URL of the Medusa server.
- `debug`: A boolean indicating whether to log debug information.
- `auth`: An object specifying the authentication type. When using the JS SDK for admin customizations, you use the `session` authentication type.

### Add Admin Types

In your development, you'll need types that represents the data you'll retrieve from the Medusa server. So, create the file `src/admin/types.ts` with the following content:

![Directory structure after adding the admin type](https://res.cloudinary.com/dza7lstvk/image/upload/v1741098478/Medusa%20Resources/quote-25_jr79pa.jpg)

```ts title="src/admin/types.ts"
import {
  AdminCustomer,
  AdminOrder,
  AdminUser,
  FindParams,
  PaginatedResponse,
  StoreCart,
} from "@medusajs/framework/types"

export type AdminQuote = {
  id: string;
  status: string;
  draft_order_id: string;
  order_change_id: string;
  cart_id: string;
  customer_id: string;
  created_at: string;
  updated_at: string;
  draft_order: AdminOrder;
  cart: StoreCart;
  customer: AdminCustomer
};

export interface QuoteQueryParams extends FindParams {}

export type AdminQuotesResponse = PaginatedResponse<{
  quotes: AdminQuote[];
}>

export type AdminQuoteResponse = {
  quote: AdminQuote;
};
```

You define the following types:

- `AdminQuote`: Represents a quote.
- `QuoteQueryParams`: Represents the query parameters that can be passed when retrieving qoutes.
- `AdminQuotesResponse`: Represents the response when retrieving a list of quotes.
- `AdminQuoteResponse`: Represents the response when retrieving a single quote, which you'll implement later in this guide.

You'll use these types in the rest of the customizations.

### Create useQuotes Hook

When sending requests to the Medusa server from your admin customizations, it's recommended to use [Tanstack Query](https://tanstack.com/query/latest), allowing you to benefit from its caching and data fetching capabilities.

So, you'll create a `useQuotes` hook that uses Tanstack Query and the JS SDK to fetch the list of quotes from the Medusa server.

Create the file `src/admin/hooks/quotes.tsx` with the following content:

![Directory structure after adding the hooks quotes file](https://res.cloudinary.com/dza7lstvk/image/upload/v1741098244/Medusa%20Resources/quote-24_apdpem.jpg)

```ts title="src/admin/hooks/quotes.tsx"
import { ClientHeaders, FetchError } from "@medusajs/js-sdk"
import {
  QuoteQueryParams,
  AdminQuotesResponse,
} from "../types"
import {
  QueryKey,
  useQuery,
  UseQueryOptions,
} from "@tanstack/react-query"
import { sdk } from "../lib/sdk"

export const useQuotes = (
  query: QuoteQueryParams,
  options?: UseQueryOptions<
    AdminQuotesResponse,
    FetchError,
    AdminQuotesResponse,
    QueryKey
  >
) => {
  const fetchQuotes = (query: QuoteQueryParams, headers?: ClientHeaders) =>
    sdk.client.fetch<AdminQuotesResponse>(`/admin/quotes`, {
      query,
      headers,
    })

  const { data, ...rest } = useQuery({
    ...options,
    queryFn: () => fetchQuotes(query)!,
    queryKey: ["quote", "list"],
  })

  return { ...data, ...rest }
}
```

You define a `useQuotes` hook that accepts query parameters and optional options as a parameter. In the hook, you use the JS SDK's `client.fetch` method to retrieve the quotes from the `/admin/quotes` route.

You return the fetched data from the Medusa server. You'll use this hook in the UI route.

### Create Quotes UI Route

You can now create the UI route that will show a new page in the Medusa Admin with the list of quotes.

UI routes are created in a `page.tsx` file under the `src/admin/routes` directory. The path of the UI route is the file's path relative to `src/admin/routes`.

So, to add the UI route at `/quotes` in the Medusa Admin, create the file `src/admin/routes/quotes/page.tsx` with the following content:

![Directory structure after adding the Quotes UI route file](https://res.cloudinary.com/dza7lstvk/image/upload/v1741099122/Medusa%20Resources/quote-26_qrqzut.jpg)

```tsx title="src/admin/routes/quotes/page.tsx"
import { defineRouteConfig } from "@medusajs/admin-sdk"
import { DocumentText } from "@medusajs/icons"
import { 
  Container, createDataTableColumnHelper, DataTable, 
  DataTablePaginationState, Heading, Toaster, useDataTable,
} from "@medusajs/ui"
import { useNavigate } from "react-router-dom"
import { useQuotes } from "../../hooks/quotes"
import { AdminQuote } from "../../types"
import { useState } from "react"

const Quotes = () => {
  // TODO implement page content
}

export const config = defineRouteConfig({
  label: "Quotes",
  icon: DocumentText,
})

export default Quotes
```

The route file must export a React component that implements the content of the page. To show a link to the route in the sidebar, you can also export a configuation object created with `defineRouteConfig` that specifies the label and icon of the route in the Medusa Admin sidebar.

In the `Quotes` component, you'll show a table of quotes using the [DataTable component](https://docs.medusajs.com/ui/components/data-table/index.html.md) from Medusa UI. This componet requires you first define the columns of the table.

To define the table's columns, add in the same file and before the `Quotes` component the following:

```tsx title="src/admin/routes/quotes/page.tsx"
const StatusTitles: Record<string, string> = {
  accepted: "Accepted",
  customer_rejected: "Customer Rejected",
  merchant_rejected: "Merchant Rejected",
  pending_merchant: "Pending Merchant",
  pending_customer: "Pending Customer",
}

const columnHelper = createDataTableColumnHelper<AdminQuote>()

const columns = [
  columnHelper.accessor("draft_order.display_id", {
    header: "ID",
  }),
  columnHelper.accessor("status", {
    header: "Status",
    cell: ({ getValue }) => StatusTitles[getValue()],
  }),
  columnHelper.accessor("customer.email", {
    header: "Email",
  }),
  columnHelper.accessor("draft_order.customer.first_name", {
    header: "First Name",
  }),
  columnHelper.accessor("draft_order.customer.company_name", {
    header: "Company Name",
  }),
  columnHelper.accessor("draft_order.total", {
    header: "Total",
    cell: ({ getValue, row }) => 
      `${row.original.draft_order.currency_code.toUpperCase()} ${getValue()}`,
  }),
  columnHelper.accessor("created_at", {
    header: "Created At",
    cell: ({ getValue }) => new Date(getValue()).toLocaleDateString(),
  }),
]
```

You use the `createDataTableColumnHelper` utility to create a function that allows you to define the columns of the table. Then, you create a `columns` array variable that defines the following columns:

1. `ID`: The display ID of the quote's draft order.
2. `Status`: The status of the quote. Here, you use an object to map the status to a human-readable title.
   - The `cell` property of the second object passed to the `columnHelper.accessor` function allows you to customize how the cell is rendered.
3. `Email`: The email of the customer.
4. `First Name`: The first name of the customer.
5. `Company Name`: The company name of the customer.
6. `Total`: The total amount of the quote's draft order. You format it to include the currency code.
7. `Created At`: The date the quote was created.

Next, you'll use these columns to render the `DataTable` component in the `Quotes` component.

Change the implementation of `Quotes` to the following:

```tsx title="src/admin/routes/quotes/page.tsx"
const Quotes = () => {
  const navigate = useNavigate()
  const [pagination, setPagination] = useState<DataTablePaginationState>({
    pageSize: 15,
    pageIndex: 0,
  })

  const {
    quotes = [],
    count,
    isPending,
  } = useQuotes({
    limit: pagination.pageSize,
    offset: pagination.pageIndex * pagination.pageSize,
    fields:
      "+draft_order.total,*draft_order.customer",
    order: "-created_at",
  })

  const table = useDataTable({
    columns,
    data: quotes,
    getRowId: (quote) => quote.id,
    rowCount: count,
    isLoading: isPending,
    pagination: {
      state: pagination,
      onPaginationChange: setPagination,
    },
    onRowClick(event, row) {
      navigate(`/quotes/${row.id}`)
    },
  })


  return (
    <>
      <Container className="flex flex-col p-0 overflow-hidden">
        <Heading className="p-6 pb-0 font-sans font-medium h1-core">
          Quotes
        </Heading>

        <DataTable instance={table}>
          <DataTable.Toolbar>
            <Heading>Products</Heading>
          </DataTable.Toolbar>
          <DataTable.Table />
          <DataTable.Pagination />
        </DataTable>
      </Container>
      <Toaster />
    </>
  )
}
```

In the component, you use the `useQuotes` hook to fetch the quotes from the Medusa server. You pass the following query parameters in the request:

- `limit` and `offset`: Pagination fields to specify the current page and the number of quotes to retrieve. These are based on the `pagination` state variable, which will be managed by the `DataTable` component.
- `fields`: The fields to retrieve in the response. You specify the total amount of the draft order and the customer of the draft order. Since you prefix the fields with `+` and `*`, the fields are retrieved along with the default fields specified in the Query configurations.
- `order`: The order in which to retrieve the quotes. Here, you retrieve the quotes in descending order of their creation date.

Next, you use the `useDataTable` hook to create a table instance with the columns you defined. You pass the fetched quotes to the `DataTable` component, along with configurations related to pagination and loading.

Notice that as part of the `useDataTable` configurations you naviagte to the `/quotes/:id` UI route when a row is clicked. You'll create that route in a later step.

Finally, you render the `DataTable` component to display the quotes in a table.

### Test List Quotes UI Route

You can now test out the UI route and the route added in the previous section from the Medusa Admin.

First, start the Medusa application:

```bash npm2yarn
npm run dev
```

Then, open the Medusa Admin dashboard at `http://localhost:9000/app` and login using the credentials you set up earlier.

You'll find a "Quotes" sidebar item. If you click on it, it will show you the table of quotes.

![Quotes table in Medusa Admin](https://res.cloudinary.com/dza7lstvk/image/upload/v1741099952/Medusa%20Resources/Screenshot_2025-03-04_at_4.52.17_PM_nqxyfq.png)

***

## Step 8: Retrieve Quote API Route

Next, you'll add an admin API route to retrieve a single quote. You'll use this route in the next step to add a UI route to view a quote's details. You'll later expand on that UI route to allow the admin to manage the quote.

To add the API route, create the file `src/api/admin/quotes/[id]/route.ts` with the following content:

![Directory structure after adding the single quote route file](https://res.cloudinary.com/dza7lstvk/image/upload/v1741100686/Medusa%20Resources/quote-27_ugvhbb.jpg)

```ts title="src/api/admin/quotes/[id]/route.ts"
import type {
  AuthenticatedMedusaRequest,
  MedusaResponse,
} from "@medusajs/framework/http"
import { ContainerRegistrationKeys } from "@medusajs/framework/utils"

export const GET = async (
  req: AuthenticatedMedusaRequest,
  res: MedusaResponse
) => {
  const query = req.scope.resolve(ContainerRegistrationKeys.QUERY)
  const { id } = req.params

  const {
    data: [quote],
  } = await query.graph(
    {
      entity: "quote",
      filters: { id },
      fields: req.queryConfig.fields,
    },
    { throwIfKeyNotFound: true }
  )

  res.json({ quote })
}
```

You export a `GET` route handler, which will create a `GET` API route at `/admin/quotes/:id`.

In the route handler, you resolve Query and use it to retrieve the quote. You pass the ID in the path parameter as a filter in Query. You also pass the query configuration fields, which are the same as the ones you've configured before, to retrieve the default fields and the fields specified in the query parameter.

Since you applied the middleware earlier to the `/admin/quotes*` route pattern, it will automatically apply to this route as well.

You'll test this route in the next step as you create the UI route for a single quote.

***

## Step 9: Quote Details UI Route

In the Quotes List UI route, you configured the data table to navigate to a quote's page when you click on it in the table. Now that you have the API route to retrieve a single quote, you'll create the UI route that shows a quote's details.

![Preview of the quote details page in Medusa Admin](https://res.cloudinary.com/dza7lstvk/image/upload/v1741158359/Medusa%20Resources/Screenshot_2025-03-05_at_9.05.45_AM_wfmb5w.png)

Before you create the UI route, you need to create the hooks necessary to retrieve data from the Medusa server, and some components that will show the different elements of the page.

### Add Hooks

The first hook you'll add is a hook that will retrieve a quote using the API route you added in the previous step.

In `src/admin/hooks/quote.tsx`, add the following:

```tsx title="src/admin/hooks/quote.tsx"
// other imports...
import { AdminQuoteResponse } from "../types"

// ...

export const useQuote = (
  id: string,
  query?: QuoteQueryParams,
  options?: UseQueryOptions<
    AdminQuoteResponse,
    FetchError,
    AdminQuoteResponse,
    QueryKey
  >
) => {
  const fetchQuote = (
    id: string,
    query?: QuoteQueryParams,
    headers?: ClientHeaders
  ) =>
    sdk.client.fetch<AdminQuoteResponse>(`/admin/quotes/${id}`, {
      query,
      headers,
    })

  const { data, ...rest } = useQuery({
    queryFn: () => fetchQuote(id, query),
    queryKey: ["quote", id],
    ...options,
  })

  return { ...data, ...rest }
}
```

You define a `useQuote` hook that accepts the quote's ID and optional query parameters and options as parameters. In the hook, you use the JS SDK's `client.fetch` method to retrieve the quotes from the `/admin/quotes/:id` route.

The hook returns the fetched data from the Medusa server. You'll use this hook later in the UI route.

In addition, you'll need a hook to retrieve a preview of the quote's draft order. An order preview includes changes or edits to be applied on an order's items, such as changes in prices and quantities. Medusa already provides a [Get Order Preview API route](https://docs.medusajs.com/api/admin#orders_getordersidpreview) that you can use to retrieve the preview.

To create the hook, create the file `src/admin/hooks/order-preview.tsx` with the following content:

![Directory structure after adding the order preview hook file](https://res.cloudinary.com/dza7lstvk/image/upload/v1741157692/Medusa%20Resources/quote-32_tb1tqw.jpg)

```tsx title="src/admin/hooks/order-preview.tsx"
import { HttpTypes } from "@medusajs/framework/types"
import { FetchError } from "@medusajs/js-sdk"
import { QueryKey, useQuery, UseQueryOptions } from "@tanstack/react-query"
import { sdk } from "../lib/sdk"

export const orderPreviewQueryKey = "custom_orders"

export const useOrderPreview = (
  id: string,
  query?: HttpTypes.AdminOrderFilters,
  options?: Omit<
    UseQueryOptions<
      HttpTypes.AdminOrderPreviewResponse,
      FetchError,
      HttpTypes.AdminOrderPreviewResponse,
      QueryKey
    >,
    "queryFn" | "queryKey"
  >
) => {
  const { data, ...rest } = useQuery({
    queryFn: async () => sdk.admin.order.retrievePreview(id, query),
    queryKey: [orderPreviewQueryKey, id],
    ...options,
  })

  return { ...data, ...rest }
}
```

You add a `useOrderPreview` hook that accepts as parameters the order's ID, query parameters, and options. In the hook, you use the JS SDK's `admin.order.retrievePreview` method to retrieve the order preview and return it.

You'll use this hook later in the quote's details page.

### Add formatAmount Utility

In the quote's details page, you'll display the amounts of the items in the quote. To format the amounts, you'll create a utility function that formats the amount based on the currency code.

Create the file `src/admin/utils/format-amount.ts` with the following content:

![Directory structure after adding the format amount utility file](https://res.cloudinary.com/dza7lstvk/image/upload/v1741157986/Medusa%20Resources/quote-33_k5sa9q.jpg)

```ts title="src/admin/utils/format-amount.ts"
export const formatAmount = (amount: number, currency_code: string) => {
  return new Intl.NumberFormat("en-US", {
    style: "currency",
    currency: currency_code,
  }).format(amount)
}
```

You define a `formatAmount` function that accepts an amount and a currency code as parameters. The function uses the `Intl.NumberFormat` API to format the amount as a currency based on the currency code.

You'll use this function in the UI route and its components.

### Create Amount Component

In the quote's details page, you want to display changes in amounts for items and totals. This is useful as you later add the capability to edit the price and quantity of items.

![Diagram showcasing where this component will be in the page](https://res.cloudinary.com/dza7lstvk/image/upload/v1741183186/Medusa%20Resources/amount-highlighted_havznm.png)

To display changes in an amount, you'll create an `Amount` component and re-use it where necessary. So, create the file `src/admin/components/amount.tsx` with the following content:

![Directory structure after adding the amount component](https://res.cloudinary.com/dza7lstvk/image/upload/v1741101819/Medusa%20Resources/quote-28_iwukg2.jpg)

```tsx title="src/admin/components/amount.tsx"
import { clx } from "@medusajs/ui"
import { formatAmount } from "../utils/format-amount"

type AmountProps = {
  currencyCode: string;
  amount?: number | null;
  originalAmount?: number | null;
  align?: "left" | "right";
  className?: string;
};

export const Amount = ({
  currencyCode,
  amount,
  originalAmount,
  align = "left",
  className,
}: AmountProps) => {
  if (typeof amount === "undefined" || amount === null) {
    return (
      <div className="flex h-full w-full items-center">
        <span className="text-ui-fg-muted">-</span>
      </div>
    )
  }

  const formatted = formatAmount(amount, currencyCode)
  const originalAmountPresent = typeof originalAmount === "number"
  const originalAmountDiffers = originalAmount !== amount
  const shouldShowAmountDiff = originalAmountPresent && originalAmountDiffers

  return (
    <div
      className={clx(
        "flex h-full w-full items-center overflow-hidden",
        {
          "flex-col": shouldShowAmountDiff,
          "justify-start text-left": align === "left",
          "justify-end text-right": align === "right",
        },
        className
      )}
    >
      {shouldShowAmountDiff ? (
        <>
          <span className="truncate line-through text-xs">
            {formatAmount(originalAmount!, currencyCode)}
          </span>
          <span className="truncate text-blue-400 txt-small">{formatted}</span>
        </>
      ) : (
        <>
          <span className="truncate">{formatted}</span>
        </>
      )}
    </div>
  )
}
```

In this component, you show the current amount of an item and, if it has been changed, you show previous amount as well.

You'll use this component in other components whenever you want to display any amount that can be changed.

### Create QuoteItems Component

In the quote's UI route, you want to display the details of the items in the quote. You'll create a separate component that you'll use within the UI route.

![Screenshot showcasing where this component will be in the page](https://res.cloudinary.com/dza7lstvk/image/upload/v1741183303/Medusa%20Resources/item-highlighted-cropped_ddyikt.png)

Create the file `src/admin/components/quote-items.tsx` with the following content:

![Directory structure after adding the quote items component](https://res.cloudinary.com/dza7lstvk/image/upload/v1741102170/Medusa%20Resources/quote-29_r5ljph.jpg)

```tsx title="src/admin/components/quote-items.tsx"
import {
  AdminOrder,
  AdminOrderLineItem,
  AdminOrderPreview,
} from "@medusajs/framework/types"
import { Badge, Text } from "@medusajs/ui"
import { useMemo } from "react"
import { Amount } from "./amount-cell"

export const QuoteItem = ({
  item,
  originalItem,
  currencyCode,
}: {
  item: AdminOrderPreview["items"][0];
  originalItem?: AdminOrderLineItem;
  currencyCode: string;
}) => {

  const isItemUpdated = useMemo(
    () => !!item.actions?.find((a) => a.action === "ITEM_UPDATE"),
    [item]
  )

  return (
    <div
      key={item.id}
      className="text-ui-fg-subtle grid grid-cols-2 items-center gap-x-4 px-6 py-4 text-left"
    >
      <div className="flex items-start gap-x-4">
        <div>
          <Text
            size="small"
            leading="compact"
            weight="plus"
            className="text-ui-fg-base"
          >
            {item.title}
          </Text>

          {item.variant_sku && (
            <div className="flex items-center gap-x-1">
              <Text size="small">{item.variant_sku}</Text>
            </div>
          )}
          <Text size="small">
            {item.variant?.options?.map((o) => o.value).join(" · ")}
          </Text>
        </div>
      </div>

      <div className="grid grid-cols-3 items-center gap-x-4">
        <div className="flex items-center justify-end gap-x-4">
          <Amount
            className="text-sm text-right justify-end items-end"
            currencyCode={currencyCode}
            // @ts-ignore
            amount={item.detail.unit_price}
            originalAmount={item.unit_price}
          />
        </div>

        <div className="flex items-center gap-x-2">
          <div className="w-fit min-w-[27px]">
            <Badge size="xsmall" color="grey">
              <span className="tabular-nums text-xs">{item.quantity}</span>x
            </Badge>
          </div>

          <div>

            {isItemUpdated && (
              <Badge
                size="2xsmall"
                rounded="full"
                color="orange"
                className="mr-1"
              >
                Modified
              </Badge>
            )}
          </div>

          <div className="overflow-visible"></div>
        </div>

        <Amount
          className="text-sm text-right justify-end items-end"
          currencyCode={currencyCode}
          amount={item.total}
          originalAmount={originalItem?.total}
        />
      </div>
    </div>
  )
}
```

You first define the component for one quote item. In the component, you show the item's title, variant SKU, and quantity. You also use the `Amount` component to show the item's current and previous amounts.

Next, add to the same file the `QuoteItems` component:

```tsx title="src/admin/components/quote-items.tsx"
export const QuoteItems = ({
  order,
  preview,
}: {
  order: AdminOrder;
  preview: AdminOrderPreview;
}) => {
  const itemsMap = useMemo(() => {
    return new Map(order.items.map((item) => [item.id, item]))
  }, [order])

  return (
    <div>
      {preview.items?.map((item) => {
        return (
          <QuoteItem
            key={item.id}
            item={item}
            originalItem={itemsMap.get(item.id)}
            currencyCode={order.currency_code}
          />
        )
      })}
    </div>
  )
}
```

In this component, you loop over the order's items and show each of them using the `QuoteItem` component.

### Create TotalsBreakdown Component

Another component you'll need in the quote's UI route is a component that breaks down the totals of the quote's draft order, such as its discount or shipping totals.

![Screenshot showcasing where this component will be in the page](https://res.cloudinary.com/dza7lstvk/image/upload/v1741183481/Medusa%20Resources/totals-highlighted_hpxier.png)

Create the file `src/admin/components/totals-breakdown.tsx` with the following content:

![Directory structure after adding the totals breakdown component](https://res.cloudinary.com/dza7lstvk/image/upload/v1741155757/Medusa%20Resources/quote-30_de0kjq.jpg)

```tsx title="src/admin/components/totals-breakdown.tsx"
import { AdminOrder } from "@medusajs/framework/types"
import { Text } from "@medusajs/ui"
import { ReactNode } from "react"
import { formatAmount } from "../utils/format-amount"

export const Total = ({
  label,
  value,
  secondaryValue,
  tooltip,
}: {
  label: string;
  value: string | number;
  secondaryValue: string;
  tooltip?: ReactNode;
}) => (
  <div className="grid grid-cols-3 items-center">
    <Text size="small" leading="compact">
      {label} {tooltip}
    </Text>
    <div className="text-right">
      <Text size="small" leading="compact">
        {secondaryValue}
      </Text>
    </div>

    <div className="text-right">
      <Text size="small" leading="compact">
        {value}
      </Text>
    </div>
  </div>
)
```

You first define the `Total` component, which breaksdown a total item, such as discount. You'll use this component to breakdown the different totals in the `TotalsBreakdown` component.

Add the `TotalsBreakdown` component after the `Total` component:

```tsx title="src/admin/components/totals-breakdown.tsx"
export const TotalsBreakdown = ({ order }: { order: AdminOrder }) => {
  return (
    <div className="text-ui-fg-subtle flex flex-col gap-y-2 px-6 py-4">
      <Total
        label="Discounts"
        secondaryValue=""
        value={
          order.discount_total > 0
            ? `- ${formatAmount(order.discount_total, order.currency_code)}`
            : "-"
        }
      />
      {(order.shipping_methods || [])
        .sort((m1, m2) =>
          (m1.created_at as string).localeCompare(m2.created_at as string)
        )
        .map((sm, i) => {
          return (
            <div key={i}>
              <Total
                key={sm.id}
                label={"Shipping"}
                secondaryValue={sm.name}
                value={formatAmount(sm.total, order.currency_code)}
              />
            </div>
          )
        })}
    </div>
  )
}
```

In this component, you show the different totals of the quote's draft order, such as discounts and shipping totals. You use the `Total` component to show each total item.

### Create Quote Details UI Route

You can now create the UI route that will show a quote's details in the Medusa Admin.

Create the file `src/admin/routes/quote/[id]/page.tsx` with the following content:

![Diagram showcasing the directory structure after adding the quote details UI route file](https://res.cloudinary.com/dza7lstvk/image/upload/v1741157385/Medusa%20Resources/quote-31_grwlon.jpg)

```tsx title="src/admin/routes/quote/[id]/page.tsx" highlights={quotesDetailsHighlights}
import { CheckCircleSolid } from "@medusajs/icons"
import {
  Button,
  Container,
  Heading,
  Text,
  Toaster,
} from "@medusajs/ui"
import { Link, useNavigate, useParams } from "react-router-dom"
import { useOrderPreview } from "../../../hooks/order-preview"
import { 
  useQuote, 
} from "../../../hooks/quotes"
import { QuoteItems } from "../../../components/quote-items"
import { TotalsBreakdown } from "../../../components/totals-breakdown"
import { formatAmount } from "../../../utils/format-amount"

const QuoteDetails = () => {
  const { id } = useParams()
  const navigate = useNavigate()
  const { quote, isLoading } = useQuote(id!, {
    fields:
      "*draft_order.customer",
  })

  const { order: preview, isLoading: isPreviewLoading } = useOrderPreview(
    quote?.draft_order_id!,
    {},
    { enabled: !!quote?.draft_order_id }
  )

  if (isLoading || !quote) {
    return <></>
  }

  if (isPreviewLoading) {
    return <></>
  }

  if (!isPreviewLoading && !preview) {
    throw "preview not found"
  }

  // TODO render content
}

export default QuoteDetails
```

The `QuoteDetails` component will render the content of the quote's details page. So far, you retrieve the quote and its preview using the hooks you created earlier. You also render empty components or an error message if the data is still loading or not found.

To add the rendered content, replace the `TODO` with the following:

```tsx title="src/admin/routes/quote/[id]/page.tsx"
return (
  <div className="flex flex-col gap-y-3">
    <div className="flex flex-col gap-x-4 lg:flex-row xl:items-start">
      <div className="flex w-full flex-col gap-y-3">
        {quote.status === "accepted" && (
          <Container className="divide-y divide-dashed p-0">
            <div className="flex items-center justify-between px-6 py-4">
              <Text className="txt-compact-small">
                <CheckCircleSolid className="inline-block mr-2 text-green-500 text-lg" />
                Quote accepted by customer. Order is ready for processing.
              </Text>

              <Button
                size="small"
                onClick={() => navigate(`/orders/${quote.draft_order_id}`)}
              >
                View Order
              </Button>
            </div>
          </Container>
        )}

        <Container className="divide-y divide-dashed p-0">
          <div className="flex items-center justify-between px-6 py-4">
            <Heading level="h2">Quote Summary</Heading>
          </div>
          <QuoteItems order={quote.draft_order} preview={preview!} />
          <TotalsBreakdown order={quote.draft_order} />
          <div className=" flex flex-col gap-y-2 px-6 py-4">
            <div className="text-ui-fg-base flex items-center justify-between">
              <Text
                weight="plus"
                className="text-ui-fg-subtle"
                size="small"
                leading="compact"
              >
                Original Total
              </Text>
              <Text
                weight="plus"
                className="text-ui-fg-subtle"
                size="small"
                leading="compact"
              >
                {formatAmount(quote.draft_order.total, quote.draft_order.currency_code)}
              </Text>
            </div>
      
            <div className="text-ui-fg-base flex items-center justify-between">
              <Text
                className="text-ui-fg-subtle text-semibold"
                size="small"
                leading="compact"
                weight="plus"
              >
                Quote Total
              </Text>
              <Text
                className="text-ui-fg-subtle text-bold"
                size="small"
                leading="compact"
                weight="plus"
              >
                {formatAmount(preview!.summary.current_order_total, quote.draft_order.currency_code)}
              </Text>
            </div>
          </div>

          {/* TODO add actions later */}
        </Container>

      </div>

      <div className="mt-2 flex w-full max-w-[100%] flex-col gap-y-3 xl:mt-0 xl:max-w-[400px]">
        <Container className="divide-y p-0">
          <div className="flex items-center justify-between px-6 py-4">
            <Heading level="h2">Customer</Heading>
          </div>

          <div className="text-ui-fg-subtle grid grid-cols-2 items-start px-6 py-4">
            <Text size="small" weight="plus" leading="compact">
              Email
            </Text>

            <Link
              className="text-sm text-pretty text-blue-500"
              to={`/customers/${quote.draft_order?.customer?.id}`}
              onClick={(e) => e.stopPropagation()}
            >
              {quote.draft_order?.customer?.email}
            </Link>
          </div>
        </Container>
      </div>
    </div>

    <Toaster />
  </div>
)
```

You first check if the quote has been accepted by the customer, and show a banner to view the created order if so.

Next, you use the `QuoteItems` and `TotalsBreakdown` components that you created to show the quote's items and totals. You also show the original and current totals of the quote, where the original total is the total of the draft order before any changes are made to its items.

Finally, you show the customer's email and a link to view their details.

### Test Quote Details UI Route

To test the quote details UI route, start the Medusa application:

```bash npm2yarn
npm run dev
```

Then, open the Medusa Admin dashboard at `http://localhost:9000/app` and login using the credentials you set up earlier.

Next, click on Quotes in the sidebar, which will open the list of quotes UI route you created earlier. Click on one of the quotes to view its details page.

On the quote's details page, you can see the quote's items, its totals, and the customer's details. In the next steps, you'll add management features to the page.

![Quote details page in Medusa Admin](https://res.cloudinary.com/dza7lstvk/image/upload/v1741158359/Medusa%20Resources/Screenshot_2025-03-05_at_9.05.45_AM_wfmb5w.png)

***

## Step 10: Add Merchant Reject Quote Feature

After the merchant or admin views the quote, they can choose to either reject it, send the quote back to the customer to review it, or make changes to the quote's prices and quantities.

In this step, you'll implement the functionality to reject a quote from the quote's details page. This will include:

1. Implementing the workflow to reject a quote.
2. Adding the API route to reject a quote that uses the workflow.
3. Add a hook in admin customizations that sends a request to the reject quote API route.
4. Add a button to reject the quote in the quote's details page.

### Implement Merchant Reject Quote Workflow

To reject a quote, you'll need to create a workflow that will handle the rejection process. The workflow has the following steps:

- [useQueryGraphStep](https://docs.medusajs.com/references/helper-steps/useQueryGraphStep/index.html.md): Retrieve the quote's details.
- [validateQuoteNotAccepted](#validateQuoteNotAccepted): Validate that the quote isn't already accepted by the customer.
- [updateQuoteStatusStep](#updateQuoteStatusStep): Update the quote's status to \`merchant\_rejected\`.

As mentioned before, the `useQueryGraphStep` is provided by Medusa's `@medusajs/medusa/core-flows` package. So, you'll only implement the remaining steps.

#### validateQuoteNotAccepted

The second step of the merchant rejection workflow ensures that a quote isn't already accepted, as it can't be rejected afterwards.

To create the step, create the file `src/workflows/steps/validate-quote-not-accepted.ts` with the following content:

![Diagram showcasing the directory structure after adding the validate quote rejection step file](https://res.cloudinary.com/dza7lstvk/image/upload/v1741159537/Medusa%20Resources/quote-34_mtcxwa.jpg)

```ts title="src/workflows/steps/validate-quote-not-accepted.ts"
import { MedusaError } from "@medusajs/framework/utils"
import { createStep } from "@medusajs/framework/workflows-sdk"
import { InferTypeOf } from "@medusajs/framework/types"
import { Quote, QuoteStatus } from "../../modules/quote/models/quote"

type StepInput = {
  quote: InferTypeOf<typeof Quote>
}

export const validateQuoteNotAccepted = createStep(
  "validate-quote-not-accepted",
  async function ({ quote }: StepInput) {
    if (quote.status === QuoteStatus.ACCEPTED) {
      throw new MedusaError(
        MedusaError.Types.INVALID_DATA,
        `Quote is already accepted by customer`
      )
    }
  }
)
```

You create a step that accepts a quote as an input and throws an error if the quote's status is `accepted`, as you can't reject a quote that has been accepted by the customer.

#### updateQuoteStatusStep

In the last step of the workflow, you'll change the workflow's status to `merchant_rejected`. So, you'll create a step that can be used to update a quote's status.

Create the file `src/workflows/steps/update-quotes.ts` with the following content:

![Diagram showcasing the directory structure after adding the update quotes step file](https://res.cloudinary.com/dza7lstvk/image/upload/v1741159754/Medusa%20Resources/quote-35_moaulz.jpg)

```ts title="src/workflows/steps/update-quotes.ts"
import { StepResponse, createStep } from "@medusajs/framework/workflows-sdk"
import { QUOTE_MODULE } from "../../modules/quote"
import { QuoteStatus } from "../../modules/quote/models/quote"
import QuoteModuleService from "../../modules/quote/service"

type StepInput = {
  id: string;
  status?: QuoteStatus;
}[]

export const updateQuotesStep = createStep(
  "update-quotes",
  async (data: StepInput, { container }) => {
    const quoteModuleService: QuoteModuleService = container.resolve(
      QUOTE_MODULE
    )

    const dataBeforeUpdate = await quoteModuleService.listQuotes(
      { id: data.map((d) => d.id) }
    )

    const updatedQuotes = await quoteModuleService.updateQuotes(data)

    return new StepResponse(updatedQuotes, {
      dataBeforeUpdate,
    })
  },
  async (revertInput, { container }) => {
    if (!revertInput) {
      return
    }

    const quoteModuleService: QuoteModuleService = container.resolve(
      QUOTE_MODULE
    )

    await quoteModuleService.updateQuotes(
      revertInput.dataBeforeUpdate
    )
  }
)
```

This step accepts an array of quotes to update their status. In the step function, you resolve the Quote Module's service. Then, you retrieve the quotes' original data so that you can pass them to the compensation function. Finally, you update the quotes' data and return the updated quotes.

In the compensation function, you resolve the Quote Module's service and update the quotes with their original data.

#### Implement Workflow

You can now implement the merchant-rejection workflow. Create the file `src/workflows/merchant-reject-quote.ts` with the following content:

![Diagram showcasing the directory structure after adding the merchant reject quote workflow file](https://res.cloudinary.com/dza7lstvk/image/upload/v1741159969/Medusa%20Resources/quote-36_l1ffxm.jpg)

```ts title="src/workflows/merchant-reject-quote.ts" highlights={merchantRejectionWorkflowHighlights}
import { useQueryGraphStep } from "@medusajs/core-flows"
import { createWorkflow } from "@medusajs/workflows-sdk"
import { QuoteStatus } from "../modules/quote/models/quote"
import { validateQuoteNotAccepted } from "./steps/validate-quote-not-accepted"
import { updateQuotesStep } from "./steps/update-quotes"

type WorkflowInput = {
  quote_id: string;
}

export const merchantRejectQuoteWorkflow = createWorkflow(
  "merchant-reject-quote-workflow",
  (input: WorkflowInput) => {
    // @ts-ignore
    const { data: quotes } = useQueryGraphStep({
      entity: "quote",
      fields: ["id", "status"],
      filters: { id: input.quote_id },
      options: {
        throwIfKeyNotFound: true,
      },
    })

    validateQuoteNotAccepted({ 
      // @ts-ignore
      quote: quotes[0],
    })

    updateQuotesStep([
      {
        id: input.quote_id,
        status: QuoteStatus.MERCHANT_REJECTED,
      },
    ])
  }
)
```

You create a workflow that accepts the ID of a quote to reject. In the workflow, you:

1. Use the `useQueryGraphStep` to retrieve the quote's details.
2. Validate that the quote isn't already accepted using the `validateQuoteNotAccepted`.
3. Update the quote's status to `merchant_rejected` using the `updateQuotesStep`.

You'll use this workflow next in an API route that allows a merchant to reject a quote.

### Add Admin Reject Quote API Route

You'll now add the API route that allows a merchant to reject a quote. The route will use the `merchantRejectQuoteWorkflow` you created in the previous step.

Create the file `src/api/admin/quotes/[id]/reject/route.ts` with the following content:

![Diagram showcasing the directory structure after adding the reject quote API route file](https://res.cloudinary.com/dza7lstvk/image/upload/v1741160251/Medusa%20Resources/quote-37_jwlfcw.jpg)

```ts title="src/api/admin/quotes/[id]/reject/route.ts"
import type {
  AuthenticatedMedusaRequest,
  MedusaResponse,
} from "@medusajs/framework/http"
import { ContainerRegistrationKeys } from "@medusajs/framework/utils"
import { merchantRejectQuoteWorkflow } from "../../../../../workflows/merchant-reject-quote"

export const POST = async (
  req: AuthenticatedMedusaRequest,
  res: MedusaResponse
) => {
  const query = req.scope.resolve(ContainerRegistrationKeys.QUERY)
  const { id } = req.params

  await merchantRejectQuoteWorkflow(req.scope).run({
    input: {
      quote_id: id,
    },
  })

  const {
    data: [quote],
  } = await query.graph(
    {
      entity: "quote",
      filters: { id },
      fields: req.queryConfig.fields,
    },
    { throwIfKeyNotFound: true }
  )

  res.json({ quote })
}
```

You create a `POST` route handler, which will expose a `POST` API route at `/admin/quotes/:id/reject`. In the route handler, you run the `merchantRejectQuoteWorkflow` with the quote's ID as input. You then retrieve the updated quote using Query and return it in the response.

Notice that you can pass `req.queryConfig.fields` to the `query.graph` method because you've applied the `validateAndTransformQuery` middleware before to all routes starting with `/admin/quotes`.

### Add Reject Quote Hook

Now that you have the API route, you can add a React hook in the admin customizations that sends a request to the route to reject a quote.

In `src/admin/hooks/quotes.tsx` add the following new hook:

```tsx title="src/admin/hooks/quotes.tsx"
// other imports...
import {
  useMutation,
  UseMutationOptions,
} from "@tanstack/react-query"

// ...

export const useRejectQuote = (
  id: string,
  options?: UseMutationOptions<AdminQuoteResponse, FetchError, void>
) => {
  const queryClient = useQueryClient()

  const rejectQuote = async (id: string) =>
    sdk.client.fetch<AdminQuoteResponse>(`/admin/quotes/${id}/reject`, {
      method: "POST",
    })

  return useMutation({
    mutationFn: () => rejectQuote(id),
    onSuccess: (data: AdminQuoteResponse, variables: any, context: any) => {
      queryClient.invalidateQueries({
        queryKey: [orderPreviewQueryKey, id],
      })

      queryClient.invalidateQueries({
        queryKey: ["quote", id],
      })

      queryClient.invalidateQueries({
        queryKey: ["quote", "list"],
      })

      options?.onSuccess?.(data, variables, context)
    },
    ...options,
  })
}
```

You add a `useRejectQuote` hook that accepts the quote's ID and optional options as parameters. In the hook, you use the `useMutation` hook to define the mutation action that sends a request to the reject quote API route.

When the mutation is invoked, the hook sends a request to the API route to reject the quote, then invalidates all data related to the quote in the query client, which will trigger a re-fetch of the data.

### Add Reject Quote Button

Finally, you can add a button to the quote's details page that allows a merchant to reject the quote.

In `src/admin/routes/quote/[id]/page.tsx`, add the following imports:

```tsx title="src/admin/routes/quote/[id]/page.tsx"
import {
  toast,
  usePrompt,
} from "@medusajs/ui"
import { useEffect, useState } from "react"
import { 
  useRejectQuote, 
} from "../../../hooks/quotes"
```

Then, in the `QuoteDetails` component, add the following after the `useOrderPreview` hook usage:

```tsx title="src/admin/routes/quote/[id]/page.tsx"
const prompt = usePrompt()
const { mutateAsync: rejectQuote, isPending: isRejectingQuote } =
  useRejectQuote(id!)
const [showRejectQuote, setShowRejectQuote] = useState(false)

useEffect(() => {
  if (
    ["customer_rejected", "merchant_rejected", "accepted"].includes(
      quote?.status!
    )
  ) {
    setShowRejectQuote(false)
  } else {
    setShowRejectQuote(true)
  }
}, [quote])

const handleRejectQuote = async () => {
  const res = await prompt({
    title: "Reject quote?",
    description:
      "You are about to reject this customer's quote. Do you want to continue?",
    confirmText: "Continue",
    cancelText: "Cancel",
    variant: "confirmation",
  })

  if (res) {
    await rejectQuote(void 0, {
      onSuccess: () =>
        toast.success("Successfully rejected customer's quote"),
      onError: (e) => toast.error(e.message),
    })
  }
}
```

First, you initialize the following variables:

1. `prompt`: A function that you'll use to show a confirmation pop-up when the merchant tries to reject the quote. The `usePrompt` hook is available from the Medusa UI package.
2. `rejectQuote` and `isRejectingQuote`: both are returned by the `useRejectQuote` hook. The `rejectQuote` function invokes the mutation, rejecting the quote; `isRejectingQuote` is a boolean that indicates if the mutation is in progress.
3. `showRejectQuote`: A boolean that indicates whether the "Reject Quote" button should be shown. The button is shown if the quote's status is not `customer_rejected`, `merchant_rejected`, or `accepted`. This state variable is changed based on the quote's status in the `useEffect` hook.

You also define a `handleRejectQuote` function that will be called when the merchant clicks the reject quote button. The function shows a confirmation pop-up using the `prompt` function. If the user confirms the action, the function calls the `rejectQuote` function to reject the quote.

Finally, find the `TODO` in the `return` statement and replace it with the following:

```tsx title="src/admin/routes/quote/[id]/page.tsx"
<div className="bg-ui-bg-subtle flex items-center justify-end gap-x-2 rounded-b-xl px-4 py-4">
  {showRejectQuote && (
    <Button
      size="small"
      variant="secondary"
      onClick={() => handleRejectQuote()}
      disabled={isRejectingQuote}
    >
      Reject Quote
    </Button>
  )}
</div>
```

In this code snippet, you show the reject quote button if the `showRejectQuote` state is `true`. When the button is clicked, you call the `handleRejectQuote` function to reject the quote.

### Test Reject Quote Feature

To test the reject quote feature, start the Medusa application:

```bash npm2yarn
npm run dev
```

Then, open the Medusa Admin dashboard at `http://localhost:9000/app` and login using the credentials you set up earlier.

Next, open a quote's details page. You'll find a new "Reject Quote" button. If you click on it and confirm rejecting the quote, the quote will be rejected, and a success message will be shown.

![Quote details page with reject quote button in Medusa Admin](https://res.cloudinary.com/dza7lstvk/image/upload/v1741161544/Medusa%20Resources/Screenshot_2025-03-05_at_9.58.41_AM_xzdv6k.png)

***

## Step 11: Add Merchant Send Quote Feature

Another action that a merchant can take on a quote is to send the quote back to the customer for review. The customer can then reject or accept the quote, which would convert it to an order.

In this step, you'll implement the functionality to send a quote back to the customer for review. This will include:

1. Implementing the workflow to send a quote back to the customer.
2. Adding the API route to send a quote back to the customer that uses the workflow.
3. Add a hook in admin customizations that sends a request to the send quote API route.
4. Add a button to send the quote back to the customer in the quote's details page.

### Implement Merchant Send Quote Workflow

You'll implement the logic of sending the quote in a workflow. The workflow has the following steps:

- [useQueryGraphStep](https://docs.medusajs.com/references/helper-steps/useQueryGraphStep/index.html.md): Retrieve the quote's details.
- [validateQuoteNotAccepted](#validateQuoteNotAccepted): Validate that the quote isn't already accepted by the customer.
- [updateQuoteStatusStep](#updateQuoteStatusStep): Update the quote's status to \`pending\_customer\`.

All the steps are available for use, so you can implement the workflow directly.

Create the file `src/workflows/merchant-send-quote.ts` with the following content:

![Directory structure after adding the merchant send quote workflow file](https://res.cloudinary.com/dza7lstvk/image/upload/v1741162342/Medusa%20Resources/quote-38_n4ksr0.jpg)

```ts title="src/workflows/merchant-send-quote.ts" highlights={sendQuoteHighlights}
import { useQueryGraphStep } from "@medusajs/core-flows"
import { createWorkflow } from "@medusajs/workflows-sdk"
import { QuoteStatus } from "../modules/quote/models/quote"
import { updateQuotesStep } from "./steps/update-quotes"
import { validateQuoteNotAccepted } from "./steps/validate-quote-not-accepted"

type WorkflowInput = {
  quote_id: string;
}

export const merchantSendQuoteWorkflow = createWorkflow(
  "merchant-send-quote-workflow",
  (input: WorkflowInput) => {
    // @ts-ignore
    const { data: quotes } = useQueryGraphStep({
      entity: "quote",
      fields: ["id", "status"],
      filters: { id: input.quote_id },
      options: {
        throwIfKeyNotFound: true,
      },
    })

    validateQuoteNotAccepted({
      // @ts-ignore
      quote: quotes[0],
    })

    updateQuotesStep([
      {
        id: input.quote_id,
        status: QuoteStatus.PENDING_CUSTOMER,
      },
    ])
  }
)
```

You create a workflow that accepts the ID of a quote to send back to the customer. In the workflow, you:

1. Use the `useQueryGraphStep` to retrieve the quote's details.
2. Validate that the quote can be sent back to the customer using the `validateQuoteNotAccepted` step.
3. Update the quote's status to `pending_customer` using the `updateQuotesStep`.

You'll use this workflow next in an API route that allows a merchant to send a quote back to the customer.

### Add Send Quote API Route

You'll now add the API route that allows a merchant to send a quote back to the customer. The route will use the `merchantSendQuoteWorkflow` you created in the previous step.

Create the file `src/api/admin/quotes/[id]/send/route.ts` with the following content:

![Directory structure after adding the send quote API route file](https://res.cloudinary.com/dza7lstvk/image/upload/v1741162497/Medusa%20Resources/quote-39_us1jbh.jpg)

```ts title="src/api/admin/quotes/[id]/send/route.ts"
import type {
  AuthenticatedMedusaRequest,
  MedusaResponse,
} from "@medusajs/framework/http"
import { ContainerRegistrationKeys } from "@medusajs/framework/utils"
import { 
  merchantSendQuoteWorkflow,
} from "../../../../../workflows/merchant-send-quote"

export const POST = async (
  req: AuthenticatedMedusaRequest,
  res: MedusaResponse
) => {
  const query = req.scope.resolve(ContainerRegistrationKeys.QUERY)
  const { id } = req.params

  await merchantSendQuoteWorkflow(req.scope).run({
    input: {
      quote_id: id,
    },
  })

  const {
    data: [quote],
  } = await query.graph(
    {
      entity: "quote",
      filters: { id },
      fields: req.queryConfig.fields,
    },
    { throwIfKeyNotFound: true }
  )

  res.json({ quote })
}
```

You create a `POST` route handler, which will expose a `POST` API route at `/admin/quotes/:id/send`. In the route handler, you run the `merchantSendQuoteWorkflow` with the quote's ID as input. You then retrieve the updated quote using Query and return it in the response.

Notice that you can pass `req.queryConfig.fields` to the `query.graph` method because you've applied the `validateAndTransformQuery` middleware before to all routes starting with `/admin/quotes`.

### Add Send Quote Hook

Now that you have the API route, you can add a React hook in the admin customizations that sends a request to the quote send API route.

In `src/admin/hooks/quotes.tsx` add the new hook:

```tsx title="src/admin/hooks/quotes.tsx"
export const useSendQuote = (
  id: string,
  options?: UseMutationOptions<AdminQuoteResponse, FetchError, void>
) => {
  const queryClient = useQueryClient()

  const sendQuote = async (id: string) =>
    sdk.client.fetch<AdminQuoteResponse>(`/admin/quotes/${id}/send`, {
      method: "POST",
    })

  return useMutation({
    mutationFn: () => sendQuote(id),
    onSuccess: (data: any, variables: any, context: any) => {
      queryClient.invalidateQueries({
        queryKey: [orderPreviewQueryKey, id],
      })

      queryClient.invalidateQueries({
        queryKey: ["quote", id],
      })

      queryClient.invalidateQueries({
        queryKey: ["quote", "list"],
      })

      options?.onSuccess?.(data, variables, context)
    },
    ...options,
  })
}
```

You add a `useSendQuote` hook that accepts the quote's ID and optional options as parameters. In the hook, you use the `useMutation` hook to define the mutation action that sends a request to the send quote API route.

When the mutation is invoked, the hook sends a request to the send quote API route, then invalidates all data related to the quote in the query client, which will trigger a re-fetch of the data.

### Add Send Quote Button

Finally, you can add a button to the quote's details page that allows a merchant to send the quote back to the customer for review.

First, add the following import to the `src/admin/routes/quote/[id]/page.tsx` file:

```tsx title="src/admin/routes/quote/[id]/page.tsx"
import { 
  useSendQuote,
} from "../../../hooks/quotes"
```

Then, after the `useRejectQuote` hook usage, add the following:

```tsx title="src/admin/routes/quote/[id]/page.tsx"
const { mutateAsync: sendQuote, isPending: isSendingQuote } = useSendQuote(
  id!
)
const [showSendQuote, setShowSendQuote] = useState(false)
```

You initialize the following variables:

1. `sendQuote` and `isSendingQuote`: Data returned by the `useSendQuote` hook. The `sendQuote` function invokes the mutation, sending the quote back to the customer; `isSendingQuote` is a boolean that indicates if the mutation is in progress.
2. `showSendQuote`: A boolean that indicates whether the "Send Quote" button should be shown.

Next, update the existing `useEffect` hook to change `showSendQuote` based on the quote's status:

```tsx title="src/admin/routes/quote/[id]/page.tsx"
useEffect(() => {
  if (["pending_merchant", "customer_rejected"].includes(quote?.status!)) {
    setShowSendQuote(true)
  } else {
    setShowSendQuote(false)
  }

  if (
    ["customer_rejected", "merchant_rejected", "accepted"].includes(
      quote?.status!
    )
  ) {
    setShowRejectQuote(false)
  } else {
    setShowRejectQuote(true)
  }
}, [quote])
```

The `useEffect` hook now updates both the `showSendQuote` and `showRejectQuote` states based on the quote's status. The "Send Quote" button is hidden if the quote's status is not `pending_merchant` or `customer_rejected`.

Then, after the `handleRejectQuote` function, add the following `handleSendQuote` function:

```tsx title="src/admin/routes/quote/[id]/page.tsx"
const handleSendQuote = async () => {
  const res = await prompt({
    title: "Send quote?",
    description:
      "You are about to send this quote to the customer. Do you want to continue?",
    confirmText: "Continue",
    cancelText: "Cancel",
    variant: "confirmation",
  })

  if (res) {
    await sendQuote(
      void 0,
      {
        onSuccess: () => toast.success("Successfully sent quote to customer"),
        onError: (e) => toast.error(e.message),
      }
    )
  }
}
```

You define a `handleSendQuote` function that will be called when the merchant clicks the "Send Quote" button. The function shows a confirmation pop-up using the `prompt` hook. If the user confirms the action, the function calls the `sendQuote` function to send the quote back to the customer.

Finally, add the following after the reject quote button in the `return` statement:

```tsx title="src/admin/routes/quote/[id]/page.tsx"
{showSendQuote && (
  <Button
    size="small"
    variant="secondary"
    onClick={() => handleSendQuote()}
    disabled={isSendingQuote || isRejectingQuote}
  >
    Send Quote
  </Button>
)}
```

In this code snippet, you show the "Send Quote" button if the `showSendQuote` state is `true`. When the button is clicked, you call the `handleSendQuote` function to send the quote back to the customer.

### Test Send Quote Feature

To test the send quote feature, start the Medusa application:

```bash npm2yarn
npm run dev
```

Then, open the Medusa Admin dashboard at `http://localhost:9000/app` and login using the credentials you set up earlier.

Next, open a quote's details page. You'll find a new "Send Quote" button. If you click on it and confirm sending the quote, the quote will be sent back to the customer, and a success message will be shown.

You'll later add the feature to update the quote item's details before sending the quote back to the customer.

![Quote details page with send quote button in Medusa Admin](https://res.cloudinary.com/dza7lstvk/image/upload/v1741162950/Medusa%20Resources/Screenshot_2025-03-05_at_10.22.11_AM_sjuipg.png)

***

## Step 12: Add Customer Preview Order API Route

When the merchant sends back the quote to the customer, you want to show the customer the details of the quote and the order that would be created if they accept the quote. This helps the customer decide whether to accept or reject the quote (which you'll implement next).

In this step, you'll add the API route that allows a customer to preview a quote's order.

To create the API route, create the file `src/api/store/customers/me/quotes/[id]/preview/route.ts` with the following content:

![Directory structure after adding the customer preview order API route file](https://res.cloudinary.com/dza7lstvk/image/upload/v1741163145/Medusa%20Resources/quote-40_lmcgve.jpg)

```ts title="src/api/store/customers/me/quotes/[id]/preview/route.ts"
import type {
  AuthenticatedMedusaRequest,
  MedusaResponse,
} from "@medusajs/framework/http"
import { ContainerRegistrationKeys, Modules } from "@medusajs/framework/utils"

export const GET = async (
  req: AuthenticatedMedusaRequest,
  res: MedusaResponse
) => {
  const { id } = req.params
  const query = req.scope.resolve(
    ContainerRegistrationKeys.QUERY
  )

  const {
    data: [quote],
  } = await query.graph(
    {
      entity: "quote",
      filters: { id },
      fields: req.queryConfig.fields,
    },
    { throwIfKeyNotFound: true }
  )

  const orderModuleService = req.scope.resolve(
    Modules.ORDER
  )

  const preview = await orderModuleService.previewOrderChange(
    quote.draft_order_id
  )

  res.status(200).json({
    quote: {
      ...quote,
      order_preview: preview,
    },
  })
}
```

You create a `GET` route handler, which will expose a `GET` API route at `/store/customers/me/quotes/:id/preview`. In the route handler, you retrieve the quote's details using Query, then preview the order that would be created from the quote using the `previewOrderChange` method from the Order Module's service. Finally, you return the quote and its order preview in the response.

Notice that you're using the `req.queryConfig.fields` object in the `query.graph` method because you've applied the `validateAndTransformQuery` middleware before to all routes starting with `/store/customers/me/quotes`.

### Test Customer Preview Order API Route

To test the customer preview order API route, start the Medusa application:

```bash npm2yarn
npm run dev
```

Then, grab the ID of a quote placed by a customer that you have their [authentication token](#retrieve-customer-authentication-token). You can find the quote ID in the URL when viewing the quote's details page in the Medusa Admin dashboard.

Finally, send the following request to get a preview of the customer's quote and order:

```bash
curl 'http://localhost:9000/store/customers/me/quotes/{quote_id}/preview' \
-H 'x-publishable-api-key: {your_publishable_api_key}' \
-H 'Authorization: Bearer {token}'
```

Make sure to replace:

- `{quote_id}` with the ID of the quote you want to preview.
- `{your_publishable_api_key}` with [your publishable API key](#retrieve-publishable-api-key).
- `{token}` with the customer's authentication token.

You'll receive in the response the quote's details with the order preview. You can show the customer these details in the storefront.

***

## Step 13: Add Customer Reject Quote Feature

After the customer previews the quote and its order, they can choose to reject the quote. When the customer rejects the quote, the quote's status is changed to `customer_rejected`. The merchant will still be able to update the quote and send it back to the customer for review.

In this step, you'll implement the functionality to reject a quote from the customer's perspective. This will include:

1. Implementing the workflow to reject a quote as a customer.
2. Adding the API route to allow customers to reject a quote using the workflow.

### Implement Customer Reject Quote Workflow

To reject a quote from the customer's perspective, you'll need to create a workflow that will handle the rejection process. The workflow has the following steps:

- [useQueryGraphStep](https://docs.medusajs.com/references/helper-steps/useQueryGraphStep/index.html.md): Retrieve the quote's details.
- [validateQuoteNotAccepted](#validateQuoteNotAccepted): Validate that the quote isn't already accepted by the customer.
- [updateQuoteStatusStep](#updateQuoteStatusStep): Update the quote's status to \`customer\_rejected\`.

All the steps are available for use, so you can implement the workflow directly.

Create the file `src/workflows/customer-reject-quote.ts` with the following content:

![Directory structure after adding the customer reject quote workflow file](https://res.cloudinary.com/dza7lstvk/image/upload/v1741164371/Medusa%20Resources/quote-41_fgpqhz.jpg)

```ts title="src/workflows/customer-reject-quote.ts" highlights={customerRejectQuoteHighlights}
import { useQueryGraphStep } from "@medusajs/core-flows"
import { createWorkflow } from "@medusajs/workflows-sdk"
import { QuoteStatus } from "../modules/quote/models/quote"
import { updateQuotesStep } from "./steps/update-quotes"
import { validateQuoteNotAccepted } from "./steps/validate-quote-not-accepted"

type WorkflowInput = {
  quote_id: string;
  customer_id: string;
}

export const customerRejectQuoteWorkflow = createWorkflow(
  "customer-reject-quote-workflow",
  (input: WorkflowInput) => {
    // @ts-ignore
    const { data: quotes } = useQueryGraphStep({
      entity: "quote",
      fields: ["id", "status"],
      filters: { id: input.quote_id, customer_id: input.customer_id },
      options: {
        throwIfKeyNotFound: true,
      },
    })

    validateQuoteNotAccepted({ 
      // @ts-ignore
      quote: quotes[0],
    })

    updateQuotesStep([
      {
        id: input.quote_id,
        status: QuoteStatus.CUSTOMER_REJECTED,
      },
    ])
  }
)
```

You create a workflow that accepts the IDs of the quote to reject and the customer rejecting it. In the workflow, you:

1. Use the `useQueryGraphStep` to retrieve the quote's details. Notice that you pass the IDs of the quote and the customer as filters to ensure that the quote belongs to the customer.
2. Validate that the quote isn't already accepted using the `validateQuoteNotAccepted` step.
3. Update the quote's status to `customer_rejected` using the `updateQuotesStep`.

You'll use this workflow next in an API route that allows a customer to reject a quote.

### Add Customer Reject Quote API Route

You'll now add the API route that allows a customer to reject a quote. The route will use the `customerRejectQuoteWorkflow` you created in the previous step.

Create the file `src/api/store/customers/me/quotes/[id]/reject/route.ts` with the following content:

![Directory structure after adding the customer reject quote API route file](https://res.cloudinary.com/dza7lstvk/image/upload/v1741164538/Medusa%20Resources/quote-42_bryo2z.jpg)

```ts title="src/api/store/customers/me/quotes/[id]/reject/route.ts"
import type { 
  AuthenticatedMedusaRequest, 
  MedusaResponse,
} from "@medusajs/framework/http"
import { ContainerRegistrationKeys } from "@medusajs/framework/utils"
import { 
  customerRejectQuoteWorkflow,
} from "../../../../../../../workflows/customer-reject-quote"

export const POST = async (
  req: AuthenticatedMedusaRequest,
  res: MedusaResponse
) => {
  const { id } = req.params
  const query = req.scope.resolve(
    ContainerRegistrationKeys.QUERY
  )

  await customerRejectQuoteWorkflow(req.scope).run({
    input: {
      quote_id: id,
      customer_id: req.auth_context.actor_id,
    },
  })

  const {
    data: [quote],
  } = await query.graph(
    {
      entity: "quote",
      filters: { id },
      fields: req.queryConfig.fields,
    },
    { throwIfKeyNotFound: true }
  )

  return res.json({ quote })
}
```

You create a `POST` route handler, which will expose a `POST` API route at `/store/customers/me/quotes/:id/reject`. In the route handler, you run the `customerRejectQuoteWorkflow` with the quote's ID as input. You then retrieve the updated quote using Query and return it in the response.

Notice that you can pass `req.queryConfig.fields` to the `query.graph` method because you've applied the `validateAndTransformQuery` middleware before to all routes starting with `/store/customers/me/quotes`.

### Test Customer Reject Quote Feature

To test the customer reject quote feature, start the Medusa application:

```bash npm2yarn
npm run dev
```

Then, send a request to reject a quote for the authenticated customer:

```bash
curl -X POST 'http://localhost:9000/store/customers/me/quotes/{quote_id}/reject' \
-H 'x-publishable-api-key: {your_publishable_api_key}' \
-H 'Authorization: Bearer {token}'
```

Make sure to replace:

- `{quote_id}` with the ID of the quote you want to reject.
- `{your_publishable_api_key}` with [your publishable API key](#retrieve-publishable-api-key).
- `{token}` with the customer's [authentication token](#retrieve-customer-authentication-token).

After sending the request, the quote will be rejected, and the updated quote will be returned in the response. You can also view the quote from the Medusa Admin dashboard, where you'll find its status has changed.

***

## Step 14: Add Customer Accept Quote Feature

The customer alternatively can choose to accept a quote after previewing it. When the customer accepts a quote, the quote's draft order should become an order whose payment can be processed and items fulfilled. No further changes can be made on the quote after it's accepted.

In this step, you'll implement the functionality to allow a customer to accept a quote. This will include:

1. Implementing the workflow to accept a quote as a customer.
2. Adding the API route to allow customers to accept a quote using the workflow.

### Implement Customer Accept Quote Workflow

You'll implement the quote acceptance logic in a workflow. The workflow has the following steps:

- [useQueryGraphStep](https://docs.medusajs.com/references/helper-steps/useQueryGraphStep/index.html.md): Retrieve the quote's details.
- [validateQuoteCanAcceptStep](#validateQuoteCanAcceptStep): Validate that the quote can be accepted.
- [updateQuotesStep](#updateQuotesStep): Update the quote's status to \`accepted\`.
- [confirmOrderEditRequestWorkflow](https://docs.medusajs.com/references/medusa-workflows/confirmOrderEditRequestWorkflow/index.html.md): Confirm the changes made on the draft order, such as changes to item quantities and prices.
- [updateOrderWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateOrderWorkflow/index.html.md): Update the draft order to change its status and convert it into an order.

You only need to implement the `validateQuoteCanAcceptStep` step before implementing the workflow, as the other steps are already available for use.

#### validateQuoteCanAcceptStep

In the `validateQuoteCanAcceptStep`, you'll validate whether the customer can accept the quote. The customer can only accept a quote if the quote's status is `pending_customer`, meaning the merchant sent the quote back to the customer for review.

Create the file `src/workflows/steps/validate-quote-can-accept.ts` with the following content:

![Directory structure after adding the validate quote can accept step file](https://res.cloudinary.com/dza7lstvk/image/upload/v1741165829/Medusa%20Resources/quote-43_cxc3qi.jpg)

```ts title="src/workflows/steps/validate-quote-can-accept.ts"
import { MedusaError } from "@medusajs/framework/utils"
import { createStep } from "@medusajs/framework/workflows-sdk"
import { InferTypeOf } from "@medusajs/framework/types"
import { Quote, QuoteStatus } from "../../modules/quote/models/quote"

type StepInput = {
  quote: InferTypeOf<typeof Quote>
}

export const validateQuoteCanAcceptStep = createStep(
  "validate-quote-can-accept",
  async function ({ quote }: StepInput) {
    if (quote.status !== QuoteStatus.PENDING_CUSTOMER) {
      throw new MedusaError(
        MedusaError.Types.INVALID_DATA,
        `Cannot accept quote when quote status is ${quote.status}`
      )
    }
  }
)
```

You create a step that accepts a quote as input. In the step function, you throw an error if the quote's status is not `pending_customer`.

#### Implement Workflow

You can now implement the workflow that accepts a quote for a customer. Create the file `src/workflows/customer-accept-quote.ts` with the following content:

![Directory structure after adding the customer accept quote workflow file](https://res.cloudinary.com/dza7lstvk/image/upload/v1741166025/Medusa%20Resources/quote-44_c09ts9.jpg)

```ts title="src/workflows/customer-accept-quote.ts" highlights={customerAcceptQuoteHighlights}
import {
  confirmOrderEditRequestWorkflow,
  updateOrderWorkflow,
  useQueryGraphStep,
} from "@medusajs/core-flows"
import { OrderStatus } from "@medusajs/framework/utils"
import { createWorkflow } from "@medusajs/workflows-sdk"
import { validateQuoteCanAcceptStep } from "./steps/validate-quote-can-accept"
import { QuoteStatus } from "../modules/quote/models/quote"
import { updateQuotesStep } from "./steps/update-quotes"

type WorkflowInput = {
  quote_id: string;
  customer_id: string;
};

export const customerAcceptQuoteWorkflow = createWorkflow(
  "customer-accept-quote-workflow",
  (input: WorkflowInput) => {
    // @ts-ignore
    const { data: quotes } = useQueryGraphStep({
      entity: "quote",
      fields: ["id", "draft_order_id", "status"],
      filters: { id: input.quote_id, customer_id: input.customer_id },
      options: {
        throwIfKeyNotFound: true,
      },
    })

    validateQuoteCanAcceptStep({ 
      // @ts-ignore
      quote: quotes[0],
    })

    updateQuotesStep([{ 
      id: input.quote_id, 
      status: QuoteStatus.ACCEPTED,
    }])

    confirmOrderEditRequestWorkflow.runAsStep({
      input: {
        order_id: quotes[0].draft_order_id,
        confirmed_by: input.customer_id,
      },
    })

    updateOrderWorkflow.runAsStep({
      input:{ 
        id: quotes[0].draft_order_id,
        // @ts-ignore
        status: OrderStatus.PENDING,
        is_draft_order: false,
      },
    })
  }
)
```

You create a workflow that accepts the IDs of the quote to accept and the customer accepting it. In the workflow, you:

1. Use the `useQueryGraphStep` to retrieve the quote's details. You pass the IDs of the quotes and the customer as filters to ensure that the quote belongs to the customer.
2. Validate that the quote can be accepted using the `validateQuoteCanAcceptStep`.
3. Update the quote's status to `accepted` using the `updateQuotesStep`.
4. Confirm the changes made on the draft order using the `confirmOrderEditRequestWorkflow` executed as a step. This is useful when you soon add the admin functionality to edit the quote items. Any changes that the admin has made will be applied on the draft order using this step.
5. Update the draft order to change its status and convert it into an order using the `updateOrderWorkflow` executed as a step.

You'll use this workflow next in an API route that allows a customer to accept a quote.

### Add Customer Accept Quote API Route

You'll now add the API route that allows a customer to accept a quote. The route will use the `customerAcceptQuoteWorkflow` you created in the previous step.

Create the file `src/api/store/customers/me/quotes/[id]/accept/route.ts` with the following content:

![Directory structure after adding the customer accept quote API route file](https://res.cloudinary.com/dza7lstvk/image/upload/v1741166543/Medusa%20Resources/quote-45_y8zprn.jpg)

```ts title="src/api/store/customers/me/quotes/[id]/accept/route.ts"
import type {
  AuthenticatedMedusaRequest,
  MedusaResponse,
} from "@medusajs/framework/http"
import { ContainerRegistrationKeys } from "@medusajs/framework/utils"
import { 
  customerAcceptQuoteWorkflow,
} from "../../../../../../../workflows/customer-accept-quote"

export const POST = async (
  req: AuthenticatedMedusaRequest,
  res: MedusaResponse
) => {
  const query = req.scope.resolve(ContainerRegistrationKeys.QUERY)
  const { id } = req.params

  await customerAcceptQuoteWorkflow(req.scope).run({
    input: {
      quote_id: id,
      customer_id: req.auth_context.actor_id,
    },
  })

  const {
    data: [quote],
  } = await query.graph(
    {
      entity: "quote",
      filters: { id },
      fields: req.queryConfig.fields,
    },
    { throwIfKeyNotFound: true }
  )

  return res.json({ quote })
}
```

You create a `POST` route handler, which will expose a `POST` API route at `/store/customers/me/quotes/:id/accept`. In the route handler, you run the `customerAcceptQuoteWorkflow` with the quote's ID as input. You then retrieve the updated quote using Query and return it in the response.

Notice that you can pass `req.queryConfig.fields` to the `query.graph` method because you've applied the `validateAndTransformQuery` middleware before to all routes starting with `/store/customers/me/quotes`.

### Test Customer Accept Quote Feature

To test the customer accept quote feature, start the Medusa application:

```bash npm2yarn
npm run dev
```

Then, send a request to accept a quote for the authenticated customer:

```bash
curl -X POST 'http://localhost:9000/store/customers/me/quotes/{quote_id}/accept' \
-H 'x-publishable-api-key: {your_publishable_api_key}' \
-H 'Authorization: Bearer {token}'
```

Make sure to replace:

- `{quote_id}` with the ID of the quote you want to accept.
- `{your_publishable_api_key}` with [your publishable API key](#retrieve-publishable-api-key).
- `{token}` with the customer's [authentication token](#retrieve-customer-authentication-token).

After sending the request, the quote will be accepted, and the updated quote will be returned in the response.

You can also view the quote from the Medusa Admin dashboard, where you'll find its status has changed. The quote will also have an order, which you can view in the Orders page or using the "View Order" button on the quote's details page.

![View order button on quote's details page in Medusa Admin](https://res.cloudinary.com/dza7lstvk/image/upload/v1741166844/Medusa%20Resources/Screenshot_2025-03-05_at_11.27.02_AM_s90rqh.png)

***

## Step 15: Edit Quote Items UI Route

The last feature you'll add is allowing merchants or admin users to make changes to the quote's items. This includes updating the item's quantity and price.

Since you're using an [order change](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/order/order-change/index.html.md) to manage edits to the quote's draft orders, you don't need to implement customizations on the server side, such as adding workflows or API routes. Instead, you'll only add a new UI route in the Medusa Admin that uses the [Order Edit API routes](https://docs.medusajs.com/api/admin#order-edits) to provide the functionality to edit the quote's items.

Order changes also allow you to add or remove items from the quote. However, for simplicity, this guide only covers how to update the item's quantity and price. Refer to the [Order Change](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/order/order-change/index.html.md) documentation to learn more.

![Edit quote items page in Medusa Admin](https://res.cloudinary.com/dza7lstvk/image/upload/v1741169659/Medusa%20Resources/Screenshot_2025-03-05_at_12.14.05_PM_ufvkqb.png)

In this step, you'll add a new UI route to manage the quote's items. This will include:

1. Adding hooks to send requests to [Medusa's Order Edits API routes](https://docs.medusajs.com/api/admin#order-edits).
2. Implement the components you'll use within the UI route.
3. Add the new UI route to the Medusa Admin.

### Intermission: Order Editing Overview

Before you start implementing the customizations, here's a quick overview of how order editing works in Medusa.

When the admin wants to edit an order's items, Medusa creates an order change. You've already implemented this part on quote creation.

Then, when the admin makes an edit to an item, Medusa saves that edit but without applying it to the order or finalizing the edit. This allows the admin to make multiple edits before finalizing the changes.

Once the admin is finished editing, they can confirm the order edit, which finalizes it to later be applied on the order. You've already implemented applying the order edit on the order when the customer accepts the quote.

So, you still need two implement two aspects: updating the quote items, and confirming the order edit. You'll implement these in the next steps.

### Add Hooks

To implement the edit quote items functionality, you'll need two hooks:

1. A hook that updates a quote item's quantity and price using the Order Edits API routes.
2. A hook that confirms the edit of the items using the Order Edits API routes.

#### Update Quote Item Hook

The first hook updates an item's quantity and price using the Order Edits API routes. You'll use this whenever an admin updates an item's quantity or price.

In `src/admin/hooks/quotes.tsx`, add the following hook:

```tsx title="src/admin/hooks/quotes.tsx"
// other imports...
import { HttpTypes } from "@medusajs/framework/types"

// ...

export const useUpdateQuoteItem = (
  id: string,
  options?: UseMutationOptions<
    HttpTypes.AdminOrderEditPreviewResponse,
    FetchError,
    UpdateQuoteItemParams
  >
) => {
  const queryClient = useQueryClient()

  return useMutation({
    mutationFn: ({
      itemId,
      ...payload
    }: UpdateQuoteItemParams) => {
      return sdk.admin.orderEdit.updateOriginalItem(id, itemId, payload)
    },
    onSuccess: (data: any, variables: any, context: any) => {
      queryClient.invalidateQueries({
        queryKey: [orderPreviewQueryKey, id],
      })

      options?.onSuccess?.(data, variables, context)
    },
    ...options,
  })
}
```

You create a `useUpdateQuoteItem` hook that accepts the quote's ID and optional options as parameters. In the hook, you use the `useMutation` hook to define the mutation action that updates an item's quantity and price using the `sdk.admin.orderEdit.updateOriginalItem` method.

When the mutation is invoked, the hook invalidates the quote's data in the query client, which will trigger a re-fetch of the data.

#### Confirm Order Edit Hook

Next, you'll add a hook that confirms the order edit. This hook will be used when the admin is done editing the quote's items. As mentioned earlier, confirming the order edit doesn't apply the changes to the order but finalizes the edit.

In `src/admin/hooks/quotes.tsx`, add the following hook:

```tsx title="src/admin/hooks/quotes.tsx"
export const useConfirmQuote = (
  id: string,
  options?: UseMutationOptions<
    HttpTypes.AdminOrderEditPreviewResponse,
    FetchError,
    void
  >
) => {
  const queryClient = useQueryClient()

  return useMutation({
    mutationFn: () => sdk.admin.orderEdit.request(id),
    onSuccess: (data: any, variables: any, context: any) => {
      queryClient.invalidateQueries({
        queryKey: [orderPreviewQueryKey, id],
      })

      options?.onSuccess?.(data, variables, context)
    },
    ...options,
  })
}
```

You create a `useConfirmQuote` hook that accepts the quote's ID and optional options as parameters. In the hook, you use the `useMutation` hook to define the mutation action that confirms the order edit using the `sdk.admin.orderEdit.request` method.

When the mutation is invoked, the hook invalidates the quote's data in the query client, which will trigger a re-fetch of the data.

Now that you have the necessary hooks, you can use them in the UI route and its components.

### Add ManageItem Component

The UI route will show the list of items to the admin user and allows them to update the item's quantity and price. So, you'll create a component that allows the admin to manage a single item's details. You'll later use this component for each item in the quote.

![Screenshot of the manage item component in Medusa Admin](https://res.cloudinary.com/dza7lstvk/image/upload/v1741186495/Medusa%20Resources/manage-item-highlight_ouffnu.png)

Create the file `src/admin/components/manage-item.tsx` with the following content:

![Directory structure after adding the manage item component file](https://res.cloudinary.com/dza7lstvk/image/upload/v1741168152/Medusa%20Resources/quote-46_yxanj7.jpg)

```tsx
import { AdminOrder, AdminOrderPreview } from "@medusajs/framework/types"
import {
  Badge,
  CurrencyInput,
  Hint,
  Input,
  Label,
  Text,
  toast,
} from "@medusajs/ui"
import { useMemo } from "react"
import {
  useUpdateQuoteItem,
} from "../hooks/quotes"
import { Amount } from "./amount"

type ManageItemProps = {
  originalItem: AdminOrder["items"][0];
  item: AdminOrderPreview["items"][0];
  currencyCode: string;
  orderId: string;
};

export function ManageItem({
  originalItem,
  item,
  currencyCode,
  orderId,
}: ManageItemProps) {
  const { mutateAsync: updateItem } = useUpdateQuoteItem(orderId)

  const isItemUpdated = useMemo(
    () => !!item.actions?.find((a) => a.action === "ITEM_UPDATE"),
    [item]
  )

  const onUpdate = async ({
    quantity,
    unit_price,
  }: {
    quantity?: number;
    unit_price?: number;
  }) => {
    if (
      typeof quantity === "number" &&
      quantity <= item.detail.fulfilled_quantity
    ) {
      toast.error("Quantity should be greater than the fulfilled quantity")
      return
    }

    try {
      await updateItem({
        quantity,
        unit_price,
        itemId: item.id,
      })
    } catch (e) {
      toast.error((e as any).message)
    }
  }
  
  // TODO render the item's details and input fields
}
```

You define a `ManageItem` component that accepts the following props:

- `originalItem`: The original item details from the quote. This is the item's details before any edits.
- `item`: The item's details from the quote's order preview. This is the item's details which may have been edited.
- `currencyCode`: The currency code of the quote's draft order.
- `orderId`: The ID of the quote's draft order.

In the component, you define the following variables:

- `updateItem`: The `mutateAsync` function returned by the `useUpdateQuoteItem` hook. This function updates the item's quantity and price using Medusa's Order Edits API routes.
- `isItemUpdated`: A boolean that indicates whether the item has been updated.

You also define an `onUpdate` function that will be called when the admin updates the item's quantity or price. The function sends a request to update the item's quantity and price using the `updateItem` function. If the quantity is less than or equal to the fulfilled quantity, you show an error message.

Next, you'll add a return statement to show the item's details and allow the admin to update the item's quantity and price. Replace the `TODO` with the following:

```tsx title="src/admin/components/manage-item.tsx"
return (
  <div
    key={item.quantity}
    className="bg-ui-bg-subtle shadow-elevation-card-rest my-2 rounded-xl "
  >
    <div className="flex flex-col items-center gap-x-2 gap-y-2 p-3 text-sm md:flex-row">
      <div className="flex flex-1 items-center justify-between">
        <div className="flex flex-row items-center gap-x-3">

          <div className="flex flex-col">
            <div>
              <Text className="txt-small" as="span" weight="plus">
                {item.title}{" "}
              </Text>

              {item.variant_sku && <span>({item.variant_sku})</span>}
            </div>
            <Text as="div" className="text-ui-fg-subtle txt-small">
              {item.product_title}
            </Text>
          </div>
        </div>

        {isItemUpdated && (
            <Badge
              size="2xsmall"
              rounded="full"
              color="orange"
              className="mr-1"
            >
              Modified
            </Badge>
        )}
      </div>

      <div className="flex flex-1 justify-between">
        <div className="flex flex-grow items-center gap-2">
          <Input
            className="bg-ui-bg-base txt-small w-[67px] rounded-lg [appearance:textfield] [&::-webkit-inner-spin-button]:appearance-none [&::-webkit-outer-spin-button]:appearance-none"
            type="number"
            disabled={item.detail.fulfilled_quantity === item.quantity}
            min={item.detail.fulfilled_quantity}
            defaultValue={item.quantity}
            onBlur={(e) => {
              const val = e.target.value
              const quantity = val === "" ? null : Number(val)

              if (quantity) {
                onUpdate({ quantity })
              }
            }}
          />
          <Text className="txt-small text-ui-fg-subtle">
            Quantity
          </Text>
        </div>

        <div className="text-ui-fg-subtle txt-small mr-2 flex flex-shrink-0">
          <Amount
            currencyCode={currencyCode}
            amount={item.total}
            originalAmount={originalItem?.total}
          />
        </div>
      </div>
    </div>

    <div className="grid grid-cols-1 gap-2 p-3 md:grid-cols-2">
      <div className="flex flex-col gap-y-1">
        <Label>Price</Label>
        <Hint className="!mt-1">
          Override the unit price of this product
        </Hint>
      </div>

      <div className="flex items-center gap-1">
        <div className="flex-grow">
          <CurrencyInput
            symbol={currencyCode}
              code={currencyCode}
            defaultValue={item.unit_price}
            type="numeric"
            min={0}
            onBlur={(e) => {
              onUpdate({
                unit_price: parseFloat(e.target.value),
                quantity: item.quantity,
              })
            }}
            className="bg-ui-bg-field-component hover:bg-ui-bg-field-component-hover"
          />
        </div>
      </div>
    </div>
  </div>
)
```

You show the item's title, product title, and variant SKU. If the item has been updated, you show a "Modified" badge.

You also show input fields for the quantity and price of the item, allowing the admin to update the item's quantity and price. Once the admin updates the quantity or price, the `onUpdate` function is called to send a request to update the item's details.

### Add ManageQuoteForm Component

Next, you'll add the form component that shows the list of items in the quote and allows the admin to manage each item. You'll use the `ManageItem` component you created in the previous step for each item in the quote.

![Screenshot of the manage quote form in the Medusa Admin](https://res.cloudinary.com/dza7lstvk/image/upload/v1741186643/Medusa%20Resources/manage-quote-form-highlight_pfyee5.png)

Create the file `src/admin/components/manage-quote-form.tsx` with the following content:

![Directory structure after adding the manage quote form component file](https://res.cloudinary.com/dza7lstvk/image/upload/v1741168581/Medusa%20Resources/quote-47_f5kamq.jpg)

```tsx title="src/admin/components/manage-quote-form.tsx"
import { AdminOrder } from "@medusajs/framework/types"
import { Button, Heading, toast } from "@medusajs/ui"
import { useConfirmQuote } from "../hooks/quotes"
import { formatAmount } from "../utils/format-amount"
import { useOrderPreview } from "../hooks/order-preview"
import { useNavigate, useParams } from "react-router-dom"
import { useMemo } from "react"
import { ManageItem } from "./manage-item"

type ReturnCreateFormProps = {
  order: AdminOrder;
};

export const ManageQuoteForm = ({ order }: ReturnCreateFormProps) => {
  const { order: preview } = useOrderPreview(order.id)
  const navigate = useNavigate()
  const { id: quoteId } = useParams()

  const { mutateAsync: confirmQuote, isPending: isRequesting } =
    useConfirmQuote(order.id)

  const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
    e.preventDefault()
    try {
      await confirmQuote()
      navigate(`/quotes/${quoteId}`)

      toast.success("Successfully updated quote")
    } catch (e) {
      toast.error("Error", {
        description: (e as any).message,
      })
    }
  }
  
    const originalItemsMap = useMemo(() => {
      return new Map(order.items.map((item) => [item.id, item]))
    }, [order])

  if (!preview) {
    return <></>
  }

  // TODO render form
}
```

You define a `ManageQuoteForm` component that accepts the quote's draft order as a prop. In the component, you retrieve the preview of that order. The preview holds any edits made on the order's items.

You also define the `confirmQuote` function using the `useConfirmQuote` hook. This function confirms the order edit, finalizing the changes made on the order's items.

Then, you define the `handleSubmit` function that will be called when the admin submits the form. The function confirms the order edit using the `confirmQuote` function and navigates the admin back to the quote's details page.

Next, you'll add a return statement to show the edit form for the quote's items. Replace the `TODO` with the following:

```tsx title="src/admin/components/manage-quote-form.tsx"
return (
  <form onSubmit={handleSubmit} className="flex h-full flex-col p-4 gap-2">
    <div>
      <div className="mb-3 mt-8 flex items-center justify-between">
        <Heading level="h2">Items</Heading>
      </div>

      {preview.items.map((item) => (
        <ManageItem
          key={item.id}
          originalItem={originalItemsMap.get(item.id)!}
          item={item}
          orderId={order.id}
          currencyCode={order.currency_code}
        />
      ))}
    </div>

    <div className="mt-8 border-y border-dotted py-4">
      <div className="mb-2 flex items-center justify-between">
        <span className="txt-small text-ui-fg-subtle">
          Current Total
        </span>

        <span className="txt-small text-ui-fg-subtle">
          {formatAmount(order.total, order.currency_code)}
        </span>
      </div>

      <div className="mb-2 flex items-center justify-between">
        <span className="txt-small text-ui-fg-subtle">
          New Total
        </span>

        <span className="txt-small text-ui-fg-subtle">
          {formatAmount(preview.total, order.currency_code)}
        </span>
      </div>
    </div>

    <div className="flex w-full items-center justify-end gap-x-4">
      <div className="flex items-center justify-end gap-x-2">
        <Button
          key="submit-button"
          type="submit"
          variant="primary"
          size="small"
          disabled={isRequesting}
        >
          Confirm Edit
        </Button>
      </div>
    </div>
  </form>
)
```

You use the `ManageItem` component to show each item in the quote and allow the admin to update the item's quantity and price. You also show the updated total amount of the quote and a button to confirm the order edit.

You'll use this component next in the UI route that allows the admin to edit the quote's items.

### Implement UI Route

Finally, you'll add the UI route that allows the admin to edit the quote's items. The route will use the `ManageQuoteForm` component you created in the previous step.

Create the file `src/admin/routes/quotes/[id]/manage/page.tsx` with the following content:

![Directory structure after adding the edit quote items UI route file](https://res.cloudinary.com/dza7lstvk/image/upload/v1741168993/Medusa%20Resources/quote-48_roangs.jpg)

```tsx title="src/admin/routes/quotes/[id]/manage/page.tsx"
import { useParams } from "react-router-dom"
import { useQuote } from "../../../../hooks/quotes"
import { Container, Heading, Toaster } from "@medusajs/ui"
import { ManageQuoteForm } from "../../../../components/manage-quote-form"

const QuoteManage = () => {
  const { id } = useParams()
  const { quote, isLoading } = useQuote(id!, {
    fields:
      "*draft_order.customer",
  })

  if (isLoading) {
    return <></>
  }

  if (!quote) {
    throw "quote not found"
  }

  return (
    <>
      <Container className="divide-y p-0">
        <Heading className="flex items-center justify-between px-6 py-4">
          Manage Quote
        </Heading>

        <ManageQuoteForm order={quote.draft_order} />
      </Container>
      <Toaster />
    </>
  )
}

export default QuoteManage
```

You define a `QuoteManage` component that will show the form to manage the quote's items in the Medusa Admin dashboard.

In the component, you first retrieve the quote's details using the `useQuote` hook. Then, you show the `ManageQuoteForm` component, passing the quote's draft order as a prop.

### Add Manage Button to Quote Details Page

To allow the admin to access the manage page you just added, you'll add a new button on the quote's details page that links to the manage page.

In `src/admin/routes/quotes/[id]/page.tsx`, add the following variable definition after the `showSendQuote` variable:

```tsx title="src/admin/routes/quotes/[id]/page.tsx"
const [showManageQuote, setShowManageQuote] = useState(false)
```

This variable will be used to show or hide the manage quote button.

Then, update the existing `useEffect` hook to the following:

```tsx title="src/admin/routes/quotes/[id]/page.tsx"
useEffect(() => {
  if (["pending_merchant", "customer_rejected"].includes(quote?.status!)) {
    setShowSendQuote(true)
  } else {
    setShowSendQuote(false)
  }

  if (
    ["customer_rejected", "merchant_rejected", "accepted"].includes(
      quote?.status!
    )
  ) {
    setShowRejectQuote(false)
  } else {
    setShowRejectQuote(true)
  }

  if (![
    "pending_merchant",
    "customer_rejected",
    "merchant_rejected",
  ].includes(quote?.status!)) {
    setShowManageQuote(false)
  } else {
    setShowManageQuote(true)
  }
}, [quote])
```

The `showManageQuote` variable is now updated based on the quote's status, where you only show it if the quote is pending the merchant's action, or if it has been rejected by either the customer or merchant.

Finally, add the following button component after the `Send Quote` button:

```tsx title="src/admin/routes/quotes/[id]/page.tsx"
{showManageQuote && (
  <Button
    size="small"
    variant="secondary"
    onClick={() => navigate(`/quotes/${quote.id}/manage`)}
  >
    Manage Quote
  </Button>
)}
```

The Manage Quote button is now shown if the `showManageQuote` variable is `true`. When clicked, it navigates the admin to the manage quote page.

### Test Edit Quote Items UI Route

To test the edit quote items UI route, start the Medusa application:

```bash npm2yarn
npm run dev
```

Then, open the Medusa Admin dashboard at `http://localhost:9000/admin`. Open a quote's details page whose status is either `pending_merchant`, `merchant_rejected` or `customer_rejected`. You'll find a new "Manage Quote" button.

![Manage Quote button on quote's details page in Medusa Admin](https://res.cloudinary.com/dza7lstvk/image/upload/v1741169567/Medusa%20Resources/Screenshot_2025-03-05_at_12.12.21_PM_c5fhsp.png)

Click on the button, and you'll be taken to the manage quote page where you can update the quote's items. Try to update the items' quantities or price. Then, once you're done, click the "Confirm Edit" button to finalize the changes.

![Edit quote items page in Medusa Admin](https://res.cloudinary.com/dza7lstvk/image/upload/v1741169659/Medusa%20Resources/Screenshot_2025-03-05_at_12.14.05_PM_ufvkqb.png)

The changes can now be previewed from the quote's details page. The customer can also see these changes using the preview API route you created earlier. Once the customer accepts the quote, the changes will be applied to the order.

***

## Next Steps

You've now implemented quote management features in Medusa. There's still more that you can implement to enhance the quote management experience:

- Refer to the [B2B starter](https://github.com/medusajs/b2b-starter-medusa) for more quote-management related features, including how to add or remove items from a quote, and how to allow messages between the customer and the merchant.
- To build a storefront, refer to the [Storefront development guide](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/storefront-development/index.html.md). You can also add to the storefront features related to quote-management using the APIs you implemented in this guide.

If you're new to Medusa, check out the [main documentation](https://docs.medusajs.com/docs/learn/index.html.md), 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](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/index.html.md).


# How-to & Tutorials

In this section of the documentation, you'll find how-to guides and tutorials that will help you customize the Medusa server and admin. These guides are useful after you've learned Medusa's main concepts in the [Get Started](https://docs.medusajs.com/docs/learn/index.html.md) section of the documentation.

You can follow these guides to learn how to customize the Medusa server and admin to fit your business requirements. This section of the documentation also includes deployment guides to help you deploy your Medusa server and admin to different platforms.

## Example Snippets

For a quick access to code snippets of the different concepts you learned about, such as API routes and workflows, refer to the [Examples Snippets](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/examples/index.html.md) documentation.

***

***

## Deployment Guides

Deployment guides are a collection of guides that help you deploy your Medusa server and admin to different platforms. Learn more in the [Deployment Overview](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/deployment/index.html.md) documentation.


# Send Abandoned Cart Notifications in Medusa

In this tutorial, you will learn how to send notifications to customers who have abandoned their carts.

When you install a Medusa application, you get a fully-fledged commerce platform with a Framework for customization. The Medusa application's commerce features are built around [Commerce Modules](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/index.html.md), which are available out-of-the-box. These features include cart-management capabilities.

Medusa's [Notification Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/infrastructure-modules/notification/index.html.md) allows you to send notifications to users or customers, such as password reset emails, order confirmation SMS, or other types of notifications.

In this tutorial, you will use the Notification Module to send an email to customers who have abandoned their carts. The email will contain a link to recover the customer's cart, encouraging them to complete their purchase. You will use SendGrid to send the emails, but you can also use other email providers.

## Summary

By following this tutorial, you will:

- Install and set up Medusa.
- Create the logic to send an email to customers who have abandoned their carts.
- Run the above logic once a day.
- Add a route to the storefront to recover the cart.

![Diagram illustrating the flow of the abandoned-cart functionalities](https://res.cloudinary.com/dza7lstvk/image/upload/v1742460588/Medusa%20Resources/abandoned-cart-summary_fcf2tn.jpg)

[View on Github](https://github.com/medusajs/examples/tree/main/abandoned-cart): Find the full code for this tutorial.

***

## Step 1: Install a Medusa Application

### Prerequisites

- [Node.js v20+](https://nodejs.org/en/download)
- [Git CLI tool](https://git-scm.com/downloads)
- [PostgreSQL](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 will first be asked for the project's name. Then, when asked whether you want to install the [Next.js Starter Storefront](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/nextjs-starter/index.html.md), choose "Yes."

Afterwards, the installation process will start, which will install the Medusa application in a directory with your project's name and the Next.js Starter Storefront in a separate directory with the `{project-name}-storefront` name.

The Medusa application is composed of a headless Node.js server and an admin dashboard. The storefront is installed or custom-built separately and connects to the Medusa application through its REST endpoints, called [API routes](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/index.html.md). Learn more in [Medusa's Architecture documentation](https://docs.medusajs.com/docs/learn/introduction/architecture/index.html.md).

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.

Check out the [troubleshooting guides](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/troubleshooting/create-medusa-app-errors/index.html.md) for help.

***

## Step 2: Set up SendGrid

### Prerequisites

- [SendGrid account](https://sendgrid.com)
- [Verified Sender Identity](https://mc.sendgrid.com/senders)
- [SendGrid API Key](https://app.sendgrid.com/settings/api_keys)

Medusa's Notification Module provides the general functionality to send notifications, but the sending logic is implemented in a module provider. This allows you to integrate the email provider of your choice.

To send the cart-abandonment emails, you will use SendGrid. Medusa provides a [SendGrid Notification Module Provider](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/infrastructure-modules/notification/sendgrid/index.html.md) that you can use to send emails.

Alternatively, you can use [other Notification Module Providers](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/infrastructure-modules/notification#what-is-a-notification-module-provider/index.html.md) or [create a custom provider](https://docs.medusajs.com/references/notification-provider-module/index.html.md).

To set up SendGrid, add the SendGrid Notification Module Provider to `medusa-config.ts`:

```ts title="medusa-config.ts"
module.exports = defineConfig({
  // ...
  modules: [
    {
      resolve: "@medusajs/medusa/notification",
      options: {
        providers: [
          {
            resolve: "@medusajs/medusa/notification-sendgrid",
            id: "sendgrid",
            options: {
              channels: ["email"],
              api_key: process.env.SENDGRID_API_KEY,
              from: process.env.SENDGRID_FROM,
            },
          },
        ],
      },
    },
  ],
})
```

In the `modules` configuration, you pass the Notification Provider and add SendGrid as a provider. You also pass to the SendGrid Module Provider the following options:

- `channels`: The channels that the provider supports. In this case, it is only email.
- `api_key`: Your SendGrid API key.
- `from`: The email address that the emails will be sent from.

Then, set the SendGrid API key and "from" email as environment variables, such as in the `.env` file at the root of your project:

```plain
SENDGRID_API_KEY=your-sendgrid-api-key
SENDGRID_FROM=test@gmail.com
```

You can now use SendGrid to send emails in Medusa.

***

## Step 3: Send Abandoned Cart Notification Flow

You will now implement the sending logic for the abandoned cart notifications.

To build custom commerce features in Medusa, you create a [workflow](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md). 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 is 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 scheduled job.

In this step, you will create the workflow that sends the abandoned cart notifications. Later, you will learn how to execute it once a day.

The workflow will receive the list of abandoned carts as an input. The workflow has the following steps:

- [sendAbandonedNotificationsStep](#sendAbandonedNotificationsStep): Send the abandoned cart notifications.
- [updateCartsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateCartsStep/index.html.md): Update the cart to store the last notification date.

Medusa provides the second step in its `@medusajs/medusa/core-flows` package. So, you only need to implement the first one.

### sendAbandonedNotificationsStep

The first step of the workflow sends a notification to the owners of the abandoned carts that are passed as an input.

To implement the step, create the file `src/workflows/steps/send-abandoned-notifications.ts` with the following content:

```ts title="src/workflows/steps/send-abandoned-notifications.ts"
import { 
  createStep,
  StepResponse, 
} from "@medusajs/framework/workflows-sdk"
import { Modules } from "@medusajs/framework/utils"
import { CartDTO, CustomerDTO } from "@medusajs/framework/types"

type SendAbandonedNotificationsStepInput = {
  carts: (CartDTO & {
    customer: CustomerDTO
  })[]
}

export const sendAbandonedNotificationsStep = createStep(
  "send-abandoned-notifications",
  async (input: SendAbandonedNotificationsStepInput, { container }) => {
    const notificationModuleService = container.resolve(
      Modules.NOTIFICATION
    )

    const notificationData = input.carts.map((cart) => ({
      to: cart.email!,
      channel: "email", 
      template: process.env.ABANDONED_CART_TEMPLATE_ID || "",
      data: {
        customer: {
          first_name: cart.customer?.first_name || cart.shipping_address?.first_name,
          last_name: cart.customer?.last_name || cart.shipping_address?.last_name,
        },
        cart_id: cart.id,
        items: cart.items?.map((item) => ({
          product_title: item.title,
          quantity: item.quantity,
          unit_price: item.unit_price,
          thumbnail: item.thumbnail,
        })),
      },
    }))

    const notifications = await notificationModuleService.createNotifications(
      notificationData
    )

    return new StepResponse({
      notifications,
    })
  }
)
```

You create a step with `createStep` from the Workflows SDK. It accepts two parameters:

1. The step's unique name, which is `create-review`.
2. An async function that receives two parameters:
   - The step's input, which is in this case an object with the review's properties.
   - An object that has properties including the [Medusa container](https://docs.medusajs.com/docs/learn/fundamentals/medusa-container/index.html.md), which is a registry of Framework and commerce tools that you can access in the step.

In the step function, you first resolve the Notification Module's service, which has methods to manage notifications. Then, you prepare the data of each notification, and create the notifications with the `createNotifications` method.

Notice that each notification is an object with the following properties:

- `to`: The email address of the customer.
- `channel`: The channel that the notification will be sent through. The Notification Module uses the provider registered for the channel.
- `template`: The ID or name of the email template in the third-party provider. Make sure to set it as an environment variable once you have it.
- `data`: The data to pass to the template to render the email's dynamic content.

Based on the dynamic template you create in SendGrid or another provider, you can pass different data in the `data` object.

A step function must return a `StepResponse` instance. The `StepResponse` constructor accepts the step's output as a parameter, which is the created notifications.

### Create Workflow

You can now create the workflow that uses the step you just created to send the abandoned cart notifications.

Create the file `src/workflows/send-abandoned-carts.ts` with the following content:

```ts title="src/workflows/send-abandoned-carts.ts"
import {
  createWorkflow,
  WorkflowResponse,
  transform,
} from "@medusajs/framework/workflows-sdk"
import { 
  sendAbandonedNotificationsStep,
} from "./steps/send-abandoned-notifications"
import { updateCartsStep } from "@medusajs/medusa/core-flows"
import { CartDTO } from "@medusajs/framework/types"
import { CustomerDTO } from "@medusajs/framework/types"

export type SendAbandonedCartsWorkflowInput = {
  carts: (CartDTO & {
    customer: CustomerDTO
  })[]
}

export const sendAbandonedCartsWorkflow = createWorkflow(
  "send-abandoned-carts",
  function (input: SendAbandonedCartsWorkflowInput) {
    sendAbandonedNotificationsStep(input)

    const updateCartsData = transform(
      input,
      (data) => {
        return data.carts.map((cart) => ({
          id: cart.id,
          metadata: {
            ...cart.metadata,
            abandoned_notification: new Date().toISOString(),
          },
        }))
      }
    )

    const updatedCarts = updateCartsStep(updateCartsData)

    return new WorkflowResponse(updatedCarts)
  }
)
```

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 arra of carts.

In the workflow's constructor function, you:

- Use the `sendAbandonedNotificationsStep` to send the notifications to the carts' customers.
- Use the `updateCartsStep` from Medusa's core flows to update the carts' metadata with the last notification date.

Notice that you use the `transform` function to prepare the `updateCartsStep`'s input. Medusa does not support direct data manipulation in a workflow's constructor function. You can learn more about it in the [Data Manipulation in Workflows documentation](https://docs.medusajs.com/docs/learn/fundamentals/workflows/variable-manipulation/index.html.md).

Your workflow is now ready for use. You will learn how to execute it in the next section.

### Setup Email Template

Before you can test the workflow, you need to set up an email template in SendGrid. The template should contain the dynamic content that you pass in the workflow's step.

To create an email template in SendGrid:

- Go to [Dynamic Templates](https://mc.sendgrid.com/dynamic-templates) in the SendGrid dashboard.
- Click on the "Create Dynamic Template" button.

![Button is at the top right of the page](https://res.cloudinary.com/dza7lstvk/image/upload/v1742457153/Medusa%20Resources/Screenshot_2025-03-20_at_9.51.38_AM_g5nk80.png)

- In the side window that opens, enter a name for the template, then click on the Create button.
- The template will be added to the middle of the page. When you click on it, a new section will show with an "Add Version" button. Click on it.

![The template is a collapsible in the middle of the page,with the "Add Version" button shown in the middle](https://res.cloudinary.com/dza7lstvk/image/upload/v1742458096/Medusa%20Resources/Screenshot_2025-03-20_at_10.07.54_AM_y2dys7.png)

In the form that opens, you can either choose to start with a blank template or from an existing design. You can then use the drag-and-drop or code editor to design the email template.

You can also use the following template as an example:

```html title="Abandoned Cart Email Template"
<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>Complete Your Purchase</title>
    <style>
        body {
            font-family: Arial, sans-serif;
            background-color: #f8f9fa;
            margin: 0;
            padding: 20px;
        }
        .container {
            max-width: 600px;
            background: #ffffff;
            padding: 20px;
            border-radius: 10px;
            box-shadow: 0px 4px 10px rgba(0, 0, 0, 0.1);
            text-align: center;
        }
        .header {
            font-size: 26px;
            font-weight: bold;
            color: #333;
            margin-bottom: 20px;
        }
        .message {
            font-size: 16px;
            color: #555;
            margin-bottom: 20px;
        }
        .item {
            display: flex;
            align-items: center;
            background: #f9f9f9;
            padding: 10px;
            border-radius: 8px;
            margin-bottom: 15px;
        }
        .item img {
            width: 80px;
            height: auto;
            margin-right: 15px;
            border-radius: 5px;
        }
        .item-details {
            text-align: left;
            flex-grow: 1;
        }
        .item-details strong {
            font-size: 18px;
            color: #333;
        }
        .item-details p {
            font-size: 14px;
            color: #777;
            margin: 5px 0;
        }
        .button {
            display: inline-block;
            background-color: #007bff;
            color: #ffffff;
            text-decoration: none;
            font-size: 18px;
            padding: 12px 20px;
            border-radius: 5px;
            margin-top: 20px;
            transition: background 0.3s ease;
        }
        .button:hover {
            background-color: #0056b3;
        }
        .footer {
            font-size: 12px;
            color: #888;
            margin-top: 20px;
        }
    </style>
</head>
<body>
    <div class="container">
        <div class="header">Hi {{customer.first_name}}, your cart is waiting! 🛍️</div>
        <p class="message">You left some great items in your cart. Complete your purchase before they're gone!</p>
        
        {{#each items}}
        <div class="item">
            <img src="{{thumbnail}}" alt="{{product_title}}">
            <div class="item-details">
                <strong>{{product_title}}</strong>
                <p>{{subtitle}}</p>
                <p>Quantity: <strong>{{quantity}}</strong></p>
                <p>Price: <strong>$ {{unit_price}}</strong></p>
            </div>
        </div>
        {{/each}}
        
        <a href="https://yourstore.com/cart/recover/{{cart_id}}" class="button">Return to Cart & Checkout</a>
        <p class="footer">Need help? <a href="mailto:support@yourstore.com">Contact us</a></p>
    </div>
</body>
</html>
```

This template will show each item's image, title, quantity, and price in the cart. It will also show a button to return to the cart and checkout.

You can replace `https://yourstore.com` with your storefront's URL. You'll later implement the `/cart/recover/:cart_id` route in the storefront to recover the cart.

Once you are done, copy the template ID from SendGrid and set it as an environment variable in your Medusa project:

```plain
ABANDONED_CART_TEMPLATE_ID=your-sendgrid-template-id
```

***

## Step 4: Schedule Cart Abandonment Notifications

The next step is to automate sending the abandoned cart notifications. You need a task that runs once a day to find the carts that have been abandoned for a certain period and send the notifications to the customers.

To run a task at a scheduled interval, you can use a [scheduled job](https://docs.medusajs.com/docs/learn/fundamentals/scheduled-jobs/index.html.md). A scheduled job is an asynchronous function that the Medusa application runs at the interval you specify during the Medusa application's runtime.

You can create a scheduled job in a TypeScript or JavaScript file under the `src/jobs` directory. So, to create the scheduled job that sends the abandoned cart notifications, create the file `src/jobs/send-abandoned-cart-notification.ts` with the following content:

```ts title="src/jobs/send-abandoned-cart-notification.ts"
import { MedusaContainer } from "@medusajs/framework/types"
import { 
  sendAbandonedCartsWorkflow, 
  SendAbandonedCartsWorkflowInput,
} from "../workflows/send-abandoned-carts"

export default async function abandonedCartJob(
  container: MedusaContainer
) {
  const logger = container.resolve("logger")
  const query = container.resolve("query")

  const oneDayAgo = new Date()
  oneDayAgo.setDate(oneDayAgo.getDate() - 1)
  const limit = 100
  const offset = 0
  const totalCount = 0
  const abandonedCartsCount = 0

  do {
    // TODO retrieve paginated abandoned carts
  } while (offset < totalCount)

  logger.info(`Sent ${abandonedCartsCount} abandoned cart notifications`)
}

export const config = {
  name: "abandoned-cart-notification",
  schedule: "0 0 * * *", // Run at midnight every day
}
```

In a scheduled job's file, you must export:

1. An asynchronous function that holds the job's logic. The function receives the [Medusa container](https://docs.medusajs.com/docs/learn/fundamentals/medusa-container/index.html.md) as a parameter.
2. A `config` object that specifies the job's name and schedule. The schedule is a [cron expression](https://crontab.guru/) that defines the interval at which the job runs.

In the scheduled job function, so far you resolve the [Logger](https://docs.medusajs.com/docs/learn/debugging-and-testing/logging/index.html.md) to log messages, and [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md) to retrieve data across modules.

You also define a `oneDayAgo` date, which is the date that you will use as the condition of an abandoned cart. In addition, you define variables to paginate the carts.

Next, you will retrieve the abandoned carts using Query. Replace the `TODO` with the following:

```ts title="src/jobs/send-abandoned-cart-notification.ts"
const { 
  data: abandonedCarts, 
  metadata,
} = await query.graph({
  entity: "cart",
  fields: [
    "id",
    "email",
    "items.*",
    "metadata",
    "customer.*",
  ],
  filters: {
    updated_at: {
      $lt: oneDayAgo,
    },
    // @ts-ignore
    email: {
      $ne: null,
    },
    // @ts-ignore
    completed_at: null,
  },
  pagination: {
    skip: offset,
    take: limit,
  },
})

totalCount = metadata?.count ?? 0
const cartsWithItems = abandonedCarts.filter((cart) => 
  cart.items?.length > 0 && !cart.metadata?.abandoned_notification
)

try {
  await sendAbandonedCartsWorkflow(container).run({
    input: {
      carts: cartsWithItems,
    } as unknown as SendAbandonedCartsWorkflowInput,
  })
  abandonedCartsCount += cartsWithItems.length

} catch (error) {
  logger.error(
    `Failed to send abandoned cart notification: ${error.message}`
  )
}

offset += limit
```

In the do-while loop, you use Query to retrieve carts matching the following criteria:

- The cart was last updated more than a day ago.
- The cart has an email address.
- The cart has not been completed.

You also filter the retrieved carts to only include carts with items and customers that have not received an abandoned cart notification.

Finally, you execute the `sendAbandonedCartsWorkflow` passing it the abandoned carts as an input. You will execute the workflow for each paginated batch of carts.

### Test it Out

To test out the scheduled job and workflow, it is recommended to change the `oneDayAgo` date to a minute before now for easy testing:

```ts title="src/jobs/send-abandoned-cart-notification.ts"
oneDayAgo.setMinutes(oneDayAgo.getMinutes() - 1) // For testing
```

And to change the job's schedule in `config` to run every minute:

```ts title="src/jobs/send-abandoned-cart-notification.ts"
export const config = {
  // ...
  schedule: "* * * * *", // Run every minute for testing
}
```

Finally, start the Medusa application with the following command:

```bash npm2yarn
npm run dev
```

And in the [Next.js Starter Storefront](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/nextjs-starter/index.html.md)'s directory (that you installed in the first step), start the storefront with the following command:

```bash npm2yarn
npm run dev
```

Open the storefront at `localhost:8000`. You can either:

- Create an account and add items to the cart, then leave the cart for a minute.
- Add an item to the cart as a guest. Then, start the checkout process, but only enter the shipping and email addresses, and leave the cart for a minute.

Afterwards, wait for the job to execute. Once it is executed, you will see the following message in the terminal:

```bash
info:    Sent 1 abandoned cart notifications
```

Once you're done testing, make sure to revert the changes to the `oneDayAgo` date and the job's schedule.

***

## Step 5: Recover Cart in Storefront

In the storefront, you need to add a route that recovers the cart when the customer clicks on the link in the email. The route should receive the cart ID, set the cart ID in the cookie, and redirect the customer to the cart page.

To implement the route, in the Next.js Starter Storefront create the file `src/app/[countryCode]/(main)/cart/recover/[id]/route.tsx` with the following content:

```tsx title="src/app/[countryCode]/(main)/cart/recover/[id]/route.tsx" badgeLabel="Storefront" badgeColor="blue"
import { NextRequest } from "next/server"
import { retrieveCart } from "../../../../../../lib/data/cart"
import { setCartId } from "../../../../../../lib/data/cookies"
import { notFound, redirect } from "next/navigation"
type Params = Promise<{
  id: string
}>

export async function GET(req: NextRequest, { params }: { params: Params }) {
  const { id } = await params
  const cart = await retrieveCart(id)

  if (!cart) {
    return notFound()
  }

  setCartId(id)

  const countryCode = cart.shipping_address?.country_code || 
    cart.region?.countries?.[0]?.iso_2

  redirect(
    `/${countryCode ? `${countryCode}/` : ""}cart`
  )
}
```

You add a `GET` route handler that receives the cart ID as a path parameter. In the route handler, you:

- Try to retrieve the cart from the Medusa application. The `retrieveCart` function is already available in the Next.js Starter Storefront. If the cart is not found, you return a 404 response.
- Set the cart ID in a cookie using the `setCartId` function. This is also a function that is already available in the storefront.
- Redirect the customer to the cart page. You set the country code in the URL based on the cart's shipping address or region.

### Test it Out

To test it out, start the Medusa application:

```bash npm2yarn
npm run dev
```

And in the Next.js Starter Storefront's directory, start the storefront:

```bash npm2yarn
npm run dev
```

Then, either open the link in an abandoned cart email or navigate to `localhost:8000/cart/recover/:cart_id` in your browser. You will be redirected to the cart page with the recovered cart.

![Cart page in the storefront](https://res.cloudinary.com/dza7lstvk/image/upload/v1742459552/Medusa%20Resources/Screenshot_2025-03-20_at_10.32.17_AM_frmbup.png)

***

## Next Steps

You have now implemented the logic to send abandoned cart notifications in Medusa. You can implement other customizations with Medusa, such as:

- [Implement Product Reviews](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/how-to-tutorials/tutorials/product-reviews/index.html.md).
- [Implement Wishlist](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/plugins/guides/wishlist/index.html.md).
- [Allow Custom-Item Pricing](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/examples/guides/custom-item-price/index.html.md).

If you are new to Medusa, check out the [main documentation](https://docs.medusajs.com/docs/learn/index.html.md), where you will get a more in-depth learning of all the concepts you have used in this guide and more.

To learn more about the commerce features that Medusa provides, check out Medusa's [Commerce Modules](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/index.html.md).


# Implement Loyalty Points System in Medusa

In this tutorial, you'll learn how to implement a loyalty points system in Medusa.

Medusa Cloud provides a beta Store Credits feature that facilitates building a loyalty point system. [Get in touch](https://medusajs.com/contact) for early access.

When you install a Medusa application, you get a fully-fledged commerce platform with a Framework for customization. The Medusa application's commerce features are built around [Commerce Modules](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/index.html.md), which are available out-of-the-box. These features include management capabilities related to carts, orders, promotions, and more.

A loyalty point system allows customers to earn points for purchases, which can be redeemed for discounts or rewards. In this tutorial, you'll learn how to customize the Medusa application to implement a loyalty points system.

You can follow this tutorial whether you're new to Medusa or an advanced Medusa developer.

## Summary

By following this tutorial, you will learn how to:

- Install and set up Medusa.
- Define models to store loyalty points and the logic to manage them.
- Build flows that allow customers to earn and redeem points during checkout.
  - Points are redeemed through dynamic promotions specific to the customer.
- Customize the cart completion flow to validate applied loyalty points.

![Diagram illustrating redeem loyalty points flow](https://res.cloudinary.com/dza7lstvk/image/upload/v1744126213/Medusa%20Resources/redeem-points-flow_kzgkux.jpg)

- [Loyalty Points Repository](https://github.com/medusajs/examples/tree/main/loyalty-points): Find the full code for this guide in this repository.
- [OpenApi Specs for Postman](https://res.cloudinary.com/dza7lstvk/raw/upload/v1744212595/OpenApi/Loyalty-Points_jwi5e9.yaml): Import this OpenApi Specs file into tools like Postman.

***

## Step 1: Install a Medusa Application

### Prerequisites

- [Node.js v20+](https://nodejs.org/en/download)
- [Git CLI tool](https://git-scm.com/downloads)
- [PostgreSQL](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 asked whether you want to install the [Next.js Starter Storefront](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/nextjs-starter/index.html.md), choose Yes.

Afterward, the installation process will start, which will install the Medusa application in a directory with your project's name, and the Next.js Starter Storefront in a separate directory with the `{project-name}-storefront` name.

The Medusa application is composed of a headless Node.js server and an admin dashboard. The storefront is installed or custom-built separately and connects to the Medusa application through its REST endpoints, called [API routes](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/index.html.md). Learn more in [Medusa's Architecture documentation](https://docs.medusajs.com/docs/learn/introduction/architecture/index.html.md).

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. Afterward, you can log in with the new user and explore the dashboard.

Check out the [troubleshooting guides](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/troubleshooting/create-medusa-app-errors/index.html.md) for help.

***

## Step 2: Create Loyalty Module

In Medusa, you can build custom features in a [module](https://docs.medusajs.com/docs/learn/fundamentals/modules/index.html.md). A module is a reusable 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.

In the module, you define the data models necessary for a feature and the logic to manage these data models. Later, you can build commerce flows around your module.

In this step, you'll build a Loyalty Module that defines the necessary data models to store and manage loyalty points for customers.

Refer to the [Modules documentation](https://docs.medusajs.com/docs/learn/fundamentals/modules/index.html.md) to learn more.

### Create Module Directory

Modules are created under the `src/modules` directory of your Medusa application. So, create the directory `src/modules/loyalty`.

### Create Data Models

A data model represents a table in the database. You create data models using Medusa's Data Model Language (DML). It simplifies defining a table's columns, relations, and indexes with straightforward methods and configurations.

Refer to the [Data Models documentation](https://docs.medusajs.com/docs/learn/fundamentals/modules#1-create-data-model/index.html.md) to learn more.

For the Loyalty Module, you need to define a `LoyaltyPoint` data model that represents a customer's loyalty points. So, create the file `src/modules/loyalty/models/loyalty-point.ts` with the following content:

```ts title="src/modules/loyalty/models/loyalty-point.ts" highlights={dmlHighlights}
import { model } from "@medusajs/framework/utils"

const LoyaltyPoint = model.define("loyalty_point", {
  id: model.id().primaryKey(),
  points: model.number().default(0),
  customer_id: model.text().unique("IDX_LOYALTY_CUSTOMER_ID"), 
})

export default LoyaltyPoint
```

You define the `LoyaltyPoint` data model using the `model.define` method of the DML. It accepts the data model's table name as a first parameter, and the model's schema object as a second parameter.

The `LoyaltyPoint` data model has the following properties:

- `id`: A unique ID for the loyalty points.
- `points`: The number of loyalty points a customer has.
- `customer_id`: The ID of the customer who owns the loyalty points. This property has a unique index to ensure that each customer has only one record in the `loyalty_point` table.

Learn more about defining data model properties in the [Property Types documentation](https://docs.medusajs.com/docs/learn/fundamentals/data-models/properties/index.html.md).

### Create Module's Service

You now have the necessary data model in the Loyalty Module, but you'll need to manage its records. You do this by creating a service in the module.

A service is a TypeScript or JavaScript class that the module exports. In the service's methods, you can connect to the database, allowing you to manage your data models, or connect to a third-party service, which is useful if you're integrating with external services.

Refer to the [Module Service documentation](https://docs.medusajs.com/docs/learn/fundamentals/modules#2-create-service/index.html.md) to learn more.

To create the Loyalty Module's service, create the file `src/modules/loyalty/service.ts` with the following content:

```ts title="src/modules/loyalty/service.ts"
import { MedusaError, MedusaService } from "@medusajs/framework/utils"
import LoyaltyPoint from "./models/loyalty-point"
import { InferTypeOf } from "@medusajs/framework/types"

type LoyaltyPoint = InferTypeOf<typeof LoyaltyPoint>

class LoyaltyModuleService extends MedusaService({
  LoyaltyPoint,
}) {
  // TODO add methods
}

export default LoyaltyModuleService
```

The `LoyaltyModuleService` extends `MedusaService` from the Modules SDK which generates a class with data-management methods for your module's data models. This saves you time on implementing Create, Read, Update, and Delete (CRUD) methods.

So, the `LoyaltyModuleService` class now has methods like `createLoyaltyPoints` and `retrieveLoyaltyPoint`.

Find all methods generated by the `MedusaService` in [the Service Factory reference](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/service-factory-reference/index.html.md).

#### Add Methods to the Service

Aside from the basic CRUD methods, you need to add methods that handle custom functionalities related to loyalty points.

First, you need a method that adds loyalty points for a customer. Add the following method to the `LoyaltyModuleService`:

```ts title="src/modules/loyalty/service.ts"
class LoyaltyModuleService extends MedusaService({
  LoyaltyPoint,
}) {
  async addPoints(customerId: string, points: number): Promise<LoyaltyPoint> {
    const existingPoints = await this.listLoyaltyPoints({
      customer_id: customerId,
    })

    if (existingPoints.length > 0) {
      return await this.updateLoyaltyPoints({
        id: existingPoints[0].id,
        points: existingPoints[0].points + points,
      })
    }

    return await this.createLoyaltyPoints({
      customer_id: customerId,
      points,
    })
  }
}
```

You add an `addPoints` method that accepts two parameters: the ID of the customer and the points to add.

In the method, you retrieve the customer's existing loyalty points using the `listLoyaltyPoints` method, which is automatically generated by the `MedusaService`. If the customer has existing points, you update them with the new points using the `updateLoyaltyPoints` method.

Otherwise, if the customer doesn't have existing loyalty points, you create a new record with the `createLoyaltyPoints` method.

The next method you'll add deducts points from the customer's loyalty points, which is useful when the customer redeems points. Add the following method to the `LoyaltyModuleService`:

```ts title="src/modules/loyalty/service.ts"
class LoyaltyModuleService extends MedusaService({
  LoyaltyPoint,
}) {
  // ...
  async deductPoints(customerId: string, points: number): Promise<LoyaltyPoint> {
    const existingPoints = await this.listLoyaltyPoints({
      customer_id: customerId,
    })

    if (existingPoints.length === 0 || existingPoints[0].points < points) {
      throw new MedusaError(
        MedusaError.Types.NOT_ALLOWED,
        "Insufficient loyalty points"
      )
    }

    return await this.updateLoyaltyPoints({
      id: existingPoints[0].id,
      points: existingPoints[0].points - points,
    })
  }
}
```

The `deductPoints` method accepts the customer ID and the points to deduct.

In the method, you retrieve the customer's existing loyalty points using the `listLoyaltyPoints` method. If the customer doesn't have existing points or if the points to deduct are greater than the existing points, you throw an error.

Otherwise, you update the customer's loyalty points with the new value using the `updateLoyaltyPoints` method, which is automatically generated by `MedusaService`.

Next, you'll add the method that retrieves the points of a customer. Add the following method to the `LoyaltyModuleService`:

```ts title="src/modules/loyalty/service.ts"
class LoyaltyModuleService extends MedusaService({
  LoyaltyPoint,
}) {
  // ...
  async getPoints(customerId: string): Promise<number> {
    const points = await this.listLoyaltyPoints({
      customer_id: customerId,
    })

    return points[0]?.points || 0
  }
}
```

The `getPoints` method accepts the customer ID and retrieves the customer's loyalty points using the `listLoyaltyPoints` method. If the customer has no points, it returns `0`.

#### Add Method to Map Points to Discount

Finally, you'll add a method that implements the logic of mapping loyalty points to a discount amount. This is useful when the customer wants to redeem their points during checkout.

The mapping logic may differ for each use case. For example, you may need to use a third-party service to map the loyalty points discount amount, or use some custom calculation.

To simplify the logic in this tutorial, you'll use a simple calculation that maps 1 point to 1 currency unit. For example, `100` points = `$100` discount.

Add the following method to the `LoyaltyModuleService`:

```ts title="src/modules/loyalty/service.ts"
class LoyaltyModuleService extends MedusaService({
  LoyaltyPoint,
}) {
  // ...
  async calculatePointsFromAmount(amount: number): Promise<number> {
    // Convert amount to points using a standard conversion rate
    // For example, $1 = 1 point
    // Round down to nearest whole point
    const points = Math.floor(amount)

    if (points < 0) {
      throw new MedusaError(
        MedusaError.Types.INVALID_DATA,
        "Amount cannot be negative"
      )
    }

    return points
  }
}
```

The `calculatePointsFromAmount` method accepts the amount and converts it to the nearest whole number of points. If the amount is negative, it throws an error.

You'll use this method later to calculate the amount discounted when a customer redeems their loyalty points.

### Export Module Definition

The final piece to a module is its definition, which you export in an `index.ts` file at its root directory. This definition tells Medusa the name of the module and its service.

So, create the file `src/modules/loyalty/index.ts` with the following content:

```ts title="src/modules/loyalty/index.ts"
import { Module } from "@medusajs/framework/utils"
import LoyaltyModuleService from "./service"

export const LOYALTY_MODULE = "loyalty"

export default Module(LOYALTY_MODULE, {
  service: LoyaltyModuleService,
})
```

You use the `Module` function from the Modules SDK to create the module's definition. It accepts two parameters:

1. The module's name, which is `loyalty`.
2. An object with a required property `service` indicating the module's service.

You also export the module's name as `LOYALTY_MODULE` so you can reference it later.

### Add Module 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 and pass an array with your custom module:

```ts title="medusa-config.ts"
module.exports = defineConfig({
  // ...
  modules: [
    {
      resolve: "./src/modules/loyalty",
    },
  ],
})
```

Each object in the `modules` array has a `resolve` property, whose value is either a path to the module's directory, or an `npm` package’s name.

### Generate Migrations

Since data models represent tables in the database, you define how they're created in the database with migrations. A migration is a TypeScript or JavaScript file that defines database changes made by a module.

Refer to the [Migrations documentation](https://docs.medusajs.com/docs/learn/fundamentals/modules#5-generate-migrations/index.html.md) to learn more.

Medusa's CLI tool can generate the migrations for you. To generate a migration for the Loyalty Module, run the following command in your Medusa application's directory:

```bash
npx medusa db:generate loyalty
```

The `db:generate` command of the Medusa CLI accepts the name of the module to generate the migration for. You'll now have a `migrations` directory under `src/modules/loyalty` that holds the generated migration.

Then, to reflect these migrations on the database, run the following command:

```bash
npx medusa db:migrate
```

The table for the `LoyaltyPoint` data model is now created in the database.

***

## Step 3: Change Loyalty Points Flow

Now that you have a module that stores and manages loyalty points in the database, you'll start building flows around it that allow customers to earn and redeem points.

The first flow you'll build will either add points to a customer's loyalty points or deduct them based on a purchased order. If the customer hasn't redeemed points, the points are added to their loyalty points. Otherwise, the points are deducted from their loyalty points.

To build custom commerce features in Medusa, you create a [workflow](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md). 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 an endpoint.

In this section, you'll build the workflow that adds or deducts loyalty points for an order's customer. Later, you'll execute this workflow when an order is placed.

Learn more about workflows in the [Workflows documentation](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md).

The workflow will have the following steps:

- [useQueryGraphStep](https://docs.medusajs.com/references/helper-steps/useQueryGraphStep/index.html.md): Retrieve the order's details.
- [validateCustomerExistsStep](#validateCustomerExistsStep): Validate that the customer is registered.
- [getCartLoyaltyPromoStep](#getCartLoyaltyPromoStep): Retrieve the cart's loyalty promotion.

Medusa provides the `useQueryGraphStep` and `updatePromotionsStep` in its `@medusajs/medusa/core-flows` package. So, you'll only implement the other steps.

### validateCustomerExistsStep

In the workflow, you first need to validate that the customer is registered. Only registered customers can earn and redeem loyalty points.

To do this, create the file `src/workflows/steps/validate-customer-exists.ts` with the following content:

```ts title="src/workflows/steps/validate-customer-exists.ts"
import { CustomerDTO } from "@medusajs/framework/types"
import { createStep } from "@medusajs/framework/workflows-sdk"
import { MedusaError } from "@medusajs/framework/utils"

export type ValidateCustomerExistsStepInput = {
  customer: CustomerDTO | null | undefined
}

export const validateCustomerExistsStep = createStep(
  "validate-customer-exists",
  async ({ customer }: ValidateCustomerExistsStepInput) => {
    if (!customer) {
      throw new MedusaError(
        MedusaError.Types.INVALID_DATA, 
        "Customer not found"
      )
    }

    if (!customer.has_account) {
      throw new MedusaError(
        MedusaError.Types.INVALID_DATA, 
        "Customer must have an account to earn or manage points"
      )
    }
  }
)
```

You create a step with `createStep` from the Workflows SDK. It accepts two parameters:

1. The step's unique name, which is `validate-customer-exists`.
2. An async function that receives two parameters:
   - The step's input, which is in this case an object with the customer's details.
   - An object that has properties including the [Medusa container](https://docs.medusajs.com/docs/learn/fundamentals/medusa-container/index.html.md), which is a registry of Framework and commerce tools that you can access in the step.

In the step function, you validate that the customer is defined and that it's registered based on its `has_account` property. Otherwise, you throw an error.

### getCartLoyaltyPromoStep

Next, you'll need to retrieve the loyalty promotion applied on the cart, if there's any. This is useful to determine whether the customer has redeemed points.

Before you create a step, you'll create a utility function that the step uses to retrieve the loyalty promotion of a cart. You'll create it as a separate utility function to use it later in other customizations.

Create the file `src/utils/promo.ts` with the following content:

```ts title="src/utils/promo.ts"
import { PromotionDTO, CustomerDTO, CartDTO } from "@medusajs/framework/types"

export type CartData = CartDTO & {
  promotions?: PromotionDTO[]
  customer?: CustomerDTO
  metadata: {
    loyalty_promo_id?: string
  }
}

export function getCartLoyaltyPromotion(
  cart: CartData
): PromotionDTO | undefined {
  if (!cart?.metadata?.loyalty_promo_id) {
    return
  }

  return cart.promotions?.find(
    (promotion) => promotion.id === cart.metadata.loyalty_promo_id
  )
}
```

You create a `getCartLoyaltyPromotion` function that accepts the cart's details as an input and returns the loyalty promotion if it exists. You retrieve the loyalty promotion if its ID is stored in the cart's `metadata.loyalty_promo_id` property.

You can now create the step that uses this utility to retrieve a carts loyalty points promotion. To create the step, create the file `src/workflows/steps/get-cart-loyalty-promo.ts` with the following content:

```ts title="src/workflows/steps/get-cart-loyalty-promo.ts"
import { createStep, StepResponse } from "@medusajs/framework/workflows-sdk"
import { CartData, getCartLoyaltyPromotion } from "../../utils/promo"
import { MedusaError } from "@medusajs/framework/utils"

type GetCartLoyaltyPromoStepInput = {
  cart: CartData,
  throwErrorOn?: "found" | "not-found"
}

export const getCartLoyaltyPromoStep = createStep(
  "get-cart-loyalty-promo",
  async ({ cart, throwErrorOn }: GetCartLoyaltyPromoStepInput) => {
    const loyaltyPromo = getCartLoyaltyPromotion(cart)

    if (throwErrorOn === "found" && loyaltyPromo) {
      throw new MedusaError(
        MedusaError.Types.INVALID_DATA,
        "Loyalty promotion already applied to cart"
      )
    } else if (throwErrorOn === "not-found" && !loyaltyPromo) {
      throw new MedusaError(
        MedusaError.Types.INVALID_DATA,
        "No loyalty promotion found on cart"
      )
    }

    return new StepResponse(loyaltyPromo)
  }
)
```

You create a step that accepts an object having the following properties:

- `cart`: The cart's details.
- `throwErrorOn`: An optional property that indicates whether to throw an error if the loyalty promotion is found or not found.

The `throwErrorOn` property is useful to make the step reusable in different scenarios, allowing you to use it in later workflows.

In the step, you call the `getCartLoyaltyPromotion` utility to retrieve the loyalty promotion. If the `throwErrorOn` property is set to `found` and the loyalty promotion is found, you throw an error.

Otherwise, if the `throwErrorOn` property is set to `not-found` and the loyalty promotion is not found, you throw an error.

To return data from a step, you return an instance of `StepResponse` from the Workflows SDK. It accepts as a parameter the data to return, which is the loyalty promotion in this case.

### deductPurchasePointsStep

If the order's cart has a loyalty promotion, you need to deduct points from the customer's loyalty points. To do this, create the file `src/workflows/steps/deduct-purchase-points.ts` with the following content:

```ts title="src/workflows/steps/deduct-purchase-points.ts" highlights={deductStepHighlights} collapsibleLines="1-7" expandButtonLabel="Show Imports"
import { 
  createStep,
  StepResponse, 
} from "@medusajs/framework/workflows-sdk"
import { LOYALTY_MODULE } from "../../modules/loyalty"
import LoyaltyModuleService from "../../modules/loyalty/service"

type DeductPurchasePointsInput = {
  customer_id: string
  amount: number
}

export const deductPurchasePointsStep = createStep(
  "deduct-purchase-points",
  async ({ 
    customer_id, amount,
  }: DeductPurchasePointsInput, { container }) => {
    const loyaltyModuleService: LoyaltyModuleService = container.resolve(
      LOYALTY_MODULE
    )

    const pointsToDeduct = await loyaltyModuleService.calculatePointsFromAmount(
      amount
    )

    const result = await loyaltyModuleService.deductPoints(
      customer_id,
      pointsToDeduct
    )

    return new StepResponse(result, {
      customer_id,
      points: pointsToDeduct,
    })
  },
  async (data, { container }) => {
    if (!data) {
      return
    }

    const loyaltyModuleService: LoyaltyModuleService = container.resolve(
      LOYALTY_MODULE
    )

    // Restore points in case of failure
    await loyaltyModuleService.addPoints(
      data.customer_id,
      data.points
    )
  }
)
```

You create a step that accepts an object having the following properties:

- `customer_id`: The ID of the customer to deduct points from.
- `amount`: The promotion's amount, which will be used to calculate the points to deduct.

In the step, you resolve the Loyalty Module's service from the Medusa container. Then, you use the `calculatePointsFromAmount` method to calculate the points to deduct from the promotion's amount.

After that, you call the `deductPoints` method to deduct the points from the customer's loyalty points.

Finally, you return a `StepResponse` with the result of the `deductPoints`.

#### Compensation Function

This step has a compensation function, which is passed as a third parameter to the `createStep` function.

The compensation function undoes the actions performed in a step. Then, if an error occurs during the workflow's execution, the compensation functions of executed steps are called to roll back the changes. This mechanism ensures data consistency in your application, especially as you integrate external systems.

The compensation function accepts two parameters:

1. Data passed from the step function to the compensation function. The data is passed as a second parameter of the returned `StepResponse` instance.
2. An object that has properties including the [Medusa container](https://docs.medusajs.com/docs/learn/fundamentals/medusa-container/index.html.md).

In the compensation function, you resolve the Loyalty Module's service from the Medusa container. Then, you call the `addPoints` method to restore the points deducted from the customer's loyalty points if an error occurs.

### addPurchaseAsPointsStep

The last step you'll create adds points to the customer's loyalty points. You'll use this step if the customer didn't redeem points during checkout.

To create the step, create the file `src/workflows/steps/add-purchase-as-points.ts` with the following content:

```ts title="src/workflows/steps/add-purchase-as-points.ts" highlights={addPointsHighlights} collapsibleLines="1-7" expandButtonLabel="Show Imports"
import {
  createStep,
  StepResponse,
} from "@medusajs/framework/workflows-sdk"
import { LOYALTY_MODULE } from "../../modules/loyalty"
import LoyaltyModuleService from "../../modules/loyalty/service"

type StepInput = {
  customer_id: string
  amount: number
}

export const addPurchaseAsPointsStep = createStep(
  "add-purchase-as-points",
  async (input: StepInput, { container }) => {
    const loyaltyModuleService: LoyaltyModuleService = container.resolve(
      LOYALTY_MODULE
    )

    const pointsToAdd = await loyaltyModuleService.calculatePointsFromAmount(
      input.amount
    )

    const result = await loyaltyModuleService.addPoints(
      input.customer_id,
      pointsToAdd
    )

    return new StepResponse(result, {
      customer_id: input.customer_id,
      points: pointsToAdd,
    })
  },
  async (data, { container }) => {
    if (!data) {
      return
    }

    const loyaltyModuleService: LoyaltyModuleService = container.resolve(
      LOYALTY_MODULE
    )

    await loyaltyModuleService.deductPoints(
      data.customer_id,
      data.points
    )
  }
)
```

You create a step that accepts an object having the following properties:

- `customer_id`: The ID of the customer to add points to.
- `amount`: The order's amount, which will be used to calculate the points to add.

In the step, you resolve the Loyalty Module's service from the Medusa container. Then, you use the `calculatePointsFromAmount` method to calculate the points to add from the order's amount.

After that, you call the `addPoints` method to add the points to the customer's loyalty points.

Finally, you return a `StepResponse` with the result of the `addPoints`.

You also pass to the compensation function the customer's ID and the points added. In the compensation function, you deduct the points if an error occurs.

### Add Utility Functions

Before you create the workflow, you need a utility function that checks whether an order's cart has a loyalty promotion. This is useful to determine whether the customer redeemed points during checkout, allowing you to decide which steps to execute.

To add the utility function, add the following to `src/utils/promo.ts`:

```ts title="src/utils/promo.ts"
import { OrderDTO } from "@medusajs/framework/types"

export type OrderData = OrderDTO & {
  promotion?: PromotionDTO[]
  customer?: CustomerDTO
  cart?: CartData
}

export const CUSTOMER_ID_PROMOTION_RULE_ATTRIBUTE = "customer_id"

export function orderHasLoyaltyPromotion(order: OrderData): boolean {
  const loyaltyPromotion = getCartLoyaltyPromotion(
    order.cart as unknown as CartData
  )

  return loyaltyPromotion?.rules?.some((rule) => {
    return rule?.attribute === CUSTOMER_ID_PROMOTION_RULE_ATTRIBUTE && (
      rule?.values?.some((value) => value.value === order.customer?.id) || false
    )
  }) || false
}
```

You first define an `OrderData` type that extends the `OrderDTO` type. This type has the order's details, including the cart, customer, and promotions details.

Then, you define a constant `CUSTOMER_ID_PROMOTION_RULE_ATTRIBUTE` that represents the attribute used in the promotion rule to check whether the customer ID is valid.

Finally, you create the `orderHasLoyaltyPromotion` function that accepts an order's details and checks whether it has a loyalty promotion. It returns `true` if:

- The order's cart has a loyalty promotion. You use the `getCartLoyaltyPromotion` utility to try to retrieve the loyalty promotion.
- The promotion's rules include the `customer_id` attribute and its value matches the order's customer ID.
  - When you create the promotion for the cart later, you'll see how to set this rule.

You'll use this utility in the workflow next.

### Create the Workflow

Now that you have all the steps, you can create the workflow that uses them.

To create the workflow, create the file `src/workflows/handle-order-points.ts` with the following content:

```ts title="src/workflows/handle-order-points.ts" highlights={handleOrderPointsHighlights} collapsibleLines="1-9" expandButtonLabel="Show Imports"
import { createWorkflow, when } from "@medusajs/framework/workflows-sdk"
import { updatePromotionsStep, useQueryGraphStep } from "@medusajs/medusa/core-flows"
import { validateCustomerExistsStep, ValidateCustomerExistsStepInput } from "./steps/validate-customer-exists"
import { deductPurchasePointsStep } from "./steps/deduct-purchase-points"
import { addPurchaseAsPointsStep } from "./steps/add-purchase-as-points"
import { OrderData, CartData } from "../utils/promo"
import { orderHasLoyaltyPromotion } from "../utils/promo"
import { getCartLoyaltyPromoStep } from "./steps/get-cart-loyalty-promo"

type WorkflowInput = {
  order_id: string
}

export const handleOrderPointsWorkflow = createWorkflow(
  "handle-order-points",
  ({ order_id }: WorkflowInput) => {
    // @ts-ignore
    const { data: orders } = useQueryGraphStep({
      entity: "order",
      fields: [
        "id", 
        "customer.*", 
        "total", 
        "cart.*",
        "cart.promotions.*",
        "cart.promotions.rules.*",
        "cart.promotions.rules.values.*",
        "cart.promotions.application_method.*",
      ],
      filters: {
        id: order_id,
      },
      options: {
        throwIfKeyNotFound: true,
      },
    })

    validateCustomerExistsStep({
      customer: orders[0].customer,
    } as ValidateCustomerExistsStepInput)

    const loyaltyPointsPromotion = getCartLoyaltyPromoStep({
      cart: orders[0].cart as unknown as CartData,
    })

    when(orders, (orders) => 
      orderHasLoyaltyPromotion(orders[0] as unknown as OrderData) && 
      loyaltyPointsPromotion !== undefined
    )
    .then(() => {
      deductPurchasePointsStep({
        customer_id: orders[0].customer!.id,
        amount: loyaltyPointsPromotion.application_method!.value as number,
      })

      updatePromotionsStep([
        {
          id: loyaltyPointsPromotion.id,
          status: "inactive",
        },
      ])
    })


    when(
      orders, 
      (order) => !orderHasLoyaltyPromotion(order[0] as unknown as OrderData)
    )
    .then(() => {
      addPurchaseAsPointsStep({
        customer_id: orders[0].customer!.id,
        amount: orders[0].total,
      })
    })
  }
)
```

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 with the order's ID.

In the workflow's constructor function, you:

- Use `useQueryGraphStep` to retrieve the order's details. You pass the order's ID as a filter to retrieve the order.
  - This step uses [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), which is a tool that retrieves data across modules.
- Validate that the customer is registered using the `validateCustomerExistsStep`.
- Retrieve the cart's loyalty promotion using the `getCartLoyaltyPromoStep`.
- Use `when` to check whether the order's cart has a loyalty promotion.
  - Since you can't perform data manipulation in a workflow's constructor function, `when` allows you to perform steps if a condition is satisfied.
  - You pass as a first parameter the object to perform the condition on, which is the order in this case. In the second parameter, you pass a function that returns a boolean value, indicating whether the condition is satisfied.
  - To specify the steps to perform if a condition is satisfied, you chain a `then` method to the `when` method. You can perform any step within the `then` method.
  - In this case, if the order's cart has a loyalty promotion, you call the `deductPurchasePointsStep` to deduct points from the customer's loyalty points. You also call the `updatePromotionsStep` to deactivate the cart's loyalty promotion.
- You use another `when` to check whether the order's cart doesn't have a loyalty promotion.
  - If the condition is satisfied, you call the `addPurchaseAsPointsStep` to add points to the customer's loyalty points.

You'll use this workflow next when an order is placed.

To learn more about the constraints on a workflow's constructor function, refer to the [Workflow Constraints](https://docs.medusajs.com/docs/learn/fundamentals/workflows/constructor-constraints/index.html.md) documentation. Refer to the [When-Then](https://docs.medusajs.com/docs/learn/fundamentals/workflows/conditions/index.html.md) documentation to learn more about the `when` method and how to use it in a workflow.

***

## Step 4: Handle Order Placed Event

Now that you have the workflow that handles adding or deducting loyalty points for an order, you need to execute it when an order is placed.

Medusa has an event system that allows you to listen to events emitted by the Medusa server using a [subscriber](https://docs.medusajs.com/docs//learn/fundamentals/events-and-subscribers/index.html.md). A subscriber is an asynchronous function that's executed when its associated event is emitted. In a subscriber, you can execute a workflow that performs actions in result of the event.

In this step, you'll create a subscriber that listens to the `order.placed` event and executes the `handleOrderPointsWorkflow` workflow.

Refer to the [Events and Subscribers](https://docs.medusajs.com/docs/learn/fundamentals/events-and-subscribers/index.html.md) documentation to learn more.

Subscribers are created in a TypeScript or JavaScript file under the `src/subscribers` directory. So, to create a subscriber, create the fle `src/subscribers/order-placed.ts` with the following content:

```ts title="src/subscribers/order-placed.ts"
import type {
  SubscriberArgs,
  SubscriberConfig,
} from "@medusajs/framework"
import { handleOrderPointsWorkflow } from "../workflows/handle-order-points"

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

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

The subscriber file must export:

- An asynchronous subscriber function that's executed whenever the associated event, which is `order.placed` is triggered.
- A configuration object with an event property whose value is the event the subscriber is listening to. You can also pass an array of event names to listen to multiple events in the same subscriber.

The subscriber function accepts an object with the following properties:

- `event`: An object with the event's data payload. For example, the `order.placed` event has the order's ID in its data payload.
- `container`: The Medusa container, which you can use to resolve services and tools.

In the subscriber function, you execute the `handleOrderPointsWorkflow` by invoking it, passing it the Medusa container, then using its `run` method, passing it the workflow's input.

Whenever an order is placed now, the subscriber will be executed, which in turn will execute the workflow that handles the loyalty points flow.

### Test it Out

To test out the loyalty points flow, you'll use the [Next.js Starter Storefront](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/nextjs-starter/index.html.md) that you installed in the first step. As mentioned in that step, the storefront will be installed in a separate directory from the Medusa application, and its name is `{project-name}-storefront`, where `{project-name}` is the name of your Medusa application's directory.

So, run the following command in the Medusa application's directory to start the Medusa server:

```bash npm2yarn badgeLabel="Medusa Application" badgeColor="green"
npm run dev
```

Then, run the following command in the Next.js Starter Storefront's directory to start the Next.js server:

```bash npm2yarn badgeLabel="Storefront" badgeColor="blue"
npm run dev
```

The Next.js Starter Storefront will be running on `http://localhost:8000`, and the Medusa server will be running on `http://localhost:9000`.

Open the Next.js Starter Storefront in your browser and create a new account by going to Account at the top right.

Once you're logged in, add an item to the cart and go through the checkout flow.

After you place the order, you'll see the following message in your Medusa application's terminal:

```bash
info:    Processing order.placed which has 1 subscribers
```

This message indicates that the `order.placed` event was emitted, and that your subscriber was executed.

Since you didn't redeem any points during checkout, loyalty points will be added to your account. You'll implement an API route that allows you to retrieve the loyalty points in the next step.

***

## Step 5: Retrieve Loyalty Points API Route

Next, you want to allow customers to view their loyalty points. You can show them on their profile page, or during checkout.

To expose a feature to clients, you create an [API route](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/index.html.md). An API Route is an endpoint that exposes commerce features to external applications and clients, such as storefronts.

You'll create an API route at the path `/store/customers/me/loyalty-points` that returns the loyalty points of the authenticated customer.

Learn more about API routes in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/index.html.md).

An API route is created in a `route.ts` file under a sub-directory of the `src/api` directory. The path of the API route is the file's path relative to `src/api`.

So, to create an API route at the path `/store/customers/me/loyalty-points`, create the file `src/api/store/customers/me/loyalty-points/route.ts` with the following content:

```ts title="src/api/store/customers/me/loyalty-points/route.ts"

import { 
  AuthenticatedMedusaRequest, 
  MedusaResponse,
} from "@medusajs/framework/http"
import { LOYALTY_MODULE } from "../../../../../modules/loyalty"
import LoyaltyModuleService from "../../../../../modules/loyalty/service"

export async function GET(
  req: AuthenticatedMedusaRequest,
  res: MedusaResponse
) {
  const loyaltyModuleService: LoyaltyModuleService = req.scope.resolve(
    LOYALTY_MODULE
  )

  const points = await loyaltyModuleService.getPoints(
    req.auth_context.actor_id
  )

  res.json({
    points,
  })
}
```

Since you export a `GET` route handler function, you're exposing a `GET` endpoint at `/store/customers/me/loyalty-points`.  The route handler function accepts two parameters:

1. A request object with details and context on the request, such as body parameters or authenticated customer details.
2. A response object to manipulate and send the response.

In the route handler, you resolve the Loyalty Module's service from the Medusa container (which is available at `req.scope`).

Then, you call the service's `getPoints` method to retrieve the authenticated customer's loyalty points. Note that routes starting with `/store/customers/me` are only accessible by authenticated customers. You can access the authenticated customer ID from the request's context, which is available at `req.auth_context.actor_id`.

Finally, you return the loyalty points in the response.

You'll test out this route as you customize the Next.js Starter Storefront next.

***

## Step 6: Show Loyalty Points During Checkout

Now that you have the API route to retrieve the loyalty points, you can show them during checkout.

In this step, you'll customize the Next.js Starter Storefront to show the loyalty points in the checkout page.

First, you'll add a server action function that retrieves the loyalty points from the route you created earlier. In `src/lib/data/customer.ts`, add the following function:

```ts title="src/lib/data/customer.ts" badgeLabel="Storefront" badgeColor="blue"
export const getLoyaltyPoints = async () => {
  const headers = {
    ...(await getAuthHeaders()),
  }

  return sdk.client.fetch<{ points: number }>(
    `/store/customers/me/loyalty-points`,
    {
      method: "GET",
      headers,
    }
  )
    .then(({ points }) => points)
    .catch(() => null)
}
```

You add a `getLoyaltyPoints` function that retrieves the authenticated customer's loyalty points from the API route you created earlier. You pass the authentication headers using the `getAuthHeaders` function, which is a utility function defined in the Next.js Starter Storefront.

If the customer isn't authenticated, the request will fail. So, you catch the error and return `null` in that case.

Next, you'll create a component that shows the loyalty points in the checkout page. Create the file `src/modules/checkout/components/loyalty-points/index.tsx` with the following content:

```tsx title="src/modules/checkout/components/loyalty-points/index.tsx" badgeLabel="Storefront" badgeColor="blue" highlights={loyaltyPointsHighlights}
"use client"

import { HttpTypes } from "@medusajs/types"
import { useEffect, useMemo, useState } from "react"
import { getLoyaltyPoints } from "../../../../lib/data/customer"
import { Button, Heading } from "@medusajs/ui"
import Link from "next/link"

type LoyaltyPointsProps = {
  cart: HttpTypes.StoreCart & {
    promotions: HttpTypes.StorePromotion[]
  }
}

const LoyaltyPoints = ({ cart }: LoyaltyPointsProps) => {
  const isLoyaltyPointsPromoApplied = useMemo(() => {
    return cart.promotions.find(
      (promo) => promo.id === cart.metadata?.loyalty_promo_id
    ) !== undefined
  }, [cart])
  const [loyaltyPoints, setLoyaltyPoints] = useState<
    number | null
  >(null)

  useEffect(() => {
    getLoyaltyPoints()
    .then((points) => {
      console.log(points)
      setLoyaltyPoints(points)
    })
  }, [])

  const handleTogglePromotion = async (
    e: React.MouseEvent<HTMLButtonElement, MouseEvent>
  ) => {
    e.preventDefault()
    // TODO apply or remove loyalty promotion
  }

  return (
    <>
      <div className="h-px w-full border-b border-gray-200 my-4" />
      <div className="flex flex-col">
        <Heading className="txt-medium mb-2">
          Loyalty Points
        </Heading>
        {loyaltyPoints === null && (
          <Link href="/account" className="txt-medium text-ui-fg-interactive hover:text-ui-fg-interactive-hover">
            Sign up to get and use loyalty points
          </Link>
        )}
        {loyaltyPoints !== null && (
          <div className="flex items-center justify-between my-6 gap-1">
          <Button
            variant="secondary"
            className="w-1/2"
            onClick={handleTogglePromotion}
          >
            {isLoyaltyPointsPromoApplied ? "Remove" : "Apply"} Loyalty Points
          </Button>
          <span className="txt-medium text-ui-fg-subtle">
            You have {loyaltyPoints} loyalty points
          </span>
        </div>
        )}
      </div>
    </>
  )
}

export default LoyaltyPoints
```

You create a `LoyaltyPoints` component that accepts the cart's details as a prop. In the component, you:

- Create a `isLoyaltyPointsPromoApplied` memoized value that checks whether the cart has a loyalty promotion applied. You use the `cart.metadata.loyalty_promo_id` property to check this.
- Create a `loyaltyPoints` state to store the customer's loyalty points.
- Call the `getLoyaltyPoints` function in a `useEffect` hook to retrieve the loyalty points from the API route you created earlier. You set the `loyaltyPoints` state with the retrieved points.
- Define `handleTogglePromotion` that, when clicked, would either apply or remove the promotion. You'll implement these functionalities later.
- Render the loyalty points in the component. If the customer isn't authenticated, you show a link to the account page to sign up. Otherwise, you show the loyalty points and a button to apply or remove the promotion.

Next, you'll show this component at the end of the checkout's summary component. So, import the component in `src/modules/checkout/templates/checkout-summary/index.tsx`:

```tsx title="src/modules/checkout/templates/checkout-summary/index.tsx" badgeLabel="Storefront" badgeColor="blue"
import LoyaltyPoints from "../../components/loyalty-points"
```

Then, in the return statement of the `CheckoutSummary` component, add the following after the `div` wrapping the `DiscountCode`:

```tsx title="src/modules/checkout/templates/checkout-summary/index.tsx" badgeLabel="Storefront" badgeColor="blue"
<LoyaltyPoints cart={cart} />
```

This will show the loyalty points component at the end of the checkout summary.

### Test it Out

To test out the customizations to the checkout flow, make sure both the Medusa application and Next.js Starter Storefront are running.

Then, as an authenticated customer, add an item to cart and proceed to checkout. You'll find a new "Loyalty Points" section at the end of the checkout summary.

![Loyalty Points Section at the end of the summary section at the right](https://res.cloudinary.com/dza7lstvk/image/upload/v1744195223/Medusa%20Resources/Screenshot_2025-04-09_at_1.39.34_PM_l5oltc.png)

If you made a purchase before, you can see your loyalty points. You'll also see the "Apply Loyalty Points" button, which doesn't yet do anything. You'll add the functionality next.

***

## Step 7: Apply Loyalty Points to Cart

The next feature you'll implement allows the customer to apply their loyalty points during checkout. To implement the feature, you need:

- A workflow that implements the steps of the apply loyalty points flow.
- An API route that exposes the workflow's functionality to clients. You'll then send a request to this API route to apply the loyalty points on the customer's cart.
- A function in the Next.js Starter Storefront that sends the request to the API route you created earlier.

The workflow will have the following steps:

- [useQueryGraphStep](https://docs.medusajs.com/references/helper-steps/useQueryGraphStep/index.html.md): Retrieve the cart's details.
- [validateCustomerExistsStep](#validateCustomerExistsStep): Validate that the customer is registered.
- [getCartLoyaltyPromoStep](#getCartLoyaltyPromoStep): Retrieve the cart's loyalty promotion.
- [getCartLoyaltyPromoAmountStep](#getCartLoyaltyPromoAmountStep): Get the amount to be discounted based on the loyalty points.
- [createPromotionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createPromotionsStep/index.html.md): Create a new loyalty promotion for the cart.
- [updateCartPromotionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateCartPromotionsWorkflow/index.html.md): Update the cart's promotions with the new loyalty promotion.
- [updateCartsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateCartsStep/index.html.md): Update the cart to store the ID of the loyalty promotion in the metadata.
- [useQueryGraphStep](https://docs.medusajs.com/references/helper-steps/useQueryGraphStep/index.html.md): Retrieve the cart's details again.

Most of the workflow's steps are either provided by Medusa in the `@medusajs/medusa/core-flows` package or steps you've already implemented. You only need to implement the `getCartLoyaltyPromoAmountStep` step.

### getCartLoyaltyPromoAmountStep

The fourth step in the workflow is the `getCartLoyaltyPromoAmountStep`, which retrieves the amount to be discounted based on the loyalty points. This step is useful to determine how much discount to apply to the cart.

To create the step, create the file `src/workflows/steps/get-cart-loyalty-promo-amount.ts` with the following content:

```ts title="src/workflows/steps/get-cart-loyalty-promo-amount.ts" highlights={getCartLoyaltyPromoAmountStepHighlights}
import { PromotionDTO, CustomerDTO } from "@medusajs/framework/types"
import { MedusaError } from "@medusajs/framework/utils"
import { createStep, StepResponse } from "@medusajs/framework/workflows-sdk"
import LoyaltyModuleService from "../../modules/loyalty/service"
import { LOYALTY_MODULE } from "../../modules/loyalty"

export type GetCartLoyaltyPromoAmountStepInput = {
  cart: {
    id: string
    customer: CustomerDTO
    promotions?: PromotionDTO[]
    total: number
  }
}

export const getCartLoyaltyPromoAmountStep = createStep(
  "get-cart-loyalty-promo-amount",
  async ({ cart }: GetCartLoyaltyPromoAmountStepInput, { container }) => {
    // Check if customer has any loyalty points
    const loyaltyModuleService: LoyaltyModuleService = container.resolve(
      LOYALTY_MODULE
    )
    const loyaltyPoints = await loyaltyModuleService.getPoints(
      cart.customer.id
    )

    if (loyaltyPoints <= 0) {
      throw new MedusaError(
        MedusaError.Types.INVALID_DATA,
        "Customer has no loyalty points"
      )
    }
    
    const pointsAmount = await loyaltyModuleService.calculatePointsFromAmount(
      loyaltyPoints
    )

    const amount = Math.min(pointsAmount, cart.total)

    return new StepResponse(amount)
  }
)
```

You create a step that accepts an object having the cart's details.

In the step, you resolve the Loyalty Module's service from the Medusa container. Then, you call the `getPoints` method to retrieve the customer's loyalty points. If the customer has no loyalty points, you throw an error.

Next, you call the `calculatePointsFromAmount` method to calculate the amount to be discounted based on the loyalty points. You use the `Math.min` function to ensure that the amount doesn't exceed the cart's total.

Finally, you return a `StepResponse` with the amount to be discounted.

### Create the Workflow

You can now create the workflow that applies a loyalty promotion to the cart.

To create the workflow, create the file `src/workflows/apply-loyalty-on-cart.ts` with the following content:

```ts title="src/workflows/apply-loyalty-on-cart.ts" highlights={applyLoyaltyOnCartWorkflowHighlights} collapsibleLines="1-24" expandButtonLabel="Show Imports"
import {
  createWorkflow,
  transform,
  WorkflowResponse,
} from "@medusajs/framework/workflows-sdk"
import { 
  createPromotionsStep, 
  updateCartPromotionsWorkflow, 
  updateCartsStep, 
  useQueryGraphStep,
} from "@medusajs/medusa/core-flows"
import { 
  validateCustomerExistsStep, 
  ValidateCustomerExistsStepInput,
} from "./steps/validate-customer-exists"
import { 
  getCartLoyaltyPromoAmountStep, 
  GetCartLoyaltyPromoAmountStepInput,
} from "./steps/get-cart-loyalty-promo-amount"
import { CartData, CUSTOMER_ID_PROMOTION_RULE_ATTRIBUTE } from "../utils/promo"
import { CreatePromotionDTO } from "@medusajs/framework/types"
import { PromotionActions } from "@medusajs/framework/utils"
import { getCartLoyaltyPromoStep } from "./steps/get-cart-loyalty-promo"

type WorkflowInput = {
  cart_id: string
}

const fields = [
  "id",
  "customer.*",
  "promotions.*",
  "promotions.application_method.*",
  "promotions.rules.*",
  "promotions.rules.values.*",
  "currency_code",
  "total",
  "metadata",
]

export const applyLoyaltyOnCartWorkflow = createWorkflow(
  "apply-loyalty-on-cart",
  (input: WorkflowInput) => {
    // @ts-ignore
    const { data: carts } = useQueryGraphStep({
      entity: "cart",
      fields,
      filters: {
        id: input.cart_id,
      },
      options: {
        throwIfKeyNotFound: true,
      },
    })

    validateCustomerExistsStep({
      customer: carts[0].customer,
    } as ValidateCustomerExistsStepInput)

    getCartLoyaltyPromoStep({
      cart: carts[0] as unknown as CartData,
      throwErrorOn: "found",
    })

    const amount = getCartLoyaltyPromoAmountStep({
      cart: carts[0],
    } as unknown as GetCartLoyaltyPromoAmountStepInput)

    // TODO create and apply the promotion on the cart
  }
)
```

You create a workflow that accepts an object with the cart's ID as input.

So far, you:

- Use `useQueryGraphStep` to retrieve the cart's details. You pass the cart's ID as a filter to retrieve the cart.
- Validate that the customer is registered using the `validateCustomerExistsStep`.
- Check whether the cart has a loyalty promotion using the `getCartLoyaltyPromoStep`. You pass the `throwErrorOn` parameter with the value `found` to throw an error if a loyalty promotion is found in the cart.
- Retrieve the amount to be discounted based on the loyalty points using the `getCartLoyaltyPromoAmountStep`.

Next, you need to create a new loyalty promotion for the cart. First, you'll prepare the data of the promotion to be created.

Replace the `TODO` with the following:

```ts title="src/workflows/apply-loyalty-on-cart.ts" highlights={prepareLoyaltyPromoDataHighlights}
const promoToCreate = transform({
  carts,
  amount,
}, (data) => {
  const randomStr = Math.random().toString(36).substring(2, 8)
  const uniqueId = (
    "LOYALTY-" + data.carts[0].customer?.first_name + "-" + randomStr
  ).toUpperCase()
  return {
    code: uniqueId,
    type: "standard",
    status: "active",
    application_method: {
      type: "fixed",
      value: data.amount,
      target_type: "order",
      currency_code: data.carts[0].currency_code,
      allocation: "across",
    },
    rules: [
      {
        attribute: CUSTOMER_ID_PROMOTION_RULE_ATTRIBUTE,
        operator: "eq",
        values: [data.carts[0].customer!.id],
      },
    ],
    campaign: {
      name: uniqueId,
      description: "Loyalty points promotion for " + data.carts[0].customer!.email,
      campaign_identifier: uniqueId,
      budget: {
        type: "usage",
        limit: 1,
      },
    },
  }
})

// TODO create promotion and apply it on cart
```

Since data manipulation isn't allowed in a workflow constructor, you use the [transform](https://docs.medusajs.com/docs/learn/fundamentals/workflows/variable-manipulation/index.html.md) function from the Workflows SDK. It accepts two parameters:

- The data to perform manipulation on. In this case, you pass the cart's details and the amount to be discounted.
- A function that receives the data from the first parameter, and returns the transformed data.

In the transformation function, you prepare th data of the loyalty promotion to be created. Some key details include:

- You set the discount amount in the application method of the promotion.
- You add a rule to the promotion that ensures it can be used only in carts having their `customer_id` equal to this customer's ID. This prevents other customers from using this promotion.
- You create a campaign for the promotion, and you set the campaign budget to a single usage. This prevents the customer from using the promotion again.

Learn more about promotion concepts in the [Promotion Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/promotion/index.html.md)'s documentation.

You can now use the returned data to create a promotion and apply it to the cart. Replace the new `TODO` with the following:

```ts title="src/workflows/apply-loyalty-on-cart.ts" highlights={createLoyaltyPromoStepHighlights}
const loyaltyPromo = createPromotionsStep([
  promoToCreate,
] as CreatePromotionDTO[])

const { metadata, ...updatePromoData } = transform({
  carts,
  promoToCreate,
  loyaltyPromo,
}, (data) => {
  const promos = [
    ...(data.carts[0].promotions?.map((promo) => promo?.code).filter(Boolean) || []) as string[],
    data.promoToCreate.code,
  ]

  return {
    cart_id: data.carts[0].id,
    promo_codes: promos,
    action: PromotionActions.ADD,
    metadata: {
      loyalty_promo_id: data.loyaltyPromo[0].id,
    },
  }
})

updateCartPromotionsWorkflow.runAsStep({
  input: updatePromoData,
})

updateCartsStep([
  {
    id: input.cart_id,
    metadata,
  },
])

// retrieve cart with updated promotions
// @ts-ignore
const { data: updatedCarts } = useQueryGraphStep({
  entity: "cart",
  fields,
  filters: { id: input.cart_id },
}).config({ name: "retrieve-cart" })

return new WorkflowResponse(updatedCarts[0])
```

In the rest of the workflow, you:

- Create the loyalty promotion using the data you prepared earlier using the `createPromotionsStep`.
- Use the `transform` function to prepare the data to update the cart's promotions. You add the new loyalty promotion code to the cart's promotions codes, and set the `loyalty_promo_id` in the cart's metadata.
- Update the cart's promotions with the new loyalty promotion using the `updateCartPromotionsWorkflow` workflow.
- Update the cart's metadata with the loyalty promotion ID using the `updateCartsStep`.
- Retrieve the cart's details again using `useQueryGraphStep` to get the updated cart with the new loyalty promotion.

To return data from the workflow, you must return an instance of `WorkflowResponse`. You pass it the data to be returned, which is in this case the cart's details.

### Create the API Route

Next, you'll create the API route that executes this workflow.

To create the API route, create the file `src/api/store/carts/[id]/loyalty-points/route.ts` with the following content:

```ts title="src/api/store/carts/[id]/loyalty-points/route.ts"
import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http"
import { applyLoyaltyOnCartWorkflow } from "../../../../../workflows/apply-loyalty-on-cart"

export async function POST(
  req: MedusaRequest,
  res: MedusaResponse
) {
  const { id: cart_id } = req.params

  const { result: cart } = await applyLoyaltyOnCartWorkflow(req.scope)
    .run({
      input: {
        cart_id,
      },
    })

  res.json({ cart })
}
```

Since you export a `POST` route handler, you expose a `POST` API route at `/store/carts/[id]/loyalty-points`.

In the route handler, you execute the `applyLoyaltyOnCartWorkflow` workflow, passing it the cart ID as an input. You return the cart's details in the response.

You can now use this API route in the Next.js Starter Storefront.

### Apply Loyalty Points in the Storefront

In the Next.js Starter Storefront, you need to add a server action function that sends a request to the API route you created earlier. Then, you'll use that function when the customer clicks the "Apply Loyalty Points" button.

To add the function, add the following to `src/lib/data/cart.ts` in the Next.js Starter Storefront:

```ts title="src/lib/data/cart.ts" badgeLabel="Storefront" badgeColor="blue"
export async function applyLoyaltyPointsOnCart() {
  const cartId = await getCartId()
  const headers = {
    ...(await getAuthHeaders()),
  }

  return await sdk.client.fetch<{
    cart: HttpTypes.StoreCart & {
      promotions: HttpTypes.StorePromotion[]
    }
  }>(`/store/carts/${cartId}/loyalty-points`, {
    method: "POST",
    headers,
  })
  .then(async (result) => {
    const cartCacheTag = await getCacheTag("carts")
    revalidateTag(cartCacheTag)

    return result
  })
}
```

You create an `applyLoyaltyPointsOnCart` function that sends a request to the API route you created earlier.

In the function, you retrieve the cart ID stored in the cookie using the `getCartId` function, which is available in the Next.js Starter Storefront.

Then, you send the request. Once the request is resolved successfully, you revalidate the cart cache tag to ensure that the cart's details are updated and refetched by other components. This ensures that the applied promotion is shown in the checkout summary without needing to refresh the page.

Finally, you'll use this function in the `handleTogglePromotion` function in the `LoyaltyPoints` component you created earlier.

At the top of `src/modules/checkout/components/loyalty-points/index.tsx`, import the function:

```tsx title="src/modules/checkout/components/loyalty-points/index.tsx" badgeLabel="Storefront" badgeColor="blue"
import { applyLoyaltyPointsOnCart } from "../../../../lib/data/cart"
```

Then, replace the `handleTogglePromotion` function with the following:

```tsx title="src/modules/checkout/components/loyalty-points/index.tsx" badgeLabel="Storefront" badgeColor="blue"
const handleTogglePromotion = async (
  e: React.MouseEvent<HTMLButtonElement, MouseEvent>
) => {
  e.preventDefault()
  if (!isLoyaltyPointsPromoApplied) {
    await applyLoyaltyPointsOnCart()
  } else {
    // TODO remove loyalty points
  }
}
```

In the `handleTogglePromotion` function, you call the `applyLoyaltyPointsOnCart` function if the cart doesn't have a loyalty promotion. This will send a request to the API route you created earlier, which will execute the workflow that applies the loyalty promotion to the cart.

You'll implement removing the loyalty points promotion in a later step.

### Test it Out

To test out applying the loyalty points on the cart, start the Medusa application and Next.js Starter Storefront.

Then, in the checkout flow as an authenticated customer, click on the "Apply Loyalty Points" button. The checkout summary will be updated with the applied promotion and the discount amount.

If you don't want the promotion to be shown in the "Promotions(s) applied" section, you can filter the promotions in `src/modules/checkout/components/discount-code/index.tsx` to not show a promotion matching `cart.metadata.loyalty_promo_id`.

![Discounted amount is shown as part of the summary and the promotion is shown as part of the applied promotions](https://res.cloudinary.com/dza7lstvk/image/upload/v1744200895/Medusa%20Resources/Screenshot_2025-04-09_at_3.14.19_PM_abmtjh.png)

***

## Step 8: Remove Loyalty Points From Cart

In this step, you'll implement the functionality to remove the loyalty points promotion from the cart. This is useful if the customer changes their mind and wants to remove the promotion.

To implement this functionality, you'll need to:

- Create a workflow that removes the loyalty points promotion from the cart.
- Create an API route that executes the workflow.
- Create a function in the Next.js Starter Storefront that sends a request to the API route you created earlier.
- Use the function in the `handleTogglePromotion` function in the `LoyaltyPoints` component you created earlier.

### Create the Workflow

The workflow will have the following steps:

- [useQueryGraphStep](https://docs.medusajs.com/references/helper-steps/useQueryGraphStep/index.html.md): Retrieve the cart's details.
- [getCartLoyaltyPromoStep](#getCartLoyaltyPromoStep): Retrieve the cart's loyalty promotion.
- [updateCartPromotionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateCartPromotionsWorkflow/index.html.md): Update the cart's promotions to remove the loyalty promotion.
- [updateCartsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateCartsStep/index.html.md): Update the cart to remove the loyalty promotion ID from the metadata.
- [updatePromotionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updatePromotionsStep/index.html.md): Deactive the loyalty promotion.
- [useQueryGraphStep](https://docs.medusajs.com/references/helper-steps/useQueryGraphStep/index.html.md): Retrieve the cart's details again.

Since you already have all the steps, you can create the workflow.

To create the workflow, create the file `src/workflows/remove-loyalty-from-cart.ts` with the following content:

```ts title="src/workflows/remove-loyalty-from-cart.ts" collapsibleLines="1-15" expandButtonLabel="Show Imports" highlights={removeLoyaltyFromCartWorkflowHighlights}
import {
  createWorkflow,
  transform,
  WorkflowResponse,
} from "@medusajs/framework/workflows-sdk"
import {
  useQueryGraphStep,
  updateCartPromotionsWorkflow,
  updateCartsStep,
  updatePromotionsStep,
} from "@medusajs/medusa/core-flows"
import { getCartLoyaltyPromoStep } from "./steps/get-cart-loyalty-promo"
import { PromotionActions } from "@medusajs/framework/utils"
import { CartData } from "../utils/promo"

type WorkflowInput = {
  cart_id: string
}

const fields = [
  "id",
  "customer.*",
  "promotions.*",
  "promotions.application_method.*",
  "promotions.rules.*",
  "promotions.rules.values.*",
  "currency_code",
  "total",
  "metadata",
]

export const removeLoyaltyFromCartWorkflow = createWorkflow(
  "remove-loyalty-from-cart",
  (input: WorkflowInput) => {
    // @ts-ignore
    const { data: carts } = useQueryGraphStep({
      entity: "cart",
      fields,
      filters: {
        id: input.cart_id,
      },
    })

    const loyaltyPromo = getCartLoyaltyPromoStep({
      cart: carts[0] as unknown as CartData,
      throwErrorOn: "not-found",
    })

    updateCartPromotionsWorkflow.runAsStep({
      input: {
        cart_id: input.cart_id,
        promo_codes: [loyaltyPromo.code!],
        action: PromotionActions.REMOVE,
      },
    })

    const newMetadata = transform({
      carts,
    }, (data) => {
      const { loyalty_promo_id, ...rest } = data.carts[0].metadata || {}

      return {
        ...rest,
        loyalty_promo_id: null,
      }
    })

    updateCartsStep([
      {
        id: input.cart_id,
        metadata: newMetadata,
      },
    ])

    updatePromotionsStep([
      {
        id: loyaltyPromo.id,
        status: "inactive",
      },
    ])

    // retrieve cart with updated promotions
    // @ts-ignore
    const { data: updatedCarts } = useQueryGraphStep({
      entity: "cart",
      fields,
      filters: { id: input.cart_id },
    }).config({ name: "retrieve-cart" })

    return new WorkflowResponse(updatedCarts[0])
  }
)
```

You create a workflow that accepts an object with the cart's ID as input.

In the workflow, you:

- Use `useQueryGraphStep` to retrieve the cart's details. You pass the cart's ID as a filter to retrieve the cart.
- Check whether the cart has a loyalty promotion using the `getCartLoyaltyPromoStep`. You pass the `throwErrorOn` parameter with the value `not-found` to throw an error if a loyalty promotion isn't found in the cart.
- Update the cart's promotions using the `updateCartPromotionsWorkflow`, removing the loyalty promotion.
- Use the `transform` function to prepare the new metadata of the cart. You remove the `loyalty_promo_id` from the metadata.
- Update the cart's metadata with the new metadata using the `updateCartsStep`.
- Deactivate the loyalty promotion using the `updatePromotionsStep`.
- Retrieve the cart's details again using `useQueryGraphStep` to get the updated cart with the new loyalty promotion.
- Return the cart's details in a `WorkflowResponse` instance.

### Create the API Route

Next, you'll create the API route that executes this workflow.

To create the API route, add the following in `src/api/store/carts/[id]/loyalty-points/route.ts`:

```ts title="src/api/store/carts/[id]/loyalty-points/route.ts"
// other imports...
import { removeLoyaltyFromCartWorkflow } from "../../../../../workflows/remove-loyalty-from-cart"

// ...
export async function DELETE(
  req: MedusaRequest,
  res: MedusaResponse
) {
  const { id: cart_id } = req.params

  const { result: cart } = await removeLoyaltyFromCartWorkflow(req.scope)
    .run({
      input: {
        cart_id,
      },
    })

  res.json({ cart })
}
```

You export a `DELETE` route handler, which exposes a `DELETE` API route at `/store/carts/[id]/loyalty-points`.

In the route handler, you execute the `removeLoyaltyFromCartWorkflow` workflow, passing it the cart ID as an input. You return the cart's details in the response.

You can now use this API route in the Next.js Starter Storefront.

### Remove Loyalty Points in the Storefront

In the Next.js Starter Storefront, you need to add a server action function that sends a request to the API route you created earlier. Then, you'll use that function when the customer clicks the "Remove Loyalty Points" button, which shows when the cart has a loyalty promotion applied.

To add the function, add the following to `src/lib/data/cart.ts`:

```ts title="src/lib/data/cart.ts" badgeLabel="Storefront" badgeColor="blue"
export async function removeLoyaltyPointsOnCart() {
  const cartId = await getCartId()
  const headers = {
    ...(await getAuthHeaders()),
  }
  const next = {
    ...(await getCacheOptions("carts")),
  }

  return await sdk.client.fetch<{
    cart: HttpTypes.StoreCart & {
      promotions: HttpTypes.StorePromotion[]
    }
  }>(`/store/carts/${cartId}/loyalty-points`, {
    method: "DELETE",
    headers,
  })
  .then(async (result) => {
    const cartCacheTag = await getCacheTag("carts")
    revalidateTag(cartCacheTag)

    return result
  })
}
```

You create a `removeLoyaltyPointsOnCart` function that sends a request to the API route you created earlier.

In the function, you retrieve the cart ID stored in the cookie using the `getCartId` function, which is available in the Next.js Starter Storefront.

Then, you send the request to the API route. Once the request is resolved successfully, you revalidate the cart cache tag to ensure that the cart's details are updated and refetched by other components. This ensures that the promotion is removed from the checkout summary without needing to refresh the page.

Finally, you'll use this function in the `handleTogglePromotion` function in the `LoyaltyPoints` component you created earlier.

At the top of `src/modules/checkout/components/loyalty-points/index.tsx`, add the following import:

```tsx title="src/modules/checkout/components/loyalty-points/index.tsx" badgeLabel="Storefront" badgeColor="blue"
import { removeLoyaltyPointsOnCart } from "../../../../lib/data/cart"
```

Then, replace the `TODO` in `handleTogglePromotion` with the following:

```tsx title="src/modules/checkout/components/loyalty-points/index.tsx" badgeLabel="Storefront" badgeColor="blue"
await removeLoyaltyPointsOnCart()
```

In the `handleTogglePromotion` function, you call the `removeLoyaltyPointsOnCart` function if the cart has a loyalty promotion. This will send a request to the API route you created earlier, which will execute the workflow that removes the loyalty promotion from the cart.

### Test it Out

To test out removing the loyalty points from the cart, start the Medusa application and Next.js Starter Storefront.

Then, in the checkout flow as an authenticated customer, after applying the loyalty points, click on the "Remove Loyalty Points" button. The checkout summary will be updated with the removed promotion and the discount amount.

![The "Remove Loyalty Points" button is shown in the "Loyalty Points" section](https://res.cloudinary.com/dza7lstvk/image/upload/v1744204436/Medusa%20Resources/Screenshot_2025-04-09_at_4.13.24_PM_xt5trh.png)

***

## Step 9: Validate Loyalty Points on Cart Completion

After the customer applies the loyalty points to the cart and places the order, you need to validate that the customer actually has the loyalty points. This prevents edge cases where the customer may have applied the loyalty points previously but they don't have them anymore.

So, in this step, you'll hook into Medusa's cart completion flow to perform the validation.

Since Medusa uses workflows in its API routes, it allows you to hook into them and perform custom functionalities using [Workflow Hooks](https://docs.medusajs.com/docs/learn/fundamentals/workflows/workflow-hooks/index.html.md). A workflow hook is a point in a workflow where you can inject custom functionality as a step function, called a hook handler.

Medusa uses the [completeCartWorkflow](https://docs.medusajs.com/references/medusa-workflows/completeCartWorkflow/index.html.md) hook to complete the cart and place an order. This workflow has a `validate` hook that allows you to perform custom validation before the cart is completed.

To consume the `validate` hook, create the file `src/workflows/hooks/complete-cart.ts` with the following content:

```ts title="src/workflows/hooks/complete-cart.ts" highlights={completeCartWorkflowHookHighlights} collapsibleLines="1-6" expandButtonLabel="Show Imports"
import { completeCartWorkflow } from "@medusajs/medusa/core-flows"
import LoyaltyModuleService from "../../modules/loyalty/service"
import { LOYALTY_MODULE } from "../../modules/loyalty"
import { CartData, getCartLoyaltyPromotion } from "../../utils/promo"
import { MedusaError } from "@medusajs/framework/utils"

completeCartWorkflow.hooks.validate(
  async ({ cart }, { container }) => {
    const query = container.resolve("query")
    const loyaltyModuleService: LoyaltyModuleService = container.resolve(
      LOYALTY_MODULE
    )

    const { data: carts } = await query.graph({
      entity: "cart",
      fields: [
        "id", 
        "promotions.*", 
        "customer.*", 
        "promotions.rules.*", 
        "promotions.rules.values.*", 
        "promotions.application_method.*", 
        "metadata",
      ],
      filters: {
        id: cart.id,
      },
    }, {
      throwIfKeyNotFound: true,
    })

    const loyaltyPromo = getCartLoyaltyPromotion(
      carts[0] as unknown as CartData
    )

    if (!loyaltyPromo) {
      return
    }
    
    const customerLoyaltyPoints = await loyaltyModuleService.getPoints(
      carts[0].customer!.id
    )
    const requiredPoints = await loyaltyModuleService.calculatePointsFromAmount(
      loyaltyPromo.application_method!.value as number
    )

    if (customerLoyaltyPoints < requiredPoints) {
      throw new MedusaError(
        MedusaError.Types.INVALID_DATA,
        `Customer does not have enough loyalty points. Required: ${
          requiredPoints
        }, Available: ${customerLoyaltyPoints}`
      )
    }
  }
)
```

Workflows have a special `hooks` property that includes all the hooks tht you can consume in that workflow. You consume the hook by invoking it from the workflow's `hooks` property.

Since the hook is essentially a step function, it accepts the following parameters:

- The hook's input passed from the workflow, which differs for each hook. The `validate` hook receives an object having the cart's details.
- The step context object, which contains the Medusa container. You can use it to resolve services and perform actions.

In the hook, you resolve Query and the Loyalty Module's service. Then, you use Query to retrieve the cart's necessary details, including its promotions, customer, and metadata.

After that, you retrieve the customer's loyalty points and calculate the required points to apply the loyalty promotion.

If the customer doesn't have enough loyalty points, you throw an error. This will prevent the cart from being completed if the customer doesn't have enough loyalty points.

***

## Test Out Cart Completion with Loyalty Points

Since you now have the entire loyalty points flow implemented, you can test it out by going through the checkout flow, applying the loyalty points to the cart.

When you place the order, if the customer has sufficient loyalty points, the validation hook will pass.

Then, the `order.placed` event will be emitted, which will execute the subscriber that calls the `handleOrderPointsWorkflow`.

In the workflow, since the order's cart has a loyalty promotion, the points equivalent to the promotion will be deducted, and the promotion becomes inactive.

You can confirm that the loyalty points were deducted either by sending a request to the [retrieve loyalty points API route](#step-5-retrieve-loyalty-points-api-route), or by going through the checkout process again in the storefront.

***

## Next Steps

You've now implement a loyalty points system in Medusa. There's still more that you can implement based on your use case:

- Add loyalty points on registration or other events. Refer to the [Events Reference](https://docs.medusajs.com/references/events/index.html.md) for a full list of available events you can listen to.
- Show the customer their loyalty point usage history. This will require adding another data model in the Loyalty Module that records the usage history. You can create records of that data model when an order that has a loyalty promotion is placed, then customize the storefront to show a new page for loyalty points history.
- Customize the Medusa Admin to show a new page or [UI Route](https://docs.medusajs.com/docs/learn/fundamentals/admin/ui-routes/index.html.md) for loyalty points information and analytics.

If you're new to Medusa, check out the [main documentation](https://docs.medusajs.com/docs/learn/index.html.md), 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](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/index.html.md).


# Implement Quick Re-Order Functionality in Medusa

In this tutorial, you'll learn how to implement a re-order functionality in Medusa.

When you install a Medusa application, you get a fully-fledged commerce platform with a Framework for customization. The Medusa application's commerce features are built around [Commerce Modules](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/index.html.md) which are available out-of-the-box. The features include order-management features.

The Medusa Framework facilitates building custom features that are necessary for your business use case. In this tutorial, you'll learn how to implement a re-order functionality in Medusa. This feature is useful for businesses whose customers are likely to repeat their orders, such as B2B or food delivery businesses.

You can follow this guide whether you're new to Medusa or an advanced Medusa developer.

## Summary

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

- Install and set up Medusa.
- Define the logic to re-order an order.
- Customize the Next.js Starter Storefront to add a re-order button.

![Diagram showcasing the re-order logic](https://res.cloudinary.com/dza7lstvk/image/upload/v1746451309/Medusa%20Resources/reorder-summary_wnbbws.jpg)

- [Re-Order Repository](https://github.com/medusajs/examples/tree/main/re-order): Find the full code for this guide in this repository.
- [OpenApi Specs for Postman](https://res.cloudinary.com/dza7lstvk/raw/upload/v1741941475/OpenApi/product-reviews_jh8ohj.yaml): Import this OpenApi Specs file into tools like Postman.

***

## Step 1: Install a Medusa Application

### Prerequisites

- [Node.js v20+](https://nodejs.org/en/download)
- [Git CLI tool](https://git-scm.com/downloads)
- [PostgreSQL](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 asked whether you want to install the [Next.js Starter Storefront](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/nextjs-starter/index.html.md), choose Yes.

Afterwards, the installation process will start, which will install the Medusa application in a directory with your project's name, and the Next.js Starter Storefront in a separate directory with the `{project-name}-storefront` name.

The Medusa application is composed of a headless Node.js server and an admin dashboard. The storefront is installed or custom-built separately and connects to the Medusa application through its REST endpoints, called [API routes](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/index.html.md). Learn more in [Medusa's Architecture documentation](https://docs.medusajs.com/docs/learn/introduction/architecture/index.html.md).

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.

Check out the [troubleshooting guides](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/troubleshooting/create-medusa-app-errors/index.html.md) for help.

***

## Step 2: Implement Re-Order Workflow

To build custom commerce features in Medusa, you create a [workflow](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md). A workflow is a series of queries and actions, called steps, that complete a task.

By using workflows, you can track their executions' progress, define roll-back logic, and configure other advanced features. Then, you execute the workflow from other customizations, such as in an API Route.

In this section, you'll implement the re-order functionality in a workflow. Later, you'll execute the workflow in a custom API route.

Refer to the [Workflows documentation](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md) to learn more.

The workflow will have the following steps:

- [useQueryGraphStep](https://docs.medusajs.com/references/helper-steps/useQueryGraphStep/index.html.md): Retrieve the order's details.
- [createCartWorkflow](https://docs.medusajs.com/references/medusa-workflows/createCartWorkflow/index.html.md): Create a cart for the re-order.
- [addShippingMethodToCartWorkflow](https://docs.medusajs.com/references/medusa-workflows/addShippingMethodToCartWorkflow/index.html.md): Add the order's shipping method(s) to the cart.
- [useQueryGraphStep](https://docs.medusajs.com/references/helper-steps/useQueryGraphStep/index.html.md): Retrieve the cart's details.

This workflow uses steps from Medusa's `@medusajs/medusa/core-flows` package. So, you can implement the workflow without implementing custom steps.

### a. Create the Workflow

To create the workflow, create the file `src/workflows/reorder.ts` with the following content:

```ts title="src/workflows/reorder.ts" highlights={workflowHighlights1}
import { 
  createWorkflow, 
  transform, 
  WorkflowResponse,
} from "@medusajs/framework/workflows-sdk"
import { 
  addShippingMethodToCartWorkflow, 
  createCartWorkflow, 
  useQueryGraphStep,
} from "@medusajs/medusa/core-flows"

type ReorderWorkflowInput = {
  order_id: string
}

export const reorderWorkflow = createWorkflow(
  "reorder",
  ({ order_id }: ReorderWorkflowInput) => {
    // @ts-ignore
    const { data: orders } = useQueryGraphStep({
      entity: "order",
      fields: [
        "*",
        "items.*",
        "shipping_address.*",
        "billing_address.*",
        "region.*",
        "sales_channel.*",
        "shipping_methods.*",
        "customer.*",
      ],
      filters: {
        id: order_id,
      },
    })

    // TODO create a cart with the order's items
  }
)
```

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 to re-order.

In the workflow's constructor function, so far you use the `useQueryGraphStep` step to retrieve the order's details. This step uses [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md) under the hood, which allows you to query data across [modules](https://docs.medusajs.com/docs/learn/fundamentals/modules/index.html.md).

Refer to the [Query documentation](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md) to learn more about how to use it.

### b. Create a Cart

Next, you need to create a cart using the old order's details. You can use the `createCartWorkflow` step to create a cart, but you first need to prepare its input data.

Replace the `TODO` in the workflow with the following:

```ts title="src/workflows/reorder.ts" highlights={workflowHighlights2}
const createInput = transform({
  orders,
}, (data) => {
  return {
    region_id: data.orders[0].region_id!,
    sales_channel_id: data.orders[0].sales_channel_id!,
    customer_id: data.orders[0].customer_id!,
    email: data.orders[0].email!,
    billing_address: {
      first_name: data.orders[0].billing_address?.first_name!,
      last_name: data.orders[0].billing_address?.last_name!,
      address_1: data.orders[0].billing_address?.address_1!,
      city: data.orders[0].billing_address?.city!,
      country_code: data.orders[0].billing_address?.country_code!,
      province: data.orders[0].billing_address?.province!,
      postal_code: data.orders[0].billing_address?.postal_code!,
      phone: data.orders[0].billing_address?.phone!,
    },
    shipping_address: {
      first_name: data.orders[0].shipping_address?.first_name!,
      last_name: data.orders[0].shipping_address?.last_name!,
      address_1: data.orders[0].shipping_address?.address_1!,
      city: data.orders[0].shipping_address?.city!,
      country_code: data.orders[0].shipping_address?.country_code!,
      province: data.orders[0].shipping_address?.province!,
      postal_code: data.orders[0].shipping_address?.postal_code!,
      phone: data.orders[0].shipping_address?.phone!,
    },
    items: data.orders[0].items?.map((item) => ({
      variant_id: item?.variant_id!,
      quantity: item?.quantity!,
      unit_price: item?.unit_price!,
    })),
  }
})

const { id: cart_id } = createCartWorkflow.runAsStep({
  input: createInput,
})

// TODO add the shipping method to the cart
```

Data manipulation is not allowed in a workflow, as Medusa stores its definition before executing it. Instead, you can use `transform` from the Workflows SDK to manipulate the data.

Learn more about why you can't manipulate data in a workflow and the `transform` function in the [Data Manipulation in Workflows documentation](https://docs.medusajs.com/docs/learn/fundamentals/workflows/variable-manipulation/index.html.md).

`transform` accepts the following parameters:

1. The data to use in the transformation function.
2. A transformation function that accepts the data from the first parameter and returns the transformed data.

In the above code snippet, you use `transform` to create the input for the `createCartWorkflow` step. The input is an object that holds the cart's details, including its items, shipping and billing addresses, and more.

Learn about other input parameters you can pass in the [createCartWorkflow reference](https://docs.medusajs.com/references/medusa-workflows/createCartWorkflow/index.html.md).

After that, you execute the `createCartWorkflow` passing it the transformed input. The workflow returns the cart's details, including its ID.

### c. Add Shipping Methods

Next, you need to add the order's shipping method(s) to the cart. This saves the customer from having to select a shipping method again.

You can use the `addShippingMethodToCartWorkflow` step to add the shipping method(s) to the cart.

Replace the `TODO` in the workflow with the following:

```ts title="src/workflows/reorder.ts" highlights={workflowHighlights3}
const addShippingMethodToCartInput = transform({
  cart_id,
  orders,
}, (data) => {
  return {
    cart_id: data.cart_id,
    options: data.orders[0].shipping_methods?.map((method) => ({
      id: method?.shipping_option_id!,
      data: method?.data!,
    })) ?? [],
  }
})

addShippingMethodToCartWorkflow.runAsStep({
  input: addShippingMethodToCartInput,
})

// TODO retrieve and return the cart's details
```

Again, you use `transform` to prepare the input for the `addShippingMethodToCartWorkflow`. The input includes the cart's ID and the shipping method(s) to add to the cart.

Then, you execute the `addShippingMethodToCartWorkflow` to add the shipping method(s) to the cart.

### d. Retrieve and Return the Cart's Details

Finally, you need to retrieve the cart's details and return them as the workflow's output.

Replace the `TODO` in the workflow with the following:

```ts title="src/workflows/reorder.ts" highlights={workflowHighlights4}
// @ts-ignore
const { data: carts } = useQueryGraphStep({
  entity: "cart",
  fields: [
    "*",
    "items.*",
    "shipping_methods.*",
    "shipping_address.*",
    "billing_address.*",
    "region.*",
    "sales_channel.*",
    "promotions.*",
    "currency_code",
    "subtotal",
    "item_total",
    "total",
    "item_subtotal",
    "shipping_subtotal",
    "customer.*",
    "payment_collection.*",
    
  ],
  filters: {
    id: cart_id,
  },
}).config({ name: "retrieve-cart" })

return new WorkflowResponse(carts[0])
```

You execute the `useQueryGraphStep` again to retrieve the cart's details. Since you're re-using a step, you have to rename it using the `config` method.

Finally, you return the cart's details. A workflow must return an instance of `WorkflowResponse`.

The `WorkflowResponse` constructor accepts the workflow's output as a parameter, which is the cart's details in this case.

In the next step, you'll create an API route that exposes the re-order functionality.

***

## Step 3: Create Re-Order API Route

Now that you have the logic to re-order, you need to expose it so that frontend clients, such as a storefront, can use it. You do this by creating an [API route](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/index.html.md).

An API Route is an endpoint that exposes commerce features to external applications and clients, such as storefronts. You'll create an API route at the path `/store/customers/me/orders/:id` that executes the workflow from the previous step.

Refer to the [API Routes documentation](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/index.html.md) to learn more.

An API route is created in a `route.ts` file under a sub-directory of the `src/api` directory. The path of the API route is the file's path relative to `src/api`.

So, create the file `src/api/store/customers/me/orders/[id]/route.ts` with the following content:

```ts title="src/api/store/customers/me/orders/[id]/route.ts"
import { 
  AuthenticatedMedusaRequest, 
  MedusaResponse,
} from "@medusajs/framework/http"
import { reorderWorkflow } from "../../../../../../workflows/reorder"

export async function POST(
  req: AuthenticatedMedusaRequest,
  res: MedusaResponse
) {
  const { id } = req.params

  const { result } = await reorderWorkflow(req.scope).run({
    input: {
      order_id: id,
    },
  })

  return res.json({
    cart: result,
  })
}
```

Since you export a `POST` route handler function, you expose a `POST` API route at `/store/customers/me/orders/:id`.

API routes that start with `/store/customers/me` are protected by default, meaning that only authenticated customers can access them. Learn more in the [Protected API Routes documentation](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/protected-routes/index.html.md).

The route handler function accepts two parameters:

1. A request object with details and context on the request, such as path parameters or authenticated customer details.
2. A response object to manipulate and send the response.

In the route handler function, you execute the `reorderWorkflow`. To execute a workflow, you:

- Invoke it, passing it the [Medusa container](https://docs.medusajs.com/docs/learn/fundamentals/medusa-container/index.html.md) available in the `req.scope` property.
  - The Medusa container is a registry of Framework and commerce resources that you can resolve and use in your customizations.
- Call the `run` method, passing it an object with the workflow's input.

You pass the order ID from the request's path parameters as the workflow's input. Finally, you return the created cart's details in the response.

You'll test out this API route after you customize the Next.js Starter Storefront.

***

## Step 4: Customize the Next.js Starter Storefront

In this step, you'll customize the [Next.js Starter Storefront](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/nextjs-starter/index.html.md) to add a re-order button. You installed the Next.js Starter Storefront in the first step with the Medusa application, but you can also install it separately as explained in the [Next.js Starter Storefront documentation](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/nextjs-starter/index.html.md).

The Next.js Starter Storefront provides rich commerce features and a sleek design. You can use it as-is or build on top of it to tailor it for your business's unique use case, design, and customer experience.

The Next.js Starter Storefront was installed in a separate directory from Medusa. The directory's name is `{your-project}-storefront`.

So, if your Medusa application's directory is `medusa-reorder`, you can find the storefront by going back to the parent directory and changing to the `medusa-reorder-storefront` directory:

```bash
cd ../medusa-reorder-storefront # change based on your project name
```

To add the re-order button, you will:

- Add a server function that re-orders an order using the API route from the previous step.
- Add a button to the order details page that calls the server function.

### a. Add the Server Function

You'll add the server function for the re-order functionality in the `src/lib/data/orders.ts` file.

First, add the following import statement to the top of the file:

```ts title="src/lib/data/orders.ts" badgeLabel="Storefront" badgeColor="blue"
import { setCartId } from "./cookies"
```

Then, add the function at the end of the file:

```ts title="src/lib/data/orders.ts" badgeLabel="Storefront" badgeColor="blue"
export const reorder = async (id: string) => {
  const headers = await getAuthHeaders()

  const { cart } = await sdk.client.fetch<HttpTypes.StoreCartResponse>(
    `/store/customers/me/orders/${id}`,
    {
      method: "POST",
      headers,
    }
  )

  await setCartId(cart.id)

  return cart
}
```

You add a function that accepts the order ID as a parameter.

The function uses the `client.fetch` method of the [JS SDK](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/js-sdk/index.html.md) to send a request to the API route you created in the previous step.

The JS SDK is already configured in the Next.js Starter Storefront. Refer to the [JS SDK documentation](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/js-sdk/index.html.md) to learn more about it.

Once the request succeeds, you use the `setCartId` function that's defined in the storefront to set the cart ID in a cookie. This ensures the cart is used across the storefront.

Finally, you return the cart's details.

### b. Add the Re-Order Button Component

Next, you'll add the component that shows the re-order button. You'll later add the component to the order details page.

To create the component, create the file `src/modules/order/components/reorder-action/index.tsx` with the following content:

```tsx title="src/modules/order/components/reorder-action/index.tsx" badgeLabel="Storefront" badgeColor="blue" highlights={componentHighlights}
import { Button, toast } from "@medusajs/ui"
import { reorder } from "../../../../lib/data/orders"
import { useState } from "react"
import { useRouter } from "next/navigation"

type ReorderActionProps = {
  orderId: string
}

export default function ReorderAction({ orderId }: ReorderActionProps) {
  const [isLoading, setIsLoading] = useState(false)
  const router = useRouter()

  const handleReorder = async () => {
    setIsLoading(true)
    try {
      const cart = await reorder(orderId)

      setIsLoading(false)
      toast.success("Prepared cart to reorder. Proceeding to checkout...")
      router.push(`/${cart.shipping_address!.country_code}/checkout?step=payment`)
    } catch (error) {
      setIsLoading(false)
      toast.error(`Error reordering: ${error}`)
    }
  }

  return (
    <Button
      variant="primary"
      size="small"
      onClick={handleReorder}
      disabled={isLoading}
    >
      Reorder
    </Button>
  )
}
```

You create a `ReorderAction` component that accepts the order ID as a prop.

In the component, you render a button that, when clicked, calls a `handleReorder` function. The function calls the `reorder` function you created in the previous step to re-order the order.

If the re-order succeeds, you redirect the user to the payment step of the checkout page. If it fails, you show an error message.

### c. Show Re-Order Button on Order Details Page

Finally, you'll show the `ReorderAction` component on the order details page.

In `src/modules/order/templates/order-details-template.tsx`, add the following import statement to the top of the file:

```tsx title="src/modules/order/templates/order-details-template.tsx" badgeLabel="Storefront" badgeColor="blue"
import ReorderAction from "../components/reorder-action"
```

Then, in the return statement of the `OrderDetailsTemplate` component, find the `OrderDetails` component and add the `ReorderAction` component below it:

```tsx title="src/modules/order/templates/order-details-template.tsx" badgeLabel="Storefront" badgeColor="blue"
<ReorderAction orderId={order.id} />
```

The re-order button will now be shown on the order details page.

### Test it Out

You'll now test out the re-order functionality.

First, to start the Medusa application, run the following command in the Medusa application's directory:

```bash npm2yarn badgeLabel="Medusa application" badgeColor="green"
npm run dev
```

Then, in the Next.js Starter Storefront directory, run the following command to start the storefront:

```bash npm2yarn badgeLabel="Storefront" badgeColor="blue"
npm run dev
```

The storefront will be running at `http://localhost:8000`. Open it in your browser.

To test out the re-order functionality:

- Create an account in the storefront.
- Add a product to the cart and complete the checkout process to place an order.
- Go to Account -> Orders, and click on the "See details" button.

![Orders page on customer account](https://res.cloudinary.com/dza7lstvk/image/upload/v1746449666/Medusa%20Resources/Screenshot_2025-05-05_at_3.52.46_PM_ae4e78.png)

On the order's details page, you'll find a "Reorder" button.

![Reorder button on order details page](https://res.cloudinary.com/dza7lstvk/image/upload/v1746450255/Medusa%20Resources/Screenshot_2025-05-05_at_4.03.51_PM_xaslqo.png)

When you click on the button, a new cart will be created with the order's details, and you'll be redirected to the checkout page where you can complete the purchase.

![Checkout page showing the payment step](https://res.cloudinary.com/dza7lstvk/image/upload/v1746450342/Medusa%20Resources/Screenshot_2025-05-05_at_4.05.29_PM_vqpdqo.png)

***

## Next Steps

You now have a re-order functionality in your Medusa application and Next.js Starter Storefront. You can expand more on this feature based on your use case.

For example, you can add quick orders on the storefront's homepage, allowing customers to quickly re-order their last orders.

If you're new to Medusa, check out the [main documentation](https://docs.medusajs.com/docs/learn/index.html.md), 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](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/index.html.md).


# Use Saved Payment Methods During Checkout

In this tutorial, you'll learn how to allow customers to save their payment methods and use them for future purchases.

When you install a Medusa application, you get a fully-fledged commerce platform with a Framework for customization. The Medusa application's commerce features are built around [Commerce Modules](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/index.html.md) which are available out-of-the-box.

Medusa's architecture facilitates integrating third-party services, such as payment providers. These payment providers can process payments and securely store customers' payment methods for future use.

In this tutorial, you'll expand on Medusa's [Stripe Module Provider](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/payment/payment-provider/stripe/index.html.md) to allow customers to re-use their saved payment methods during checkout.

You can follow this guide whether you're new to Medusa or an advanced Medusa developer.

While this tutorial uses Stripe as an example, you can follow the same steps to implement saved payment methods with other payment providers.

## Summary

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

- Install and set up Medusa and the Next.js Starter Storefront.
- Set up the Stripe Module Provider in Medusa.
- Customize the checkout flow to save customers' payment methods.
- Allow customers to select saved payment methods during checkout.

![Diagram illustrating the features of this guide](https://res.cloudinary.com/dza7lstvk/image/upload/v1745309355/Medusa%20Resources/saved-payment-methods_orjnix.jpg)

[Saved Payment Methods Repository](https://github.com/medusajs/examples/tree/main/stripe-saved-payment): Find the full code for this guide in this repository.

***

## Step 1: Install a Medusa Application

### Prerequisites

- [Node.js v20+](https://nodejs.org/en/download)
- [Git CLI tool](https://git-scm.com/downloads)
- [PostgreSQL](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 asked whether you want to install the [Next.js Starter Storefront](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/nextjs-starter/index.html.md), choose Yes.

Afterwards, the installation process will start, which will install the Medusa application in a directory with your project's name, and the Next.js Starter Storefront in a separate directory with the `{project-name}-storefront` name.

The Medusa application is composed of a headless Node.js server and an admin dashboard. The storefront is installed or custom-built separately and connects to the Medusa application through its REST endpoints, called [API routes](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/index.html.md). Learn more in [Medusa's Architecture documentation](https://docs.medusajs.com/docs/learn/introduction/architecture/index.html.md).

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.

Check out the [troubleshooting guides](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/troubleshooting/create-medusa-app-errors/index.html.md) for help.

***

## Step 2: Set Up the Stripe Module Provider

Medusa's [Payment Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/payment/index.html.md) provides payment-related models and the interface to manage and process payments. However, it delegates the actual payment processing to module providers that integrate third-party payment services.

The [Stripe Module Provider](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/payment/payment-provider/stripe/index.html.md) is a Payment Module Provider that integrates Stripe into your Medusa application to process payments. It can also save payment methods securely.

In this section, you'll set up the Stripe Module Provider in your Medusa application.

### Prerequisites

- [Stripe account](https://stripe.com/)
- [Stripe Secret and Public API Keys](https://support.stripe.com/questions/locate-api-keys-in-the-dashboard)

### Register the Stripe Module Provider

To register the Stripe Module Provider in your Medusa application, add it to the array of providers passed to the Payment Module in `medusa-config.ts`:

```ts title="medusa-config.ts"
module.exports = defineConfig({
  // ...
  modules: [
    {
      resolve: "@medusajs/medusa/payment",
      options: {
        providers: [
          {
            resolve: "@medusajs/medusa/payment-stripe",
            id: "stripe",
            options: {
              apiKey: process.env.STRIPE_API_KEY,
            },
          },
        ],
      },
    },
  ],
})
```

The Medusa configuration accepts a `modules` array, which contains the modules to be loaded. While the Payment Module is loaded by default, you need to add it again when registering a new provider.

You register provides in the `providers` option of the Payment Module. Each provider is an object with the following properties:

- `resolve`: The package name of the provider.
- `id`: The ID of the provider. This is used to identify the provider in the Medusa application.
- `options`: The options to be passed to the provider. In this case, the `apiKey` option is required for the Stripe Module Provider.

Learn about other options in the [Stripe Module Provider](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/payment/payment-provider/stripe#module-options/index.html.md) documentation.

### Add Environment Variables

Next, add the following environment variables to your `.env` file:

```plain
STRIPE_API_KEY=sk_...
```

Where `STRIPE_API_KEY` is your Stripe Secret API Key. You can find it in the Stripe dashboard under Developers > API keys.

![Secret API Key in the Stripe dashboard](https://res.cloudinary.com/dza7lstvk/image/upload/v1745313823/Medusa%20Resources/Screenshot_2025-04-22_at_12.20.08_PM_w6rxxo.png)

### Enable Stripe in a Region

In Medusa, each [region](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/region/index.html.md) (which is a geographical area where your store operates) can have different payment methods enabled. So, after registering the Stripe Module Provider, you need to enable it in a region.

To enable it in a region, start the Medusa application with the following command:

```bash npm2yarn
npm run dev
```

Then, go to `localhost:9000/app` and log in with the user you created earlier.

Once you're logged in:

1. Go to Settings -> Regions.
2. Click on the region where you want to enable the payment provider.
3. Click the <InlineIcon Icon={EllipsisHorizontal} alt="three-dots" /> icon at the top right of the first section
4. Choose "Edit" from the dropdown menu
5. In the side window that opens, find the "Payment Providers" field and select Stripe from the dropdown.
6. Once you're done, click the "Save" button.

Stripe will now be available as a payment option during checkout.

The Stripe Module Provider supports different payment methods in Stripe, such as Bancontact or iDEAL. This guide focuses only on the card payment method, but you can enable other payment methods as well.

![Stripe in the dropdown](https://res.cloudinary.com/dza7lstvk/image/upload/v1745245433/Medusa%20Resources/Screenshot_2025-04-21_at_5.23.07_PM_yd7xji.png)

### Add Evnironement Variable to Storefront

The [Next.js Starter Storefront](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/nextjs-starter/index.html.md) supports payment with Stripe during checkout if it's enabled in the region.

The Next.js Starter Storefront was installed in a separate directory from Medusa. The directory's name is `{your-project}-storefront`.

So, if your Medusa application's directory is `medusa-payment`, you can find the storefront by going back to the parent directory and changing to the `medusa-payment-storefront` directory:

```bash
cd ../medusa-payment-storefront # change based on your project name
```

In the Next.js Starter Storefront project, add the Stripe public API key as an environment variable in `.env.local`:

```plain badgeLabel="Storefront" badgeColor="blue"
NEXT_PUBLIC_STRIPE_KEY=pk_123...
```

Where `NEXT_PUBLIC_STRIPE_KEY` is your Stripe public API key. You can find it in the Stripe dashboard under Developers > API keys.

***

## Step 3: List Payment Methods API Route

The Payment Module uses [account holders](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/payment/account-holder/index.html.md) to represent a customer's details that are stored in a third-party payment provider. Medusa creates an account holder for each customer, allowing you later to retrieve the customer's saved payment methods in the third-party provider.

![Diagram illustrating the relation between customers and account holders in Medusa, and customers in Stripe](https://res.cloudinary.com/dza7lstvk/image/upload/v1745314344/Medusa%20Resources/customer-account-stripe_in5ei6.jpg)

While this feature is available out-of-the-box, you need to expose it to clients, like storefronts, by creating an [API route](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/index.html.md). An API Route is an endpoint that exposes commerce features to external applications and clients.

In this step, you'll create an API route that lists the saved payment methods for an authenticated customer.

Refer to the [API routes](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/index.html.md) documentation to learn more.

### Create API Route

An API route is created in a `route.ts` file under a sub-directory of the `src/api` directory. The path of the API route is the file's path relative to `src/api`, and it can include path parameters using square brackets.

So, to create an API route at the path `/store/payment-methods/:account-holder-id`, create the file `src/api/store/payment-methods/[account_holder_id]/route.ts` with the following content:

```ts title="src/api/store/payment-methods/[account_holder_id]/route.ts" highlights={apiRouteHighlights}
import { MedusaError } from "@medusajs/framework/utils"
import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http"

export async function GET(
  req: MedusaRequest,
  res: MedusaResponse
) {
  const { account_holder_id } = req.params
  const query = req.scope.resolve("query")
  const paymentModuleService = req.scope.resolve("payment")

  const { data: [accountHolder] } = await query.graph({
    entity: "account_holder",
    fields: [
      "data",
      "provider_id",
    ],
    filters: {
      id: account_holder_id,
    },
  })

  if (!accountHolder) {
    throw new MedusaError(
      MedusaError.Types.NOT_FOUND, 
      "Account holder not found"
    )
  }

  const paymentMethods = await paymentModuleService.listPaymentMethods(
    {
      provider_id: accountHolder.provider_id,
      context: {
        account_holder: {
          data: {
            id: accountHolder.data.id,
          },
        },
      },
    }
  )

  res.json({
    payment_methods: paymentMethods,
  })
}
```

Since you export a route handler function named `GET`, you expose a `GET` API route at `/store/payment-methods/:account-holder-id`. The route handler function accepts two parameters:

1. A request object with details and context on the request, such as body parameters or authenticated customer details.
2. A response object to manipulate and send the response.

The request object has a `scope` property, which is an instance of the [Medusa container](https://docs.medusajs.com/docs/learn/fundamentals/medusa-container/index.html.md). The Medusa container is a registry of Framework and commerce tools that you can access in the API route.

You use the Medusa container to resolve:

- [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), which is a tool that retrieves data across modules in the Medusa application.
- The [Payment Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/payment/index.html.md)'s service, which provides an interface to manage and process payments with third-party providers.

You use Query to retrieve the account holder with the ID passed as a path parameter. If the account holder is not found, you throw an error.

Then, you use the [listPaymentMethods](https://docs.medusajs.com/references/payment/listPaymentMethods/index.html.md) method of the Payment Module's service to retrieve the payment providers saved in the third-party provider. The method accepts an object with the following properties:

- `provider_id`: The ID of the provider, such as Stripe's ID. The account holder stores the ID its associated provider.
- `context`: The context of the request. In this case, you pass the account holder's ID to retrieve the payment methods associated with it in the third-party provider.

Finally, you return the payment methods in the response.

### Protect API Route

Only authenticated customers can access and use saved payment methods. So, you need to protect the API route to ensure that only authenticated customers can access it.

To protect an API route, you can add a [middleware](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/middlewares/index.html.md). A middleware is a function executed when a request is sent to an API Route. You can add an authentication middleware that ensures that the request is authenticated before executing the route handler function.

Refer to the [Middlewares](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/middlewares/index.html.md) documentation to learn more.

Middlewares are added in the `src/api/middlewares.ts` file. So, create the file with the following content:

```ts title="src/api/middlewares.ts"
import { authenticate, defineMiddlewares } from "@medusajs/framework/http"

export default defineMiddlewares({
  routes: [
    {
      matcher: "/store/payment-methods/:provider_id/:account_holder_id",
      method: "GET",
      middlewares: [
        authenticate("customer", ["bearer", "session"]),
      ],
    },
  ],
})
```

The `src/api/middlewares.ts` file must use the `defineMiddlewares` function and export its result. The `defineMiddlewares` function accepts a `routes` array that accepts objects with the following properties:

- `matcher`: The path of the API route to apply the middleware to.
- `method`: The HTTP method of the API route to apply the middleware to.
- `middlewares`: An array of middlewares to apply to the API route.

You apply the `authenticate` middleware to the API route you created earlier. The `authenticate` middleware ensures that only authenticated customers can access the API route.

Refer to the [Protected Routes](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/protected-routes/index.html.md) documentation to learn more about the `authenticate` middleware.

Your API route can now only be accessed by authenticated customers. You'll test it out as you customize the Next.js Starter Storefront in the next steps.

***

## Step 4: Save Payment Methods During Checkout

In this step, you'll customize the checkout flow in the [Next.js Starter Storefront](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/nextjs-starter/index.html.md) to save payment methods during checkout.

The Next.js Starter Storefront was installed in a separate directory from Medusa. The directory's name is `{your-project}-storefront`.

So, if your Medusa application's directory is `medusa-payment`, you can find the storefront by going back to the parent directory and changing to the `medusa-payment-storefront` directory:

```bash
cd ../medusa-payment-storefront # change based on your project name
```

During checkout, when the customer chooses a payment method, such as Stripe, the Next.js Starter Storefront creates a [payment session](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/payment/payment-session/index.html.md) in Medusa using the [Initialize Payment Session](https://docs.medusajs.com/api/store#payment-collections_postpaymentcollectionsidpaymentsessions) API route.

Under the hood, Medusa uses the associated payment provider (Stripe) to initiate the payment process with the associated third-party payment provider. The [Initialize Payment Session](https://docs.medusajs.com/api/store#payment-collections_postpaymentcollectionsidpaymentsessions) API route accepts a `data` object parameter in the request body that allows you to pass data relevant to the third-party payment provider.

So, to save the payment method that the customer uses during checkout with Stripe, you must pass the `setup_future_usage` property in the `data` object. The `setup_future_usage` property is a Stripe-specific property that allows you to save the payment method for future use.

In `src/modules/checkout/components/payment/index.tsx` of the Next.js Starter Storefront, there are two uses of the `initiatePaymentSession` function. Update each of them to pass the `data` property:

```ts title="src/modules/checkout/components/payment/index.tsx" badgeLabel="Storefront" badgeColor="blue"
// update in two places
await initiatePaymentSession(cart, {
  // ...
  data: {
    setup_future_usage: "off_session",
  },
})
```

You customize the `initiatePaymentSession` function to pass the `data` object with the `setup_future_usage` property. You set the value to `off_session` to allow using the payment method outside of the checkout flow, such as for follow up payments. You can use `on_session` instead if you only want the payment method to be used by the customer during checkout.

By making this change, you always save the payment method that the customer uses during checkout. You can alternatively show a checkbox to confirm saving the payment method, and only pass the `data` object if the customer checks it.

### Test it Out

To test it out, start the Medusa application by running the following command in the Medusa application's directory:

```bash npm2yarn
npm run dev
```

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

```bash npm2yarn
npm run dev
```

You can open the storefront in your browser at `localhost:8000`. Then, create a new customer account by clicking on the "Account" link at the top right.

After creating an account and logging in, add a product to the cart and go to the checkout page. Once you get to the payment step, choose Stripe and enter a [test card number](https://docs.stripe.com/testing#cards), such as `4242 4242 4242 4242`.

Then, place the order. Once the order is placed, you can check on the Stripe dashboard that the payment method was saved by:

1. Going to the "Customers" section in the Stripe dashboard.
2. Clicking on the customer you just placed the order with.
3. Scrolling down to the "Payment methods" section. You'll find the payment method you just used to place the order.

![Saved payment method on the Stripe dashboard](https://res.cloudinary.com/dza7lstvk/image/upload/v1745249064/Medusa%20Resources/Screenshot_2025-04-21_at_6.24.00_PM_peotrd.png)

In the next step, you'll show the saved payment methods to the customer during checkout and allow them to select one of them.

***

## Step 5: Use Saved Payment Methods During Checkout

In this step, you'll customize the checkout flow in the Next.js Starter Storefront to show the saved payment methods to the customer and allow them to select one of them to place the order.

### Retrieve Saved Payment Methods

To retrieve the saved payment methods, you'll add a server function that retrieves the customer's saved payment methods from the API route you created earlier.

Add the following in `src/lib/data/payment.ts`:

```ts title="src/lib/data/payment.ts" badgeLabel="Storefront" badgeColor="blue" highlights={paymentHighlights}
export type SavedPaymentMethod = {
  id: string
  provider_id: string
  data: {
    card: {
      brand: string
      last4: string
      exp_month: number
      exp_year: number
    }
  }
}

export const getSavedPaymentMethods = async (accountHolderId: string) => {
  const headers = {
    ...(await getAuthHeaders()),
  }

  return sdk.client.fetch<{
    payment_methods: SavedPaymentMethod[]
  }>(
    `/store/payment-methods/${accountHolderId}`,
    {
      method: "GET",
      headers,
    }
  ).catch(() => {
    return {
      payment_methods: [],
    }
  })
}
```

You define a type for the retrieved payment methods. It contains the following properties:

- `id`: The ID of the payment method in the third-party provider.
- `provider_id`: The ID of the provider in the Medusa application, such as Stripe's ID.
- `data`: Additional data retrieved from the third-party provider related to the saved payment method. The type is modeled after the data returned by Stripe, but you can change it to match other payment providers.

You also create a `getSavedPaymentMethods` function that retrieves the saved payment methods from the API route you created earlier. The function accepts the account holder ID as a parameter and returns the saved payment methods.

### Add Saved Payment Methods Component

Next, you'll add the component that shows the saved payment methods and allows the customer to select one of them.

The component that shows the Stripe card element is defined in `src/modules/checkout/components/payment-container/index.tsx`. So, you'll define the component for the saved payment methods in the same file.

Start by adding the following import statements at the top of the file:

```ts title="src/modules/checkout/components/payment-container/index.tsx" badgeLabel="Storefront" badgeColor="blue"
import { Button } from "@medusajs/ui"
import { useEffect, useState } from "react"
import { HttpTypes } from "@medusajs/types"
import { SavedPaymentMethod, getSavedPaymentMethods } from "@lib/data/payment"
import { initiatePaymentSession } from "../../../../lib/data/cart"
import { capitalize } from "lodash"
```

Then, update the `PaymentContainerProps` type to include the payment session and cart details:

```ts title="src/modules/checkout/components/payment-container/index.tsx" badgeLabel="Storefront" badgeColor="blue"
type PaymentContainerProps = {
  // ...
  paymentSession?: HttpTypes.StorePaymentSession
  cart: HttpTypes.StoreCart
}
```

You'll need these details to find which saved payment method the customer selected, and to initiate a new payment session for the cart when the customer chooses a saved payment method.

Next, add the following component at the end of the file:

```tsx title="src/modules/checkout/components/payment-container/index.tsx" badgeLabel="Storefront" badgeColor="blue" highlights={stripeSavedPaymentMethodsHighlights}
const StripeSavedPaymentMethodsContainer = ({
  paymentSession,
  setCardComplete,
  setCardBrand,
  setError,
  cart,
}: {
  paymentSession?: HttpTypes.StorePaymentSession
  setCardComplete: (complete: boolean) => void
  setCardBrand: (brand: string) => void
  setError: (error: string | null) => void
  cart: HttpTypes.StoreCart
}) => {
  const [savedPaymentMethods, setSavedPaymentMethods] = useState<
    SavedPaymentMethod[]
  >([])
  const [selectedPaymentMethod, setSelectedPaymentMethod] = useState<
    string | undefined
  >(
    paymentSession?.data?.payment_method_id as string | undefined
  )

  useEffect(() => {
    const accountHolderId = (
      paymentSession?.context?.account_holder as Record<string, string>
    )
      ?.id

    if (!accountHolderId) {
      return
    }

    getSavedPaymentMethods(accountHolderId)
      .then(({ payment_methods }) => {
        setSavedPaymentMethods(payment_methods)
      })
  }, [paymentSession])

  useEffect(() => {
    if (!selectedPaymentMethod || !savedPaymentMethods.length) {
      setCardComplete(false)
      setCardBrand("")
      setError(null)
      return
    }
    const selectedMethod = savedPaymentMethods.find(
      (method) => method.id === selectedPaymentMethod
    )

    if (!selectedMethod) {
      return
    }

    setCardBrand(capitalize(selectedMethod.data.card.brand))
    setCardComplete(true)
    setError(null)
  }, [selectedPaymentMethod, savedPaymentMethods])

  const handleSelect = async (method: SavedPaymentMethod) => {
    // initiate a new payment session with the selected payment method
    await initiatePaymentSession(cart, {
      provider_id: method.provider_id,
      data: {
        payment_method_id: method.id,
      },
    }).catch((error) => {
      setError(error.message)
    })

    setSelectedPaymentMethod(method.id)
  }

  if (!savedPaymentMethods.length) {
    return <></>
  }

  // TODO add return statement
}
```

You define a `StripeSavedPaymentMethodsContainer` component that accepts the following props:

- `paymentSession`: The cart's current payment session.
- `setCardComplete`: A function to tell parent components whether the cart or payment method selection is complete. This allows the customer to proceed to the next step in the checkout flow.
- `setCardBrand`: A function to set the brand of the selected payment method. This is useful to show the brand of the selected payment method in review sections of the checkout flow.
- `setError`: A function to set the error message in case of an error.
- `cart`: The cart's details.

In the component, you define a state variable to store the saved payment methods and another one to store the selected payment method.

Then, you use the `useEffect` hook to retrieve the saved payment methods for the account holder set in the cart's payment session. You use the `getSavedPaymentMethods` function you created earlier to retrieve the saved payment methods.

You also use another `useEffect` hook that is executed when the selected payment method changes. In this hook, you check if the selected payment method is valid and set the card brand and completion status accordingly.

Finally, you define a `handleSelect` function that you'll execute when the customer selects a saved payment method. It creates a new payment session with the selected payment method.

To show the saved payment methods, replace the `TODO` with the following `return` statement:

```tsx title="src/modules/checkout/components/payment-container/index.tsx" badgeLabel="Storefront" badgeColor="blue"
return (
  <div className="flex flex-col gap-y-2">
    <Text className="txt-medium-plus text-ui-fg-base">
      Choose a saved payment method:
    </Text>
    {savedPaymentMethods.map((method) => (
      <div 
        key={method.id}
        className={`flex items-center justify-between p-4 border rounded-lg cursor-pointer hover:border-ui-border-interactive ${
          selectedPaymentMethod === method.id ? "border-ui-border-interactive" : ""
        }`}
        role="button"
        onClick={() => handleSelect(method)}
      >
        <div className="flex items-center gap-x-4">
          <input
            type="radio"
            name="saved-payment-method" 
            value={method.id}
            checked={selectedPaymentMethod === method.id}
            className="h-4 w-4 text-ui-fg-interactive"
            onChange={(e) => {
              if (e.target.checked) {
                handleSelect(method)
              }
            }}
          />
          <div className="flex flex-col">
            <span className="text-sm font-medium text-ui-fg-base">
              {capitalize(method.data.card.brand)} •••• {method.data.card.last4}
            </span>
            <span className="text-xs text-ui-fg-subtle">
              Expires {method.data.card.exp_month}/{method.data.card.exp_year}
            </span>
          </div>
        </div>
      </div>
    ))}
  </div>
)
```

You display the saved payment methods as radio buttons. When the customer selects one of them, you execute the `handleSelect` function to initiate a new payment session with the selected payment method.

### Modify Existing Stripe Element

Now that you have the component to show the saved payment methods, you need to modify the existing Stripe element to allow customers to select an existing payment method or enter a new one.

In the same `src/modules/checkout/components/payment-container/index.tsx` file, expand the new `paymentSession` and `cart` props of the `StripeCardContainer` component:

```ts title="src/modules/checkout/components/payment-container/index.tsx" badgeLabel="Storefront" badgeColor="blue"
export const StripeCardContainer = ({
  // ...
  paymentSession,
  cart,
}: Omit<PaymentContainerProps, "children"> & {
  // ...
}) => {
  // ...
}
```

Then, add a new state variable that keeps track of whether the customer is using a saved payment method or entering a new one:

```ts title="src/modules/checkout/components/payment-container/index.tsx" badgeLabel="Storefront" badgeColor="blue"
const [isUsingSavedPaymentMethod, setIsUsingSavedPaymentMethod] = useState(
  paymentSession?.data?.payment_method_id !== undefined
)
```

Next, add a function that resets the payment session when the customer switches between saved and new payment methods:

```ts title="src/modules/checkout/components/payment-container/index.tsx" badgeLabel="Storefront" badgeColor="blue"
const handleRefreshSession = async () => {
  await initiatePaymentSession(cart, {
    provider_id: paymentProviderId,
  })
  setIsUsingSavedPaymentMethod(false)
}
```

This function initiates a new payment session for the cart and disables the `isUsingSavedPaymentMethod` state variable.

Finally, replace the `return` statement of the `StripeCardContainer` component with the following:

```tsx title="src/modules/checkout/components/payment-container/index.tsx" badgeLabel="Storefront" badgeColor="blue" highlights={stripeCardReturnHighlights}
return (
  <PaymentContainer
    paymentProviderId={paymentProviderId}
    selectedPaymentOptionId={selectedPaymentOptionId}
    paymentInfoMap={paymentInfoMap}
    disabled={disabled}
    paymentSession={paymentSession}
    cart={cart}
  >
    {selectedPaymentOptionId === paymentProviderId &&
      (stripeReady ? (
        <div className="my-4 transition-all duration-150 ease-in-out">
          <StripeSavedPaymentMethodsContainer
            setCardComplete={setCardComplete}
            setCardBrand={setCardBrand}
            setError={setError}
            paymentSession={paymentSession}
            cart={cart}
          />
          {isUsingSavedPaymentMethod && (
            <Button 
              variant="secondary" 
              size="small" 
              className="mt-2" 
              onClick={handleRefreshSession}
            >
              Use a new payment method
            </Button>
          )}
          {!isUsingSavedPaymentMethod && (
            <>
              <Text className="txt-medium-plus text-ui-fg-base my-1">
                Enter your card details:
              </Text>
              <CardElement
                options={useOptions as StripeCardElementOptions}
                onChange={(e) => {
                  setCardBrand(
                    e.brand && e.brand.charAt(0).toUpperCase() + e.brand.slice(1)
                  )
                  setError(e.error?.message || null)
                  setCardComplete(e.complete)
                  }}
                />              
            </>
          )}
        </div>
      ) : (
        <SkeletonCardDetails />
      ))}
  </PaymentContainer>
)
```

You update the `return` statement to:

- Pass the new `paymentSession` and `cart` props to the `PaymentContainer` component.
- Show the `StripeSavedPaymentMethodsContainer` component before Stripe's card element.
- Add a button that's shown when the customer selects a saved payment method. The button allows the customer to switch back to entering a new payment method.

The existing Stripe element in checkout will now show the saved payment methods to the customer along with the component to enter a card's details.

Since you added new props to the `StripeCardContainer` and `PaymentContainer` components, you need to update other components that use them to pass the props.

In `src/modules/checkout/components/payment/index.tsx`, find usages of `StripeCardContainer` and `PaymentContainer` in the return statement and add the `paymentSession` and `cart` props:

```tsx title="src/modules/checkout/components/payment/index.tsx" badgeLabel="Storefront" badgeColor="blue" highlights={[["5"], ["6"], ["11"], ["12"]]}
<div key={paymentMethod.id}>
  {isStripeFunc(paymentMethod.id) ? (
    <StripeCardContainer
      // ...
      paymentSession={activeSession}
      cart={cart}
    />
  ) : (
    <PaymentContainer
      // ...
      paymentSession={activeSession}
      cart={cart}
    />
  )}
</div>
```

### Support Updating Stripe's Client Secret

The Next.js Starter Storefront uses Stripe's `Elements` component to wrap the payment elements. The `Elements` component requires a `clientSecret` prop, which is available in the cart's payment session.

With the recent changes, the client secret will be updated whenever a payment session is initiated, such as when the customer selects a saved payment method. However, the `options.clientSecret` prop of the `Elements` component is immutable, meaning that it cannot be changed after the component is mounted.

To force the component to re-mount and update the `clientSecret` prop, you can add a `key` prop to the `Elements` component. The `key` prop ensures that the `Elements` component re-mounts whenever the client secret changes, allowing Stripe to process the updated payment session.

In `src/modules/checkout/components/payment-wrapper/stripe-wrapper.tsx`, find the `Elements` component in the `return` statement and add the `key` prop:

```tsx title="src/modules/checkout/components/payment-wrapper/stripe-wrapper.tsx" badgeLabel="Storefront" badgeColor="blue" highlights={[["4"]]}
<Elements 
  options={options} 
  stripe={stripePromise} 
  key={options.clientSecret}
>
  {children}
</Elements>
```

You set the `key` prop to the client secret, which forces the `Elements` component to re-mount whenever the client secret changes.

### Support Payment with Saved Payment Method

The last change you need to make ensures that the customer can place an order with a saved payment method.

When the customer places the order, and they've chosen Stripe as a payment method, the Next.js Starter Storefront uses Stripe's `confirmCardPayment` method to confirm the payment. This method accepts either the ID of a saved payment method, or the details of a new card.

So, you need to update the `confirmCardPayment` usage to support passing the ID of the selected payment method if the customer has selected one.

In `src/modules/checkout/components/payment-button/index.tsx`, find the `handlePayment` method and update its first `if` condition:

```ts title="src/modules/checkout/components/payment-button/index.tsx" badgeLabel="Storefront" badgeColor="blue"
if (!stripe || !elements || (!card && !session?.data.payment_method_id) || !cart) {
  setSubmitting(false)
  return
}
```

This allows the customer to place their order if they have selected a saved payment method but have not entered a new card.

Then, find the usage of `confirmCardPayment` in the `handlePayment` function and change it to the following:

```ts title="src/modules/checkout/components/payment-button/index.tsx" badgeLabel="Storefront" badgeColor="blue" highlights={confirmPaymentHighlights}
await stripe
.confirmCardPayment(session?.data.client_secret as string, {
  payment_method: session?.data.payment_method_id as string || {
    card: card!,
    billing_details: {
      name:
        cart.billing_address?.first_name +
        " " +
        cart.billing_address?.last_name,
      address: {
        city: cart.billing_address?.city ?? undefined,
        country: cart.billing_address?.country_code ?? undefined,
        line1: cart.billing_address?.address_1 ?? undefined,
        line2: cart.billing_address?.address_2 ?? undefined,
        postal_code: cart.billing_address?.postal_code ?? undefined,
        state: cart.billing_address?.province ?? undefined,
      },
      email: cart.email,
      phone: cart.billing_address?.phone ?? undefined,
    },
  },
})
.then(({ error, paymentIntent }) => {
  if (error) {
    const pi = error.payment_intent

    if (
      (pi && pi.status === "requires_capture") ||
      (pi && pi.status === "succeeded")
    ) {
      onPaymentCompleted()
    }

    setErrorMessage(error.message || null)
    return
  }

  if (
    (paymentIntent && paymentIntent.status === "requires_capture") ||
    paymentIntent.status === "succeeded"
  ) {
    return onPaymentCompleted()
  }

  return
})
```

In particular, you're changing the `payment_method` property to either be the ID of the selected payment method, or the details of a new card. This allows the customer to place an order with either a saved payment method or a new one.

### Test it Out

You can now test out placing orders with a saved payment method.

To do that, start the Medusa application by running the following command in the Medusa application's directory:

```bash npm2yarn
npm run dev
```

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

```bash npm2yarn
npm run dev
```

In the Next.js Starter Storefront, login with the customer account you created earlier and add a product to the cart.

Then, proceed to the checkout flow. In the payment step, you should see the saved payment method you used earlier. You can select it and place the order.

![Saved payment method in checkout](https://res.cloudinary.com/dza7lstvk/image/upload/v1745308047/Medusa%20Resources/Screenshot_2025-04-21_at_4.07.34_PM_krudjq.png)

Once the order is placed successfully, you can check it in the Medusa Admin dashboard. You can view the order and capture the payment.

![Order in the Medusa Admin dashboard](https://res.cloudinary.com/dza7lstvk/image/upload/v1745318042/Medusa%20Resources/Screenshot_2025-04-22_at_1.33.39_PM_uynlfp.png)

***

## Next Steps

You've added support for saved payment methods in your Medusa application and Next.js Starter Storefront, allowing customers to save their payment methods during checkout and use them in future orders.

You can add more features to the saved payment methods, such as allowing customers to delete saved payment methods. You can use [Stripe's APIs](https://docs.stripe.com/api/payment_methods/detach) in the storefront or add an [API route](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/index.html.md) in Medusa to delete the saved payment method.

If you're new to Medusa, check out the [main documentation](https://docs.medusajs.com/docs/learn/index.html.md), 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](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/index.html.md).


# Implement Phone Authentication and Integrate Twilio SMS

In this tutorial, you will learn how to implement phone number authentication in your Medusa application.

When you install a Medusa application, you get a fully-fledged commerce platform with a Framework for customization. The Medusa application's commerce features are built around [Commerce Modules](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/index.html.md), which are available out-of-the-box. These features include authentication with custom providers and for custom user or actor types.

In this tutorial, you'll learn how to implement a custom authentication provider that allows customers to log in with their phone number. You'll also integrate [Twilio](https://www.twilio.com/en-us/messaging/channels/sms) to send SMS messages to those customers with the one-time password (OTP) for authentication.

Twilio is just one option to deliver the OTP to the customer. You can integrate a different SMS provider or use a different method to send OTPs.

## Summary

By following this tutorial, you will learn how to:

- Install and set up Medusa.
- Implement a custom phone authentication provider.
- Integrate Twilio to send OTPs by SMS.
- Customize the Next.js Starter Storefront to allow customers to log in with their phone numbers.

You can follow this tutorial whether you're new to Medusa or an advanced Medusa developer.

![Diagram showcasing the phone authentication flow](https://res.cloudinary.com/dza7lstvk/image/upload/v1747744941/Medusa%20Resources/phone-auth-overview_bt5yrp.jpg)

While this tutorial focuses on supporting phone authentication for customers, you can use the authentication provider for any actor type, such as admin user or vendor. [At the end of this tutorial](#next-steps), you'll learn how to authenticate other actor types.

- [Phone Authentication Repository](https://github.com/medusajs/examples/tree/main/phone-auth): Find the full code for this guide in this repository.
- [OpenApi Specs for Postman](https://res.cloudinary.com/dza7lstvk/raw/upload/v1747745832/OpenApi/Phone_Auth_g4xsqv.yaml): Import this OpenApi Specs file into tools like Postman.

***

## Step 1: Install a Medusa Application

### Prerequisites

- [Node.js v20+](https://nodejs.org/en/download)
- [Git CLI tool](https://git-scm.com/downloads)
- [PostgreSQL](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 asked whether you want to install the [Next.js Starter Storefront](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/nextjs-starter/index.html.md), choose Yes.

Afterward, the installation process will start, which will install the Medusa application in a directory with your project's name, and the Next.js Starter Storefront in a separate directory with the `{project-name}-storefront` name.

The Medusa application is composed of a headless Node.js server and an admin dashboard. The storefront is installed or custom-built separately and connects to the Medusa application through its REST endpoints, called [API routes](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/index.html.md). Learn more in [Medusa's Architecture documentation](https://docs.medusajs.com/docs/learn/introduction/architecture/index.html.md).

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. Afterward, you can log in with the new user and explore the dashboard.

Check out the [troubleshooting guides](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/troubleshooting/create-medusa-app-errors/index.html.md) for help.

***

## Step 2: Implement Phone Authentication Module Provider

In Medusa, you integrate custom authentication providers by creating an [Authentication Module Provider](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/auth/auth-providers/index.html.md). Then, you can use that provider to authenticate users using custom logic.

In this step, you'll create a Phone Authentication Module Provider that allows users to log in with their phone numbers and an OTP. Later, you'll integrate Twilio to send the OTPs to the users, and customize the storefront to allow customers to log in with their phone numbers.

An Authentication Module Provider doesn't need to handle storing and managing specific user details, such as creating customers or admin users. Instead, it only focuses on the logic of authenticating a type of user using custom logic or integration. You can learn more in the [Auth Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/auth/index.html.md) documentation.

### Prerequisite: Install jsonwebtoken

In the Phone Authentication Module Provider, you'll use the `jsonwebtoken` package to sign and verify the OTPs.

To install the package, run the following command in the Medusa application directory:

```bash npm2yarn
npm install jsonwebtoken
npm install @types/jsonwebtoken --save-dev
```

### a. Create Module Directory

Modules are created under the `src/modules` directory. So, start by creating the directory `src/modules/phone-auth`.

### b. Create Auth Module Provider Service

A module has a service that contains its logic. For Authentication Module Providers, the service implements the logic to authenticate users.

To create the service of the Phone Authentication Module Provider, create the file `src/modules/phone-auth/service.ts` with the following content:

```ts title="src/modules/phone-auth/service.ts" highlights={phoneAuthServiceHighlights}
import { 
  AbstractAuthModuleProvider, 
  AbstractEventBusModuleService, 
} from "@medusajs/framework/utils"
import { 
  Logger, 
} from "@medusajs/types"

type InjectedDependencies = {
  logger: Logger
  event_bus: AbstractEventBusModuleService
}

type Options = {
  jwtSecret: string
}

class PhoneAuthService extends AbstractAuthModuleProvider {
  static DISPLAY_NAME = "Phone Auth"
  static identifier = "phone-auth"
  private options: Options
  private logger: Logger
  private event_bus: AbstractEventBusModuleService

  constructor(container: InjectedDependencies, options: Options) {
    // @ts-ignore
    super(...arguments)

    this.options = options
    this.logger = container.logger
    this.event_bus = container.event_bus
  }
}

export default PhoneAuthService
```

An Authentication Module Provider's service must extend the `AbstractAuthModuleProvider` class. You'll get a type error about implementing the abstract methods of that class, which you'll add in the next steps.

An Authentication Module Provider must also have the following static properties:

- `identifier`: A unique identifier for the provider.
- `DISPLAY_NAME`: A human-readable name for the provider. This name is used for display purposes.

A module provider's constructor receives two parameters:

- `container`: The [module's container](https://docs.medusajs.com/docs/learn/fundamentals/modules/container/index.html.md) that contains Framework resources available to the module. You access the following resources:
  - `logger`: A [Logger](https://docs.medusajs.com/docs/learn/debugging-and-testing/logging/index.html.md) class to log debug messages.
  - `event_bus`: The [Event Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/infrastructure-modules/event/index.html.md)'s service to emit events.
- `options`: Options that are passed to the module provider when it's registered in Medusa's configurations. You define the following option:
  - `jwtSecret`: A secret used to sign and verify the OTPs.

You'll learn how to set this option when you [add the module provider to Medusa's configurations](#h-add-module-provider-to-medusas-configurations).

In the constructor, you set the class's properties to the injected dependencies and options.

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

Refer to the [Create Auth Module Provider](https://docs.medusajs.com/references/auth/provider/index.html.md) guide for detailed information about the methods.

### c. 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 `PhoneAuthService` class:

```ts title="src/modules/phone-auth/service.ts"
// other imports...
import { 
  MedusaError,
} from "@medusajs/framework/utils"

class PhoneAuthService extends AbstractAuthModuleProvider {
  // ...
  static validateOptions(options: Record<any, any>): void | never {
    if (!options.jwtSecret) {
      throw new MedusaError(
        MedusaError.Types.INVALID_DATA,
        "JWT secret is required"
      )
    }
  }
}
```

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

In the method, you throw an error if the `jwtSecret` option is not set.

### d. Implement register Method

When a customer (or another actor type) registers in your application, they must also have an [auth identity](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/auth/auth-identity-and-actor-types/index.html.md) that allows them to login.

The `register` method of an auth provider uses custom logic to create the auth identity for the actor type (such as customer). In the method, you can perform custom validation and specify the custom authentication details to store for the user's auth identity.

Medusa uses the `register` method to create an auth identity that will be associated with the customer when they register. You can learn more in the [Authentication Flows](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/auth/auth-flows/index.html.md) documentation.

![Diagram showcasing the relation between a customer and auth identity](https://res.cloudinary.com/dza7lstvk/image/upload/v1747747179/Medusa%20Resources/customer-auth-identity_je1bvh.jpg)

So, add the `register` method to the `PhoneAuthService` class:

```ts title="src/modules/phone-auth/service.ts" highlights={registerHighlights}
// other imports...
import { 
  AuthenticationInput, 
  AuthIdentityProviderService, 
  AuthenticationResponse, 
} from "@medusajs/types"

class PhoneAuthService extends AbstractAuthModuleProvider {
  // ...
  async register(
    data: AuthenticationInput,
    authIdentityProviderService: AuthIdentityProviderService
  ): Promise<AuthenticationResponse> {
    const { phone } = data.body || {}

    if (!phone) {
      return {
        success: false,
        error: "Phone number is required",
      }
    }

    try {
      await authIdentityProviderService.retrieve({
        entity_id: phone,
      })

      return {
        success: false,
        error: "User with phone number already exists",
      }
    } catch (error) {
      const user = await authIdentityProviderService.create({
        entity_id: phone,
      })

      return {
        success: true,
        authIdentity: user,
      }
    }
  }
}
```

#### Parameters

The `register` method receives an object parameter with the following properties:

- `data`: An object containing properties like `body` that holds request-body parameters. Clients will pass relevant authentication data, such as the user's phone number, in the request body.
- `authIdentityProviderService`: A service injected by the [Auth Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/auth/index.html.md) that allows you to manage auth identities.

The method receives other parameters, which you can find in the [Create Auth Module Provider](https://docs.medusajs.com/references/auth/provider#register/index.html.md) guide.

#### Method Logic

In the method, you extract the `phone` property from the request body, and return an error if it's not provided. You also return an error if another user is using the same phone number.

Otherwise, you create a new auth identity for the user. You set the phone number as the `entity_id` of the auth identity, which is a unique identifier.

#### Return Value

Finally, you return an object with the following properties:

- `success`: A boolean indicating whether the registration was successful.
- `authIdentity`: The created auth identity of the user. This property is only set if the registration was successful.
- `error`: An error message if the registration failed.

### e. Implement authenticate Method

When a customer (or another actor type) logs in, the `authenticate` method of an auth provider is called. This method uses custom logic to authenticate the user.

Authentication providers may implement one of the following flows:

- Direct authentication, where the user is authenticated using this method only. For example, authenticating with an email and password.
- Authentication with callback verification, where the user is authenticated using this method and then a callback is used to verify additional information.

For the Phone Authentication Module Provider, you'll implement the second flow. The user will first be authenticated using the `authenticate` method to make sure the user exists and generate an OTP. Then, they need to supply the OTP to verify their identity.

![Diagram showcasing authentication with callback verification](https://res.cloudinary.com/dza7lstvk/image/upload/v1747749683/Medusa%20Resources/authentication-flow_ey59hw.jpg)

So, add the `authenticate` method to the `PhoneAuthService` class:

```ts title="src/modules/phone-auth/service.ts" highlights={authenticateHighlights}
// other imports...
import { 
  AuthIdentityDTO,
} from "@medusajs/types"
import jwt from "jsonwebtoken"

class PhoneAuthService extends AbstractAuthModuleProvider {
  // ...
  async authenticate(
    data: AuthenticationInput,
    authIdentityProviderService: AuthIdentityProviderService
  ): Promise<AuthenticationResponse> {
    const { phone } = data.body || {}

    if (!phone) {
      return {
        success: false,
        error: "Phone number is required",
      }
    }

    try {
      await authIdentityProviderService.retrieve({
        entity_id: phone,
      })
    } catch (error) {
      return {
        success: false,
        error: "User with phone number does not exist",
      }
    }

    const { hashedOTP, otp } = await this.generateOTP()

    await authIdentityProviderService.update(phone, {
      provider_metadata: {
        otp: hashedOTP,
      },
    })

    await this.event_bus.emit({
      name: "phone-auth.otp.generated",
      data: {
        otp,
        phone,
      },
    }, {})

    return {
      success: true,
      location: "otp",
    }
  }

  async generateOTP(): Promise<{ hashedOTP: string, otp: string }> {
    // Generate a 6-digit OTP
    const otp = Math.floor(100000 + Math.random() * 900000).toString()

    // for debug
    this.logger.info(`Generated OTP: ${otp}`)
    
    const hashedOTP = jwt.sign({ otp }, this.options.jwtSecret, {
      expiresIn: "60s",
    })
    
    return { hashedOTP, otp }
  }
}
```

You add two methods: the `authenticate` method, and a helper `generateOTP` method.

#### authenticate Parameters

The `authenticate` method receives an object parameter with the following properties:

- `data`: An object containing properties like `body` that holds request-body parameters. Clients will pass relevant authentication data, such as the user's phone number, in the request body.
- `authIdentityProviderService`: A service injected by the [Auth Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/auth/index.html.md) that allows you to manage auth identities.

The method receives other parameters, which you can find in the [Create Auth Module Provider](https://docs.medusajs.com/references/auth/provider#authenticate/index.html.md) guide.

#### authenticate Logic

In the method, you return an error if the `phone` property is not provided in the request body, or if a user with that phone number doesn't exist.

Next, you generate a 6-digit OTP using the `generateOTP` method. Notice that you currently log the OTP for debugging purposes. You can remove this line later once you integrate Twilio.

The OTP is hashed and stored in the `provider_metadata` property of the user's auth identity. The `provider_metadata` property is a JSON object that stores additional information about the auth identity.

Then, you emit an event with the generated OTP and the user's phone number. This allows you later to handle the event and send the OTP to the user using services like Twilio.

#### authenticate Return Value

Finally, you return an object with the following properties:

- `success`: A boolean indicating whether the authentication was successful.
- `location`: A string indicating a URL to perform additional actions in. In this case, you set the location to `otp`, indicating that the user should verify with the OTP.
- `error`: An error message if the authentication failed.

### f. Implement validateCallback Method

When an authentication provider requires a callback to verify the user, the Medusa application calls the `validateCallback` method.

You can use this method to verify the OTP that the user entered. If valid, you return the logged in user, and the Medusa application will return a JWT token that the user can use to authenticate in the application.

![Diagram showcasing how callback verification fits in the authentication flow](https://res.cloudinary.com/dza7lstvk/image/upload/v1747749734/Medusa%20Resources/callback-flow_yney5a.jpg)

So, add the `validateCallback` method to the `PhoneAuthService` class:

```ts title="src/modules/phone-auth/service.ts" highlights={validateCallbackHighlights}
class PhoneAuthService extends AbstractAuthModuleProvider {
  // ...
  async validateCallback(
    data: AuthenticationInput,
    authIdentityProviderService: AuthIdentityProviderService
  ): Promise<AuthenticationResponse> {
    const { phone, otp } = data.query || {}

    if (!phone || !otp) {
      return {
        success: false,
        error: "Phone number and OTP are required",
      }
    }

    const user = await authIdentityProviderService.retrieve({
      entity_id: phone,
    })

    if (!user) {
      return {
        success: false,
        error: "User with phone number does not exist",
      }
    }
    
    // verify that OTP is correct
    const userProvider = user.provider_identities?.find((provider) => provider.provider === this.identifier)
    if (!userProvider || !userProvider.provider_metadata?.otp) {
      return {
        success: false,
        error: "User with phone number does not have a phone auth provider",
      }
    }
    
    try {
      const decodedOTP = jwt.verify(
        userProvider.provider_metadata.otp as string, 
        this.options.jwtSecret
      ) as { otp: string }
  
      if (decodedOTP.otp !== otp) {
        throw new Error("Invalid OTP")
      }
    } catch (error) {
      return {
        success: false,
        error: error.message || "Invalid OTP",
      }
    }
    
    const updatedUser = await authIdentityProviderService.update(phone, {
      provider_metadata: {
        otp: null,
      },
    })

    return {
      success: true,
      authIdentity: updatedUser,
    }
  }
}
```

#### Parameters

The `validateCallback` method receives an object parameter with the following properties:

- `data`: An object containing properties like `query` that holds query parameters. Clients will pass relevant authentication data, such as the user's phone number and OTP, in the request query.
- `authIdentityProviderService`: A service injected by the [Auth Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/auth/index.html.md) that allows you to manage auth identities.

The method receives other parameters, which you can find in the [Create Auth Module Provider](https://docs.medusajs.com/references/auth/provider#validatecallback/index.html.md) guide.

#### Method Logic

In the method, you return an error if the phone and otp aren't provided in the request query, or if a user with that phone number doesn't exist.

Next, you verify that the OTP provided by the user is correct. You retrieve the hashed OTP from the `provider_metadata` property of the user's auth identity. If the OTP is not valid, you return an error.

Since you set the hash expiration to 60 seconds, the OTP will be valid for 60 seconds. After that, the user will need to request a new OTP.

After that, you update the user's auth identity to remove the OTP from the `provider_metadata` property.

#### Return Value

Finally, you return an object with the following properties:

- `success`: A boolean indicating whether the authentication was successful.
- `authIdentity`: The user's auth identity. This property is only set if the authentication was successful.
- `error`: An error message if the authentication failed.

### g. Export Module Definition

You've now finished implementing the necessary methods for the Phone Authentication 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 name of the module, its service, and optionally its loaders.

To create the module's definition, create the file `src/modules/phone-auth/index.ts` with the following content:

```ts title="src/modules/phone-auth/index.ts"
import PhoneAuthService from "./service"
import { 
  ModuleProvider, 
  Modules,
} from "@medusajs/framework/utils"

export default ModuleProvider(Modules.AUTH, {
  services: [PhoneAuthService],
})
```

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.AUTH` in this case.
2. An object with a required property `services` indicating the module provider's services. Each of these services will be registered as authentication providers in Medusa.

### h. 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:

```ts title="medusa-config.ts"
// other imports...
import { Modules, ContainerRegistrationKeys } from "@medusajs/framework/utils"

module.exports = defineConfig({
  // ...
  modules: [
    {
      resolve: "@medusajs/medusa/auth",
      dependencies: [
        Modules.CACHE, 
        ContainerRegistrationKeys.LOGGER, 
        Modules.EVENT_BUS,
      ],
      options: {
        providers: [
          // default provider
          {
            resolve: "@medusajs/medusa/auth-emailpass",
            id: "emailpass",
          },
          {
            resolve: "./src/modules/phone-auth",
            id: "phone-auth",
            options: {
              jwtSecret: process.env.PHONE_AUTH_JWT_SECRET || "supersecret",
            },
          },
        ],
      },
    },
  ],
})
```

To pass an Auth Module Provider to the Auth Module, you add the `modules` property to the Medusa configuration and pass the Auth Module in its value.

The Auth Module accepts a `dependencies` option, allowing you to inject dependencies into the containers of the module and its providers. The Auth Module requires passing the [Cache Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/infrastructure-modules/cache/index.html.md) and Logger, but you also inject the `event_bus` dependency to use the Event Module's service in the Phone Authentication Module Provider.

The Auth Module also accepts a `providers` option, which is an array of Auth Module Providers to register. You register the `emailpass` provider, which is registered by default when you don't provide any other providers.

To register the Phone Authentication 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/phone-auth` directory.
- `id`: The ID of the module provider. The auth provider is then registered with the ID `au_{id}`.
- `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.

### i. Enable Phone Authentication for Customers

By default, customers and admin users can be authenticated using the `emailpass` provider. When you add a new provider, you need to specify which actor types can use it.

In `medusa-config.ts`, add to `projectConfig.http` a new `authMethodsPerActor` property:

```ts title="medusa-config.ts" highlights={authMethodsPerActorHighlights}
module.exports = defineConfig({
  projectConfig: {
    // ...
    http: {
      // ...
      authMethodsPerActor: {
        user: ["emailpass"],
        customer: ["emailpass", "phone-auth"],
      },
    },
  },
  // ...
})
```

The `authMethodsPerActor` property is an object whose keys are actor types. The values are arrays of authentication method IDs that can be used for that actor type.

In this case, you enable the `phone-auth` provider for customers. You can also enable it for other actor types, such as admin users or vendors.

### Test Out Phone Authentication

In this section, you'll test out the Phone Authentication Module Provider using Medusa's API routes. You can, instead, test it out later using the [Next.js Starter Storefront](#step-4-use-phone-authentication-in-the-nextjs-starter-storefront).

First, start the Medusa application with the following command:

```bash npm2yarn
npm run start
```

#### Prerequisite: Retrieve Publishable API Key

Before you start testing the authentication provider using the API routes, you need to retrieve your application's publishable API key. This key is necessary to send requests to API routes starting with `/store`.

To retrieve the publishable API key:

1. Open the Medusa Admin dashboard at `http://localhost:9000/admin` and log in.
2. Go to Settings -> Publishable API Keys.
3. Click on the API key in the table.
4. In its details page, click on the API key to copy it.

![Publishable API Key page with the API key clicked](https://res.cloudinary.com/dza7lstvk/image/upload/fl_lossy/f_auto/r_16/ar_16:9,c_pad/v1/User%20Guide/Screenshot_2025-02-25_at_6.14.15_PM_muwq9e.png)

#### a. Retrieve Registration Token

The first step is to retrieve a registration token for a new customer. This token will allow them to register in the application.

To retrieve the registration token, send a `POST` request to `/auth/customer/phone-auth/register`:

```bash
curl -X POST 'http://localhost:9000/auth/customer/phone-auth/register' \
--header 'Content-Type: application/json' \
--data '{
    "phone": "+19077890116"
}'
```

Make sure to replace the phone number with the one you want to use.

This will return a `token` in the response:

```json title="Example Response"
{
  "token": "123..."
}
```

#### b. Register Customer

Next, you'll register the customer using the [Register Customer](https://docs.medusajs.com/api/store#customers_postcustomers) API route. You'll pass the registration token you received in the previous step in the header of this request.

So, send a `POST` request to `/store/customers`:

```bash
curl -X POST 'http://localhost:9000/store/customers' \
--header 'x-publishable-api-key: {publishable_api_key}' \
--header 'Content-Type: application/json' \
--header 'Authorization: Bearer {reg_token}' \
--data-raw '{
    "email": "+19077890116@gmail.com",
    "phone": "19077890116",
    "first_name": "John",
    "last_name": "Smith"
}'
```

Make sure to replace:

- `{publishable_api_key}` with the publishable API key you retrieved from the Medusa Admin dashboard.
- `{reg_token}` with the registration token you received in the previous step.
- The customer details in the request body with the ones you want to use. Use the same phone number you used in the previous step.
  - You pass the email because it's required by the [Register Customer](https://docs.medusajs.com/api/store#customers_postcustomers) API route. You set it to the phone number with a `gmail.com` domain.

The request will return the created customer's details:

```json title="Example Response"
{
  "customer": {
    "id": "cus_01JVPESW5SM1MSVPNM2MSC0ZEC",
    "email": "+19077890116@gmail.com",
    "company_name": null,
    "first_name": "John",
    "last_name": "Smith",
    "phone": "19077890116",
    "metadata": null,
    "has_account": true,
    "deleted_at": null,
    "created_at": "2025-05-20T09:01:13.273Z",
    "updated_at": "2025-05-20T09:01:13.273Z",
    "addresses": []
  }
}
```

The customer can now authenticate using the phone number and OTP.

#### c. Authenticate Customer

Next, you'll authenticate the customer using the [Authenticate Customer](https://docs.medusajs.com/api/store#auth_postactor_typeauth_provider) API route. This would send the customer an OTP to their phone number (which you'll implement in the next step).

So, send a `POST` request to `/auth/customer/phone-auth`:

```bash
curl -X POST 'http://localhost:9000/auth/customer/phone-auth' \
--header 'Content-Type: application/json' \
--data '{
    "phone": "+19077890116"
}'
```

Make sure to replace the phone number with the one you used to register the customer.

This will return a `location` in the response:

```json title="Example Response"
{
  "location": "otp"
}
```

Indicating that the user should verify their OTP.

You can also use this route to resend the OTP if the user didn't receive it or if a minute has passed since the last OTP was sent.

#### d. Verify OTP

If you check the logs of the Medusa application, you'll see that the OTP was generated and logged:

```bash
info:    Generated OTP: 576794
```

As mentioned before, this is only for debugging purposes. In the next step, you'll implement the logic to send the OTP to the user using Twilio.

So, to verify the OTP, you'll send a request to the [Verify Callback API route](https://docs.medusajs.com/api/store#auth_postactor_typeauth_providercallback):

```bash
curl -X POST 'http://localhost:9000/auth/customer/phone-auth/callback?phone=%2B19077890116&otp=476588'
```

You pass the following query parameters:

- `phone`: The phone number of the customer. Make sure to use the same phone number you used to register the customer, and to encode it. For example, the `+` sign should be encoded as `%2B`.
- `otp`: The OTP that the customer received. Make sure to use the same OTP shown in the logs.

If the OTP is valid, you'll receive a JWT token in the response:

```json title="Example Response"
{
  "token": "123..."
}
```

You can use this token to authenticate the customer in the application. For example, you can use the token to [retrieve the customer's details](https://docs.medusajs.com/api/store#customers_getcustomersme).

If the OTP has expired, send a request to the [Authenticate Customer](#c-authenticate-customer) API route to generate a new OTP

***

## Step 3: Integrate Twilio SMS

Similar to the Auth Module, the [Notification Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/infrastructure-modules/notification/index.html.md) allows registering custom providers to send notifications, such as SMS or email.

In this step, you'll create a Twilio Notification Module Provider, then use it to send the OTP to the customer.

### Prerequisites

- [Twilio Account](https://console.twilio.com/)
- [Twilio From Phone Number](https://www.twilio.com/docs/phone-numbers)
- [Twilio Account SID, which you can retrieve from the Twilio Console homepage.](https://www.twilio.com/docs/usage/tutorials/how-to-use-your-free-trial-account-namer#console-dashboard-home-page)
- [Twilio Auth Token, which you can retrieve from the Twilio Console homepage.](https://www.twilio.com/docs/usage/tutorials/how-to-use-your-free-trial-account-namer#console-dashboard-home-page)

### a. Install Twilio SDK

Before you start implementing the Twilio Notification Module Provider, install the Twilio SDK to interact with the Twilio API.

Run the following command in the Medusa application directory:

```bash npm2yarn
npm install twilio
```

You'll use the Twilio SDK in the Notification Module Provider's service.

### b. Create Module Directory

Create the directory `src/modules/twilio-sms` to create the Twilio Notification Module Provider.

### c. Create Notification Module Provider Service

A Notification Module Provider has a service that contains the sending logic. The service must extend the `AbstractNotificationProviderService` class.

So, create the file `src/modules/twilio-sms/service.ts` with the following content:

```ts title="src/modules/twilio-sms/service.ts" highlights={twilioSmsServiceHighlights}
import { 
  AbstractNotificationProviderService,
} from "@medusajs/framework/utils"
import { Twilio } from "twilio"

type InjectedDependencies = {}

type TwilioSmsServiceOptions = {
  accountSid: string
  authToken: string
  from: string
}

class TwilioSmsService extends AbstractNotificationProviderService {
  static readonly identifier = "twilio-sms"
  private readonly client: Twilio
  private readonly from: string

  constructor(container: InjectedDependencies, options: TwilioSmsServiceOptions) {
    super()

    this.client = new Twilio(options.accountSid, options.authToken)
    this.from = options.from
  }
}
```

You'll get a type error about implementing the abstract methods of the `AbstractNotificationProviderService` class, which you'll add in the next steps.

A Notification Module Provider must have an `identifier` static property, which is a unique identifier for the module. This identifier is used to register the module in the Medusa application.

A module provider's constructor receives two parameters:

- `container`: The [module's container](https://docs.medusajs.com/docs/learn/fundamentals/modules/container/index.html.md) 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 option:
  - `accountSid`: The Twilio account SID.
  - `authToken`: The Twilio auth token.
  - `from`: The Twilio phone number to send the SMS from.

You'll learn how to set these options when you [add the module provider to Medusa's configurations](#g-add-module-provider-to-medusas-configurations).

In the constructor, you set the class's properties to the injected dependencies and options.

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

Refer to the [Create Notification Module Provider](https://docs.medusajs.com/references/notification-provider-module/index.html.md) guide for detailed information about the methods.

### 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 `TwilioSmsService` class:

```ts title="src/modules/twilio-sms/service.ts"
class TwilioSmsService extends AbstractNotificationProviderService {
  // ...
  static validateOptions(options: Record<any, any>): void | never {
    if (!options.accountSid) {
      throw new Error("Account SID is required")
    }
    if (!options.authToken) {
      throw new Error("Auth token is required")
    }
    if (!options.from) {
      throw new Error("From 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

The only required method for a Notification Module Provider is the `send` method. When the Medusa application needs to send a notification using the provider's channel (such as SMS), it calls this method of the registered provider.

So, add the `send` method to the `TwilioSmsService` class:

```ts title="src/modules/twilio-sms/service.ts" highlights={sendHighlights}
// other imports...
import { 
  ProviderSendNotificationDTO, 
  ProviderSendNotificationResultsDTO,
} from "@medusajs/types"

class TwilioSmsService extends AbstractNotificationProviderService {
  // ...
  async send(
    notification: ProviderSendNotificationDTO
  ): Promise<ProviderSendNotificationResultsDTO> {
    const { to, content, template, data } = notification
    const contentText = content?.text || await this.getTemplateContent(
      template, data
    )

    const message = await this.client.messages.create({
      body: contentText,
      from: this.from,
      to,
    })

    return {
      id: message.sid,
    }
  }

  async getTemplateContent(
    template: string, 
    data?: Record<string, unknown> | null
  ): Promise<string> {
    switch (template) {
      case "otp-template":
        if (!data?.otp) {
          throw new Error("OTP is required for OTP template")
        }

        return `Your OTP is ${data.otp}`
      default:
        throw new Error(`Template ${template} not found`)
    }
  }
}
```

You implement the `send` method and a helper `getTemplateContent` method.

#### send Parameters

The `send` method receives an object parameter with the following properties:

- `to`: The phone number to send the SMS to.
- `content`: An object containing the content of the SMS. The `text` property is the text to send.
- `template`: The template to use for the SMS. This is used to retrieve the fallback content of the SMS if `content.text` is not provided.
- `data`: An object containing the data to use in the template. This is used to replace placeholders in the template with actual values.

The method receives other parameters, which you can find in the [Create Notification Module Provider](https://docs.medusajs.com/references/notification-provider-module#send/index.html.md) guide.

#### send Method Logic

In the method, you set the SMS content either to the `text` property of the `content` object or to the template content. You define a `getTemplateContent` method that retrieves the content for a template.

Then, you use the `messages.create` method of the Twilio client to send the SMS. You pass the following parameters:

- `body`: The content of the SMS.
- `from`: The Twilio phone number to send the SMS from.
- `to`: The phone number to send the SMS to.

#### send Return Value

Finally, you return an object that has an `id` property with the ID of the sent SMS. This ID is stored in the notification record in the database.

### f. Export Module Definition

You've now finished implementing the necessary methods for the Twilio Notification Module Provider. You only need to export its definition.

To create the module's definition, create the file `src/modules/twilio-sms/index.ts` with the following content:

```ts title="src/modules/twilio-sms/index.ts"
import { 
  ModuleProvider, 
  Modules,
} from "@medusajs/framework/utils"
import TwilioSMSNotificationService from "./service"

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

You use `ModuleProvider` from the Modules SDK to create the module provider's definition passing it 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. Each of these services will be registered as notification providers in Medusa.

### g. Add Module Provider to Medusa's Configurations

You'll now add the Twilio Notification Module Provider to Medusa's configurations to start using it.

In `medusa-config.ts`, add the following to the `modules` property:

```ts title="medusa-config.ts"
module.exports = defineConfig({
  // ...
  modules: [
    // ...
    {
      resolve: "@medusajs/medusa/notification",
      options: {
        providers: [
          // default provider
          {
            resolve: "@medusajs/medusa/notification-local",
            id: "local",
            options: {
              name: "Local Notification Provider",
              channels: ["feed"],
            },
          },
          {
            resolve: "./src/modules/twilio-sms",
            id: "twilio-sms",
            options: {
              channels: ["sms"],
              accountSid: process.env.TWILIO_ACCOUNT_SID,
              authToken: process.env.TWILIO_AUTH_TOKEN,
              from: process.env.TWILIO_FROM,
            },
          },
        ],
      },
    },
  ],
})
```

You pass the Notification Module in the `modules` property to register the Twilio Notification Module Provider.

The Notification Module accepts a `providers` option, which is an array of Notification Module Providers to register. You register the `local` provider, which is registered by default when you don't provide any other providers.

To register the Twilio Notification Module Provider, you add an object with the following properties:

- `resolve`: The path to the module provider.
- `id`: The ID of the module provider. The notification provider is then registered with the ID `np_{identifier}_{id}`.
- `options`: The options to pass to the module provider. These include the options you defined in the `Options` interface of the module provider's service.
  - `channels`: The channels that the notification provider supports. In this case, you set it to `sms`, which is the channel used to send SMS notifications.
  - `accountSid`: The Twilio account SID.
  - `authToken`: The Twilio auth token.
  - `from`: The Twilio phone number to send the SMS from.

### h. Add Environment Variables

To set the value of the Twilio options, add the following environment variables to your `.env` file:

```shell title=".env"
TWILIO_ACCOUNT_SID=AC...
TWILIO_AUTH_TOKEN=05...
TWILIO_FROM=+1...
```

Where:

- `TWILIO_ACCOUNT_SID`: The Twilio account SID.
- `TWILIO_AUTH_TOKEN`: The Twilio auth token.
- `TWILIO_FROM`: The Twilio phone number to send the SMS from. Make sure to use the phone number you purchased from Twilio.

You can retrieve these information from the Twilio Console homepage.

![Twilio console homepage showing the account SID, phone number, and auth token](https://res.cloudinary.com/dza7lstvk/image/upload/v1747736641/Medusa%20Resources/CleanShot_2025-05-20_at_13.22.55_2x_sztmfo.png)

### i. Handle OTP Generated Event

Now that you have integrated Twilio into Medusa, you can use it to send the OTP to the customer. To do that, you need to handle the `phone-auth.otp.generated` event that you emitted in the `authenticate` method of the Phone Authentication Module Provider.

You can listen to events in a [subscriber](https://docs.medusajs.com/docs/learn/fundamentals/events-and-subscribers/index.html.md). A subscriber is an asynchronous function that listens to events to perform actions when the event is emitted.

In this step, you'll create a subscriber that listens to the `phone-auth.otp.generated` event and sends an SMS to the customer with the OTP.

Refer to the [Events and Subscribers](https://docs.medusajs.com/docs/learn/fundamentals/events-and-subscribers/index.html.md) documentation to learn more.

Subscribers are created in a TypeScript or JavaScript file under the `src/subscribers` directory. So, to create a subscriber, create the file `src/subscribers/send-otp.ts` with the following content:

```ts title="src/subscribers/send-otp.ts" highlights={sendOtpHighlights}
import {
  SubscriberArgs,
  type SubscriberConfig,
} from "@medusajs/medusa"
import { Modules } from "@medusajs/framework/utils"

export default async function sendOtpHandler({
  event: { data: {
    phone,
    otp,
  } },
  container,
}: SubscriberArgs<{ phone: string, otp: string }>) {
  const notificationModuleService = container.resolve(
    Modules.NOTIFICATION
  )

  await notificationModuleService.createNotifications({
    to: phone,
    channel: "sms",
    template: "otp-template",
    data: {
      otp,
    },
  })
}

export const config: SubscriberConfig = {
  event: "phone-auth.otp.generated",
}
```

The subscriber file must export:

- An asynchronous subscriber function that's executed whenever the associated event is triggered.
- A configuration object with an event property whose value is the event the subscriber is listening to, which is `phone-auth.otp.generated`.

The subscriber function accepts an object with the following properties:

- `event`: An object with the event's data payload. In the `authenticate` method, you emitted the event with the following data:
  - `phone`: The phone number of the user.
  - `otp`: The OTP that was generated.
- `container`: The [Medusa container](https://docs.medusajs.com/docs/learn/fundamentals/medusa-container/index.html.md), which you can use to resolve Framework and commerce resources.

In the subscriber function, you resolve the Notification Module's service from the Medusa container. Then, you use its `createNotifications` method to send the OTP to the user.

Under the hood, the Notification Module's service delegates the sending to the Notification Module Provider of the `sms` channel, which is the Twilio Notification Module Provider in this case.

The `createNotifications` method accepts an object with the following properties:

- `to`: The phone number to send the notification to. You use the phone number from the event's data payload.
- `channel`: The channel to use to send the notification, which is `sms`.
- `template`: The template to use for the notification content, which is `otp-template`.
- `data`: An object containing the data to use in the template. You pass the OTP to the template.

### j. Test it Out

To test out the Twilio Notification Module Provider, you can follow the steps in the [Test Out Phone Authentication](#test-out-phone-authentication) section.

After you authenticate the customer, the OTP will be sent to the customer's phone number using Twilio. Then, you can use the OTP to verify the authentication and receive a JWT token.

Alternatively, you can also test it out after customizing the Next.js Starter Storefront, which you'll do in the next step.

Make sure to remove the OTP logging line in the `generateOTP` method of the Phone Authentication Module Provider's service now that you have integrated Twilio.

***

## Step 4: Use Phone Authentication in the Next.js Starter Storefront

In this step, you'll customize the [Next.js Starter Storefront](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/nextjs-starter/index.html.md) to allow customers to authenticate using their phone number and OTP.

By default, the Next.js Starter Storefront supports email and password authentication. You'll replace it with phone authentication, but you can also keep both authentication methods if you want to.

The Next.js Starter Storefront was installed in a separate directory from Medusa. The directory's name is `{your-project}-storefront`.

So, if your Medusa application's directory is `medusa-phone-auth`, you can find the storefront by going back to the parent directory and changing to the `medusa-phone-auth-storefront` directory:

```bash
cd ../medusa-phone-auth-storefront # change based on your project name
```

### a. Install Phone Input Package

To easily show a phone input where the user can enter their phone number, install the `react-phone-number-input` package:

```bash npm2yarn badgeLabel="Storefront" badgeColor="blue"
npm install react-phone-number-input
```

You'll use it in the login and registration forms to show a phone input.

### b. Add Authenticate Function

Before adding the forms, you'll add the functions that send requests to the Medusa API to authenticate the customer.

The first one you'll add is the `authenticateWithPhone` function, which sends a request to the `/auth/customer/phone-auth` API route to authenticate the customer using their phone number.

In `src/lib/data/customer.ts`, add the following function:

```ts title="src/lib/data/customer.ts" badgeLabel="Storefront" badgeColor="blue"
export const authenticateWithPhone = async (phone: string) => {
  try {
    const response = await sdk.auth.login("customer", "phone-auth", {
      phone,
    })

    if (
      typeof response === "string" || 
      !response.location || 
      response.location !== "otp"
    ) {
      throw new Error("Failed to login")
    }

    return true
  } catch (error: any) {
    return error.toString()
  }
}
```

The function accepts the phone number as a parameter.

In the function, you use the [JS SDK](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/js-sdk/index.html.md), which is configured within the Next.js Starter Storefront, to send a request to the `/auth/customer/phone-auth` API route. You pass the phone number in the request body.

If the request doesn't return a `location` property set to `otp`, you throw an error. Otherwise, you return `true` to indicate that the request was successful.

### c. add Verify OTP Function

Next, you'll add the `verifyOTP` function, which sends a request to the `/auth/customer/phone-auth/callback` API route to verify the OTP.

In `src/lib/data/customer.ts`, add the following function:

```ts title="src/lib/data/customer.ts" badgeLabel="Storefront" badgeColor="blue" highlights={verifyOtpHighlights}
export const verifyOtp = async ({
  otp,
  phone,
}: {
  otp: string
  phone: string
}) => {
  try {
    const token = await sdk.auth.callback("customer", "phone-auth", {
      phone,
      otp,
    })

    await setAuthToken(token)

    const customerCacheTag = await getCacheTag("customers")
    revalidateTag(customerCacheTag)

    await transferCart()

    return true
  } catch (e: any) {
    return e.toString()
  }  
}
```

The function accepts an object with the following properties:

- `otp`: The OTP to verify.
- `phone`: The phone number of the customer.

In the function, you use the JS SDK to send a request to the `/auth/customer/phone-auth/callback` API route. You pass the phone number and OTP in the request body.

If the request is successful and you receive a token, you set the token in the cookies and refresh the customer cache. This ensures that all customer-related UI is updated after logging in, such as showing the customer's profile when accessing the `/account` page.

Then, you call the `transferCart` function to transfer the cart from the guest user to the authenticated customer.

Finally, you return `true` to indicate that the request was successful.

### d. Add Registration Function

The last function you'll add is the `registerWithPhone` function, which will register the customer using their phone number.

In `src/lib/data/customer.ts`, add the following function:

```ts title="src/lib/data/customer.ts" badgeLabel="Storefront" badgeColor="blue" highlights={registerWithPhoneHighlights}
export const registerWithPhone = async ({
  firstName,
  lastName,
  phone,
}: {
  firstName: string
  lastName: string
  phone: string
}) => {
  try {
    const { token: regToken } = await sdk.client.fetch<
      { token: string }
    >(`/auth/customer/phone-auth/register`, {
      method: "POST",
      body: {
        phone,
      },
    })
    
    await setAuthToken(regToken as string)
    const headers = {
      ...(await getAuthHeaders()),
    }

    const email = `${phone}@gmail.com`
    const customerData = {
      email,
      first_name: firstName,
      last_name: lastName,
      phone,
    }
    
    await sdk.store.customer.create(
      customerData,
      {},
      headers
    )

    return await authenticateWithPhone(phone)
  } catch (error: any) {
    return error.toString()
  }
}
```

The function accepts an object with the following properties:

- `firstName`: The first name of the customer.
- `lastName`: The last name of the customer.
- `phone`: The phone number of the customer.

In the function, you retrieve a registration token for the customer using the `/auth/customer/phone-auth/register` API route. You pass the phone number in the request body.

Then, after setting the registration token in the cookies, you create a customer using the [Create Customer](https://docs.medusajs.com/api/store#customers_postcustomers) API route. You pass the following properties in the request body:

- `email`: The email of the customer. You set it to the phone number with a `@gmail.com` domain.
- `first_name`: The first name of the customer.
- `last_name`: The last name of the customer.
- `phone`: The phone number of the customer.

Finally, you call the `authenticateWithPhone` function to authenticate the customer using their phone number. At this step, the customer would receive an OTP to login.

### e. Add OTP Form

Next, you'll add an OTP form that allows the customer to enter the OTP they receive after login or registration. Later, you'll reuse this form in both the login and registration pages.

Create the file `src/modules/account/components/otp/index.tsx` with the following content:

```tsx title="src/modules/account/components/otp/index.tsx" badgeLabel="Storefront" badgeColor="blue" highlights={otpHighlights}
"use client"

import { Input } from "@medusajs/ui"
import { useState, useRef, useEffect } from "react"
import { authenticateWithPhone, verifyOtp } from "../../../../lib/data/customer"
import ErrorMessage from "../../../checkout/components/error-message"

type Props = {
  phone: string
}

export const Otp = ({ phone }: Props) => {
  const [otp, setOtp] = useState<string>("")
  const [error, setError] = useState<string>("")
  const [isLoading, setIsLoading] = useState<boolean>(false)
  const [countdown, setCountdown] = useState<number>(60)
  const inputRefs = useRef<(HTMLInputElement | null)[]>([])

  const handleSubmit = async () => {
    setIsLoading(true)
    const response = await verifyOtp({
      otp,
      phone,
    })
    setOtp("")
    setIsLoading(false)

    if (typeof response === "string") {
      setError(response)
    }
  }

  const handleResend = async () => {
    authenticateWithPhone(phone)
    setCountdown(60)
  }

  const handlePaste = (e: React.ClipboardEvent) => {
    e.preventDefault()
    const pastedData = e.clipboardData.getData("text")
    const numericValue = pastedData.replace(/\D/g, "").slice(0, 6)
    
    if (numericValue) {
      setOtp(numericValue)
      // Focus the next empty input after pasted content
      const nextEmptyIndex = Math.min(numericValue.length, 5)
      inputRefs.current[nextEmptyIndex]?.focus()
    }
  }

  // TODO add use effects
}
```

You create an `Otp` component that accepts the phone number as a prop.

In the component, you define the following state variables:

- `otp`: The OTP entered by the customer.
- `error`: The error message to show if the OTP verification fails.
- `isLoading`: A boolean indicating whether the OTP verification is in progress.
- `countdown`: The countdown timer for resending the OTP.
- `inputRefs`: A ref to store the input elements for the OTP digits. You'll show six input elements for the OTP digits.

You also define the following functions:

- `handleSubmit`: This function is called when the customer submits the OTP. It calls the `verifyOtp` function to verify the OTP entered by the customer.
- `handleResend`: This function is called when the customer clicks the "Resend OTP" button that you'll add later. It calls the `authenticateWithPhone` function to resend the OTP to the customer's phone number.
- `handlePaste`: This function is called when the customer pastes the OTP in the input field. It improves the experience of pasting the OTP without having to enter it manually.

#### Handle Variable Changes

Next, you'll add `useEffect` hooks to handle changes in the state variables.

Replace the `TODO` with the following:

```tsx title="src/modules/account/components/otp/index.tsx" badgeLabel="Storefront" badgeColor="blue" highlights={useEffectHighlights}
useEffect(() => {
  if (inputRefs.current[0]) {
    inputRefs.current[0].focus()
  }
}, [inputRefs.current])

useEffect(() => {
  if (otp.length !== 6 || isLoading) {
    return
  }

  handleSubmit()
}, [otp, isLoading])

useEffect(() => {
  const timer = setInterval(() => {
    setCountdown((prev) => {
      return prev > 0 ? prev - 1 : 0
    })
  }, 1000)

  return () => clearInterval(timer)
}, [])

// TODO render form
```

You add three `useEffect` hooks:

1. The first one focuses the first input element when the component mounts.
2. The second one automatically submits the OTP when the customer enters six digits.
3. The third one adds an interval to update the countdown timer every second.

### f. Render OTP Form

Lastly, you'll render the OTP form with the input elements and the resend button.

Replace the `TODO` with the following:

```tsx title="src/modules/account/components/otp/index.tsx" badgeLabel="Storefront" badgeColor="blue"
return (
  <div
    className="max-w-sm flex flex-col items-center"
    data-testid="otp-page"
  >
    <h1 className="text-large-semi uppercase mb-6">
      Verify Phone Number
    </h1>
    <p className="text-center text-base-regular text-ui-fg-base mb-4">
      Enter the code sent to your phone number to login.
    </p>
    <div className="flex gap-2 mb-4">
      {[...Array(6)].map((_, index) => (
        <Input
          key={index}
          type="text"
          maxLength={1}
          pattern="\d*"
          inputMode="numeric"
          disabled={isLoading}
          className="w-10 h-10 text-center"
          ref={(el) => {
            inputRefs.current[index] = el
          }}
          onPaste={handlePaste}
          value={otp[index] || ""}
          onChange={(e) => {
            const elm = e.target
            const value = elm.value
            setOtp((prev) => {
              const newOtp = prev.split("")
              newOtp[index] = value
              return newOtp.join("")
            })
            if (value && /^\d+$/.test(value)) {
              // Move focus to next input
              const nextInput = elm.parentElement?.nextElementSibling?.querySelector("input")
              nextInput?.focus()
            }
          }}
          onKeyDown={(e) => {
            if (e.key === "Backspace" && !e.currentTarget.value) {
              // Move focus to previous input on backspace
              const prevInput = e.currentTarget.parentElement?.previousElementSibling?.querySelector("input")
              prevInput?.focus()
            }
          }}
        />
      ))}        
    </div>
    <div className="flex items-center gap-x-2 mb-4">
      <button
        className="text-small-regular text-ui-fg-interactive disabled:text-ui-fg-disabled disabled:cursor-not-allowed"
        onClick={handleResend}
        disabled={countdown > 0}
      >
        {countdown > 0 ? `Resend code in ${countdown}s` : "Resend Code"}
      </button>
    </div>
    <ErrorMessage error={error} />
  </div>
)
```

You show six input elements for the OTP digits. When a value is entered in an input, the focus moves to the next input. In addition, when the customer presses backspace on an empty input, the focus moves to the previous input.

You also show a resend button that allows the customer to resend the OTP once the countdown timer reaches zero.

You now have an OTP form that you can use in both the login and registration pages.

### g. Add Registration Form

You'll now add a registration form that allows the customer to register using their phone number.

First, in `src/modules/account/templates/login-template.tsx`, update the `LOGIN_VIEW` to the following:

```tsx title="src/modules/account/templates/login-template.tsx" badgeLabel="Storefront" badgeColor="blue" highlights={[["4"], ["5"]]}
export enum LOGIN_VIEW {
  SIGN_IN = "sign-in",
  REGISTER = "register",
  REGISTER_PHONE = "register-phone",
  SIGN_IN_PHONE = "sign-in-phone",
}
```

By default, the login template supports switching between the login and registration views for email and password authentication. With the above change, you add two new views: login and registration with phone number.

Then, to add the registration form, create the file `src/modules/account/components/register-phone/index.tsx` with the following content:

```tsx title="src/modules/account/components/register-phone/index.tsx" badgeLabel="Storefront" badgeColor="blue" highlights={registerPhoneHighlights}
"use client"

import { useState } from "react"
import Input from "@modules/common/components/input"
import { LOGIN_VIEW } from "@modules/account/templates/login-template"
import ErrorMessage from "@modules/checkout/components/error-message"
import LocalizedClientLink from "@modules/common/components/localized-client-link"
import { registerWithPhone } from "@lib/data/customer"
import "react-phone-number-input/style.css"
import PhoneInput from "react-phone-number-input"
import { Otp } from "../otp"
import { useParams } from "next/navigation"
import { Button } from "@medusajs/ui"

type Props = {
  setCurrentView: (view: LOGIN_VIEW) => void
}

const RegisterPhone = ({ setCurrentView }: Props) => {
  const [firstName, setFirstName] = useState("")
  const [lastName, setLastName] = useState("")
  const [phone, setPhone] = useState("")
  const [error, setError] = useState("")
  const [loading, setLoading] = useState(false)
  const [enterOtp, setEnterOtp] = useState(false)
  const { countryCode } = useParams() as { countryCode: string }

  const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
    e.preventDefault()
    setLoading(true)
    const response = await registerWithPhone({
      firstName,
      lastName,
      phone,
    })
    setLoading(false)
    if (typeof response === "string") {
      setError(response)
      return
    }

    setEnterOtp(true)
  }

  if (enterOtp) {
    return <Otp phone={phone} />
  }

  // TODO render form
}

export default RegisterPhone
```

You create a `RegisterPhone` component that accepts a `setCurrentView` prop to switch between the login and registration views.

In the component, you define the following state variables:

- `firstName`, `lastName`, and `phone` to store the form inputs' values.
- `error`: The error message to show if the registration fails.
- `loading`: A boolean indicating whether the registration is in progress.
- `enterOtp`: A boolean indicating whether to show the OTP form. This is enabled once the customer is registered and they need to authenticate using the OTP.
- `countryCode`: The country code of the customer, which is retrieved from the URL parameters. You'll use this to show the phone input with a default selected country.

You also define a `handleSubmit` function that handles the form submission. It calls the `registerWithPhone` function to register the customer using their phone number.

If the registration is successful, you set `enterOtp` to `true` to show the OTP form. Otherwise, you set the error message.

#### Render Registration Form

Next, you'll render the registration form with the input fields and the submit button.

Replace the `TODO` with the following:

```tsx title="src/modules/account/components/register-phone/index.tsx" badgeLabel="Storefront" badgeColor="blue"
return (
  <div
    className="max-w-sm flex flex-col items-center"
    data-testid="register-page"
  >
    <h1 className="text-large-semi uppercase mb-6">
      Become a Medusa Store Member
    </h1>
    <p className="text-center text-base-regular text-ui-fg-base mb-4">
      Create your Medusa Store Member profile, and get access to an enhanced
      shopping experience.
    </p>
    <form className="w-full flex flex-col" onSubmit={handleSubmit}>
      <div className="flex flex-col w-full gap-y-2">
        <Input
          label="First name"
          name="first_name"
          required
          autoComplete="given-name"
          data-testid="first-name-input"
          value={firstName}
          onChange={(e) => setFirstName(e.target.value)}
        />
        <Input
          label="Last name"
          name="last_name"
          required
          autoComplete="family-name"
          data-testid="last-name-input"
          value={lastName}
          onChange={(e) => setLastName(e.target.value)}
        />
        <PhoneInput
          placeholder="Enter phone number"
          value={phone}
          onChange={(value) => setPhone(value as string)}
          name="phone"
          required
          autoComplete="off"
          // @ts-ignore
          defaultCountry={countryCode.toUpperCase()}
        />
      </div>
      <ErrorMessage error={error} data-testid="register-error" />
      <span className="text-center text-ui-fg-base text-small-regular mt-6">
        By creating an account, you agree to Medusa Store&apos;s{" "}
        <LocalizedClientLink
          href="/content/privacy-policy"
          className="underline"
        >
          Privacy Policy
        </LocalizedClientLink>{" "}
        and{" "}
        <LocalizedClientLink
          href="/content/terms-of-use"
          className="underline"
        >
          Terms of Use
        </LocalizedClientLink>
        .
      </span>
      <Button 
        className="w-full mt-6" 
        type="submit"
        size="large"
        variant="primary"
        isLoading={loading}
      >
        Join
      </Button>
    </form>
    <span className="text-center text-ui-fg-base text-small-regular mt-6">
      Already a member?{" "}
      <button
        onClick={() => setCurrentView(LOGIN_VIEW.SIGN_IN_PHONE)}
        className="underline"
      >
        Sign in
      </button>
      .
    </span>
  </div>
)
```

You render the registration form with input fields for the first name, last name, and phone number.

For the phone number input, you use the `PhoneInput` component from the `react-phone-number-input` package. You set the `defaultCountry` prop to the country code retrieved from the URL parameters.

You also show a submit button that calls the `handleSubmit` function when clicked, and a button to switch to the login form.

#### Add to Login Template

Next, you'll add the `RegisterPhone` component to the login template.

In `src/modules/account/templates/login-template.tsx`, add the following import:

```tsx title="src/modules/account/templates/login-template.tsx" badgeLabel="Storefront" badgeColor="blue"
import RegisterPhone from "@modules/account/components/register-phone"
```

Then, change the `return` statement of the `LoginTemplate` component to the following:

```tsx title="src/modules/account/templates/login-template.tsx" badgeLabel="Storefront" badgeColor="blue"
return (
  <div className="w-full flex justify-start px-8 py-8">
    {currentView === "sign-in" ? (
      <Login setCurrentView={setCurrentView} />
    ) : currentView === "register" ? (
      <Register setCurrentView={setCurrentView} />
    ) : currentView === "register-phone" ? (
      <RegisterPhone setCurrentView={setCurrentView} />
    ) : (
      // TODO: Add login phone view
      <></>
    )}
  </div>
)
```

You show the registration form when the `currentView` is set to `register-phone`. You'll also add the login form later.

### h. Add Login Form

Next, you'll add a login form that allows the customer to log in using their phone number.

To create the form, create the file `src/modules/account/components/login-phone/index.tsx` with the following content:

```tsx title="src/modules/account/components/login-phone/index.tsx" badgeLabel="Storefront" badgeColor="blue" highlights={loginPhoneHighlights}
"use client"

import { authenticateWithPhone } from "@lib/data/customer"
import { LOGIN_VIEW } from "@modules/account/templates/login-template"
import ErrorMessage from "@modules/checkout/components/error-message"
import { useState } from "react"
import "react-phone-number-input/style.css"
import PhoneInput from "react-phone-number-input"
import { Otp } from "../otp"
import { useParams } from "next/navigation"
import { Button } from "@medusajs/ui"

type Props = {
  setCurrentView: (view: LOGIN_VIEW) => void
}

const LoginPhone = ({ setCurrentView }: Props) => {
  const [phone, setPhone] = useState("")
  const [error, setError] = useState("")
  const [loading, setLoading] = useState(false)
  const [enterOtp, setEnterOtp] = useState(false)
  const { countryCode } = useParams() as { countryCode: string }

  const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
    e.preventDefault()
    setLoading(true)
    const response = await authenticateWithPhone(phone)
    setLoading(false)
    if (typeof response === "string") {
      setError(response)
      return
    }

    setEnterOtp(true)
  }

  if (enterOtp) {
    return <Otp phone={phone} />
  }

  // TODO render form
}

export default LoginPhone
```

You create a `LoginPhone` component that accepts a `setCurrentView` prop to switch between the login and registration views.

In the component, you define the following state variables:

- `phone`: The phone number entered by the customer.
- `error`: The error message to show if the login fails.
- `loading`: A boolean indicating whether the login is in progress.
- `enterOtp`: A boolean indicating whether to show the OTP form. This is enabled after the form is submitted.
- `countryCode`: The country code of the customer, which is retrieved from the URL parameters. You'll use this to show the phone input with a default selected country.

You also define a `handleSubmit` function that handles the form submission. It calls the `authenticateWithPhone` function to authenticate the customer using their phone number. Then, it enables `enterOtp` to show the OTP form.

#### Render Login Form

Next, you'll render the login form with the input field and the submit button.

Replace the `TODO` with the following:

```tsx title="src/modules/account/components/login-phone/index.tsx" badgeLabel="Storefront" badgeColor="blue"
return (
  <div
    className="max-w-sm w-full flex flex-col items-center"
    data-testid="login-page"
  >
    <h1 className="text-large-semi uppercase mb-6">Welcome back</h1>
    <p className="text-center text-base-regular text-ui-fg-base mb-8">
      Sign in to access an enhanced shopping experience.
    </p>
    <form className="w-full" onSubmit={handleSubmit}>
      <div className="flex flex-col w-full gap-y-2">
        <PhoneInput
          placeholder="Enter phone number"
          value={phone}
          onChange={(value) => setPhone(value as string)}
          name="phone"
          required
          // @ts-ignore
          defaultCountry={countryCode.toUpperCase()}
        />
      </div>
      {error && <ErrorMessage error={error} data-testid="login-error-message" />}
      <Button 
        className="w-full mt-6" 
        disabled={loading}
        type="submit"
        size="large"
        variant="primary"
        isLoading={loading}
      >
        Sign in
      </Button>
    </form>
    <span className="text-center text-ui-fg-base text-small-regular mt-6">
      Not a member?{" "}
      <button
        onClick={() => setCurrentView(LOGIN_VIEW.REGISTER_PHONE)}
        className="underline"
        data-testid="register-button"
      >
        Join us
      </button>
      .
    </span>
  </div>
)
```

You render the login form with an input field for the phone number. You also show a submit button that calls the `handleSubmit` function when clicked.

### i. Add to Login Template

Next, you'll add the `LoginPhone` component to the login template.

In `src/modules/account/templates/login-template.tsx`, add the following import:

```tsx title="src/modules/account/templates/login-template.tsx" badgeLabel="Storefront" badgeColor="blue"
import LoginPhone from "../components/login-phone"
```

Next, in the `LoginTemplate` component, change the default value of the `currentView` state to `LOGIN_VIEW.SIGN_IN_PHONE`:

```tsx title="src/modules/account/templates/login-template.tsx" badgeLabel="Storefront" badgeColor="blue"
const [currentView, setCurrentView] = useState(LOGIN_VIEW.SIGN_IN_PHONE)
```

This ensures the phone login form is shown by default.

Finally, replace the `return` statement of the `LoginTemplate` component to the following:

```tsx title="src/modules/account/templates/login-template.tsx" badgeLabel="Storefront" badgeColor="blue"
return (
  <div className="w-full flex justify-start px-8 py-8">
    {currentView === "sign-in" ? (
      <Login setCurrentView={setCurrentView} />
    ) : currentView === "register" ? (
      <Register setCurrentView={setCurrentView} />
    ) : currentView === "register-phone" ? (
      <RegisterPhone setCurrentView={setCurrentView} />
    ) : (
      <LoginPhone setCurrentView={setCurrentView} />
    )}
  </div>
)
```

You show the login form when the `currentView` is set to `sign-in-phone`.

### Test it Out

You can now test out the phone authentication feature in the Next.js Starter Storefront.

First, start the Medusa application by running the following command in the Medusa project's directory:

```bash npm2yarn badgeLabel="Medusa Application" badgeColor="green"
npm run dev
```

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

```bash npm2yarn badgeLabel="Storefront" badgeColor="blue"
npm run dev
```

Open your browser, navigate to `http://localhost:8000`, and click on the "Account" link at the top right. This will show the login form with just the phone number input.

![Login form with phone number input](https://res.cloudinary.com/dza7lstvk/image/upload/v1747739945/Medusa%20Resources/CleanShot_2025-05-20_at_14.18.36_2x_ubuika.png)

You can also switch to the registration form by clicking on the "Join" link below the login form.

![Registration form with phone number input](https://res.cloudinary.com/dza7lstvk/image/upload/v1747739988/Medusa%20Resources/CleanShot_2025-05-20_at_14.19.35_2x_btlc2s.png)

You can try to login with the account you created before, or register with a new one. Once successful, you'll see the OTP form to enter the OTP you received as SMS.

![OTP form](https://res.cloudinary.com/dza7lstvk/image/upload/v1747740102/Medusa%20Resources/CleanShot_2025-05-20_at_14.21.18_2x_yrkuyg.png)

After you enter the six digits, you'll be logged in and you'll see your profile page.

![Profile page](https://res.cloudinary.com/dza7lstvk/image/upload/v1747740166/Medusa%20Resources/CleanShot_2025-05-20_at_14.22.30_2x_kshb83.png)

***

## Step 5: Disallow Phone Updates

The phone authentication feature is now complete, but there are two improvements you can make:

1. Show the phone number in the profile page: Currently, it shows the email address, which is a fake address you've set.
2. Disable phone and email updates: Currently, the customer can update their phone number, which is not allowed for phone authentication.

### a. Show Phone Number in Profile Page

To show the phone number in the profile page instead of the email, in `src/modules/account/components/overview/index.tsx`, find the following in the `return` statement:

```tsx title="src/modules/account/components/overview/index.tsx" badgeLabel="Storefront" badgeColor="blue"
<span
  className="font-semibold"
  data-testid="customer-email"
  data-value={customer?.email}
>
  {customer?.email}
</span>
```

And replace it with the following:

```tsx title="src/modules/account/components/overview/index.tsx" badgeLabel="Storefront" badgeColor="blue"
<span
  className="font-semibold"
  data-testid="customer-phone"
  data-value={customer?.phone}
>
  {customer?.phone}
</span>
```

If you check the profile page now, you'll see the phone number instead of the email address at the top right.

![Phone number showing in profile page](https://res.cloudinary.com/dza7lstvk/image/upload/v1747740378/Medusa%20Resources/CleanShot_2025-05-20_at_14.25.59_2x_x0pxvt.png)

### b. Remove Email and Phone Fields

Next, you'll remove the fields to update email and phone number from the profile page.

In `src/app/[countryCode]/(main)/account/@dashboard/profile/page.tsx`, find the following lines to remove from the `return` statement:

```tsx title="src/app/[countryCode]/(main)/account/@dashboard/profile/page.tsx" badgeLabel="Storefront" badgeColor="blue"
<div className="flex flex-col gap-y-8 w-full">
  {/* ... */}
  {/* Remove the following */}
  <Divider />
  <ProfileEmail customer={customer} />
  <Divider />
  <ProfilePhone customer={customer} />
  {/* ... */}
</div>
```

If you go to your profile page and click on "Profile" in the sidebar, the email and phone number sections will be removed.

![Profile page without email and phone fields](https://res.cloudinary.com/dza7lstvk/image/upload/v1747740515/Medusa%20Resources/CleanShot_2025-05-20_at_14.28.09_2x_ugab7d.png)

### c. Disable Phone Updates in Medusa

While removing the email and phone fields from the profile page prevents customers using the storefront from updating their phone number, it doesn't prevent them from updating it using Medusa's API.

In this section, you'll add a [middleware](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/middlewares/index.html.md) to the `/store/customers/me` API route that prevents customers from updating their phone number.

A middleware is a function that's executed whenever a request is sent to an API route. It's executed before the route handler, allowing you to validate requests, apply authentication guards, and more.

Learn more in the [Middlewares](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/middlewares/index.html.md) documentation.

To add a middleware in your Medusa application, create the file `src/api/middlewares.ts` with the following content:

```ts title="src/api/middlewares.ts" badgeLabel="Medusa Application" badgeColor="green"
import { defineMiddlewares } from "@medusajs/framework/http"

export default defineMiddlewares({
  routes: [
    {
      matcher: "/store/customers/me",
      method: ["POST"],
      middlewares: [
        async (req, res, next) => {
          const { phone } = req.body as Record<string, string>

          if (phone) {
            return res.status(400).json({
              error: "Phone number is not allowed to be updated",
            })
          }

          next()
        },
      ],
    },
  ],
})
```

You define middlewares using the `defineMiddlewares` function. It accepts an object having a `routes` property that holds all middlewares applied to API routes.

Each object in `routes` has the following properties:

- `matcher`: The API route path to apply the middleware on. You set it to `/store/customers/me`.
- `method`: The HTTP method to apply the middleware to. You set it to `POST` so that the middleware is applied only on `POST` requests sent to the `/store/customers/me` route.
- `middlewares`: An array of middlewares to apply on the route. You add a middleware that throws an error if the request body contains a `phone` property. This prevents customers from updating their phone number using the API.

Any `POST` request to the `/store/customers/me` route will now be validated to ensure it's not updating the phone number.

***

## Next Steps

You've now implemented phone authentication in Medusa with Twilio integration. You can further customize the phone authentication feature based on your business use case.

### Authenticate Other Actor Types

This tutorial focused on authenticating customers using their phone number. However, you can also authenticate other actor types, such as admin users and vendors.

To do that, first, enable the `phone-auth` authentication strategy in `medusa-config.ts` for the actor types. For example:

```ts title="medusa-config.ts"
module.exports = defineConfig({
  projectConfig: {
    // ...
    http: {
      // ...
      authMethodsPerActor: {
        user: ["emailpass", "phone-auth"],
        customer: ["emailpass", "phone-auth"],
        vendor: ["emailpass", "phone-auth"],
      },
    },
  },
})
```

Then, when sending requests to the authentication API routes mentioned in the [Test Out Phone Authentication](#test-out-phone-authentication) section, replace `customer` in the API route paths with the actor type you want to authenticate:

- `/auth/customer/phone-auth/register` -> `/auth/user/phone-auth/register`
- `/auth/customer/phone-auth` -> `/auth/user/phone-auth`
- `/auth/customer/phone-auth/callback` -> `/auth/user/phone-auth/callback`

Finally, update the UI to show the phone authentication option for the actor type you want to authenticate. This depends on the UI you're using, but you can follow an approach similar to the Next.js Starter Storefront customizations.

The login form of Medusa Admin can't be customized, so you'll have to build a custom admin dashboard to support phone authentication for admin users.

### Learn More about Medusa

If you're new to Medusa, check out the [main documentation](https://docs.medusajs.com/docs/learn/index.html.md), 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](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/index.html.md).

### Troubleshooting

If you encounter issues during your development, check out the [troubleshooting guides](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/troubleshooting/index.html.md).

### Getting Help

If you encounter issues not covered in the troubleshooting guides:

1. Visit the [Medusa GitHub repository](https://github.com/medusajs/medusa) to report issues or ask questions.
2. Join the [Medusa Discord community](https://discord.gg/medusajs) for real-time support from community members.
3. Contact the [sales team](https://medusajs.com/contact/) to get help from the Medusa team.


# Implement Product Reviews in Medusa

In this tutorial, you'll learn how to implement product reviews in Medusa.

When you install a Medusa application, you get a fully-fledged commerce platform with a Framework for customization. The Medusa application's commerce features are built around [Commerce Modules](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/index.html.md) which are available out-of-the-box. The features include product-management features.

Medusa doesn't provide product reviews out-of-the-box, but the Medusa Framework facilitates implementing customizations like product reviews. In this tutorial, you'll learn how to customize the Medusa server, Admin dashboard, and Next.js Starter Storefront to implement product reviews.

You can follow this guide whether you're new to Medusa or an advanced Medusa developer.

## Summary

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

- Install and set up Medusa.
- Define product reviews models and implement their management features in the Medusa server.
- Customize the Medusa Admin to allow merchants to view and manage product reviews.
- Customize the Next.js Starter Storefront to display product reviews and allow customers to submit reviews.

![Diagram showcasing the product review features in the storefront and admin](https://res.cloudinary.com/dza7lstvk/image/upload/v1741941058/Medusa%20Resources/reviews-overview_nufybf.jpg)

- [Product Reviews Repository](https://github.com/medusajs/examples/tree/main/product-reviews): Find the full code for this guide in this repository.
- [OpenApi Specs for Postman](https://res.cloudinary.com/dza7lstvk/raw/upload/v1741941475/OpenApi/product-reviews_jh8ohj.yaml): Import this OpenApi Specs file into tools like Postman.

***

## Step 1: Install a Medusa Application

### Prerequisites

- [Node.js v20+](https://nodejs.org/en/download)
- [Git CLI tool](https://git-scm.com/downloads)
- [PostgreSQL](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 asked whether you want to install the [Next.js Starter Storefront](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/nextjs-starter/index.html.md), choose Yes.

Afterwards, the installation process will start, which will install the Medusa application in a directory with your project's name, and the Next.js Starter Storefront in a separate directory with the `{project-name}-storefront` name.

The Medusa application is composed of a headless Node.js server and an admin dashboard. The storefront is installed or custom-built separately and connects to the Medusa application through its REST endpoints, called [API routes](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/index.html.md). Learn more in [Medusa's Architecture documentation](https://docs.medusajs.com/docs/learn/introduction/architecture/index.html.md).

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.

Check out the [troubleshooting guides](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/troubleshooting/create-medusa-app-errors/index.html.md) for help.

***

## Step 2: Add Product Review Module

In Medusa, you can build custom features in a [module](https://docs.medusajs.com/docs/learn/fundamentals/modules/index.html.md). A module is a reusable 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.

In the module, you define the data models necessary for a feature and the logic to manage these data models. Later, you can build commerce flows around your module.

In this step, you'll build a Product Review Module that defines the necessary data models to store and manage product reviews.

Refer to the [Modules documentation](https://docs.medusajs.com/docs/learn/fundamentals/modules/index.html.md) to learn more.

### Create Module Directory

A module is created under the `src/modules` directory of your Medusa application. So, create the directory `src/modules/product-review`.

### Create Data Models

A data model represents a table in the database. You create data models using Medusa's Data Model Language (DML). It simplifies defining a table's columns, relations, and indexes with straightforward methods and configurations.

Refer to the [Data Models documentation](https://docs.medusajs.com/docs/learn/fundamentals/modules#1-create-data-model/index.html.md) to learn more.

For the Product Review Module, you need to define a `Review` data model that represents a product review. So, create the file `src/modules/product-review/models/review.ts` with the following content:

```ts title="src/modules/product-review/models/review.ts"
import { model } from "@medusajs/framework/utils"

const Review = model.define("review", {
  id: model.id().primaryKey(),
  title: model.text().nullable(),
  content: model.text(),
  rating: model.float(),
  first_name: model.text(),
  last_name: model.text(),
  status: model.enum(["pending", "approved", "rejected"]).default("pending"),
  product_id: model.text().index("IDX_REVIEW_PRODUCT_ID"),
  customer_id: model.text().nullable(),
})
.checks([
  {
    name: "rating_range", 
    expression: (columns) => `${columns.rating} >= 1 AND ${columns.rating} <= 5`,
  },
])

export default Review
```

You define the `Review` data model using the `model.define` method of the DML. It accepts the data model's table name as a first parameter, and the model's schema object as a second parameter.

The `Review` data model has the following properties:

- `id`: A unique ID for the review.
- `title`: The review's title.
- `content`: The review's content.
- `rating`: The review's rating. You also add a [check constraint](https://docs.medusajs.com/docs/learn/fundamentals/data-models/check-constraints/index.html.md) to ensure the rating is between 1 and 5.
- `first_name`: The first name of the reviewer.
- `last_name`: The last name of the reviewer.
- `status`: The review's status, which can be `pending`, `approved`, or `rejected`.
- `product_id`: The ID of the product the review is for.
- `customer_id`: The ID of the customer who submitted the review.

Learn more about defining data model properties in the [Property Types documentation](https://docs.medusajs.com/docs/learn/fundamentals/data-models/properties/index.html.md).

### Create Module's Service

You now have the necessary data model in the Review Module, but you'll need to manage its records. You do this by creating a service in the module.

A service is a TypeScript or JavaScript class that the module exports. In the service's methods, you can connect to the database, allowing you to manage your data models, or connect to a third-party service, which is useful if you're integrating with external services.

Refer to the [Module Service documentation](https://docs.medusajs.com/docs/learn/fundamentals/modules#2-create-service/index.html.md) to learn more.

To create the Review Module's service, create the file `src/modules/product-review/service.ts` with the following content:

```ts title="src/modules/product-review/service.ts"
import { MedusaService } from "@medusajs/framework/utils"
import Review from "./models/review"

class ProductReviewModuleService extends MedusaService({
  Review,
}) {
}

export default ProductReviewModuleService
```

The `ProductReviewModuleService` extends `MedusaService` from the Modules SDK which generates a class with data-management methods for your module's data models. This saves you time on implementing Create, Read, Update, and Delete (CRUD) methods.

So, the `ProductReviewModuleService` class now has methods like `createReviews` and `retrieveReview`.

Find all methods generated by the `MedusaService` in [the Service Factory reference](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/service-factory-reference/index.html.md).

You'll use this service later when you implement custom flows for product reviews.

### Export Module Definition

The final piece to a module is its definition, which you export in an `index.ts` file at its root directory. This definition tells Medusa the name of the module and its service.

So, create the file `src/modules/product-review/index.ts` with the following content:

```ts title="src/modules/product-review/index.ts"
import { Module } from "@medusajs/framework/utils"
import ProductReviewModuleService from "./service"

export const PRODUCT_REVIEW_MODULE = "productReview"

export default Module(PRODUCT_REVIEW_MODULE, {
  service: ProductReviewModuleService,
})
```

You use the `Module` function from the Modules SDK to create the module's definition. It accepts two parameters:

1. The module's name, which is `productReview`.
2. An object with a required property `service` indicating the module's service.

You also export the module's name as `PRODUCT_REVIEW_MODULE` so you can reference it later.

### Add Module 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 and pass an array with your custom module:

```ts title="medusa-config.ts"
module.exports = defineConfig({
  // ...
  modules: [
    {
      resolve: "./src/modules/product-review",
    },
  ],
})
```

Each object in the `modules` array has a `resolve` property, whose value is either a path to the module's directory, or an `npm` package’s name.

### Generate Migrations

Since data models represent tables in the database, you define how they're created in the database with migrations. A migration is a TypeScript or JavaScript file that defines database changes made by a module.

Refer to the [Migrations documentation](https://docs.medusajs.com/docs/learn/fundamentals/modules#5-generate-migrations/index.html.md) to learn more.

Medusa's CLI tool can generate the migrations for you. To generate a migration for the Review Module, run the following command in your Medusa application's directory:

```bash
npx medusa db:generate productReview
```

The `db:generate` command of the Medusa CLI accepts the name of the module to generate the migration for. You'll now have a `migrations` directory under `src/modules/product-review` that holds the generated migration.

Then, to reflect these migrations on the database, run the following command:

```bash
npx medusa db:migrate
```

The table for the `Review` data model is now created in the database.

***

## Step 3: Define Review \<> Product Link

When you defined the `Review` data model, you added properties that store the ID of records managed by other modules. For example, the `product_id` property stores the ID of the product this review is for, but products are managed by the [Product Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/product/index.html.md).

Medusa integrates modules into your application without implications or side effects by isolating modules from one another. This means you can't directly create relationships between data models in your module and data models in other modules.

Instead, Medusa provides the mechanism to define links between data models, and retrieve and manage linked records while maintaining module isolation. Links are useful to define associations between data models in different modules, or extend a model in another module to associate custom properties with it.

Refer to the [Module Isolation documentation](https://docs.medusajs.com/docs/learn/fundamentals/modules/isolation/index.html.md) to learn more.

In this step, you'll define a link between the Product Review Module's `Review` data model, and the Product Module's `Product` data model. You'll then use this link to retrieve the product associated with a review.

You can also define a link between the `Review` data model and the `Customer` data model to retrieve the customer who submitted the review in a similar manner.

You can define links between data models in a TypeScript or JavaScript file under the `src/links` directory. So, create the file `src/links/review-product.ts` with the following content:

```ts title="src/links/review-product.ts"
import { defineLink } from "@medusajs/framework/utils"
import ProductReviewModule from "../modules/product-review"
import ProductModule from "@medusajs/medusa/product"

export default defineLink(
  {
    linkable: ProductReviewModule.linkable.review,
    field: "product_id",
    isList: false,
  },
  ProductModule.linkable.product,
  {
    readOnly: true,
  }
)
```

You define a link using the `defineLink` function from the Modules SDK. It accepts three parameters:

1. An object indicating the first data model part of the link. A module has a special `linkable` property that contains link configurations for its data models. So, you can pass the link configurations for the `Review` data model from the Product Review module, specifying that its `product_id` property holds the ID of the linked record. You also specify `isList` as `false` since a review can only have one product.
2. An object indicating the second data model part of the link. You pass the linkable configurations of the Product Module's `Product` data model.
3. An optional object with additional configurations for the link. By default, Medusa creates a table in the database to represent the link you define. However, when you only want to retrieve the linked records without managing and storing the links, you can set the `readOnly` option to `true`.

You can now retrieve the product of a review, as you'll see in later steps.

***

## Step 4: Create Review Workflow

You're now ready to start implementing product-review features. The first one you'll implement is the ability for customers to create a product review.

To build custom commerce features in Medusa, you create a [workflow](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md). 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 an endpoint.

So, in this section, you'll learn how to create a workflow that creates a review. Later, you'll execute this workflow in an API route.

Learn more about workflows in the [Workflows documentation](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md).

The workflow will have the following steps:

- [useQueryGraphStep](https://docs.medusajs.com/references/helper-steps/useQueryGraphStep/index.html.md): Retrieve the product to confirm it exists.
- [createReviewStep](#createReviewStep): Create the review.

The `useQueryGraphStep` step is provided by Medusa in its `@medusajs/medusa/core-flows` package. So, you only need to implement the `createReviewStep` step.

### createReviewStep

In the second step of the workflow, you create the review. To create a step, create the file `src/workflows/steps/create-review.ts` with the following content:

```ts title="src/workflows/steps/create-review.ts" highlights={createReviewHighlights}
import {
  createStep,
  StepResponse,
} from "@medusajs/framework/workflows-sdk"
import { PRODUCT_REVIEW_MODULE } from "../../modules/product-review"
import ProductReviewModuleService from "../../modules/product-review/service"

export type CreateReviewStepInput = {
  title?: string
  content: string
  rating: number
  product_id: string
  customer_id?: string
  first_name: string
  last_name: string
  status?: "pending" | "approved" | "rejected"
}

export const createReviewStep = createStep(
  "create-review",
  async (input: CreateReviewStepInput, { container }) => {
    const reviewModuleService: ProductReviewModuleService = container.resolve(
      PRODUCT_REVIEW_MODULE
    )

    const review = await reviewModuleService.createReviews(input)

    return new StepResponse(review, review.id)
  },
  async (reviewId, { container }) => {
    if (!reviewId) {
      return
    }

    const reviewModuleService: ProductReviewModuleService = container.resolve(
      PRODUCT_REVIEW_MODULE
    )

    await reviewModuleService.deleteReviews(reviewId)
  }
)
```

You create a step with `createStep` from the Workflows SDK. It accepts two parameters:

1. The step's unique name, which is `create-review`.
2. An async function that receives two parameters:
   - The step's input, which is in this case an object with the review's properties.
   - An object that has properties including the [Medusa container](https://docs.medusajs.com/docs/learn/fundamentals/medusa-container/index.html.md), which is a registry of Framework and commerce tools that you can access in the step.

In the step function, you resolve the Review Module's service from the Medusa container using its `resolve` method, passing it the module's name as a parameter.

Then, you create the review using the `createReviews` method. As you remember, the Review Module's service extends the `MedusaService` which generates data-management methods for you.

A step function must return a `StepResponse` instance. The `StepResponse` constructor accepts two parameters:

1. The step's output, which is the review created.
2. Data to pass to the step's compensation function.

#### Compensation Function

The compensation function undoes the actions performed in a step. Then, if an error occurs during the workflow's execution, the compensation functions of executed steps are called to roll back the changes. This mechanism ensures data consistency in your application, especially as you integrate external systems.

The compensation function accepts two parameters:

1. The data passed from the step in the second parameter of `StepResponse`, which in this case is the ID of the created review.
2. An object that has properties including the [Medusa container](https://docs.medusajs.com/docs/learn/fundamentals/medusa-container/index.html.md).

In the compensation function, you resolve the Review Module's service from the Medusa container and call the `deleteReviews` method to delete the review created in the step.

### Add createReviewWorkflow

You can now create the workflow using the step provided by Medusa and your custom step.

To create the workflow, create the file `src/workflows/create-review.ts` with the following content:

```ts title="src/workflows/create-review.ts"
import { 
  createWorkflow,
  WorkflowResponse,
} from "@medusajs/framework/workflows-sdk"
import { createReviewStep } from "./steps/create-review"
import { useQueryGraphStep } from "@medusajs/medusa/core-flows"

type CreateReviewInput = {
  title?: string
  content: string
  rating: number
  product_id: string
  customer_id?: string
  first_name: string
  last_name: string
  status?: "pending" | "approved" | "rejected"
}

export const createReviewWorkflow = createWorkflow(
  "create-review",
  (input: CreateReviewInput) => {
    // Check product exists
    // @ts-ignore
    useQueryGraphStep({
      entity: "product",
      fields: ["id"],
      filters: {
        id: input.product_id,
      },
      options: {
        throwIfKeyNotFound: true,
      },
    })

    // Create the review
    const review = createReviewStep(input)

    // @ts-ignore
    return new WorkflowResponse({
      review,
    })
  }
)
```

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 of the review's details.

In the workflow's constructor function, you:

- use `useQueryGraphStep` to retrieve the product. By setting the `options.throwIfKeyNotFound` to `true`, the step throws an error if the product doesn't exist.
- Call the `createReviewStep` step to create the review.

`useQueryGraphStep` uses [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), which allows you to retrieve data across modules. For example, in the above snippet you're retrieving the product, which is managed in the [Product Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/product/index.html.md), by passing `id` to the `fields` array.

A workflow must return an instance of `WorkflowResponse`. The `WorkflowResponse` constructor accepts the workflow's output as a parameter, which is an object holding the created review in this case.

In the next step, you'll learn how to execute this workflow in an API route.

***

## Step 5: Create Review API Route

Now that you have the logic to create a product review, you need to expose it so that frontend clients, such as a storefront, can use it. You do this by creating an [API route](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/index.html.md).

An API Route is an endpoint that exposes commerce features to external applications and clients, such as storefronts. You'll create an API route at the path `/store/reviews` that executes the workflow from the previous step.

Learn more about API routes in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/index.html.md).

### Implement API Route

An API route is created in a `route.ts` file under a sub-directory of the `src/api` directory. The path of the API route is the file's path relative to `src/api`.

So, to create an API route at the path `/store/reviews`, create the file `src/api/store/reviews/route.ts` with the following content:

```ts title="src/api/store/reviews/route.ts" highlights={PostStoreReviewHighlights}
import type {
  AuthenticatedMedusaRequest,
  MedusaResponse,
} from "@medusajs/framework/http"
import { createReviewWorkflow } from "../../../workflows/create-review"

import { z } from "zod"

export const PostStoreReviewSchema = z.object({
  title: z.string().optional(),
  content: z.string(),
  rating: z.preprocess(
    (val) => {
      if (val && typeof val === "string") {
        return parseInt(val)
      }
      return val
    },
    z.number().min(1).max(5)
  ),
  product_id: z.string(),
  first_name: z.string(),
  last_name: z.string(),
})

type PostStoreReviewReq = z.infer<typeof PostStoreReviewSchema>

export const POST = async (
  req: AuthenticatedMedusaRequest<PostStoreReviewReq>,
  res: MedusaResponse
) => {
  const input = req.validatedBody

  const { result } = await createReviewWorkflow(req.scope)
    .run({
      input: {
        ...input,
        customer_id: req.auth_context?.actor_id,
      },
    })

  res.json(result)
}
```

You first define a [Zod](https://zod.dev/) schema for the request body of the API route. You'll later use this schema to enforce validation on the API route.

Then, since you export a `POST` function, you're exposing a `POST` API route at the path `/store/reviews`. The route handler function accepts two parameters:

1. A request object with details and context on the request, such as body parameters or authenticated customer details.
2. A response object to manipulate and send the response.

`AuthenticatedMedusaRequest` accepts the request body's type as a type argument.

In the route handler, you execute the `createReviewWorkflow` workflow by invoking it, passing it the Medusa container (which is stored in the `scope` property of a request object). Then, you call its `run` method, passing to the workflow the request body as input.

### Apply Validation and Authentication Middlewares

Now that you have the API route, you need to enforce validation of the request body, and require authentication to access the route. You can do this with a middleware. A middleware is a function executed when a request is sent to an API Route. It's executed before the route handler.

Learn more about middleware in the [Middlewares documentation](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/middlewares/index.html.md).

Middlewares are created in the `src/api/middlewares.ts` file. So create the file `src/api/middlewares.ts` with the following content:

```ts title="src/api/middlewares.ts"
import { 
  defineMiddlewares,
  authenticate,
  validateAndTransformBody,
} from "@medusajs/framework/http"
import { PostStoreReviewSchema } from "./store/reviews/route"


export default defineMiddlewares({
  routes: [
    {
      method: ["POST"], 
      matcher: "/store/reviews",
      middlewares: [
        authenticate("customer", ["session", "bearer"]),
        validateAndTransformBody(PostStoreReviewSchema),
      ],
    },
  ],
})
```

To export the middlewares, you use the `defineMiddlewares` function. It accepts an object having a `routes` property, whose value is an array of middleware route objects. Each middleware route object has the following properties:

- `method`: The HTTP methods the middleware applies to, which is in this case `POST`.
- `matcher`: The path of the route the middleware applies to.
- `middlewares`: An array of middleware functions to apply to the route. In this case, you apply two middlewares:
  - `authenticate`: ensures the request is authenticated as a customer with a session or bearer token.
  - `validateAndTransformBody`: validates that the request body parameters match the Zod schema passed as a parameter.

The create product review route is now ready for use.

### Test the API Route

To test out the API route, start the Medusa application:

```bash npm2yarn
npm run dev
```

Then, open the Medusa Admin dashboard at `http://localhost:9000/app` and login using the credentials you set up earlier.

#### Retrieve Publishable API Key

All requests sent to routes starting with `/store` must have a publishable API key in their header. This ensures that the request is scoped to a specific sales channel of your storefront.

To learn more about publishable API keys, refer to the [Publishable API Key documentation](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/sales-channel/publishable-api-keys/index.html.md).

To retrieve the publishable API key from the Medusa Admin, refer to [this user guide](https://docs.medusajs.com/user-guide/settings/developer/publishable-api-keys/index.html.md).

#### Retrieve Customer Authentication Token

As mentioned before, the API route you added requires the customer to be authenticated. So, you'll first create a customer, then retrieve their authentication token to use in the request.

Before creating the customer, retrieve a registration token using the [Retrieve Registration JWT Token API route](https://docs.medusajs.com/api/store#auth_postactor_typeauth_provider_register):

```bash
curl -X POST 'http://localhost:9000/auth/customer/emailpass/register' \
-H 'Content-Type: application/json' \
--data-raw '{
  "email": "customer@gmail.com",
  "password": "supersecret"
}'
```

Make sure to replace the email and password with the credentials you want.

Then, register the customer using the [Create Customer API route](https://docs.medusajs.com/api/store#customers_postcustomers):

```bash
curl -X POST 'http://localhost:9000/store/customers' \
-H 'Authorization: Bearer {token}' \
-H 'Content-Type: application/json' \
-H 'x-publishable-api-key: {your_publishable_api_key}' \
--data-raw '{
  "email": "customer@gmail.com"
}'
```

Make sure to replace:

- `{token}` with the registration token you received from the previous request.
- `{your_publishable_api_key}` with the publishable API key you retrieved from the Medusa Admin.

Also, if you changed the email in the first request, make sure to change it here as well.

The customer is now registered. Lastly, you need to retrieve its authenticated token by sending a request to the [Authenticate Customer API route](https://docs.medusajs.com/api/store#auth_postactor_typeauth_provider):

```bash
curl -X POST 'http://localhost:9000/auth/customer/emailpass' \
-H 'Content-Type: application/json' \
--data-raw '{
  "email": "customer@gmail.com",
  "password": "supersecret"
}'
```

Copy the returned token to use it in the next requests.

#### Retrieve Product ID

Before creating a review, you need the ID of a product. You can either copy one from the Medusa Admin, or send the following request:

```bash
curl 'http://localhost:9000/store/products' \
-H 'x-publishable-api-key: {your_publishable_api_key}'
```

Make sure to replace `{your_publishable_api_key}` with the publishable API key you retrieved from the Medusa Admin.

#### Create a Review

You can now create a review for the product you chose. To do that, send the following request:

```bash
curl --location 'http://localhost:9000/store/reviews' \
--header 'x-publishable-api-key: {your_publishable_api_key}' \
--header 'Content-Type: application/json' \
-H 'Authorization: Bearer {token}' \
--data '{
    "product_id": "{product_id}",
    "title": "Really good",
    "content": "The material is nice",
    "rating": 5,
    "first_name": "John",
    "last_name": "Smith"
}'
```

Make sure to replace:

- `{your_publishable_api_key}` with the publishable API key you retrieved from the Medusa Admin.
- `{token}` with the authentication token you retrieved from the previous request.
- `{product_id}` with the ID of the product you chose.

If the request is successful, you'll receive a response with the created review. Notice that the review is in the `pending` status. In the upcoming steps, you'll allow admin users to approve or reject reviews.

***

## Step 6: List Reviews Admin API Route

In this step, you'll create an API route that lists the reviews of a product. You'll use this route in the Medusa Admin customizations to allow admin users to view and manage product reviews.

### Create API Route

To create the API route that retrieves a paginated list of reviews, create the file `src/api/admin/reviews/route.ts` with the following content:

```ts title="src/api/admin/reviews/route.ts" highlights={GetAdminReviewsHighlights}
import {
  MedusaRequest,
  MedusaResponse,
} from "@medusajs/framework/http"
import { createFindParams } from "@medusajs/medusa/api/utils/validators"

export const GetAdminReviewsSchema = createFindParams()

export const GET = async (
  req: MedusaRequest,
  res: MedusaResponse
) => {
  const query = req.scope.resolve("query")
  
  const { 
    data: reviews, 
    metadata: { count, take, skip } = {
      count: 0,
      take: 20,
      skip: 0,
    },
  } = await query.graph({
    entity: "review",
    ...req.queryConfig,
  })

  res.json({ 
    reviews,
    count,
    limit: take,
    offset: skip,
  })
}
```

You first define a `GetAdminReviewsSchema` schema that will allow clients to pass the following query parameters:

- `limit`: The number of reviews to retrieve.
- `offset`: The number of items to skip before retrieving the reviews.
- `order`: The fields to sort the reviews by in ascending or descending order.

Then, you export a `GET` function, which exposes a `GET` API Route at the path `/admin/reviews`. In the route handler you resolve [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md) from the Medusa container, which allows you to retrieve data across modules.

Next, you retrieve all reviews using Query. Notice that you pass in `query.graph` the `req.queryConfig` object. This object holds the fields to retrieve and the pagination configurations.

Finally, you return the reviews with pagination fields.

### Apply Query Configurations Middleware

After adding the API route, you need to add a middleware that validates the query parameters passed to the request, and sets the default Query configurations.

Routes starting with `/admin` are protected by default. So, you don't need to add the `authenticate` middleware to enforce authentication.

In `src/api/middlewares.ts`, add a new middleware:

```ts title="src/api/middlewares.ts"
// other imports...
import { 
  validateAndTransformQuery,
} from "@medusajs/framework/http"
import { GetAdminReviewsSchema } from "./admin/reviews/route"

export default defineMiddlewares({
  routes: [
    {
      matcher: "/admin/reviews",
      method: ["GET"],
      middlewares: [
        validateAndTransformQuery(GetAdminReviewsSchema, {
          isList: true,
          defaults: [
            "id",
            "title",
            "content",
            "rating",
            "product_id",
            "customer_id",
            "status",
            "created_at",
            "updated_at",
            "product.*",
          ],
        }),
      ],
    },
  ],
})
```

You use the `validateAndTransformQuery` middleware to enforce validation on the query parameters passed to the request. The middleware accepts two parameters:

- The Zod schema to validate the query parameters, which is the `GetAdminReviewsSchema` schema you defined earlier.
- The Query configurations, which is an object with the following properties:
  - `isList`: A boolean that indicates whether the query is a list query.
  - `defaults`: An array of fields to retrieve by default.

You'll test the API route as you customize the Medusa Admin in the next step.

You pass `product.*` in the fields to retrieve, allowing you to retrieve the product associated with each review. This is possible because you defined a link between the `Review` data model and the `Product` data model in a previous step.

***

## Step 7: Add Reviews UI Route

Now that you have an API route that retrieves reviews, you'll customize the Medusa Admin to add a new "Reviews" page by creating a [UI Route](https://docs.medusajs.com/docs/learn/fundamentals/admin/ui-routes/index.html.md).

A UI route is a React component that specifies the content to be shown in a new page in the Medusa Admin dashboard. You'll create a UI route to display the list of reviews in the Medusa Admin.

Learn more about UI routes in the [UI Routes documentation](https://docs.medusajs.com/docs/learn/fundamentals/admin/ui-routes/index.html.md).

### Configure JS SDK

Medusa provides a [JS SDK](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/js-sdk/index.html.md) that you can use to send requests to the Medusa server from any client application, including your Medusa Admin customizations.

The JS SDK is installed by default in your Medusa application. To configure it, create the file `src/admin/lib/sdk.ts` with the following content:

```ts title="src/admin/lib/sdk.ts"
import Medusa from "@medusajs/js-sdk"

export const sdk = new Medusa({
  baseUrl: "http://localhost:9000",
  debug: process.env.NODE_ENV === "development",
  auth: {
    type: "session",
  },
})
```

You create an instance of the JS SDK using the `Medusa` class from the JS SDK. You pass it an object having the following properties:

- `baseUrl`: The base URL of the Medusa server.
- `debug`: A boolean indicating whether to log debug information into the console.
- `auth`: An object specifying the authentication type. When using the JS SDK for admin customizations, you use the `session` authentication type.

### Create UI Route

You'll now create the UI Route that lists the reviews. To do this, create the file `src/admin/routes/reviews/page.tsx` with the following content:

```tsx title="src/admin/routes/reviews/page.tsx" highlights={listUIRoutesHighlight1} collapsibleLines="1-18" expandButtonLabel="Show Imports"
import { defineRouteConfig } from "@medusajs/admin-sdk"
import { ChatBubbleLeftRight } from "@medusajs/icons"
import { 
  createDataTableColumnHelper, 
  Container, 
  DataTable, 
  useDataTable, 
  Heading, 
  StatusBadge, 
  Toaster, 
  DataTablePaginationState,
} from "@medusajs/ui"
import { useQuery } from "@tanstack/react-query"
import { useMemo, useState } from "react"
import { sdk } from "../../lib/sdk"
import { HttpTypes } from "@medusajs/framework/types"
import { Link } from "react-router-dom"

type Review = {
  id: string
  title?: string
  content: string
  rating: number
  product_id: string
  customer_id?: string
  status: "pending" | "approved" | "rejected"
  created_at: Date
  updated_at: Date
  product?: HttpTypes.AdminProduct
  customer?: HttpTypes.AdminCustomer
}


const columnHelper = createDataTableColumnHelper<Review>()

const columns = [
  columnHelper.accessor("id", {
    header: "ID",
  }),
  columnHelper.accessor("title", {
    header: "Title",
  }),
  columnHelper.accessor("rating", {
    header: "Rating", 
  }),
  columnHelper.accessor("content", {
    header: "Content",
  }),
  columnHelper.accessor("status", {
    header: "Status",
    cell: ({ row }) => {
      const color = row.original.status === "approved" ? 
        "green" : row.original.status === "rejected" 
        ? "red" : "grey"
      return (
        <StatusBadge color={color}>
          {row.original.status.charAt(0).toUpperCase() + row.original.status.slice(1)}
          </StatusBadge>
      )
    },
  }),
  columnHelper.accessor("product", {
    header: "Product",
    cell: ({ row }) => {
      return (
        <Link
          to={`/products/${row.original.product_id}`}
        >
          {row.original.product?.title}
        </Link>
      )
    },
  }),
]

// TODO add component
```

Before defining the component, you define a `Review` type, then define the columns of the table you'll show on the page.

To display the table, you'll use the [DataTable](https://docs.medusajs.com/ui/components/data-table/index.html.md) component from Medusa UI. To define the columns of the table, you use the `createDataTableColumnHelper` function from Medusa UI, which returns a `columnHelper` object. You then use the `columnHelper` object to define the table's columns.

Next, you'll add the component that renders the content of the page. Replace the `TODO` with the following:

```tsx title="src/admin/routes/reviews/page.tsx" highlights={reviewsPageHighlights}
const limit = 15

const ReviewsPage = () => {
  const [pagination, setPagination] = useState<DataTablePaginationState>({
    pageSize: limit,
    pageIndex: 0,
  })

  const offset = useMemo(() => {
    return pagination.pageIndex * limit
  }, [pagination])

  const { data, isLoading, refetch } = useQuery<{
    reviews: Review[]
    count: number
    limit: number
    offset: number
  }>({
    queryKey: ["reviews", offset, limit],
    queryFn: () => sdk.client.fetch("/admin/reviews", {
      query: {
        offset: pagination.pageIndex * pagination.pageSize,
        limit: pagination.pageSize,
        order: "-created_at",
      },
    }),
  })

  const table = useDataTable({
    columns,
    data: data?.reviews || [],
    rowCount: data?.count || 0,
    isLoading,
    pagination: {
      state: pagination,
      onPaginationChange: setPagination,
    },
    getRowId: (row) => row.id,
  })

  return (
    <Container>
      <DataTable instance={table}>
        <DataTable.Toolbar className="flex flex-col items-start justify-between gap-2 md:flex-row md:items-center">
          <Heading>
            Reviews
          </Heading>
        </DataTable.Toolbar>
        <DataTable.Table />
        <DataTable.Pagination />
      </DataTable>
      <Toaster />
    </Container>
  )
}

export const config = defineRouteConfig({
  label: "Reviews",
  icon: ChatBubbleLeftRight,
})

export default ReviewsPage
```

You create a `ReviewPage` component, which holds the UI route's content. In the component, you:

- Define state variables to configure pagination.
- Use the `useQuery` hook from `@tanstack/react-query` to fetch the reviews from the API route. In the query function, you use the JS SDK to send a request to the `/admin/reviews` API route. The JS SDK has a `client.fetch` method that has a similar signature to JavaScript's [Fetch API](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API). You can use it to send requests to custom routes.
- Use the `useDataTable` hook from Medusa UI to create a DataTable instance. You pass the columns, data, and pagination configurations to the hook.
- Render the DataTable component, passing the DataTable instance to the `instance` prop. You also render the DataTable's toolbar, table, and pagination components.

The file also exports a configuration object created with `defineRouteConfig`. You export this object to tell Medusa that you want to add the new route to the Medusa Admin's sidebar. You specify the sidebar's item and title.

### Test the UI Route

To test out the UI route, start the Medusa application:

```bash npm2yarn
npm run dev
```

Then, open the Medusa Admin dashboard at `http://localhost:9000/app` and login using the credentials you set up earlier.

You'll find a new sidebar item `Review`. Click on it to view the list of reviews. In the upcoming steps, you'll add functionality to approve or reject reviews.

![Reviews page showing list of reviews](https://res.cloudinary.com/dza7lstvk/image/upload/v1741935325/Medusa%20Resources/Screenshot_2025-03-14_at_8.54.14_AM_tfhnyu.png)

***

## Step 8: Change Review Status API Route

Next, you want to allow the admin user to approve or reject reviews. To do this, you'll create a workflow that updates a review's status, then use it in an API route that exposes the functionality.

### Update Review Step

The workflow to update a review's status will have on step that updates the review. To create the step, create the file `src/workflows/steps/update-review.ts` with the following content:

```ts title="src/workflows/steps/update-review.ts" highlights={updateReviewStepHighlights}
import {
  createStep,
  StepResponse,
} from "@medusajs/framework/workflows-sdk"
import { PRODUCT_REVIEW_MODULE } from "../../modules/product-review"
import ProductReviewModuleService from "../../modules/product-review/service"

export type UpdateReviewsStepInput = {
  id: string
  status: "pending" | "approved" | "rejected"
}[]

export const updateReviewsStep = createStep(
  "update-review-step",
  async (input: UpdateReviewsStepInput, { container }) => {
    const reviewModuleService: ProductReviewModuleService = container.resolve(
      PRODUCT_REVIEW_MODULE
    )

    // Get original review before update
    const originalReviews = await reviewModuleService.listReviews({
      id: input.map((review) => review.id),
    })

    const reviews = await reviewModuleService.updateReviews(input)

    return new StepResponse(reviews, originalReviews)
  },
  async (originalData, { container }) => {
    if (!originalData) {
      return
    }

    const reviewModuleService: ProductReviewModuleService = container.resolve(
      PRODUCT_REVIEW_MODULE
    )

    // Restore original review status
    await reviewModuleService.updateReviews(originalData)
  }
)
```

This step receives an array of objects, each with the ID of the review to update and its new status.

In the step function, you first retrieve the original reviews before the update. Then, you update the reviews using the `updateReviews` method of the Review Module's service.

After that, you return the updated reviews, and you pass the original reviews to the compensation function.

In the compensation function, you restore the original reviews' status if an error occurs.

### Update Review Workflow

You can now create the workflow that uses the above step to update the review. To create the workflow, create the file `src/workflows/update-review.ts` with the following content:

```ts title="src/workflows/update-review.ts"
import {
  createWorkflow,
  WorkflowResponse,
} from "@medusajs/framework/workflows-sdk"
import { updateReviewsStep } from "./steps/update-review"

export type UpdateReviewInput = {
  id: string
  status: "pending" | "approved" | "rejected"
}[]

export const updateReviewWorkflow = createWorkflow(
  "update-review",
  (input: UpdateReviewInput) => {
    const reviews = updateReviewsStep(input)

    return new WorkflowResponse({
      reviews,
    })
  }
)
```

The workflow receives an array of objects, each with the ID of the review to update and its new status. It uses the `updateReviewsStep` to update the reviews, then returns the updated reviews.

### Create API Route

Next, you'll create the API route that exposes the workflow's functionality. Create the file `src/api/admin/reviews/status/route.ts` with the following content:

```ts title="src/api/admin/reviews/status/route.ts" highlights={PostAdminUpdateReviewsStatusHighlights}
import type {
  MedusaRequest,
  MedusaResponse,
} from "@medusajs/framework/http"
import { updateReviewWorkflow } from "../../../../workflows/update-review"
import { z } from "zod"

export const PostAdminUpdateReviewsStatusSchema = z.object({
  ids: z.array(z.string()),
  status: z.enum(["pending", "approved", "rejected"]),
})

export async function POST(
  req: MedusaRequest<z.infer<typeof PostAdminUpdateReviewsStatusSchema>>, 
  res: MedusaResponse
) {
  const { ids, status } = req.validatedBody

  const { result } = await updateReviewWorkflow(req.scope).run({
    input: ids.map((id) => ({
      id,
      status,
    })),
  })

  res.json(result)
}
```

You first define a Zod schema for the request body of the API route. You'll later use this schema to enforce validation on the API route. The request body must include the following parameters:

- `ids`: An array of review IDs to update.
- `status`: The new status to set for the reviews.

Then, since you export a `POST` function, you're exposing a `POST` API route at the path `/admin/reviews/status`. In the route handler you execute the `updateReviewWorkflow` workflow, passing it the data from the request body.

Finally, you return the updated reviews.

### Apply Validation Middlewares

The last step is to add the validation middleware that enforces validation the body parameters of requests sent to the API route.

In `src/api/middlewares.ts`, add a new middleware:

```ts title="src/api/middlewares.ts"
// other imports...
import { PostAdminUpdateReviewsStatusSchema } from "./admin/reviews/status/route"

export default defineMiddlewares({
  routes: [
    // ...
    {
      matcher: "/admin/reviews/status",
      method: ["POST"],
      middlewares: [
        validateAndTransformBody(PostAdminUpdateReviewsStatusSchema),
      ],
    },
  ],
})
```

You use the `validateAndTransformBody` middleware to enforce validation on an incoming request's body parameters. You pass the Zod schema you defined in the API route's file to the middleware.

In the next step, you'll customize the UI route you added earlier to allow the admin user to approve or reject reviews.

***

## Step 9: Approve and Reject Reviews in UI Route

You'll now customize the UI route you added earlier to allow the admin user to approve or reject reviews. You'll add a checkbox column to the table that allows the admin user to select multiple reviews, then choose to approve or reject them.

The `DataTable` component from Medusa UI supports a command bar that is triggered by a select (or checkbox) column in the table.

Start by adding the necessary imports at the top of `src/admin/routes/reviews/page.tsx`:

```tsx title="src/admin/routes/reviews/page.tsx"
import { 
  createDataTableCommandHelper, 
  DataTableRowSelectionState, 
} from "@medusajs/ui"
```

Then, in the `columns` array, add a new select column as the first item in the array:

```tsx title="src/admin/routes/reviews/page.tsx"
const columns = [
  columnHelper.select(),
  // ...
]
```

The select column adds a checkbox to each row in the table, allowing the admin user to select multiple reviews.

Next, you need to add the commands that allow the admin user to approve or reject the selected reviews. So, add the following after the `columns` array:

```tsx title="src/admin/routes/reviews/page.tsx" highlights={commandHelperHighlights}
const commandHelper = createDataTableCommandHelper()

const useCommands = (refetch: () => void) => {
  return [
    commandHelper.command({
      label: "Approve",
      shortcut: "A",
      action: async (selection) => {
        const reviewsToApproveIds = Object.keys(selection)

        sdk.client.fetch("/admin/reviews/status", {
          method: "POST",
          body: {
            ids: reviewsToApproveIds,
            status: "approved",
          },
        }).then(() => {
          toast.success("Reviews approved")
          refetch()
        }).catch(() => {
          toast.error("Failed to approve reviews")
        })
      },
    }),
    commandHelper.command({
      label: "Reject",
      shortcut: "R",
      action: async (selection) => {
        const reviewsToRejectIds = Object.keys(selection)

        sdk.client.fetch("/admin/reviews/status", {
          method: "POST",
          body: {
            ids: reviewsToRejectIds,
            status: "rejected",
          },
        }).then(() => {
          toast.success("Reviews rejected")
          refetch()
        }).catch(() => {
          toast.error("Failed to reject reviews")
        })
      },
    }),
  ]
}
```

You first initialize the command helper using the `createDataTableCommandHelper` function from Medusa UI. Then, you create a custom hook `useCommands` that returns an array of commands created with the command helper.

You add `Approve` and `Reject` commands, and both of them send a request to the `/admin/reviews/status` API route to update the reviews' status, but each with a different status in the request body.

Next, add the following state variable in the `ReviewsPage` component:

```tsx title="src/admin/routes/reviews/page.tsx"
const [rowSelection, setRowSelection] = useState<DataTableRowSelectionState>({})
```

This state variable will hold the selected reviews in the table.

Then, call the `useCommands` hook and pass new properties to the `useDataTable` hook:

```tsx title="src/admin/routes/reviews/page.tsx"
const commands = useCommands(refetch)

const table = useDataTable({
  // ...
  commands,
  rowSelection: {
    state: rowSelection,
    onRowSelectionChange: setRowSelection,
  },
})
```

You call the `useCommands` hook and pass it the `refetch` function (returned by `useQuery`). The `refetch` function allows you to refetch the reviews after approving or rejecting them to ensure their status in the table is updated.

Then, you pass the commands and row selection configurations (from the state variables you added) to the `useDataTable` hook.

Finally, in the `return` statement, add the command bar after the pagination component:

```tsx title="src/admin/routes/reviews/page.tsx"
<DataTable.CommandBar selectedLabel={(count) => `${count} selected`} />
```

This command bar will show the actions to perform on the selected reviews.

### Test the UI Route

To test out the UI route, start the Medusa application:

```bash npm2yarn
npm run dev
```

Then, open the Medusa Admin dashboard and go to the Reviews page. You'll see a new column with checkboxes that allow you to select multiple reviews.

If you try selecting multiple reviews, you'll see a command bar at the bottom center of the page that allows you to approve or reject the selected reviews.

If you choose to approve or reject the reviews, the status of the selected reviews will change, and the table will update to reflect the new status.

![Checkboxes are now shown next to the items in the table, and when you click on them the command bar shows at the bottom of the page with Approve and Reject commands](https://res.cloudinary.com/dza7lstvk/image/upload/v1741937101/Medusa%20Resources/Screenshot_2025-03-14_at_9.24.29_AM_y9vhac.png)

***

## Step 10: List Reviews Store API Route

In the upcoming steps, you'll start customizing the storefront to show the reviews of a product and allow logged-in customers to add reviews.

Before doing that, you need to add an API route that retrieves the list of approved reviews. You'll later show these in the storefront.

### Add Average Rating Method in Service

On the product's page, you want to display the average rating of a product. To do this, you'll add a method that retrieves the average rating of a product's reviews in the Review Module's service.

In `src/modules/product-review/service.ts`, add the following methods to the `ProductReviewModuleService` class:

```ts title="src/modules/product-review/service.ts"
import { InjectManager, MedusaService, MedusaContext } from "@medusajs/framework/utils"
import Review from "./models/review"
import { Context } from "@medusajs/framework/types"
import { EntityManager } from "@mikro-orm/knex"

class ProductReviewModuleService extends MedusaService({
  Review,
}) {
  @InjectManager() 
  async getAverageRating(
    productId: string,
    @MedusaContext() sharedContext?: Context<EntityManager>
  ): Promise<number> { 
    const result = await sharedContext?.manager?.execute(
      `SELECT AVG(rating) as average 
       FROM review 
       WHERE product_id = '${productId}' AND status = 'approved'`
    )

    return parseFloat(parseFloat(result?.[0]?.average ?? 0).toFixed(2))
  }
}

export default ProductReviewModuleService
```

To run queries on the database in a service's method, you need to:

- Add the `InjectManager` decorator to the method.
- Pass as the last parameter a context parameter that has the `MedusaContext` decorator.

By doing the above, Medusa injects the method with a context parameter that has a `manger` property whose value is a [forked entity manager](https://mikro-orm.io/docs/identity-map#forking-entity-manager).

Then, you run a raw SQL query to calculate the average rating of the reviews for a product with the given ID. You also filter the reviews by the status `approved`.

You'll use this method next in the API route.

### Create API Route

To create the API route that lists the reviews of a product with average rating, create the file `src/api/store/products/[id]/reviews/route.ts` with the following content:

```ts title="src/api/store/products/[id]/reviews/route.ts" highlights={GetStoreReviewsHighlights} collapsibleLines="1-9" expandButtonLabel="Show Imports"
import {
  MedusaRequest,
  MedusaResponse,
} from "@medusajs/framework/http"
import { ContainerRegistrationKeys } from "@medusajs/framework/utils"
import { PRODUCT_REVIEW_MODULE } from "../../../../../modules/product-review"
import ProductReviewModuleService from "../../../../../modules/product-review/service"
import { createFindParams } from "@medusajs/medusa/api/utils/validators"

export const GetStoreReviewsSchema = createFindParams()

export const GET = async (
  req: MedusaRequest,
  res: MedusaResponse
) => {
  const { id } = req.params

  const query = req.scope.resolve(ContainerRegistrationKeys.QUERY)
  const reviewModuleService: ProductReviewModuleService = req.scope.resolve(PRODUCT_REVIEW_MODULE)

  // Get reviews for product
  const { data: reviews, metadata: {
    count,
    take,
    skip,
  } = { count: 0, take: 10, skip: 0 } } = await query.graph({
    entity: "review",
    filters: {
      product_id: id,
      // @ts-ignore
      status: "approved",
    },
    ...req.queryConfig,
  })

  res.json({
    reviews,
    count,
    limit: take,
    offset: skip,
    average_rating: await reviewModuleService.getAverageRating(id),
  })
}
```

You first define a `GetStoreReviewsSchema` schema that will allow clients to pass the following query parameters:

- `limit`: The number of reviews to retrieve.
- `offset`: The number of items to skip before retrieving the reviews.
- `order`: The fields to sort the reviews by in ascending or descending order.

Then, you export a `GET` function, and that exposes a `GET` API Route at the path `/store/products/[id]/reviews`. In the route handler you resolve [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md) from the Medusa container, which allows you to retrieve data across modules.

Next, you retrieve the approved reviews of a product using Query. Notice that you pass in `query.graph` the `req.queryConfig` object. This object holds the fields to retrieve and the pagination configurations. You'll configure this object in a bit.

Finally, you return the reviews with pagination fields and the average rating of the product.

### Apply Query Configurations Middleware

The last step is to add a middleware that validates the query parameters passed to the request, and sets the default Query configuations.

In `src/api/middlewares.ts`, add a new middleware:

```ts title="src/api/middlewares.ts"
// other imports
import { 
  validateAndTransformQuery,
} from "@medusajs/framework/http"
import { GetStoreReviewsSchema } from "./store/products/[id]/reviews/route"

export default defineMiddlewares({
  routes: [
    // ...
    {
      matcher: "/store/products/:id/reviews",
      methods: ["GET"],
      middlewares: [
        validateAndTransformQuery(GetStoreReviewsSchema, {
          isList: true,
          defaults: [
            "id", 
            "rating", 
            "title", 
            "first_name", 
            "last_name", 
            "content", 
            "created_at",
          ],
        }),
      ],
    },
  ],
})
```

You apply the `validateAndTransformQuery` middleware to the `GET` API route at the path `/store/products/:id/reviews`. Similar to before, you pass to the middleware:

- The validation schema of the request's query parameters, which is the `GetStoreReviewsSchema` you created earlier.
- An object of Query configurations. It has the following properties:
  - `isList`: A boolean indicating whether the route returns a list of items. This enables the pagination configurations.
  - `defaults`: An array of fields to retrieve by default.

By adding this middleware, you allow clients to pass pagination query parameters to the API route, and set default fields to retrieve.

You'll use this API route next as you customize the Next.js Starter Storefront.

***

## Step 11: Customize Next.js Starter Storefront

In this step, you'll customize the Next.js Starter Storefront to:

- Display a product's review and average rating on its page.
- Allow authenticated customers to submit a review for a product.

### Add Product Review Types

Before implementing the customizations, you'll add a type definition for the product review which you'll re-use in the storefront.

In `src/types/global.ts`, add the following types:

```ts title="src/types/global.ts" badgeLabel="Storefront" badgeColor="blue"
export type StoreProductReview = {
  id: string
  title: string
  rating: number
  content: string
  first_name: string
  last_name: string
}
```

You define the type of a product review object and the properties it has.

### Add Functions to Fetch and Submit Reviews

Next, you'll add two functions that fetch and submit reviews using the API routes you created earlier. To send requests to the API routes, you can use Medusa's [JS SDK](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/js-sdk/index.html.md).

In `src/lib/data/products.ts`, add the following functions:

```ts title="src/lib/data/products.ts" badgeLabel="Storefront" badgeColor="blue"
import { StoreProductReview } from "../../types/global"

// ...

export const getProductReviews = async ({
  productId,
  limit = 10,
  offset = 0,
}: {
  productId: string
  limit?: number
  offset?: number 
}) => {
  const headers = {
    ...(await getAuthHeaders()),
  }

  const next = {
    ...(await getCacheOptions(`product-reviews-${productId}`)),
  }

  return sdk.client.fetch<{
    reviews: StoreProductReview[]
    average_rating: number
    limit: number
    offset: number
    count: number
  }>(`/store/products/${productId}/reviews`, {
    headers,
    query: {
      limit,
      offset,
      order: "-created_at",
    },
    next,
    cache: "force-cache",
  })
}

export const addProductReview = async (input: {
  title?: string
  content: string
  first_name: string
  last_name: string
  rating: number,
  product_id: string
}) => {
  const headers = {
    ...(await getAuthHeaders()),
  }

  return sdk.client.fetch(`/store/reviews`, {
    method: "POST",
    headers,
    body: input,
    next: {
      ...(await getCacheOptions(`product-reviews-${input.product_id}`)),
    },
    cache: "no-store",
  })
}
```

You define two functions:

- `getProductReviews`: Fetches the reviews of a product with the given ID. It accepts an object with the product ID, and optional limit and offset parameters, allowing you to paginate the reviews.
- `addProductReview`: Submits a review for a product. It accepts an object with the review's details.

To send requests to your custom API routes, you use the JS SDK's `client.fetch` method.

### Add Product Review Form

You'll now create a component that shows the product review form for authenticated customers. Afterwards, you'll display this component on the product's page.

To create the form component, create the file `src/modules/products/components/product-reviews/form.tsx` with the following content:

```tsx title="src/modules/products/components/product-reviews/form.tsx" badgeLabel="Storefront" badgeColor="blue"
"use client"

import { useState } from "react"

import { useEffect } from "react"
import { retrieveCustomer } from "../../../../lib/data/customer"
import { HttpTypes } from "@medusajs/types"
import { Button, Input, Label, Textarea, toast, Toaster } from "@medusajs/ui"
import { Star, StarSolid } from "@medusajs/icons"
import { addProductReview } from "../../../../lib/data/products"

type ProductReviewsFormProps = {
  productId: string
}

export default function ProductReviewsForm({ productId }: ProductReviewsFormProps) {
  const [customer, setCustomer] = useState<HttpTypes.StoreCustomer | null>(null)
  const [isLoading, setIsLoading] = useState(false)
  const [showForm, setShowForm] = useState(false)
  const [title, setTitle] = useState("")
  const [content, setContent] = useState("")
  const [rating, setRating] = useState(0)

  useEffect(() => {
    if (customer) {
      return
    }

    retrieveCustomer().then(setCustomer)
  }, [])

  if (!customer) {
    return <></>
  }

  const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
    if (!content || !rating) {
      toast.error("Error", {
        description: "Please fill in required fields.",
      })
      return
    }

    e.preventDefault()
    setIsLoading(true)
    addProductReview({
      title,
      content,
      rating,
      first_name: customer.first_name || "",
      last_name: customer.last_name || "",
      product_id: productId,
    }).then(() => {
      setShowForm(false)
      setTitle("")
      setContent("")
      setRating(0)
      toast.success("Success", {
        description: "Your review has been submitted and is awaiting approval.",
      })
    }).catch(() => {
      toast.error("Error", {
        description: "An error occurred while submitting your review. Please try again later.",
      })
    }).finally(() => {
      setIsLoading(false)
    })
  }

  // TODO render form
}
```

You create a `ProductReviewsForm` component that accepts the product's ID as a prop. In the component, you:

- Fetch the authenticated customer's details. If the customer is not authenticated, you return an empty fragment.
- Implement a `handleSubmit` function that submits the review when the form is submitted.

Next, you'll add a return statement that shows the form when the customer is authenticated. Replace the `TODO` with the following:

```tsx title="src/modules/products/components/product-reviews/form.tsx" badgeLabel="Storefront" badgeColor="blue"
return (
  <div className="product-page-constraint mt-8">
    {!showForm && (
      <div className="flex justify-center">
        <Button variant="secondary" onClick={() => setShowForm(true)}>Add a review</Button>
      </div>
    )}
    {showForm && (
      <div className="flex flex-col gap-y-4">
        <div className="flex flex-col gap-y-2">
          <span className="text-xl-regular text-ui-fg-base">
          Add a review
        </span>
        
        <form onSubmit={handleSubmit} className="flex flex-col gap-y-4">
          <div className="flex flex-col gap-y-2">
            <Label>Title</Label>
            <Input name="title" value={title} onChange={(e) => setTitle(e.target.value)} placeholder="Title" />
          </div>
          <div className="flex flex-col gap-y-2">
            <Label>Content</Label>
            <Textarea name="content" value={content} onChange={(e) => setContent(e.target.value)} placeholder="Content" />
          </div>
          <div className="flex flex-col gap-y-2">
            <Label>Rating</Label>
            <div className="flex gap-x-1">
              {Array.from({ length: 5 }).map((_, index) => (
                <Button key={index} variant="transparent" onClick={(e) => {
                  e.preventDefault()
                  setRating(index + 1)
                }} className="p-0">
                  {rating >= index + 1 ? <StarSolid className="text-ui-tag-orange-icon" /> : <Star />}
                </Button>
              ))}
            </div>
          </div>
          <Button type="submit" disabled={isLoading} variant="primary">Submit</Button>
        </form>
        </div>
      </div>
    )}
    <Toaster />
  </div>
)
```

In the return statement, you:

- Show an "Add a review" button. When clicked, the form is displayed.
- In the form, you show the customer fields for the title, content, and rating for the review. The rating input is displayed as stars, and the customer can click on a star to set the rating.
- When the form is submitted, you call the `handleSubmit` function to submit the review.

### Display Product Reviews

Now, you'll add the components to display the product reviews and the product review form on the product's page.

Create the file `src/modules/products/components/product-reviews/index.tsx` with the following content:

```tsx title="src/modules/products/components/product-reviews/index.tsx" badgeLabel="Storefront" badgeColor="blue"
"use client"

import { getProductReviews } from "../../../../lib/data/products"
import { Star, StarSolid } from "@medusajs/icons"
import { StoreProductReview } from "../../../../types/global"
import { Button } from "@medusajs/ui"
import { useState, useEffect } from "react"
import ProductReviewsForm from "./form"
type ProductReviewsProps = {
  productId: string
}

export default function ProductReviews({
  productId,
}: ProductReviewsProps) {
  const [page, setPage] = useState(1)
  const defaultLimit = 10
  const [reviews, setReviews] = useState<StoreProductReview[]>([])
  const [rating, setRating] = useState(0)
  const [hasMoreReviews, setHasMoreReviews] = useState(false)
  const [count, setCount] = useState(0)

  useEffect(() => {
    getProductReviews({
      productId,
      limit: defaultLimit,
      offset: (page - 1) * defaultLimit,
    }).then(({ reviews: paginatedReviews, average_rating, count, limit }) => {
      setReviews((prev) => {
        const newReviews = paginatedReviews.filter(
          (review) => !prev.some((r) => r.id === review.id)
        )
        return [...prev, ...newReviews]
      })
      setRating(Math.round(average_rating))
      console.log(count, limit, page, count > limit * page)
      setHasMoreReviews(count > limit * page)
      setCount(count)
    })
  }, [page])

  // TODO add return statement
}
```

You create a `ProductReviews` component that accepts the product's ID as a prop. In the component, you:

- Define state variables related to the reviews and pagination.
- When the page changes, you fetch the reviews of the product with the given ID.

Before adding the return statement that will show the reviews and the create-review form, you'll add a component that renders a single review.

Add the following component after the `ProductReviews` component:

```tsx title="src/modules/products/components/product-reviews/index.tsx" badgeLabel="Storefront" badgeColor="blue"
function Review({ review }: { review: StoreProductReview }) {
  return (
    <div className="flex flex-col gap-y-2 text-base-regular text-ui-fg-base">
      <div className="flex gap-x-2 items-center">
        {review.title && <strong>{review.title}</strong>}
        <div className="flex gap-x-1">
          {Array.from({ length: 5 }).map((_, index) => (
            <span key={index}>
              {index <= review.rating ? (
                <StarSolid className="text-ui-tag-orange-icon" />
              ) : (
                <Star />
              )}
            </span>
          ))}
        </div>
      </div>
      <div>{review.content}</div>
      <div className="border-t border-ui-border-base pt-4 text-sm-regular">
        {review.first_name} {review.last_name}
      </div>
    </div>
  )
}
```

You add a `Review` component that accepts a review object as a prop. In the component, you render the review's title, rating, content, and the reviewer's name.

Next, replace the `TODO` in the `ProductReviews` component with the following:

```tsx title="src/modules/products/components/product-reviews/index.tsx" badgeLabel="Storefront" badgeColor="blue"
return (
  <div className="product-page-constraint">
    <div className="flex flex-col items-center text-center mb-16">
      <span className="text-base-regular text-gray-600 mb-6">
        Product Reviews
      </span>
      <p className="text-2xl-regular text-ui-fg-base max-w-lg">
        See what our customers are saying about this product.
      </p>
      <div className="flex gap-x-2 justify-center items-center">
        <div className="flex gap-x-2">
          {Array.from({ length: 5 }).map((_, index) => (
            <span key={index}>
              {!rating || index > rating ? (
                <Star />
              ) : (
                <StarSolid className="text-ui-tag-orange-icon" />
              )}
            </span>
          ))}
        </div>
        <span className="text-base-regular text-gray-600">
          {count} reviews
        </span>
      </div>
    </div>

    <div className="grid grid-cols-1 small:grid-cols-2 gap-x-6 gap-y-8">
      {reviews.map((review) => (
        <Review key={review.id} review={review} />
      ))}
    </div>

    {hasMoreReviews && (
      <div className="flex justify-center mt-8">
        <Button variant="secondary" onClick={() => setPage(page + 1)}>
          Load more reviews
        </Button>
      </div>
    )}

    <ProductReviewsForm productId={productId} />
  </div>
)
```

You show the average rating of the product and the number of reviews. Then, you show every review loaded. You also show a "Load more reviews" button if there are more reviews to load, which changes the `page` and fetches more reviews.

After the reviews, you show the `ProductReviewsForm` component to allow authenticated customers to submit a review.

### Display Product Reviews on Product Page

Finally, you'll customize the product's page to show the `ProductReviews` component.

In `src/modules/products/templates/index.tsx`, import the `ProductReviews` component at the top of the file:

```tsx title="src/modules/products/templates/index.tsx" badgeLabel="Storefront" badgeColor="blue"
import ProductReviews from "../components/product-reviews"
```

Then, add the `ProductReviews` component before the `div` wrapping the `RelatedProducts` component:

```tsx title="src/modules/products/templates/index.tsx" badgeLabel="Storefront" badgeColor="blue"
<div className="content-container my-16 small:my-32">
  <ProductReviews productId={product.id} />
</div>
```

This will show the product reviews after the product's image and details, but before the related products.

### Test the Customizations

To test out both the server and storefront customizations, first, start the Medusa application by running the following command in its directory:

```bash npm2yarn
npm run dev
```

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

```bash npm2yarn
npm run dev
```

The storefront will run at `http://localhost:8000`. Open it, then click on Menu -> Store. This will show you the list of products.

If you click on one of them and scroll down below the images, you'll find a section showing the average rating and reviews of the product.

![Product page showing the average rating and reviews of the product](https://res.cloudinary.com/dza7lstvk/image/upload/v1741937534/Medusa%20Resources/Screenshot_2025-03-14_at_9.31.58_AM_tw9vui.png)

To add a review, you first need to log in as a customer. You can do so by clicking on Account at the top right of the page. In the new page, either enter the credentials of the customer you created earlier, or create a new customer.

Afterwards, go back to the product's page, you'll see the "Add a review" button below the reviews.

![Product page showing the Add a review button](https://res.cloudinary.com/dza7lstvk/image/upload/v1741937731/Medusa%20Resources/Screenshot_2025-03-14_at_9.35.11_AM_w1wzdp.png)

If you click on the button, a form will appear where you can fill in the review's details and submit it.

![Product page showing the Add a review form](https://res.cloudinary.com/dza7lstvk/image/upload/v1741938961/Medusa%20Resources/Screenshot_2025-03-14_at_9.55.37_AM_epnfz0.png)

After submitting the review, you can approve or reject it from the Medusa Admin dashboard.

***

## Next Steps

You've now implemented product-review features in Medusa. There's still more that you can implement to enhance these features:

- Link a Review to a customer as you did in [Step 3](#step-3-define-review--product-link) and customize the storefront to show the customer's reviews on their profile.
- Add a feature to allow customers to upvote or downvote reviews.
- Allow customers to add images to their reviews.

If you're new to Medusa, check out the [main documentation](https://docs.medusajs.com/docs/learn/index.html.md), 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](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/index.html.md).


# Integrations

You can integrate any third-party service to Medusa, including storage services, notification systems, Content-Management Systems (CMS), etc… By integrating third-party services, you build flows and synchronize data around these integrations, making Medusa not only your commerce application, but a middleware layer between your data sources and operations.

Medusa provides integrations out-of-the-box that are listed here, but you can also create your own integrations, such as integrating ERP systems, as explained in [this guide](https://docs.medusajs.com/docs/learn/customization/integrate-systems/index.html.md).

This section holds guides to help technical teams add integrations to a Medusa application. If you're not a technical user, refer your technical team to this documentation instead.

## Analytics

An Analytics Module Provider tracks events and user behavior in your Medusa application using a third-party service.

- [PostHog](https://docs.medusajs.com/infrastructure-modules/analytics/posthog/index.html.md)
- [Segment](https://docs.medusajs.com/integrations/guides/segment/index.html.md)

Learn how to integrate a custom third-party analytics provider in the [Create Analytics Module Provider](https://docs.medusajs.com/references/analytics/provider/index.html.md) documentation.

***

## Auth

An Auth Module Provider authenticates users with their account on a third-party service.

- [Google](https://docs.medusajs.com/commerce-modules/auth/auth-providers/google/index.html.md)
- [GitHub](https://docs.medusajs.com/commerce-modules/auth/auth-providers/github/index.html.md)

Learn how to integrate a custom third-party authentication provider in the [Create Auth Module Provider](https://docs.medusajs.com/references/auth/provider/index.html.md) documentation.

***

## CMS

Integrate a third-party Content-Management System (CMS) to utilize rich content-related features.

- [Contentful (Localization)](https://docs.medusajs.com/integrations/guides/contentful/index.html.md)
- [Sanity](https://docs.medusajs.com/integrations/guides/sanity/index.html.md)

***

## ERP

Integrate your business's Enterprise Resource Planning (ERP) system with Medusa to sync products and orders, restrict purchase with custom rules, and more.

To learn about the general approach of integrating an ERP with Medusa and the different use cases you can implement, refer to the [ERP Recipe](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/recipes/erp/index.html.md).

- [Odoo](https://docs.medusajs.com/recipes/erp/odoo/index.html.md)

***

## File

A File Module Provider uploads and manages assets, such as product images, on a third-party service.

- [AWS S3 (and Compatible APIs)](https://docs.medusajs.com/infrastructure-modules/file/s3/index.html.md)

Learn how to integrate a custom third-party file or storage provider in the [Create File Module Provider](https://docs.medusajs.com/references/file-provider-module/index.html.md) documentation.

***

## Fulfillment

A Fulfillment Module Provider provides fulfillment options during checkout, calculates shipping rates, and processes an order's fulfillments.

- [ShipStation](https://docs.medusajs.com/integrations/guides/shipstation/index.html.md)

Learn how to integrate a third-party fulfillment provider in the [Create Fulfillment Module Provider](https://docs.medusajs.com/references/fulfillment/provider/index.html.md) documentation.

***

## Notification

A Notification Module Provider sends messages to users and customers in your Medusa application using a third-party service.

- [SendGrid](https://docs.medusajs.com/infrastructure-modules/notification/sendgrid/index.html.md)
- [Resend](https://docs.medusajs.com/integrations/guides/resend/index.html.md)
- [Twilio SMS](https://docs.medusajs.com/how-to-tutorials/tutorials/phone-auth#step-3-integrate-twilio-sms/index.html.md)

Learn how to integrate a third-party notification provider in the [Create Notification Module Provider](https://docs.medusajs.com/references/notification-provider-module/index.html.md) documentation.

***

## Payment

A Payment Module Provider processes payments made in your Medusa store using a third-party service.

- [Stripe](https://docs.medusajs.com/commerce-modules/payment/payment-provider/stripe/index.html.md)

Learn how to integrate a third-party payment provider in the [Create Payment Module Provider](https://docs.medusajs.com/references/payment/provider/index.html.md) documentation.

***

## Search

Integrate a search engine to index and search products or other types of data in your Medusa application.

- [Algolia](https://docs.medusajs.com/integrations/guides/algolia/index.html.md)


# Integrate Algolia (Search) with Medusa

In this tutorial, you'll learn how to integrate Medusa with Algolia.

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 a search engine, allowing you to build your unique requirements around core commerce flows.

[Algolia](https://www.algolia.com/doc/) is a search engine that enables you to build and manage an intuitive search experience for your customers. By integrating Algolia with Medusa, you can index e-commerce data, such as products, and allow clients to search through them.

You can follow this guide whether you're new to Medusa or an advanced Medusa developer.

## Summary

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

- Install and set up Medusa.
- Integrate Algolia into Medusa.
- Trigger Algolia reindexing when a product is created, updated, deleted, or when the admin manually triggers a reindex.
- Customize the Next.js Starter Storefront to allow searching for products through Algolia.

![Diagram illustrating the integration of Algolia with Medusa](https://res.cloudinary.com/dza7lstvk/image/upload/v1742889842/Medusa%20Resources/algolia-summary_lhegrr.jpg)

- [Algolia Integration Repository](https://github.com/medusajs/examples/tree/main/algolia-integration): Find the full code for this guide in this repository.
- [OpenApi Specs for Postman](https://res.cloudinary.com/dza7lstvk/raw/upload/v1742829748/OpenApi/Algolia-Search_t1zlkd.yaml): Import this OpenApi Specs file into tools like Postman.

***

## Step 1: Install a Medusa Application

### Prerequisites

- [Node.js v20+](https://nodejs.org/en/download)
- [Git CLI tool](https://git-scm.com/downloads)
- [PostgreSQL](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 asked whether you want to install the [Next.js Starter Storefront](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/nextjs-starter/index.html.md), choose "Yes."

Afterwards, the installation process will start, which will install the Medusa application in a directory with your project's name and the Next.js Starter Storefront in a separate directory named `{project-name}-storefront`.

The Medusa application is composed of a headless Node.js server and an admin dashboard. The storefront is installed or custom-built separately and connects to the Medusa application through its REST endpoints, called [API routes](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/index.html.md). Learn more in [Medusa's Architecture documentation](https://docs.medusajs.com/docs/learn/introduction/architecture/index.html.md).

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.

Check out the [troubleshooting guides](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/troubleshooting/create-medusa-app-errors/index.html.md) for help.

***

## Step 2: Create Algolia Module

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 integrates the module into your application without implications or side effects on your setup.

In this step, you'll create a custom module that provides the necessary functionalities to integrate Algolia with Medusa.

Refer to the [Modules documentation](https://docs.medusajs.com/docs/learn/fundamentals/modules/index.html.md) to learn more.

Before building the module, you need to install Algolia's JavaScript client. Run the following command in your Medusa application's root directory:

```bash npm2yarn
npm install algoliasearch
```

### Create Module Directory

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

### 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 Algolia Module's service and the methods necessary to manage indexed products in Algolia and search through them.

To create the Algolia Module's service, create the file `src/modules/algolia/service.ts` with the following content:

```ts title="src/modules/algolia/service.ts"
import { algoliasearch, SearchClient } from "algoliasearch"

type AlgoliaOptions = {
  apiKey: string;
  appId: string;
  productIndexName: string;
}

export type AlgoliaIndexType = "product"

export default class AlgoliaModuleService {
  private client: SearchClient
  private options: AlgoliaOptions

  constructor({}, options: AlgoliaOptions) {
    this.client = algoliasearch(options.appId, options.apiKey)
    this.options = options
  }

  // TODO add methods
}
```

You export a class that will be the Algolia Module's main service. In the service, you define two properties:

- `client`: An instance of the Algolia Search Client, which you'll use to perform actions with Algolia's API.
- `options`: An object of options that the Module receives when it's registered, which you'll learn about later. The options contain:
  - `apiKey`: The Algolia API key.
  - `appId`: The Algolia App ID.
  - `productIndexName`: The name of the index where products are stored.

If you want to index other types of data, such as product categories, you can add new properties for their index names in the `AlgoliaOptions` type.

A module's service receives the module's options as a second parameter in its constructor. In the constructor, you initialize the Algolia client using the module's options.

A module has a container that holds all resources registered in that module, and you can access those resources in the first parameter of the constructor. Learn more about it in the [Module Container documentation](https://docs.medusajs.com/docs/learn/fundamentals/modules/container/index.html.md).

#### Index Data Method

The first method you need to add to the servie is a method that receives an array of data to add or update in Algolia's index.

Add the following methods to the `AlgoliaModuleService` class:

```ts title="src/modules/algolia/service.ts"
export default class AlgoliaModuleService {
  // ...
  async getIndexName(type: AlgoliaIndexType) {
    switch (type) {
      case "product":
        return this.options.productIndexName
      default:
        throw new Error(`Invalid index type: ${type}`)
    }
  }

  async indexData(data: Record<string, unknown>[], type: AlgoliaIndexType = "product") {
    const indexName = await this.getIndexName(type)
    this.client.saveObjects({
      indexName,
      objects: data.map((item) => ({
        ...item,
        // set the object ID to allow updating later
        objectID: item.id,
      })),
    })
  }
}
```

You define two methods:

1. `getIndexName`: A method that receives an `AlgoliaIndexType` (defined in the previous snippt) and returns the index name for that type. In this case, you only have one type, `product`, so you return the product index name.
   - If you want to index other types of data, you can add more cases to the switch statement.
2. `indexData`: A method that receives an array of data and an `AlgoliaIndexType`. The method indexes the data in the Algolia index for the given type.
   - Notice that you set the `objectID` property of each object to the object's `id`. This ensures that you later update the object instead of creating a new one.

#### Retrieve and Delete Methods

The next methods you'll add to the service are methods to retrieve and delete data from the Algolia index. You'll see their use later as you keep the Algolia index in sync with Medusa.

Add the following methods to the `AlgoliaModuleService` class:

```ts title="src/modules/algolia/service.ts"
export default class AlgoliaModuleService {
  // ...

  async retrieveFromIndex(objectIDs: string[], type: AlgoliaIndexType = "product") {
    const indexName = await this.getIndexName(type)
    return await this.client.getObjects<Record<string, unknown>>({
      requests: objectIDs.map((objectID) => ({
        indexName,
        objectID,
      })),
    })
  }

  async deleteFromIndex(objectIDs: string[], type: AlgoliaIndexType = "product") {
    const indexName = await this.getIndexName(type)
    await this.client.deleteObjects({
      indexName,
      objectIDs,
    })
  }
}
```

You define two methods:

1. `retrieveFromIndex`: A method that receives an array of object IDs and an `AlgoliaIndexType`. The method retrieves the objects with the given IDs from the Algolia index.
2. `deleteFromIndex`: A method that receives an array of object IDs and an `AlgoliaIndexType`. The method deletes the objects with the given IDs from the Algolia index.

#### Search Method

The last method you'll implement is a method to search through the Algolia index. You'll later use this method to expose the search functionality to clients, such as the Next.js Starter Storefront.

Add the following method to the `AlgoliaModuleService` class:

```ts title="src/modules/algolia/service.ts"
export default class AlgoliaModuleService {
  // ...

  async search(query: string, type: AlgoliaIndexType = "product") {
    const indexName = await this.getIndexName(type)
    return await this.client.search({
      requests: [
        {
          indexName,
          query,
        },
      ],
    })
  }
}
```

The `search` method receives a query string and an `AlgoliaIndexType`. The method searches through the Algolia index for the given type, such as products, and returns the results.

### Export Module Definition

The final piece to a module is its definition, which you export in an `index.ts` file at its root directory. This definition tells Medusa the name of the module and its service.

So, create the file `src/modules/algolia/index.ts` with the following content:

```ts title="src/modules/algolia/index.ts"
import { Module } from "@medusajs/framework/utils"
import AlgoliaModuleService from "./service"

export const ALGOLIA_MODULE = "algolia"

export default Module(ALGOLIA_MODULE, {
  service: AlgoliaModuleService,
})
```

You use the `Module` function from the Modules SDK to create the module's definition. It accepts two parameters:

1. The module's name, which is `algolia`.
2. An object with a required property `service` indicating the module's service.

You also export the module's name as `ALGOLIA_MODULE` so you can reference it later.

### Add Module 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 and pass an array with your custom module:

```ts title="medusa-config.ts"
module.exports = defineConfig({
  // ...
  modules: [
    {
      resolve: "./src/modules/algolia",
      options: {
        appId: process.env.ALGOLIA_APP_ID!,
        apiKey: process.env.ALGOLIA_API_KEY!,
        productIndexName: process.env.ALGOLIA_PRODUCT_INDEX_NAME!,
      },
    },
  ],
})
```

Each object in the `modules` array has a `resolve` property, whose value is either a path to the module's directory, or an `npm` package’s name.

You also pass an `options` property with the module's options, including the Algolia App ID, API Key, and the product index name.

### Add Environment Variables

Before you can start using the Algolia Module, you need to set the environment variables for the Algolia App ID, API Key, and the product index name.

Add the following environment variables to your `.env` file:

```env
ALGOLIA_APP_ID=your-algolia-app-id
ALGOLIA_API_KEY=your-algolia-api-key
ALGOLIA_PRODUCT_INDEX_NAME=your-product-index-name
```

Where:

- `your-algolia-app-id` is your Algolia App ID. You can retrieve it from the Algolia dashboard by clicking at the application ID at the top left next to the sidebar. The pop up will show the application ID below the application's name.

![Find the Algolia App ID by clicking on the application name in the Algolia dashboard, then copying the ID below the name in the pop-up](https://res.cloudinary.com/dza7lstvk/image/upload/v1742815360/Medusa%20Resources/Screenshot_2025-03-24_at_1.19.30_PM_kdp3y5.png)

- `your-algolia-api-key` is your Algolia API Key. To retrieve it from the Algolia dashboard:
  1. Click on Settings in the sidebar.
  2. Choose API Keys under "Team and Access".

![In the settings page, find the Team and Access section at the right of the page and choose API Keys](https://res.cloudinary.com/dza7lstvk/image/upload/v1742815534/Medusa%20Resources/Screenshot_2025-03-24_at_1.25.09_PM_hwsiba.png)

3. Copy the Admin API Key.

- `your-product-index-name` is the name of the index where you'll store products. You can find it by going to Search -> Index, and copying the index name at the top of the page.

![In the Algolia dashboard, go to Search -> Index and copy the index name at the top of the page](https://res.cloudinary.com/dza7lstvk/image/upload/v1742815790/Medusa%20Resources/Screenshot_2025-03-24_at_1.28.58_PM_yq10sf.png)

Your module is now ready for use. You'll see how to use it in the next steps.

***

## Step 3: Sync Products to Algolia Workflow

To keep the Algolia index in sync with Medusa, you need to trigger indexing when products are created, updated, or deleted in Medusa. You can also allow the admin to manually trigger a reindex.

To implement the indexing functionality, you need to create a [workflow](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md). A workflow is a series of 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.

Learn more about workflows in the [Workflows documentation](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md).

In this step, you'll create a workflow that indexes products in Algolia. In the next steps, you'll learn how to use the workflow when products are created, updated, or deleted, or when the admin manually triggers a reindex.

The workflow has the following steps:

- [useQueryGraphStep](https://docs.medusajs.com/references/helper-steps/useQueryGraphStep/index.html.md): Retrieve products matching specified filters and pagination parameters.
- [syncProductsStep](#syncProductsStep): Index products in Algolia.

Medusa provides the `useQueryGraphStep` in its `@medusajs/medusa/core-flows` package. So, you only need to implement the second step.

### syncProductsStep

In the second step of the workflow, you create or update indexes in Algolia for the products retrieved in the first step.

To create the step, create the file `src/workflows/steps/sync-products.ts` with the following content:

```ts title="src/workflows/steps/sync-products.ts"
import { ProductDTO } from "@medusajs/framework/types"
import { createStep, StepResponse } from "@medusajs/framework/workflows-sdk"
import { ALGOLIA_MODULE } from "../../modules/algolia"
import AlgoliaModuleService from "../../modules/algolia/service"

export type SyncProductsStepInput = {
  products: ProductDTO[]
}

export const syncProductsStep = createStep(
  "sync-products",
  async ({ products }: SyncProductsStepInput, { container }) => {
    const algoliaModuleService: AlgoliaModuleService = container.resolve(ALGOLIA_MODULE)

    const existingProducts = (await algoliaModuleService.retrieveFromIndex(
      products.map((product) => product.id),
      "product"
    )).results.filter(Boolean)
    const newProducts = products.filter(
      (product) => !existingProducts.some((p) => p.objectID === product.id)
    )
    await algoliaModuleService.indexData(
      products as unknown as Record<string, unknown>[], 
      "product"
    )

    return new StepResponse(undefined, {
      newProducts: newProducts.map((product) => product.id),
      existingProducts,
    })
  }
  // TODO add compensation
)
```

You create a step with `createStep` from the Workflows SDK. It accepts two parameters:

1. The step's unique name, which is `sync-products`.
2. An async function that receives two parameters:
   - The step's input, which is in this case an object holding an array of products to sync into Algolia.
   - An object that has properties including the [Medusa container](https://docs.medusajs.com/docs/learn/fundamentals/medusa-container/index.html.md), which is a registry of Framework and commerce tools that you can access in the step.

In the step function, you resolve the Algolia Module's service from the Medusa container using the name you exported in the module definition's file.

Then, you retrieve the products that are already indexed in Algolia and determine which products are new. You'll learn why this is useful in a bit.

Finally, you pass the products you received in the input to Algolia to create or update its indices.

A step function must return a `StepResponse` instance. The `StepResponse` constructor accepts two parameters:

1. The step's output, which in this case is `undefined`.
2. Data to pass to the step's compensation function.

#### Compensation Function

The compensation function undoes the actions performed in a step. Then, if an error occurs during the workflow's execution, the compensation functions of executed steps are called to roll back the changes. This mechanism ensures data consistency in your application, especially as you integrate external systems.

To add a compensation function to a step, pass it as a third parameter to `createStep`:

```ts title="src/workflows/steps/sync-products.ts"
export const syncProductsStep = createStep(
  // ...
  async (input, { container }) => {
    if (!input) {
      return
    }

    const algoliaModuleService: AlgoliaModuleService = container.resolve(ALGOLIA_MODULE)
    
    if (input.newProducts) {
      await algoliaModuleService.deleteFromIndex(
        input.newProducts,
        "product"
      )
    }

    if (input.existingProducts) {
      await algoliaModuleService.indexData(
        input.existingProducts,
        "product"
      )
    }
  }
)
```

The compensation function receives two parameters:

1. The data you passed as a second parameter of `StepResponse` in the step function.
2. A context object similar to the step function that holds the Medusa container.

In the compensation function, you resolve the Algolia Module's service from the container. Then, you delete from Algolia the products that were newly indexed, and revert the existing products to their original data.

### Add Sync Products Workflow

You can now create the worklow that syncs the products to Algolia.

To create the workflow, create the file `src/workflows/sync-products.ts` with the following content:

```ts title="src/workflows/sync-products.ts"
import { createWorkflow, WorkflowResponse } from "@medusajs/framework/workflows-sdk"
import { useQueryGraphStep } from "@medusajs/medusa/core-flows"
import { syncProductsStep, SyncProductsStepInput } from "./steps/sync-products"

type SyncProductsWorkflowInput = {
  filters?: Record<string, unknown>
  limit?: number
  offset?: number
}

export const syncProductsWorkflow = createWorkflow(
  "sync-products",
  ({ filters, limit, offset }: SyncProductsWorkflowInput) => {
    // @ts-ignore
    const { data, metadata } = useQueryGraphStep({
      entity: "product",
      fields: ["id", "title", "description", "handle", "thumbnail", "categories.*", "tags.*"],
      pagination: {
        take: limit,
        skip: offset,
      },
      filters: {
        // @ts-ignore
        status: "published",
        ...filters,
      },
    })

    syncProductsStep({
      products: data,
    } as SyncProductsStepInput)

    return new WorkflowResponse({
      products: data,
      metadata,
    })
  }
)
```

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 pagination and filter parameters for the products to retrieve.

In the workflow's constructor function, you:

1. Execute `useQueryGraphStep` to retrieve products from Medusa's database. This step uses Medusa's [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md) tool to retrieve data across modules. You pass it the pagination and filter parameters you received in the input.
2. Execute `syncProductsStep` to index the products in Algolia. You pass it the products you retrieved in the previous step.

A workflow must return an instance of `WorkflowResponse`. The `WorkflowResponse` constructor accepts the workflow's output as a parameter, which is an object holding the retrieved products and their pagination details.

In the next step, you'll learn how to execute this workflow.

***

## Step 4: Trigger Algolia Sync Manually

As mentioned earlier, you'll trigger the Algolia sync automatically when product events occur, but you also want to allow the admin to manually trigger a reindex.

In this step, you'll add the functionality to trigger the `syncProductsWorkflow` manually from the Medusa Admin dashboard. This requires:

1. Creating a subscriber that listens to a custom `algolia.sync` event to trigger syncing products to Algolia.
2. Creating an API route that the Medusa Admin dashboard can call to emit the `algolia.sync` event, which triggers the subscriber.
3. Add a new page or UI route to the Medusa Admin dashboard to allow the admin to trigger the reindex.

### Create Products Sync Subscriber

A subscriber is an asynchronous function that listens to one or more events and performs actions when these events are emitted. A subscriber is useful when syncing data across systems, as the operation can be time-consuming and should be performed in the background.

Learn more about subscribers in the [Events and Subscribers documentation](https://docs.medusajs.com/docs/learn/fundamentals/events-and-subscribers/index.html.md).

You create a subscriber in a TypeScript or JavaScript file under the `src/subscribers` directory. So, to create the subscriber that listens to the `algolia.sync` event, create the file `src/subscribers/algolia-sync.ts` with the following content:

```ts title="src/subscribers/algolia-sync.ts"
import {
  SubscriberArgs,
  type SubscriberConfig,
} from "@medusajs/framework"
import { syncProductsWorkflow } from "../workflows/sync-products"

export default async function algoliaSyncHandler({ 
  container,
}: SubscriberArgs) {
  const logger = container.resolve("logger")
  
  let hasMore = true
  let offset = 0
  const limit = 50
  let totalIndexed = 0

  logger.info("Starting product indexing...")

  while (hasMore) {
    const { result: { products, metadata } } = await syncProductsWorkflow(container)
      .run({
        input: {
          limit,
          offset,
        },
      })

    hasMore = offset + limit < (metadata?.count ?? 0)
    offset += limit
    totalIndexed += products.length
  }

  logger.info(`Successfully indexed ${totalIndexed} products`)
}

export const config: SubscriberConfig = {
  event: "algolia.sync",
}
```

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 the subscriber listens to, which is `algolia.sync` in this case.

The subscriber function receives an object as a parameter that has a `container` property, which is the Medusa container.

In the subscriber function, you initialize variables to keep track of the pagination and the total number of products indexed.

Then, you start a loop that retrieves products in batches of 50 and indexes them in Algolia using the `syncProductsWorkflow`. Finally, you log the total number of products indexed.

You'll learn how to emit the `algolia.sync` event next.

If you want to sync other data types, you can do it in this subscriber as well.

### Create API Route to Trigger Sync

To allow the Medusa Admin dashboard to trigger the `algolia.sync` event, you need to create an API route that emits the event.

An API Route is an endpoint that exposes commerce features to external applications and clients, such as storefronts.

Learn more about API routes in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/index.html.md).

An API route is created in a `route.ts` file under a sub-directory of the `src/api` directory. The path of the API route is the file's path relative to `src/api`.

So, to create an API route at the path `/admin/algolia/sync`, create the file `src/api/admin/algolia/sync/route.ts` with the following content:

```ts title="src/api/admin/algolia/sync/route.ts"
import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http"
import { Modules } from "@medusajs/framework/utils"

export async function POST(
  req: MedusaRequest,
  res: MedusaResponse
) {
  const eventModuleService = req.scope.resolve(Modules.EVENT_BUS)
  await eventModuleService.emit({
    name: "algolia.sync",
    data: {},
  })
  res.send({
    message: "Syncing data to Algolia",
  })
}
```

Since you export a `POST` route handler function, you expose an `API` route at `/admin/algolia/sync`. The route handler function accepts two parameters:

1. A request object with details and context on the request, such as body parameters or authenticated user details.
2. A response object to manipulate and send the response.

In the route handler, you use the Medusa container that is available in the request object to resolve the [Event Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/infrastructure-modules/event/index.html.md). This module manages events and their subscribers.

Then, you emit the `algolia.sync` event using the Event Module's `emit` method, passing it the event name.

Finally, you send a response with a message indicating that data is being synced to Algolia.

### Add Algolia Sync Page to Admin Dashboard

The last step is to add a new page to the admin dashboard that allows the admin to trigger the reindex. You add a new page using a [UI Route](https://docs.medusajs.com/docs/learn/fundamentals/admin/ui-routes/index.html.md).

A UI route is a React component that specifies the content to be shown in a new page in the Medusa Admin dashboard. You'll create a UI route to display a button that triggers the reindex when clicked.

Learn more about UI routes in the [UI Routes documentation](https://docs.medusajs.com/docs/learn/fundamentals/admin/ui-routes/index.html.md).

#### Configure JS SDK

Before creating the UI route, you'll configure Medusa's [JS SDK](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/js-sdk/index.html.md) that you can use to send requests to the Medusa server from any client application, including your Medusa Admin customizations.

The JS SDK is installed by default in your Medusa application. To configure it, create the file `src/admin/lib/sdk.ts` with the following content:

```ts title="src/admin/lib/sdk.ts"
import Medusa from "@medusajs/js-sdk"

export const sdk = new Medusa({
  baseUrl: "http://localhost:9000",
  debug: process.env.NODE_ENV === "development",
  auth: {
    type: "session",
  },
})
```

You create an instance of the JS SDK using the `Medusa` class from the JS SDK. You pass it an object having the following properties:

- `baseUrl`: The base URL of the Medusa server.
- `debug`: A boolean indicating whether to log debug information into the console.
- `auth`: An object specifying the authentication type. When using the JS SDK for admin customizations, you use the `session` authentication type.

#### Create UI Route

You'll now create the UI route that displays a button to trigger the reindex. You create a UI route in a `page.tsx` file under a sub-directory of `src/admin/routes` directory. The file's path relative to `src/admin/routes` determines its path in the dashboard.

So, to create a new page under the Settings section of the Medusa Admin, create the file `src/admin/routes/settings/algolia/page.tsx` with the following content:

```tsx title="src/admin/routes/settings/algolia/page.tsx"
import { Container, Heading, Button, toast } from "@medusajs/ui"
import { useMutation } from "@tanstack/react-query"
import { sdk } from "../../../lib/sdk"
import { defineRouteConfig } from "@medusajs/admin-sdk"

const AlgoliaPage = () => {
  const { mutate, isPending } = useMutation({
    mutationFn: () => 
      sdk.client.fetch("/admin/algolia/sync", {
        method: "POST",
      }),
    onSuccess: () => {
      toast.success("Successfully triggered data sync to Algolia") 
    },
    onError: (err) => {
      console.error(err)
      toast.error("Failed to sync data to Algolia") 
    },
  })

  const handleSync = () => {
    mutate()
  }

  return (
    <Container className="divide-y p-0">
      <div className="flex items-center justify-between px-6 py-4">
        <Heading level="h2">Algolia Sync</Heading>
      </div>
      <div className="px-6 py-8">
        <Button 
          variant="primary"
          onClick={handleSync}
          isLoading={isPending}
        >
          Sync Data to Algolia
        </Button>
      </div>
    </Container>
  )
}

export const config = defineRouteConfig({
  label: "Algolia",
})

export default AlgoliaPage
```

A UI route's file must export:

1. A React component that defines the content of the page.
2. A configuration object that specifies the route's label in the dashboard. This label is used to show a sidebar item for the new route.

In the React component, you use `useMutation` hook from `@tanstack/react-query` to create a mutation that sends a `POST` request to the API route you created earlier. In the mutation function, you use the JS SDK to send the request.

Then, in the return statement, you display a button that triggers the mutation when clicked, which sends a request to the API route you created earlier.

### Test it Out

You'll now test out the entire flow, starting from triggering the reindex manually from the Medusa Admin dashboard, to checking the Algolia dashboard for the indexed products.

Run the following command to start the Medusa application:

```bash npm2yarn
npm run dev
```

Then, open the Medusa Admin at `http://localhost:9000/app` and log in with the credentials you set up in the first step.

Can't remember the credentials? Learn how to create a user in the [Medusa CLI reference](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/medusa-cli/commands/user/index.html.md).

After you log in, go to Settings from the sidebar. You'll find in the Settings' sidebar a new "Algolia" item. If you click on it, you'll find the page you created with the button to sync products to Algolia.

If you click on the button, the products will be synced to Algolia.

![The Algolia Sync page in the Medusa Admin dashboard with a button to sync products to Algolia](https://res.cloudinary.com/dza7lstvk/image/upload/v1742820813/Medusa%20Resources/Screenshot_2025-03-24_at_2.52.31_PM_eiegzb.png)

You can check that the sync ran and was completed by checking the Medusa logs in the terminal where you started the Medusa application. You should find the following messages:

```bash
info:    Processing algolia.sync which has 1 subscribers
info:    Starting product indexing...
info:    Successfully indexed 4 products
```

These messages indicate that the `algolia.sync` event was emitted, which triggered the subscriber you created to sync the products using the `syncProductsWorkflow`.

Finally, you can check the Algolia dashboard to see the indexed products. Go to Search -> Index, and check the records of the index you set up in the Algolia Module's options (`products`, for example).

![The Algolia dashboard showing the indexed products](https://res.cloudinary.com/dza7lstvk/image/upload/v1742821034/Medusa%20Resources/Screenshot_2025-03-24_at_2.56.38_PM_mtojrv.png)

***

## Step 5: Update Index on Product Changes

You'll now automate the indexing of the products whenever a change occurs. That includes when a product is created, updated, or deleted.

Similar to before, you'll create subscribers to listen to these events.

### Handle Create and Update Products

The action to perform when a product is created or updated is the same. You'll use the `syncProductsWorkflow` to sync the product to Algolia.

So, you only need one subscriber to handle these two events. To create the subscriber, create the file `src/subscribers/product-sync.ts` with the following content:

```ts title="src/subscribers/product-sync.ts"
import { SubscriberArgs, type SubscriberConfig } from "@medusajs/framework"
import { syncProductsWorkflow } from "../workflows/sync-products"

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

export const config: SubscriberConfig = {
  event: ["product.created", "product.updated"],
}
```

The subscriber listens to the `product.created` and `product.updated` events. When either of these events is emitted, the subscriber triggers the `syncProductsWorkflow` to sync the product to Algolia.

When the `product.created` and `product.updated` events are emitted, the product's ID is passed in the event data payload, which you can access in the `event.data` property of the subscriber function's parameter.

So, you pass the product's ID to the `syncProductsWorkflow` as a filter to retrieve only the product that was created or updated.

#### Test it Out

To test it out, start the Medusa application:

```bash npm2yarn
npm run dev
```

Then, either create a product or update an existing one using the Medusa Admin dashboard. If you check the Algolia dashboard, you'll find that the product was created or updated.

### Handle Product Deletion

When a product is deleted, you need to remove it from the Algolia index. As this requires a different action than creating or updating a product, you'll create a new workflow that deletes the product from Algolia, then create a subscriber that listens to the `product.deleted` event to trigger the workflow.

#### Create Delete Product Step

The workflow to delete a product from Algolia will have only one step that deletes products by their IDs from Algolia.

So, create the step at `src/workflows/steps/delete-products-from-algolia.ts` with the following content:

```ts title="src/workflows/steps/delete-products-from-algolia.ts"
import {
  createStep,
  StepResponse,
} from "@medusajs/framework/workflows-sdk"
import { ALGOLIA_MODULE } from "../../modules/algolia"

export type DeleteProductsFromAlgoliaWorkflow = {
  ids: string[]
}

export const deleteProductsFromAlgoliaStep = createStep(
  "delete-products-from-algolia-step",
  async (
    { ids }: DeleteProductsFromAlgoliaWorkflow,
    { container }
  ) => {
    const algoliaModuleService = container.resolve(ALGOLIA_MODULE)
    
    const existingRecords = await algoliaModuleService.retrieveFromIndex(
      ids, 
      "product"
    )
    await algoliaModuleService.deleteFromIndex(
      ids,
      "product"
    )

    return new StepResponse(undefined, existingRecords)
  },
  async (existingRecords, { container }) => {
    if (!existingRecords) {
      return
    }
    const algoliaModuleService = container.resolve(ALGOLIA_MODULE)
    
    await algoliaModuleService.indexData(
      existingRecords as unknown as Record<string, unknown>[],
      "product"
    )
  }
)
```

The step receives the IDs of the products to delete as an input.

In the step, you resolve the Algolia Module's service and retrieve the existing records from Algolia. This is useful to revert the deletion if an error occurs.

Then, you delete the products from Algolia and pass the existing records to the compensation function.

In the compensation function, you reindex the existing records if an error occurs.

#### Create Delete Product Workflow

You can now create the workflow that deletes products from Algolia. Create the file `src/workflows/delete-products-from-algolia.ts` with the following content:

```ts title="src/workflows/delete-products-from-algolia.ts"
import { createWorkflow } from "@medusajs/framework/workflows-sdk"
import { deleteProductsFromAlgoliaStep } from "./steps/delete-products-from-algolia"

type DeleteProductsFromAlgoliaWorkflowInput = {
  ids: string[]
}

export const deleteProductsFromAlgoliaWorkflow = createWorkflow(
  "delete-products-from-algolia",
  (input: DeleteProductsFromAlgoliaWorkflowInput) => {
    deleteProductsFromAlgoliaStep(input)
  }
)
```

The workflow receives an object with the IDs of the products to delete. It then executes the `deleteProductsFromAlgoliaStep` to delete the products from Algolia.

#### Create Delete Product Subscriber

Finally, you'll create the subscriber that listens to the `product.deleted` event to trigger the above workflow.

Create the file `src/subscribers/product-delete.ts` with the following content:

```ts title="src/subscribers/product-delete.ts"
import { SubscriberArgs, type SubscriberConfig } from "@medusajs/framework"
import { deleteProductsFromAlgoliaWorkflow } from "../workflows/delete-products-from-algolia"

export default async function handleProductDeleted({
  event: { data },
  container,
}: SubscriberArgs<{ id: string }>) {
  await deleteProductsFromAlgoliaWorkflow(container)
    .run({
      input: {
        ids: [data.id],
      },
    })
}

export const config: SubscriberConfig = {
  event: "product.deleted",
}
```

The subscriber listens to the `product.deleted` event. When the event is emitted, the subscriber triggers the `deleteProductsFromAlgoliaWorkflow`, passing it the ID of the product to delete.

#### Test it Out

To test product deletion, start the Medusa application:

```bash npm2yarn
npm run dev
```

Then, delete a product from the Medusa Admin dashboard. If you check the Algolia dashboard, you'll find that the product was deleted there as well.

***

## Step 6: Add Search API Route

Before customizing the storefront to show the search UI, you'll create an API route in your Medusa application that allows storefronts to search products in Algolia.

While you can implement the search functionality directly in the storefront to interact with Algolia, this approach centralizes your search integration in Medusa, allowing you to change or modify the integration as necessary. You can also rely on the same behavior and results across different storefronts.

To implement the API Route, create the file `src/api/store/products/search/route.ts` with the following content:

```ts title="src/api/store/products/search/route.ts"
import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http"
import { ALGOLIA_MODULE } from "../../../../modules/algolia"
import AlgoliaModuleService from "../../../../modules/algolia/service"
import { z } from "zod"

export const SearchSchema = z.object({
  query: z.string(),
})

type SearchRequest = z.infer<typeof SearchSchema>

export async function POST(
  req: MedusaRequest<SearchRequest>,
  res: MedusaResponse
) {
  const algoliaModuleService: AlgoliaModuleService = req.scope.resolve(ALGOLIA_MODULE)

  const { query } = req.validatedBody

  const results = await algoliaModuleService.search(
    query as string 
  )

  res.json(results)
}
```

You first define a schema with [Zod](https://zod.dev/), a library to define validation schemas. The schema defines the structure of the request body, which in this case is an object with a `query` property of type `string`. Later, you'll use the schema to enforce request body validation.

Then, you expose a `POST` API Route at `/store/search` that searches Algolia using the `search` method you implemented in the Algolia Module's service. You pass to it the query string from the request body.

Finally, you return the search results as it is in the response.

### Add Validation Middleware

To ensure that requests sent to the API route have the required request body parameters, you can use a [middleware](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/middlewares/index.html.md). A middleware is a function executed when a request is sent to an API Route. It's executed before the route handler.

Learn more about middleware in the [Middlewares documentation](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/middlewares/index.html.md).

Middlewares are created in the `src/api/middlewares.ts` file. So create the file `src/api/middlewares.ts` with the following content:

```ts title="src/api/middlewares.ts"
import { 
  defineMiddlewares,
  validateAndTransformBody, 
} from "@medusajs/framework/http"
import { SearchSchema } from "./store/products/search/route"

export default defineMiddlewares({
  routes: [
    {
      matcher: "/store/products/search",
      method: ["POST"],
      middlewares: [
        validateAndTransformBody(SearchSchema),
      ],
    },
  ],
})
```

To export the middlewares, you use the `defineMiddlewares` function. It accepts an object having a `routes` property, whose value is an array of middleware route objects. Each middleware route object has the following properties:

- `matcher`: The path of the route the middleware applies to.
- `method`: The HTTP methods the middleware applies to, which is in this case `POST`.
- `middlewares`: An array of middleware functions to apply to the route. You apply the `validateAndTransformBody` middleware which ensures that a request's body has the required parameters. You pass it the schema you defined earlier in the API route's file.

You can use the search API route now. You'll see it in action as you customize the storefront in the next step.

***

## Step 7: Search Products in Next.js Starter Storefront

The last step is to provide the search functionalities to customers on your storefront. In the first step, you installed the [Next.js Starter Storefront](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/nextjs-starter/index.html.md) along with the Medusa application.

In this step, you'll customize the Next.js Starter Storefront to add the search functionality.

The Next.js Starter Storefront was installed in a separate directory from Medusa. The directory's name is `{your-project}-storefront`.

So, if your Medusa application's directory is `medusa-search`, you can find the storefront by going back to the parent directory and changing to the `medusa-search-storefront` directory:

```bash
cd ../medusa-search-storefront # change based on your project name
```

### Install Algolia Packages

Before adding the implementation of the search functionality, you need to install the Algolia packages necessary to add the search functionality in your storefront.

Run the following command in the directory of your Next.js Starter Storefront:

```bash npm2yarn
npm install algoliasearch react-instantsearch
```

This installs the Algolia Search JavaScript client and the React InstantSearch library, which you'll use to build the search functionality.

### Add Search Client Configuration

Next, you need to configure the search client. Not only do you need to initialize Algolia, but you also need to change the searching mechanism to use your custom API route in the Medusa application instead of Algolia's API directly.

In `src/lib/config.ts`, add the following imports at the top of the file:

```ts title="src/lib/config.ts" badgeLabel="Storefront" badgeColor="blue"
import { 
  liteClient as algoliasearch, 
  LiteClient as SearchClient,
} from "algoliasearch/lite"
```

Then, add the following at the end of the file:

```ts title="src/lib/config.ts" badgeLabel="Storefront" badgeColor="blue"
export const searchClient: SearchClient = {
  ...(algoliasearch(
    process.env.NEXT_PUBLIC_ALGOLIA_APP_ID || "", 
    process.env.NEXT_PUBLIC_ALGOLIA_API_KEY || ""
  )),
  search: async (params) => {
    const request = Array.isArray(params) ? params[0] : params
    const query = "params" in request ? request.params?.query : 
      "query" in request ? request.query : ""

    if (!query) {
      return {
        results: [
          {
            hits: [],
            nbHits: 0,
            nbPages: 0,
            page: 0,
            hitsPerPage: 0,
            processingTimeMS: 0,
            query: "",
            params: "",
          },
        ],
      }
    }

    return await sdk.client.fetch(`/store/products/search`, {
      method: "POST",
      body: {
        query,
      },
    })
  },
}
```

In the code above, you create a `searchClient` object that initializes the Algolia client with your Algolia App ID and API Key.

You also define a `search` method that sends a `POST` request to the search API route you created in the Medusa application. You use the JS SDK (which is initialized in this same file) to send the request.

### Set Environment Variables

In the storefront's `.env.local` file, add the following Algolia-related environment variables:

```plain badgeLabel="Storefront" badgeColor="blue"
NEXT_PUBLIC_ALGOLIA_APP_ID=your_algolia_app_id
NEXT_PUBLIC_ALGOLIA_API_KEY=your_algolia_api_key
NEXT_PUBLIC_ALGOLIA_PRODUCT_INDEX_NAME=your-products-index-name
```

Where:

- `your_algolia_app_id` is your Algolia App ID, as explained in the [Add Environment Variables section](#add-environment-variables) earlier.
- `your_algolia_api_key` is your Algolia Search API key. You can retrieve it from the [same API keys page on the Algolia dashboard](#add-environment-variables) that you retrieved the Admin API key from.
- `your-products-index-name` is the name of the index you created in Algolia to store the products, which you can retrieve as explained in the [Add Environment Variables section](#add-environment-variables) earlier. You'll use this variable later.

Do not expose your Admin API key in the storefront. The Admin API key should only be used in the Medusa application to interact with Algolia, as it has full access to your Algolia account.

### Add Search Modal Component

You'll now add a search modal component that customers can use to search for products. The search modal will display the search results in real-time as the customer types in the search query.

Later, you'll add the search modal to the navigation bar, allowing customers to open the search modal from any page.

Create the file `src/modules/search/components/modal/index.tsx` with the following content:

```tsx title="src/modules/search/components/modal/index.tsx" badgeLabel="Storefront" badgeColor="blue"
"use client"

import React, { useEffect, useState } from "react"
import { Hits, InstantSearch, SearchBox } from "react-instantsearch"
import { searchClient } from "../../../../lib/config"
import Modal from "../../../common/components/modal"
import { Button } from "@medusajs/ui"
import Image from "next/image"
import Link from "next/link"
import { usePathname } from "next/navigation"

type Hit = {
  objectID: string;
  id: string;
  title: string;
  description: string;
  handle: string;
  thumbnail: string;
}

export default function SearchModal() {
  const [isOpen, setIsOpen] = useState(false)
  const pathname = usePathname()

  useEffect(() => {
    setIsOpen(false)
  }, [pathname])

  return (
    <>
      <div className="hidden small:flex items-center gap-x-6 h-full">
        <Button 
          onClick={() => setIsOpen(true)} 
          variant="transparent"
          className="hover:text-ui-fg-base text-small-regular px-0 hover:bg-transparent focus:!bg-transparent"
        >
          Search
        </Button>
      </div>
      <Modal isOpen={isOpen} close={() => setIsOpen(false)}>
        <InstantSearch 
          searchClient={searchClient} 
          indexName={process.env.NEXT_PUBLIC_ALGOLIA_PRODUCT_INDEX_NAME}
        >
          <SearchBox className="w-full [&_input]:w-[94%] [&_input]:outline-none [&_button]:w-[3%]" />
          <Hits hitComponent={Hit} />
        </InstantSearch>
      </Modal>
    </>
  )
}

const Hit = ({ hit }: { hit: Hit }) => {
  return (
    <div className="flex flex-row gap-x-2 mt-4 relative">
      <Image src={hit.thumbnail} alt={hit.title} width={100} height={100} />
      <div className="flex flex-col gap-y-1">
        <h3>{hit.title}</h3>
        <p className="text-sm text-gray-500">{hit.description}</p>
      </div>
      <Link href={`/products/${hit.handle}`} className="absolute right-0 top-0 w-full h-full" aria-label={`View Product: ${hit.title}`} />
    </div>
  )
}
```

You create a `SearchModal` component that displays a search box and the search results using widgets from Algolia's `react-instantsearch` library.

To display each result item (or hit), you create a `Hit` component that displays the product's title, description, and thumbnail. You also add a link to the product's page.

Finally, you show the search modal when the customer clicks a "Search" button, which you'll add to the navigation bar next.

### Add Search Button to Navigation Bar

The last step is to show the search button in the navigation bar.

In `src/modules/layout/templates/nav/index.tsx`, add the following imports at the top of the file:

```tsx title="src/modules/layout/templates/nav/index.tsx" badgeLabel="Storefront" badgeColor="blue"
import SearchModal from "@modules/search/components/modal"
```

Then, in the return statement of the `Nav` component, add the `SearchModal` component before the `div` surrounding the "Account" link:

```tsx title="src/modules/layout/templates/nav/index.tsx" badgeLabel="Storefront" badgeColor="blue"
<SearchModal />
```

The search button will now appear in the navigation bar before the Account link.

### Test it Out

To test out the storefront changes and the search API route, start the Medusa application:

```bash npm2yarn
npm run dev
```

Then, start the Next.js Starter Storefront from its directory:

```bash npm2yarn
npm run dev
```

Next, go to `localhost:8000`. You'll find a Search button at the top right of the navigation bar. If you click on it, you can search through your products. You can also click on a product to view its page.

![The Next.js Starter Storefront showing the search modal with search results](https://res.cloudinary.com/dza7lstvk/image/upload/v1742827777/Medusa%20Resources/Screenshot_2025-03-24_at_4.49.23_PM_kzhldx.png)

***

## Next Steps

You've now integrated Algolia with Medusa and added search functionality to your storefront. You can expand on these features to:

- Add filters to the search results. You can do that using Algolia's [widgets](https://www.algolia.com/doc/guides/building-search-ui/widgets/showcase/react/) and customizing the search API route in Medusa to accept filter parameters.
- Support indexing other data types, such as product categories. You can create the subscribers and workflows for categories similar to products.

If you're new to Medusa, check out the [main documentation](https://docs.medusajs.com/docs/learn/index.html.md), 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](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/index.html.md).


# Implement Localization in Medusa by Integrating Contentful

In this tutorial, you'll learn how to localize your Medusa store's data with Contentful.

When you install a Medusa application, you get a fully-fledged commerce platform with a Framework for customization. While Medusa provides features essential for internationalization, such as support for multiple [regions](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/region/index.html.md) and [currencies](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/currency/index.html.md), it doesn't provide content localization.

However, Medusa's architecture supports the integration of third-party services to provide additional features, such as data localization. One service you can integrate is [Contentful](https://www.contentful.com/), a headless content management system (CMS) that allows you to manage and deliver content across multiple channels.

## Summary

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

- Install and set up Medusa.
- Integrate Contentful with Medusa.
- Create content types in Contentful for Medusa models.
- Trigger syncing products and related data to Contentful when:
  - A product is created.
  - The admin user triggers syncing the products.
- Customize the [Next.js Starter Storefront](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/nextjs-starter/index.html.md) to fetch localized data from Contentful through Medusa.
- Listen to webhook events in Contentful to update Medusa's data accordingly.

You can follow this tutorial whether you're new to Medusa or an advanced Medusa developer.

![Diagram illustrating the integration of Contentful with Medusa](https://res.cloudinary.com/dza7lstvk/image/upload/v1744791908/Medusa%20Resources/contentful-summary_j5cpdx.jpg)

- [Tutorial Repository](https://github.com/medusajs/examples/tree/main/localization-contentful): Find the full code for this guide in this repository.
- [OpenApi Specs for Postman](https://res.cloudinary.com/dza7lstvk/raw/upload/v1744790686/OpenApi/Contentful_jysc07.yaml): Import this OpenApi Specs file into tools like Postman.

***

## Step 1: Install a Medusa Application

### Prerequisites

- [Node.js v20+](https://nodejs.org/en/download)
- [Git CLI tool](https://git-scm.com/downloads)
- [PostgreSQL](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](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/nextjs-starter/index.html.md), choose "Yes."

Afterwards, the installation process will start, which will install the Medusa application in a directory with your project's name and the Next.js Starter Storefront in a separate directory named `{project-name}-storefront`.

The Medusa application is composed of a headless Node.js server and an admin dashboard. The storefront is installed or custom-built separately and connects to the Medusa application through its REST endpoints, called [API routes](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/index.html.md). Learn more in [Medusa's Architecture documentation](https://docs.medusajs.com/docs/learn/introduction/architecture/index.html.md).

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.

Check out the [troubleshooting guides](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/troubleshooting/create-medusa-app-errors/index.html.md) for help.

***

## Step 2: Create Contentful Module

To integrate third-party services into Medusa, you create a module. A [module](https://docs.medusajs.com/docs/learn/fundamentals/modules/index.html.md) is a reusable package that provides functionalities related to a single feature or domain. Medusa integrates the module into your application without implications or side effects on your setup.

In this step, you'll create a module that provides the necessary functionalities to integrate Contentful with Medusa.

Refer to the [Modules](https://docs.medusajs.com/docs/learn/fundamentals/modules/index.html.md) documentation to learn more about modules and their structure.

### Install Contentful SDKs

Before building the module, you need to install Contentful's management and delivery JS SDKs. So, run the following command in the Medusa application's directory:

```bash npm2yarn
npm install contentful contentful-management
```

Where `contentful` is the delivery SDK and `contentful-management` is the management SDK.

### Create Module Directory

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

### Create Loader

When the Medusa application starts, you want to establish a connection to Contentful, then create the necessary content types if they don't exist in Contentful.

A module can specify a task to run on the Medusa application's startup using [loaders](https://docs.medusajs.com/docs/learn/fundamentals/modules/loaders/index.html.md). A loader is an asynchronous function that a module exports. Then, when the Medusa application starts, it runs the loader. The loader can be used to perform one-time tasks such as connecting to a database, creating content types, or initializing data.

Refer to the [Loaders](https://docs.medusajs.com/docs/learn/fundamentals/modules/loaders/index.html.md) documentation to learn more about how loaders work and when to use them.

Loaders are created in a TypeScript or JavaScript file under the `loaders` directory of a module. So, create the file `src/modules/contentful/loader/create-content-models.ts` with the following content:

```ts title="src/modules/contentful/loader/create-content-models.ts" highlights={loaderHighlights}
import { LoaderOptions } from "@medusajs/framework/types"
import { asValue } from "awilix"
import { createClient } from "contentful-management"
import { MedusaError } from "@medusajs/framework/utils"

const { createClient: createDeliveryClient } = require("contentful")

export type ModuleOptions = {
  management_access_token: string
  delivery_token: string
  space_id: string
  environment: string
  default_locale?: string
}

export default async function syncContentModelsLoader({
  container,
  options,
}: LoaderOptions<ModuleOptions>) {
  if (
    !options?.management_access_token || !options?.delivery_token || 
    !options?.space_id || !options?.environment
  ) {
    throw new MedusaError(
      MedusaError.Types.INVALID_DATA,
      "Contentful access token, space ID and environment are required"
    )
  }

  const logger = container.resolve("logger")

  try {
    const managementClient = createClient({
      accessToken: options.management_access_token,
    }, {
      type: "plain",
      defaults: {
        spaceId: options.space_id,
        environmentId: options.environment,
      },
    })

    const deliveryClient = createDeliveryClient({
      accessToken: options.delivery_token,
      space: options.space_id,
      environment: options.environment,
    })


    // TODO try to create content types

  } catch (error) {
    logger.error(
      `Failed to connect to Contentful: ${error}`
    )
    throw error
  }
}
```

The loader file exports an asynchronous function that accepts an object having the following properties:

- `container`: The [Module container](https://docs.medusajs.com/docs/learn/fundamentals/modules/container/index.html.md), which is a registry of resources available to the module. You can use it to resolve or register resources in the module's container.
- `options`: An object of options passed to the module. These options are useful to pass secrets or options that may change per environment. You'll learn how to pass these options later.
  - The Contentful Module expects the options to include the Contentful tokens for the management and delivery APIs, the space ID, environment, and optionally the default locale to use.

In the loader function, you validate the options passed to the module, and throw an error if they're invalid. Then, you resolve from the Module's container the [Logger](https://docs.medusajs.com/docs/learn/debugging-and-testing/logging/index.html.md) used to log messages in the terminal.

Finally, you create clients for Contentful's management and delivery APIs, passing them the necessary module's options. If the connection fails, an error is thrown, which is handled in the `catch` block.

#### Create Content Types

In the loader, you need to create content types in Contentful if they don't already exist.

In this tutorial, you'll only create content types for a product and its variants and options. However, you can create content types for other data models, such as categories or collections, by following the same approach.

You can learn more about the product-related data models, which the content types are based on, in the [Product Module's Data Models](https://docs.medusajs.com/references/product/models/index.html.md) reference.

To create the content type for products, replace the `TODO` in the loader with the following:

```ts title="src/modules/contentful/loader/create-content-models.ts"
// Try to create the product content type
try {
  await managementClient.contentType.get({
    contentTypeId: "product",
  })
} catch (error) {
  const productContentType = await managementClient.contentType.createWithId({
    contentTypeId: "product",
  }, {
    name: "Product",
    description: "Product content type synced from Medusa",
    displayField: "title",
    fields: [
      {
        id: "title", 
        name: "Title",
        type: "Symbol",
        required: true,
        localized: true,
      },
      {
        id: "handle",
        name: "Handle", 
        type: "Symbol",
        required: true,
        localized: false,
      },
      {
        id: "medusaId",
        name: "Medusa ID",
        type: "Symbol",
        required: true,
        localized: false,
      },
      {
        type: "RichText",
        name: "description", 
        id: "description",
        validations: [
          {
            enabledMarks: [
              "bold",
              "italic",
              "underline", 
              "code",
              "superscript",
              "subscript",
              "strikethrough",
            ],
          },
          {
            enabledNodeTypes: [
              "heading-1",
              "heading-2", 
              "heading-3",
              "heading-4",
              "heading-5",
              "heading-6",
              "ordered-list",
              "unordered-list",
              "hr",
              "blockquote",
              "embedded-entry-block",
              "embedded-asset-block",
              "table",
              "asset-hyperlink",
              "embedded-entry-inline",
              "entry-hyperlink",
              "hyperlink",
            ],
          },
          {
            nodes: {},
          },
        ],
        localized: true,
        required: true,
      },
      {
        type: "Symbol",
        name: "subtitle",
        id: "subtitle",
        localized: true,
        required: false,
        validations: [],
      },
      {
        type: "Array",
        items: {
          type: "Link",
          linkType: "Asset",
          validations: [],
        },
        name: "images",
        id: "images",
        localized: true,
        required: false,
        validations: [],
      },
      {
        id: "productVariants",
        name: "Product Variants",
        type: "Array",
        localized: false,
        required: false,
        items: {
          type: "Link",
          validations: [
            {
              linkContentType: ["productVariant"],
            },
          ],
          linkType: "Entry",
        },
        disabled: false,
        omitted: false,
      },
      {
        id: "productOptions",
        name: "Product Options",
        type: "Array",
        localized: false,
        required: false,
        items: {
          type: "Link",
          validations: [
            {
              linkContentType: ["productOption"],
            },
          ],
          linkType: "Entry",
        },
        disabled: false,
        omitted: false,
      },
    ],
  })

  await managementClient.contentType.publish({
    contentTypeId: "product",
  }, productContentType)
}

// TODO create product variant content type
```

In the above snippet, you first try to retrieve the product content type using Contentful's Management APIs. If the content type doesn't exist, an error is thrown, which you handle in the `catch` block.

In the `catch` block, you create the product content type with the following fields:

- `title`: The product's title, which is a localized field.
- `handle`: The product's handle, which is used to create a human-readable URL for the product in the storefront.
- `medusaId`: The product's ID in Medusa, which is a non-localized field. You'll store in this field the ID of the product in Medusa.
- `description`: The product's description, which is a localized rich-text field.
- `subtitle`: The product's subtitle, which is a localized field.
- `images`: The product's images, which is a localized array of assets in Contentful.
- `productVariants`: The product's variants, which is an array that references content of the `productVariant` content type.
- `productOptions`: The product's options, which is an array that references content of the `productOption` content type.

Next, you'll create the `productVariant` content type that represents a product's variant. A variant is a combination of the product's options that customers can purchase. For example, a "red" shirt is a variant whose color option is `red`.

To create the variant content type, replace the new `TODO` with the following:

```ts title="src/modules/contentful/loader/create-content-models.ts"
// Try to create the product variant content type
try {
  await managementClient.contentType.get({
    contentTypeId: "productVariant",
  })
} catch (error) {
  const productVariantContentType = await managementClient.contentType.createWithId({
    contentTypeId: "productVariant",
  }, {
  name: "Product Variant",
  description: "Product variant content type synced from Medusa",
  displayField: "title",
  fields: [
    {
      id: "title",
      name: "Title",
      type: "Symbol",
      required: true,
      localized: true,
    },
    {
      id: "product",
      name: "Product",
      type: "Link",
      required: true,
      localized: false,
      validations: [
        {
          linkContentType: ["product"],
        },
      ],
      disabled: false,
      omitted: false,
      linkType: "Entry",
    },
    {
      id: "medusaId",
      name: "Medusa ID",
      type: "Symbol",
      required: true,
      localized: false,
    },
    {
      id: "productOptionValues",
      name: "Product Option Values",
      type: "Array",
      localized: false,
      required: false,
      items: {
        type: "Link",
        validations: [
          {
            linkContentType: ["productOptionValue"],
          },
        ],
        linkType: "Entry",
      },
      disabled: false,
      omitted: false,
      },
    ],
  })

  await managementClient.contentType.publish({
    contentTypeId: "productVariant",
  }, productVariantContentType)
}

// TODO create product option content type
```

In the above snippet, you create the `productVariant` content type with the following fields:

- `title`: The product variant's title, which is a localized field.
- `product`: References the `product` content type, which is the product that the variant belongs to.
- `medusaId`: The product variant's ID in Medusa, which is a non-localized field. You'll store in this field the ID of the variant in Medusa.
- `productOptionValues`: The product variant's option values, which is an array that references content of the `productOptionValue` content type.

Then, you'll create the `productOption` content type that represents a product's option, like size or color. Replace the new `TODO` with the following:

```ts title="src/modules/contentful/loader/create-content-models.ts"
// Try to create the product option content type
try {
  await managementClient.contentType.get({
    contentTypeId: "productOption",
  })
} catch (error) {
  const productOptionContentType = await managementClient.contentType.createWithId({
    contentTypeId: "productOption",
  }, {
    name: "Product Option",
    description: "Product option content type synced from Medusa",
    displayField: "title",
    fields: [
    {
      id: "title",
      name: "Title",
      type: "Symbol",
      required: true,
      localized: true,
    },
    {
      id: "product",
      name: "Product",
      type: "Link",
      required: true,
      localized: false,
      validations: [
        {
          linkContentType: ["product"],
        },
      ],
      disabled: false,
      omitted: false,
      linkType: "Entry",
    },
    {
      id: "medusaId",
      name: "Medusa ID",
      type: "Symbol",
      required: true,
      localized: false,
    },
    {
      id: "values",
      name: "Values",
      type: "Array",
      required: false,
      localized: false,
      items: {
        type: "Link",
        validations: [
          {
            linkContentType: ["productOptionValue"],
          },
        ],
        linkType: "Entry",
      },
      disabled: false,
      omitted: false,
      },
    ],
  })

  await managementClient.contentType.publish({
    contentTypeId: "productOption",
  }, productOptionContentType)
}

// TODO create product option value content type
```

In the above snippet, you create the `productOption` content type with the following fields:

- `title`: The product option's title, which is a localized field.
- `product`: References the `product` content type, which is the product that the option belongs to.
- `medusaId`: The product option's ID in Medusa, which is a non-localized field. You'll store in this field the ID of the option in Medusa.
- `values`: The product option's values, which is an array that references content of the `productOptionValue` content type.

Finally, you'll create the `productOptionValue` content type that represents a product's option value, like "red" or "blue" for the color option. A variant references option values.

To create the option value content type, replace the new `TODO` with the following:

```ts title="src/modules/contentful/loader/create-content-models.ts"
// Try to create the product option value content type
try {
  await managementClient.contentType.get({
    contentTypeId: "productOptionValue",
  })
} catch (error) {
  const productOptionValueContentType = await managementClient.contentType.createWithId({
    contentTypeId: "productOptionValue",
  }, {
    name: "Product Option Value",
    description: "Product option value content type synced from Medusa",
    displayField: "value",
    fields: [
    {
      id: "value",
      name: "Value",
      type: "Symbol",
      required: true,
      localized: true,
    },
    {
      id: "medusaId",
      name: "Medusa ID",
      type: "Symbol",
      required: true,
      localized: false,
    },
  ],
})

await managementClient.contentType.publish({
    contentTypeId: "productOptionValue",
  }, productOptionValueContentType)
}

// TODO register clients in container
```

In the above snippet, you create the `productOptionValue` content type with the following fields:

- `value`: The product option value, which is a localized field.
- `medusaId`: The product option value's ID in Medusa, which is a non-localized field. You'll store in this field the ID of the option value in Medusa.

You've now created all the necessary content types to localize products.

### Register Clients in the Container

The last step in the loader is to register the Contentful management and delivery clients in the module's container. This will allow you to resolve and use them in the module's service, which you'll create next.

To register resources in the container, you can use its `register` method, which accepts an object containing key-value pairs. The keys are the names of the resources in the container, and the values are the resources themselves.

To register the management and delivery clients, replace the last `TODO` in the loader with the following:

```ts title="src/modules/contentful/loader/create-content-models.ts"
container.register({
  contentfulManagementClient: asValue(managementClient),
  contentfulDeliveryClient: asValue(deliveryClient),
})

logger.info("Connected to Contentful")
```

Now, you can resolve the management and delivery clients from the module's container using the keys `contentfulManagementClient` and `contentfulDeliveryClient`, respectively.

### Create Service

You define a module's functionality 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 perform actions with a third-party service.

In this section, you'll create the Contentful Module's service that can be used to retrieve content from Contentful, create content, and more.

To create the service, create the file `src/modules/contenful/service.ts` with the following content:

```ts title="src/modules/contentful/service.ts" highlights={serviceHighlights}
import { ModuleOptions } from "./loader/create-content-models"
import { PlainClientAPI } from "contentful-management"

type InjectedDependencies = {
  contentfulManagementClient: PlainClientAPI;
  contentfulDeliveryClient: any;
}

export default class ContentfulModuleService {
  private managementClient: PlainClientAPI
  private deliveryClient: any
  private options: ModuleOptions

  constructor(
    { 
      contentfulManagementClient, 
      contentfulDeliveryClient,
    }: InjectedDependencies, 
    options: ModuleOptions
  ) {
    this.managementClient = contentfulManagementClient
    this.deliveryClient = contentfulDeliveryClient
    this.options = {
      ...options,
      default_locale: options.default_locale || "en-US",
    }
  }

  // TODO add methods
}
```

You export a class that will be the Contentful Module's main service. In the class, you define properties for the Contentful clients and options passed to the module.

You also add a constructor to the class. A service's constructor accepts the following params:

1. The module's container, which you can use to resolve resources. You use it to resolve the Contentful clients you previously registered in the loader.
2. The options passed to the module.

In the constructor, you assign the clients and options to the class properties. You also set the default locale to `en-US` if it's not provided in the module's options.

Since the loader is executed on application start-up, if an error occurs while connecting to Contentful, the module will not be registered and the service will not be executed. So, in the service, you're guaranteed that the clients are registered in the container and have successful connection to Contentful.

As you implement the syncing and content retrieval features later, you'll add the necessary methods for them.

### Export Module Definition

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 name of the module, its service, and optionally its loaders.

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

```ts title="src/modules/contentful/index.ts" highlights={moduleHighlights}
import { Module } from "@medusajs/framework/utils"
import ContentfulModuleService from "./service"
import createContentModelsLoader from "./loader/create-content-models"

export const CONTENTFUL_MODULE = "contentful"

export default Module(CONTENTFUL_MODULE, {
  service: ContentfulModuleService,
  loaders: [
    createContentModelsLoader,
  ],
})
```

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

1. The module's name, which is `contentful`.
2. An object with a required property `service` indicating the module's service. You also pass the loader you created to ensure it's executed when the application starts.

Aside from the module definition, you export the module's name as `CONTENTFUL_MODULE` so you can reference it later.

### Add Module 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 and pass an array with your custom module:

```ts title="medusa-config.ts"
module.exports = defineConfig({
  // ...
  modules: [
    {
      resolve: "./src/modules/contentful",
      options: {
        management_access_token: process.env.CONTENTFUL_MANAGEMNT_ACCESS_TOKEN,
        delivery_token: process.env.CONTENTFUL_DELIVERY_TOKEN,
        space_id: process.env.CONTENTFUL_SPACE_ID,
        environment: process.env.CONTENTFUL_ENVIRONMENT,
        default_locale: "en-US",
      },
    },
  ],
})
```

Each object in the `modules` array has a `resolve` property, whose value is either a path to the module's directory, or an `npm` package's name.

You also pass an `options` property with the module's options, including the Contentful's tokens for the management and delivery APIs, the Contentful's space ID, environment, and default locale.

### Note about Locales

By default, your Contentful space will have one locale (for example, `en-US`). You can add locales as explained in the [Contentful documentation](https://www.contentful.com/help/localization/manage-locales/).

When you add a locale, make sure to:

- Set the fallback locale to the default locale (for example, `en-US`). This ensure that values are retrieved in the default locale if values for the requested locale are not available.
- Allow the required fields to be empty for the locale. Otherwise, you'll have to specify the values for the localized fields in each locale when you create the products later.

![Example of the locale's settings in the Contentful dashboard](https://res.cloudinary.com/dza7lstvk/image/upload/v1744716537/Medusa%20Resources/Screenshot_2025-04-15_at_2.28.46_PM_v50lrm.png)

### Add Environment Variables

Before you can start using the Contentful Module, you need to add the necessary environment variables used in the module's options.

Add the following environment variables to your `.env` file:

```plain
CONTENTFUL_MANAGEMNT_ACCESS_TOKEN=CFPAT-...
CONTENTFUL_DELIVERY_TOKEN=eij...
CONTENTFUL_SPACE_ID=t2a...
CONTENTFUL_ENVIRONMENT=master
```

Where:

- `CONTENTFUL_MANAGEMNT_ACCESS_TOKEN`: The Contentful management API access token. To create it on the Contentful dashboard:
  - Click on the cog icon at the top right, then choose "CMA tokens" from the dropdown.

![The cog icon is at the top right next to the user avatar. If you click on it, a dropdown will show where you can click on "CMA tokens".](https://res.cloudinary.com/dza7lstvk/image/upload/v1744714244/Medusa%20Resources/Screenshot_2025-04-15_at_1.48.35_PM_ct2tfk.png)

- In the CMA tokens page, click on the "Create personal access token" button.
- In the window that pops up, enter a name for the token, and choose an expiry date. Once you're done, click the Generate button.
- The token is generated and shown in the pop-up. Make sure to copy it and use it in the `.env` file, as you can't access it again.

![Click on the copy button to copy the key](https://res.cloudinary.com/dza7lstvk/image/upload/v1744714749/Medusa%20Resources/Screenshot_2025-04-15_at_1.58.12_PM_tuugam.png)

- `CONTENTFUL_DELIVERY_TOKEN`: An API token that you can use with the delivery API. To create it on the Contentful dashboard:
  - Click on the cog icon at the top right, then choose "API keys" from the dropdown.

![The cog icon is at the top right next to the user avatar. If you click on it, a dropdown will show where you can click on "API keys".](https://res.cloudinary.com/dza7lstvk/image/upload/v1744714971/Medusa%20Resources/Screenshot_2025-04-15_at_2.02.31_PM_qfsn1h.png)

- In the APIs page, click on the "Add API key" button.
- In the window that pops up, enter a name for the token, then click the Add API Key button.
- This will create an API key and opens its page. On its page, copy the token for the "Content Delivery API" and use it as the value for `CONTENTFUL_DELIVERY_TOKEN`.

![Copy the API key from the "Content Delivery API - access token" field](https://res.cloudinary.com/dza7lstvk/image/upload/v1744715228/Medusa%20Resources/Screenshot_2025-04-15_at_2.06.44_PM_ifu1mx.png)

- `CONTENTFUL_SPACE_ID`: The ID of your Contentful space. You can copy this from the dashboard's URL which is of the format `https://app.contentful.com/spaces/{space_id}/...`.
- `CONTENTFUL_ENVIRONMENT`: The environment to manage and retrieve the content in. By default, you have the `master` environment which you can use. However, you can use another Contentful environment that you've created.

Your module is now ready for use.

### Test the Module

To test out the module, you'll start the Medusa application, which will run the module's loader.

To start the Medusa application, run the following command:

```bash npm2yarn
npm run dev
```

If the loader ran successfully, you'll see the following message in the terminal:

```bash
info:    Connected to Contentful
```

You can also see on the Contentful dashboard that the content types were created. To view them, go to the Content Model page.

![In the Contentful dashboard, go to the Content Model page](https://res.cloudinary.com/dza7lstvk/image/upload/v1744715783/Medusa%20Resources/Screenshot_2025-04-15_at_2.16.09_PM_w7oszm.png)

***

## Step 3: Create Products in Contentful

Now that you have the Contentful Module ready for use, you can start creating products in Contentful.

In this step, you'll implement the logic to create products in Contentful. Later, you'll execute it when:

- A product is created in Medusa.
- The admin user triggers a sync manually.

### Add Methods to Contentful Module Service

To create products in Contentful, you need to add the necessary methods in the Contentful Module's service. Then, you can use these methods later when building the creation flow.

To create a product in Contentful, you'll need three methods: One to create the product's variants, another to create the product's options and values, and a third to create the product.

In the service at `src/modules/contentful/service.ts`, start by adding the method to create the product's variants:

```ts title="src/modules/contentful/service.ts"
// imports...
import { ProductVariantDTO } from "@medusajs/framework/types"
import { EntryProps } from "contentful-management"

export default class ContentfulModuleService {
  // ...

  private async createProductVariant(
    variants: ProductVariantDTO[],
    productEntry: EntryProps
  ) {
    for (const variant of variants) {
      await this.managementClient.entry.createWithId(
        {
          contentTypeId: "productVariant",
          entryId: variant.id,
        },
        {
          fields: {
            medusaId: {
              [this.options.default_locale!]: variant.id,
            },
            title: {
              [this.options.default_locale!]: variant.title,
            },
            product: {
              [this.options.default_locale!]: {
                sys: {
                  type: "Link",
                  linkType: "Entry",
                  id: productEntry.sys.id,
                },
              },
            },
            productOptionValues: {
              [this.options.default_locale!]: variant.options.map((option) => ({
                sys: {
                  type: "Link",
                  linkType: "Entry",
                  id: option.id,
                },
              })),
            },
          },
        }
      )
    }
  }
}
```

You define a private method `createProductVariant` that accepts two parameters:

1. The product's variants to create in Contentful.
2. The product's entry in Contentful.

In the method, you iterate over the product's variants and create a new entry in Contentful for each variant. You set the fields based on the product variant content type you created earlier.

For each field, you specify the value for the default locale. In the Contentful dashboard, you can manage the values for other locales.

Next, add the method to create the product's options and values:

```ts title="src/modules/contentful/service.ts" highlights={createProductOptionHighlights}
// other imports...
import { ProductOptionDTO } from "@medusajs/framework/types"

export default class ContentfulModuleService {
  // ...
  private async createProductOption(
    options: ProductOptionDTO[],
    productEntry: EntryProps
  ) {
    for (const option of options) {
      const valueIds: {
        sys: {
          type: "Link",
          linkType: "Entry",
          id: string
        }
      }[] = []
      for (const value of option.values) {
        await this.managementClient.entry.createWithId(
          {
            contentTypeId: "productOptionValue",
            entryId: value.id,
          },
          {
            fields: {
              value: {
                [this.options.default_locale!]: value.value,
              },
              medusaId: {
                [this.options.default_locale!]: value.id,
              },
            },
          }
        )
        valueIds.push({
          sys: {
            type: "Link",
            linkType: "Entry",
            id: value.id,
          },
        })
      }
      await this.managementClient.entry.createWithId(
        {
          contentTypeId: "productOption",
          entryId: option.id,
        },
        {
          fields: {
            medusaId: {
              [this.options.default_locale!]: option.id,
            },
            title: {
              [this.options.default_locale!]: option.title,
            },
            product: {
              [this.options.default_locale!]: {
                sys: {
                  type: "Link",
                  linkType: "Entry",
                  id: productEntry.sys.id,
                },
              },
            },
            values: {
              [this.options.default_locale!]: valueIds,
            },
          },
        }
      )
    }
  }
}
```

You define a private method `createProductOption` that accepts two parameters:

1. The product's options, which is an array of objects.
2. The product's entry in Contentful, which is an object.

In the method, you iterate over the product's options and create entries for each of its values. Then, you create an entry for the option, and reference the values you created in Contentful. You set the fields based on the option and value content types you created earlier.

Finally, add the method to create the product:

```ts title="src/modules/contentful/service.ts" highlights={createProductHighlights}
// other imports...
import { ProductDTO } from "@medusajs/framework/types"

export default class ContentfulModuleService {
  // ...
  async createProduct(
    product: ProductDTO
  ) {
    try {
      // check if product already exists
      const productEntry = await this.managementClient.entry.get({
        environmentId: this.options.environment,
        entryId: product.id,
      })
      
      return productEntry
    } catch(e) {}
    
    // Create product entry in Contentful
    const productEntry = await this.managementClient.entry.createWithId(
      {
        contentTypeId: "product",
        entryId: product.id,
      },
      {
        fields: {
          medusaId: {
            [this.options.default_locale!]: product.id,
          },
          title: {
            [this.options.default_locale!]: product.title,
          },
          description: product.description ? {
            [this.options.default_locale!]: {
              nodeType: "document",
              data: {},
              content: [
                {
                  nodeType: "paragraph",
                  data: {},
                  content: [
                    {
                      nodeType: "text",
                      value: product.description,
                      marks: [],
                      data: {},
                    },
                  ],
                },
              ],
            },
          } : undefined,
          subtitle: product.subtitle ? {
            [this.options.default_locale!]: product.subtitle,
          } : undefined,
          handle: product.handle ? {
            [this.options.default_locale!]: product.handle,
          } : undefined,
        },
      }
    )

    // Create options if they exist
    if (product.options?.length) {
      await this.createProductOption(product.options, productEntry)
    }

    // Create variants if they exist
    if (product.variants?.length) {
      await this.createProductVariant(product.variants, productEntry)
    }

    // update product entry with variants and options
    await this.managementClient.entry.update(
      {
        entryId: productEntry.sys.id,
      },
      {
        sys: productEntry.sys,
        fields: {
          ...productEntry.fields,
          productVariants: {
            [this.options.default_locale!]: product.variants?.map((variant) => ({
              sys: {
                type: "Link",
                linkType: "Entry",
                id: variant.id,
              },
            })),
          },
          productOptions: {
            [this.options.default_locale!]: product.options?.map((option) => ({
              sys: {
                type: "Link",
                linkType: "Entry",
                id: option.id,
              },
            })),
          },
        },
      }
    )

    return productEntry
  }
}
```

You define a public method `createProduct` that accepts a product object as a parameter.

In the method, you first check if the product already exists in Contentful. If it does, you return the existing product entry. Otherwise, you create a new product entry with the fields based on the product content type you created earlier.

Next, you create entries for the product's options and variants using the methods you created earlier.

Finally, you update the product entry to reference the variants and options you created.

You now have all the methods to create products in Contentful. You'll also need one last method to delete a product in Contentful. This is useful when you implement the rollback mechanism in the flow that creates the products.

Add the following method to the service:

```ts title="src/modules/contentful/service.ts" highlights={deleteProductHighlights}
// other imports...
import { MedusaError } from "@medusajs/framework/utils"

export default class ContentfulModuleService {
  // ...
  async deleteProduct(productId: string) {
    try {
      // Get the product entry
      const productEntry = await this.managementClient.entry.get({
        environmentId: this.options.environment,
        entryId: productId,
      })

      if (!productEntry) {
        return
      }

      // Delete the product entry
      await this.managementClient.entry.unpublish({
        environmentId: this.options.environment,
        entryId: productId,
      })

      await this.managementClient.entry.delete({
        environmentId: this.options.environment,
        entryId: productId,
      })

      // Delete the product variant entries
      for (const variant of productEntry.fields.productVariants[this.options.default_locale!]) {
        await this.managementClient.entry.unpublish({
          environmentId: this.options.environment,
          entryId: variant.sys.id,
        })

        await this.managementClient.entry.delete({
          environmentId: this.options.environment,
          entryId: variant.sys.id,
        })
      }

      // Delete the product options entries and values
      for (const option of productEntry.fields.productOptions[this.options.default_locale!]) {
        for (const value of option.fields.values[this.options.default_locale!]) {
          await this.managementClient.entry.unpublish({
            environmentId: this.options.environment,
            entryId: value.sys.id,
        })

        await this.managementClient.entry.delete({
          environmentId: this.options.environment,
            entryId: value.sys.id,
          })
        }

        await this.managementClient.entry.unpublish({
          environmentId: this.options.environment,
          entryId: option.sys.id,
        })

        await this.managementClient.entry.delete({
          environmentId: this.options.environment,
          entryId: option.sys.id,
        })
      }
    } catch (error) {
      throw new MedusaError(
        MedusaError.Types.INVALID_DATA,
        `Failed to delete product from Contentful: ${error.message}`
      )
    }
  }
}
```

You define a public method `deleteProduct` that accepts a product ID as a parameter.

In the method, you retrieve the product entry from Contentful with its variants, options, and values. For each entry, you must unpublish and delete it.

You now have all the methods necessary to build the creation flow.

### Create Contentful Product Workflow

To implement the logic that's triggered when a product is created in Medusa, or when the admin user triggers a sync manually, you need to create a workflow.

A [workflow](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md) is a series of 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.

Learn more about workflows in the [Workflows documentation](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md).

In this section, you'll create a workflow that creates Medusa products in Contentful using the Contentful Module.

The workflow has the following steps:

- [useQueryGraphStep](https://docs.medusajs.com/references/helper-steps/useQueryGraphStep/index.html.md): Retrieve products to create in Contentful.
- [createProductsContentfulStep](#createProductsContentfulStep): Create the products in Contentful.

Medusa provides the `useQueryGraphStep` in its `@medusajs/medusa/core-flows` package. So, you only need to implement the second step.

#### createProductsContentfulStep

In the second step, you create the retrieved products in Contentful.

To create the step, create the file `src/workflows/steps/create-products-contentful.ts` with the following content:

```ts title="src/workflows/steps/create-products-contentful.ts" highlights={createProductsContentfulStepHighlights}
import { ProductDTO } from "@medusajs/framework/types"
import { CONTENTFUL_MODULE } from "../../modules/contentful"
import { createStep, StepResponse } from "@medusajs/framework/workflows-sdk"
import ContentfulModuleService from "../../modules/contentful/service"
import { EntryProps } from "contentful-management"

type StepInput = {
  products: ProductDTO[]
}

export const createProductsContentfulStep = createStep(
  "create-products-contentful-step",
  async (input: StepInput, { container }) => {
    const contentfulModuleService: ContentfulModuleService = 
      container.resolve(CONTENTFUL_MODULE)

    const products: EntryProps[] = []

    try {
      for (const product of input.products) {
        products.push(await contentfulModuleService.createProduct(product))
      }
    } catch(e) {
      return StepResponse.permanentFailure(
        `Error creating products in Contentful: ${e.message}`,
        products
      )
    }

    return new StepResponse(
      products,
      products
    )
  },
  async (products, { container }) => {
    if (!products) {
      return
    }

    const contentfulModuleService: ContentfulModuleService = 
      container.resolve(CONTENTFUL_MODULE)

    for (const product of products) {
      await contentfulModuleService.deleteProduct(product.sys.id)
    }
  }
)
```

You create a step with `createStep` from the Workflows SDK. It accepts three parameters:

1. The step's unique name, which is `create-products-contentful-step`.
2. An async function that receives two parameters:
   - The step's input, which is in this case an object holding an array of products to create in Contentful.
   - An object that has properties including the [Medusa container](https://docs.medusajs.com/docs/learn/fundamentals/medusa-container/index.html.md), which is a registry of Framework and commerce tools that you can access in the step.
3. An optional compensation function that undoes the actions performed in the step if an error occurs in the workflow's execution. This mechanism ensures data consistency in your application, especially as you integrate external systems.

The Medusa container is different from the module's container. Since modules are isolated, they each have a container with their resources. Refer to the [Module Container](https://docs.medusajs.com/docs/learn/fundamentals/modules/container/index.html.md) documentation for more information.

In the step function, you resolve the Contentful Module's service from the Medusa container using the name you exported in the module definition's file.

Then, you iterate over the products and create a new entry in Contentful for each product using the `createProduct` method you created earlier. If the creation of any product fails, you fail the step and pass the created products to the compensation function.

A step function must return a `StepResponse` instance. The `StepResponse` constructor accepts two parameters:

1. The step's output, which is the product entries created in Contentful.
2. Data to pass to the step's compensation function.

The compensation function accepts as a parameter the data passed from the step, and an object containing the Medusa container.

In the compensation function, you iterate over the created product entries and delete them from Contentful using the `deleteProduct` method you created earlier.

#### Create the Workflow

Now that you have all the necessary steps, you can create the workflow.

To create the workflow, create the file `src/workflows/create-products-contentful.ts` with the following content:

```ts title="src/workflows/create-products-contentful.ts"
import { createWorkflow, WorkflowResponse } from "@medusajs/framework/workflows-sdk"
import { useQueryGraphStep } from "@medusajs/medusa/core-flows"
import { createProductsContentfulStep } from "./steps/create-products-contentful"
import { ProductDTO } from "@medusajs/framework/types"

type WorkflowInput = {
  product_ids: string[]
}

export const createProductsContentfulWorkflow = createWorkflow(
  { name: "create-products-contentful-workflow" },
  (input: WorkflowInput) => {
    // @ts-ignore
    const { data } = useQueryGraphStep({
      entity: "product",
      fields: [
        "id",
        "title",
        "description",
        "subtitle",
        "status",
        "handle",
        "variants.*",
        "variants.options.*",
        "options.*",
        "options.values.*",
      ],
      filters: {
        id: input.product_ids,
      },
    })
    
    const contentfulProducts = createProductsContentfulStep({
      products: data as ProductDTO[],
    })

    return new WorkflowResponse(contentfulProducts)
  }
)
```

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 the product IDs to create in Contentful.

In the workflow's constructor function, you:

1. Retrieve the Medusa products using the `useQueryGraphStep` helper step. This step uses Medusa's [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md) tool to retrieve data across modules. You pass it the product IDs to retrieve.
2. Create the product entries in Contentful using the `createProductsContentfulStep` step.

A workflow must return an instance of `WorkflowResponse`. The `WorkflowResponse` constructor accepts the workflow's output as a parameter, which is an object of the product entries created in Contentful.

You now have the workflow that you can execute when a product is created in Medusa, or when the admin user triggers a sync manually.

***

## Step 4: Trigger Sync on Product Creation

Medusa has an event system that allows you to listen for events, such as `product.created`, and perform an asynchronous action when the event is emitted.

You listen to events in a subscriber. A [subscriber](https://docs.medusajs.com/docs/learn/fundamentals/events-and-subscribers/index.html.md) is an asynchronous function that listens to one or more events and performs actions when these events are emitted. A subscriber is useful when syncing data across systems, as the operation can be time-consuming and should be performed in the background.

In this step, you'll create a subscriber that listens to the `product.created` event and executes the `createProductsContentfulWorkflow` workflow.

Learn more about subscribers in the [Events and Subscribers documentation](https://docs.medusajs.com/docs/learn/fundamentals/events-and-subscribers/index.html.md).

To create a subscriber, create the file `src/subscribers/create-product.ts` with the following content:

```ts title="src/subscribers/create-product.ts" highlights={createProductSubscriberHighlights}
import {
  type SubscriberConfig,
  type SubscriberArgs,
} from "@medusajs/framework"
import { 
  createProductsContentfulWorkflow,
} from "../workflows/create-products-contentful"

export default async function handleProductCreate({
  event: { data },
  container,
}: SubscriberArgs<{ id: string }>) {
  await createProductsContentfulWorkflow(container)
    .run({
      input: {
        product_ids: [data.id],
      },
    })

  console.log("Product created in Contentful")
}

export const config: SubscriberConfig = {
  event: "product.created",
}
```

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 the subscriber listens to, which is `product.created` 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 `product.created` event is an array of product IDs.
- `container`: The Medusa container to access the Framework and commerce tools.

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

Finally, you log a message to the console to indicate that the product was created in Contentful.

### Test the Subscriber

To test out the subscriber, start the Medusa application:

```bash npm2yarn
npm run dev
```

Then, open the Medusa Admin dashboard and login.

Can't remember the credentials? Learn how to create a user in the [Medusa CLI reference](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/medusa-cli/commands/user/index.html.md).

Next, open the Products page and create a new product.

You should see the following message in the terminal:

```bash
info: Product created in Contentful
```

You can also see the product in the Contentful dashboard by going to the Content page.

***

## Step 5: Trigger Product Sync Manually

The other way to sync products is when the admin user triggers a sync manually. This is useful when you already have products in Medusa and you want to sync them to Contentful.

To allow admin users to trigger a sync manually, you need:

1. A subscriber that listens to a custom event.
2. An API route that emits the custom event when a request is sent to it.
3. A UI route in the Medusa Admin that displays a button to trigger the sync.

### Create Manual Sync Subscriber

You'll start by creating the subscriber that listens to a custom event to sync the Medusa products to Contentful.

To create the subscriber, create the file `src/subscribers/sync-products.ts` with the following content:

```ts title="src/subscribers/sync-products.ts" highlights={syncProductsSubscriberHighlights}
import type { 
  SubscriberConfig,
  SubscriberArgs,
} from "@medusajs/framework"
import { ContainerRegistrationKeys } from "@medusajs/framework/utils"
import { 
  createProductsContentfulWorkflow,
} from "../workflows/create-products-contentful"

export default async function syncProductsHandler({
  container,
}: SubscriberArgs<Record<string, unknown>>) {
  const query = container.resolve(ContainerRegistrationKeys.QUERY)
  
  const batchSize = 100
  let hasMore = true
  let offset = 0
  let totalCount = 0

  while (hasMore) {
    const {
      data: products,
      metadata: { count } = {},
    } = await query.graph({
      entity: "product",
      fields: [
        "id",
      ],
      pagination: {
        skip: offset,
        take: batchSize,
      },
    })

    if (products.length) {
      await createProductsContentfulWorkflow(container).run({
        input: {
          product_ids: products.map((product) => product.id),
        },
      })
    }

    hasMore = products.length === batchSize
    offset += batchSize
    totalCount = count ?? 0
  }

  console.log(`Synced ${totalCount} products to Contentful`)
}

export const config: SubscriberConfig = {
  event: "products.sync",
}
```

You create a subscriber that listens to the `products.sync` event.

In the subscriber function, you use [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md) to retrieve all the products in Medusa with pagination. Then, for each batch of products, you execute the `createProductsContentfulWorkflow` workflow, passing the product IDs to the workflow.

Finally, you log a message to the console to indicate that the products were synced to Contentful.

### Create API Route to Trigger Sync

Next, to allow the admin user to trigger the sync manually, you need to create an API route that emits the `products.sync` event.

An API Route is an endpoint that exposes commerce features to external applications and clients, such as storefronts.

Learn more about API routes in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/index.html.md).

An API route is created in a `route.ts` file under a sub-directory of the `src/api` directory. The path of the API route is the file's path relative to `src/api`.

So, to create an API route at the path `/admin/contentful/sync`, create the file `src/api/admin/contentful/sync/route.ts` with the following content:

```ts title="src/api/admin/contentful/sync/route.ts" highlights={syncProductsRouteHighlights}
import {
  MedusaRequest,
  MedusaResponse,
} from "@medusajs/framework/http"

export const POST = async (
  req: MedusaRequest,
  res: MedusaResponse
) => {
  const eventService = req.scope.resolve("event_bus")

  await eventService.emit({
    name: "products.sync",
    data: {},
  })

  res.status(200).json({
    message: "Products sync triggered successfully",
  })
}
```

Since you export a `POST` route handler function, you expose an `API` route at `/admin/contentful/sync`. The route handler function accepts two parameters:

1. A request object with details and context on the request, such as body parameters or authenticated user details.
2. A response object to manipulate and send the response.

In the route handler, you resolve the [Event Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/infrastructure-modules/event/index.html.md)'s service from the Medusa container and emit the `products.sync` event.

### Create UI Route to Trigger Sync

Finally, you'll add a new page to the Medusa Admin dashboard that displays a button to trigger the sync. To add a page, you need to create a UI route.

A [UI route](https://docs.medusajs.com/docs/learn/fundamentals/admin/ui-routes/index.html.md) is a React component that specifies the content to be shown in a new page of the Medusa Admin dashboard. You'll create a UI route to display a button that triggers product syncing to Contentful when clicked.

Refer to the [UI Routes](https://docs.medusajs.com/docs/learn/fundamentals/admin/ui-routes/index.html.md) documentation for more information.

#### Configure JS SDK

Before creating the UI route, you'll configure Medusa's [JS SDK](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/js-sdk/index.html.md) so that you can use it to send requests to the Medusa server.

The JS SDK is installed by default in your Medusa application. To configure it, create the file `src/admin/lib/sdk.ts` with the following content:

```ts title="src/admin/lib/sdk.ts"
import Medusa from "@medusajs/js-sdk"

export const sdk = new Medusa({
  baseUrl: "http://localhost:9000",
  debug: process.env.NODE_ENV === "development",
  auth: {
    type: "session",
  },
})
```

You create an instance of the JS SDK using the `Medusa` class from the JS SDK. You pass it an object having the following properties:

- `baseUrl`: The base URL of the Medusa server.
- `debug`: A boolean indicating whether to log debug information into the console.
- `auth`: An object specifying the authentication type. When using the JS SDK for admin customizations, you use the `session` authentication type.

#### Create UI Route

UI routes are created in a `page.tsx` file under a sub-directory of `src/admin/routes` directory. The file's path relative to `src/admin/routes` determines its path in the dashboard.

So, create the file `src/admin/routes/contentful/page.tsx` with the following content:

```tsx title="src/admin/routes/contentful/page.tsx" highlights={contentfulPageHighlights}
import { defineRouteConfig } from "@medusajs/admin-sdk"
import { Container, Heading, Button } from "@medusajs/ui"
import { useMutation } from "@tanstack/react-query"
import { sdk } from "../../lib/sdk"
import { toast } from "@medusajs/ui"

const ContentfulSettingsPage = () => {
  const { mutate, isPending } = useMutation({
    mutationFn: () => 
      sdk.client.fetch("/admin/contentful/sync", {
        method: "POST",
      }),
    onSuccess: () => {
      toast.success("Sync to Contentful triggered successfully")
    },
  })

  return (
    <Container className="p-6">
      <div className="flex flex-col gap-y-4">
        <div>
          <Heading level="h1">Contentful Settings</Heading>
        </div>
        <div>
          <Button 
            variant="primary"
            onClick={() => mutate()}
            isLoading={isPending}
          >
            Sync to Contentful
          </Button>
        </div>
      </div>
    </Container>
  )
}

export const config = defineRouteConfig({
  label: "Contentful",
})

export default ContentfulSettingsPage
```

A UI route's file must export:

1. A React component that defines the content of the page.
2. A configuration object that specifies the route's label in the dashboard. This label is used to show a sidebar item for the new route.

In the React component, you use `useMutation` hook from `@tanstack/react-query` to create a mutation that sends a `POST` request to the API route you created earlier. In the mutation function, you use the JS SDK to send the request.

Then, in the return statement, you display a button that triggers the mutation when clicked, which sends a request to the API route you created earlier.

### Test the Sync

To test out the sync, start the Medusa application:

```bash npm2yarn
npm run dev
```

Then, open the Medusa Admin dashboard and login. In the sidebar, you'll find a new "Contentful" item. If you click on it, you'll see the page you created with the button to trigger the sync.

![The Contentful page in the Medusa Admin dashboard with a button to trigger the sync](https://res.cloudinary.com/dza7lstvk/image/upload/v1744718825/Medusa%20Resources/Screenshot_2025-04-15_at_3.06.53_PM_va8e20.png)

If you click on the button, you'll see the following message in the terminal:

```bash
info: Synced 4 products to Contentful
```

Assuming you have `4` products in Medusa, the message indicates that the sync was successful.

You can also see the products in the Contentful dashboard.

![The Contentful dashboard showing the synced products](https://res.cloudinary.com/dza7lstvk/image/upload/v1744718896/Medusa%20Resources/Screenshot_2025-04-15_at_3.08.02_PM_qexr0x.png)

***

## Step 6: Retrieve Locales API Route

In the next steps, you'll implement customizations that are useful for storefronts. A storefront should show the customer a list of available locales and allow them to select from them.

In this step, you will:

1. Add the logic to retrieve locales from Contentful in the Contentful Module's service.
2. Create an API route that exposes the locales to the storefront.
3. Customize the Next.js Starter Storefront to show the locales to customers.

### Retrieve Locales from Contentful Method

You'll start by adding two methods to the Contentful Module's service that are useful to retrieve locales from Contentful.

The first method retrieves all locales from Contentful. Add it to the service at `src/modules/contentful/service.ts`:

```ts title="src/modules/contentful/service.ts"
export default class ContentfulModuleService {
  // ...
  async getLocales() {
    return await this.managementClient.locale.getMany({})
  }
}
```

You use the `locale.getMany` method of the Contentful Management API client to retrieve all locales.

The second method returns the code of the default locale:

```ts title="src/modules/contentful/service.ts"
export default class ContentfulModuleService {
  // ...
  async getDefaultLocaleCode() {
    return this.options.default_locale
  }
}
```

You return the default locale using the `default_locale` option you set in the module's options.

### Create API Route to Retrieve Locales

Next, you'll create an API route that exposes the locales to the storefront.

To create the API route, create the file `src/api/store/locales/route.ts` with the following content:

```ts title="src/api/store/locales/route.ts" highlights={getLocalesRouteHighlights}
import {
  MedusaRequest,
  MedusaResponse,
} from "@medusajs/framework/http"
import { CONTENTFUL_MODULE } from "../../../modules/contentful"
import ContentfulModuleService from "../../../modules/contentful/service"

export const GET = async (
  req: MedusaRequest,
  res: MedusaResponse
) => {
  const contentfulModuleService: ContentfulModuleService = req.scope.resolve(
    CONTENTFUL_MODULE
  )

  const locales = await contentfulModuleService.getLocales()
  const defaultLocaleCode = await contentfulModuleService.getDefaultLocaleCode()

  const formattedLocales = locales.items.map((locale) => {
    return {
      name: locale.name,
      code: locale.code,
      is_default: locale.code === defaultLocaleCode,
    }
  })

  res.json({
    locales: formattedLocales,
  })
}
```

Since you export a `GET` route handler function, you expose a `GET` route at `/store/locales`.

In the route handler, you resolve the Contentful Module's service from the Medusa container to retrieve the locales and the default locale code.

Then, you format the locales to include their name, code, and whether they are the default locale.

Finally, you return the formatted locales in the JSON response.

### Customize Storefront to Show Locales

In the first step of this tutorial, you installed the [Next.js Starter Storefront](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/nextjs-starter/index.html.md) along with the Medusa application. This storefront provides ecommerce features like a product catalog, a cart, and a checkout.

In this section, you'll customize the storefront to show the locales to customers and allow them to select from them. The selected locale will be stored in the browser's cookies, allowing you to use it later when retrieving a product's localized data.

The Next.js Starter Storefront was installed in a separate directory from Medusa. The directory's name is `{your-project}-storefront`.

So, if your Medusa application's directory is `medusa-contentful`, you can find the storefront by going back to the parent directory and changing to the `medusa-contentful-storefront` directory:

```bash
cd ../medusa-contentful-storefront # change based on your project name
```

#### Add Cookie Functions

You'll start by adding two functions that retrieve and set the locale in the browser's cookies.

In `src/lib/data/cookies.ts` add the following functions:

```ts title="src/lib/data/cookies.ts" highlights={getLocaleHighlights} badgeLabel="Storefront" badgeColor="blue"
export const getLocale = async () => {
  const cookies = await nextCookies()
  return cookies.get("_medusa_locale")?.value
}

export const setLocale = async (locale: string) => {
  const cookies = await nextCookies()
  cookies.set("_medusa_locale", locale, {
    maxAge: 60 * 60 * 24 * 7,
  })
}
```

The `getLocale` function retrieves the locale from the browser's cookies, and the `setLocale` function sets the locale in the browser's cookies.

#### Manage Locales Functions

Next, you'll add server actions to retrieve the locales and set the selected locale.

Create the file `src/lib/data/locale.ts` with the following content:

```ts title="src/lib/data/locale.ts" highlights={getLocalesHighlights} badgeLabel="Storefront" badgeColor="blue"
"use server"

import { sdk } from "@lib/config"
import type { Document } from "@contentful/rich-text-types"
import { getLocale, setLocale } from "./cookies"

export type Locale = {
  name: string
  code: string
  is_default: boolean
}

export async function getLocales() {
  return await sdk.client.fetch<{
    locales: Locale[]
  }>("/store/locales")
}

export async function getSelectedLocale() {
  let localeCode = await getLocale()
  if (!localeCode) {
    const locales = await getLocales()
    localeCode = locales.locales.find((l) => l.is_default)?.code
  }
  return localeCode
}

export async function setSelectedLocale(locale: string) {
  await setLocale(locale)
}
```

You add the following functions:

1. `getLocales`: Retrieves the locales from the Medusa server using the API route you created earlier.
2. `getSelectedLocale`: Retrieves the selected locale from the browser's cookies, or the default locale if no locale is selected.
3. `setSelectedLocale`: Sets the selected locale in the browser's cookies.

You'll use these functions as you add the UI to show the locales next.

#### Show Locales in the Storefront

You'll now add the UI to show the locales to customers and allow them to select from them.

Create the file `src/modules/layout/components/locale-select/index.tsx` with the following content:

```tsx title="src/modules/layout/components/locale-select/index.tsx" highlights={localeSelectHighlights} badgeLabel="Storefront" badgeColor="blue"
"use client"

import { useState, useEffect, Fragment } from "react"
import { getLocales, Locale, getSelectedLocale, setSelectedLocale } from "../../../../lib/data/locale"
import { Listbox, ListboxButton, ListboxOption, ListboxOptions, Transition } from "@headlessui/react"
import { ArrowRightMini } from "@medusajs/icons"
import { clx } from "@medusajs/ui"

const LocaleSelect = () => {
  const [locales, setLocales] = useState<Locale[]>([])
  const [locale, setLocale] = useState<Locale | undefined>()
  const [open, setOpen] = useState(false)

  useEffect(() => {
    getLocales()
      .then(({ locales }) => {
        setLocales(locales)
      })
  }, [])

  useEffect(() => {
    if (!locales.length || locale) {
      return
    }

    getSelectedLocale().then((locale) => {
      const localeDetails = locales.find((l) => l.code === locale) 
      setLocale(localeDetails)
    })
  }, [locales])

  useEffect(() => {
    if (locale) {
      setSelectedLocale(locale.code)
    }
  }, [locale])

  const handleChange = (locale: Locale) => {
    setLocale(locale)
    setOpen(false)
  }

  // TODO add return statement
}

export default LocaleSelect
```

You create a `LocaleSelect` component with the following state variables:

1. `locales`: The list of locales retrieved from the Medusa server.
2. `locale`: The selected locale.
3. `open`: A boolean indicating whether the dropdown is open.

Then, you use three `useEffect` hooks:

1. The first `useEffect` hook retrieves the locales using the `getLocales` function and sets them in the `locales` state variable.
2. The second `useEffect` hook is triggered when the `locales` state variable changes. It retrieves the selected locale using the `getSelectedLocale` function and sets the `locale` state variable.
3. The third `useEffect` hook is triggered when the `locale` state variable changes. It sets the selected locale in the browser's cookies using the `setSelectedLocale` function.

You also create a `handleChange` function that sets the selected locale and closes the dropdown. You'll execute this function when the customer selects a locale from the dropdown.

Finally, you'll add a return statement that shows the locale dropdown. Replace the `TODO` with the following:

```tsx title="src/modules/layout/components/locale-select/index.tsx" badgeLabel="Storefront" badgeColor="blue"
return (
  <div
    className="flex justify-between"
    onMouseEnter={() => setOpen(true)}
    onMouseLeave={() => setOpen(false)}
  >
    <div>
      <Listbox as="span" onChange={handleChange} defaultValue={locale}>
        <ListboxButton className="py-1 w-full">
          <div className="txt-compact-small flex items-start gap-x-2">
            <span>Language:</span>
            {locale && (
              <span className="txt-compact-small flex items-center gap-x-2">
                {locale.name}
              </span>
            )}
          </div>
        </ListboxButton>
        <div className="flex relative w-full min-w-[320px]">
          <Transition
            show={open}
            as={Fragment}
            leave="transition ease-in duration-150"
            leaveFrom="opacity-100"
            leaveTo="opacity-0"
          >
            <ListboxOptions
              className="absolute -bottom-[calc(100%-36px)] left-0 xsmall:left-auto xsmall:right-0 max-h-[442px] overflow-y-scroll z-[900] bg-white drop-shadow-md text-small-regular uppercase text-black no-scrollbar rounded-rounded w-full"
              static
            >
              {locales?.map((l, index) => {
                return (
                  <ListboxOption
                    key={index}
                    value={l}
                    className="py-2 hover:bg-gray-200 px-3 cursor-pointer flex items-center gap-x-2"
                  >
                    {l.name}
                  </ListboxOption>
                )
              })}
            </ListboxOptions>
          </Transition>
        </div>
      </Listbox>
    </div>
    <ArrowRightMini
      className={clx(
        "transition-transform duration-150",
        open ? "-rotate-90" : ""
      )}
    />
  </div>
)
```

You show the selected locale. Then, when the customer hovers over the locale, the dropdown is shown to select a different locale.

When the customer selects a locale, you execute the `handleChange` function, which sets the selected locale and closes the dropdown.

#### Add Locale Select to the Side Menu

The last step is to show the locale selector in the side menu after the country selector.

In `src/modules/layout/components/side-menu/index.tsx`, add the following import:

```tsx title="src/modules/layout/components/side-menu/index.tsx" badgeLabel="Storefront" badgeColor="blue"
import LocaleSelect from "../locale-select"
```

Then, add the `LocaleSelect` component in the return statement of the `SideMenu` component, after the `div` wrapping the country selector:

```tsx title="src/modules/layout/components/side-menu/index.tsx" badgeLabel="Storefront" badgeColor="blue"
<LocaleSelect />
```

The locale selector will now show in the side menu after the country selector.

### Test out the Locale Selector

To test out all the changes made in this step, start the Medusa application by running the following command in the Medusa application's directory:

```bash npm2yarn
npm run dev
```

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

```bash npm2yarn
npm run dev
```

The storefront will run at `http://localhost:8000`. Open it in your browser, then click on "Menu" at the top right. You'll see at the bottom of the side menu the locale selector.

![The locale selector in the side menu](https://res.cloudinary.com/dza7lstvk/image/upload/v1744720332/Medusa%20Resources/Screenshot_2025-04-15_at_3.31.51_PM_vdqou5.png)

You can try selecting a different locale. The selected locale will be stored, but products will still be shown in the default locale. You'll implement the locale-based product retrieval in the next step.

***

## Step 7: Retrieve Product Details for Locale

The next feature you'll implement is retrieving and displaying product details for a selected locale.

You'll implement this feature by:

1. Linking Medusa's product to Contentful's product.
2. Adding the method to retrieve product details for a selected locale from Contentful.
3. Adding a new route to retrieve the product details for a selected locale.
4. Customizing the storefront to show the product details for the selected locale.

### Link Medusa's Product to Contentful's Product

Medusa facilitates retrieving data across systems using [module links](https://docs.medusajs.com/docs/learn/fundamentals/module-links/index.html.md). A module link forms an association between data models of two modules while maintaining module isolation.

Not only do module links support Medusa data models, but they also support virtual data models that are not persisted in Medusa's database. In that case, you create a [read-only module link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/read-only/index.html.md) that allows you to retrieve data across systems.

In this section, you'll define a read-only module link between Medusa's product and Contentful's product, allowing you to later retrieve a product's entry in Contentful within a single query.

Learn more about read-only module links in the [Read-Only Module Links](https://docs.medusajs.com/docs/learn/fundamentals/module-links/read-only/index.html.md) documentation.

Module links are defined in a TypeScript or JavaScript file under the `src/links` directory. So, create the file `src/links/product-contentful.ts` with the following content:

```ts title="src/links/product-contentful.ts" highlights={productContentfulLinkHighlights}
import { defineLink } from "@medusajs/framework/utils"
import ProductModule from "@medusajs/medusa/product"
import { CONTENTFUL_MODULE } from "../modules/contentful"

export default defineLink(
  {
    linkable: ProductModule.linkable.product,
    field: "id",
  },
  {
    linkable: {
      serviceName: CONTENTFUL_MODULE,
      alias: "contentful_product",
      primaryKey: "product_id",
    },
  },
  {
    readOnly: true,
  }
)
```

You define a module link using `defineLink` from the Modules SDK. It accepts three parameters:

1. An object with the linkable configuration of the data model in Medusa, and the field that will be passed as a filter to the Contentful Module's service.
2. An object with the linkable configuration of the virtual data model in Contentful. This object must have the following properties:
   - `serviceName`: The name of the service, which is the Contentful Module's name. Medusa uses this name to resolve the module's service from the Medusa container.
   - `alias`: The alias to use when querying the linked records. You'll see how that works in a bit.
   - `primaryKey`: The field in Contentful's virtual data model that holds the ID of a product.
3. An object with the `readOnly` property set to `true`.

You'll see how the module link works in the upcoming steps.

### List Contentful Products Method

Next, you'll add a method that lists Contentful products for a given locale.

Add the following method to the Contentful Module's service at `src/modules/contentful/service.ts`:

```ts title="src/modules/contentful/service.ts" highlights={listContentfulProductsMethodHighlights}
export default class ContentfulModuleService {
  // ...
  async list(
    filter: {
      id: string | string[]
      context?: {
        locale: string
      }
    }
  ) {
    const contentfulProducts = await this.deliveryClient.getEntries({
      limit: 15,
      content_type: "product",
      "fields.medusaId": filter.id,
      locale: filter.context?.locale,
      include: 3,
    })

    return contentfulProducts.items.map((product) => {
      // remove links
      const { productVariants: _, productOptions: __, ...productFields } = product.fields
      return {
        ...productFields,
        product_id: product.fields.medusaId,
        variants: product.fields.productVariants.map((variant) => {
          // remove circular reference
          const { product: _, productOptionValues: __, ...variantFields } = variant.fields
          return {
            ...variantFields,
            product_variant_id: variant.fields.medusaId,
            options: variant.fields.productOptionValues.map((option) => {
              // remove circular reference
              const { productOption: _, ...optionFields } = option.fields
              return {
                ...optionFields,
                product_option_id: option.fields.medusaId,
              }
            }),
          }
        }),
        options: product.fields.productOptions.map((option) => {
          // remove circular reference
          const { product: _, ...optionFields } = option.fields
          return {
            ...optionFields,
            product_option_id: option.fields.medusaId,
            values: option.fields.values.map((value) => {
              // remove circular reference
              const { productOptionValue: _, ...valueFields } = value.fields
              return {
                ...valueFields,
                product_option_value_id: value.fields.medusaId,
              }
            }),
          }
        }),
      }
    })
  }
}
```

You add a `list` method that accepts an object with the following properties:

1. `id`: The ID of the product(s) in Medusa to retrieve their entries in Contentful.
2. `context`: An object with the `locale` property that holds the locale code to retrieve the product's entry in Contentful for that locale.

In the method, you use the Delivery API client's `getEntries` method to retrieve the products. You pass the following parameters:

- `limit`: The maximum number of products to retrieve.
- `content_type`: The content type of the entries to retrieve, which is `product`.
- `fields.medusaId`: Filter the products by their `medusaId` field, which holds the ID of the product in Medusa.
- `locale`: The locale code to retrieve the fields of the product in that locale.
- `include`: The depth of the included nested entries. This ensures that you can retrieve the product's variants and options, and their values.

Then, you format the retrieved products to:

- Pass the product's ID in the `product_id` property. This is essential to map a product in Medusa to its entry in Contentful.
- Remove the circular references in the product's variants, options, and values to avoid infinite loops.

To paginate the retrieved products, implemet a `listAndCount` method as explained in the [Query Context](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query-context#using-pagination-with-query/index.html.md) documentation.

### Retrieve Product Details for Locale API Route

You'll now create the API route that returns a product's details for a given locale.

You can create an API route that accepts path parameters by creating a directory within the route file's path whose name is of the format `[param]`.

So, create the file `src/api/store/products/[id]/[locale]/route.ts` with the following content:

```ts title="src/api/store/products/[id]/[locale]/route.ts" highlights={getProductLocaleDetailsRouteHighlights}
import {
  MedusaRequest,
  MedusaResponse,
} from "@medusajs/framework/http"
import { QueryContext } from "@medusajs/framework/utils"

export const GET = async (
  req: MedusaRequest,
  res: MedusaResponse
) => {
  const { locale, id } = req.params
  
  const query = req.scope.resolve("query")

  const { data } = await query.graph({
    entity: "product",
    fields: [
      "id",
      "contentful_product.*",
    ],
    filters: {
      id,
    },
    context: {
      contentful_product: QueryContext({
        locale,
      }),
    },
  })

  res.json({
    product: data[0],
  })  
}
```

Since you export a `GET` route handler function, you expose a `GET` route at `/store/products/[id]/[locale]`. The route accepts two path parameters: the product's ID and the locale code.

In the route handler, you retrieve the `locale` and `id` path parameters from the request. Then, you resolve [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md) from the Medusa container.

Next, you use Query to retrieve the localized details of the specified product. To do that, you pass an object with the following properties:

- `entity`: The entity to retrieve, which is `product`.
- `fields`: The fields to retrieve. Notice that you include the `contentful_product.*` field, which is available through the module link you created earlier.
- `filters`: The filter to apply on the retrieved products. You apply the product's ID as a filter.
- `context`: An additional context to be passed to the methods retrieving the data. To pass a context, you use [Query Context](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query-context/index.html.md).

By specifying `contentful_product.*` in the `fields` property, Medusa will retrieve the product's entry from Contentful using the `list` method you added to the Contentful Module's service.

Medusa passes the filters and context to the `list` method, and attaches the returned data to the Medusa product if its `product_id` matches the product's ID.

Finally, you return the product's details in the JSON response.

You can now use this route to retrieve a product's details for a given locale.

### Show Localized Product Details in Storefront

Now that you expose the localized product details, you can customize the storefront to show them.

#### Install Contentful Rich Text Package

When you retrieve the entries from Contentful, rich-text fields are returned as an object that requires special rendering. So, Contentful provides a package to render rich-text fields.

Install the package by running the following command:

```bash npm2yarn
npm install @contentful/@contentful/rich-text-types
```

You'll use this package to render the product's description.

#### Retrieve Localized Product Details Function

To retrieve a product's details for a given locale, you'll add a function that sends a request to the API route you created.

First, add the following import at the top of `src/lib/data/locale.ts`:

```ts title="src/lib/data/locale.ts" badgeLabel="Storefront" badgeColor="blue"
import type { Document } from "@contentful/rich-text-types"
```

Then, add the following type and function at the end of the file:

```ts title="src/lib/data/locale.ts" badgeLabel="Storefront" badgeColor="blue"
export type ProductLocaleDetails = {
  id: string
  contentful_product: {
    product_id: string
    title: string
    handle: string
    description: Document
    subtitle?: string
    variants: {
      title: string
      product_variant_id: string
      options: {
        value: string
        product_option_id: string
      }[]
    }[]
    options: {
      title: string
      product_option_id: string
      values: {
        title: string
        product_option_value_id: string
      }[]
    }[]
  }
}

export async function getProductLocaleDetails(
  productId: string
) {
  const localeCode = await getSelectedLocale()

  return await sdk.client.fetch<{
    product: ProductLocaleDetails
  }>(`/store/products/${productId}/${localeCode}`)
}
```

You define a `ProductLocaleDetails` type that describes the structure of a localized product's details.

You also define a `getProductLocaleDetails` function that sends a request to the API route you created and returns the localized product's details.

#### Show Localized Product Title in Products Listing

Next, you'll customize existing components to show the localized product details.

The component defined in `src/modules/products/components/product-preview/index.tsx` shows the product's details in the products listing page. You need to retrieve the localized product details and show the product's title in the selected locale.

In `src/modules/products/components/product-preview/index.tsx`, add the following import:

```tsx title="src/modules/products/components/product-preview/index.tsx" badgeLabel="Storefront" badgeColor="blue"
import { getProductLocaleDetails } from "@lib/data/locale"
```

Then, in the `ProductPreview` component in the same file, add the following before the `return` statement:

```tsx title="src/modules/products/components/product-preview/index.tsx" badgeLabel="Storefront" badgeColor="blue"
const productLocaleDetails = await getProductLocaleDetails(product.id!)
```

This will retrieve the localized product details for the selected locale.

Finally, to show the localized product title, find in the `ProductPreview` component's `return` statement the following line:

```tsx title="src/modules/products/components/product-preview/index.tsx" badgeLabel="Storefront" badgeColor="blue"
{product.title}
```

And replace it with the following:

```tsx title="src/modules/products/components/product-preview/index.tsx" badgeLabel="Storefront" badgeColor="blue"
{productLocaleDetails.product.contentful_product?.title || product.title}
```

You'll test it out after the next step.

#### Show Localized Product Details in Product Page

Next, you'll customize the product page to show the localized product details.

The product's details page is defined in `src/app/[countryCode]/(main)/products/[handle]/page.tsx`. So, add the following import at the top of the file:

```tsx title="src/app/[countryCode]/(main)/products/[handle]/page.tsx" badgeLabel="Storefront" badgeColor="blue"
import { getProductLocaleDetails } from "@lib/data/locale"
```

Then, in the `ProductPage` component in the same file, add the following before the `return` statement:

```tsx title="src/app/[countryCode]/(main)/products/[handle]/page.tsx" badgeLabel="Storefront" badgeColor="blue"
const productLocaleDetails = await getProductLocaleDetails(pricedProduct.id!)
```

This will retrieve the localized product details for the selected locale.

Finally, in the `ProductPage` component in the same file, pass the following prop to `ProductTemplate`:

```tsx title="src/app/[countryCode]/(main)/products/[handle]/page.tsx" badgeLabel="Storefront" badgeColor="blue"
return (
  <ProductTemplate
    // ...
    productLocaleDetails={productLocaleDetails.product}
  />
)
```

Next, you'll customize the `ProductTemplate` component to accept and use this prop.

In `src/modules/products/templates/index.tsx`, add the following import:

```tsx title="src/modules/products/templates/index.tsx" badgeLabel="Storefront" badgeColor="blue"
import { ProductLocaleDetails } from "@lib/data/locale"
```

Then, update the `ProductTemplateProps` type to include the `productLocaleDetails` prop:

```tsx title="src/modules/products/templates/index.tsx" badgeLabel="Storefront" badgeColor="blue"
export type ProductTemplateProps = {
  // ...
  productLocaleDetails: ProductLocaleDetails
}
```

Next, update the `ProductTemplate` component to destructure the `productLocaleDetails` prop:

```tsx title="src/modules/products/templates/index.tsx" badgeLabel="Storefront" badgeColor="blue"
const ProductTemplate: React.FC<ProductTemplateProps> = ({
  // ...
  productLocaleDetails,
}) => {
  // ...
}
```

Finally, pass the `productLocaleDetails` prop to the `ProductInfo` component in the `return` statement:

```tsx title="src/modules/products/templates/index.tsx" badgeLabel="Storefront" badgeColor="blue"
<ProductInfo
  // ...
  productLocaleDetails={productLocaleDetails}
/>
```

The `ProductInfo` component shows the product's details. So, you need to update it to accept and use the `productLocaleDetails` prop.

In `src/modules/products/templates/product-info/index.tsx`, add the following imports:

```tsx title="src/modules/products/templates/product-info/index.tsx" badgeLabel="Storefront" badgeColor="blue"
import { ProductLocaleDetails } from "@lib/data/locale"
import { documentToHtmlString } from "@contentful/rich-text-html-renderer"
```

Then, update the `ProductInfoProps` type to include the `productLocaleDetails` prop:

```tsx title="src/modules/products/templates/product-info/index.tsx" badgeLabel="Storefront" badgeColor="blue"
export type ProductInfoProps = {
  // ...
  productLocaleDetails: ProductLocaleDetails
}
```

Next, update the `ProductInfo` component to destructure the `productLocaleDetails` prop:

```tsx title="src/modules/products/templates/product-info/index.tsx" badgeLabel="Storefront" badgeColor="blue"
const ProductInfo = ({ product, productLocaleDetails }: ProductInfoProps) => {
  // ...
}
```

Then, find the following line in the `return` statement:

```tsx title="src/modules/products/templates/product-info/index.tsx" badgeLabel="Storefront" badgeColor="blue"
{product.title}
```

And replace it with the following:

```tsx title="src/modules/products/templates/product-info/index.tsx" badgeLabel="Storefront" badgeColor="blue"
{productLocaleDetails.contentful_product?.title || product.title}
```

Also, find the following line:

```tsx title="src/modules/products/templates/product-info/index.tsx" badgeLabel="Storefront" badgeColor="blue"
{product.description}
```

And replace it with the following:

```tsx title="src/modules/products/templates/product-info/index.tsx" badgeLabel="Storefront" badgeColor="blue"
{productLocaleDetails.contentful_product?.description ? 
  <div dangerouslySetInnerHTML={{ __html: documentToHtmlString(productLocaleDetails.contentful_product?.description) }} /> : 
  product.description
}
```

You use the `documentToHtmlString` function to render the rich-text field. The function returns an HTML string that you can use to render the description.

### Test out the Localized Product Details

You can now test out all the changes made in this step.

To do that, start the Medusa application by running the following command in the Medusa application's directory:

```bash npm2yarn
npm run dev
```

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

```bash npm2yarn
npm run dev
```

Open the storefront at `http://localhost:8000` and select a different locale.

Then, open the products listing page by clicking on Menu -> Store. You'll see the product titles in the selected locale.

![The product titles are shown in the selected locale](https://res.cloudinary.com/dza7lstvk/image/upload/v1744723796/Medusa%20Resources/Screenshot_2025-04-15_at_4.29.39_PM_izxt6j.png)

Then, if you click on a product, you'll see the product's title and description in the selected locale.

![The product's title and description are shown in the selected locale](https://res.cloudinary.com/dza7lstvk/image/upload/v1744723844/Medusa%20Resources/Screenshot_2025-04-15_at_4.30.31_PM_pt6ngz.png)

***

## Step 8: Sync Changes from Contentful to Medusa

The last feature you'll implement is syncing changes from Contentful to Medusa.

Contentful's webhooks allow you to listen to changes in your Contentful entries. You can then set up a webhook listener API route in Medusa that updates the product's data.

In this step, you'll set up a webhook listener that updates Medusa's product data when a Contentful entry is published.

### Prerequisites: Public Server

Webhooks can only trigger deployed listeners. So, you must either [deploy your Medusa application](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/deployment/index.html.md), or use tools like [ngrok](https://ngrok.com/) to publicly expose your local application.

### Set Up Webhooks in Contentful

Before setting up the webhook listener, you need to set up a webhook in Contentful. To do that, on the Contentful dashboard:

1. Click on the cog icon at the top right, then choose "Webhooks" from the dropdown.

![The cog icon is at the top right next to the avatar icon. When you click on it, you see a dropdown with the "Webhooks" option](https://res.cloudinary.com/dza7lstvk/image/upload/v1744733023/Medusa%20Resources/Screenshot_2025-04-15_at_7.03.21_PM_ji7ohw.png)

2. On the Webhooks page, click on the "Add Webhook" button.
3. In the webhook form:
   - In the Name fields, enter a name, such as "Medusa".
   - In the URL field, enter `{your_app}/hooks/contentful`, where `{your_app}` is the public URL of your Medusa application. You'll create the `/hooks/contentful` API route in a bit.
   - In the Triggers section, select the "Published" trigger for "Entry".

![The webhook form with the URL and triggers highlighted](https://res.cloudinary.com/dza7lstvk/image/upload/v1744733332/Medusa%20Resources/Screenshot_2025-04-15_at_7.08.24_PM_coq6oo.png)

- Scroll down to the "Headers" section, and choose "application/json" for the "Content type" field.

![The webhook form](https://res.cloudinary.com/dza7lstvk/image/upload/v1744733411/Medusa%20Resources/Screenshot_2025-04-15_at_7.09.53_PM_kvlppv.png)

4. Once you're done, click the Save button.

### Setup Webhook Secret in Contentful

You also need to add a webhook secret in Contentful. To do that, on the Contentful dashboard:

1. Click on the cog icon at the top right, then choose "Webhooks" from the dropdown.
2. On the Webhooks page, click on the "Settings" tab.
3. Click on the "Enable request verification" button.

![The "Enable request verification" button in the "Settings" tab](https://res.cloudinary.com/dza7lstvk/image/upload/v1744786362/Medusa%20Resources/Screenshot_2025-04-16_at_9.51.39_AM_byv3tt.png)

4. Copy the secret that shows up. You can update it later but you can't see the same secret again.

You'll use the secret to verify the webhook request in Medusa next.

### Update Contentful Module Options

First, add the webhook secret as an environment variable in the Medusa application's `.env` file:

```plain
CONTENTFUL_WEBHOOK_SECRET=aEl7...
```

Next, add the webhook secret to the Contentful Module options in the Medusa application's `medusa-config.ts` file:

```ts title="medusa-config.ts"
module.exports = defineConfig({
  // ...
  modules: [
    {
      resolve: "./src/modules/contentful",
      options: {
        // ...
        webhook_secret: process.env.CONTENTFUL_WEBHOOK_SECRET,
      },
    },
  ],
})
```

Finally, update the `ModuleOptions` type in `src/modules/contentful/loader/create-content-models.ts` to include the `webhook_secret` option:

```ts title="src/modules/contentful/loader/create-content-models.ts"
export type ModuleOptions = {
  // ...
  webhook_secret: string
}
```

### Add Verify Request Method

Next, you'll add a method to the Contentful Module's service that verifies a webhook request.

To verify the request, you'll need the `@contentful/node-apps-toolkit` package that provides utility functions for Node.js applications.

So, run the following command to install it:

```bash npm2yarn
npm install @contentful/node-apps-toolkit
```

Then, add the following method to the Contentful Module's service in `src/modules/contentful/service.ts`:

```ts title="src/modules/contentful/service.ts"
// other imports...
import { 
  CanonicalRequest, 
  verifyRequest,
} from "@contentful/node-apps-toolkit"

export default class ContentfulModuleService {
  // ...
  async verifyWebhook(request: CanonicalRequest) {
    if (!this.options.webhook_secret) {
      throw new MedusaError(
        MedusaError.Types.INVALID_DATA, 
        "Webhook secret is not set"
      )
    }
    return verifyRequest(this.options.webhook_secret, request, 0)
  }
}
```

You add a `verifyWebhook` method that verifies a webhook request using the `verifyRequest` function.

You pass to the `verifyRequest` function the webhook secret from the module's options with the request's details. You also disable time-to-live (TTL) check by passing `0` as the third argument.

### Handle Contentful Webhook Workflow

Before you add the webhook listener, the last piece you need is to add a workflow that handles the necessary updates based on the webhook event.

The workflow will have the following steps:

- [prepareUpdateDataStep](#prepareUpdateDataStep): Prepare the data for the update

You only need to implement the first step, as Medusa provides the other workflows (running as steps) in the `@medusajs/medusa/core-flows` package.

#### prepareUpdateDataStep

The first step receives the webhook data payload and, based on the entry type, returns the data necessary for the update.

To create the step, create the file `src/workflows/steps/prepare-update-data.ts` with the following content:

```ts title="src/workflows/steps/prepare-update-data.ts" highlights={prepareUpdateDataStepHighlights}
import { createStep, StepResponse } from "@medusajs/framework/workflows-sdk"
import { EntryProps } from "contentful-management"
import ContentfulModuleService from "../../modules/contentful/service"
import { CONTENTFUL_MODULE } from "../../modules/contentful"

type StepInput = {
  entry: EntryProps
}

export const prepareUpdateDataStep = createStep(
  "prepare-update-data",
  async ({ entry }: StepInput, { container }) => {
    const contentfulModuleService: ContentfulModuleService = 
      container.resolve(CONTENTFUL_MODULE)
    
    const defaultLocale = await contentfulModuleService.getDefaultLocaleCode()

    let data: Record<string, unknown> = {}

    switch (entry.sys.contentType.sys.id) {
      case "product":
        data = {
          id: entry.fields.medusaId[defaultLocale!],
          title: entry.fields.title[defaultLocale!],
          subtitle: entry.fields.subtitle?.[defaultLocale!] || undefined,
          handle: entry.fields.handle[defaultLocale!],
        }
        break
      case "productVariant":
        data = {
          id: entry.fields.medusaId[defaultLocale!],
          title: entry.fields.title[defaultLocale!],
        }
        break
      case "productOption":
        data = {
          selector: {
            id: entry.fields.medusaId[defaultLocale!],
          },
          update: {
            title: entry.fields.title[defaultLocale!],
          },
        }
        break
    }

    return new StepResponse(data)
  }
)
```

You define a `prepareUpdateDataStep` function that receives the webhook data payload as an input.

In the step, you resolve the Contentful Module's service and use it to retrieve the default locale code. You need it to find the value to update the fields in Medusa.

Next, you prepare the data to return based on the entry type:

- `product`: The product's ID, title, subtitle, and handle.
- `productVariant`: The product variant's ID and title.
- `productOption`: The product option's ID and title.

The data is prepared based on the expected input for the workflows that will be used to update the data.

#### Create the Workflow

You can now create the workflow that handles the webhook event.

Create the file `src/workflows/handle-contentful-hook.ts` with the following content:

```ts title="src/workflows/handle-contentful-hook.ts" highlights={handleContentfulHookWorkflowHighlights} collapsibleLines="1-14" expandButtonLabel="Show Imports"
import { createWorkflow, when } from "@medusajs/framework/workflows-sdk"
import { EntryProps } from "contentful-management"
import { prepareUpdateDataStep } from "./steps/prepare-update-data"
import { 
  updateProductOptionsWorkflow, 
  updateProductsWorkflow, 
  updateProductVariantsWorkflow, 
  UpdateProductOptionsWorkflowInput,
} from "@medusajs/medusa/core-flows"
import { 
  UpsertProductDTO, 
  UpsertProductVariantDTO,
} from "@medusajs/framework/types"

export type WorkflowInput = {
  entry: EntryProps
}

export const handleContentfulHookWorkflow = createWorkflow(
  { name: "handle-contentful-hook-workflow" },
  (input: WorkflowInput) => {
    const prepareUpdateData = prepareUpdateDataStep({
      entry: input.entry,
    })

    when(input, (input) => input.entry.sys.contentType.sys.id === "product")
      .then(() => {
        updateProductsWorkflow.runAsStep({
          input: {
            products: [prepareUpdateData as UpsertProductDTO],
          },
        })
      })

    when(input, (input) => 
      input.entry.sys.contentType.sys.id === "productVariant"
    )
    .then(() => {
      updateProductVariantsWorkflow.runAsStep({
        input: {
            product_variants: [prepareUpdateData as UpsertProductVariantDTO],
          },
        })
      })

    when(input, (input) => 
      input.entry.sys.contentType.sys.id === "productOption"
    )
    .then(() => {
      updateProductOptionsWorkflow.runAsStep({
        input: prepareUpdateData as unknown as UpdateProductOptionsWorkflowInput,
      })
    })
  }
)
```

You define a `handleContentfulHookWorkflow` function that receives the webhook data payload as an input.

In the workflow, you:

- Prepare the data for the update using the `prepareUpdateDataStep` step.
- Use a [when](https://docs.medusajs.com/docs/learn/fundamentals/workflows/conditions/index.html.md) condition to check if the entry type is a `product`, and if so, update the product using the `updateProductsWorkflow`.
- Use a `when` condition to check if the entry type is a `productVariant`, and if so, update the product variant using the `updateProductVariantsWorkflow`.
- Use a `when` condition to check if the entry type is a `productOption`, and if so, update the product option using the `updateProductOptionsWorkflow`.

You can't perform data manipulation in a workflow's constructor function. Instead, the Workflows SDK includes utility functions like `when` to perform typical operations that requires accessing data values. Learn more about workflow constraints in the [Workflow Constraints](https://docs.medusajs.com/docs/learn/fundamentals/workflows/constructor-constraints/index.html.md) documetation.

### Add the Webhook Listener API Route

You can finally add the API route that acts as a webhook listener.

To add the API route, create the file `src/api/hooks/contentful/route.ts` with the following content:

```ts title="src/api/hooks/contentful/route.ts" highlights={contentfulHookRouteHighlights} collapsibleLines="1-10" expandButtonLabel="Show Imports"
import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http"
import { 
  handleContentfulHookWorkflow, 
  HandleContentfulHookWorkflowInput,
} from "../../../workflows/handle-contentful-hook"
import { CONTENTFUL_MODULE } from "../../../modules/contentful"
import { CanonicalRequest } from "@contentful/node-apps-toolkit"
import { MedusaError } from "@medusajs/framework/utils"
import ContentfulModuleService from "../../../modules/contentful/service"

export const POST = async (
  req: MedusaRequest,
  res: MedusaResponse
) => {
  const contentfulModuleService: ContentfulModuleService = 
    req.scope.resolve(CONTENTFUL_MODULE)
  
  const isValid = await contentfulModuleService.verifyWebhook({
    path: req.path,
    method: req.method.toUpperCase(),
    headers: req.headers,
    body: JSON.stringify(req.body),
  } as unknown as CanonicalRequest)
  
  if (!isValid) {
    throw new MedusaError(
      MedusaError.Types.UNAUTHORIZED, 
      "Invalid webhook request"
    )
  }
          
  await handleContentfulHookWorkflow(req.scope).run({
    input: {
      entry: req.body,
    } as unknown as HandleContentfulHookWorkflowInput,
  })

  res.status(200).send("OK")
}
```

Since you export a `POST` route handler function, you expose a `POST` route at `/hooks/contentful`.

In the route, you first use the `verifyWebhook` method of the Contentful Module's service to verify the request. If the request is invalid, you throw an error.

Then, you run the `handleContentfulHookWorkflow` passing the request's body, which is the webhook data payload, as an input.

Finally, you return a `200` response to Contentful to confirm that the webhook was received and processed.

### Test the Webhook Listener

To test out the webhook listener, start the Medusa application:

```bash npm2yarn
npm run dev
```

Then, try updating a product's title (in the default locale) in Contentful. You should see the product's title updated in Medusa.

***

## Next Steps

You've now integrated Contentful with Medusa and supported localized product details. You can expand on the features in this tutorial to:

1. Add support for other data types, such as product categories or collections.
   - Refer to the data model references for each [Commerce Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/index.html.md) to figure out the content types you need to create in Contentful.
2. Listen to other product events and update the Contentful entries accordingly.
   - Refer to the [Events Reference](https://docs.medusajs.com/references/events/index.html.md) for details on all events emitted in Medusa.
3. Add localization for the entire Next.js Starter Storefront. You can either:
   - Create content types in Contentful for different sections in the storefront, then use them to retrieve the localized content;
   - Or use the approaches recommended in the [Next.js documentation](https://nextjs.org/docs/app/building-your-application/routing/internationalization).

If you're new to Medusa, check out the [main documentation](https://docs.medusajs.com/docs/learn/index.html.md), 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](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/index.html.md).

### Troubleshooting

If you encounter issues during your development, check out the [troubleshooting guides](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/troubleshooting/index.html.md).

### Getting Help

If you encounter issues not covered in the troubleshooting guides:

1. Visit the [Medusa GitHub repository](https://github.com/medusajs/medusa) to report issues or ask questions.
2. Join the [Medusa Discord community](https://discord.gg/medusajs) for real-time support from community members.
3. Contact the [sales team](https://medusajs.com/contact/) to get help from the Medusa team.


# How to Build Magento Data Migration Plugin

In this tutorial, you'll learn how to build a [plugin](https://docs.medusajs.com/docs/learn/fundamentals/plugins/index.html.md) that migrates data, such as products, from Magento to Medusa.

Magento is known for its customization capabilities. However, its monolithic architecture imposes limitations on business requirements, often forcing development teams to implement hacky workarounds. Over time, these customizations become challenging to maintain, especially as the business scales, leading to increased technical debt and slower feature delivery.

Medusa's modular architecture allows you to build a custom digital commerce platform that meets your business requirements without the limitations of a monolithic system. By migrating from Magento to Medusa, you can take advantage of Medusa's modern technology stack to build a scalable and flexible commerce platform that grows with your business.

By following this tutorial, you'll create a Medusa plugin that migrates data from a Magento server to a Medusa application in minimal time. You can re-use this plugin across multiple Medusa applications, allowing you to adopt Medusa across your projects.

## Summary

### Prerequisites



This tutorial will teach you how to:

- Install and set up a Medusa application project.
- Install and set up a Medusa plugin.
- Implement a Magento Module in the plugin to connect to Magento's APIs and retrieve products.
  - This guide will only focus on migrating product data from Magento to Medusa. You can extend the implementation to migrate other data, such as customers, orders, and more.
- Trigger data migration from Magento to Medusa in a scheduled job.

You can follow this tutorial whether you're new to Medusa or an advanced Medusa developer.

![Diagram showcasing the flow of migrating data from Magento to Medusa](https://res.cloudinary.com/dza7lstvk/image/upload/v1739360550/Medusa%20Resources/magento-summary_hsewci.jpg)

[Example Repository](https://github.com/medusajs/examples/tree/main/migrate-from-magento): Find the full code of the guide in this repository. The repository also includes additional features, such as triggering migrations from the Medusa Admin dashboard.

***

## Step 1: Install a Medusa Application

You'll first install a Medusa application that exposes core commerce features through REST APIs. You'll later install the Magento plugin in this application to test it out.

### Prerequisites

- [Node.js v20+](https://nodejs.org/en/download)
- [Git CLI tool](https://git-scm.com/downloads)
- [PostgreSQL](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 be asked for the project's name. You can also optionally choose to install the [Next.js Starter Storefront](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/nextjs-starter/index.html.md).

Afterward, the installation process will start, which will install the Medusa application in a directory with your project's name. If you chose to install the Next.js starter, it'll be installed in a separate directory with the `{project-name}-storefront` name.

The Medusa application is composed of a headless Node.js server and an admin dashboard. The storefront is installed or custom-built separately and connects to the Medusa application through its REST endpoints, called [API routes](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/index.html.md). Refer to the [Medusa Architecture](https://docs.medusajs.com/docs/learn/introduction/architecture/index.html.md) documentation to learn more.

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. Afterward, you can log in with the new user and explore the dashboard.

Check out the [troubleshooting guides](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/troubleshooting/create-medusa-app-errors/index.html.md) for help.

***

## Step 2: Install a Medusa Plugin Project

A plugin is a package of reusable Medusa customizations that you can install in any Medusa application. You can add in the plugin [API Routes](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/index.html.md), [Workflows](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md), and other customizations, as you'll see in this guide. Afterward, you can test it out locally in a Medusa application, then publish it to npm to install and use it in any Medusa application.

Refer to the [Plugins](https://docs.medusajs.com/docs/learn/fundamentals/plugins/index.html.md) documentation to learn more about plugins.

A Medusa plugin is set up in a different project, giving you the flexibility in building and publishing it, while providing you with the tools to test it out locally in a Medusa application.

To create a new Medusa plugin project, run the following command in a directory different than that of the Medusa application:

```bash npm2yarn
npx create-medusa-app@latest medusa-plugin-magento --plugin
```

Where `medusa-plugin-magento` is the name of the plugin's directory and the name set in the plugin's `package.json`. So, if you wish to publish it to NPM later under a different name, you can change it here in the command or later in `package.json`.

Once the installation process is done, a new directory named `medusa-plugin-magento` will be created with the plugin project files.

![Directory structure of a plugin project](https://res.cloudinary.com/dza7lstvk/image/upload/v1737019441/Medusa%20Book/project-dir_q4xtri.jpg)

***

## Step 3: Set up Plugin in Medusa Application

Before you start your development, you'll set up the plugin in the Medusa application you installed in the first step. This will allow you to test the plugin during your development process.

In the plugin's directory, run the following command to publish the plugin to the local package registry:

```bash title="Plugin project"
npx medusa plugin:publish
```

This command uses [Yalc](https://github.com/wclr/yalc) under the hood to publish the plugin to a local package registry. The plugin is published locally under the name you specified in `package.json`.

Next, you'll install the plugin in the Medusa application from the local registry.

If you've installed your Medusa project before v2.3.1, you must install [yalc](https://github.com/wclr/yalc) as a development dependency first.

Run the following command in the Medusa application's directory to install the plugin:

```bash title="Medusa application"
npx medusa plugin:add medusa-plugin-magento
```

This command installs the plugin in the Medusa application from the local package registry.

Next, register the plugin in the `medusa-config.ts` file of the Medusa application:

```ts title="medusa-config.ts"
module.exports = defineConfig({
  // ...
  plugins: [
    {
      resolve: "medusa-plugin-magento",
      options: {
        // TODO add options
      },
    },
  ],
})
```

You add the plugin to the array of plugins. Later, you'll pass options useful to retrieve data from Magento.

Finally, to ensure your plugin's changes are constantly published to the local registry, simplifying your testing process, keep the following command running in the plugin project during development:

```bash title="Plugin project"
npx medusa plugin:develop
```

***

## Step 4: Implement Magento Module

To connect to external applications in Medusa, you create a custom module. A module is a reusable 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.

In this step, you'll create a Magento Module in the Magento plugin that connects to a Magento server's REST APIs and retrieves data, such as products.

Refer to the [Modules](https://docs.medusajs.com/docs/learn/fundamentals/modules/index.html.md) documentation to learn more about modules.

### Create Module Directory

A module is created under the `src/modules` directory of your plugin. So, create the directory `src/modules/magento`.

![Diagram showcasing the module directory to create](https://res.cloudinary.com/dza7lstvk/image/upload/v1739272368/magento-1_ikev4x.jpg)

### Create Module's 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 external systems or the database, which is useful if your module defines tables in the database.

In this section, you'll create the Magento Module's service that connects to Magento's REST APIs and retrieves data.

Start by creating the file `src/modules/magento/service.ts` in the plugin with the following content:

![Diagram showcasing the service file to create](https://res.cloudinary.com/dza7lstvk/image/upload/v1739272483/magento-2_ajetpr.jpg)

```ts title="src/modules/magento/service.ts"
type Options = {
  baseUrl: string
  storeCode?: string
  username: string
  password: string
  migrationOptions?: {
    imageBaseUrl?: string
  }
}

export default class MagentoModuleService {
  private options: Options

  constructor({}, options: Options) {
    this.options = {
      ...options,
      storeCode: options.storeCode || "default",
    }
  }
}
```

You create a `MagentoModuleService` that has an `options` property to store the module's options. These options include:

- `baseUrl`: The base URL of the Magento server.
- `storeCode`: The store code of the Magento store, which is `default` by default.
- `username`: The username of a Magento admin user to authenticate with the Magento server.
- `password`: The password of the Magento admin user.
- `migrationOptions`: Additional options useful for migrating data, such as the base URL to use for product images.

The service's constructor accepts as a first parameter the [Module Container](https://docs.medusajs.com/docs/learn/fundamentals/modules/container/index.html.md), which allows you to access resources available for the module. As a second parameter, it accepts the module's options.

### Add Authentication Logic

To authenticate with the Magento server, you'll add a method to the service that retrieves an access token from Magento using the username and password in the options. This access token is used in subsequent requests to the Magento server.

First, add the following property to the `MagentoModuleService` class:

```ts title="src/modules/magento/service.ts"
export default class MagentoModuleService {
  private accessToken: {
    token: string
    expiresAt: Date
  }
  // ...
}
```

You add an `accessToken` property to store the access token and its expiration date. The access token Magento returns expires after four hours, so you store the expiration date to know when to refresh the token.

Next, add the following `authenticate` method to the `MagentoModuleService` class:

```ts title="src/modules/magento/service.ts"
import { MedusaError } from "@medusajs/framework/utils"

export default class MagentoModuleService {
  // ...
  async authenticate() {
    const response = await fetch(`${this.options.baseUrl}/rest/${this.options.storeCode}/V1/integration/admin/token`, {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
      },
      body: JSON.stringify({ username: this.options.username, password: this.options.password }),
    })

    const token = await response.text()

    if (!response.ok) {
      throw new MedusaError(MedusaError.Types.UNAUTHORIZED, `Failed to authenticate with Magento: ${token}`)
    }

    this.accessToken = {
      token: token.replaceAll("\"", ""),
      expiresAt: new Date(Date.now() + 4 * 60 * 60 * 1000), // 4 hours in milliseconds
    }
  }
}
```

You create an `authenticate` method that sends a POST request to the Magento server's `/rest/{storeCode}/V1/integration/admin/token` endpoint, passing the username and password in the request body.

If the request is successful, you store the access token and its expiration date in the `accessToken` property. If the request fails, you throw a `MedusaError` with the error message returned by Magento.

Lastly, add an `isAccessTokenExpired` method that checks if the access token has expired:

```ts title="src/modules/magento/service.ts"
export default class MagentoModuleService {
  // ...
  async isAccessTokenExpired(): Promise<boolean> {
    return !this.accessToken || this.accessToken.expiresAt < new Date()
  }
}
```

In the `isAccessTokenExpired` method, you return a boolean indicating whether the access token has expired. You'll use this in later methods to check if you need to refresh the access token.

### Retrieve Products from Magento

Next, you'll add a method that retrieves products from Magento. Due to limitations in Magento's API that makes it difficult to differentiate between simple products that don't belong to a configurable product and those that do, you'll only retrieve configurable products and their children. You'll also retrieve the configurable attributes of the product, such as color and size.

First, you'll add some types to represent a Magento product and its attributes. Create the file `src/modules/magento/types.ts` in the plugin with the following content:

![Diagram showcasing the types file to create](https://res.cloudinary.com/dza7lstvk/image/upload/v1739346287/Medusa%20Resources/magento-3_fpghog.jpg)

```ts title="src/modules/magento/types.ts"
export type MagentoProduct = {
  id: number
  sku: string
  name: string
  price: number
  status: number
  // not handling other types
  type_id: "simple" | "configurable"
  created_at: string
  updated_at: string
  extension_attributes: {
    category_links: {
      category_id: string
    }[]
    configurable_product_links?: number[] 
    configurable_product_options?: {
      id: number
      attribute_id: string
      label: string
      position: number
      values: {
        value_index: number
      }[]
    }[]
  }
  media_gallery_entries: {
    id: number
    media_type: string
    label: string
    position: number
    disabled: boolean
    types: string[]
    file: string
  }[]
  custom_attributes: {
    attribute_code: string
    value: string
  }[]
  // added by module
  children?: MagentoProduct[]
}

export type MagentoAttribute = {
  attribute_code: string
  attribute_id: number
  default_frontend_label: string
  options: {
    label: string
    value: string
  }[]
}

export type MagentoPagination = {
  search_criteria: {
    filter_groups: [],
    page_size: number
    current_page: number
  }
  total_count: number
}

export type MagentoPaginatedResponse<TData> = {
  items: TData[]
} & MagentoPagination
```

You define the following types:

- `MagentoProduct`: Represents a product in Magento.
- `MagentoAttribute`: Represents an attribute in Magento.
- `MagentoPagination`: Represents the pagination information returned by Magento's API.
- `MagentoPaginatedResponse`: Represents a paginated response from Magento's API for a specific item type, such as products.

Next, add the `getProducts` method to the `MagentoModuleService` class:

```ts title="src/modules/magento/service.ts"
export default class MagentoModuleService {
  // ...
  async getProducts(options?: {
    currentPage?: number
    pageSize?: number
  }): Promise<{
    products: MagentoProduct[]
    attributes: MagentoAttribute[]
    pagination: MagentoPagination
  }> {
    const { currentPage = 1, pageSize = 100 } = options || {}
    const getAccessToken = await this.isAccessTokenExpired()
    if (getAccessToken) {
      await this.authenticate()
    }

    // TODO prepare query params
  }
}
```

The `getProducts` method receives an optional `options` object with the `currentPage` and `pageSize` properties. So far, you check if the access token has expired and, if so, retrieve a new one using the `authenticate` method.

Next, you'll prepare the query parameters to pass in the request that retrieves products. Replace the `TODO` with the following:

```ts title="src/modules/magento/service.ts"
const searchQuery = new URLSearchParams()
// pass pagination parameters
searchQuery.append(
  "searchCriteria[currentPage]", 
  currentPage?.toString() || "1"
)
searchQuery.append(
  "searchCriteria[pageSize]", 
  pageSize?.toString() || "100"
)

// retrieve only configurable products
searchQuery.append(
  "searchCriteria[filter_groups][1][filters][0][field]", 
  "type_id"
)
searchQuery.append(
  "searchCriteria[filter_groups][1][filters][0][value]", 
  "configurable"
)
searchQuery.append(
  "searchCriteria[filter_groups][1][filters][0][condition_type]", 
  "in"
)

// TODO send request to retrieve products
```

You create a `searchQuery` object to store the query parameters to pass in the request. Then, you add the pagination parameters and the filter to retrieve only configurable products.

Next, you'll send the request to retrieve products from Magento. Replace the `TODO` with the following:

```ts title="src/modules/magento/service.ts"
const { items: products, ...pagination }: MagentoPaginatedResponse<MagentoProduct> = await fetch(
  `${this.options.baseUrl}/rest/${this.options.storeCode}/V1/products?${searchQuery}`, 
  {
    headers: {
      "Authorization": `Bearer ${this.accessToken.token}`,
    },
  }
).then((res) => res.json())
.catch((err) => {
  console.log(err)
  throw new MedusaError(
    MedusaError.Types.INVALID_DATA, 
    `Failed to get products from Magento: ${err.message}`
  )
})

// TODO prepare products
```

You send a `GET` request to the Magento server's `/rest/{storeCode}/V1/products` endpoint, passing the query parameters in the URL. You also pass the access token in the `Authorization` header.

Next, you'll prepare the retrieved products by retrieving their children, configurable attributes, and modifying their image URLs. Replace the `TODO` with the following:

```ts title="src/modules/magento/service.ts"
const attributeIds: string[] = []

await promiseAll(
  products.map(async (product) => {
    // retrieve its children
    product.children = await fetch(
      `${this.options.baseUrl}/rest/${this.options.storeCode}/V1/configurable-products/${product.sku}/children`,
      {
        headers: {
          "Authorization": `Bearer ${this.accessToken.token}`,
        },
      }
    ).then((res) => res.json())
    .catch((err) => {
      throw new MedusaError(
        MedusaError.Types.INVALID_DATA, 
        `Failed to get product children from Magento: ${err.message}`
      )
    })

    product.media_gallery_entries = product.media_gallery_entries.map(
      (entry) => ({
        ...entry,
        file: `${this.options.migrationOptions?.imageBaseUrl}${entry.file}`,
      }
    ))

    attributeIds.push(...(
      product.extension_attributes.configurable_product_options?.map(
        (option) => option.attribute_id) || []
      )
    )
  })
)

// TODO retrieve attributes
```

You loop over the retrieved products and retrieve their children using the `/rest/{storeCode}/V1/configurable-products/{sku}/children` endpoint. You also modify the image URLs to use the base URL in the migration options, if provided.

In addition, you store the IDs of the configurable products' attributes in the `attributeIds` array. You'll add a method that retrieves these attributes.

Add the new method `getAttributes` to the `MagentoModuleService` class:

```ts title="src/modules/magento/service.ts"
export default class MagentoModuleService {
  // ...
  async getAttributes({
    ids,
  }: {
    ids: string[]
  }): Promise<MagentoAttribute[]> {
    const getAccessToken = await this.isAccessTokenExpired()
    if (getAccessToken) {
      await this.authenticate()
    }

    // filter by attribute IDs
    const searchQuery = new URLSearchParams()
    searchQuery.append(
      "searchCriteria[filter_groups][0][filters][0][field]", 
      "attribute_id"
    )
    searchQuery.append(
      "searchCriteria[filter_groups][0][filters][0][value]", 
      ids.join(",")
    )
    searchQuery.append(
      "searchCriteria[filter_groups][0][filters][0][condition_type]", 
      "in"
    )

    const { 
      items: attributes,
    }: MagentoPaginatedResponse<MagentoAttribute> = await fetch(
      `${this.options.baseUrl}/rest/${this.options.storeCode}/V1/products/attributes?${searchQuery}`, 
      {
        headers: {
          "Authorization": `Bearer ${this.accessToken.token}`,
        },
      }
    ).then((res) => res.json())
    .catch((err) => {
      throw new MedusaError(
        MedusaError.Types.INVALID_DATA, 
        `Failed to get attributes from Magento: ${err.message}`
      )
    })

    return attributes
  }
}
```

The `getAttributes` method receives an object with the `ids` property, which is an array of attribute IDs. You check if the access token has expired and, if so, retrieve a new one using the `authenticate` method.

Next, you prepare the query parameters to pass in the request to retrieve attributes. You send a `GET` request to the Magento server's `/rest/{storeCode}/V1/products/attributes` endpoint, passing the query parameters in the URL. You also pass the access token in the `Authorization` header.

Finally, you return the retrieved attributes.

Now, go back to the `getProducts` method and replace the `TODO` with the following:

```ts title="src/modules/magento/service.ts"
const attributes = await this.getAttributes({ ids: attributeIds })
    
return { products, attributes, pagination }
```

You retrieve the configurable products' attributes using the `getAttributes` method and return the products, attributes, and pagination information.

You'll use this method in a later step to retrieve products from Magento.

### Export Module Definition

The final piece to a module is its definition, which you export in an `index.ts` file at its root directory. This definition tells Medusa the name of the module and its service.

So, create the file `src/modules/magento/index.ts` with the following content:

![Diagram showcasing the module definition file to create](https://res.cloudinary.com/dza7lstvk/image/upload/v1739348316/Medusa%20Resources/magento-4_bmepvh.jpg)

```ts title="src/modules/magento/index.ts"
import { Module } from "@medusajs/framework/utils"
import MagentoModuleService from "./service"

export const MAGENTO_MODULE = "magento"

export default Module(MAGENTO_MODULE, {
  service: MagentoModuleService,
})
```

You use the `Module` function from the Modules SDK to create the module's definition. It accepts two parameters:

1. The module's name, which is `magento`.
2. An object with a required property `service` indicating the module's service.

You'll later use the module's service to retrieve products from Magento.

### Pass Options to Plugin

As mentioned earlier when you registered the plugin in the Medusa Application's `medusa-config.ts` file, you can pass options to the plugin. These options are then passed to the modules in the plugin.

So, add the following options to the plugin's registration in the `medusa-config.ts` file of the Medusa application:

```ts title="medusa-config.ts"
module.exports = defineConfig({
  // ...
  plugins: [
    {
      resolve: "medusa-plugin-magento",
      options: {
        baseUrl: process.env.MAGENTO_BASE_URL,
        username: process.env.MAGENTO_USERNAME,
        password: process.env.MAGENTO_PASSWORD,
        migrationOptions: {
          imageBaseUrl: process.env.MAGENTO_IMAGE_BASE_URL,
        },
      },
    },
  ],
})
```

You pass the options that you defined in the `MagentoModuleService`. Make sure to also set their environment variables in the `.env` file:

```bash
MAGENTO_BASE_URL=https://magento.example.com
MAGENTO_USERNAME=admin
MAGENTO_PASSWORD=password
MAGENTO_IMAGE_BASE_URL=https://magento.example.com/pub/media/catalog/product
```

Where:

- `MAGENTO_BASE_URL`: The base URL of the Magento server. It can also be a local URL, such as `http://localhost:8080`.
- `MAGENTO_USERNAME`: The username of a Magento admin user to authenticate with the Magento server.
- `MAGENTO_PASSWORD`: The password of the Magento admin user.
- `MAGENTO_IMAGE_BASE_URL`: The base URL to use for product images. Magento stores product images in the `pub/media/catalog/product` directory, so you can reference them directly or use a CDN URL. If the URLs of product images in the Medusa server already have a different base URL, you can omit this option.

Medusa supports integrating third-party services, such as [S3](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/infrastructure-modules/file/s3/index.html.md), in a File Module Provider. Refer to the [File Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/infrastructure-modules/file/index.html.md) documentation to find other module providers and how to create a custom provider.

You can now use the Magento Module to migrate data, which you'll do in the next steps.

***

## Step 5: Build Product Migration Workflow

In this section, you'll add the feature to migrate products from Magento to Medusa. To implement this feature, you'll use 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 an API route or a scheduled job.

By implementing the migration feature in a workflow, you ensure that the data remains consistent and that the migration process can be rolled back if an error occurs.

Refer to the [Workflows](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md) documentation to learn more about workflows.

### Workflow Steps

The workflow you'll create will have the following steps:

- [getMagentoProductsStep](#getMagentoProductsStep): Retrieve products from Magento using the Magento Module.
- [useQueryGraphStep](https://docs.medusajs.com/references/helper-steps/useQueryGraphStep/index.html.md): Retrieve Medusa store details, which you'll need when creating the products.
- [useQueryGraphStep](https://docs.medusajs.com/references/helper-steps/useQueryGraphStep/index.html.md): Retrieve a shipping profile, which you'll associate the created products with.
- [useQueryGraphStep](https://docs.medusajs.com/references/helper-steps/useQueryGraphStep/index.html.md): Retrieve Magento products that are already in Medusa to update them, instead of creating them.
- [createProductsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createProductsWorkflow/index.html.md): Create products in the Medusa application.
- [updateProductsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateProductsWorkflow/index.html.md): Update existing products in the Medusa application.

You only need to implement the `getMagentoProductsStep` step, which retrieves the products from Magento. The other steps and workflows are provided by Medusa's `@medusajs/medusa/core-flows` package.

### getMagentoProductsStep

The first step of the workflow retrieves and returns the products from Magento.

In your plugin, create the file `src/workflows/steps/get-magento-products.ts` with the following content:

![Diagram showcasing the get-magento-products file to create](https://res.cloudinary.com/dza7lstvk/image/upload/v1739349590/Medusa%20Resources/magento-5_ueb4wn.jpg)

```ts title="src/workflows/steps/get-magento-products.ts"
import { createStep, StepResponse } from "@medusajs/framework/workflows-sdk"
import { MAGENTO_MODULE } from "../../modules/magento"
import MagentoModuleService from "../../modules/magento/service"

type GetMagentoProductsInput = {
  currentPage: number
  pageSize: number
}

export const getMagentoProductsStep = createStep(
  "get-magento-products",
  async ({ currentPage, pageSize }: GetMagentoProductsInput, { container }) => {
    const magentoModuleService: MagentoModuleService = 
      container.resolve(MAGENTO_MODULE)

    const response = await magentoModuleService.getProducts({
      currentPage,
      pageSize,
    })

    return new StepResponse(response)
  }
)
```

You create a step using `createStep` from the Workflows SDK. It accepts two parameters:

1. The step's name, which is `get-magento-products`.
2. An async function that executes the step's logic. The function receives two parameters:
   - The input data for the step, which in this case is the pagination parameters.
   - An object holding the workflow's context, including the [Medusa Container](https://docs.medusajs.com/docs/learn/fundamentals/medusa-container/index.html.md) that allows you to resolve Framework and commerce tools.

In the step function, you resolve the Magento Module's service from the container, then use its `getProducts` method to retrieve the products from Magento.

Steps that return data must return them in a `StepResponse` instance. The `StepResponse` constructor accepts as a parameter the data to return.

### Create migrateProductsFromMagentoWorkflow

You'll now create the workflow that migrates products from Magento using the step you created and steps from Medusa's `@medusajs/medusa/core-flows` package.

In your plugin, create the file `src/workflows/migrate-products-from-magento.ts` with the following content:

![Diagram showcasing the migrate-products-from-magento file to create](https://res.cloudinary.com/dza7lstvk/image/upload/v1739349820/Medusa%20Resources/magento-6_jjdaxj.jpg)

```ts title="src/workflows/migrate-products-from-magento.ts"
import { 
  createWorkflow, transform, WorkflowResponse,
} from "@medusajs/framework/workflows-sdk"
import { 
  CreateProductWorkflowInputDTO, UpsertProductDTO,
} from "@medusajs/framework/types"
import { 
  createProductsWorkflow, 
  updateProductsWorkflow, 
  useQueryGraphStep,
} from "@medusajs/medusa/core-flows"
import { getMagentoProductsStep } from "./steps/get-magento-products"

type MigrateProductsFromMagentoWorkflowInput = {
  currentPage: number
  pageSize: number
}

export const migrateProductsFromMagentoWorkflowId = 
  "migrate-products-from-magento"

export const migrateProductsFromMagentoWorkflow = createWorkflow(
  {
    name: migrateProductsFromMagentoWorkflowId,
    retentionTime: 10000,
    store: true,
  },
  (input: MigrateProductsFromMagentoWorkflowInput) => {
    const { pagination, products, attributes } = getMagentoProductsStep(
      input
    )
    // TODO prepare data to create and update products
  }
)
```

You create a workflow using `createWorkflow` from the Workflows SDK. It accepts two parameters:

1. An object with the workflow's configuration, including the name and whether to store the workflow's executions. You enable storing the workflow execution so that you can view it later in the Medusa Admin dashboard.
2. A worflow constructor function, which holds the workflow's implementation. The function receives the input data for the workflow, which is the pagination parameters.

In the workflow constructor function, you use the `getMagentoProductsStep` step to retrieve the products from Magento, passing it the pagination parameters from the workflow's input.

Next, you'll retrieve the Medusa store details and shipping profiles. These are necessary to prepare the data of the products to create or update.

Replace the `TODO` in the workflow function with the following:

```ts title="src/workflows/migrate-products-from-magento.ts"
const { data: stores } = useQueryGraphStep({
  entity: "store",
  fields: ["supported_currencies.*", "default_sales_channel_id"],
  pagination: {
    take: 1,
    skip: 0,
  },
})

const { data: shippingProfiles } = useQueryGraphStep({
  entity: "shipping_profile",
  fields: ["id"],
  pagination: {
    take: 1,
    skip: 0,
  },
}).config({ name: "get-shipping-profiles" })

// TODO retrieve existing products
```

You use the `useQueryGraphStep` step to retrieve the store details and shipping profiles. `useQueryGraphStep` is a Medusa step that wraps [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), allowing you to use it in a workflow. Query is a tool that retrieves data across modules.

Whe retrieving the store details, you specifically retrieve its supported currencies and default sales channel ID. You'll associate the products with the store's default sales channel, and set their variant prices in the supported currencies. You'll also associate the products with a shipping profile.

Next, you'll retrieve products that were previously migrated from Magento to determine which products to create or update. Replace the `TODO` with the following:

```ts title="src/workflows/migrate-products-from-magento.ts"
const externalIdFilters = transform({
  products,
}, (data) => {
  return data.products.map((product) => product.id.toString())
})

const { data: existingProducts } = useQueryGraphStep({
  entity: "product",
  fields: ["id", "external_id", "variants.id", "variants.metadata"],
  filters: {
    external_id: externalIdFilters,
  },
}).config({ name: "get-existing-products" })

// TODO prepare products to create or update
```

Since the Medusa application creates an internal representation of the workflow's constructor function, you can't manipulate data directly, as variables have no value while creating the internal representation.

Refer to the [Workflows](https://docs.medusajs.com/docs/learn/fundamentals/workflows/constructor-constraints/index.html.md) documentation to learn more about the workflow constructor function's constraints.

Instead, you can manipulate data in a workflow's constructor function using `transform` from the Workflows SDK. `transform` is a function that accepts two parameters:

- The data to transform, which in this case is the Magento products.
- A function that transforms the data. The function receives the data passed in the first parameter and returns the transformed data.

In the transformation function, you return the IDs of the Magento products. Then, you use the `useQueryGraphStep` to retrieve products in the Medusa application that have an `external_id` property matching the IDs of the Magento products. You'll use this property to store the IDs of the products in Magento.

Next, you'll prepare the data to create and update the products. Replace the `TODO` in the workflow function with the following:

```ts title="src/workflows/migrate-products-from-magento.ts" highlights={prepareHighlights}
const { 
  productsToCreate,
  productsToUpdate,
} = transform({
  products,
  attributes,
  stores,
  shippingProfiles,
  existingProducts,
}, (data) => {
  const productsToCreate = new Map<string, CreateProductWorkflowInputDTO>()
  const productsToUpdate = new Map<string, UpsertProductDTO>()

  data.products.forEach((magentoProduct) => {
    const productData: CreateProductWorkflowInputDTO | UpsertProductDTO = {
      title: magentoProduct.name,
      description: magentoProduct.custom_attributes.find(
        (attr) => attr.attribute_code === "description"
      )?.value,
      status: magentoProduct.status === 1 ? "published" : "draft",
      handle: magentoProduct.custom_attributes.find(
        (attr) => attr.attribute_code === "url_key"
      )?.value,
      external_id: magentoProduct.id.toString(),
      thumbnail: magentoProduct.media_gallery_entries.find(
        (entry) => entry.types.includes("thumbnail")
      )?.file,
      sales_channels: [{
        id: data.stores[0].default_sales_channel_id,
      }],
      shipping_profile_id: data.shippingProfiles[0].id,
    }
    const existingProduct = data.existingProducts.find((p) => p.external_id === productData.external_id)

    if (existingProduct) {
      productData.id = existingProduct.id
    }

    productData.options = magentoProduct.extension_attributes.configurable_product_options?.map((option) => {
      const attribute = data.attributes.find((attr) => attr.attribute_id === parseInt(option.attribute_id))
      return {
        title: option.label,
        values: attribute?.options.filter((opt) => {
          return option.values.find((v) => v.value_index === parseInt(opt.value))
        }).map((opt) => opt.label) || [],
      }
    }) || []

    productData.variants = magentoProduct.children?.map((child) => {
      const childOptions: Record<string, string> = {}

      child.custom_attributes.forEach((attr) => {
        const attrData = data.attributes.find((a) => a.attribute_code === attr.attribute_code)
        if (!attrData) {
          return
        }

        childOptions[attrData.default_frontend_label] = attrData.options.find((opt) => opt.value === attr.value)?.label || ""
      })

      const variantExternalId = child.id.toString()
      const existingVariant = existingProduct.variants.find((v) => v.metadata.external_id === variantExternalId)

      return {
        title: child.name,
        sku: child.sku,
        options: childOptions,
        prices: data.stores[0].supported_currencies.map(({ currency_code }) => {
          return {
            amount: child.price,
            currency_code,
          }
        }),
        metadata: {
          external_id: variantExternalId,
        },
        id: existingVariant?.id,
      }
    })

    productData.images = magentoProduct.media_gallery_entries.filter((entry) => !entry.types.includes("thumbnail")).map((entry) => {
      return {
        url: entry.file,
        metadata: {
          external_id: entry.id.toString(),
        },
      }
    })

    if (productData.id) {
      productsToUpdate.set(existingProduct.id, productData)
    } else {
      productsToCreate.set(productData.external_id!, productData)
    }
  })

  return {
    productsToCreate: Array.from(productsToCreate.values()),
    productsToUpdate: Array.from(productsToUpdate.values()),
  }
})

// TODO create and update products
```

You use `transform` again to prepare the data to create and update the products in the Medusa application. For each Magento product, you map its equivalent Medusa product's data:

- You set the product's general details, such as the title, description, status, handle, external ID, and thumbnail using the Magento product's data and custom attributes.
- You associate the product with the default sales channel and shipping profile retrieved previously.
- You map the Magento product's configurable product options to Medusa product options. In Medusa, a product's option has a label, such as "Color", and values, such as "Red". To map the option values, you use the attributes retrieved from Magento.
- You map the Magento product's children to Medusa product variants. For the variant options, you pass an object whose keys is the option's label, such as "Color", and values is the option's value, such as "Red". For the prices, you set the variant's price based on the Magento child's price for every supported currency in the Medusa store. Also, you set the Magento child product's ID in the Medusa variant's `metadata.external_id` property.
- You map the Magento product's media gallery entries to Medusa product images. You filter out the thumbnail image and set the URL and the Magento image's ID in the Medusa image's `metadata.external_id` property.

In addition, you use the existing products retrieved in the previous step to determine whether a product should be created or updated. If there's an existing product whose `external_id` matches the ID of the magento product, you set the existing product's ID in the `id` property of the product to be updated. You also do the same for its variants.

Finally, you return the products to create and update.

The last steps of the workflow is to create and update the products. Replace the `TODO` in the workflow function with the following:

```ts title="src/workflows/migrate-products-from-magento.ts"
createProductsWorkflow.runAsStep({
  input: {
    products: productsToCreate,
  },
})

updateProductsWorkflow.runAsStep({
  input: {
    products: productsToUpdate,
  },
})

return new WorkflowResponse(pagination)
```

You use the `createProductsWorkflow` and `updateProductsWorkflow` workflows from Medusa's `@medusajs/medusa/core-flows` package to create and update the products in the Medusa application.

Workflows must return an instance of `WorkflowResponse`, passing as a parameter the data to return to the workflow's executor. This workflow returns the pagination parameters, allowing you to paginate the product migration process.

You can now use this workflow to migrate products from Magento to Medusa. You'll learn how to use it in the next steps.

***

## Step 6: Schedule Product Migration

There are many ways to execute tasks asynchronously in Medusa, such as [scheduling a job](https://docs.medusajs.com/docs/learn/fundamentals/scheduled-jobs/index.html.md) or [handling emitted events](https://docs.medusajs.com/docs/learn/fundamentals/events-and-subscribers/index.html.md).

In this guide, you'll learn how to schedule the product migration at a specified interval using a scheduled job. A scheduled job is an asynchronous function that the Medusa application runs at the interval you specify during the Medusa application's runtime.

Refer to the [Scheduled Jobs](https://docs.medusajs.com/docs/learn/fundamentals/scheduled-jobs/index.html.md) documentation to learn more about scheduled jobs.

To create a scheduled job, in your plugin, create the file `src/jobs/migrate-magento.ts` with the following content:

![Diagram showcasing the migrate-magento file to create](https://res.cloudinary.com/dza7lstvk/image/upload/v1739358924/Medusa%20Resources/magento-7_rqoodo.jpg)

```ts title="src/jobs/migrate-magento.ts"
import { MedusaContainer } from "@medusajs/framework/types"
import { migrateProductsFromMagentoWorkflow } from "../workflows"

export default async function migrateMagentoJob(
  container: MedusaContainer
) {
  const logger = container.resolve("logger")
    logger.info("Migrating products from Magento...")
    
    let currentPage = 0
    const pageSize = 100
    let totalCount = 0
  
    do {
      currentPage++
  
      const { 
        result: pagination,
      } = await migrateProductsFromMagentoWorkflow(container).run({
        input: {
          currentPage,
          pageSize,
        },
      })
  
      totalCount = pagination.total_count
    } while (currentPage * pageSize < totalCount)
  
    logger.info("Finished migrating products from Magento")
}

export const config = {
  name: "migrate-magento-job",
  schedule: "0 0 * * *",
}
```

A scheduled job file must export:

- An asynchronous function that executes the job's logic. The function receives the [Medusa container](https://docs.medusajs.com/docs/learn/fundamentals/medusa-container/index.html.md) as a parameter.
- An object with the job's configuration, including the name and the schedule. The schedule is a cron job pattern as a string.

In the job function, you resolve the [logger](https://docs.medusajs.com/docs/learn/debugging-and-testing/logging/index.html.md) from the container to log messages. Then, you paginate the product migration process by running the `migrateProductsFromMagentoWorkflow` workflow at each page until you've migrated all products. You use the pagination result returned by the workflow to determine whether there are more products to migrate.

Based on the job's configurations, the Medusa application will run the job at midnight every day.

### Test it Out

To test out this scheduled job, first, change the configuration to run the job every minute:

```ts title="src/jobs/migrate-magento.ts"
export const config = {
  // ...
  schedule: "* * * * *",
}
```

Then, make sure to run the `plugin:develop` command in the plugin if you haven't already:

```bash
npx medusa plugin:develop
```

This ensures that the plugin's latest changes are reflected in the Medusa application.

Finally, start the Medusa application that the plugin is installed in:

```bash npm2yarn
npm run dev
```

After a minute, you'll see a message in the terminal indicating that the migration started:

```plain title="Terminal"
info: Migrating products from Magento...
```

Once the migration is done, you'll see the following message:

```plain title="Terminal"
info: Finished migrating products from Magento
```

To confirm that the products were migrated, open the Medusa Admin dashboard at `http://localhost:9000/app` and log in. Then, click on Products in the sidebar. You'll see your magento products in the list of products.

![Click on products at the sidebar on the right, then view the products in the table in the middle.](https://res.cloudinary.com/dza7lstvk/image/upload/v1739359394/Medusa%20Resources/Screenshot_2025-02-12_at_1.22.44_PM_uva98i.png)

***

## Next Steps

You've now implemented the logic to migrate products from Magento to Medusa. You can re-use the plugin across Medusa applications. You can also expand on the plugin to:

- Migrate other entities, such as orders, customers, and categories. Migrating other entities follows the same pattern as migrating products, using workflows and scheduled jobs. You only need to format the data to be migrated as needed.
- Allow triggering migrations from the Medusa Admin dashboard using [Admin Customizations](https://docs.medusajs.com/docs/learn/fundamentals/admin/index.html.md). This feature is available in the [Example Repository](https://github.com/medusajs/example-repository/tree/main/src/admin).

If you're new to Medusa, check out the [main documentation](https://docs.medusajs.com/docs/learn/index.html.md), 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](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/index.html.md).


# Integrate Medusa with Resend (Email Notifications)

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](https://resend.com/docs/introduction) 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.

[Example Repository](https://github.com/medusajs/examples/tree/main/resend-integration): Find the full code of the guide in this repository.

***

## Step 1: Install a Medusa Application

### Prerequisites

- [Node.js v20+](https://nodejs.org/en/download)
- [Git CLI tool](https://git-scm.com/downloads)
- [PostgreSQL](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 in a directory with your project's name, and the Next.js Starter Storefront in a directory with the `{project-name}-storefront` name.

The Medusa application is composed of a headless Node.js server and an admin dashboard. The storefront is installed or custom-built separately and connects to the Medusa application through its REST endpoints, called [API routes](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/index.html.md). Learn more about Medusa's architecture in [this documentation](https://docs.medusajs.com/docs/learn/introduction/architecture/index.html.md).

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](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/troubleshooting/create-medusa-app-errors/index.html.md) for help.

***

## Step 2: Prepare Resend Account

If you don't have a Resend Account, create one on [their website](https://resend.com/emails).

In addition, Resend allows you to send emails from the address `onboarding@resend.dev` 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.

![Click on Domains in the sidebar then on the Add Domain button in the middle of the page.](https://res.cloudinary.com/dza7lstvk/image/upload/v1732523238/Medusa%20Resources/Screenshot_2024-11-25_at_10.18.11_AM_pmqgtv.png)

3\. In the form that opens, enter your domain name and select a region close to your users, then click Add.

![A pop-up window with Domain and Region fields.](https://res.cloudinary.com/dza7lstvk/image/upload/v1732523280/Medusa%20Resources/Screenshot_2024-11-25_at_10.18.52_AM_sw2pr4.png)

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.

The steps to add DNS records are different for each provider, so refer to your provider's documentation or knowledge base for more details.

![The DNS records to add are in a table under the DNS Records section. Once added, click the Verify DNS Records button at the top right.](https://res.cloudinary.com/dza7lstvk/image/upload/v1732523394/Medusa%20Resources/Screenshot_2024-11-25_at_10.20.56_AM_ktvbse.png)

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 npm2yarn
npm install resend
```

2\. [react-email](https://github.com/resend/react-email),  which is a package created by Resend to create email templates with React:

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

Learn more about modules in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/modules/index.html.md).

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

```ts title="src/modules/resend/service.ts" highlights={serviceHighlights1}
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](https://docs.medusajs.com/docs/learn/debugging-and-testing/logging/index.html.md), 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 title="src/modules/resend/service.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](https://docs.medusajs.com/docs/learn/fundamentals/modules/container/index.html.md), which is the module's local registry that the Medusa application adds Framework tools to. In this service, you resolve the [Logger utility](https://docs.medusajs.com/docs/learn/debugging-and-testing/logging/index.html.md) 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 title="src/modules/resend/service.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:

```ts title="src/modules/resend/service.ts" highlights={serviceHighlights2}
// 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:

```ts title="src/modules/resend/service.ts" highlights={serviceHighlights3}
// 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 title="src/modules/resend/index.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 title="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`:

```shell
RESEND_FROM_EMAIL=onboarding@resend.dev
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](#step-2-prepare-resend-account), change this email to an email from your custom domain. Otherwise, you can use `onboarding@resend.dev` 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.

![Click on the API keys in the sidebar, then click on the Create API Key button at the top right](https://res.cloudinary.com/dza7lstvk/image/upload/v1732535399/Medusa%20Resources/Screenshot_2024-11-25_at_10.22.25_AM_v4d09s.png)

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

![The form to create an API key with fields for the API key's name, permissions, and domain](https://res.cloudinary.com/dza7lstvk/image/upload/v1732535464/Medusa%20Resources/Screenshot_2024-11-25_at_10.23.26_AM_g7gcuc.png)

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

![Click the copy icon to copy the API key](https://res.cloudinary.com/dza7lstvk/image/upload/v1732535791/Medusa%20Resources/Screenshot_2024-11-25_at_10.23.43_AM_divins.png)

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](https://github.com/resend/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:

```tsx title="src/modules/resend/emails/order-placed.tsx" highlights={templateHighlights} collapsibleLines="1-17" expandMoreLabel="Show Imports"
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">
          {/* Header */}
          <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>

          {/* Thank You Message */}
          <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>

          {/* Promotional Banner */}
          {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>
          )}

          {/* Order Items */}
          <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">
                    <Img
                      src={item.thumbnail ?? ""}
                      alt={item.product_title ?? ""}
                      className="rounded-lg"
                      width="100%"
                    />
                  </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>
            ))}

            {/* Order Summary */}
            <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>

          {/* Footer */}
          <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 support@medusajs.com.
            </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 title="src/modules/resend/service.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](https://react.email/docs/cli).

First, install the CLI tool in your Medusa application:

```bash npm2yarn
npm install -D react-email
```

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

```tsx title="src/modules/resend/emails/order-placed.tsx"
const mockOrder = {
  "order": {
    "id": "order_01JSNXDH9BPJWWKVW03B9E9KW8",
    "display_id": 1,
    "email": "afsaf@gmail.com",
    "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": "afsaf@gmail.com",
      "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 npm2yarn
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.

![The email template rendered in the browser](https://res.cloudinary.com/dza7lstvk/image/upload/v1745568201/Medusa%20Resources/Screenshot_2025-04-25_at_10.41.26_AM_u86abc.png)

***

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

Learn more about Medusa's event system in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/events-and-subscribers/index.html.md).

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

Learn more about workflows in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md)

#### 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 title="src/workflows/steps/send-notification.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](https://docs.medusajs.com/docs/learn/fundamentals/medusa-container/index.html.md), 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.

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:

```ts title="src/workflows/send-order-confirmation.ts" highlights={workflowHighlights}
import { 
  createWorkflow, 
  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) => {
    // @ts-ignore
    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,
      },
    })
    
    const notification = 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](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), a tool that allows you to retrieve data across modules. You use it to retrieve the order's details.
2. `sendNotificationStep` which is the step you implemented. You pass it an array with one object, which is the notification's details having 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.

A workflow's constructor function has some constraints in implementation. Learn more about them in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/workflows/constructor-constraints/index.html.md).

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:

```ts title="src/subscribers/order-placed.ts" highlights={subscriberHighlights}
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 paramter 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](https://docs.medusajs.com/docs/learn/fundamentals/medusa-container/index.html.md).

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](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/nextjs-starter/index.html.md) that you installed as part of installing Medusa.

Start your Medusa application first:

```bash npm2yarn
npm run dev
```

Then, in the Next.js Starter Storefront's directory (which was installed in a directory outside of the Medusa application's directory with the name `{project-name}-storefront`, where `{project-name}` is the name of the Medusa application's directory), run the following command to start the storefront:

```bash npm2yarn
npm run dev
```

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

1. Go to Menu -> Store.

![Choose Store from Menu](https://res.cloudinary.com/dza7lstvk/image/upload/v1732539139/Medusa%20Resources/Screenshot_2024-11-25_at_2.51.59_PM_fubiwj.png)

2\. Click on a product, select its options, and add it to the cart.

![Choose an option, such as size, then click on the Add to cart button](https://res.cloudinary.com/dza7lstvk/image/upload/v1732539227/Medusa%20Resources/Screenshot_2024-11-25_at_2.53.11_PM_iswcjy.png)

3\. Click on Cart at the top right, then click Go to Cart.

![Cart is at the top right. It opens a dropdown with a Go to Cart button](https://res.cloudinary.com/dza7lstvk/image/upload/v1732539354/Medusa%20Resources/Screenshot_2024-11-25_at_2.54.44_PM_b1pnlu.png)

4\. On the cart's page, click on the "Go to checkout" button.

![The Go to checkout button is at the right side of the page](https://res.cloudinary.com/dza7lstvk/image/upload/v1732539443/Medusa%20Resources/Screenshot_2024-11-25_at_2.56.27_PM_cvqshj.png)

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.

![Enter your Resend account email if you didn't set up a custom domain](https://res.cloudinary.com/dza7lstvk/image/upload/v1732539536/Medusa%20Resources/Screenshot_2024-11-25_at_2.58.31_PM_wmlh60.png)

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.

![Example of order-confirmation email](https://res.cloudinary.com/dza7lstvk/image/upload/v1732551372/Medusa%20Resources/Screenshot_2024-11-25_at_6.15.59_PM_efyuoj.png)

***

## 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](https://docs.medusajs.com/references/events/index.html.md) for a list of all events that the Medusa application emits.

If you're new to Medusa, check out the [main documentation](https://docs.medusajs.com/docs/learn/index.html.md), 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](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/index.html.md).


# Integrate Segment (Analytics) with Medusa

In this tutorial, you'll learn how to integrate Segment with Medusa to track events and analytics.

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.

To track analytics in your Medusa application, you can integrate [Segment](https://segment.com/), a service that collects analytics from multiple sources and sends them to various destinations. This tutorial will help you set up Segment in your Medusa application and track common events.

## Summary

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

- Install and set up Medusa.
- Integrate Segment with your Medusa application.
- Handle Medusa's `order.placed` event to track order placements.
- Track custom events in your Medusa application with Segment.

You can follow this tutorial whether you're new to Medusa or an advanced Medusa developer.

![Diagram showcasing the integration of Segment with Medusa](https://res.cloudinary.com/dza7lstvk/image/upload/v1748264333/Medusa%20Book/segment-overview_apkrtp.jpg)

[Example Repository](https://github.com/medusajs/examples/tree/main/segment-integration): Find the full code of the guide in this repository.

***

## Step 1: Install a Medusa Application

### Prerequisites

- [Node.js v20+](https://nodejs.org/en/download)
- [Git CLI tool](https://git-scm.com/downloads)
- [PostgreSQL](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](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/nextjs-starter/index.html.md), choose "Yes."

Afterwards, the installation process will start, which will install the Medusa application in a directory with your project's name and the Next.js Starter Storefront in a separate directory named `{project-name}-storefront`.

The Medusa application is composed of a headless Node.js server and an admin dashboard. The storefront is installed or custom-built separately and connects to the Medusa application through its REST endpoints, called [API routes](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/index.html.md). Learn more in [Medusa's Architecture documentation](https://docs.medusajs.com/docs/learn/introduction/architecture/index.html.md).

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.

Check out the [troubleshooting guides](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/troubleshooting/create-medusa-app-errors/index.html.md) for help.

***

## Step 2: Create Segment 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 [Analytics Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/infrastructure-modules/analytics/index.html.md) provides an interface to track events in your Medusa application. It delegates the actual tracking to the configured Analytics Module Provider.

In this step, you'll integrate Segment as an Analytics Module Provider. Later, you'll use it to track events in your Medusa application.

Refer to the [Modules](https://docs.medusajs.com/docs/learn/fundamentals/modules/index.html.md) documentation to learn more about modules in Medusa.

### a. Install Segment Node SDK

Before you create the Segment Module Provider, you'll install the Segment Node SDK to interact with Segment's API.

Run the following command in your Medusa application's directory:

```bash npm2yarn
npm install @segment/analytics-node
```

You'll use the SDK in the next steps.

### b. Create Module Directory

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

### c. Create Segment Module's Service

A module has a service that contains its logic. For Analytics Module Providers, the service implements the logic to track events in the third-party service.

To create the service of the Segment Analytics Module Provider, create the file `src/modules/segment/service.ts` with the following content:

```ts title="src/modules/segment/service.ts" highlights={serviceHighlights}
import { 
  AbstractAnalyticsProviderService,
  MedusaError,
} from "@medusajs/framework/utils"
import { Analytics } from "@segment/analytics-node"

type Options = {
  writeKey: string
}

type InjectedDependencies = {}

class SegmentAnalyticsProviderService extends AbstractAnalyticsProviderService {
  private client: Analytics
  static identifier = "segment"

  constructor(container: InjectedDependencies, options: Options) {
    super()
    if (!options.writeKey) {
      throw new MedusaError(
        MedusaError.Types.INVALID_DATA,
        "Segment write key is required"
      )
    }
    this.client = new Analytics({ writeKey: options.writeKey })
  }
}

export default SegmentAnalyticsProviderService
```

An Analytics Module Provider's service must extend the `AbstractAnalyticsProviderService` class. It must also have an `identifier` static property with the unique identifier of the provider.

A module provider's constructor receives two parameters:

- `container`: The [module's container](https://docs.medusajs.com/docs/learn/fundamentals/modules/container/index.html.md) that contains Framework resources available to the module. In this tutorial, you don't need to resolve any resources.
- `options`: Options that are passed to the module provider when it's registered in Medusa's configurations. You define the following option:
  - `writeKey`: The Segment write key. You'll learn how to retrieve and set this option in the [Add Module Provider to Medusa's Configurations](#h-add-module-provider-to-medusas-configurations) section.

In the constructor, you create a Segment client using the Segment Node SDK. You pass the `writeKey` option to the client.

You'll use this client to implement the service's methods in the next sections.

Refer to the [Create Analytics Module Provider](https://docs.medusajs.com/references/analytics/provider/index.html.md) guide for detailed information about the methods.

### d. Implement identify Method

The `identify` method is used to identify a user in Segment. It associates the user's ID with their profile information, such as name and email.

Add the `identify` method to the `SegmentAnalyticsProviderService` class:

```ts title="src/modules/segment/service.ts"
// other imports...
import { ProviderIdentifyAnalyticsEventDTO } from "@medusajs/types"

class SegmentAnalyticsProviderService extends AbstractAnalyticsProviderService {
  // ...
  async identify(data: ProviderIdentifyAnalyticsEventDTO): Promise<void> {
    const anonymousId = data.properties && "anonymousId" in data.properties ? 
      data.properties.anonymousId : undefined
    const traits = data.properties && "traits" in data.properties ? 
        data.properties.traits : undefined

    if ("group" in data) {
      this.client.group({
        groupId: data.group.id,
        userId: data.actor_id,
        anonymousId,
        traits,
        context: data.properties
      })
    } else {
      this.client.identify({
        userId: data.actor_id,
        anonymousId,
        traits,
        context: data.properties
      })
    }
  }
}
```

#### Parameters

The `identify` method receives an object with the following properties:

- `actor_id`: The ID of the user being identified.
- `group`: Alternatively, the group being identified. If this property is present, the `actor_id` is ignored.
- `properties`: Additional properties to associate with the user or group. This can include traits like name, email, and so on.

The method receives other parameters, which you can find in the [Create Analytics Module Provider](https://docs.medusajs.com/references/analytics/provider#identify/index.html.md) guide.

#### Method Logic

In the method, if the `group` property is present, you call the `group` method of the Segment client to identify a group. Otherwise, you call the `identify` method to identify a user.

For both methods, you extract the `anonymousId` and `traits` from the `properties` object if they are present. You also pass the `actor_id` as the `userId`, and `group.id` for groups.

### e. Implement track Method

The `track` method is used to track events in Segment. It can track events like order placements, cart updates, and more.

Add the `track` method to the `SegmentAnalyticsProviderService` class:

```ts title="src/modules/segment/service.ts"
// other imports...
import { ProviderTrackAnalyticsEventDTO } from "@medusajs/types"

class SegmentAnalyticsProviderService extends AbstractAnalyticsProviderService {
  // ...
  async track(data: ProviderTrackAnalyticsEventDTO): Promise<void> {
    const userId = "group" in data ? 
      data.actor_id || data.group?.id : data.actor_id
    const anonymousId = data.properties && "anonymousId" in data.properties ? 
      data.properties.anonymousId : undefined

    if (!userId && !anonymousId) {
      throw new MedusaError(
        MedusaError.Types.INVALID_DATA, 
        `Actor or group ID is required for event ${data.event}`
      )
    }

    this.client.track({
      userId,
      anonymousId,
      event: data.event,
      properties: data.properties,
      timestamp: data.properties && "timestamp" in data.properties ? 
        new Date(data.properties.timestamp) : undefined,
    })
  }
}
```

#### Parameters

The `track` method receives an object with the following properties:

- `actor_id`: The ID of the user performing the event.
- `group`: Alternatively, the group performing the event. If this property is present, the `actor_id` is ignored.
- `event`: The name of the event being tracked.
- `properties`: Additional properties associated with the event. This can include details like product ID, order ID, and so on.

The method receives other parameters, which you can find in the [Create Analytics Module Provider](https://docs.medusajs.com/references/analytics/provider#track/index.html.md) guide.

#### Method Logic

In the method, you set the user ID either to the actor or group ID. You also check if the anonymous ID is present in the properties to use it.

Next, you call the `track` method of the Segment client, passing it the user ID, anonymous ID, event name, properties, and timestamp (if present in the properties).

### f. Implement shutdown Method

The `shutdown` method is used to gracefully shut down the Segment client when the Medusa application is stopped. It allows you to send all pending events to Segment before the application exits.

Add the following method to the `SegmentAnalyticsProviderService` class:

```ts title="src/modules/segment/service.ts"
class SegmentAnalyticsProviderService extends AbstractAnalyticsProviderService {
  // ...
  async shutdown(): Promise<void> {
    await this.client.flush({
      close: true,
    })
  }
}
```

#### Method Logic

In the method, you call the `flush` method of the Segment client with the `close` option set to `true`. This method will send all pending events to Segment and close the client connection.

### g. Export Module Definition

You've now finished implementing the necessary methods for the Segment Analytics 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/segment/index.ts` with the following content:

```ts title="src/modules/segment/index.ts"
import SegmentAnalyticsProviderService from "./service"
import { 
  ModuleProvider, 
  Modules,
} from "@medusajs/framework/utils"

export default ModuleProvider(Modules.ANALYTICS, {
  services: [SegmentAnalyticsProviderService],
})
```

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.ANALYTICS` in this case.
2. An object with a required property `services` indicating the Module Provider's services.

### h. 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:

```ts title="medusa-config.ts"
module.exports = defineConfig({
  // ...
  modules: [
    {
      resolve: "@medusajs/medusa/analytics",
      options: {
        providers: [
          {
            resolve: "./src/modules/segment",
            id: "segment",
            options: {
              writeKey: process.env.SEGMENT_WRITE_KEY || "",
            },
          },
        ],
      },
    },
  ],
})
```

To pass an Analytics Module Provider to the Analytics Module, you add the `modules` property to the Medusa configuration and pass the Analytics Module in its value.

The Analytics Module accepts a `providers` option, which is an array of Analytics Module Providers to register. However, you can only register one analytics provider in your Medusa application.

To register the Segment Analytics 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/segment` directory.
- `id`: The ID of the module provider. The Analytics Module Provider is then registered with the ID `aly_{identifier}_{id}`, where:
  - `{identifier}`: The identifier static property defined in the Module Provider's service, which is `segment` in this case.
  - `{id}`: The ID set in this configuration, which is also `segment` 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.

### i. Set Option as Environment Variable

Next, you'll set the Segment write key as an environment variable.

To retrieve the Segment write key:

1. Log into your [Segment](https://app.segment.com) account.
2. Go to the Connections page and click the "Add More" button next to the "Sources" section.

![Add more button in the Connections page](https://res.cloudinary.com/dza7lstvk/image/upload/v1748254988/Medusa%20Book/CleanShot_2025-05-26_at_10.47.24_2x_qqlkwk.png)

3. In the "Choose a Source" step, select "Node.js" and click the "Next" button.

![Select Node.js as the Source](https://res.cloudinary.com/dza7lstvk/image/upload/v1748255028/Medusa%20Book/CleanShot_2025-05-26_at_10.47.57_2x_zy9g8k.png)

4. In the "Connect your Node.js Source" step, enter a name for the source and click the "Create Source" button. This will show you the write key to copy.

![Copy the write key](https://res.cloudinary.com/dza7lstvk/image/upload/v1748255065/Medusa%20Book/CleanShot_2025-05-26_at_10.48.41_2x_lpgsbb.png)

You can skip the next step of testing out the source for now.

Then, add the following environment variable to your `.env` file:

```shell
SEGMENT_WRITE_KEY=123...
```

Replace `123...` with the write key you copied from Segment.

You'll test out the integration as you set up event tracking in the next steps.

***

## Step 3: Track Order Placement Event

You'll first track the order-placement event, which is triggered natively in the Medusa application.

Medusa's events system allows you to listen to events triggered by the Medusa application and execute custom logic asynchronously in a [subscriber](https://docs.medusajs.com/docs/learn/fundamentals/events-and-subscribers/index.html.md).

In the subscriber, you execute functionalities created in [workflows](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md). A workflow is a series of actions, called steps, that complete a task.

In this step, you'll create a workflow that tracks the `order.placed` event in Segment. Then, you'll create a subscriber that listens to this event and executes the workflow.

### a. Create Track Event Step

Before you create the workflow, you'll create a step that tracks an event in Segment. Later, you'll use this step in the workflows that track events, such as the order-placement event.

To create a step, create the file `src/workflows/steps/track-event.ts` with the following content:

```ts title="src/workflows/steps/track-event.ts" highlights={stepHighlights}
import { createStep } from "@medusajs/framework/workflows-sdk"

type TrackEventStepInput = {
  event: string
  userId?: string
  properties?: Record<string, unknown>
  timestamp?: Date
}

export const trackEventStep = createStep(
  "track-event",
  async (input: TrackEventStepInput, { container }) => {
    const analyticsModuleService = container.resolve(
      "analytics"
    )

    if (!input.userId) {
      // generate a random user id
      input.properties = {
        ...input.properties,
        anonymousId: Math.random().toString(36).substring(2, 15) + 
          Math.random().toString(36).substring(2, 15),
      }
    }

    await analyticsModuleService.track({
      event: input.event,
      actor_id: input.userId,
      properties: input.properties,
    })
  }
)
```

You create a step with `createStep` from the Workflows SDK. It accepts two parameters:

1. The step's unique name, which is `track-event`.
2. An async function that receives two parameters:
   - The step's input, which is in this case an object with the following properties:
     - `event`: The name of the event to track.
     - `userId`: The ID of the user performing the event.
     - `properties`: Additional properties associated with the event.
     - `timestamp`: The timestamp of the event (optional).
   - An object that has properties including the [Medusa container](https://docs.medusajs.com/docs/learn/fundamentals/medusa-container/index.html.md), which is a registry of Framework and commerce tools that you can access in the step.

The Medusa container is different from the module's container. Since modules are isolated, they each have a container with their resources. Refer to the [Module Container](https://docs.medusajs.com/docs/learn/fundamentals/modules/container/index.html.md) documentation for more information.

In the step function, you resolve the Analytics Module's service from the Medusa container. This service is the interface to track events with the configured Analytics Module Provider, which is Segment in this case.

If the `userId` is not provided, you generate a random anonymous ID and add it to the properties. This is useful for tracking events from users who are not logged in.

Finally, you call the `track` method of the Analytics Module's service, passing it the event name, user ID, and properties.

### b. Create Track Order Placed Workflow

Next, you'll create the workflow that tracks the order placement event.

To create the workflow, create the file `src/workflows/track-order-placed.ts` with the following content:

```ts title="src/workflows/track-order-placed.ts" highlights={workflowHighlights}
import { 
  createWorkflow,
  transform,
} from "@medusajs/framework/workflows-sdk"
import { useQueryGraphStep } from "@medusajs/medusa/core-flows"
import { trackEventStep } from "./steps/track-event"

type WorkflowInput = {
  id: string
}

export const trackOrderPlacedWorkflow = createWorkflow(
  "track-order-placed",
  ({ id }: WorkflowInput) => {
    // @ts-ignore
    const { data: orders } = useQueryGraphStep({
      entity: "order",
      fields: [
        "id",
        "email",
        "total",
        "currency_code",
        "items.*",
        "customer.id",
        "customer.email",
        "customer.first_name",
        "customer.last_name",
        "created_at",
      ],
      filters: {
        id,
      },
    })

    const order = transform({
      order: orders[0],
    }, ({ order }) => ({
      orderId: order.id,
      email: order.email,
      total: order.total,
      currency: order.currency_code,
      items: order.items?.map((item) => ({
        id: item?.id,
        title: item?.title,
        quantity: item?.quantity,
        variant: item?.variant,
        unit_price: item?.unit_price,
      })),
      customer: {
        id: order.customer?.id,
        email: order.customer?.email,
        firstName: order.customer?.first_name,
        lastName: order.customer?.last_name,
      },
      timestamp: order.created_at,
    }))

    trackEventStep({
      event: "order.placed",
      userId: order.customer?.id,
      properties: order,
    })
  }
)
```

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 an object holding the ID of the order placed.

In the workflow's constructor function, you:

1. Retrieve the Medusa order using the `useQueryGraphStep` helper step. This step uses Medusa's [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md) tool to retrieve data across modules. You pass it the order ID to retrieve.
2. Use [transform](https://docs.medusajs.com/docs/learn/fundamentals/workflows/variable-manipulation/index.html.md) to prepare the tracking data, as direct data and variable manipulation isn't allowed in workflows. Learn more in the [Data Manipulation](https://docs.medusajs.com/docs/learn/fundamentals/workflows/variable-manipulation/index.html.md) documentation.
3. Send the tracking event to Segment using the `trackEventStep` you created in the previous step.

You now have the workflow that tracks the order placement event.

Refer to the [Workflows](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md) documentation to learn more about workflows and steps.

### c. Handle order.placed Event

Next, you'll create a subscriber that listens to the `order.placed` event and executes the workflow you created in the previous step.

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

```ts title="src/subscribers/order-placed.ts" highlights={subscriberHighlights}
import { SubscriberArgs, type SubscriberConfig } from "@medusajs/framework"
import { trackOrderPlacedWorkflow } from "../workflows/track-order-placed"

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

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

Refer to the [Events and Subscribers](https://docs.medusajs.com/docs/learn/fundamentals/events-and-subscribers/index.html.md) documentation to learn more about creating subscribers.

### Test it Out

You'll now test out the segment integration by placing an order using the [Next.js Starter Storefront](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/nextjs-starter/index.html.md).

The Next.js Starter Storefront was installed in a separate directory from Medusa. The directory's name is `{your-project}-storefront`.

So, if your Medusa application's directory is `medusa-segment`, you can find the storefront by going back to the parent directory and changing to the `medusa-segment-storefront` directory:

```bash
cd ../medusa-segment-storefront # change based on your project name
```

First, run the following command in your Medusa application's directory to start the Medusa server:

```bash npm2yarn badgeLabel="Medusa Application" badgeColor="green"
npm run dev
```

Then, run the following command in your Next.js Starter Storefront's directory to start the storefront:

```bash npm2yarn badgeLabel="Storefront" badgeColor="blue"
npm run dev
```

In the storefront, add a product to the cart and proceed to checkout. Once you place the order, open the Segment dashboard to view the order event:

1. Go to Connections > Sources.
2. Click on the Node.js source you created earlier.
3. Click on the "Debugger" tab at the top of the page.
4. You should see the `order.placed` event with the order details.

The event may take a few seconds to appear in the debugger.

![Order Placed Event in Segment Debugger](https://res.cloudinary.com/dza7lstvk/image/upload/v1748259137/Medusa%20Book/CleanShot_2025-05-26_at_14.31.25_2x_xi5p9h.png)

***

## Track Custom Event

In your Medusa application, you often need to track custom events that are relevant to your business use case. For example, a B2B business may want to track whenever a user requests a quote.

In Medusa, you can emit custom events in your workflows when an action occurs. Then, you can create a subscriber that listens to the custom event and executes a workflow to track it in Segment.

For example, if you have a `createQuoteWorkflow`, you can use Medusa's [emitEventStep](https://docs.medusajs.com/docs/learn/fundamentals/events-and-subscribers/emit-event#emit-event-in-a-workflow/index.html.md) to emit a custom event after the quote is created:

```ts title="src/workflows/create-quote.ts"
import { 
  createWorkflow,
} from "@medusajs/framework/workflows-sdk"
import {
  emitEventStep,
} from "@medusajs/medusa/core-flows"

const createQuoteWorkflow = createWorkflow(
  "create-quote",
  () => {
    // ...

    emitEventStep({
      eventName: "quote.created",
      data: {
        id: "123",
        // other data payload
      },
    })
  }
)
```

You can then create a subscriber that listens to the `quote.created` event and executes a workflow to track it in Segment:

```ts title="src/subscribers/quote-created.ts"
import { SubscriberArgs, type SubscriberConfig } from "@medusajs/framework"
import { trackQuoteWorkflow } from "../workflows/track-order-placed"

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

export const config: SubscriberConfig = {
  event: "quote.created",
}
```

The above example assumes you have a `trackQuoteWorkflow` that tracks the quote creation event in Segment, similar to the [trackOrderPlacedWorkflow](#b-create-track-order-placed-workflow) you created earlier.

***

## Next Steps

You've now integrated Segment with your Medusa application and tracked common events like order placement. You can expand on the features in this tutorial to:

- Track more events in your Medusa application, such as user sign-ups, cart additions, and more. You can refer to the [Events Reference](https://docs.medusajs.com/references/events/index.html.md) for a full list of events emitted by Medusa.
- Emit custom events that are relevant for your business use case, and track them in Segment.
- Add destinations to Segment to benefit from the data collected. Segment supports various destinations, such as Google Analytics, Metabase, and more.

If you're new to Medusa, check out the [main documentation](https://docs.medusajs.com/docs/learn/index.html.md), 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](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/index.html.md).

### Troubleshooting

If you encounter issues during your development, check out the [troubleshooting guides](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/troubleshooting/index.html.md).

### Getting Help

If you encounter issues not covered in the troubleshooting guides:

1. Visit the [Medusa GitHub repository](https://github.com/medusajs/medusa) to report issues or ask questions.
2. Join the [Medusa Discord community](https://discord.gg/medusajs) for real-time support from community members.
3. Contact the [sales team](https://medusajs.com/contact/) to get help from the Medusa team.


# Integrate Medusa with Sanity (CMS)

In this guide, you'll learn how to integrate Medusa with Sanity.

When you install a Medusa application, you get a fully-fledged commerce platform with support for customizations. While Medusa allows you to manage basic content, such as product description and images, you might need rich content-management features, such as localized content. The Medusa Framework supports you in integrating a CMS with these features.

Sanity is a CMS that simplifies managing content from third-party sources into a single interface. By integrating it with Medusa, you can manage your storefront and commerce-related content, such as product details, from a single interface. You also benefit from advanced content-management features, such as live-preview editing.

This guide will teach you how to:

- Install and set up Medusa.
- Install and set up Sanity with Medusa's Next.js Starter storefront.
- Sync product data from Medusa to Sanity when a product is created or updated.
- Customize the Medusa Admin dashboard to check the sync status and trigger syncing products to Sanity.

You can follow this guide whether you're new to Medusa or an advanced Medusa developer. This guide also assumes you're familiar with Sanity concepts, which you can learn about in [their documentation](https://www.sanity.io/docs).

[Example Repository](https://github.com/medusajs/examples/tree/main/sanity-integration): Find the full code of the guide in this repository.

***

## Step 1: Install a Medusa Application

### Prerequisites

- [Node.js v20+](https://nodejs.org/en/download)
- [Git CLI tool](https://git-scm.com/downloads)
- [PostgreSQL](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 in a directory with your project's name, and the Next.js Starter Storefront in a directory with the `{project-name}-storefront` name.

The Medusa application is composed of a headless Node.js server and an admin dashboard. The storefront is installed or custom-built separately and connects to the Medusa application through its REST endpoints, called [API routes](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/index.html.md). Learn more about Medusa's architecture in [this documentation](https://docs.medusajs.com/docs/learn/introduction/architecture/index.html.md).

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](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/troubleshooting/create-medusa-app-errors/index.html.md) for help.

***

## Step 2: Install Sanity Client SDK

In this step, you'll install [Sanity's JavaScript client SDK](https://www.sanity.io/docs/js-client) in the Medusa application, which you'll use later in your code when sending requests to Sanity.

In your terminal, move to the Medusa application's directory and run the following command:

```bash npm2yarn
cd project-name # replace with directory name
npm install @sanity/client
```

***

## Step 3: Create a Sanity Project

When the Medusa application connects to Sanity, it must connect to a project in Sanity.

So, before building the integration in Medusa, create a project in Sanity using their website:

1. [Sign in or sign up on the Sanity website.](https://www.sanity.io/login)
2. On your account's dashboard, click the "Create new project" button.

![The Create new project button is at the top of the dashboard page.](https://res.cloudinary.com/dza7lstvk/image/upload/v1732091565/Medusa%20Resources/Screenshot_2024-11-20_at_10.31.10_AM_vvq7y6.png)

3. Enter a project name and click "Create Project"

![A pop-up form will open where you can choose project name and organization.](https://res.cloudinary.com/dza7lstvk/image/upload/v1732091565/Medusa%20Resources/Screenshot_2024-11-20_at_10.32.33_AM_xb0rsn.png)

You'll go back to the project's setting page in a later step.

***

## Step 4: Create Sanity Module

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.

In this step, you'll create a Sanity Module that provides the interface to connect to and interact with Sanity. In later steps, you'll use the functionalities provided by this module to sync products to Sanity or retrieve documents from it.

Learn more about modules in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/modules/index.html.md).

### Create Module Directory

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

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

Medusa registers the module's service in the [Medusa container](https://docs.medusajs.com/docs/learn/fundamentals/medusa-container/index.html.md), allowing you to easily resolve the service from other customizations and use its methods.

The Medusa application registers resources, such as a module's service or the [logging tool](https://docs.medusajs.com/docs/learn/debugging-and-testing/logging/index.html.md), in the Medusa container so that you can resolve them from other customizations, as you'll see in later sections. Learn more about it in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/medusa-container/index.html.md).

In this section, you'll create the Sanity Module's service and the methods necessary to connect to Sanity.

Start by creating the file `src/modules/sanity/service.ts` with the following content:

```ts title="src/modules/sanity/service.ts"
import {
  Logger,
} from "@medusajs/framework/types"
import {
  SanityClient,
} from "@sanity/client"

class SanityModuleService {
  private client: SanityClient
  private studioUrl?: string
  private logger: Logger

  // TODO
}

export default SanityModuleService
```

You create the `SanityModuleService` class that for now only has three properties:

- `client` property of type `SanityClient` (from the Sanity SDK you installed in the previous step) to send requests to Sanity.
- `studioUrl` property which will hold the URL to access the Sanity studio.
- `logger` property, which is an instance of Medusa's [Logger](https://docs.medusajs.com/docs/learn/debugging-and-testing/logging/index.html.md), to log messages.

In the service, you want to initialize the client early-on so that you can use it in the service's methods. This requires options to be passed to the client, like the Sanity API key or project ID.

So, add after the import at the top of the file the following types:

```ts title="src/modules/sanity/service.ts"
// other imports...

const SyncDocumentTypes = {
  PRODUCT: "product",
} as const

type SyncDocumentTypes =
  (typeof SyncDocumentTypes)[keyof typeof SyncDocumentTypes];

type ModuleOptions = {
  api_token: string;
  project_id: string;
  api_version: string;
  dataset: "production" | "development";
  type_map?: Record<SyncDocumentTypes, string>;
  studio_url?: string;
}
```

The `ModuleOptions` type defines the type of options that the module expects:

- `api_token`: API token to connect to Sanity.
- `project_id`: The ID of the Sanity project.
- `api_version`: The Sanity API version.
- `dataset`: The dataset to use, which is either `production` or `development`.
- `type_map`: The types to sync from Medusa to Sanity. For simplicity, this guide only covers syncing products, but you can support other data types like product categories, too.
- `studio_url`: The URL to the Sanity studio. This is used to show the studio URL later in the Medusa Admin dashboard.

You can now initialize the client, which you'll do in the `constructor` of the `SanityModuleService`:

```ts title="src/modules/sanity/service.ts"
import {
  // other imports...
  createClient,
} from "@sanity/client"

// types...

type InjectedDependencies = {
  logger: Logger
};

class SanityModuleService {
  // properties...
  constructor({
    logger,
  }: InjectedDependencies, options: ModuleOptions) {
    this.client = createClient({
      projectId: options.project_id,
      apiVersion: options.api_version,
      dataset: options.dataset,
      token: options.api_token,
    })
    this.logger = logger

    this.logger.info("Connected to Sanity")

    this.studioUrl = options.studio_url
    
    // TODO initialize more properties
  }
}
```

The service's constructor accepts two parameters:

1. Resources to resolve from the Module's container. A module has a different container than the Medusa application, which you can learn more about it in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/modules/container/index.html.md).
2. The options passed to the module.

In the constructor, you create a Sanity client using the `createClient` function imported from `@sanity/client`. You pass it the options that the module receives.

You also initialize the `logger` and `studioUrl` properties, and log a message indicating that connection to Sanity was successful.

#### Transform Product Data

When you create or update products in Sanity, you must prepare the product object based on what Sanity expects.

So, you'll add methods to the service that transform a Medusa product to a Sanity document object.

Start by adding the following types and class properties to `src/modules/sanity/service.ts`:

```ts title="src/modules/sanity/service.ts"
type SyncDocumentInputs<T> = T extends "product"
  ? ProductDTO
  : never

type TransformationMap<T> = Record<
  SyncDocumentTypes,
  (data: SyncDocumentInputs<T>) => any
>;

class SanityModuleService {
  // other properties...
  private typeMap: Record<SyncDocumentTypes, string>
  private createTransformationMap: TransformationMap<SyncDocumentTypes>
  private updateTransformationMap: TransformationMap<SyncDocumentTypes>

  // ...
}
```

First, you define types for a transformation map, which is a map that pairs up a document type (such as `product`) to a function that handles transforming its data.

Then, in the service, you define three new properties:

- `typeMap`: Pair of `SyncDocumentTypes` values (for example, `product`) and their type name in Sanity.
- `createTransformationMap`: Pair of `SyncDocumentTypes` values (for example, `product`) and the method used to transform a Medusa product to a Sanity document data to be created.
- `updateTransformationMap`: Pair of `SyncDocumentTypes` values (for example, `product`) and the method used to transform a Medusa product to a Sanity update operation.

Next, add the following two methods to transform a product:

```ts title="src/modules/sanity/service.ts"
// other imports...
import {
  ProductDTO,
} from "@medusajs/framework/types"

class SanityModuleService {
  // ...
  private transformProductForCreate = (product: ProductDTO) => {
    return {
      _type: this.typeMap[SyncDocumentTypes.PRODUCT],
      _id: product.id,
      title: product.title,
      specs: [
        {
          _key: product.id,
          _type: "spec",
          title: product.title,
          lang: "en",
        },
      ],
    }
  }

  private transformProductForUpdate = (product: ProductDTO) => {
    return {
      set: {
        title: product.title,
      },
    }
  }
}
```

The `transformProductForCreate` method accepts a product and returns an object that you'll later pass to Sanity to create the product document. Similarly, the `transformProductForUpdate` method accepts a product and returns an object that you'll later pass to Sanity to update the product document.

The Sanity document's schema type will be defined in a later chapter. If you add other fields to it, make sure to edit these methods.

Finally, initialize the new properties you added in the `SanityModuleService`'s constructor:

```ts title="src/modules/sanity/service.ts"
class SanityModuleService {
  // ...
  constructor({
    logger,
  }: InjectedDependencies, options: ModuleOptions) {
    // ...
    this.typeMap = Object.assign(
      {},
      {
        [SyncDocumentTypes.PRODUCT]: "product",
      },
      options.type_map || {}
    )

    this.createTransformationMap = {
      [SyncDocumentTypes.PRODUCT]: this.transformProductForCreate,
    }

    this.updateTransformationMap = {
      [SyncDocumentTypes.PRODUCT]: this.transformProductForUpdate,
    }
  }
  // ...
}
```

You initialize the `typeMap` property to map the `product` type in Medusa to the `product` schema type in Sanity. You also initialize the `createTransformationMap` and `updateTransformationMap` to map the methods to transform a product for creation or update.

You can modify these properties to add support for other schema types, such as product categories or collections.

#### Methods to Manage Documents

In this section, you'll add the methods that accept data from Medusa and create or update them as documents in Sanity.

Add the following methods to the `SanityModuleService` class:

```ts title="src/modules/sanity/service.ts" highlights={syncMethodsHighlights}
// other imports...
import {
  // ...
  FirstDocumentMutationOptions,
} from "@sanity/client"

class SanityModuleService {
  // ...
  async upsertSyncDocument<T extends SyncDocumentTypes>(
    type: T,
    data: SyncDocumentInputs<T>
  ) {
    const existing = await this.client.getDocument(data.id)
    if (existing) {
      return await this.updateSyncDocument(type, data)
    }

    return await this.createSyncDocument(type, data)
  }

  async createSyncDocument<T extends SyncDocumentTypes>(
    type: T,
    data: SyncDocumentInputs<T>,
    options?: FirstDocumentMutationOptions
  ) {
    const doc = this.createTransformationMap[type](data)
    return await this.client.create(doc, options)
  }

  async updateSyncDocument<T extends SyncDocumentTypes>(
    type: T,
    data: SyncDocumentInputs<T>
  ) {
    const operations = this.updateTransformationMap[type](data)
    return await this.client.patch(data.id, operations).commit()
  }
}
```

You add three methods:

- `upsertSyncDocument`: Creates or updates a document in Sanity for a data type in Medusa.
- `createSyncDocument`: Creates a document in Sanity for a data type in Medusa. It uses the `createTransformationMap` property to use the transform method of the specified Medusa data type (for example, a product's data).
- `updateSyncDocument`: Updates a document in Sanity for a data type in Medusa. It uses the `updateTransformationMap` property to use the transform method of the specified Medusa data type (for example, a product's data).

You also need methods to manage the Sanity documents without transformations. So, add the following methods to `SanityModuleService`:

```ts title="src/modules/sanity/service.ts" highlights={methodsHighlights}
class SanityModuleService {
  // ...
  async retrieve(id: string) {
    return this.client.getDocument(id)
  }

  async delete(id: string) {
    return this.client.delete(id)
  }

  async update(id: string, data: any) {
    return await this.client.patch(id, {
      set: data,
    }).commit()
  }

  async list(
    filter: {
      id: string | string[]
    }
  ) {
    const data = await this.client.getDocuments(
      Array.isArray(filter.id) ? filter.id : [filter.id]
    )

    return data.map((doc) => ({
      id: doc?._id,
      ...doc,
    }))
  }
}
```

You add other three methods:

- `retrieve` to retrieve a document by its ID.
- `delete` to delete a document by its ID.
- `update` to update a document by its ID with new data.
- `list` to list documents, with ability to filter them by their IDs. Since a Sanity document's ID is a product's ID, you can pass product IDs as a filter to retrieve their documents.

### Export Module Definition

The `SanityModuleService` class now has the methods necessary to connect to and perform actions in Sanity.

Next, you must export the Module definition, which lets Medusa know what the Module's name is and what is its service.

Create the file `src/modules/sanity/index.ts` with the following content:

```ts title="src/modules/sanity/index.ts"
import { Module } from "@medusajs/framework/utils"
import SanityModuleService from "./service"

export const SANITY_MODULE = "sanity"

export default Module(SANITY_MODULE, {
  service: SanityModuleService,
})
```

In the file, you export the `SANITY_MODULE` which is the Module's name. You'll use it later when you resolve the module from the Medusa container.

You also export the module definition using `Module` from the Modueles SDK, which accepts as a first parameter the module's name, and as a second parameter an object having a `service` property, indicating the module's service.

### Add Module to Configurations

Finally, to register a module in Medusa, you must add it 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 title="medusa-config.ts"
module.exports = defineConfig({
  // ...
  modules: [
    {
      resolve: "./src/modules/sanity",
      options: {
        api_token: process.env.SANITY_API_TOKEN,
        project_id: process.env.SANITY_PROJECT_ID,
        api_version: new Date().toISOString().split("T")[0],
        dataset: "production",
        studio_url: process.env.SANITY_STUDIO_URL || 
          "http://localhost:3000/studio",
        type_map: {
          product: "product",
        },
      },
    },
  ],
})
```

In the `modules` array, you pass a module object having the following properties:

- `resolve`: The path to the module to register in the application. It can also be the name of an NPM package.
- `options`: An object of options to pass to the module. These are the options you expect and use in the module's service.

Some of the module's options, such as the Sanity API key, are set in environment variables. So, add the following environment variables to `.env`:

```shell
SANITY_API_TOKEN=
SANITY_PROJECT_ID=
SANITY_STUDIO_URL=http://localhost:8000/studio
```

Where:

- `SANITY_API_TOKEN`: The API key token to connect to Sanity, which you can retrieve from the Sanity project's dashboard:
  - Go to the API tab.

![The API tab is at the top of the project dashboard next to Settings.](https://res.cloudinary.com/dza7lstvk/image/upload/v1732091810/Medusa%20Resources/Screenshot_2024-11-20_at_10.35.29_AM_ltj7cd.png)

- Scroll down to Tokens and click on the "Add API Token" button.

![The Add API token button is at the top right of the Tokens section.](https://res.cloudinary.com/dza7lstvk/image/upload/v1732091810/Medusa%20Resources/Screenshot_2024-11-20_at_10.35.52_AM_ccgsum.png)

- Enter a name for the API token, choose "Editor" for the permissions, then click Save.

![In the Token form, enter the name and choose "Editor" for permisions.](https://res.cloudinary.com/dza7lstvk/image/upload/v1732091811/Medusa%20Resources/Screenshot_2024-11-20_at_10.36.25_AM_nqxa5y.png)

- `SANITY_PROJECT_ID`: The ID of the project, which you can find at the top section of your Sanity project's dashboard.

![The project ID is in the top information of the project.](https://res.cloudinary.com/dza7lstvk/image/upload/v1732091988/Medusa%20Resources/Screenshot_2024-11-20_at_10.39.24_AM_cscir8.png)

- `SANITY_STUDIO_URL`: The URL to access the studio. You'll set the studio up in a later section, but for now set it to `http://localhost:8000/studio`.

### Test the Module

To test that the module is working, you'll start the Medusa application and see if the "Connected to Sanity" message is logged in the console.

To start the Medusa application, run the following command in the application's directory:

```bash npm2yarn
npm run dev
```

If you see the following message among the logs:

```bash
info:    Connected to Sanity
```

That means your Sanity credentials were correct, and Medusa was able to connect to Sanity.

In the next steps, you'll create a link between the Product and Sanity modules to retrieve data between them easily, and build a flow around the Sanity Module to sync data.

***

## Step 5: Link Product and Sanity Modules

Since a product has a document in Sanity, you want to build an association between the [Product](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/product/index.html.md) and Sanity modules so that when you retrieve a product, you also retrieve its associated Sanity document.

However, modules are [isolated](https://docs.medusajs.com/docs/learn/fundamentals/modules/isolation/index.html.md) to ensure they're re-usable and don't have side effects when integrated into the Medusa application. So, to build associations between modules, you define [module links](https://docs.medusajs.com/docs/learn/fundamentals/module-links/index.html.md).

A Module Link associates two modules' data models while maintaining module isolation. A data model can be a table in the database or a virtual model from an external systems.

In this section, you'll define a link between the Product and Sanity modules.

Links are defined in a TypeScript or JavaScript file under the `src/links` directory. So, create the file `src/links/product-sanity.ts` with the following content:

```ts title="src/links/product-sanity.ts"
import { defineLink } from "@medusajs/framework/utils"
import ProductModule from "@medusajs/medusa/product"
import { SANITY_MODULE } from "../modules/sanity"

defineLink(
  {
    linkable: ProductModule.linkable.product.id,
    field: "id",
  },
  {
    linkable: {
      serviceName: SANITY_MODULE,
      alias: "sanity_product",
      primaryKey: "id",
    },
  },
  {
    readOnly: true,
  }
)
```

You define a link using `defineLink` from the Modules SDK. It accepts three parameters:

1. The first data model part of the link. In this case, it's the Product Module's `product` data model. A module has a special `linkable` property that contain link configurations for its data models.
2. The second data model part of the link. Since the Sanity Module doesn't have a Medusa data model, you specify the configurations in a `linkable` object that has the following properties:
   - `serviceName`: The registration name in the Medusa container of the service managing the data model, which in this case is the Sanity Module's name (since the module's service is registered under that name).
   - `alias`: The name to refer to the model part of this link, such as when retrieving the Sanity document of a product. You'll use this in a later section.
   - `primaryKey`: The name of the data model's primary key field.
3. An object of configurations for the module link. By default, Medusa creates a table in the database to represent the link you define. Since the module link isn't created between two Medusa data models, you enable the `readOnly` configuration, which will tell Medusa not to create a table in the database for this link.

In the next steps, you'll see how this link allows you to retrieve documents when retrieving products.

***

## Step 6: Sync Data to Sanity

After integrating Sanity with a custom module, you now want to sync product data from Medusa to Sanity, automatically and manually. To implement the sync logic, you need 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. You'll see how all of this works in the upcoming sections.

Within a workflow's steps, you resolve modules to use their service's functionalities as part of a bigger flow. Then, you can execute the workflow from other customizations, such as in response to an event or in an API route.

Learn more about workflows in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md)

In this section, you'll create a workflow that syncs products from Medusa to Sanity. Later, you'll execute this workflow when a product is created or updated, or when an admin user triggers the syncing manually.

### Create Step

The syncing workflow will have a single step that syncs products provided as an input to Sanity.

So, to implement that step, create the file `src/workflows/sanity-sync-products/steps/sync.ts` with the following content:

```ts title="src/workflows/sanity-sync-products/steps/sync.ts" highlights={syncStepHighlights}
import { createStep, StepResponse } from "@medusajs/framework/workflows-sdk"
import { ProductDTO } from "@medusajs/framework/types"
import { 
  ContainerRegistrationKeys,
  promiseAll,
} from "@medusajs/framework/utils"
import SanityModuleService from "../../../modules/sanity/service"
import { SANITY_MODULE } from "../../../modules/sanity"

export type SyncStepInput = {
  product_ids?: string[];
}

export const syncStep = createStep(
  { name: "sync-step", async: true },
  async (input: SyncStepInput, { container }) => {
    const sanityModule: SanityModuleService = container.resolve(SANITY_MODULE)
    const query = container.resolve(ContainerRegistrationKeys.QUERY)

    const total = 0
    const upsertMap: {
      before: any
      after: any
    }[] = []

    const batchSize = 200
    const hasMore = true
    const offset = 0
    const filters = input.product_ids ? {
      id: input.product_ids,
    } : {}

    while (hasMore) {
      const {
        data: products,
        metadata: { count } = {},
      } = await query.graph({
        entity: "product",
        fields: [
          "id",
          "title",
          // @ts-ignore
          "sanity_product.*",
        ],
        filters,
        pagination: {
          skip: offset,
          take: batchSize,
          order: {
            id: "ASC",
          },
        },
      })

      // TODO sync products
    }
  }
)
```

You define the `syncStep` using the `createStep` function, which accepts two parameters:

- An object of step configurations. The object must have the `name` property, which is this step's unique name. Enabling the `async` property means that the workflow should run asynchronously in the background. This is useful when the workflow is triggered manually through an HTTP request, meaning the response will be returned to the client even if the workflow hasn't finished executing.
- The step's function definition as a second parameter.

The step function accepts the step's input as a first parameter, and an object of options as a second. The object of options has a `container` property, which is an instance of the Medusa container that you can use to resolve resources.

In the step, you resolve from the Medusa container Sanity Module's service and [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), which is a tool that allows you to retrieve data across modules and links.

You use Query's `graph` method to retrieve products, filtering them by their IDs and applying pagination configurations. The `graph` method accepts a `fields` property in its object parameter, which indicates the product data model's fields and relations to retrieve.

Notice that you pass `sanity_product.*` in the `fields` array. Medusa will retrieve the Sanity document of each product using Sanity Module's `list` method and attach it to the returned product. So, you don't have to retrieve the products and documents separately. Each product object in the returned array will look similar to this:

```json title="Example Product Object"
{
  "id": "prod_123",
  "title": "Shirt",
  "sanity_product": {
    "id": "prod_123",
    "_type": "product",
    // other Sanity fields...
  }
}
```

Next, you want to sync the retrieved products. So, replace the `TODO` in the `while` loop with the following:

```ts title="src/workflows/sanity-sync-products/steps/sync.ts"
while (hasMore) {
  // ...
  try {
    await promiseAll(
      products.map(async (prod) => {
        const after = await sanityModule.upsertSyncDocument(
          "product", 
          prod as ProductDTO
        )

        upsertMap.push({
          // @ts-ignore
          before: prod.sanity_product,
          after,
        })

        return after
      })
    )
  } catch (e) {
    return StepResponse.permanentFailure(
      `An error occurred while syncing documents: ${e}`,
      upsertMap
    )
  }

  offset += batchSize
  hasMore = offset < count
  total += products.length
}
```

In the `while` loop, you loop over the array of products to sync them to Sanity. You use the `promiseAll` Medusa utility that loops over an array of promises and ensures that all transactions within these promises are rolled back in case an error occurs.

For each product, you upsert it into Sanity, then push its document before and after the update to the `upsertMap`. You'll learn more about its use later.

You also wrap the `promiseAll` function within a try-catch block. In the catch block, you invoke and return `StepResponse.permanentFailure` which indicates that the step has failed but still invokes the rollback mechanism that you'll implement in a bit. The first parameter of `permanentFailure` is the error message, and the second is the data to use in the rollback mechanism.

Finally, after the `while` loop and at the end of the step, add the following return statement:

```ts title="src/workflows/sanity-sync-products/steps/sync.ts"
return new StepResponse({ total }, upsertMap)
```

If no errors occur, the step returns an instance of `StepResponse`, which must be returned by any step. It accepts as a first parameter the data to return to the workflow that executed this step.

#### Add Compensation Function

`StepResponse` accepts a second parameter, which is passed to the compensation function. A compensation function defines the rollback logic of a step, and it's only executed if an error occurs in the workflow. This eliminates data inconsistency if an error occurs and the workflow can't finish execution successfully.

Learn more about compensation functions in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/workflows/compensation-function/index.html.md).

The `syncStep` creates or updates products in Sanity. So, the compensation function must delete created documents or revert the update of a document to its previous data. The compensation function is only executed if an error occurs.

To define the compensation function, pass a third-parameter to the `createStep` function:

```ts title="src/workflows/sanity-sync-products/steps/sync.ts"
export const syncStep = createStep(
  { name: "sync-step", async: true },
  async (input: SyncStepInput, { container }) => {
    // ...
  },
  async (upsertMap, { container }) => {
    if (!upsertMap) {
      return
    }

    const sanityModule: SanityModuleService = container.resolve(SANITY_MODULE)

    await promiseAll(
      upsertMap.map(({ before, after }) => {
        if (!before) {
          // delete the document
          return sanityModule.delete(after._id)
        }

        const { _id: id, ...oldData } = before

        return sanityModule.update(
          id,
          oldData
        )
      })
    )
  }
)
```

The compensation function accepts the data passed in the step's `StepResponse` second parameter (in this case, `upsertMap`), and an object of options similar to that of the step.

In the compensation function, you resolve the Sanity Module's service, then loop over the `upsertMap` to delete created documents, or revert existing ones.

### Create Workflow

You'll now create the workflow that uses the `syncStep`. This is the workflow that you'll later execute to sync data automatically or manually.

Workflows are created in a file under the `src/workflows` directory. So, create the file `src/workflows/sanity-sync-products/index.ts` with the following content:

```ts title="src/workflows/sanity-sync-products/index.ts"
import {
  createWorkflow,
  WorkflowResponse,
} from "@medusajs/framework/workflows-sdk"
import { syncStep } from "./steps/sync"

export type SanitySyncProductsWorkflowInput = {
  product_ids?: string[];
};

export const sanitySyncProductsWorkflow = createWorkflow(
  { name: "sanity-sync-products", retentionTime: 10000 },
  function (input: SanitySyncProductsWorkflowInput) {
    const result = syncStep(input)

    return new WorkflowResponse(result)
  }
)
```

You create a workflow using the `createWorkflow` from the Workflows SDK. It accepts an object of options as a first parameter, where the `name` property is required and indicates the workflow's unique name.

The `retentionTime` property indicates how long should the workflow's progress be saved in the database. This is useful if you later want to track whether the workflow is successfully executing.

`createWorkflow` accepts as a second parameter a constructor function, which is the workflow's implementation. In the function, you execute the `syncStep` to sync the specified products in the input, then return its result. Workflows must return an instance of `WorkflowResponse`.

A workflow's constructor function has some constraints in implementation. Learn more about them in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/workflows/constructor-constraints/index.html.md).

You'll execute and test this workflow in the next steps.

***

## Step 7: Handle Product Changes in Medusa

You've defined the workflow to sync the products. Now, you want to execute it when a product is created or updated.

Medusa emits events when certain actions occur, such as when a product is created. Then, you can listen to those events in a subscriber.

A subscriber is an asynchronous function that listens to one or more events. Then, when those events are emitted, the subscriber is executed in the background of your application.

Subscribers are useful when you want to perform an action that isn't an integral part of a flow, but as a reaction to a performed action. In this case, syncing the products to Sanity isn't integral to creating a product, so you do it in a subscriber after the product is created.

Learn more about events and subscribers in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/events-and-subscribers/index.html.md). You can also find the list of emitted events in [this reference](https://docs.medusajs.com/references/events/index.html.md).

So, to run the workflow you defined in the previous event when a product is created or updated, you'll create a subscriber that listens to the `product.created` and `product.updated` events.

Subscribers are created under the `src/subscribers` directory. So, create the file `src/subscribers/sanity-product-sync.ts` with the following content:

```ts title="src/subscribers/sanity-product-sync.ts" highlights={subscriberHighlights}
import type { 
  SubscriberArgs, 
  SubscriberConfig,
} from "@medusajs/medusa"
import { 
  sanitySyncProductsWorkflow,
} from "../workflows/sanity-sync-products"

export default async function upsertSanityProduct({
  event: { data },
  container,
}: SubscriberArgs<{ id: string }>) {
  await sanitySyncProductsWorkflow(container).run({
    input: {
      product_ids: [data.id],
    },
  })
}

export const config: SubscriberConfig = {
  event: ["product.created", "product.updated"],
}
```

The subscriber function `upsertSanityProduct` accepts an object as a parameter that has the following properties:

- `event`: An object of the event's details. Its `data` property holds the data payload emitted with the event, which in this case is the ID of the product created or updated.
- `container`: An instance of the Medusa container to resolve resources.

In the subscriber, you execute the `sanitySyncProductsWorkflow` by invoking it, passing it the container, then invoking its `run` method. You pass the workflow's input in the `input` property of the `run`'s object parameter.

The subscriber file must also export a configuration object. It has an `event` property, which is the names of the events that the subscriber is listening to.

### Test it Out

To test it out, run the Medusa application, then open the Medusa Admin in your browser at `http://localhost:9000/app`. Try creating or updating a product. You'll see the following message in the console:

```bash
info:    Processing product.created which has 1 subscribers
```

This means that the `product.created` event was emitted and your subscriber was executed.

In the next step, you'll setup Sanity with Next.js, and you can then monitor the updates in Sanity's studio.

***

## Step 8: Setup Sanity with Next.js Starter Storefront

In this step, you'll install Sanity in the Next.js Starter and configure it. You'll then have a Sanity studio in your Next.js Starter Storefront, where you'll later view the product documents being synced from Medusa, and update their content that you'll display in the storefront on the product details page.

Sanity has a CLI tool that helps you with the setup. First, change to the Next.js Starter's directory (it's outside the Medusa application's directory and its name is `{project-name}-storefront`, where `{project-name}` is the name of the Medusa application's directory).

Then, run the following command:

```bash badgeLabel="Storefront" badgeColor="blue"
npx sanity@latest init
```

You'll then be asked a few questions:

- For the project, select the Sanity project you created earlier in this guide.
- For dataset, use `production` unless you changed it in the Sanity project.
- Select yes for adding the Sanity configuration files to the Next.js folder.
- Select yes for TypeScript.
- Select yes for Sanity studio, and choose the `/studio` route.
- Select clean project template.
- Select yes for adding the project ID and dataset to `.env.local`.

Afterwards, the command will install the necessary dependencies for Sanity.

### Error during installation

If you run into an error during the installation of peer dependencies, try running the following command to install them:

```bash
yarn add next-sanity@9.8.15 @sanity/client@^6.22.4 @sanity/icons@^3.4.0 @sanity/types@^3.62.0 @sanity/ui@^2.8.10 next@^15.0.0 react@^19.0.0 react-dom@^19.0.0 sanity@^3.62.0 styled-components@^6.1
```

### Update Middleware

The Next.js Starter storefront has a middleware that ensures all requests start with a country code (for example, `/us`).

Since the Sanity studio runs at `/studio`, the middleware should ignore requests to this path.

Open the file `src/middleware.ts` and find the following `if` condition:

```ts title="src/middleware.ts" badgeLabel="Storefront" badgeColor="blue"
if (
  urlHasCountryCode &&
  (!isOnboarding || onboardingCookie) &&
  (!cartId || cartIdCookie)
) {
  return NextResponse.next()
}
```

Replace it with the following condition:

```ts title="src/middleware.ts" badgeLabel="Storefront" badgeColor="blue"
if (
  request.nextUrl.pathname.startsWith("/studio") ||
  urlHasCountryCode &&
  (!isOnboarding || onboardingCookie) &&
  (!cartId || cartIdCookie)
) {
  return NextResponse.next()
}
```

If the path starts with `/studio`, the middleware will stop executing and the page will open.

### Set CORS Settings

Every Sanity project has a configured set of CORS origins allowed, with the default being `http://localhost:3333`.

The Next.js Starter runs on the `8000` port, so you must add it to the allowed CORS origins.

In your Sanity project's dashboard:

1. Click on the API tab.

![Find the API tab at the top of the dashboard.](https://res.cloudinary.com/dza7lstvk/image/upload/v1732096643/Medusa%20Resources/Screenshot_2024-11-20_at_10.35.29_AM_ltj7cd.png)

2. Scroll down to CORS origins and click the "Add CORS origin" button.

![Find the CORS origins section and click the Add CORS origin button at its top right.](https://res.cloudinary.com/dza7lstvk/image/upload/v1732096997/Medusa%20Resources/Screenshot_2024-11-20_at_12.02.50_PM_ahsthb.png)

3. Enter `http://localhost:8000` in the Origin field.
4. Enable the "Allow credentials" checkbox.

![After filling out the Origin field, click on the Allow credentials checkbox to enable it.](https://res.cloudinary.com/dza7lstvk/image/upload/v1732097074/Medusa%20Resources/Screenshot_2024-11-20_at_12.04.09_PM_nxdvwh.png)

5. Click the Save button.

### Open Sanity Studio

To open the Sanity studio, start the Next.js Starter's development server:

```bash npm2yarn
npm run dev
```

Then, open `http://localhost:8000/studio` in your browser. The Sanity studio will open, but right now it's empty.

***

## Step 9: Add Product Schema Type in Sanity

In this step, you'll define the `product` schema type in Sanity. You' can then view the documents of that schema in the studio and update their content.

To create the schema type, create the file `src/sanity/schemaTypes/documents/product.ts` with the following content:

```ts title="src/sanity/schemaTypes/documents/product.ts" badgeLabel="Storefront" badgeColor="blue"
import { ComposeIcon } from "@sanity/icons"
import { DocumentDefinition } from "sanity"

const productSchema: DocumentDefinition = {
  fields: [
    {
      name: "title",
      type: "string",
    },
    {
      group: "content",
      name: "specs",
      of: [
        {
          fields: [
            { name: "lang", title: "Language", type: "string" },
            { name: "title", title: "Title", type: "string" },
            {
              name: "content",
              rows: 3,
              title: "Content",
              type: "text",
            },
          ],
          name: "spec",
          type: "object",
        },
      ],
      type: "array",
    },
    {
      fields: [
        { name: "title", title: "Title", type: "string" },
        {
          name: "products",
          of: [{ to: [{ type: "product" }], type: "reference" }],
          title: "Addons",
          type: "array",
          validation: (Rule) => Rule.max(3),
        },
      ],
      name: "addons",
      type: "object",
    },
  ],
  name: "product",
  preview: {
    select: {
      title: "title",
    },
  },
  title: "Product Page",
  type: "document",
  groups: [{
    default: true,
    // @ts-ignore
    icon: ComposeIcon,
    name: "content",
    title: "Content",
  }],
}

export default productSchema
```

This creates a schema that has the following fields:

- `title`: The title of a document, which is in this case the product's type.
- `specs`: An array of product specs. Each object in the array has the following fields:
  - `lang`: This is useful if you want to have localized content.
  - `title`: The product's title.
  - `content`: Textual content, such as the product's description.
- `addons`: An object of products related to this product.

When you sync the products from Medusa, you only sync the title. You manage the `specs` and `addons` fields within Sanity.

Next, replace the content of `src/sanity/schemaTypes/index.ts` with the following:

```ts title="src/sanity/schemaTypes/index.ts" badgeLabel="Storefront" badgeColor="blue"
import { SchemaPluginOptions } from "sanity"
import productSchema from "./documents/product"

export const schema: SchemaPluginOptions = {
  types: [productSchema],
  templates: (templates) => templates.filter(
    (template) => template.schemaType !== "product"
  ),
}
```

You add the product schema to the list of exported schemas, but also disable creating a new product. You can only create the products in Medusa.

### Test it Out

To ensure that your schema is defined correctly and working, start the Next.js Starter Storefront's server, and open the Sanity studio again at `http://localhost:8000/studio`.

You'll find "Product Page" under Content. If you click on it, you'll find any product you've synced from Medusa.

If you haven't synced any products yet or you want to see the live update, try now creating or updating a product in Medusa. You'll find it added in the Sanity studio.

If you click on any product, you can edit its existing field under "Specs" or add new ones. In the next section, you'll learn how to show the content in the "Specs" field on the storefront's product details page.

***

## Step 10: Show Sanity Content in Next.js Starter Storefront

Now that you're managing a product's content in Sanity, you want to show that content on the storefront. In this step, you'll customize the Next.js Starter storefront to show a product's content as defined in Sanity.

A product's details are retrieved in the file `src/app/[countryCode]/(main)/products/[handle]/page.tsx`. So, replace the `ProductPage` function with the following:

```tsx title="src/app/[countryCode]/(main)/products/[handle]/page.tsx" badgeLabel="Storefront" badgeColor="blue" highlights={sanityContentHighlights}
// other imports...
import { client } from "../../../../../sanity/lib/client"

// ...

export default async function ProductPage(props: Props) {
  const params = await props.params
  const region = await getRegion(params.countryCode)

  if (!region) {
    notFound()
  }

  const pricedProduct = await listProducts({
    countryCode: params.countryCode,
    queryParams: { handle: params.handle },
  }).then(({ response }) => response.products[0])

  if (!pricedProduct) {
    notFound()
  }

  // alternatively, you can filter the content by the language
  const sanity = (await client.getDocument(pricedProduct.id))?.specs[0]

  return (
    <ProductTemplate
      product={pricedProduct}
      region={region}
      countryCode={params.countryCode}
    />
  )
}
```

You import the Sanity client defined in `src/sanity/lib/client.ts` (this was generated by Sanity's CLI). Then, in the page's function, you retrieve the product's document by ID and pass its first step to the `ProductTemplate` component.

This is a simplified approach, but you can also have languages in your storefront and filter the spec based on the current language.

Next, you need to customize the `ProductTemplate` to accept the `sanity` prop. In the file `src/modules/products/templates/index.tsx` add the following to `ProductTemplateProps`:

```tsx title="src/modules/products/templates/index.tsx" badgeLabel="Storefront" badgeColor="blue"
type ProductTemplateProps = {
  // ...
  sanity?: {
    content: string
  }
}
```

Then, add the `sanity` property to the expanded props of the component:

```tsx title="src/modules/products/templates/index.tsx" badgeLabel="Storefront" badgeColor="blue"
const ProductTemplate: React.FC<ProductTemplateProps> = ({
  // ...
  sanity,
}) => {
  // ...
}
```

Finally, pass the `sanity` prop to the `ProductInfo` component in the return statement:

```tsx title="src/modules/products/templates/index.tsx" badgeLabel="Storefront" badgeColor="blue"
<ProductInfo product={product} sanity={sanity} />
```

Next, you need to update the `ProductInfo` component to accept and use the `sanity` prop.

In `src/modules/products/templates/product-info/index.tsx`, update the `ProductInfoProps` to accept the `sanity` prop:

```tsx title="src/modules/products/templates/product-info/index.tsx" badgeLabel="Storefront" badgeColor="blue"
type ProductInfoProps = {
  // ...
  sanity?: {
    content: string
  }
}
```

Then, add the `sanity` property to the expanded props of the component:

```tsx title="src/modules/products/templates/index.tsx" badgeLabel="Storefront" badgeColor="blue"
const ProductInfo = ({ 
  // ...
  sanity,
}: ProductInfoProps) => {
  // ...
}
```

Next, find the following line in the return statement:

```tsx title="src/modules/products/templates/index.tsx" badgeLabel="Storefront" badgeColor="blue"
{product.description}
```

And replace it with the following:

```tsx title="src/modules/products/templates/index.tsx" badgeLabel="Storefront" badgeColor="blue"
{sanity?.content || product.description}
```

Instead of showing the product's description on the product details page, this will show the content defined in Sanity if available.

### Test it Out

To test this out, first, run both the Next.js Starter storefront and the Medusa application, and open the Sanity studio. Try editing the content of the first spec of a product.

Then, open the Next.js Starter storefront at `http://localhost:8000` and go to "Store" from the menu, then select the product you edited in Sanity.

In the product's page, you'll find under the product's name the content you put in Sanity.

You can now manage the product's content in Sanity, add more fields, and customize how you show them in the storefront. The Medusa application will also automatically create documents in Sanity for new products you add or update, ensuring your products are always synced across systems.

***

## Step 11: Customize Admin to Manually Sync Data

There are cases where you need to trigger the syncing of products manually, such as when an error occurs or you have products from before creating this integration.

The Medusa Admin dashboard is customizable, allowing you to either inject components, called [widgets](https://docs.medusajs.com/docs/learn/fundamentals/admin/widgets/index.html.md), into existing pages, or adding new pages, called [UI routes](https://docs.medusajs.com/docs/learn/fundamentals/admin/ui-routes/index.html.md). In these customizations, you can send requests to the Medusa application to perform custom operations.

In this step, you'll add a widget to the product's details page. In that page, you'll show whether a product is synced with Sanity, and allow the admin user to trigger syncing it manually.

![The widget in the product details page.](https://res.cloudinary.com/dza7lstvk/image/upload/v1732093722/Medusa%20Resources/Screenshot_2024-11-20_at_11.08.23_AM_wzftfv.png)

Before you do that, however, you need two new API routes in your Medusa application: one to retrieve a document from Sanity, and one to trigger syncing the product data.

An API route is a REST API endpoint that exposes commerce features to the admin dashboard or other frontend clients. Learn more about API routes in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/index.html.md).

### Get Sanity Document API Route

In this section, you'll create the API route to retrieve a sanity document, and the URL to it in the Sanity studio.

To retrieve the URL to the Sanity studio, add the following method in the Sanity Module's service in `src/modules/sanity/service.ts`:

```ts title="src/modules/sanity/service.ts"
class SanityModuleService {
  // ...
  async getStudioLink(
    type: string,
    id: string,
    config: { explicit_type?: boolean } = {}
  ) {
    const resolvedType = config.explicit_type ? type : this.typeMap[type]
    if (!this.studioUrl) {
      throw new Error("No studio URL provided")
    }
    return `${this.studioUrl}/structure/${resolvedType};${id}`
  }
}
```

The method uses the `studioUrl` property, which you set in the `constructor` using the `studio_url` module option, to get the studio link.

Then, to create the API route, create the file `src/api/admin/sanity/documents/[id]/route.ts` with the following content:

```ts title="src/api/admin/sanity/documents/[id]/route.ts"
import { 
  MedusaRequest, 
  MedusaResponse,
} from "@medusajs/framework/http"
import SanityModuleService from "src/modules/sanity/service"
import { SANITY_MODULE } from "../../../../../modules/sanity"

export const GET = async (req: MedusaRequest, res: MedusaResponse) => {
  const { id } = req.params

  const sanityModule: SanityModuleService = req.scope.resolve(
    SANITY_MODULE
  )
  const sanityDocument = await sanityModule.retrieve(id)

  const url = sanityDocument ? 
    await sanityModule.getStudioLink(
      sanityDocument._type,
      sanityDocument._id,
      { explicit_type: true }
    )
    : ""

  res.json({ sanity_document: sanityDocument, studio_url: url })
}
```

This defines a `GET` API route at `/admin/sanity/documents/:id`, where `:id` is a dynamic path parameter indicating the ID of a document to retrieve.

In the `GET` route handler, you resolve the Sanity Module's service and use it to first retrieve the product's document, then the studio link of that document.

You return in the JSON response an object having the `sanity_document` and `studio_url` properties.

You'll test out this route in a later section.

Since the API route is added under the `/admin` prefix, only authenticated admin users can access it. Learn more about protected routes in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/protected-routes/index.html.md).

### Trigger Sanity Sync API Route

In this section, you'll add the API route that manually triggers syncing a product to Sanity.

Since you already have the workflow to sync products, you only need to create an API route that executes it.

Create the file `src/api/admin/sanity/documents/[id]/sync/route.ts` with the following content:

```ts title="src/api/admin/sanity/documents/[id]/sync/route.ts"
import { 
  MedusaRequest, 
  MedusaResponse,
} from "@medusajs/framework/http"
import { 
  sanitySyncProductsWorkflow,
} from "../../../../../../workflows/sanity-sync-products"

export const POST = async (req: MedusaRequest, res: MedusaResponse) => {
  const { transaction } = await sanitySyncProductsWorkflow(req.scope)
    .run({
      input: { product_ids: [req.params.id] },
    })

  res.json({ transaction_id: transaction.transactionId })
}
```

You add a `POST` API route at `/admin/sanity/documents/:id/sync`, where `:id` is a dynamic path parameter that indicates the ID of a product to sync to Sanity.

In the `POST` API route handler, you execute the `sanitySyncProductsWorkflow`, passing it the ID of the product from the path parameter as an input.

In the next section, you'll customize the admin dashboard and send requests to the API route from there.

### Sanity Product Widget

In this section, you'll add a widget in the product details page. The widget will show the Sanity document of the product and triggers syncing it to Sanity using the API routes you created.

To send requests from admin customizations to the Medusa server, you need to use Medusa's [JS SDK](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/js-sdk/index.html.md). You'll also use [Tanstack Query](https://tanstack.com/query/latest) to benefit from features like data caching and invalidation.

Do not install Tanstack Query as that will cause unexpected errors in your development. If you prefer installing it for better auto-completion in your code editor, make sure to install `v5.64.2` as a development dependency.

To configure the JS SDK, create the file `src/admin/lib/sdk.ts` with the following content:

```ts title="src/admin/lib/sdk.ts"
import Medusa from "@medusajs/js-sdk"

export const sdk = new Medusa({
  baseUrl: import.meta.env.VITE_BACKEND_URL || "/",
  debug: import.meta.env.DEV,
  auth: {
    type: "session",
  },
})
```

You initialize the JS SDK and export it. You can learn more about configuring the JS SDK in [this guide](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/js-sdk/index.html.md).

Next, you'll create hooks using Tanstack Query to send requests to the API routes you created earlier.

Create the file `src/admin/hooks/sanity.tsx` with the following content:

```ts title="src/admin/hooks/sanity.tsx"
import { 
  useMutation, 
  UseMutationOptions, 
  useQueryClient, 
} from "@tanstack/react-query"
import { sdk } from "../lib/sdk"

export const useTriggerSanityProductSync = (
  id: string,
  options?: UseMutationOptions
) => {
  const queryClient = useQueryClient()

  return useMutation({
    mutationFn: () =>
      sdk.client.fetch(`/admin/sanity/documents/${id}/sync`, {
        method: "post",
      }),
    onSuccess: (data: any, variables: any, context: any) => {
      queryClient.invalidateQueries({
        queryKey: [`sanity_document`, `sanity_document_${id}`],
      })

      options?.onSuccess?.(data, variables, context)
    },
    ...options,
  })
}
```

You define the `useTriggerSanityProductSync` hook which creates a Tanstack Query mutation that, when executed, sends a request to the API route that triggers syncing the product to Sanity.

Add in the same file another hook:

```ts title="src/admin/hooks/sanity.tsx"
// other imports...
import { 
  // ...
  QueryKey, 
  useQuery, 
  UseQueryOptions,
} from "@tanstack/react-query"
import { FetchError } from "@medusajs/js-sdk"

// ...

export const useSanityDocument = (
  id: string,
  query?: Record<any, any>,
  options?: Omit<
    UseQueryOptions<
      Record<any, any>,
      FetchError,
      { sanity_document: Record<any, any>; studio_url: string },
      QueryKey
    >,
    "queryKey" | "queryFn"
  >
) => {
  const fetchSanityProductStatus = async (query?: Record<any, any>) => {
    return await sdk.client.fetch<Record<any, any>>(
      `/admin/sanity/documents/${id}`,
      {
        query,
      }
    )
  }

  const { data, ...rest } = useQuery({
    queryFn: async () => fetchSanityProductStatus(query),
    queryKey: [`sanity_document_${id}`],
    ...options,
  })

  return { ...data, ...rest }
}
```

You define the hook `useSanityDocument` which retrieves the Sanity document of a product using Tankstack Query.

You can now create the widget injected in a product's details page. Widgets are react components created in a file under the `src/admin/widgets` directory.

So, create the file `src/admin/widgets/sanity-product.tsx` with the following content:

```tsx title="src/admin/widgets/sanity-product.tsx"
import { defineWidgetConfig } from "@medusajs/admin-sdk"
import { AdminProduct, DetailWidgetProps } from "@medusajs/types"
import { ArrowUpRightOnBox } from "@medusajs/icons"
import { Button, CodeBlock, Container, StatusBadge, toast } from "@medusajs/ui"
import { useState } from "react"
import {
  useSanityDocument,
  useTriggerSanityProductSync,
} from "../hooks/sanity"

const ProductWidget = ({ data }: DetailWidgetProps<AdminProduct>) => {
  const { mutateAsync, isPending } = useTriggerSanityProductSync(data.id)
  const { sanity_document, studio_url, isLoading } = useSanityDocument(data.id)
  const [showCodeBlock, setShowCodeBlock] = useState(false)

  const handleSync = async () => {
    try {
      await mutateAsync(undefined)
      toast.success(`Sync triggered.`)
    } catch (err) {
      toast.error(`Couldn't trigger sync: ${
        (err as Record<string, unknown>).message
      }`)
    }
  }

  return (
    <Container>
      <div className="flex justify-between w-full items-center">
        <div className="flex gap-2 items-center">
          <h2>Sanity Status</h2>
          <div>
            {isLoading ? (
              "Loading..."
            ) : sanity_document?.title === data.title ? (
              <StatusBadge color="green">Synced</StatusBadge>
            ) : (
              <StatusBadge color="red">Not Synced</StatusBadge>
            )}
          </div>
        </div>
        <Button
          size="small"
          variant="secondary"
          onClick={handleSync}
          disabled={isPending}
        >
          Sync
        </Button>
      </div>
      <div className="mt-6">
        <div className="mb-4 flex gap-4">
          <Button
            size="small"
            variant="secondary"
            onClick={() => setShowCodeBlock(!showCodeBlock)}
          >
            {showCodeBlock ? "Hide" : "Show"} Sanity Document
          </Button>
          {studio_url && (
            <a href={studio_url} target="_blank" rel="noreferrer">
              <Button variant="transparent">
                <ArrowUpRightOnBox /> Sanity Studio
              </Button>
            </a>
          )}
        </div>
        {!isLoading && showCodeBlock && (
          <CodeBlock
            className="dark"
            snippets={[
              {
                language: "json",
                label: "Sanity Document",
                code: JSON.stringify(sanity_document, null, 2),
              },
            ]}
          >
            <CodeBlock.Body />
          </CodeBlock>
        )}
      </div>
    </Container>
  )
}

// The widget's configurations
export const config = defineWidgetConfig({
  zone: "product.details.after",
})

export default ProductWidget
```

The file exports a `ProductWidget` component and a `config` object created with `defineWidgetConfig` from the Admin Extension SDK. In the `config` object, you specify the zone to inject the widget into in the `zone` property.

Find all widget injection zones in [this reference](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/admin-widget-injection-zones/index.html.md).

In the widget, you use the `useSanityDocument` to retrieve the product's document from Sanity by sending a request to the API route you created earlier. You show that document's details and a button to trigger syncing the data.

When the "Sync" button is clicked, you use the `useTriggerSanityProductSync` hook which sends a request to the API route you created earlier and executes the workflow that syncs the product to Sanity. The workflow will execute in the background, since you configured its step to be async.

To render a widget that matches the rest of the admin dashboard's design, you use components from the [Medusa UI package](https://docs.medusajs.com/ui/index.html.md), such as the `CodeBlock` or `Container` components.

Refer to the [Admin Widgets](https://docs.medusajs.com/docs/learn/fundamentals/admin/widgets/index.html.md) documentation to learn more.

### Test it Out

To test these customizations out, start the Medusa application and open the admin dashboard. Then, choose a product and scroll down to the end of the page.

You'll find a new "Sanity Status" section showing you whether the product is synced to Sanity and its document's details. You can also click the Sync button, which will sync the product to Sanity.

***

## Step 12: Add Track Syncs Page to Medusa Admin

Earlier in this guide when introducing workflows, you learned that you can track the execution of a workflow. As a last step of this guide, you'll add a new page in the admin dashboard that shows the executions of the `sanitySyncProductsWorkflow` and their status. You'll also add the ability to sync all products to Sanity from that page.

![A screenshot of the page to track and trigger syncs.](https://res.cloudinary.com/dza7lstvk/image/upload/v1732095185/Medusa%20Resources/Screenshot_2024-11-20_at_11.09.42_AM_te8xic.png)

### Retrieve Sync Executions API Route

Medusa has a [workflow engine](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/infrastructure-modules/workflow-engine/index.html.md) that manages workflow executions, roll-backs, and other functionalities under the hood.

The workflow engine is an [Infrastructure Module](https://docs.medusajs.com/docs/learn/fundamentals/modules/infrastructure-modules/index.html.md), which can be replaced with a [Redis Workflow Engine](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/infrastructure-modules/workflow-engine/redis/index.html.md), or a custom one of your choice, allowing you to take ownership of your application's tooling.

In your customizations, you can resolve the workflow engine from the container and manage executions of a workflow, such as retrieve them and check their progress.

In this section, you'll create an API route to retrieve the stored executions of the `sanitySyncProductsWorkflow` workflow, so that you can display them later on the dashboard.

When you defined the `sanitySyncProductsWorkflow`, you set its `retentionTime` option so that you can store the workflow execution's details temporarily. If a workflow doesn't have this option set, its execution won't be stored for tracking.

Create the file `src/api/admin/sanity/syncs/route.ts` with the following content:

```ts title="src/api/admin/sanity/syncs/route.ts"
import { MedusaRequest, MedusaResponse } from "@medusajs/framework"
import { Modules } from "@medusajs/framework/utils"
import { 
  sanitySyncProductsWorkflow,
} from "../../../../workflows/sanity-sync-products"

export const GET = async (req: MedusaRequest, res: MedusaResponse) => {
  const workflowEngine = req.scope.resolve(
    Modules.WORKFLOW_ENGINE
  )

  const [executions, count] = await workflowEngine
    .listAndCountWorkflowExecutions(
      {
        workflow_id: sanitySyncProductsWorkflow.getName(),
      },
      { order: { created_at: "DESC" } }
    )

  res.json({ workflow_executions: executions, count })
}
```

You add a `GET` API route at `/admin/sanity/syncs`. In the API route handler, you resolve the Workflow Engine Module's service from the Medusa container. You use the `listAndCountWorkflowExecutions` method to retrieve the executions of the `sanitySyncProductsWorkflow` workflow, filtering by its name.

You return the executions in the JSON response of the route.

### Trigger Sync API Route

In this section, you'll add another API route that triggers syncing all products to Sanity.

In the same file `src/api/admin/sanity/syncs/route.ts`, add the following:

```ts title="api/admin/sanity/syncs/route.ts"
export const POST = async (req: MedusaRequest, res: MedusaResponse) => {
  const { transaction } = await sanitySyncProductsWorkflow(req.scope).run({
    input: {},
  })

  res.json({ transaction_id: transaction.transactionId })
}
```

This adds a `POST` API route at `/admin/sanity/syncs`. In the route handler, you execute the `sanitySyncProductsWorkflow` without passing it a `product_ids` input. The step in the workflow will retrieve all products, instead of filtering them by ID, and sync them to Sanity.

You return the transaction ID of the workflow, which you can use to track the execution's progress since the workflow will run in the background. This is not implemented in this guide, but Medusa has a [Get Execution API route](https://docs.medusajs.com/api/admin#workflows-executions_getworkflowsexecutionsworkflow_idtransaction_id) that you can use to get the details of a workflow's execution.

### Add Sanity UI Route

In this section, you'll add a UI route in the admin dashboard, which is a new page, that shows the list of `sanitySyncProductsWorkflow` executions and allows triggering sync of all products in Medusa.

A UI route is React component exported in a file under the `src/admin/routes` directory. Similar to a widget, a UI route can also send requests to the Medusa application to perform actions using your custom API routes.

Before creating the UI route, you'll create hooks using Tanstack Query that send requests to these UI routes. In the file `src/admin/hooks/sanity.tsx`, add the following two new hooks:

```tsx title="src/admin/hooks/sanity.tsx"
export const useTriggerSanitySync = (options?: UseMutationOptions) => {
  const queryClient = useQueryClient()

  return useMutation({
    mutationFn: () =>
      sdk.client.fetch(`/admin/sanity/syncs`, {
        method: "post",
      }),
    onSuccess: (data: any, variables: any, context: any) => {
      queryClient.invalidateQueries({
        queryKey: [`sanity_sync`],
      })

      options?.onSuccess?.(data, variables, context)
    },
    ...options,
  })
}

export const useSanitySyncs = (
  query?: Record<any, any>,
  options?: Omit<
    UseQueryOptions<
      Record<any, any>,
      FetchError,
      { workflow_executions: Record<any, any>[] },
      QueryKey
    >,
    "queryKey" | "queryFn"
  >
) => {
  const fetchSanitySyncs = async (query?: Record<any, any>) => {
    return await sdk.client.fetch<Record<any, any>>(`/admin/sanity/syncs`, {
      query,
    })
  }

  const { data, ...rest } = useQuery({
    queryFn: async () => fetchSanitySyncs(query),
    queryKey: [`sanity_sync`],
    ...options,
  })

  return { ...data, ...rest }
}
```

The `useTriggerSanitySync` hook creates a mutation that, when executed, sends a request to the trigger sync API route you created earlier to sync all products.

The `useSanitySyncs` hook sends a request to the retrieve sync executions API route that you created earlier to retrieve the workflow's exections.

Finally, to create the UI route, create the file `src/admin/routes/sanity/page.tsx` with the following content:

```tsx title="src/admin/routes/sanity/page.tsx"
import { defineRouteConfig } from "@medusajs/admin-sdk"
import { Sanity } from "@medusajs/icons"
import {
  Badge,
  Button,
  Container,
  Heading,
  Table,
  Toaster,
  toast,
} from "@medusajs/ui"
import { useSanitySyncs, useTriggerSanitySync } from "../../hooks/sanity"

const SanityRoute = () => {
  const { mutateAsync, isPending } = useTriggerSanitySync()
  const { workflow_executions, refetch } = useSanitySyncs()

  const handleSync = async () => {
    try {
      await mutateAsync()
      toast.success(`Sync triggered.`)
      refetch()
    } catch (err) {
      toast.error(`Couldn't trigger sync: ${
        (err as Record<string, unknown>).message
      }`)
    }
  }

  const getBadgeColor = (state: string) => {
    switch (state) {
      case "invoking":
        return "blue"
      case "done":
        return "green"
      case "failed":
        return "red"
      default:
        return "grey"
    }
  }

  return (
    <>
      <Container className="flex flex-col p-0 overflow-hidden">
        <div className="p-6 flex justify-between">
          <Heading className="font-sans font-medium h1-core">
            Sanity Syncs
          </Heading>
          <Button
            variant="secondary"
            size="small"
            onClick={handleSync}
            disabled={isPending}
          >
            Trigger Sync
          </Button>
        </div>
        <Table>
          <Table.Header>
            <Table.Row>
              <Table.HeaderCell>Sync ID</Table.HeaderCell>
              <Table.HeaderCell>Status</Table.HeaderCell>
              <Table.HeaderCell>Created At</Table.HeaderCell>
              <Table.HeaderCell>Updated At</Table.HeaderCell>
            </Table.Row>
          </Table.Header>

          <Table.Body>
            {(workflow_executions || []).map((execution) => (
              <Table.Row
                key={execution.id}
                className="cursor-pointer"
                onClick={() =>
                  (window.location.href = `/app/sanity/${execution.id}`)
                }
              >
                <Table.Cell>{execution.id}</Table.Cell>
                <Table.Cell>
                  <Badge
                    rounded="full"
                    size="2xsmall"
                    color={getBadgeColor(execution.state)}
                  >
                    {execution.state}
                  </Badge>
                </Table.Cell>
                <Table.Cell>{execution.created_at}</Table.Cell>
                <Table.Cell>{execution.updated_at}</Table.Cell>
              </Table.Row>
            ))}
          </Table.Body>
        </Table>
      </Container>
      <Toaster />
    </>
  )
}

export const config = defineRouteConfig({
  label: "Sanity",
  icon: Sanity,
})

export default SanityRoute
```

The file's path relative to the `src/admin/routes` directory indicates its path in the admin dashboard. So, this adds a new route at the path `http://localhost:9000/app/sanity`.

The file must export the UI route's component. Also, to add an item in the sidebar for the UI route, you export a configuration object, created with `defineRouteConfig` from the Admin Extension SDK. The function accepts the following properties:

- `label`: The sidebar item's label.
- `icon`: The icon to the show in the sidebar.

In the UI route, you use the `useSanitySyncs` hook to retrieve the list of sync executions and display them with their status. You also show a "Trigger Sync" button that, when clicked, uses the mutation from the `useTriggerSanitySync` hook to send a request to the Medusa application and trigger the sync.

To display components that match the design of the Medusa Admin, you use components from the [Medusa UI package](https://docs.medusajs.com/ui/index.html.md).

Learn more about UI routes in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/admin/ui-routes/index.html.md).

### Test it Out

To test it out, start the Medusa application and open the admin dashboard. After logging in, you'll find a new "Sanity" item in the sidebar.

If you click on it, you'll see a table of the latest syncs. You also trigger syncing by clicking the "Trigger Sync" button. After you click the button, you should see a new execution added to the table.

***

## Next Steps

You've now integrated Medusa with Sanity and can benefit from powerful commerce and CMS features.

If you're new to Medusa, check out the [main documentation](https://docs.medusajs.com/docs/learn/index.html.md), 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](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/index.html.md).


# Integrate Medusa with ShipStation (Fulfillment)

In this guide, you'll learn how to integrate Medusa with ShipStation.

Refer your technical team to this guide to integrate ShipStation with your Medusa application. You can then enable it using the Medusa Admin as explained in [this user guide](https://docs.medusajs.com/user-guide/settings/locations-and-shipping/locations#manage-fulfillment-providers/index.html.md).

When you install a Medusa application, you get a fully-fledged commerce platform with support for customizations. Medusa's [Fulfillment Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/fulfillment/index.html.md) provides fulfillment-related resources and functionalities in your store, but it delegates the processing and shipment of order fulfillments to providers that you can integrate.

[ShipStation](https://shipstation.com/) is a shipping toolbox that connects all your shipping providers within one platform. By integrating it with Medusa, you can allow customers to choose from different providers like DHL and FedEx and view price rates retrieved from ShipStation. Admin users will also process the order fulfillment using the ShipStation integration.

This guide will teach you how to:

- Install and set up Medusa.
- Set up a ShipStation account.
- Integrate ShipStation as a fulfillment provider in Medusa.

You can follow this guide whether you're new to Medusa or an advanced Medusa developer.

[Example Repository](https://github.com/medusajs/examples/tree/main/shipstation-integration): Find the full code of the guide in this repository.

***

## Step 1: Install a Medusa Application

### Prerequisites

- [Node.js v20+](https://nodejs.org/en/download)
- [Git CLI tool](https://git-scm.com/downloads)
- [PostgreSQL](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 in a directory with your project's name, and the Next.js Starter Storefront in a directory with the `{project-name}-storefront` name.

The Medusa application is composed of a headless Node.js server and an admin dashboard. The storefront is installed or custom-built separately and connects to the Medusa application through its REST endpoints, called [API routes](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/index.html.md). Learn more about Medusa's architecture in [this documentation](https://docs.medusajs.com/docs/learn/introduction/architecture/index.html.md).

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](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/troubleshooting/create-medusa-app-errors/index.html.md) for help.

***

## Step 2: Prepare ShipStation Account

In this step, you'll prepare your ShipStation account before integrating it into Medusa. If you don't have an account, create one [here](https://www.shipstation.com/start-a-free-trial).

### Enable Carriers

To create labels for your shipments, you need to enable carriers. This requires you to enter payment and address details.

To enable carriers:

1. On the Onboard page, in the "Enable carriers & see rates" section, click on the "Enable Carriers" button.

![Scroll down to the Enable carriers & see rates section, and find the "Enable Carriers" button.](https://res.cloudinary.com/dza7lstvk/image/upload/v1734523873/Medusa%20Resources/Screenshot_2024-12-18_at_2.10.54_PM_pmvcfr.png)

2. In the pop-up that opens, click on Continue Setup.

![Click on the green Continue Setup button](https://res.cloudinary.com/dza7lstvk/image/upload/v1734524261/Medusa%20Resources/Screenshot_2024-12-18_at_2.11.47_PM_wsl98i.png)

3. In the next section of the form, you have to enter your payment details and billing address. Once done, click on Continue Setup.
4. After that, click the checkboxes on the Terms of Service section, then click the Finish Setup button.

![Enable the two checkboxes, then click on Finish Setup at the bottom right](https://res.cloudinary.com/dza7lstvk/image/upload/v1734524486/Medusa%20Resources/Screenshot_2024-12-18_at_2.20.12_PM_pkixma.png)

5. Once you're done, you can optionally add funds to your account. If you're not US-based, make sure to disable ParcelGuard insurance. Otherwise, an error will occur while retrieving rates later.

### Add Carriers

You must have at least one carrier (shipping provider) added in your ShipStation account. You'll later provide shipping options for each of these carriers in your Medusa application.

To add carriers:

1. On the Onboard page, in the "Enable carriers & see rates" section, click on the "Add your carrier accounts" link.

![Scroll down to the Enable carriers & see rates section, and find the "Add your carrier accounts" link under the "Enable Carriers" button](https://res.cloudinary.com/dza7lstvk/image/upload/v1734336612/Medusa%20Resources/Screenshot_2024-12-16_at_10.09.08_AM_nqshhg.png)

2. Click on a provider from the pop-up window.

![Click on the provider tiles in the pop-up window](https://res.cloudinary.com/dza7lstvk/image/upload/v1734336826/Medusa%20Resources/Screenshot_2024-12-16_at_10.13.37_AM_og4sdq.png)

Based on the provider you chose, you'll have to enter your account details, then submit the form.

### Activate Shipping API

To integrate ShipStation using their API, you must enable the Shipping API Add-On. To do that:

1. Go to Add-Ons from the navigation bar.
2. Find Shipping API and activate it.

You'll later retrieve your API key.

***

## Step 3: Create ShipStation 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 Fulfillment Module delegates processing fulfillments and shipments to other modules, called module providers. In this step, you'll create a ShipStation Module Provider that implements all functionalities required for fulfillment. In later steps, you'll add into Medusa shipping options for ShipStation, and allow customers to choose it during checkout.

Learn more about modules in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/modules/index.html.md).

### Create Module Directory

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

![The directory structure of the Medusa application after adding the module's directory](https://res.cloudinary.com/dza7lstvk/image/upload/v1734338950/Medusa%20Resources/shipstation-dir-overview-1_dlsrbv.jpg)

### 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 ShipStation Module Provider's service and the methods necessary to handle fulfillment.

Start by creating the file `src/modules/shipstation/service.ts` with the following content:

![The directory structure of the Medusa application after adding the service](https://res.cloudinary.com/dza7lstvk/image/upload/v1734339042/Medusa%20Resources/shipstation-dir-overview-2_cmgvcj.jpg)

```ts title="src/modules/shipstation/service.ts" highlights={serviceHighlights1}
import { AbstractFulfillmentProviderService } from "@medusajs/framework/utils"

export type ShipStationOptions = {
  api_key: string
}

class ShipStationProviderService extends AbstractFulfillmentProviderService {
  static identifier = "shipstation"
  protected options_: ShipStationOptions

  constructor({}, options: ShipStationOptions) {
    super()

    this.options_ = options
  }

  // TODO add methods
}

export default ShipStationProviderService
```

A Fulfillment Module Provider service must extend the `AbstractFulfillmentProviderService` class. You'll implement the abstract methods of this class in the upcoming sections.

The service must have an `identifier` static property, which is a unique identifier for the provider. You set the identifier to `shipstation`.

A module can receive options that are set when you later add the module to Medusa's configurations. These options allow you to safely store secret values outside of your code.

The ShipStation module requires an `api_key` option, indicating your ShipStation's API key. You receive the options as a second parameter of the service's constructor.

### Create Client

To send requests to ShipStation, you'll create a client class that provides the methods to send requests. You'll then use that class in your service.

Create the file `src/modules/shipstation/client.ts` with the following content:

![The directory structure of the Medusa application after adding the client file](https://res.cloudinary.com/dza7lstvk/image/upload/v1734339519/Medusa%20Resources/shipstation-dir-overview-3_b8im2d.jpg)

```ts title="src/modules/shipstation/client.ts" highlights={clientHighlights1}
import { ShipStationOptions } from "./service"
import { MedusaError } from "@medusajs/framework/utils"

export class ShipStationClient {
  options: ShipStationOptions

  constructor(options) {
    this.options = options
  }

  private async sendRequest(url: string, data?: RequestInit): Promise<any> {
    return fetch(`https://api.shipstation.com/v2${url}`, {
      ...data,
      headers: {
        ...data?.headers,
        "api-key": this.options.api_key,
        "Content-Type": "application/json",
      },
    }).then((resp) => {
      const contentType = resp.headers.get("content-type")
      if (!contentType?.includes("application/json")) {
        return resp.text()
      }

      return resp.json()
    })
    .then((resp) => {
      if (typeof resp !== "string" && resp.errors?.length) {
        throw new MedusaError(
          MedusaError.Types.INVALID_DATA,
          `An error occured while sending a request to ShipStation: ${
            resp.errors.map((error) => error.message)
          }`
        )
      }

      return resp
    })
  }
}
```

The `ShipStationClient` class accepts the ShipStation options in its constructor and sets those options in the `options` property.

You also add a private `sendRequest` method that accepts a path to send a request to and the request's configurations. In the method, you send a request using the Fetch API, passing the API key from the options in the request header. You also parse the response body based on its content type, and check if there are any errors to be thrown before returning the parsed response.

You'll add more methods to send requests in the upcoming steps.

To use the client in `ShipStationProviderService`, add it as a class property and initialize it in the constructor:

```ts title="src/modules/shipstation/service.ts" highlights={serviceHighlights2}
// imports...
import { ShipStationClient } from "./client"

// ...

class ShipStationProviderService extends AbstractFulfillmentProviderService {
  // properties...
  protected client: ShipStationClient

  constructor({}, options: ShipStationOptions) {
    // ...
    this.client = new ShipStationClient(options)
  }
}
```

You import `ShipStationClient` and add a new `client` property in `ShipStationProviderService`. In the class's constructor, you set the `client` property by initializing `ShipStationProviderService`, passing it the module's options.

You'll use the `client` property when implementing the service's methods.

### Implement Service Methods

In this section, you'll go back to the `ShipStationProviderService` method to implement the abstract methods of `AbstractFulfillmentProviderService`.

Refer to [this guide](https://docs.medusajs.com/references/fulfillment/provider/index.html.md) for a full reference of all methods, their parameters and return types.

#### getFulfillmentOptions

The `getFulfillmentOptions` method returns the options that this fulfillment provider supports. When admin users add shipping options later in the Medusa Admin, they'll select one of these options.

Learn more about shipping options in [this guide](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/fulfillment/shipping-option/index.html.md).

ShipStation requires that a shipment must be associated with a carrier and one of its services. So, in this method, you'll retrieve the list of carriers from ShipStation and return them as fulfillment options. Shipping options created from these fulfillment options will always have access to the option's carrier and service.

Before you start implementing methods, you'll add the expected carrier types returned by ShipStation. Create the file `src/modules/shipstation/types.ts` with the following content:

![The directory structure of the Medusa application after adding the types file](https://res.cloudinary.com/dza7lstvk/image/upload/v1734340402/Medusa%20Resources/shipstation-dir-overview-4_fwsle0.jpg)

```ts title="src/modules/shipstation/types.ts"
export type Carrier = {
  carrier_id: string
  disabled_by_billing_plan: boolean
  friendly_name: string
  services: {
    service_code: string
    name: string
  }[]
  packages: {
    package_code: string
  }[]
  [k: string]: unknown
}

export type CarriersResponse = {
  carriers: Carrier[]
}
```

You define a `Carrier` type that holds a carrier's details, and a `CarriersResponse` type, which is the response returned by ShipStation.

A carrier has more fields that you can use. Refer to [ShipStation's documentation](https://docs.shipstation.com/openapi/carriers/list_carriers#carriers/list_carriers/t=response\&c=200\&path=carriers) for all carrier fields.

Next, you'll add in `ShipStationClient` the method to retrieve the carriers from ShipStation. So, add to the class defined in `src/modules/shipstation/client.ts` a new method:

```ts title="src/modules/shipstation/client.ts" highlights={clientHighlights2}
// other imports...
import { 
  CarriersResponse,
} from "./types"

export class ShipStationClient {
  // ...
  async getCarriers(): Promise<CarriersResponse> {
    return await this.sendRequest("/carriers") 
  }
}
```

You added a new `getCarriers` method that uses the `sendRequest` method to send a request to the [ShipStation's List Carriers endpoint](https://docs.shipstation.com/openapi/carriers/list_carriers). The method returns `CarriersResponse` that you defined earlier.

Finally, add the `getFulfillmentOptions` method to `ShipStationProviderService`:

```ts title="src/modules/shipstation/service.ts" highlights={serviceHighlights3}
// other imports...
import { 
  FulfillmentOption,
} from "@medusajs/framework/types"

class ShipStationProviderService extends AbstractFulfillmentProviderService {
  // ...
  async getFulfillmentOptions(): Promise<FulfillmentOption[]> {
    const { carriers } = await this.client.getCarriers() 
    const fulfillmentOptions: FulfillmentOption[] = []

    carriers
      .filter((carrier) => !carrier.disabled_by_billing_plan)
      .forEach((carrier) => {
        carrier.services.forEach((service) => {
          fulfillmentOptions.push({
            id: `${carrier.carrier_id}__${service.service_code}`,
            name: service.name,
            carrier_id: carrier.carrier_id,
            carrier_service_code: service.service_code,
          })
        })
      })

    return fulfillmentOptions
  }
}
```

In the `getFulfillmentOptions` method, you retrieve the carriers from ShipStation. You then filter out the carriers disabled by your ShipStation billing plan, and loop over the remaining carriers and their services.

You return an array of fulfillment-option objects, where each object represents a carrier and service pairing. Each object has the following properties:

- an `id` property, which you set to a combination of the carrier ID and the service code.
- a `name` property, which you set to the service's `name`. The admin user will see this name when they create a shipping option for the ShipStation provider.
- You can pass other data, such as `carrier_id` and `carrier_service_code`, and Medusa will store the fulfillment option in the `data` property of shipping options created later.

Learn more about the shipping option's `data` property in [this guide](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/fulfillment/shipping-option/index.html.md).

You'll see this method in action later when you create a shipping option.

#### canCalculate

When an admin user creates a shipping option for your provider, they can choose whether the price is flat rate or calculated during checkout.

If the user chooses calculated, Medusa validates that your fulfillment provider supports calculated prices using the `canCalculate` method of your provider's service.

This method accepts the shipping option's `data` field, which will hold the data of an option returned by `getFulfillmentOptions`. It returns a boolean value indicating whether the shipping option can have a calculated price.

Add the method to `ShipStationProviderService` in `src/modules/shipstation/service.ts`:

```ts title="src/modules/shipstation/service.ts"
// other imports...
import {
  // ...
  CreateShippingOptionDTO,
} from "@medusajs/framework/types"

class ShipStationProviderService extends AbstractFulfillmentProviderService {
  // ...
  async canCalculate(data: CreateShippingOptionDTO): Promise<boolean> {
    return true
  }
}
```

Since all shipping option prices can be calculated with ShipStation based on the chosen carrier and service zone, you always return `true` in this method.

You'll implement the calculation mechanism in a later method.

#### calculatePrice

When the customer views available shipping options during checkout, the Medusa application requests the calculated price from your fulfillment provider using its `calculatePrice` method.

To retrieve shipping prices with ShipStation, you create a shipment first then get its rates. So, in the `calculatePrice` method, you'll either:

- Send a request to [ShipStation's get shipping rates endpoint](https://docs.shipstation.com/openapi/rates/calculate_rates) that creates a shipment and returns its prices;
- Or, if a shipment was already created before, you'll retrieve its prices using [ShipStation's get shipment rates endpoint](https://docs.shipstation.com/openapi/shipments/list_shipment_rates).

First, add the following types to `src/modules/shipstation/types.ts`:

```ts title="src/modules/shipstation/types.ts" highlights={typesHighlights1}
export type ShipStationAddress = {
  name: string
  phone: string
  email?: string | null
  company_name?: string | null
  address_line1: string
  address_line2?: string | null
  address_line3?: string | null
  city_locality: string
  state_province: string
  postal_code: string
  country_code: string
  address_residential_indicator: "unknown" | "yes" | "no"
  instructions?: string | null
  geolocation?: {
    type?: string
    value?: string
  }[]
}

export type Rate = {
  rate_id: string
  shipping_amount: {
    currency: string
    amount: number
  }
  insurance_amount: {
    currency: string
    amount: number
  }
  confirmation_amount: {
    currency: string
    amount: number
  }
  other_amount: {
    currency: string
    amount: number
  }
  tax_amount: {
    currency: string
    amount: number
  }
}

export type RateResponse = {
  rates: Rate[]
}

export type GetShippingRatesRequest = {
  shipment_id?: string
  shipment?: Omit<Shipment, "shipment_id" | "shipment_status">
  rate_options: {
    carrier_ids: string[]
    service_codes: string[]
    preferred_currency: string
  }
}

export type GetShippingRatesResponse = {
  shipment_id: string
  carrier_id?: string
  service_code?: string
  external_order_id?: string
  rate_response: RateResponse
}

export type Shipment = {
  shipment_id: string
  carrier_id: string
  service_code: string
  ship_to: ShipStationAddress
  return_to?: ShipStationAddress
  is_return?: boolean
  ship_from: ShipStationAddress
  items?: [
    {
      name?: string
      quantity?: number
      sku?: string
    }
  ]
  warehouse_id?: string
  shipment_status: "pending" | "processing" | "label_purchased" | "cancelled"
  [k: string]: unknown
}

```

You add the following types:

- `ShipStationAddress`: an address to ship from or to.
- `Rate`: a price rate for a specified carrier and service zone.
- `RateResponse`: The response when retrieving rates.
- `GetShippingRatesRequest`: The request body data for [ShipStation's get shipping rates endpoint](https://docs.shipstation.com/openapi/rates/calculate_rates). You can refer to their API reference for other accepted parameters.
- `GetShippingRatesResponse`: The response of the [ShipStation's get shipping rates endpoint](https://docs.shipstation.com/openapi/rates/calculate_rates). You can refer to their API reference for other response fields.
- `Shipment`: A shipment's details.

Then, add the following methods to `ShipStationClient`:

```ts title="src/modules/shipstation/client.ts" highlights={serviceHighlights7}
// other imports...
import { 
  // ...
  GetShippingRatesRequest,
  GetShippingRatesResponse,
  RateResponse,
} from "./types"

export class ShipStationClient {
  // ...
  async getShippingRates(
    data: GetShippingRatesRequest
  ): Promise<GetShippingRatesResponse> {
    return await this.sendRequest("/rates", {
      method: "POST",
      body: JSON.stringify(data),
    }).then((resp) => {
      if (resp.rate_response.errors?.length) {
        throw new MedusaError(
          MedusaError.Types.INVALID_DATA,
          `An error occured while retrieving rates from ShipStation: ${
            resp.rate_response.errors.map((error) => error.message)
          }`
        )
      }

      return resp
    })
  }

  async getShipmentRates(id: string): Promise<RateResponse[]> {
    return await this.sendRequest(`/shipments/${id}/rates`)
  }
}
```

The `getShippingRates` method accepts as a parameter the data to create a shipment and retrieve its rate. In the method, you send the request using the `sendRequest` method, and throw any errors in the rate retrieval before returning the response.

The `getShipmentRates` method accepts the ID of the shipment as a parameter, sends the request using the `sendRequest` method and returns its response holding the shipment's rates.

Next, add to `ShipStationProviderService` a private method that'll be used to create a shipment in ShipStation and get its rates:

```ts title="src/modules/shipstation/service.ts" highlights={serviceHighlights8}
// other imports...
import {
  // ...
  MedusaError,
} from "@medusajs/framework/utils"
import { 
  // ...
  CalculateShippingOptionPriceDTO,
} from "@medusajs/framework/types"
import {
  GetShippingRatesResponse,
  ShipStationAddress,
} from "./types"

class ShipStationProviderService extends AbstractFulfillmentProviderService {
  // ...
  private async createShipment({
    carrier_id,
    carrier_service_code,
    from_address,
    to_address,
    items,
    currency_code,
  }: {
    carrier_id: string
    carrier_service_code: string
    from_address?: {
      name?: string
      address?: Omit<
        StockLocationAddressDTO, "created_at" | "updated_at" | "deleted_at"
      >
    },
    to_address?: Omit<
      CartAddressDTO, "created_at" | "updated_at" | "deleted_at" | "id"
    >,
    items: CartLineItemDTO[] | OrderLineItemDTO[],
    currency_code: string
  }): Promise<GetShippingRatesResponse> {
    if (!from_address?.address) {
      throw new MedusaError(
        MedusaError.Types.INVALID_DATA,
        "from_location.address is required to calculate shipping rate"
      )
    }
    const ship_from: ShipStationAddress = {
      name: from_address?.name || "",
      phone: from_address?.address?.phone || "",
      address_line1: from_address?.address?.address_1 || "",
      city_locality: from_address?.address?.city || "",
      state_province: from_address?.address?.province || "",
      postal_code: from_address?.address?.postal_code || "",
      country_code: from_address?.address?.country_code || "",
      address_residential_indicator: "unknown",
    }
    if (!to_address) {
      throw new MedusaError(
        MedusaError.Types.INVALID_DATA,
        "shipping_address is required to calculate shipping rate"
      )
    }
    
    const ship_to: ShipStationAddress = {
      name: `${to_address.first_name} ${to_address.last_name}`,
      phone: to_address.phone || "",
      address_line1: to_address.address_1 || "",
      city_locality: to_address.city || "",
      state_province: to_address.province || "",
      postal_code: to_address.postal_code || "",
      country_code: to_address.country_code || "",
      address_residential_indicator: "unknown",
    }

    // TODO create shipment
  }
}
```

The `createShipment` method accepts as a parameter an object having the following properties:

- `carrier_id`: The ID of the carrier to create the shipment for.
- `carrier_service_code`: The code of the carrier's service.
- `from_address`: The address to ship items from, which is the address of the stock location associated with a shipping option.
- `to_address`: The address to ship items to, which is the customer's address.
- `items`: An array of the items in the cart or order (for fulfilling the order later).
- `currency_code`: The currency code of the cart or order.

In the `createShipment` method, so far you only prepare the data to be sent to ShipStation. ShipStation requires the addresses to ship the items from and to.

To send the request, replace the `TODO` with the following:

```ts title="src/modules/shipstation/service.ts"
// Sum the package's weight
// You can instead create different packages for each item
const packageWeight = items.reduce((sum, item) => {
  // @ts-ignore
  return sum + (item.variant.weight || 0)
}, 0)

return await this.client.getShippingRates({
  shipment: {
    carrier_id: carrier_id,
    service_code: carrier_service_code,
    ship_to,
    ship_from,
    validate_address: "no_validation",
    items: items?.map((item) => ({
      name: item.title,
      quantity: item.quantity,
      sku: item.variant_sku || "",
    })),
    packages: [{
      weight: {
        value: packageWeight,
        unit: "kilogram",
      },
    }],
    customs: {
      contents: "merchandise",
      non_delivery: "return_to_sender",
    },
  },
  rate_options: {
    carrier_ids: [carrier_id],
    service_codes: [carrier_service_code],
    preferred_currency: currency_code as string,
  },
})
```

You create a shipment and get its rates using the `getShippingRates` method you added to the client. You pass the method the expected request body parameters by [ShipStation's get shipping rates endpoint](https://docs.shipstation.com/openapi/rates/calculate_rates), including the carrier ID, the items to be shipped, and more.

The above snippet assumes all items are sent in a single package. You can instead pass a package for each item, specifying its weight and optionally its height, width, and length.

Finally, add the `calculatePrice` method to `ShipStationProviderService`:

```ts title="src/modules/shipstation/service.ts" highlights={serviceHighlights5}
// other imports...
import { 
  // ...
  CalculatedShippingOptionPrice,
} from "@medusajs/framework/types"

class ShipStationProviderService extends AbstractFulfillmentProviderService {
  // ...
  async calculatePrice(
    optionData: CalculateShippingOptionPriceDTO["optionData"], 
    data: CalculateShippingOptionPriceDTO["data"], 
    context: CalculateShippingOptionPriceDTO["context"]
  ): Promise<CalculatedShippingOptionPrice> {
    const { shipment_id } = data as {
      shipment_id?: string
    } || {}
    const { carrier_id, carrier_service_code } = optionData as {
      carrier_id: string
      carrier_service_code: string
    }
    let rate: Rate | undefined

    if (!shipment_id) {
      const shipment = await this.createShipment({
        carrier_id,
        carrier_service_code,
        from_address: {
          name: context.from_location?.name,
          address: context.from_location?.address,
        },
        to_address: context.shipping_address,
        items: context.items || [],
        currency_code: context.currency_code as string,
      })
      rate = shipment.rate_response.rates[0]
    } else {
      const rateResponse = await this.client.getShipmentRates(shipment_id)
      rate = rateResponse[0].rates[0]
    }

    const calculatedPrice = !rate ? 0 : rate.shipping_amount.amount + rate.insurance_amount.amount + 
      rate.confirmation_amount.amount + rate.other_amount.amount + 
      (rate.tax_amount?.amount || 0)

    return {
      calculated_amount: calculatedPrice,
      is_calculated_price_tax_inclusive: !!rate?.tax_amount,
    }
  }
}
```

The `calculatePrice` method accepts the following parameters:

1. The `data` property of the chosen shipping option during checkout.
2. The `data` property of the shipping method, which will hold the ID of the shipment in ShipStation.
3. An object of the checkout's context, including the cart's items, the location associated with the shipping option, and more.

In the method, you first check if a `shipment_id` is already stored in the shipping method's `data` property. If so, you retrieve the shipment's rates using the client's `getShipmentRates` method. Otherwise, you use the `createShipment` method to create the shipment and get its rates.

A rate returned by ShipStation has four properties that, when added up, make up the full price: `shipping_amount`, `insurance_amount`, `confirmation_amount`, and `other_amount`. It may have a `tax_amount` property, which is the amount for applied taxes.

Learn more about these fields in [ShipStation's documentation](https://docs.shipstation.com/rate-shopping#about-the-response).

The method returns an object having the following properties:

- `calculated_amount`: The shipping method's price calculated by adding the four rate properties with the tax property, if available.
- `is_calculated_price_tax_inclusive`: Whether the price includes taxes, which is inferred from whether the `tax_amount` property is set in the rate.

Customers will now see the calculated price of a ShipStation shipping option during checkout.

#### validateFulfillmentData

When a customer chooses a shipping option during checkout, Medusa creates a shipping method from that option. A shipping method has a `data` property to store data relevant for later processing of the method and its fulfillments.

So, in the `validateFulfillmentData` method of your provider, you'll create a shipment in ShipStation if it wasn't already created using their [get shipping rates endpoint](https://docs.shipstation.com/openapi/rates/calculate_rates), and store the ID of that shipment in the created shipping method's `data` property.

Add the `validateFulfillmentData` method to `ShipStationProviderService`:

```ts title="src/modules/shipstation/service.ts" highlights={serviceHighlights4}
class ShipStationProviderService extends AbstractFulfillmentProviderService {
  // ...
  async validateFulfillmentData(
    optionData: Record<string, unknown>, 
    data: Record<string, unknown>, 
    context: Record<string, unknown>
  ): Promise<any> {
    let { shipment_id } = data as {
      shipment_id?: string
    }

    if (!shipment_id) {
      const { carrier_id, carrier_service_code } = optionData as {
        carrier_id: string
        carrier_service_code: string
      }
      const shipment = await this.createShipment({
        carrier_id,
        carrier_service_code,
        from_address: {
          // @ts-ignore
          name: context.from_location?.name,
          // @ts-ignore
          address: context.from_location?.address,
        },
        // @ts-ignore
        to_address: context.shipping_address,
        // @ts-ignore
        items: context.items || [],
        // @ts-ignore
        currency_code: context.currency_code,
      })
      shipment_id = shipment.shipment_id
    }

    return {
      ...data,
      shipment_id,
    }
  }
}
```

The `validateFulfillmentData` method accepts the following parameters:

1. The `data` property of the chosen shipping option during checkout. It will hold the carrier ID and its service code.
2. The `data` property of the shipping method to be created. This can hold custom data sent in the [Add Shipping Method API route](https://docs.medusajs.com/api/store#carts_postcartsidshippingmethods).
3. An object of the checkout's context, including the cart's items, the location associated with the shipping option, and more.

In the method, you try to retrieve the shipment ID from the shipping method's `data` parameter if it was already created. If not, you create the shipment in ShipStation using the `createShipment` method.

Finally, you return the object to be stored in the shipping method's `data` property. You include in it the ID of the shipment in ShipStation.

#### createFulfillment

After the customer places the order, the admin user can manage its fulfillments. When the admin user creates a fulfillment for the order, Medusa uses the `createFulfillment` method of the associated provider to handle any processing in the third-party provider.

This method supports creating split fulfillments, meaning you can partially fulfill and order's items. So, you'll create a new shipment, then purchase a label for that shipment. You'll use the existing shipment to retrieve details like the address to ship from and to.

First, add a new type to `src/modules/shipstation/types.ts`:

```ts title="src/modules/shipstation/types.ts"
export type Label = {
  label_id: string
  status: "processing" | "completed" | "error" | "voided"
  shipment_id: string
  ship_date: Date
  shipment_cost: {
    currency: string
    amount: number
  }
  insurance_cost: {
    currency: string
    amount: number
  }
  confirmation_amount: {
    currency: string
    amount: number
  }
  tracking_number: string
  is_return_label: boolean
  carrier_id: string
  service_code: string
  trackable: string
  tracking_status: "unknown" | "in_transit" | "error" | "delivered"
  label_download: {
    href: string
    pdf: string
    png: string
    zpl: string
  }
}
```

You add the `Label` type for the details in a label object. You can find more properties in [ShipStation's documentation](https://docs.shipstation.com/openapi/labels/create_label#labels/create_label/response\&c=200/body).

Then, add the following methods to the `ShipStationClient`:

```ts title="src/modules/shipstation/client.ts"
// other imports...
import { 
  // ...
  Label,
  Shipment,
} from "./types"

export class ShipStationClient {
  // ...

  async getShipment(id: string): Promise<Shipment> {
    return await this.sendRequest(`/shipments/${id}`)
  }

  async purchaseLabelForShipment(id: string): Promise<Label> {
    return await this.sendRequest(`/labels/shipment/${id}`, {
      method: "POST",
      body: JSON.stringify({}),
    })
  }
}
```

You add the `getShipment` method to retrieve a shipment's details, and the `purchaseLabelForShipment` method to purchase a label in ShipStation for a shipment by its ID.

Finally, add the `createFulfillment` method in `ShipStationProviderService`:

```ts title="src/modules/shipstation/service.ts" highlights={serviceHighlights6}
class ShipStationProviderService extends AbstractFulfillmentProviderService {
  // ...
  async createFulfillment(
    data: object, 
    items: object[], 
    order: object | undefined, 
    fulfillment: Record<string, unknown>
  ): Promise<any> {
    const { shipment_id } = data as {
      shipment_id: string
    }

    const originalShipment = await this.client.getShipment(shipment_id)

    const orderItemsToFulfill = []

    items.map((item) => {
      // @ts-ignore
      const orderItem = order.items.find((i) => i.id === item.line_item_id)

      if (!orderItem) {
        return
      }

      // @ts-ignore
      orderItemsToFulfill.push({
        ...orderItem,
        // @ts-ignore
        quantity: item.quantity,
      })
    })

    const newShipment = await this.createShipment({
      carrier_id: originalShipment.carrier_id,
      carrier_service_code: originalShipment.service_code,
      from_address: {
        name: originalShipment.ship_from.name,
        address: {
          ...originalShipment.ship_from,
          address_1: originalShipment.ship_from.address_line1,
          city: originalShipment.ship_from.city_locality,
          province: originalShipment.ship_from.state_province,
        },
      },
      to_address: {
        ...originalShipment.ship_to,
        address_1: originalShipment.ship_to.address_line1,
        city: originalShipment.ship_to.city_locality,
        province: originalShipment.ship_to.state_province,
      },
      items: orderItemsToFulfill as OrderLineItemDTO[],
      // @ts-ignore
      currency_code: order.currency_code,
    })

    const label = await this.client.purchaseLabelForShipment(newShipment.shipment_id)

    return {
      data: {
        ...(fulfillment.data as object || {}),
        label_id: label.label_id,
        shipment_id: label.shipment_id,
      },
    }
  }
}
```

This method accepts the following parameters:

- `data`: The `data` property of the associated shipping method, which holds the ID of the shipment.
- `items`: The items to fulfill.
- `order`: The order's details.
- `fulfillment`: The details of the fulfillment to be created.

In the method, you:

- Retrieve the details of the shipment originally associated with the fulfillment's shipping method.
- Filter out the order items to retrieve the items to fulfill.
- Create a new shipment for the items to fulfill. You use the original shipment for details like the carrier ID or the addresses to ship from and to.
- Purchase a label for the new shipment.

You return an object whose `data` property will be stored in the created fulfillment's `data` property. You store in it the ID of the purchased label and the ID of its associated shipment.

#### cancelFulfillment

The last method you'll implement is the `cancelFulfillment` method. When an admin user cancels a fulfillment, Medusa uses the associated provider's `cancelFulfillment` method to perform any necessary actions in the third-party provider.

You'll use this method to void the label in ShipStation that was purchased in the `createFulfillment` method and cancel its associated shipment.

Start by adding the following type to `src/modules/shipstation/types.ts`:

```ts title="src/modules/shipstation/types.ts"
export type VoidLabelResponse = {
  approved: boolean
  message: string
  reason_code?: string
}
```

`VoidLabelResponse` is the response type of [ShipStation's void label endpoint](https://docs.shipstation.com/openapi/labels/void_label).

Next, add two methods to `ShipStationClient`:

```ts title="src/modules/shipstation/client.ts" highlights={clientHighlights4}
// other imports...
import { 
  // ...
  VoidLabelResponse,
} from "./types"

export class ShipStationClient {
  // ...
  async voidLabel(id: string): Promise<VoidLabelResponse> {
    return await this.sendRequest(`/labels/${id}/void`, {
      method: "PUT",
    })
  }

  async cancelShipment(id: string): Promise<void> {
    return await this.sendRequest(`/shipments/${id}/cancel`, {
      method: "PUT",
    })
  }
}
```

You added two methods:

- `voidLabel` that accepts the ID of a label to void using [ShipStation's endpoint](https://docs.shipstation.com/openapi/labels/void_label).
- `cancelShipment` that accepts the ID of a shipment to cancel using [ShipStation's endpoint](https://docs.shipstation.com/openapi/shipments/cancel_shipments).

Finally, in `ShipStationProviderService`, add the `cancelFulfillment` method:

```ts title="src/modules/shipstation/service.ts"
class ShipStationProviderService extends AbstractFulfillmentProviderService {
  // ...
  async cancelFulfillment(data: Record<string, unknown>): Promise<any> {
    const { label_id, shipment_id } = data as {
      label_id: string
      shipment_id: string
    }

    await this.client.voidLabel(label_id)
    await this.client.cancelShipment(shipment_id)
  }
}
```

This method accepts the fulfillment's `data` property as a parameter. You get the ID of the label and shipment from the `data` parameter.

Then, you use the client's `voidLabel` method to void the label, and `cancelShipment` to cancel the shipment.

Refer to [this guide](https://docs.medusajs.com/references/fulfillment/provider/index.html.md) for a full reference of all methods, their parameters and return types.

### Export Module Definition

The `ShipStationProviderService` class now has the methods necessary to handle fulfillments.

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/shipstation/index.ts` with the following content:

![The directory structure of the Medusa application after adding the index file](https://res.cloudinary.com/dza7lstvk/image/upload/v1734350125/Medusa%20Resources/shipstation-dir-overview-5_zs6beg.jpg)

```ts title="src/modules/shipstation/index.ts"
import ShipStationProviderService from "./service"
import { 
  ModuleProvider, 
  Modules,
} from "@medusajs/framework/utils"

export default ModuleProvider(Modules.FULFILLMENT, {
  services: [ShipStationProviderService],
})
```

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 Fulfillment 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 title="medusa-config.ts"
module.exports = defineConfig({
  // ...
  modules: [
    {
      resolve: "@medusajs/medusa/fulfillment",
      options: {
        providers: [
          // default provider
          {
            resolve: "@medusajs/medusa/fulfillment-manual",
            id: "manual",
          },
          {
            resolve: "./src/modules/shipstation",
            id: "shipstation",
            options: {
              api_key: process.env.SHIPSTATION_API_KEY,
            },
          },
        ],
      },
    },
  ],
})
```

In the `modules` array, you pass a module object having the following properties:

- `resolve`: The NPM package of the Fulfillment Module. Since the ShipStation Module is a Fulfillment Module Provider, it'll be passed in the options of the Fulfillment 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.

The values of the ShipStation Module's options are set in environment variables. So, add the following environment variables to `.env`:

```shell
SHIPSTATION_API_KEY=123...
```

Where `SHIPSTATION_API_KEY` is the ShipStation API key, which you can retrieve on the ShipStation dashboard:

- Click on the cog icon in the navigation bar to go to Settings.

![The cog icon is at the top right of the navigation bar. It's the third icon from the right.](https://res.cloudinary.com/dza7lstvk/image/upload/v1734352047/Medusa%20Resources/Screenshot_2024-12-16_at_2.27.02_PM_nnmwzo.png)

- In the sidebar, expand Account and click on API Settings

![The sidebar has an Account expandable. When you click on it, more items will show. Click on the API Settings](https://res.cloudinary.com/dza7lstvk/image/upload/v1734352145/Medusa%20Resources/Screenshot_2024-12-16_at_2.28.32_PM_wwfc1s.png).

- On the API Settings page, make sure V2 API is selected for "Select API Verion" field, then click the "Generate API Key" button.

![Make sure V2 API is selected in the Select API Version dropdown, then click on the "Generate API Key" button.](https://res.cloudinary.com/dza7lstvk/image/upload/v1734352261/Medusa%20Resources/Screenshot_2024-12-16_at_2.30.31_PM_vbkz4i.png)

- Copy the generated API key and use it as the value of the `SHIPSTATION_API_KEY` environment variable.

***

## Step 4: Add Shipping Options for ShipStation

Now that you've integrated ShipStation, you need to create its shipping options so that customers can choose from them during checkout.

First, start the Medusa application:

```bash npm2yarn
npm run dev
```

Then:

1. Open the Medusa Admin at `http://localhost:9000/app` and log in.
2. Go to Settings -> Locations & Shipping

![After clicking on settings in the main dashboard, a new sidebar will be shown where you can click on Location & Shipping](https://res.cloudinary.com/dza7lstvk/image/upload/v1733923761/Medusa%20Resources/Screenshot_2024-12-11_at_2.41.25_PM_wjbq5f.png)

3. Each location has shipping options. So, either create a new location, or click on the "View details" link at the top-right of a location.

![A location's card has the "View details" link at the top-right.](https://res.cloudinary.com/dza7lstvk/image/upload/v1733923793/Medusa%20Resources/Screenshot_2024-12-11_at_2.41.50_PM_idglsu.png)

4. On the location's page and under the Fulfillment Providers section, click on the three-dots icon and choose Edit from the dropdown.

![The location's page has as a "Fulfillment Providers" section in the side column at the right. Click on the three-dots icon in that section and choose Edit from the dropdown](https://res.cloudinary.com/dza7lstvk/image/upload/v1733923832/Medusa%20Resources/Screenshot_2024-12-11_at_2.42.47_PM_rzbjbf.png)

5. A pop up will open with the list of all integrated fulfillment providers. Click the checkbox at the left of the "Shipstation" provider, then click Save.

![Choose the fulfillment provider from the list and click on the Save button](https://res.cloudinary.com/dza7lstvk/image/upload/v1734517015/Medusa%20Resources/Screenshot_2024-12-18_at_12.06.05_PM_nkmljy.png)

6. Under the Shipping section, click on the "Create option" link.

![The Create Option link is in the Shipping section next to shipping options](https://res.cloudinary.com/dza7lstvk/image/upload/v1734517185/Medusa%20Resources/Screenshot_2024-12-18_at_12.19.26_PM_o3yz4n.png)

7. In the form that opens:
   - Select Calculated for the price type.
   - Enter a name for the shipping option. This is the name that customers see in the storefront.
   - Choose a Shipping Profile.
   - Choose ShipStation for Fulfillment Provider
   - This will load in the "Fulfillment option" field the ShipStation provider's options, which are retrieved on the server from the provider's `getFulfillmentOptions` method. Once they're loaded, choose one of the options retrieved from ShipStation.
   - Click the Save button.

![Select Calculated for price type, and select the correct fulfillment provider and option](https://res.cloudinary.com/dza7lstvk/image/upload/v1734517536/Medusa%20Resources/Screenshot_2024-12-18_at_12.24.44_PM_yk7s3z.png)

You can create a shipping option for each fulfillment option.

Customers can now select this shipping option during checkout, and the fulfillment for their order will be processed by ShipStation.

***

## Test it Out: Place an Order and Fulfill It

To test out the integration, you'll place an order using the Next.js Starter Storefront you installed with the Medusa application. You'll then create a fulfillment for the order's items from the Medusa Admin dashboard.

### Place Order in Storefront

Open the terminal in the Next.js Starter Storefront's directory. It's a sibling directory of the Medusa application with the name `{project-name}-storefront`, where `{project-name}` is the name of the Medusa application's project.

Then, while the Medusa application is running, run the following command in the storefront's directory:

```bash npm2yarn
npm run dev
```

This will run the storefront at `http://localhost:8000`. Open it in your browser, then:

1. Click on Menu at the top left of the navigation bar, then choose Store.

![After you click on Menu, choose Store from the side menu.](https://res.cloudinary.com/dza7lstvk/image/upload/v1734518126/Medusa%20Resources/Screenshot_2024-12-18_at_12.35.10_PM_knk46m.png)

2. Click on a product and add it to the cart.

![On a product's page, choose and option then click "add to cart"](https://res.cloudinary.com/dza7lstvk/image/upload/v1734518298/Medusa%20Resources/Screenshot_2024-12-18_at_12.36.18_PM_lrqnsj.png)

3. Click on "Cart" at the top right to go to the cart's page.

![Click on Cart at the top right of the navigation bar.](https://res.cloudinary.com/dza7lstvk/image/upload/v1734518298/Medusa%20Resources/Screenshot_2024-12-18_at_12.36.59_PM_hmvpgb.png)

4. From the cart's page, click on "Go to checkout".

![The "Go to checkout" button is at the bottom right of the page.](https://res.cloudinary.com/dza7lstvk/image/upload/v1734518298/Medusa%20Resources/Screenshot_2024-12-18_at_12.37.52_PM_ma4dij.png)

5. Enter the customer address as a first step of the Checkout. Make sure that the country you choose is the same as the location that the fulfillment provider's options are available in.

If you're entering US-based address, make sure to enter the two-letter code for your state, as that's required by ShipStation.

6. In the Delivery step, you'll find the option you added for ShipStation. There will be a loading indicator while its price is fetched, and the price will be shown afterwards.

![Price is shown next to the ShipStation shipping option](https://res.cloudinary.com/dza7lstvk/image/upload/v1734528106/Medusa%20Resources/Screenshot_2024-12-18_at_3.19.14_PM_smrzae.png)

7. Click on the ShipStation option, then click Continue to Payment.
8. Finish the payment step, then click Place order in the Review section

You've now created an order that uses a shipping option from ShipStation.

### Fulfill Order in Admin

You'll now manage the order you've created in the admin to fulfill it:

1. Open the admin at `http://localhost:9000/app` and login.
2. You'll find on the Orders page the order you've created. Click on it to view its details.
3. On the order's details page, scroll down to the Unfulfilled Items section. Then, click on the three-dots icon at the top right of the section and choose "Fulfill items" from the dropdown.

![In the Unfulfilled Items section, click on the three dots, then Fulfill items from the dropdown](https://res.cloudinary.com/dza7lstvk/image/upload/v1734530779/Medusa%20Resources/Screenshot_2024-12-18_at_4.05.29_PM_z9lwsk.png)

4. In the form that opens, choose the Location to fulfill the item(s) from, then click Create Fulfillment.

![Choose from the dropdown the location to fulfill the items from, then click Create Fulfillment at the bottom right.](https://res.cloudinary.com/dza7lstvk/image/upload/v1734530888/Medusa%20Resources/Screenshot_2024-12-18_at_4.07.35_PM_f4h5o1.png)

5. The created fulfillment will be showing on the order's details page now.

![Created fulfillment with details](https://res.cloudinary.com/dza7lstvk/image/upload/v1734535715/Medusa%20Resources/Screenshot_2024-12-18_at_5.28.06_PM_tsuk4a.png)

You can also cancel the fulfillment by clicking on the three-dots icon, then choosing Cancel from the dropdown. This will void the label in ShipStation and cancel its shipment.

***

## Next Steps

You've now integrated Medusa with ShipStation. You can fulfill orders with many carriers and providers, all from a single integration and platform.

If you're new to Medusa, check out the [main documentation](https://docs.medusajs.com/docs/learn/index.html.md), 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](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/index.html.md).


# How to Build a Wishlist Plugin

In this guide, you'll learn how to build a wishlist [plugin](https://docs.medusajs.com/docs/learn/fundamentals/plugins/index.html.md) in Medusa.

When you install a Medusa application, you get a fully-fledged commerce platform with a Framework for customization. The Medusa application's commerce features are built around [Commerce Modules](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/index.html.md) which are available out-of-the-box.

Customers browsing your store may be interested in a product but not ready to buy it yet. They may want to save the product for later or share it with friends and family. A wishlist feature allows customers to save products they like and access them later.

This guide will teach you how to:

- Install and set up a Medusa application project.
- Install and set up a Medusa plugin.
- Implement the wishlist features in the plugin.
  - Features include allowing customers to add products to a wishlist, view and manage their wishlist, and share their wishlist.
- Test and use the wishlist plugin in your Medusa application.

You can follow this guide whether you're new to Medusa or an advanced Medusa developer.

- [Wishlist Plugin Example Repository](https://github.com/medusajs/examples/tree/main/wishlist-plugin): Find the full code for this guide in, with the plugin to install.
- [OpenApi Specs for Postman](https://res.cloudinary.com/dza7lstvk/raw/upload/v1737459635/OpenApi/Wishlist_Postman_gjk7mn.yml): Import this OpenApi Specs file into tools like Postman.

***

## Step 1: Install a Medusa Application

You'll first install a Medusa application that exposes core commerce features through REST APIs. You'll later install the wishlist plugin in this application to test it out.

### Prerequisites

- [Node.js v20+](https://nodejs.org/en/download)
- [Git CLI tool](https://git-scm.com/downloads)
- [PostgreSQL](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 be asked for the project's name. You can also optionally choose to install the [Next.js Starter Storefront](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/nextjs-starter/index.html.md).

Afterward, the installation process will start, which will install the Medusa application in a directory with your project's name. If you chose to install the Next.js starter, it'll be installed in a separate directory with the `{project-name}-storefront` name.

The Medusa application is composed of a headless Node.js server and an admin dashboard. The storefront is installed or custom-built separately and connects to the Medusa application through its REST endpoints, called [API routes](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/index.html.md). Learn more about Medusa's architecture in [this documentation](https://docs.medusajs.com/docs/learn/introduction/architecture/index.html.md).

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. Afterward, you can log in with the new user and explore the dashboard.

Check out the [troubleshooting guides](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/troubleshooting/create-medusa-app-errors/index.html.md) for help.

***

## Step 2: Install a Medusa Plugin Project

A plugin is a package of reusable Medusa customizations that you can install in any Medusa application. You can add in the plugin [API Routes](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/index.html.md), [Workflows](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md), and other customizations, as you'll see in this guide. Afterward, you can test it out locally in a Medusa application, then publish it to npm to install and use it in any Medusa application.

Learn more about plugins in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/plugins/index.html.md).

A Medusa plugin is set up in a different project, giving you the flexibility in building and publishing it, while providing you with the tools to test it out locally in a Medusa application.

To create a new Medusa plugin project, run the following command in a directory different than that of the Medusa application:

```bash npm2yarn
npx create-medusa-app@latest medusa-plugin-wishlist --plugin
```

Where `medusa-plugin-wishlist` is the name of the plugin's directory and the name set in the plugin's `package.json`. So, if you wish to publish it to NPM later under a different name, you can change it here in the command or later in `package.json`.

Once the installation process is done, a new directory named `medusa-plugin-wishlist` will be created with the plugin project files.

![Directory structure of a plugin project](https://res.cloudinary.com/dza7lstvk/image/upload/v1737019441/Medusa%20Book/project-dir_q4xtri.jpg)

***

## Step 3: Set up Plugin in Medusa Application

Before you start your development, you'll set up the plugin in the Medusa application you installed in the first step. This will allow you to test the plugin during your development process.

In the plugin's directory, run the following command to publish the plugin to the local package registry:

```bash title="Plugin project"
npx medusa plugin:publish
```

This command uses [Yalc](https://github.com/wclr/yalc) under the hood to publish the plugin to a local package registry. The plugin is published locally under the name you specified in `package.json`.

Next, you'll install the plugin in the Medusa application from the local registry.

If you've installed your Medusa project before v2.3.1, you must install [yalc](https://github.com/wclr/yalc) as a development dependency first.

Run the following command in the Medusa application's directory to install the plugin:

```bash title="Medusa application"
npx medusa plugin:add medusa-plugin-wishlist
```

This command installs the plugin in the Medusa application from the local package registry.

Next, register the plugin in the `medusa-config.ts` file of the Medusa application:

```ts title="medusa-config.ts"
module.exports = defineConfig({
  // ...
  plugins: [
    {
      resolve: "medusa-plugin-wishlist",
      options: {},
    },
  ],
})
```

Finally, to ensure your plugin's changes are constantly published to the local registry, simplifying your testing process, keep the following command running in the plugin project during development:

```bash title="Plugin project"
npx medusa plugin:develop
```

***

## Step 4: Implement Wishlist Module

To add custom tables to the database, which are called data models, you create a module. A module is a 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.

While you can create modules outside of a plugin and install them in the Medusa application, plugins allow you to bundle modules with other customizations, such as API routes and workflows.

In this step, you'll create a Wishlist Module within the wishlist plugin. This module adds custom data models for wishlists and their items, which you'll use in later steps to store a customer's wishlist.

Learn more about modules in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/modules/index.html.md).

### Create Module Directory

A module is created under the `src/modules` directory of your plugin. So, create the directory `src/modules/wishlist`.

![Diagram showcasing the module directory to create](https://res.cloudinary.com/dza7lstvk/image/upload/v1737461182/Medusa%20Resources/wishlist-1_z3kzfv.jpg)

### Create Data Models

A data model represents a table in the database. You create data models using Medusa's Data Model Language (DML). It simplifies defining a table's columns, relations, and indexes with straightforward methods and configurations.

Learn more about data models in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/modules#1-create-data-model/index.html.md).

In the Wishlist Module, you'll create two data models: `Wishlist` and `WishlistItem`. The `Wishlist` model represents a customer's wishlist, while the `WishlistItem` model represents a product in the wishlist.

Starting with the `Wishlist` model, create a file `src/modules/wishlist/models/wishlist.ts` with the following content:

![Directory structure after adding the Wishlist model](https://res.cloudinary.com/dza7lstvk/image/upload/v1737461304/Medusa%20Resources/wishlist-2_co2lht.jpg)

```ts title="src/modules/wishlist/models/wishlist.ts"
import { model } from "@medusajs/framework/utils"
import { WishlistItem } from "./wishlist-item"

export const Wishlist = model.define("wishlist", {
  id: model.id().primaryKey(),
  customer_id: model.text(),
  sales_channel_id: model.text(),
  items: model.hasMany(() => WishlistItem),
})
.indexes([
  {
    on: ["customer_id", "sales_channel_id"],
    unique: true,
  },
])
```

The `Wishlist` model has the following properties:

- `id`: A unique identifier for the wishlist.
- `customer_id`: The ID of the customer who owns the wishlist.
- `sales_channel_id`: The ID of the sales channel where the wishlist is created. In Medusa, product availability can differ between sales channels. This ensures only products available in the customer's sales channel are added to the wishlist.
- `items`: A relation to the `WishlistItem` model, representing the products in the wishlist. You'll add this data model next.

Learn more about data model [properties](https://docs.medusajs.com/docs/learn/fundamentals/data-models/properties/index.html.md) and [relations](https://docs.medusajs.com/docs/learn/fundamentals/data-models/relationships/index.html.md).

You also define a unique index on the `customer_id` and `sales_channel_id` columns to ensure a customer can only have one wishlist per sales channel.

Learn more about data model indexes in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/data-models/index/index.html.md).

Next, create the `WishlistItem` model in the file `src/modules/wishlist/models/wishlist-item.ts`:

![Directory structure after adding the WishlistItem model](https://res.cloudinary.com/dza7lstvk/image/upload/v1737461521/Medusa%20Resources/wishlist-3_fxcjxy.jpg)

```ts title="src/modules/wishlist/models/wishlist-item.ts"
import { model } from "@medusajs/framework/utils"
import { Wishlist } from "./wishlist"

export const WishlistItem = model.define("wishlist_item", {
  id: model.id().primaryKey(),
  product_variant_id: model.text(),
  wishlist: model.belongsTo(() => Wishlist, {
    mappedBy: "items",
  }),
})
.indexes([
  {
    on: ["product_variant_id", "wishlist_id"],
    unique: true,
  },
])
```

The `WishlistItem` model has the following properties:

- `id`: A unique identifier for the wishlist item.
- `product_variant_id`: The ID of the product variant in the wishlist.
- `wishlist`: A relation to the `Wishlist` model, representing the wishlist the item belongs to.

You also define a unique index on the `product_variant_id` and `wishlist_id` columns to ensure a product variant is added to the wishlist only once. The `wishlist_id` column is available as a by-product of the `belongsTo` relation.

### Create Service

You define data-management methods of your data models in a service. A service is a TypeScript or JavaScript class that the module exports. In the service's methods, you can perform database operations.

Learn more about services in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/modules#2-create-service/index.html.md).

In this section, you'll create the Wishlist Module's service that's used to manage wishlists and wishlist items. Create the file `src/modules/wishlist/service.ts` with the following content:

![Directory structure after adding the service file](https://res.cloudinary.com/dza7lstvk/image/upload/v1737461698/Medusa%20Resources/wishlist-4_j5ka26.jpg)

```ts title="src/modules/wishlist/service.ts"
import { MedusaService } from "@medusajs/framework/utils"
import { Wishlist } from "./models/wishlist"
import { WishlistItem } from "./models/wishlist-item"

export default class WishlistModuleService extends MedusaService({
  Wishlist,
  WishlistItem,
}) {}
```

The `WishlistModuleService` extends `MedusaService` from the Modules SDK which generates a class with data-management methods for your module's data models. This saves you time on implementing Create, Read, Update, and Delete (CRUD) methods.

So, the `WishlistModuleService` class now has methods like `createWishlists` and `retrieveWishlist`.

Find all methods generated by the `MedusaService` in [this reference](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/service-factory-reference/index.html.md).

You'll use this service in a later method to store and manage wishlists and wishlist items in other customizations.

### Export Module Definition

The final piece to a module is its definition, which you export in an `index.ts` file at its root directory. This definition tells Medusa the name of the module and its service.

So, create the file `src/modules/wishlist/index.ts` with the following content:

![Directory structure after adding the module definition file](https://res.cloudinary.com/dza7lstvk/image/upload/v1737461829/Medusa%20Resources/wishlist-5_mb4tjf.jpg)

```ts title="src/modules/wishlist/index.ts"
import WishlistModuleService from "./service"
import { Module } from "@medusajs/framework/utils"

export const WISHLIST_MODULE = "wishlist"

export default Module(WISHLIST_MODULE, {
  service: WishlistModuleService,
})
```

You use the `Module` function from the Modules SDK to create the module's definition. It accepts two parameters:

1. The module's name, which is `wishlist`.
2. An object with a required property `service` indicating the module's service.

You'll later use the module's service to manage wishlists and wishlist items in other customizations.

### Generate Migrations

Since data models represent tables in the database, you define how they're created in the database with migrations. A migration is a TypeScript or JavaScript file that defines database changes made by a module.

Learn more about migrations in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/modules#5-generate-migrations/index.html.md).

Medusa's CLI tool generates the migrations for you. To generate a migration for the Wishlist Module, run the following command in the plugin project:

```bash title="Plugin project"
npx medusa plugin:db:generate
```

You'll now have a `migrations` directory under `src/modules/wishlist` that holds the generated migration.

Then, to reflect these migrations on the database of the Medusa application using this module, run the following command:

Make sure that `npx medusa plugin:develop` is running in the plugin project to publish the changes to the local registry.

```bash title="Medusa application"
npx medusa db:migrate
```

The tables of the Wishlist Module's data models are now created in the database.

***

## Step 5: Link Wishlist Data Models with Core Models

The Wishlist Module's data models store IDs of records in data models implemented in Medusa's core Commerce Modules, such as the ID of a customer or a product variant.

However, modules are [isolated](https://docs.medusajs.com/docs/learn/fundamentals/modules/isolation/index.html.md) to ensure they're re-usable and don't have side effects when integrated into the Medusa application. So, to build associations between modules, you define [module links](https://docs.medusajs.com/docs/learn/fundamentals/module-links/index.html.md). A Module link associates two modules' data models while maintaining module isolation.

In this section, you'll link the `Wishlist` data model to the [Customer Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/customer/index.html.md)'s `Customer` data model, and to the [Sales Channel](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/sales-channel/index.html.md) Module's `SalesChannel` data model. You'll also link the `WishlistItem` data model to the [Product Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/product/index.html.md)'s `ProductVariant` data model.

Learn more about module links in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/module-links/index.html.md).

To create the link between the `Wishlist` data model and the `Customer` data model, create the file `src/modules/wishlist/links/wishlist-customer.ts` with the following content:

![Directory structure after adding the link file](https://res.cloudinary.com/dza7lstvk/image/upload/v1737462649/Medusa%20Resources/wishlist-6_xqytog.jpg)

```ts title="src/modules/wishlist/links/wishlist-customer.ts"
import { defineLink } from "@medusajs/framework/utils"
import WishlistModule from "../modules/wishlist"
import CustomerModule from "@medusajs/medusa/customer"

export default defineLink(
  {
    linkable: WishlistModule.linkable.wishlist.id,
    field: "customer_id",
  },
  CustomerModule.linkable.customer.id,
  {
    readOnly: true,
  }
)
```

You define a link using `defineLink` from the Modules SDK. It accepts three parameters:

1. The first data model part of the link, which is the Wishlist Module's `wishlist` data model. A module has a special `linkable` property that contain link configurations for its data models. You also specify the field that points to the customer.
2. The second data model part of the link, which is the Customer Module's `customer` data model.
3. An object of configurations for the module link. By default, Medusa creates a table in the database to represent the link you define. However, in this guide, you only want this link to retrieve the customer associated with a wishlist. So, you enable `readOnly` telling Medusa not to create a table for this link.

Next, to create the link between the `Wishlist` data model and the `SalesChannel` data model, create the file `src/modules/wishlist/links/wishlist-sales-channel.ts` with the following content:

![Directory structure after adding the link file](https://res.cloudinary.com/dza7lstvk/image/upload/v1737462829/Medusa%20Resources/wishlist-7_ddiwxy.jpg)

```ts title="src/modules/wishlist/links/wishlist-sales-channel.ts"
import { defineLink } from "@medusajs/framework/utils"
import WishlistModule from "../modules/wishlist"
import SalesChannelModule from "@medusajs/medusa/sales-channel"

export default defineLink(
  {
    linkable: WishlistModule.linkable.wishlist.id,
    field: "sales_channel_id",
  },
  SalesChannelModule.linkable.salesChannel,
  {
    readOnly: true,
  }
)
```

You define a link between the `Wishlist` data model and the `SalesChannel` data model in the same way as the previous link.

Finally, to create the link between the `WishlistItem` data model and the `ProductVariant` data model, create the file `src/modules/wishlist/links/wishlist-product.ts` with the following content:

![Directory structure after adding the link file](https://res.cloudinary.com/dza7lstvk/image/upload/v1737467010/Medusa%20Resources/wishlist-8_hgoaby.jpg)

```ts title="src/modules/wishlist/links/wishlist-product.ts"
import { defineLink } from "@medusajs/framework/utils"
import WishlistModule from "../modules/wishlist"
import ProductModule from "@medusajs/medusa/product"

export default defineLink(
  {
    linkable: WishlistModule.linkable.wishlistItem.id,
    field: "product_variant_id",
  },
  ProductModule.linkable.productVariant,
  {
    readOnly: true,
  }
)
```

You define a link between the `WishlistItem` data model and the `ProductVariant` data model in the same way as the previous links.

In the next steps, you'll see how these links allow you to retrieve the resources associated with a wishlist or wishlist item.

***

## Step 6: Create Wishlist Workflow

The first feature you'll add to the wishlist plugin is the ability to create a wishlist for a customer. You'll implement this feature in 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 an endpoint.

In this section, you'll create a workflow that creates a wishlist for a customer. Later, you'll execute this workflow from an API route.

Learn more about workflows in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md)

The workflow has the following steps:

- [validateCustomerCreateWishlistStep](#validateCustomerCreateWishlistStep): Validate that the customer doesn't have an existing wishlist.
- [createWishlistStep](#createWishlistStep): Create a wishlist for the customer.

You'll implement the steps before implementing the workflow.

### validateCustomerCreateWishlistStep

The first step in the workflow will validate that a customer doesn't have an existing workflow. If not valid, the step will throw an error, stopping the workflow's execution.

To create the step, create the file `src/workflows/steps/validate-customer-create-wishlist.ts` with the following content:

![Directory structure after adding the step file](https://res.cloudinary.com/dza7lstvk/image/upload/v1737467678/Medusa%20Resources/wishlist-9_lzlifn.jpg)

```ts title="src/workflows/steps/validate-customer-create-wishlist.ts" highlights={validateCustomerWishlistHighlights}
import { MedusaError } from "@medusajs/framework/utils"
import { createStep } from "@medusajs/framework/workflows-sdk"

type ValidateCustomerCreateWishlistStepInput = {
  customer_id: string
}

export const validateCustomerCreateWishlistStep = createStep(
  "validate-customer-create-wishlist",
  async ({ customer_id }: ValidateCustomerCreateWishlistStepInput, { container }) => {
    const query = container.resolve("query")

    const { data } = await query.graph({
      entity: "wishlist",
      fields: ["*"],
      filters: {
        customer_id: customer_id,
      },
    })

    if (data.length) {
      throw new MedusaError(
        MedusaError.Types.NOT_FOUND,
        "Customer already has a wishlist"
      )
    }

    // check that customer exists
    const { data: customers } = await query.graph({
      entity: "customer",
      fields: ["*"],
      filters: {
        id: customer_id,
      },
    })

    if (customers.length === 0) {
      throw new MedusaError(
        MedusaError.Types.INVALID_DATA,
        "Specified customer was not found"
      )
    }
  }
)
```

You create a step using `createStep` from the Workflows SDK. It accepts two parameters:

1. The step's name, which is `validate-customer-create-wishlist`.
2. An async function that executes the step's logic. The function receives two parameters:
   - The input data for the step, which in this case is an object having a `customer_id` property.
   - An object holding the workflow's context, including the [Medusa Container](https://docs.medusajs.com/docs/learn/fundamentals/medusa-container/index.html.md) that allows you to resolve Framework and commerce tools.

In the step function, you use [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md) to retrieve the wishlist based on the specified customer ID. If a wishlist exists, you throw an error, stopping the workflow's execution.

You also try to retrieve the customer, and if they don't exist, you throw an error.

### createWishlistStep

The second step in the workflow will create a wishlist for the customer. To create the step, create the file `src/workflows/steps/create-wishlist.ts` with the following content:

![Directory structure after adding the step file](https://res.cloudinary.com/dza7lstvk/image/upload/v1737467998/Medusa%20Resources/wishlist-10_xex4d0.jpg)

```ts title="src/workflows/steps/create-wishlist.ts" highlights={createWishlistStepHiglights}
import { createStep, StepResponse } from "@medusajs/framework/workflows-sdk"
import { WISHLIST_MODULE } from "../../modules/wishlist"
import WishlistModuleService from "../../modules/wishlist/service"

type CreateWishlistStepInput = {
  customer_id: string
  sales_channel_id: string
}

export const createWishlistStep = createStep(
  "create-wishlist",
  async (input: CreateWishlistStepInput, { container }) => {
    const wishlistModuleService: WishlistModuleService = 
      container.resolve(WISHLIST_MODULE)

    const wishlist = await wishlistModuleService.createWishlists(input)

    return new StepResponse(wishlist, wishlist.id)
  },
  async (id, { container }) => {
    if (!id) {
      return
    }
    const wishlistModuleService: WishlistModuleService = 
      container.resolve(WISHLIST_MODULE)

    await wishlistModuleService.deleteWishlists(id)
  }
)
```

This step accepts the IDs of the customer and the sales channel as input. In the step function, you resolve the Wishlist Module's service from the container and use its generated `createWishlists` method to create the wishlist, passing it the input as a parameter.

Steps that return data must return them in a `StepResponse` instance. The `StepResponse` constructor accepts two parameters:

- The data to return, which in this case is the created wishlist.
- The data to pass to the compensation function, which in this case is the wishlist's ID.

The compensation function is an optional third parameter of `createStep`. It defines rollback logic that's executed when an error occurs during the workflow's execution. In the compensation function, you undo the actions you performed in the step function.

The compensation function accepts as a first parameter the data passed as a second parameter to the `StepResponse` returned by the step function, which in this case is the wishlist's ID. In the compensation function, you resolve the Wishlist Module's service from the container and use its generated `deleteWishlists` method to delete the wishlist.

Learn more about the generated [create](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/service-factory-reference/methods/create/index.html.md) and [delete](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/service-factory-reference/methods/delete/index.html.md) methods.

### Add createWishlistWorkflow

You can now add the `createWishlistWorkflow` to the plugin. Create the file `src/workflows/create-wishlist.ts` with the following content:

![Directory structure after adding the workflow file](https://res.cloudinary.com/dza7lstvk/image/upload/v1737468333/Medusa%20Resources/wishlist-11_absftb.jpg)

```ts title="src/workflows/create-wishlist.ts" highlights={createWishlistWorkflowHighlights}
import { createWorkflow, WorkflowResponse } from "@medusajs/framework/workflows-sdk"
import { validateCustomerCreateWishlistStep } from "./steps/validate-customer-create-wishlist"
import { createWishlistStep } from "./steps/create-wishlist"

type CreateWishlistWorkflowInput = {
  customer_id: string
  sales_channel_id: string
}

export const createWishlistWorkflow = createWorkflow(
  "create-wishlist",
  (input: CreateWishlistWorkflowInput) => {
    validateCustomerCreateWishlistStep({
      customer_id: input.customer_id,
    })

    const wishlist = createWishlistStep(input)

    return new WorkflowResponse({
      wishlist,
    })
  }
)
```

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. In the workflow, you:

- Execute the `validateCustomerCreateWishlistStep` step to validate that the customer doesn't have an existing wishlist.
- Execute the `createWishlistStep` step to create the wishlist.

A workflow's constructor function has some constraints in implementation, which is why you need to use `transform` for variable manipulation. Learn more about these constraints in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/workflows/constructor-constraints/index.html.md).

Workflows must return an instance of `WorkflowResponse`, passing as a parameter the data to return to the workflow's executor. The workflow returns an object having a `wishlist` property, which is the created wishlist.

You'll execute this workflow in an API route in the next step.

***

## Step 7: Create Wishlist API Route

Now that you implemented the flow to create a wishlist for a customer, you'll create an API route that exposes this functionality.

An API Route is an endpoint that exposes commerce features to external applications and clients, such as storefronts. You'll create a `POST` API route at the path `/store/customers/me/wishlists` that executes the workflow from the previous step.

Learn more about API routes in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/index.html.md).

An API route is created in a `route.ts` file under a sub-directory of the `src/api` directory. The path of the API route is the file's path relative to `src/api`.

So, to create the `/store/customers/me/wishlists` API route, create the file `src/api/store/customers/me/wishlists/route.ts` with the following content:

![Directory structure after adding the route file](https://res.cloudinary.com/dza7lstvk/image/upload/v1737468859/Medusa%20Resources/wishlist-12_gvvb9z.jpg)

```ts title="src/api/store/customers/me/wishlists/route.ts"
import { AuthenticatedMedusaRequest, MedusaResponse } from "@medusajs/framework/http"
import { createWishlistWorkflow } from "../../../../../workflows/create-wishlist"
import { MedusaError } from "@medusajs/framework/utils"

export async function POST(
  req: AuthenticatedMedusaRequest,
  res: MedusaResponse
) {
  if (!req.publishable_key_context?.sales_channel_ids.length) {
    throw new MedusaError(
      MedusaError.Types.INVALID_DATA,
      "At least one sales channel ID is required to be associated with the publishable API key in the request header."
    )
  }
  const { result } = await createWishlistWorkflow(req.scope)
    .run({
      input: {
        customer_id: req.auth_context.actor_id,
        sales_channel_id: req.publishable_key_context?.sales_channel_ids[0],
      },
    })

  res.json({
    wishlist: result.wishlist,
  })
}
```

Since you export a `POST` function in this file, you're exposing a `POST` API route at `/store/customers/me/wishlists`. The route handler function accepts two parameters:

1. A request object with details and context about the request, such as authenticated customer details.
2. A response object to manipulate and send the response.

API routes implemented under the `/store` path require passing a publishable API key in the header of the request. The publishable API key is created by an admin user and is associated with one or more sales channels. In the route handler function, you validate that the request has at least one sales channel ID associated with the publishable API key. You'll use that sales channel ID with the wishlist you're creating.

Also, API routes implemented under the `/store/customers/me` path are only accessible by authenticated customers. You access the ID of the authenticated customer using the `auth_context.actor_id` property of the request object.

In the route handler function, you execute the `createWishlistWorkflow`, passing the authenticated customer ID and the sales channel ID as input. The workflow returns an object having a `result` property, which is the data returned by the workflow. You return the created wishlist in the response.

### Test API Route

You'll now test that this API route defined in the plugin is working as expected using the Medusa application you installed in the first step.

Make sure that `npx medusa plugin:develop` is running in the plugin project to publish the changes to the local registry.

In the Medusa application's directory, run the following command to start the development server:

```bash npm2yarn
npm run dev
```

### Retrieve Publishable API Key

Before sending the request, you need to obtain a publishable API key. So, open the Medusa Admin at `http://localhost:9000/app` and log in with the user you created earlier.

To access your application's API keys in the admin, go to Settings -> Publishable API Keys. You'll have an API key created by default, which is associated with the default sales channel. You can use this publishable API key in the request header.

![In the admin, click on Publishable API key in the sidebar. A table will show your API keys and allow you to create one.](https://res.cloudinary.com/dza7lstvk/image/upload/v1733230421/Medusa%20Resources/Screenshot_2024-12-03_at_2.53.07_PM_gau9jy.png)

### Retrieve Authenticated Customer Token

Then, you need an authentication token of a registered customer. To create a customer, first, send the following request to the Medusa application:

```bash
curl -X POST 'http://localhost:9000/auth/customer/emailpass/register' \
--header 'Content-Type: application/json' \
--data-raw '{
    "email": "customer@gmail.com",
    "password": "supersecret"
}'
```

This API route obtains a registration token for the specified email and password in the request body.

Next, use that token to register the customer:

```bash
curl -X POST 'http://localhost:9000/store/customers' \
--header 'Content-Type: application/json' \
-H 'x-publishable-api-key: {api_key}' \
--header 'Authorization Bearer {token}' \
--data-raw '{
    "email": "customer@gmail.com"
}'
```

Make sure to replace `{api_key}` with the publishable API key you copied from the settings, and `{token}` with the token received from the previous request.

This will create a customer. You can now obtain the customer's authentication token by sending the following request:

```bash
curl -X POST 'http://localhost:9000/auth/customer/emailpass' \
--header 'Content-Type: application/json' \
--data-raw '{
    "email": "customer@gmail.com",
    "password": "supersecret"
}'
```

This API route will return an authentication token for the customer. You'll use this token in the header of the following requests.

### Send Request to Create Wishlist

Finally, send a `POST` request to the `/store/customers/me/wishlists` API route to create a wishlist for the authenticated customer:

```bash
curl -X POST 'localhost:9000/store/customers/me/wishlists' \
--header 'x-publishable-api-key: {api_key}' \
--header 'Authorization: Bearer {token}'
```

Make sure to replace `{api_key}` with the publishable API key you copied from the settings, and `{token}` with the authenticated customer token.

You'll receive in the response the created wishlist.

***

## Step 8: Retrieve Wishlist API Route

In this step, you'll add an API route to retrieve a customer's wishlist. You'll create a `GET` API route at the path `/store/customers/me/wishlists` that retrieves the wishlist of the authenticated customer.

So, add to the `src/api/store/customers/me/wishlists/route.ts` the following:

```ts title="src/api/store/customers/me/wishlists/route.ts"
export async function GET(
  req: AuthenticatedMedusaRequest,
  res: MedusaResponse
) {
  const query = req.scope.resolve("query")

  const { data } = await query.graph({
    entity: "wishlist",
    fields: ["*", "items.*", "items.product_variant.*"],
    filters: {
      customer_id: req.auth_context.actor_id,
    },
  })

  if (!data.length) {
    throw new MedusaError(
      MedusaError.Types.NOT_FOUND,
      "No wishlist found for customer"
    )
  }

  return res.json({
    wishlist: data[0],
  })
}
```

In this route handler function, you use [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md) to retrieve the wishlist of the authenticated customer. For each wishlist, you retrieve its items, and the product variants of those items.

If the wishlist doesn't exist, you throw an error. Otherwise, you return the wishlist in the response.

### Test Retrieve Wishlist API Route

To test the API route, start the Medusa application.

Make sure that `npx medusa plugin:develop` is running in the plugin project to publish the changes to the local registry.

Then, send a `GET` request to the `/store/customers/me/wishlists` API route:

```bash
curl 'localhost:9000/store/customers/me/wishlists' \
--header 'x-publishable-api-key: {api_key}' \
--header 'Authorization: Bearer {token}'
```

Make sure to replace:

- `{api_key}` with the publishable API key you copied from the settings, as explained in the [previous step](#retrieve-publishable-api-key).
- `{token}` with the authenticated customer token you received from the [previous step](#retrieve-authenticated-customer-token).

You'll receive in the response the wishlist of the authenticated customer.

***

## Step 9: Add Item to Wishlist API Route

Next, you'll add the functionality to add an item to a wishlist. You'll first define a workflow that implements this functionality, then create an API route that executes the workflow.

### Add Item to Wishlist Workflow

The workflow to add an item to a wishlist has the following steps:

- [useQueryGraphStep](https://docs.medusajs.com/references/helper-steps/useQueryGraphStep/index.html.md): Retrieve the wishlist of a customer.
- [validateWishlistExistsStep](#validateWishlistExistsStep): Validate that the customer's wishlist exists.
- [validateWishlistSalesChannelStep](#validateWishlistSalesChannelStep): Validate that the wishlist belongs to the specified sales channel.
- [validateVariantWishlistStep](#validateVariantWishlistStep): Validate that the specified variant is not already in the wishlist.
- [createWishlistItemStep](#createWishlistItemStep): Create the wishlist item.
- [useQueryGraphStep](https://docs.medusajs.com/references/helper-steps/useQueryGraphStep/index.html.md): Retrieve the wishlist again with the new item added.

The `useQueryGraphStep` is from Medusa's workflows package. So, you'll only implement the other steps.

#### validateWishlistExistsStep

The second step in the workflow validates that the customer's wishlist, retrieved in the first step, exists.

To create the step, create the file `src/workflows/steps/validate-wishlist-exists.ts` with the following content:

![Directory structure after adding the step file](https://res.cloudinary.com/dza7lstvk/image/upload/v1740071251/Medusa%20Resources/wishlist-29_bq6kcn.jpg)

```ts title="src/workflows/steps/validate-wishlist-exists.ts"
import { MedusaError } from "@medusajs/framework/utils"
import { createStep } from "@medusajs/framework/workflows-sdk"
import { InferTypeOf } from "@medusajs/framework/types"
import { Wishlist } from "../../modules/wishlist/models/wishlist"

type Input = {
  wishlists?: InferTypeOf<typeof Wishlist>[]
}

export const validateWishlistExistsStep = createStep(
  "validate-wishlist-exists",
  async (input: Input) => {
    if (!input.wishlists?.length) {
      throw new MedusaError(
        MedusaError.Types.NOT_FOUND,
        "No wishlist found for this customer"
      )
    }
  }
)
```

This step receives an array of wishlists and throws an error if it's empty. You'll use this to stop the workflow's execution if the customer doesn't have a wishlist.

#### validateWishlistSalesChannelStep

The third step in the workflow validates that the wishlist belongs to the sales channel specified in the input.

To create the step, create the file `src/workflows/steps/validate-wishlist-sales-channel.ts` with the following content:

![Directory structure after adding the step file](https://res.cloudinary.com/dza7lstvk/image/upload/v1737470093/Medusa%20Resources/wishlist-13_nn924e.jpg)

```ts title="src/workflows/steps/validate-wishlist-sales-channel.ts"
import { createStep } from "@medusajs/framework/workflows-sdk"
import { InferTypeOf } from "@medusajs/framework/types"
import { Wishlist } from "../../modules/wishlist/models/wishlist"

type ValidateWishlistSalesChannelStepInput = {
  wishlist: InferTypeOf<typeof Wishlist>
  sales_channel_id: string
}

export const validateWishlistSalesChannelStep = createStep(
  "validate-wishlist-sales-channel",
  async (input: ValidateWishlistSalesChannelStepInput, { container }) => {
    const { wishlist, sales_channel_id } = input

    if (wishlist.sales_channel_id !== sales_channel_id) {
      throw new Error("Wishlist does not belong to the current sales channel")
    }
  }
)
```

This step receives the wishlist object and the sales channel ID as input. In the step function, if the wishlist's sales channel ID doesn't match the sales channel ID in the input, you throw an error.

To represent a data model in a type, use the [InferTypeOf](https://docs.medusajs.com/docs/learn/fundamentals/data-models/infer-type/index.html.md) utility.

#### validateVariantWishlistStep

The next step in the workflow validates that the specified variant is not already in the wishlist.

Create the file `src/workflows/steps/validate-variant-wishlist.ts` with the following content:

![Directory structure after adding the step file](https://res.cloudinary.com/dza7lstvk/image/upload/v1737470156/Medusa%20Resources/wishlist-14_ckoesz.jpg)

```ts title="src/workflows/steps/validate-variant-wishlist.ts" highlights={validateVariantWishlistHighlights}
import { InferTypeOf } from "@medusajs/framework/types"
import { Wishlist } from "../../modules/wishlist/models/wishlist"
import { createStep } from "@medusajs/framework/workflows-sdk"
import { MedusaError } from "@medusajs/framework/utils"

type ValidateVariantWishlistStepInput = {
  variant_id: string
  sales_channel_id: string
  wishlist: InferTypeOf<typeof Wishlist>
}

export const validateVariantWishlistStep = createStep(
  "validate-variant-in-wishlist",
  async ({ 
    variant_id, 
    sales_channel_id,
    wishlist,
  }: ValidateVariantWishlistStepInput, { container }) => {
    // validate whether variant is in wishlist
    const isInWishlist = wishlist.items?.some(
      (item) => item.product_variant_id === variant_id
    )

    if (isInWishlist) {
      throw new MedusaError(
        MedusaError.Types.INVALID_DATA,
        "Variant is already in wishlist"
      )
    }

    // validate that the variant is available in the specified sales channel
    const query = container.resolve("query")
    const { data } = await query.graph({
      entity: "variant",
      fields: ["product.sales_channels.*"],
      filters: {
        id: variant_id,
      },
    })

    const variantInSalesChannel = data[0].product.sales_channels.some(
      (sc) => sc.id === sales_channel_id
    )

    if (!variantInSalesChannel) {
      throw new MedusaError(
        MedusaError.Types.INVALID_DATA,
        "Variant is not available in the specified sales channel"
      )
    }
  }
)
```

This step receives the variant ID, sales channel ID, and wishlist object as input. In the step function, you throw an error if:

- The variant is already in the wishlist.
- The variant is not available in the specified sales channel. You use Query to retrieve the sales channels that the variant's product is available in.

#### createWishlistItemStep

The fifth step in the workflow creates a wishlist item for the specified variant in the wishlist.

Create the file `src/workflows/steps/create-wishlist-item.ts` with the following content:

![Directory structure after adding the step file](https://res.cloudinary.com/dza7lstvk/image/upload/v1737470302/Medusa%20Resources/wishlist-15_oc696x.jpg)

```ts title="src/workflows/steps/create-wishlist-item.ts" highlights={createWishlistItemStepHighlights}
import { createStep, StepResponse } from "@medusajs/framework/workflows-sdk"
import WishlistModuleService from "../../modules/wishlist/service"
import { WISHLIST_MODULE } from "../../modules/wishlist"

type CreateWishlistItemStepInput = {
  wishlist_id: string
  product_variant_id: string
}

export const createWishlistItemStep = createStep(
  "create-wishlist-item",
  async (input: CreateWishlistItemStepInput, { container }) => {
    const wishlistModuleService: WishlistModuleService = 
      container.resolve(WISHLIST_MODULE)

    const item = await wishlistModuleService.createWishlistItems(input)

    return new StepResponse(item, item.id)
  },
  async (id, { container }) => {
    if (!id) {
      return
    }
    const wishlistModuleService: WishlistModuleService = 
      container.resolve(WISHLIST_MODULE)

    await wishlistModuleService.deleteWishlistItems(id)
  }
)
```

This step receives the wishlist ID and the variant ID as input. In the step function, you resolve the Wishlist Module's service from the container and use its generated `createWishlistItems` method to create the wishlist item, passing it the input as a parameter.

You return the created wishlist item and pass the item's ID to the compensation function. In the compensation function, you resolve the Wishlist Module's service from the container and use its generated `deleteWishlistItems` method to delete the wishlist item if an error occurs in the workflow.

#### Add Item to Wishlist Workflow

You can now add the `createWishlistItemWorkflow` to the plugin. Create the file `src/workflows/create-wishlist-item.ts` with the following content:

![Directory structure after adding the workflow file](https://res.cloudinary.com/dza7lstvk/image/upload/v1737470660/Medusa%20Resources/wishlist-16_ovujwp.jpg)

```ts title="src/workflows/create-wishlist-item.ts" collapsibleLines="1-6" expandButtonLabel="Show Imports" highlights={createWishlistItemWorkflowHighlights}
import { createWorkflow, WorkflowResponse } from "@medusajs/framework/workflows-sdk"
import { useQueryGraphStep } from "@medusajs/medusa/core-flows"
import { validateWishlistSalesChannelStep } from "./steps/validate-wishlist-sales-channel"
import { createWishlistItemStep } from "./steps/create-wishlist-item"
import { validateVariantWishlistStep } from "./steps/validate-variant-wishlist"
import { validateWishlistExistsStep } from "./steps/validate-wishlist-exists"

type CreateWishlistItemWorkflowInput = {
  variant_id: string
  customer_id: string
  sales_channel_id: string
}

export const createWishlistItemWorkflow = createWorkflow(
  "create-wishlist-item",
  (input: CreateWishlistItemWorkflowInput) => {
    const { data: wishlists } = useQueryGraphStep({
      entity: "wishlist",
      fields: ["*", "items.*"],
      filters: {
        customer_id: input.customer_id,
      },
    })

    validateWishlistExistsStep({
      wishlists,
    })

    validateWishlistSalesChannelStep({
      wishlist: wishlists[0],
      sales_channel_id: input.sales_channel_id,
    })


    validateVariantWishlistStep({
      variant_id: input.variant_id,
      sales_channel_id: input.sales_channel_id,
      wishlist: wishlists[0],
    })

    createWishlistItemStep({
      product_variant_id: input.variant_id,
      wishlist_id: wishlists[0].id,
    })

    // refetch wishlist
    const { data: updatedWishlists } = useQueryGraphStep({
      entity: "wishlist",
      fields: ["*", "items.*", "items.product_variant.*"],
      filters: {
        id: wishlists[0].id,
      },
    }).config({ name: "refetch-wishlist" })

    return new WorkflowResponse({
      wishlist: updatedWishlists[0],
    })
  }
)
```

You create a `createWishlistItemWorkflow`. In the workflow, you:

- Use the [useQueryGraphStep](https://docs.medusajs.com/references/helper-steps/useQueryGraphStep/index.html.md) to retrieve the wishlist of a customer. Notice that you pass the link definition between a wishlist and a customer as an entry point to Query. This allows you to filter the wishlist by the customer ID.
- Use the `validateWishlistSalesChannelStep` step to validate that the wishlist belongs to the sales channel specified in the input.
- Use the `validateVariantWishlistStep` step to validate that the variant specified in the input is not already in the wishlist.
- Use the `createWishlistItemStep` step to create the wishlist item.
- Use the `useQueryGraphStep` again to retrieve the wishlist with the new item added.

You return the wishlist with its items.

### Add Item to Wishlist API Route

You'll now create an API route that executes the `createWishlistItemWorkflow` to add an item to a wishlist.

Create the file `src/api/store/customers/me/wishlists/items/route.ts` with the following content:

![Directory structure after adding the route file](https://res.cloudinary.com/dza7lstvk/image/upload/v1737470985/Medusa%20Resources/wishlist-17_zmqk6c.jpg)

```ts title="src/api/store/customers/me/wishlists/items/route.ts"
import { AuthenticatedMedusaRequest, MedusaResponse } from "@medusajs/framework"
import { createWishlistItemWorkflow } from "../../../../../../workflows/create-wishlist-item"
import { MedusaError } from "@medusajs/framework/utils"

type PostStoreCreateWishlistItemType = {
  variant_id: string
}

export async function POST(
  req: AuthenticatedMedusaRequest<PostStoreCreateWishlistItemType>,
  res: MedusaResponse
) {
  if (!req.publishable_key_context?.sales_channel_ids.length) {
    throw new MedusaError(
      MedusaError.Types.INVALID_DATA,
      "At least one sales channel ID is required to be associated with the publishable API key in the request header."
    )
  }
  const { result } = await createWishlistItemWorkflow(req.scope)
    .run({
      input: {
        variant_id: req.validatedBody.variant_id,
        customer_id: req.auth_context.actor_id,
        sales_channel_id: req.publishable_key_context?.sales_channel_ids[0],
      },
    })

  res.json({
    wishlist: result.wishlist,
  })
}
```

This route exposes a `POST` endpoint at `/store/customers/me/wishlists/items`. Notice that the `AuthenticatedMedusaRequest` accepts a type parameter indicating the type of the accepted request body. In this case, the request body must have a `variant_id` property, indicating the ID of the variant to add to the wishlist.

In the route handler function, you execute the `createWishlistItemWorkflow` workflow, passing the authenticated customer ID, the variant ID, and the sales channel ID as input. You return in the response the updated wishlist.

### Add Validation Schema

To ensure that a variant ID is passed in the body of requests sent to this API route, you'll define a validation schema for the request body.

In Medusa, you create validation schemas using [Zod](https://zod.dev/) in a TypeScript file under the `src/api` directory. So, create the file `src/api/store/customers/me/wishlists/items/validators.ts` with the following content:

![Directory structure after adding the validation schema file](https://res.cloudinary.com/dza7lstvk/image/upload/v1737471383/Medusa%20Resources/wishlist-18_hj9iom.jpg)

```ts title="src/api/store/customers/me/wishlists/items/validators.ts"
import { z } from "zod"

export const PostStoreCreateWishlistItem = z.object({
  variant_id: z.string(),
})
```

You create an object schema with a `variant_id` property of type `string`.

Learn more about creating schemas in [Zod's documentation](https://zod.dev/).

You can now replace the `PostStoreCreateWishlistItemType` type in `src/api/store/customers/me/wishlists/items/route.ts` with the following:

```ts title="src/api/store/customers/me/wishlists/items/route.ts"
// ...
import { z } from "zod"
import { PostStoreCreateWishlistItem } from "./validators"

type PostStoreCreateWishlistItemType = z.infer<
  typeof PostStoreCreateWishlistItem
>
```

Finally, to use the schema for validation, you need to apply the `validateAndTransformBody` middleware on the `/store/customers/me/wishlists/items` route. A middleware is a function executed before the API route when a request is sent to it.

The `validateAndTransformBody` middleware is available out-of-the-box in Medusa, allowing you to validate and transform the request body using a Zod schema.

Learn more about middlewares in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/middlewares/index.html.md).

To apply the middleware, create the file `src/api/middlewares.ts` with the following content:

![Directory structure after adding the middleware file](https://res.cloudinary.com/dza7lstvk/image/upload/v1737471615/Medusa%20Resources/wishlist-19_ryyzdk.jpg)

```ts title="src/api/middlewares.ts"
import { 
  defineMiddlewares,
  validateAndTransformBody,
} from "@medusajs/framework/http"
import { 
  PostStoreCreateWishlistItem,
} from "./store/customers/me/wishlists/items/validators"

export default defineMiddlewares({
  routes: [
    {
      matcher: "/store/customers/me/wishlists/items",
      method: "POST",
      middlewares: [
        validateAndTransformBody(PostStoreCreateWishlistItem),
      ],
    },
  ],
})
```

In this file, you export the middlewares definition using `defineMiddlewares` from the Medusa Framework. This function accepts an object having a `routes` property, which is an array of middleware configurations to apply on routes.

You pass in the `routes` array an object having the following properties:

- `matcher`: The route to apply the middleware on.
- `method`: The HTTP method to apply the middleware on for the specified API route.
- `middlewares`: An array of the middlewares to apply. You apply the following middleware:
  - `validateAndTransformBody`: A middleware to ensure the received request body is valid against the Zod schema you defined earlier.

Any request sent to `/store/customers/me/wishlists/items` will now automatically fail if its body parameters don't match the `PostStoreCreateWishlistItem` validation schema.

### Test API Route

Start the Medusa application to test out the API route.

Make sure that `npx medusa plugin:develop` is running in the plugin project to publish the changes to the local registry.

#### Retrieve Variant ID

To retrieve an ID of a variant to add to the wishlist, send a `GET` request to the `/store/products` API route:

```bash
curl 'localhost:9000/store/products' \
--header 'x-publishable-api-key: {api_key}'
```

Make sure to replace `{api_key}` with the publishable API key you copied from the settings, as explained in [a previous section](#retrieve-publishable-api-key).

The response will contain a list of products. You can use the `id` of a product's variant to add to the wishlist.

#### Add Variant to Wishlist

Then, send a `POST` request to the `/store/customers/me/wishlists/items` API route to add the variant to the wishlist:

```bash
curl -X POST 'localhost:9000/store/customers/me/wishlists/items' \
--header 'Content-Type: application/json'  \
--header 'x-publishable-api-key: {api_key}' \
--header 'Authorization: Bearer {token}' \
--data-raw '{
    "variant_id": "{variant_id}"
}'
```

Make sure to replace:

- `{api_key}` with the publishable API key you copied from the settings, as explained in [a previous section](#retrieve-publishable-api-key).
- `{token}` with the authenticated customer token, as explained in [a previous section](#retrieve-authenticated-customer-token).
- `{variant_id}` with the ID of the variant you retrieved from the `/store/products` API route.

You'll receive in the response the updated wishlist with the added item.

***

## Step 10: Remove Item from Wishlist API Route

In this step, you'll add the functionality to remove an item from a wishlist. You'll first define a workflow that implements this functionality, then create an API route that executes the workflow.

### Remove Item from Wishlist Workflow

The workflow to remove an item from a wishlist has the following steps:

- [useQueryGraphStep](https://docs.medusajs.com/references/helper-steps/useQueryGraphStep/index.html.md): Retrieve the wishlist of a customer.
- [validateWishlistExistsStep](#validateWishlistExistsStep): Validate that the customer's wishlist exists.
- [validateItemInWishlistStep](#validateItemInWishlistStep): Validate that the item is in the customer's wishlist.
- [deleteWishlistItemStep](#deleteWishlistItemStep): Delete the wishlist item.
- [useQueryGraphStep](https://docs.medusajs.com/references/helper-steps/useQueryGraphStep/index.html.md): Retrieve the wishlist again with the item removed.

The `useQueryGraphStep` is from Medusa's workflows package, and you implemented the `validateWishlistExistsStep` [previously](#validatewishlistexistsstep) . So, you'll only implement the other steps.

#### validateItemInWishlistStep

The second step of the workflow validates that the item to remove is in the authenticated customer's wishlist.

To create the step, create the file `src/workflows/steps/validate-item-in-wishlist.ts` with the following content:

![Directory structure after adding the step file](https://res.cloudinary.com/dza7lstvk/image/upload/v1737474621/Medusa%20Resources/wishlist-20_jcwrtf.jpg)

```ts title="src/workflows/steps/validate-item-in-wishlist.ts"
import { InferTypeOf } from "@medusajs/framework/types"
import { Wishlist } from "../../modules/wishlist/models/wishlist"
import { createStep } from "@medusajs/framework/workflows-sdk"
import { MedusaError } from "@medusajs/framework/utils"

type ValidateItemInWishlistStepInput = {
  wishlist: InferTypeOf<typeof Wishlist>
  wishlist_item_id: string
}

export const validateItemInWishlistStep = createStep(
  "validate-item-in-wishlist",
  async ({ 
    wishlist, 
    wishlist_item_id,
  }: ValidateItemInWishlistStepInput, { container }) => {
    const item = wishlist.items.find((item) => item.id === wishlist_item_id)

    if (!item) {
      throw new MedusaError(
        MedusaError.Types.INVALID_DATA,
        "Item does not exist in customer's wishlist"
      )
    }
  }
)
```

This step receives the wishlist object and the wishlist item ID as input. In the step function, you find the item in the wishlist by its ID. If the item doesn't exist, you throw an error.

#### deleteWishlistItemStep

The third step of the workflow deletes the item from the wishlist.

Create the file `src/workflows/steps/delete-wishlist-item.ts` with the following content:

![Directory structure after adding the step file](https://res.cloudinary.com/dza7lstvk/image/upload/v1737474703/Medusa%20Resources/wishlist-21_e50lrg.jpg)

```ts title="src/workflows/steps/delete-wishlist-item.ts"
import { createStep, StepResponse } from "@medusajs/framework/workflows-sdk"
import WishlistModuleService from "../../modules/wishlist/service"
import { WISHLIST_MODULE } from "../../modules/wishlist"

type DeleteWishlistItemStepInput = {
  wishlist_item_id: string
}

export const deleteWishlistItemStep = createStep(
  "delete-wishlist-item",
  async ({ wishlist_item_id }: DeleteWishlistItemStepInput, { container }) => {
    const wishlistModuleService: WishlistModuleService = 
      container.resolve(WISHLIST_MODULE)

    await wishlistModuleService.softDeleteWishlistItems(wishlist_item_id)

    return new StepResponse(void 0, wishlist_item_id)
  },
  async (wishlistItemId, { container }) => {
    const wishlistModuleService: WishlistModuleService = 
      container.resolve(WISHLIST_MODULE)

    await wishlistModuleService.restoreWishlistItems([wishlistItemId])
  }
)
```

This step receives the wishlist item ID as input. In the step function, you resolve the Wishlist Module's service from the container and use its generated `softDeleteWishlistItems` method to delete the wishlist item.

You pass the deleted wishlist item ID to the compensation function. In the compensation function, you resolve the Wishlist Module's service from the container and use its generated `restoreWishlistItems` method to restore the wishlist item if an error occurs in the workflow.

Learn more about the [softDelete](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/service-factory-reference/methods/soft-delete/index.html.md) and [restore](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/service-factory-reference/methods/restore/index.html.md) generated methods.

#### Remove Item from Wishlist Workflow

You can now add the `deleteWishlistItemWorkflow` to the plugin. Create the file `src/workflows/delete-wishlist-item.ts` with the following content:

![Directory structure after adding the workflow file](https://res.cloudinary.com/dza7lstvk/image/upload/v1737474872/Medusa%20Resources/wishlist-22_wt1g36.jpg)

```ts title="src/workflows/delete-wishlist-item.ts" highlights={deleteWishlistItemWorkflowHighlights}
import { createWorkflow, WorkflowResponse } from "@medusajs/framework/workflows-sdk"
import { deleteWishlistItemStep } from "./steps/delete-wishlist-item"
import { useQueryGraphStep } from "@medusajs/medusa/core-flows"
import { validateItemInWishlistStep } from "./steps/validate-item-in-wishlist"
import { validateWishlistExistsStep } from "./steps/validate-wishlist-exists"

type DeleteWishlistItemWorkflowInput = {
  wishlist_item_id: string
  customer_id: string
}

export const deleteWishlistItemWorkflow = createWorkflow(
  "delete-wishlist-item",
  (input: DeleteWishlistItemWorkflowInput) => {
    const { data: wishlists } = useQueryGraphStep({
      entity: "wishlist",
      fields: ["*", "items.*"],
      filters: {
        customer_id: input.customer_id,
      },
    })
    
    validateWishlistExistsStep({
      wishlists,
    })

    validateItemInWishlistStep({
      wishlist: wishlists[0],
      wishlist_item_id: input.wishlist_item_id,
    })

    deleteWishlistItemStep(input)

    // refetch wishlist
    const { data: updatedWishlists } = useQueryGraphStep({
      entity: "wishlist",
      fields: ["*", "items.*", "items.product_variant.*"],
      filters: {
        id: wishlists[0].id,
      },
    }).config({ name: "refetch-wishlist" })

    return new WorkflowResponse({
      wishlist: updatedWishlists[0],
    })
  }
)
```

You create a `deleteWishlistItemWorkflow`. In the workflow, you:

- Use the [useQueryGraphStep](https://docs.medusajs.com/references/helper-steps/useQueryGraphStep/index.html.md) to retrieve the wishlist of a customer. Notice that you pass the link definition between a wishlist and a customer as an entry point to Query. This allows you to filter the wishlist by the customer ID.
- Use the `validateItemInWishlistStep` step to validate that the item to remove is in the customer's wishlist.
- Use the `deleteWishlistItemStep` step to delete the item from the wishlist.
- Use the `useQueryGraphStep` again to retrieve the wishlist with the item removed.

You return the wishlist without the removed item.

### Remove Item from Wishlist API Route

You'll now create an API route that executes the `deleteWishlistItemWorkflow` to remove an item from a wishlist.

Create the file `src/api/store/customers/me/wishlists/items/[id]/route.ts` with the following content:

![Directory structure after adding the route file](https://res.cloudinary.com/dza7lstvk/image/upload/v1737475074/Medusa%20Resources/wishlist-23_qatcia.jpg)

```ts title="src/api/store/customers/me/wishlists/items/[id]/route.ts"
import { AuthenticatedMedusaRequest, MedusaResponse } from "@medusajs/framework"
import { deleteWishlistItemWorkflow } from "../../../../../../../workflows/delete-wishlist-item"

export async function DELETE(
  req: AuthenticatedMedusaRequest,
  res: MedusaResponse
) {
  const { result } = await deleteWishlistItemWorkflow(req.scope)
    .run({
      input: {
        wishlist_item_id: req.params.id,
        customer_id: req.auth_context.actor_id,
      },
    })

  res.json({
    wishlist: result.wishlist,
  })
}
```

This route exposes a `DELETE` endpoint at `/store/customers/me/wishlists/items/:id`. The `:id` parameter in the route path represents the ID of the wishlist item to remove.

In the route handler function, you execute the `deleteWishlistItemWorkflow` workflow, passing the authenticated customer ID and the wishlist item ID as input. You return in the response the updated wishlist.

### Test API Route

Start the Medusa application to test out the API route.

Make sure that `npx medusa plugin:develop` is running in the plugin project to publish the changes to the local registry.

#### Retrieve Wishlist Item ID

To retrieve an ID of a wishlist item to remove, send a `GET` request to the `/store/customers/me/wishlists` API route:

```bash
curl 'localhost:9000/store/customers/me/wishlists' \
--header 'x-publishable-api-key: {api_key}' \
--header 'Authorization: Bearer {token}'
```

Make sure to replace:

- `{api_key}` with the publishable API key you copied from the settings, as explained in [a previous section](#retrieve-publishable-api-key).
- `{token}` with the authenticated customer token, as explained in [a previous section](#retrieve-authenticated-customer-token).

The response will contain the wishlist of the authenticated customer. You can use the `id` of an item in the wishlist to remove.

#### Remove Item from Wishlist

Then, send a `DELETE` request to the `/store/customers/me/wishlists/items/:id` API route to remove the item from the wishlist:

```bash
curl -X DELETE 'localhost:9000/store/customers/me/wishlists/items/{item_id}' \
--header 'x-publishable-api-key: {api_key}' \
--header 'Authorization: Bearer {token}'
```

Make sure to replace:

- `{api_key}` with the publishable API key you copied from the settings, as explained in [a previous section](#retrieve-publishable-api-key).
- `{token}` with the authenticated customer token, as explained in [a previous section](#retrieve-authenticated-customer-token).
- `{item_id}` with the ID of the item you retrieved from the `/store/customers/me/wishlists` API route.

You'll receive in the response the updated wishlist without the removed item.

***

## Step 11: Share Wishlist API Route

In this step, you'll add the functionality to allow customers to share their wishlist with others. The route will return a token that can be passed to another API route that you'll create in the next step to retrieve the shared wishlist.

To create the token and decode it later, you'll use the [jsonwebtoken](https://www.npmjs.com/package/jsonwebtoken) package. So, run the following command in the plugin project to install the package:

```bash npm2yarn badgeLabel="Plugin project" badgeColor="orange"
npm install jsonwebtoken
```

Then, to create the API route, create the file `src/api/store/customers/me/wishlists/share/route.ts` with the following content:

![Directory structure after adding the route file](https://res.cloudinary.com/dza7lstvk/image/upload/v1737475331/Medusa%20Resources/wishlist-24_tiwjpr.jpg)

```ts title="src/api/store/customers/me/wishlists/share/route.ts"
import { AuthenticatedMedusaRequest, MedusaResponse } from "@medusajs/framework"
import { MedusaError } from "@medusajs/framework/utils"
import jwt from "jsonwebtoken"

export async function POST(
  req: AuthenticatedMedusaRequest,
  res: MedusaResponse
) {
  if (!req.publishable_key_context?.sales_channel_ids.length) {
    throw new MedusaError(
      MedusaError.Types.INVALID_DATA,
      "At least one sales channel ID is required to be associated with the publishable API key in the request header."
    )
  }

  const query = req.scope.resolve("query")

  const { data } = await query.graph({
    entity: "wishlist",
    fields: ["*"],
    filters: {
      customer_id: req.auth_context.actor_id,
    },
  })

  if (!data.length) {
    throw new MedusaError(
      MedusaError.Types.NOT_FOUND,
      "No wishlist found for customer"
    )
  }

  if (data[0].sales_channel_id !== req.publishable_key_context.sales_channel_ids[0]) {
    throw new MedusaError(
      MedusaError.Types.INVALID_DATA,
      "Wishlist does not belong to the specified sales channel"
    )
  }

  // TODO generate the token
}
```

This route exposes a `POST` endpoint at `/store/customers/me/wishlists/share`. In the route handler function, you use Query to retrieve the wishlist of the authenticated customer. If the customer doesn't have a wishlist, or the wishlist doesn't belong to the sales channel specified in the request's publishable API key, you throw an error.

You'll now generate a token that contains the wishlist ID. To do this, replace the `TODO` in the route handler function with the following:

```ts title="src/api/store/customers/me/wishlists/share/route.ts"
const { http } = req.scope.resolve("configModule").projectConfig

const wishlistToken = jwt.sign({
  wishlist_id: data[0].id,
}, http.jwtSecret!, {
  expiresIn: http.jwtExpiresIn,
})

return res.json({
  token: wishlistToken,
})
```

You first retrieve the [http Medusa configuration](https://docs.medusajs.com/docs/learn/configurations/medusa-config#http/index.html.md) which holds configurations related to JWT secrets and expiration times. You then use the `jsonwebtoken` package to sign a token containing the wishlist ID. You return the token in the response.

### Test API Route

Start the Medusa application to test out the API route.

Make sure that `npx medusa plugin:develop` is running in the plugin project to publish the changes to the local registry.

Then, send a `POST` request to the `/store/customers/me/wishlists/share` API route to generate a share token for the authenticated customer's wishlist:

```bash
curl -X POST 'localhost:9000/store/customers/me/wishlists/share' \
--header 'x-publishable-api-key: {api_key}' \
--header 'Authorization: Bearer {token}'
```

Make sure to replace:

- `{api_key}` with the publishable API key you copied from the settings, as explained in [a previous section](#retrieve-publishable-api-key).
- `{token}` with the authenticated customer token, as explained in [a previous section](#retrieve-authenticated-customer-token).

You'll receive in the response a token that you can pass to the next API route to retrieve the shared wishlist.

***

## Step 12: Retrieve Shared Wishlist API Route

In this step, you'll add an API route that retrieves a wishlist shared using a token returned by the `/store/customers/me/wishlists/share` API route.

Create the file `src/api/store/wishlists/[token]/route.ts` with the following content:

![Directory structure after adding the route file](https://res.cloudinary.com/dza7lstvk/image/upload/v1737475795/Medusa%20Resources/wishlist-25_sodzsr.jpg)

```ts title="src/api/store/wishlists/[token]/route.ts"
import { MedusaResponse, MedusaStoreRequest } from "@medusajs/framework"
import { MedusaError } from "@medusajs/framework/utils"
import { decode, JwtPayload } from "jsonwebtoken"

export async function GET(
  req: MedusaStoreRequest,
  res: MedusaResponse
) {
  if (!req.publishable_key_context?.sales_channel_ids.length) {
    throw new MedusaError(
      MedusaError.Types.INVALID_DATA,
      "At least one sales channel ID is required to be associated with the publishable API key in the request header."
    )
  }
  
  const decodedToken = decode(req.params.token) as JwtPayload

  if (!decodedToken.wishlist_id) {
    throw new MedusaError(
      MedusaError.Types.INVALID_DATA,
      "Invalid token"
    )
  }

  const query = req.scope.resolve("query")

  const { data } = await query.graph({
    entity: "wishlist",
    fields: ["*", "items.*", "items.product_variant.*"],
    filters: {
      id: decodedToken.wishlist_id,
    },
  })

  if (!data.length) {
    throw new MedusaError(
      MedusaError.Types.NOT_FOUND,
      "No wishlist found"
    )
  }

  if (data[0].sales_channel_id !== req.publishable_key_context.sales_channel_ids[0]) {
    throw new MedusaError(
      MedusaError.Types.INVALID_DATA,
      "Wishlist does not belong to the request's sales channel"
    )
  }

  res.json({
    wishlist: data[0],
  })
}
```

This route exposes a `GET` endpoint at `/store/wishlists/:token`. The `:token` parameter in the route path represents the token generated by the `/store/customers/me/wishlists/share` API route.

In the route handler function, you decode the token to retrieve the wishlist ID. If the token is invalid, you throw an error.

Then, you use Query to retrieve the wishlist with the ID from the decoded token. If no wishlist is found or the wishlist doesn't belong to the sales channel ID of the current request, you throw an error.

You return in the response the shared wishlist.

### Test API Route

Start the Medusa application to test out the API route.

Make sure that `npx medusa plugin:develop` is running in the plugin project to publish the changes to the local registry.

Then, send a `GET` request to the `/store/wishlists/:token` API route to retrieve the shared wishlist:

```bash
curl 'localhost:9000/store/wishlists/{wishlist_token}' \
--header 'x-publishable-api-key: {api_key}'
```

Make sure to replace:

- `{wishlist_token}` with the token you received from the `/store/customers/me/wishlists/share` API route.
- `{api_key}` with the publishable API key you copied from the settings, as explained in [a previous section](#retrieve-publishable-api-key).

You'll receive in the response the shared wishlist.

***

## Step 13: Show Wishlist Count in Medusa Admin

In this step, you'll customize the Medusa Admin dashboard to show for each product the number of wishlists it's in.

The Medusa Admin dashboard's pages are customizable to insert widgets of custom content in pre-defined injection zones. You create these widgets as React components that allow admin users to perform custom actions.

Learn more about widgets in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/admin/widgets/index.html.md).

### Add Method to Retrieve Wishlist Count

To retrieve the number of wishlists a product is in, you'll add a method to the `WishlistModuleService` that runs a query to retrieve distinct wishlist IDs containing a product variant.

In `src/modules/wishlist/service.ts`, add the following imports and method:

```ts title="src/modules/wishlist/service.ts"
// other imports...
import { InjectManager } from "@medusajs/framework/utils"
import { Context } from "@medusajs/framework/types"
import { EntityManager } from "@mikro-orm/knex"

export default class WishlistModuleService extends MedusaService({
  Wishlist,
  WishlistItem,
}) {
  @InjectManager()
  async getWishlistsOfVariants(
    variantIds: string[],
    @MedusaContext() context: Context<EntityManager> = {}
  ): Promise<number> {
    return (await context.manager?.createQueryBuilder("wishlist_item", "wi")
      .select(["wi.wishlist_id"], true)
      .where("wi.product_variant_id IN (?)", [variantIds])
      .execute())?.length || 0
  }
}
```

To perform queries on the database in a method, add the `@InjectManager` decorator to the method. This will inject a [forked MikroORM entity manager](https://mikro-orm.io/docs/identity-map#forking-entity-manager) that you can use in your method.

Methods with the `@InjectManager` decorator accept as a last parameter a context object that has the `@MedusaContext` decorator. The entity manager is injected into the `manager` property of this paramter.

The method accepts an array of variant IDs as a parameter. In the method, you use the `createQueryBuilder` to construct a query, passing it the name of the `WishlistItem`'s table. You then select distinct `wishlist_id`s where the `product_variant_id` of the wishlist item is in the array of variant IDs.

You execute the query and return the number of distinct wishlist IDs containing the product variants. You'll use this method next.

### Create Wishlist Count API Route

Before creating the widget, you'll create the API route that retrieves the number of wishlists a product is in.

Create the file `src/api/store/products/[id]/wishlist/route.ts` with the following content:

![Directory structure after adding the route file](https://res.cloudinary.com/dza7lstvk/image/upload/v1737630927/Medusa%20Resources/wishlist-26_ervnfg.jpg)

```ts title="src/api/store/products/[id]/wishlist/route.ts" highlights={wishlistCountRouteHighlights}
import { MedusaRequest, MedusaResponse } from "@medusajs/framework"
import WishlistModuleService from "../../../../../modules/wishlist/service"
import { WISHLIST_MODULE } from "../../../../../modules/wishlist"
import { MedusaError } from "@medusajs/framework/utils"

export async function GET(
  req: MedusaRequest,
  res: MedusaResponse
) {
  const { id } = req.params

  const query = req.scope.resolve("query")
  const wishlistModuleService: WishlistModuleService = req.scope.resolve(
    WISHLIST_MODULE
  )

  const { data: [product] } = await query.graph({
    entity: "product",
    fields: ["variants.*"],
    filters: {
      id,
    },
  })

  if (!product) {
    throw new MedusaError(
      MedusaError.Types.NOT_FOUND,
      `Product with id: ${id} was not found`
    )
  }

  const count = await wishlistModuleService.getWishlistsOfVariants(
    product.variants.map((v) => v.id)
  )

  res.json({
    count,
  })
}
```

This route exposes a `GET` endpoint at `/store/products/:id/wishlist`. The `:id` parameter in the route path represents the ID of the product to retrieve the wishlist count for.

In the route handler function, you use Query to retrieve the product and its variants, and throw an error if the product doesn't exist.

Then, you resolve the `WishlistModuleService` from the Medusa Container and use its `getWishlistsOfVariants` method to retrieve the number of wishlists the product's variants are in. You return the count in the response.

You'll use this API route in the widget next.

### Create Wishlist Count Widget

You'll now create the widget that will be shown on a product's page in the Medusa Admin.

In the widget, you'll send a request to the API route you created to retrieve the wishlist count for the product. To send the request, you'll use the [JS SDK](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/js-sdk/index.html.md), which is a JavaScript library that simplifies sending requests to Medusa's API routes.

To initialize the JS SDK, create the file `src/admin/lib/sdk.ts` with the following content:

![Directory structure after adding the SDK file](https://res.cloudinary.com/dza7lstvk/image/upload/v1737631853/Medusa%20Resources/wishlist-27_pkzeaj.jpg)

```ts title="src/admin/lib/sdk.ts"
import Medusa from "@medusajs/js-sdk"

export const sdk = new Medusa({
  baseUrl: import.meta.env.VITE_BACKEND_URL || "/",
  debug: import.meta.env.DEV,
  auth: {
    type: "session",
  },
})
```

You initialize an instance of the JS SDK, which you'll use in the widget to send requests.

Learn more about the JS SDK and configuring it in [this documentation](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/js-sdk/index.html.md).

Then, to create the widget, create the file `src/admin/widgets/product-widget.tsx` with the following content:

![Directory structure after adding the widget file](https://res.cloudinary.com/dza7lstvk/image/upload/v1737631988/Medusa%20Resources/wishlist-28_fx8rw7.jpg)

```tsx title="src/admin/widgets/product-widget.tsx" highlights={widgetHighlights}
import { defineWidgetConfig } from "@medusajs/admin-sdk"
import { Container, Heading, Text } from "@medusajs/ui"
import { useQuery } from "@tanstack/react-query"
import { sdk } from "../lib/sdk"
import { 
  DetailWidgetProps, 
  AdminProduct,
} from "@medusajs/framework/types"

type WishlistResponse = {
  count: number
}

const ProductWidget = ({ 
  data: product,
}: DetailWidgetProps<AdminProduct>) => {
  const { data, isLoading } = useQuery<WishlistResponse>({
    queryFn: () => sdk.client.fetch(`/admin/products/${product.id}/wishlist`),
    queryKey: [["products", product.id, "wishlist"]],
  })

  return (
    <Container className="divide-y p-0">
      <div className="flex items-center justify-between px-6 py-4">
        <Heading level="h2">Wishlist</Heading>
      </div>
      <Text className="px-6 py-4">
        {isLoading ? 
          "Loading..." : `This product is in ${data?.count} wishlist(s).`
        }
      </Text>
    </Container>
  )
}

export const config = defineWidgetConfig({
  zone: "product.details.before",
})

export default ProductWidget
```

A widget file must export a React component and a `config` object created with `defineWidgetConfig` from the Admin Extension SDK. In the `config` object, you specify the zone to inject the widget into in the `zone` property. This widget is injected into a product's page before any other sections.

Find all widget injection zones in [this reference](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/admin-widget-injection-zones/index.html.md).

Since the widget is injected into a product's details page, it receives the product's details as a `data` prop. In the widget, you use [Tanstack Query](https://tanstack.com/query/latest) to benefit from features like data caching and invalidation. You use the `useQuery` hook to send a request to the API route you created to retrieve the wishlist count for the product.

Finally, you display the widget's content using components from [Medusa UI](https://docs.medusajs.com/ui/index.html.md), allowing you to align the design of your widget with the Medusa Admin's design system.

### Test it Out

To test it out, start the Medusa application.

Make sure that `npx medusa plugin:develop` is running in the plugin project to publish the changes to the local registry.

Then:

1. open the Medusa Admin at `localhost:9000/app` and log in.

2. Click on Products in the sidebar, then choose a product from the table.

![Click on the "Products" in the sidebar on the right, then choose a product from the table shown in the middle](https://res.cloudinary.com/dza7lstvk/image/upload/v1737632826/Medusa%20Resources/Screenshot_2025-01-23_at_1.46.29_PM_xjsn8s.png)

3. You should see the widget you created showing the number of wishlists the product is in at the top of the page.

![The widget is shown at the top of the product page before other sections](https://res.cloudinary.com/dza7lstvk/image/upload/v1737632826/Medusa%20Resources/Screenshot_2025-01-23_at_1.46.05_PM_hfyz7u.png)

***

## Next Steps

You've now implemented the wishlist functionality in a Medusa plugin. You can publish that plugin as explained in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/plugins/create#5-publish-plugin-to-npm/index.html.md) to NPM and install it in any Medusa application. This will allow you to re-use your plugin or share it with the community.

If you're new to Medusa, check out the [main documentation](https://docs.medusajs.com/docs/learn/index.html.md), 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](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/index.html.md).


## JS SDK Admin

- [create](https://docs.medusajs.com/references/js_sdk/admin/Campaign/methods/js_sdk.admin.Campaign.create/index.html.md)
- [batchPromotions](https://docs.medusajs.com/references/js_sdk/admin/Campaign/methods/js_sdk.admin.Campaign.batchPromotions/index.html.md)
- [list](https://docs.medusajs.com/references/js_sdk/admin/Campaign/methods/js_sdk.admin.Campaign.list/index.html.md)
- [update](https://docs.medusajs.com/references/js_sdk/admin/Campaign/methods/js_sdk.admin.Campaign.update/index.html.md)
- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/Campaign/methods/js_sdk.admin.Campaign.retrieve/index.html.md)
- [delete](https://docs.medusajs.com/references/js_sdk/admin/Campaign/methods/js_sdk.admin.Campaign.delete/index.html.md)
- [delete](https://docs.medusajs.com/references/js_sdk/admin/ApiKey/methods/js_sdk.admin.ApiKey.delete/index.html.md)
- [batchSalesChannels](https://docs.medusajs.com/references/js_sdk/admin/ApiKey/methods/js_sdk.admin.ApiKey.batchSalesChannels/index.html.md)
- [create](https://docs.medusajs.com/references/js_sdk/admin/ApiKey/methods/js_sdk.admin.ApiKey.create/index.html.md)
- [list](https://docs.medusajs.com/references/js_sdk/admin/ApiKey/methods/js_sdk.admin.ApiKey.list/index.html.md)
- [update](https://docs.medusajs.com/references/js_sdk/admin/ApiKey/methods/js_sdk.admin.ApiKey.update/index.html.md)
- [revoke](https://docs.medusajs.com/references/js_sdk/admin/ApiKey/methods/js_sdk.admin.ApiKey.revoke/index.html.md)
- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/ApiKey/methods/js_sdk.admin.ApiKey.retrieve/index.html.md)
- [getItem](https://docs.medusajs.com/references/js_sdk/admin/CustomStorage/methods/js_sdk.admin.CustomStorage.getItem/index.html.md)
- [setItem](https://docs.medusajs.com/references/js_sdk/admin/CustomStorage/methods/js_sdk.admin.CustomStorage.setItem/index.html.md)
- [removeItem](https://docs.medusajs.com/references/js_sdk/admin/CustomStorage/methods/js_sdk.admin.CustomStorage.removeItem/index.html.md)
- [list](https://docs.medusajs.com/references/js_sdk/admin/Currency/methods/js_sdk.admin.Currency.list/index.html.md)
- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/Currency/methods/js_sdk.admin.Currency.retrieve/index.html.md)
- [addInboundItems](https://docs.medusajs.com/references/js_sdk/admin/Claim/methods/js_sdk.admin.Claim.addInboundItems/index.html.md)
- [addInboundShipping](https://docs.medusajs.com/references/js_sdk/admin/Claim/methods/js_sdk.admin.Claim.addInboundShipping/index.html.md)
- [addOutboundShipping](https://docs.medusajs.com/references/js_sdk/admin/Claim/methods/js_sdk.admin.Claim.addOutboundShipping/index.html.md)
- [cancel](https://docs.medusajs.com/references/js_sdk/admin/Claim/methods/js_sdk.admin.Claim.cancel/index.html.md)
- [addItems](https://docs.medusajs.com/references/js_sdk/admin/Claim/methods/js_sdk.admin.Claim.addItems/index.html.md)
- [addOutboundItems](https://docs.medusajs.com/references/js_sdk/admin/Claim/methods/js_sdk.admin.Claim.addOutboundItems/index.html.md)
- [create](https://docs.medusajs.com/references/js_sdk/admin/Claim/methods/js_sdk.admin.Claim.create/index.html.md)
- [cancelRequest](https://docs.medusajs.com/references/js_sdk/admin/Claim/methods/js_sdk.admin.Claim.cancelRequest/index.html.md)
- [deleteOutboundShipping](https://docs.medusajs.com/references/js_sdk/admin/Claim/methods/js_sdk.admin.Claim.deleteOutboundShipping/index.html.md)
- [list](https://docs.medusajs.com/references/js_sdk/admin/Claim/methods/js_sdk.admin.Claim.list/index.html.md)
- [removeInboundItem](https://docs.medusajs.com/references/js_sdk/admin/Claim/methods/js_sdk.admin.Claim.removeInboundItem/index.html.md)
- [removeOutboundItem](https://docs.medusajs.com/references/js_sdk/admin/Claim/methods/js_sdk.admin.Claim.removeOutboundItem/index.html.md)
- [removeItem](https://docs.medusajs.com/references/js_sdk/admin/Claim/methods/js_sdk.admin.Claim.removeItem/index.html.md)
- [deleteInboundShipping](https://docs.medusajs.com/references/js_sdk/admin/Claim/methods/js_sdk.admin.Claim.deleteInboundShipping/index.html.md)
- [request](https://docs.medusajs.com/references/js_sdk/admin/Claim/methods/js_sdk.admin.Claim.request/index.html.md)
- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/Claim/methods/js_sdk.admin.Claim.retrieve/index.html.md)
- [updateInboundShipping](https://docs.medusajs.com/references/js_sdk/admin/Claim/methods/js_sdk.admin.Claim.updateInboundShipping/index.html.md)
- [updateItem](https://docs.medusajs.com/references/js_sdk/admin/Claim/methods/js_sdk.admin.Claim.updateItem/index.html.md)
- [updateInboundItem](https://docs.medusajs.com/references/js_sdk/admin/Claim/methods/js_sdk.admin.Claim.updateInboundItem/index.html.md)
- [updateOutboundShipping](https://docs.medusajs.com/references/js_sdk/admin/Claim/methods/js_sdk.admin.Claim.updateOutboundShipping/index.html.md)
- [updateOutboundItem](https://docs.medusajs.com/references/js_sdk/admin/Claim/methods/js_sdk.admin.Claim.updateOutboundItem/index.html.md)
- [clearToken](https://docs.medusajs.com/references/js_sdk/admin/Client/methods/js_sdk.admin.Client.clearToken/index.html.md)
- [fetchStream](https://docs.medusajs.com/references/js_sdk/admin/Client/methods/js_sdk.admin.Client.fetchStream/index.html.md)
- [clearToken\_](https://docs.medusajs.com/references/js_sdk/admin/Client/methods/js_sdk.admin.Client.clearToken_/index.html.md)
- [fetch](https://docs.medusajs.com/references/js_sdk/admin/Client/methods/js_sdk.admin.Client.fetch/index.html.md)
- [getApiKeyHeader\_](https://docs.medusajs.com/references/js_sdk/admin/Client/methods/js_sdk.admin.Client.getApiKeyHeader_/index.html.md)
- [getJwtHeader\_](https://docs.medusajs.com/references/js_sdk/admin/Client/methods/js_sdk.admin.Client.getJwtHeader_/index.html.md)
- [getTokenStorageInfo\_](https://docs.medusajs.com/references/js_sdk/admin/Client/methods/js_sdk.admin.Client.getTokenStorageInfo_/index.html.md)
- [getPublishableKeyHeader\_](https://docs.medusajs.com/references/js_sdk/admin/Client/methods/js_sdk.admin.Client.getPublishableKeyHeader_/index.html.md)
- [initClient](https://docs.medusajs.com/references/js_sdk/admin/Client/methods/js_sdk.admin.Client.initClient/index.html.md)
- [getToken\_](https://docs.medusajs.com/references/js_sdk/admin/Client/methods/js_sdk.admin.Client.getToken_/index.html.md)
- [setToken](https://docs.medusajs.com/references/js_sdk/admin/Client/methods/js_sdk.admin.Client.setToken/index.html.md)
- [setToken\_](https://docs.medusajs.com/references/js_sdk/admin/Client/methods/js_sdk.admin.Client.setToken_/index.html.md)
- [throwError\_](https://docs.medusajs.com/references/js_sdk/admin/Client/methods/js_sdk.admin.Client.throwError_/index.html.md)
- [batchCustomerGroups](https://docs.medusajs.com/references/js_sdk/admin/Customer/methods/js_sdk.admin.Customer.batchCustomerGroups/index.html.md)
- [create](https://docs.medusajs.com/references/js_sdk/admin/Customer/methods/js_sdk.admin.Customer.create/index.html.md)
- [delete](https://docs.medusajs.com/references/js_sdk/admin/Customer/methods/js_sdk.admin.Customer.delete/index.html.md)
- [createAddress](https://docs.medusajs.com/references/js_sdk/admin/Customer/methods/js_sdk.admin.Customer.createAddress/index.html.md)
- [deleteAddress](https://docs.medusajs.com/references/js_sdk/admin/Customer/methods/js_sdk.admin.Customer.deleteAddress/index.html.md)
- [list](https://docs.medusajs.com/references/js_sdk/admin/Customer/methods/js_sdk.admin.Customer.list/index.html.md)
- [listAddresses](https://docs.medusajs.com/references/js_sdk/admin/Customer/methods/js_sdk.admin.Customer.listAddresses/index.html.md)
- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/Customer/methods/js_sdk.admin.Customer.retrieve/index.html.md)
- [retrieveAddress](https://docs.medusajs.com/references/js_sdk/admin/Customer/methods/js_sdk.admin.Customer.retrieveAddress/index.html.md)
- [update](https://docs.medusajs.com/references/js_sdk/admin/Customer/methods/js_sdk.admin.Customer.update/index.html.md)
- [updateAddress](https://docs.medusajs.com/references/js_sdk/admin/Customer/methods/js_sdk.admin.Customer.updateAddress/index.html.md)
- [batchCustomers](https://docs.medusajs.com/references/js_sdk/admin/CustomerGroup/methods/js_sdk.admin.CustomerGroup.batchCustomers/index.html.md)
- [create](https://docs.medusajs.com/references/js_sdk/admin/CustomerGroup/methods/js_sdk.admin.CustomerGroup.create/index.html.md)
- [delete](https://docs.medusajs.com/references/js_sdk/admin/CustomerGroup/methods/js_sdk.admin.CustomerGroup.delete/index.html.md)
- [list](https://docs.medusajs.com/references/js_sdk/admin/CustomerGroup/methods/js_sdk.admin.CustomerGroup.list/index.html.md)
- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/CustomerGroup/methods/js_sdk.admin.CustomerGroup.retrieve/index.html.md)
- [update](https://docs.medusajs.com/references/js_sdk/admin/CustomerGroup/methods/js_sdk.admin.CustomerGroup.update/index.html.md)
- [addItems](https://docs.medusajs.com/references/js_sdk/admin/DraftOrder/methods/js_sdk.admin.DraftOrder.addItems/index.html.md)
- [addShippingMethod](https://docs.medusajs.com/references/js_sdk/admin/DraftOrder/methods/js_sdk.admin.DraftOrder.addShippingMethod/index.html.md)
- [addPromotions](https://docs.medusajs.com/references/js_sdk/admin/DraftOrder/methods/js_sdk.admin.DraftOrder.addPromotions/index.html.md)
- [beginEdit](https://docs.medusajs.com/references/js_sdk/admin/DraftOrder/methods/js_sdk.admin.DraftOrder.beginEdit/index.html.md)
- [convertToOrder](https://docs.medusajs.com/references/js_sdk/admin/DraftOrder/methods/js_sdk.admin.DraftOrder.convertToOrder/index.html.md)
- [cancelEdit](https://docs.medusajs.com/references/js_sdk/admin/DraftOrder/methods/js_sdk.admin.DraftOrder.cancelEdit/index.html.md)
- [confirmEdit](https://docs.medusajs.com/references/js_sdk/admin/DraftOrder/methods/js_sdk.admin.DraftOrder.confirmEdit/index.html.md)
- [removeActionShippingMethod](https://docs.medusajs.com/references/js_sdk/admin/DraftOrder/methods/js_sdk.admin.DraftOrder.removeActionShippingMethod/index.html.md)
- [create](https://docs.medusajs.com/references/js_sdk/admin/DraftOrder/methods/js_sdk.admin.DraftOrder.create/index.html.md)
- [list](https://docs.medusajs.com/references/js_sdk/admin/DraftOrder/methods/js_sdk.admin.DraftOrder.list/index.html.md)
- [removeActionItem](https://docs.medusajs.com/references/js_sdk/admin/DraftOrder/methods/js_sdk.admin.DraftOrder.removeActionItem/index.html.md)
- [removePromotions](https://docs.medusajs.com/references/js_sdk/admin/DraftOrder/methods/js_sdk.admin.DraftOrder.removePromotions/index.html.md)
- [removeShippingMethod](https://docs.medusajs.com/references/js_sdk/admin/DraftOrder/methods/js_sdk.admin.DraftOrder.removeShippingMethod/index.html.md)
- [requestEdit](https://docs.medusajs.com/references/js_sdk/admin/DraftOrder/methods/js_sdk.admin.DraftOrder.requestEdit/index.html.md)
- [update](https://docs.medusajs.com/references/js_sdk/admin/DraftOrder/methods/js_sdk.admin.DraftOrder.update/index.html.md)
- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/DraftOrder/methods/js_sdk.admin.DraftOrder.retrieve/index.html.md)
- [updateActionItem](https://docs.medusajs.com/references/js_sdk/admin/DraftOrder/methods/js_sdk.admin.DraftOrder.updateActionItem/index.html.md)
- [updateActionShippingMethod](https://docs.medusajs.com/references/js_sdk/admin/DraftOrder/methods/js_sdk.admin.DraftOrder.updateActionShippingMethod/index.html.md)
- [updateItem](https://docs.medusajs.com/references/js_sdk/admin/DraftOrder/methods/js_sdk.admin.DraftOrder.updateItem/index.html.md)
- [updateShippingMethod](https://docs.medusajs.com/references/js_sdk/admin/DraftOrder/methods/js_sdk.admin.DraftOrder.updateShippingMethod/index.html.md)
- [addInboundItems](https://docs.medusajs.com/references/js_sdk/admin/Exchange/methods/js_sdk.admin.Exchange.addInboundItems/index.html.md)
- [addInboundShipping](https://docs.medusajs.com/references/js_sdk/admin/Exchange/methods/js_sdk.admin.Exchange.addInboundShipping/index.html.md)
- [addOutboundItems](https://docs.medusajs.com/references/js_sdk/admin/Exchange/methods/js_sdk.admin.Exchange.addOutboundItems/index.html.md)
- [cancelRequest](https://docs.medusajs.com/references/js_sdk/admin/Exchange/methods/js_sdk.admin.Exchange.cancelRequest/index.html.md)
- [cancel](https://docs.medusajs.com/references/js_sdk/admin/Exchange/methods/js_sdk.admin.Exchange.cancel/index.html.md)
- [create](https://docs.medusajs.com/references/js_sdk/admin/Exchange/methods/js_sdk.admin.Exchange.create/index.html.md)
- [addOutboundShipping](https://docs.medusajs.com/references/js_sdk/admin/Exchange/methods/js_sdk.admin.Exchange.addOutboundShipping/index.html.md)
- [deleteInboundShipping](https://docs.medusajs.com/references/js_sdk/admin/Exchange/methods/js_sdk.admin.Exchange.deleteInboundShipping/index.html.md)
- [deleteOutboundShipping](https://docs.medusajs.com/references/js_sdk/admin/Exchange/methods/js_sdk.admin.Exchange.deleteOutboundShipping/index.html.md)
- [removeOutboundItem](https://docs.medusajs.com/references/js_sdk/admin/Exchange/methods/js_sdk.admin.Exchange.removeOutboundItem/index.html.md)
- [list](https://docs.medusajs.com/references/js_sdk/admin/Exchange/methods/js_sdk.admin.Exchange.list/index.html.md)
- [removeInboundItem](https://docs.medusajs.com/references/js_sdk/admin/Exchange/methods/js_sdk.admin.Exchange.removeInboundItem/index.html.md)
- [request](https://docs.medusajs.com/references/js_sdk/admin/Exchange/methods/js_sdk.admin.Exchange.request/index.html.md)
- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/Exchange/methods/js_sdk.admin.Exchange.retrieve/index.html.md)
- [updateInboundItem](https://docs.medusajs.com/references/js_sdk/admin/Exchange/methods/js_sdk.admin.Exchange.updateInboundItem/index.html.md)
- [updateOutboundShipping](https://docs.medusajs.com/references/js_sdk/admin/Exchange/methods/js_sdk.admin.Exchange.updateOutboundShipping/index.html.md)
- [updateOutboundItem](https://docs.medusajs.com/references/js_sdk/admin/Exchange/methods/js_sdk.admin.Exchange.updateOutboundItem/index.html.md)
- [updateInboundShipping](https://docs.medusajs.com/references/js_sdk/admin/Exchange/methods/js_sdk.admin.Exchange.updateInboundShipping/index.html.md)
- [cancel](https://docs.medusajs.com/references/js_sdk/admin/Fulfillment/methods/js_sdk.admin.Fulfillment.cancel/index.html.md)
- [create](https://docs.medusajs.com/references/js_sdk/admin/Fulfillment/methods/js_sdk.admin.Fulfillment.create/index.html.md)
- [createShipment](https://docs.medusajs.com/references/js_sdk/admin/Fulfillment/methods/js_sdk.admin.Fulfillment.createShipment/index.html.md)
- [list](https://docs.medusajs.com/references/js_sdk/admin/FulfillmentProvider/methods/js_sdk.admin.FulfillmentProvider.list/index.html.md)
- [listFulfillmentOptions](https://docs.medusajs.com/references/js_sdk/admin/FulfillmentProvider/methods/js_sdk.admin.FulfillmentProvider.listFulfillmentOptions/index.html.md)
- [batchInventoryItemLocationLevels](https://docs.medusajs.com/references/js_sdk/admin/InventoryItem/methods/js_sdk.admin.InventoryItem.batchInventoryItemLocationLevels/index.html.md)
- [create](https://docs.medusajs.com/references/js_sdk/admin/InventoryItem/methods/js_sdk.admin.InventoryItem.create/index.html.md)
- [batchInventoryItemsLocationLevels](https://docs.medusajs.com/references/js_sdk/admin/InventoryItem/methods/js_sdk.admin.InventoryItem.batchInventoryItemsLocationLevels/index.html.md)
- [batchUpdateLevels](https://docs.medusajs.com/references/js_sdk/admin/InventoryItem/methods/js_sdk.admin.InventoryItem.batchUpdateLevels/index.html.md)
- [delete](https://docs.medusajs.com/references/js_sdk/admin/InventoryItem/methods/js_sdk.admin.InventoryItem.delete/index.html.md)
- [deleteLevel](https://docs.medusajs.com/references/js_sdk/admin/InventoryItem/methods/js_sdk.admin.InventoryItem.deleteLevel/index.html.md)
- [list](https://docs.medusajs.com/references/js_sdk/admin/InventoryItem/methods/js_sdk.admin.InventoryItem.list/index.html.md)
- [listLevels](https://docs.medusajs.com/references/js_sdk/admin/InventoryItem/methods/js_sdk.admin.InventoryItem.listLevels/index.html.md)
- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/InventoryItem/methods/js_sdk.admin.InventoryItem.retrieve/index.html.md)
- [updateLevel](https://docs.medusajs.com/references/js_sdk/admin/InventoryItem/methods/js_sdk.admin.InventoryItem.updateLevel/index.html.md)
- [createServiceZone](https://docs.medusajs.com/references/js_sdk/admin/FulfillmentSet/methods/js_sdk.admin.FulfillmentSet.createServiceZone/index.html.md)
- [update](https://docs.medusajs.com/references/js_sdk/admin/InventoryItem/methods/js_sdk.admin.InventoryItem.update/index.html.md)
- [delete](https://docs.medusajs.com/references/js_sdk/admin/FulfillmentSet/methods/js_sdk.admin.FulfillmentSet.delete/index.html.md)
- [updateServiceZone](https://docs.medusajs.com/references/js_sdk/admin/FulfillmentSet/methods/js_sdk.admin.FulfillmentSet.updateServiceZone/index.html.md)
- [deleteServiceZone](https://docs.medusajs.com/references/js_sdk/admin/FulfillmentSet/methods/js_sdk.admin.FulfillmentSet.deleteServiceZone/index.html.md)
- [retrieveServiceZone](https://docs.medusajs.com/references/js_sdk/admin/FulfillmentSet/methods/js_sdk.admin.FulfillmentSet.retrieveServiceZone/index.html.md)
- [accept](https://docs.medusajs.com/references/js_sdk/admin/Invite/methods/js_sdk.admin.Invite.accept/index.html.md)
- [create](https://docs.medusajs.com/references/js_sdk/admin/Invite/methods/js_sdk.admin.Invite.create/index.html.md)
- [delete](https://docs.medusajs.com/references/js_sdk/admin/Invite/methods/js_sdk.admin.Invite.delete/index.html.md)
- [list](https://docs.medusajs.com/references/js_sdk/admin/Invite/methods/js_sdk.admin.Invite.list/index.html.md)
- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/Invite/methods/js_sdk.admin.Invite.retrieve/index.html.md)
- [resend](https://docs.medusajs.com/references/js_sdk/admin/Invite/methods/js_sdk.admin.Invite.resend/index.html.md)
- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/Notification/methods/js_sdk.admin.Notification.retrieve/index.html.md)
- [list](https://docs.medusajs.com/references/js_sdk/admin/Notification/methods/js_sdk.admin.Notification.list/index.html.md)
- [cancelTransfer](https://docs.medusajs.com/references/js_sdk/admin/Order/methods/js_sdk.admin.Order.cancelTransfer/index.html.md)
- [cancel](https://docs.medusajs.com/references/js_sdk/admin/Order/methods/js_sdk.admin.Order.cancel/index.html.md)
- [cancelFulfillment](https://docs.medusajs.com/references/js_sdk/admin/Order/methods/js_sdk.admin.Order.cancelFulfillment/index.html.md)
- [createCreditLine](https://docs.medusajs.com/references/js_sdk/admin/Order/methods/js_sdk.admin.Order.createCreditLine/index.html.md)
- [createShipment](https://docs.medusajs.com/references/js_sdk/admin/Order/methods/js_sdk.admin.Order.createShipment/index.html.md)
- [createFulfillment](https://docs.medusajs.com/references/js_sdk/admin/Order/methods/js_sdk.admin.Order.createFulfillment/index.html.md)
- [listChanges](https://docs.medusajs.com/references/js_sdk/admin/Order/methods/js_sdk.admin.Order.listChanges/index.html.md)
- [listLineItems](https://docs.medusajs.com/references/js_sdk/admin/Order/methods/js_sdk.admin.Order.listLineItems/index.html.md)
- [list](https://docs.medusajs.com/references/js_sdk/admin/Order/methods/js_sdk.admin.Order.list/index.html.md)
- [requestTransfer](https://docs.medusajs.com/references/js_sdk/admin/Order/methods/js_sdk.admin.Order.requestTransfer/index.html.md)
- [markAsDelivered](https://docs.medusajs.com/references/js_sdk/admin/Order/methods/js_sdk.admin.Order.markAsDelivered/index.html.md)
- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/Order/methods/js_sdk.admin.Order.retrieve/index.html.md)
- [retrievePreview](https://docs.medusajs.com/references/js_sdk/admin/Order/methods/js_sdk.admin.Order.retrievePreview/index.html.md)
- [capture](https://docs.medusajs.com/references/js_sdk/admin/Payment/methods/js_sdk.admin.Payment.capture/index.html.md)
- [update](https://docs.medusajs.com/references/js_sdk/admin/Order/methods/js_sdk.admin.Order.update/index.html.md)
- [list](https://docs.medusajs.com/references/js_sdk/admin/Payment/methods/js_sdk.admin.Payment.list/index.html.md)
- [refund](https://docs.medusajs.com/references/js_sdk/admin/Payment/methods/js_sdk.admin.Payment.refund/index.html.md)
- [listPaymentProviders](https://docs.medusajs.com/references/js_sdk/admin/Payment/methods/js_sdk.admin.Payment.listPaymentProviders/index.html.md)
- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/Payment/methods/js_sdk.admin.Payment.retrieve/index.html.md)
- [create](https://docs.medusajs.com/references/js_sdk/admin/PaymentCollection/methods/js_sdk.admin.PaymentCollection.create/index.html.md)
- [delete](https://docs.medusajs.com/references/js_sdk/admin/PaymentCollection/methods/js_sdk.admin.PaymentCollection.delete/index.html.md)
- [markAsPaid](https://docs.medusajs.com/references/js_sdk/admin/PaymentCollection/methods/js_sdk.admin.PaymentCollection.markAsPaid/index.html.md)
- [list](https://docs.medusajs.com/references/js_sdk/admin/Plugin/methods/js_sdk.admin.Plugin.list/index.html.md)
- [addItems](https://docs.medusajs.com/references/js_sdk/admin/OrderEdit/methods/js_sdk.admin.OrderEdit.addItems/index.html.md)
- [cancelRequest](https://docs.medusajs.com/references/js_sdk/admin/OrderEdit/methods/js_sdk.admin.OrderEdit.cancelRequest/index.html.md)
- [confirm](https://docs.medusajs.com/references/js_sdk/admin/OrderEdit/methods/js_sdk.admin.OrderEdit.confirm/index.html.md)
- [initiateRequest](https://docs.medusajs.com/references/js_sdk/admin/OrderEdit/methods/js_sdk.admin.OrderEdit.initiateRequest/index.html.md)
- [removeAddedItem](https://docs.medusajs.com/references/js_sdk/admin/OrderEdit/methods/js_sdk.admin.OrderEdit.removeAddedItem/index.html.md)
- [request](https://docs.medusajs.com/references/js_sdk/admin/OrderEdit/methods/js_sdk.admin.OrderEdit.request/index.html.md)
- [updateAddedItem](https://docs.medusajs.com/references/js_sdk/admin/OrderEdit/methods/js_sdk.admin.OrderEdit.updateAddedItem/index.html.md)
- [updateOriginalItem](https://docs.medusajs.com/references/js_sdk/admin/OrderEdit/methods/js_sdk.admin.OrderEdit.updateOriginalItem/index.html.md)
- [create](https://docs.medusajs.com/references/js_sdk/admin/PriceList/methods/js_sdk.admin.PriceList.create/index.html.md)
- [batchPrices](https://docs.medusajs.com/references/js_sdk/admin/PriceList/methods/js_sdk.admin.PriceList.batchPrices/index.html.md)
- [delete](https://docs.medusajs.com/references/js_sdk/admin/PriceList/methods/js_sdk.admin.PriceList.delete/index.html.md)
- [list](https://docs.medusajs.com/references/js_sdk/admin/PriceList/methods/js_sdk.admin.PriceList.list/index.html.md)
- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/PriceList/methods/js_sdk.admin.PriceList.retrieve/index.html.md)
- [linkProducts](https://docs.medusajs.com/references/js_sdk/admin/PriceList/methods/js_sdk.admin.PriceList.linkProducts/index.html.md)
- [update](https://docs.medusajs.com/references/js_sdk/admin/PriceList/methods/js_sdk.admin.PriceList.update/index.html.md)
- [batchVariantInventoryItems](https://docs.medusajs.com/references/js_sdk/admin/Product/methods/js_sdk.admin.Product.batchVariantInventoryItems/index.html.md)
- [batch](https://docs.medusajs.com/references/js_sdk/admin/Product/methods/js_sdk.admin.Product.batch/index.html.md)
- [batchVariants](https://docs.medusajs.com/references/js_sdk/admin/Product/methods/js_sdk.admin.Product.batchVariants/index.html.md)
- [confirmImport](https://docs.medusajs.com/references/js_sdk/admin/Product/methods/js_sdk.admin.Product.confirmImport/index.html.md)
- [create](https://docs.medusajs.com/references/js_sdk/admin/Product/methods/js_sdk.admin.Product.create/index.html.md)
- [createOption](https://docs.medusajs.com/references/js_sdk/admin/Product/methods/js_sdk.admin.Product.createOption/index.html.md)
- [createImport](https://docs.medusajs.com/references/js_sdk/admin/Product/methods/js_sdk.admin.Product.createImport/index.html.md)
- [createVariant](https://docs.medusajs.com/references/js_sdk/admin/Product/methods/js_sdk.admin.Product.createVariant/index.html.md)
- [deleteOption](https://docs.medusajs.com/references/js_sdk/admin/Product/methods/js_sdk.admin.Product.deleteOption/index.html.md)
- [delete](https://docs.medusajs.com/references/js_sdk/admin/Product/methods/js_sdk.admin.Product.delete/index.html.md)
- [deleteVariant](https://docs.medusajs.com/references/js_sdk/admin/Product/methods/js_sdk.admin.Product.deleteVariant/index.html.md)
- [export](https://docs.medusajs.com/references/js_sdk/admin/Product/methods/js_sdk.admin.Product.export/index.html.md)
- [listOptions](https://docs.medusajs.com/references/js_sdk/admin/Product/methods/js_sdk.admin.Product.listOptions/index.html.md)
- [list](https://docs.medusajs.com/references/js_sdk/admin/Product/methods/js_sdk.admin.Product.list/index.html.md)
- [import](https://docs.medusajs.com/references/js_sdk/admin/Product/methods/js_sdk.admin.Product.import/index.html.md)
- [retrieveVariant](https://docs.medusajs.com/references/js_sdk/admin/Product/methods/js_sdk.admin.Product.retrieveVariant/index.html.md)
- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/Product/methods/js_sdk.admin.Product.retrieve/index.html.md)
- [retrieveOption](https://docs.medusajs.com/references/js_sdk/admin/Product/methods/js_sdk.admin.Product.retrieveOption/index.html.md)
- [listVariants](https://docs.medusajs.com/references/js_sdk/admin/Product/methods/js_sdk.admin.Product.listVariants/index.html.md)
- [update](https://docs.medusajs.com/references/js_sdk/admin/Product/methods/js_sdk.admin.Product.update/index.html.md)
- [updateOption](https://docs.medusajs.com/references/js_sdk/admin/Product/methods/js_sdk.admin.Product.updateOption/index.html.md)
- [updateVariant](https://docs.medusajs.com/references/js_sdk/admin/Product/methods/js_sdk.admin.Product.updateVariant/index.html.md)
- [create](https://docs.medusajs.com/references/js_sdk/admin/ProductCategory/methods/js_sdk.admin.ProductCategory.create/index.html.md)
- [list](https://docs.medusajs.com/references/js_sdk/admin/ProductCategory/methods/js_sdk.admin.ProductCategory.list/index.html.md)
- [delete](https://docs.medusajs.com/references/js_sdk/admin/ProductCategory/methods/js_sdk.admin.ProductCategory.delete/index.html.md)
- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/ProductCategory/methods/js_sdk.admin.ProductCategory.retrieve/index.html.md)
- [update](https://docs.medusajs.com/references/js_sdk/admin/ProductCategory/methods/js_sdk.admin.ProductCategory.update/index.html.md)
- [create](https://docs.medusajs.com/references/js_sdk/admin/PricePreference/methods/js_sdk.admin.PricePreference.create/index.html.md)
- [updateProducts](https://docs.medusajs.com/references/js_sdk/admin/ProductCategory/methods/js_sdk.admin.ProductCategory.updateProducts/index.html.md)
- [delete](https://docs.medusajs.com/references/js_sdk/admin/PricePreference/methods/js_sdk.admin.PricePreference.delete/index.html.md)
- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/PricePreference/methods/js_sdk.admin.PricePreference.retrieve/index.html.md)
- [list](https://docs.medusajs.com/references/js_sdk/admin/PricePreference/methods/js_sdk.admin.PricePreference.list/index.html.md)
- [update](https://docs.medusajs.com/references/js_sdk/admin/PricePreference/methods/js_sdk.admin.PricePreference.update/index.html.md)
- [delete](https://docs.medusajs.com/references/js_sdk/admin/ProductTag/methods/js_sdk.admin.ProductTag.delete/index.html.md)
- [create](https://docs.medusajs.com/references/js_sdk/admin/ProductTag/methods/js_sdk.admin.ProductTag.create/index.html.md)
- [list](https://docs.medusajs.com/references/js_sdk/admin/ProductTag/methods/js_sdk.admin.ProductTag.list/index.html.md)
- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/ProductTag/methods/js_sdk.admin.ProductTag.retrieve/index.html.md)
- [create](https://docs.medusajs.com/references/js_sdk/admin/ProductType/methods/js_sdk.admin.ProductType.create/index.html.md)
- [update](https://docs.medusajs.com/references/js_sdk/admin/ProductTag/methods/js_sdk.admin.ProductTag.update/index.html.md)
- [list](https://docs.medusajs.com/references/js_sdk/admin/ProductType/methods/js_sdk.admin.ProductType.list/index.html.md)
- [delete](https://docs.medusajs.com/references/js_sdk/admin/ProductType/methods/js_sdk.admin.ProductType.delete/index.html.md)
- [update](https://docs.medusajs.com/references/js_sdk/admin/ProductType/methods/js_sdk.admin.ProductType.update/index.html.md)
- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/ProductType/methods/js_sdk.admin.ProductType.retrieve/index.html.md)
- [create](https://docs.medusajs.com/references/js_sdk/admin/ProductCollection/methods/js_sdk.admin.ProductCollection.create/index.html.md)
- [delete](https://docs.medusajs.com/references/js_sdk/admin/ProductCollection/methods/js_sdk.admin.ProductCollection.delete/index.html.md)
- [list](https://docs.medusajs.com/references/js_sdk/admin/ProductCollection/methods/js_sdk.admin.ProductCollection.list/index.html.md)
- [update](https://docs.medusajs.com/references/js_sdk/admin/ProductCollection/methods/js_sdk.admin.ProductCollection.update/index.html.md)
- [updateProducts](https://docs.medusajs.com/references/js_sdk/admin/ProductCollection/methods/js_sdk.admin.ProductCollection.updateProducts/index.html.md)
- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/ProductCollection/methods/js_sdk.admin.ProductCollection.retrieve/index.html.md)
- [list](https://docs.medusajs.com/references/js_sdk/admin/ProductVariant/methods/js_sdk.admin.ProductVariant.list/index.html.md)
- [create](https://docs.medusajs.com/references/js_sdk/admin/Reservation/methods/js_sdk.admin.Reservation.create/index.html.md)
- [delete](https://docs.medusajs.com/references/js_sdk/admin/Reservation/methods/js_sdk.admin.Reservation.delete/index.html.md)
- [list](https://docs.medusajs.com/references/js_sdk/admin/Reservation/methods/js_sdk.admin.Reservation.list/index.html.md)
- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/Reservation/methods/js_sdk.admin.Reservation.retrieve/index.html.md)
- [addRules](https://docs.medusajs.com/references/js_sdk/admin/Promotion/methods/js_sdk.admin.Promotion.addRules/index.html.md)
- [create](https://docs.medusajs.com/references/js_sdk/admin/Promotion/methods/js_sdk.admin.Promotion.create/index.html.md)
- [delete](https://docs.medusajs.com/references/js_sdk/admin/Promotion/methods/js_sdk.admin.Promotion.delete/index.html.md)
- [list](https://docs.medusajs.com/references/js_sdk/admin/Promotion/methods/js_sdk.admin.Promotion.list/index.html.md)
- [listRuleAttributes](https://docs.medusajs.com/references/js_sdk/admin/Promotion/methods/js_sdk.admin.Promotion.listRuleAttributes/index.html.md)
- [update](https://docs.medusajs.com/references/js_sdk/admin/Reservation/methods/js_sdk.admin.Reservation.update/index.html.md)
- [listRuleValues](https://docs.medusajs.com/references/js_sdk/admin/Promotion/methods/js_sdk.admin.Promotion.listRuleValues/index.html.md)
- [listRules](https://docs.medusajs.com/references/js_sdk/admin/Promotion/methods/js_sdk.admin.Promotion.listRules/index.html.md)
- [removeRules](https://docs.medusajs.com/references/js_sdk/admin/Promotion/methods/js_sdk.admin.Promotion.removeRules/index.html.md)
- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/Promotion/methods/js_sdk.admin.Promotion.retrieve/index.html.md)
- [update](https://docs.medusajs.com/references/js_sdk/admin/Promotion/methods/js_sdk.admin.Promotion.update/index.html.md)
- [updateRules](https://docs.medusajs.com/references/js_sdk/admin/Promotion/methods/js_sdk.admin.Promotion.updateRules/index.html.md)
- [list](https://docs.medusajs.com/references/js_sdk/admin/RefundReason/methods/js_sdk.admin.RefundReason.list/index.html.md)
- [create](https://docs.medusajs.com/references/js_sdk/admin/Region/methods/js_sdk.admin.Region.create/index.html.md)
- [delete](https://docs.medusajs.com/references/js_sdk/admin/Region/methods/js_sdk.admin.Region.delete/index.html.md)
- [list](https://docs.medusajs.com/references/js_sdk/admin/Region/methods/js_sdk.admin.Region.list/index.html.md)
- [update](https://docs.medusajs.com/references/js_sdk/admin/Region/methods/js_sdk.admin.Region.update/index.html.md)
- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/Region/methods/js_sdk.admin.Region.retrieve/index.html.md)
- [addReturnItem](https://docs.medusajs.com/references/js_sdk/admin/Return/methods/js_sdk.admin.Return.addReturnItem/index.html.md)
- [cancelReceive](https://docs.medusajs.com/references/js_sdk/admin/Return/methods/js_sdk.admin.Return.cancelReceive/index.html.md)
- [cancel](https://docs.medusajs.com/references/js_sdk/admin/Return/methods/js_sdk.admin.Return.cancel/index.html.md)
- [addReturnShipping](https://docs.medusajs.com/references/js_sdk/admin/Return/methods/js_sdk.admin.Return.addReturnShipping/index.html.md)
- [confirmReceive](https://docs.medusajs.com/references/js_sdk/admin/Return/methods/js_sdk.admin.Return.confirmReceive/index.html.md)
- [cancelRequest](https://docs.medusajs.com/references/js_sdk/admin/Return/methods/js_sdk.admin.Return.cancelRequest/index.html.md)
- [deleteReturnShipping](https://docs.medusajs.com/references/js_sdk/admin/Return/methods/js_sdk.admin.Return.deleteReturnShipping/index.html.md)
- [dismissItems](https://docs.medusajs.com/references/js_sdk/admin/Return/methods/js_sdk.admin.Return.dismissItems/index.html.md)
- [confirmRequest](https://docs.medusajs.com/references/js_sdk/admin/Return/methods/js_sdk.admin.Return.confirmRequest/index.html.md)
- [initiateRequest](https://docs.medusajs.com/references/js_sdk/admin/Return/methods/js_sdk.admin.Return.initiateRequest/index.html.md)
- [initiateReceive](https://docs.medusajs.com/references/js_sdk/admin/Return/methods/js_sdk.admin.Return.initiateReceive/index.html.md)
- [removeDismissItem](https://docs.medusajs.com/references/js_sdk/admin/Return/methods/js_sdk.admin.Return.removeDismissItem/index.html.md)
- [list](https://docs.medusajs.com/references/js_sdk/admin/Return/methods/js_sdk.admin.Return.list/index.html.md)
- [removeReceiveItem](https://docs.medusajs.com/references/js_sdk/admin/Return/methods/js_sdk.admin.Return.removeReceiveItem/index.html.md)
- [removeReturnItem](https://docs.medusajs.com/references/js_sdk/admin/Return/methods/js_sdk.admin.Return.removeReturnItem/index.html.md)
- [updateDismissItem](https://docs.medusajs.com/references/js_sdk/admin/Return/methods/js_sdk.admin.Return.updateDismissItem/index.html.md)
- [updateReceiveItem](https://docs.medusajs.com/references/js_sdk/admin/Return/methods/js_sdk.admin.Return.updateReceiveItem/index.html.md)
- [receiveItems](https://docs.medusajs.com/references/js_sdk/admin/Return/methods/js_sdk.admin.Return.receiveItems/index.html.md)
- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/Return/methods/js_sdk.admin.Return.retrieve/index.html.md)
- [updateRequest](https://docs.medusajs.com/references/js_sdk/admin/Return/methods/js_sdk.admin.Return.updateRequest/index.html.md)
- [updateReturnItem](https://docs.medusajs.com/references/js_sdk/admin/Return/methods/js_sdk.admin.Return.updateReturnItem/index.html.md)
- [updateReturnShipping](https://docs.medusajs.com/references/js_sdk/admin/Return/methods/js_sdk.admin.Return.updateReturnShipping/index.html.md)
- [create](https://docs.medusajs.com/references/js_sdk/admin/ReturnReason/methods/js_sdk.admin.ReturnReason.create/index.html.md)
- [delete](https://docs.medusajs.com/references/js_sdk/admin/ReturnReason/methods/js_sdk.admin.ReturnReason.delete/index.html.md)
- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/ReturnReason/methods/js_sdk.admin.ReturnReason.retrieve/index.html.md)
- [update](https://docs.medusajs.com/references/js_sdk/admin/ReturnReason/methods/js_sdk.admin.ReturnReason.update/index.html.md)
- [batchProducts](https://docs.medusajs.com/references/js_sdk/admin/SalesChannel/methods/js_sdk.admin.SalesChannel.batchProducts/index.html.md)
- [create](https://docs.medusajs.com/references/js_sdk/admin/SalesChannel/methods/js_sdk.admin.SalesChannel.create/index.html.md)
- [delete](https://docs.medusajs.com/references/js_sdk/admin/SalesChannel/methods/js_sdk.admin.SalesChannel.delete/index.html.md)
- [list](https://docs.medusajs.com/references/js_sdk/admin/SalesChannel/methods/js_sdk.admin.SalesChannel.list/index.html.md)
- [updateProducts](https://docs.medusajs.com/references/js_sdk/admin/SalesChannel/methods/js_sdk.admin.SalesChannel.updateProducts/index.html.md)
- [list](https://docs.medusajs.com/references/js_sdk/admin/ReturnReason/methods/js_sdk.admin.ReturnReason.list/index.html.md)
- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/SalesChannel/methods/js_sdk.admin.SalesChannel.retrieve/index.html.md)
- [update](https://docs.medusajs.com/references/js_sdk/admin/SalesChannel/methods/js_sdk.admin.SalesChannel.update/index.html.md)
- [createFulfillmentSet](https://docs.medusajs.com/references/js_sdk/admin/StockLocation/methods/js_sdk.admin.StockLocation.createFulfillmentSet/index.html.md)
- [create](https://docs.medusajs.com/references/js_sdk/admin/StockLocation/methods/js_sdk.admin.StockLocation.create/index.html.md)
- [delete](https://docs.medusajs.com/references/js_sdk/admin/StockLocation/methods/js_sdk.admin.StockLocation.delete/index.html.md)
- [list](https://docs.medusajs.com/references/js_sdk/admin/StockLocation/methods/js_sdk.admin.StockLocation.list/index.html.md)
- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/StockLocation/methods/js_sdk.admin.StockLocation.retrieve/index.html.md)
- [update](https://docs.medusajs.com/references/js_sdk/admin/StockLocation/methods/js_sdk.admin.StockLocation.update/index.html.md)
- [updateFulfillmentProviders](https://docs.medusajs.com/references/js_sdk/admin/StockLocation/methods/js_sdk.admin.StockLocation.updateFulfillmentProviders/index.html.md)
- [updateSalesChannels](https://docs.medusajs.com/references/js_sdk/admin/StockLocation/methods/js_sdk.admin.StockLocation.updateSalesChannels/index.html.md)
- [delete](https://docs.medusajs.com/references/js_sdk/admin/ShippingOption/methods/js_sdk.admin.ShippingOption.delete/index.html.md)
- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/ShippingOption/methods/js_sdk.admin.ShippingOption.retrieve/index.html.md)
- [list](https://docs.medusajs.com/references/js_sdk/admin/ShippingOption/methods/js_sdk.admin.ShippingOption.list/index.html.md)
- [update](https://docs.medusajs.com/references/js_sdk/admin/ShippingOption/methods/js_sdk.admin.ShippingOption.update/index.html.md)
- [updateRules](https://docs.medusajs.com/references/js_sdk/admin/ShippingOption/methods/js_sdk.admin.ShippingOption.updateRules/index.html.md)
- [create](https://docs.medusajs.com/references/js_sdk/admin/ShippingOption/methods/js_sdk.admin.ShippingOption.create/index.html.md)
- [create](https://docs.medusajs.com/references/js_sdk/admin/ShippingProfile/methods/js_sdk.admin.ShippingProfile.create/index.html.md)
- [list](https://docs.medusajs.com/references/js_sdk/admin/ShippingProfile/methods/js_sdk.admin.ShippingProfile.list/index.html.md)
- [delete](https://docs.medusajs.com/references/js_sdk/admin/ShippingProfile/methods/js_sdk.admin.ShippingProfile.delete/index.html.md)
- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/ShippingProfile/methods/js_sdk.admin.ShippingProfile.retrieve/index.html.md)
- [update](https://docs.medusajs.com/references/js_sdk/admin/ShippingProfile/methods/js_sdk.admin.ShippingProfile.update/index.html.md)
- [list](https://docs.medusajs.com/references/js_sdk/admin/Store/methods/js_sdk.admin.Store.list/index.html.md)
- [update](https://docs.medusajs.com/references/js_sdk/admin/Store/methods/js_sdk.admin.Store.update/index.html.md)
- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/Store/methods/js_sdk.admin.Store.retrieve/index.html.md)
- [delete](https://docs.medusajs.com/references/js_sdk/admin/TaxRegion/methods/js_sdk.admin.TaxRegion.delete/index.html.md)
- [create](https://docs.medusajs.com/references/js_sdk/admin/TaxRegion/methods/js_sdk.admin.TaxRegion.create/index.html.md)
- [list](https://docs.medusajs.com/references/js_sdk/admin/TaxRegion/methods/js_sdk.admin.TaxRegion.list/index.html.md)
- [update](https://docs.medusajs.com/references/js_sdk/admin/TaxRegion/methods/js_sdk.admin.TaxRegion.update/index.html.md)
- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/TaxRegion/methods/js_sdk.admin.TaxRegion.retrieve/index.html.md)
- [list](https://docs.medusajs.com/references/js_sdk/admin/TaxProvider/methods/js_sdk.admin.TaxProvider.list/index.html.md)
- [create](https://docs.medusajs.com/references/js_sdk/admin/Upload/methods/js_sdk.admin.Upload.create/index.html.md)
- [presignedUrl](https://docs.medusajs.com/references/js_sdk/admin/Upload/methods/js_sdk.admin.Upload.presignedUrl/index.html.md)
- [delete](https://docs.medusajs.com/references/js_sdk/admin/Upload/methods/js_sdk.admin.Upload.delete/index.html.md)
- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/Upload/methods/js_sdk.admin.Upload.retrieve/index.html.md)
- [create](https://docs.medusajs.com/references/js_sdk/admin/TaxRate/methods/js_sdk.admin.TaxRate.create/index.html.md)
- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/TaxRate/methods/js_sdk.admin.TaxRate.retrieve/index.html.md)
- [update](https://docs.medusajs.com/references/js_sdk/admin/TaxRate/methods/js_sdk.admin.TaxRate.update/index.html.md)
- [list](https://docs.medusajs.com/references/js_sdk/admin/TaxRate/methods/js_sdk.admin.TaxRate.list/index.html.md)
- [delete](https://docs.medusajs.com/references/js_sdk/admin/User/methods/js_sdk.admin.User.delete/index.html.md)
- [me](https://docs.medusajs.com/references/js_sdk/admin/User/methods/js_sdk.admin.User.me/index.html.md)
- [delete](https://docs.medusajs.com/references/js_sdk/admin/TaxRate/methods/js_sdk.admin.TaxRate.delete/index.html.md)
- [list](https://docs.medusajs.com/references/js_sdk/admin/User/methods/js_sdk.admin.User.list/index.html.md)
- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/User/methods/js_sdk.admin.User.retrieve/index.html.md)
- [update](https://docs.medusajs.com/references/js_sdk/admin/User/methods/js_sdk.admin.User.update/index.html.md)
- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/WorkflowExecution/methods/js_sdk.admin.WorkflowExecution.retrieve/index.html.md)
- [list](https://docs.medusajs.com/references/js_sdk/admin/WorkflowExecution/methods/js_sdk.admin.WorkflowExecution.list/index.html.md)


## JS SDK Auth

- [callback](https://docs.medusajs.com/references/js-sdk/auth/callback/index.html.md)
- [login](https://docs.medusajs.com/references/js-sdk/auth/login/index.html.md)
- [resetPassword](https://docs.medusajs.com/references/js-sdk/auth/resetPassword/index.html.md)
- [register](https://docs.medusajs.com/references/js-sdk/auth/register/index.html.md)
- [logout](https://docs.medusajs.com/references/js-sdk/auth/logout/index.html.md)
- [refresh](https://docs.medusajs.com/references/js-sdk/auth/refresh/index.html.md)
- [updateProvider](https://docs.medusajs.com/references/js-sdk/auth/updateProvider/index.html.md)


## JS SDK Store

- [cart](https://docs.medusajs.com/references/js-sdk/store/cart/index.html.md)
- [category](https://docs.medusajs.com/references/js-sdk/store/category/index.html.md)
- [fulfillment](https://docs.medusajs.com/references/js-sdk/store/fulfillment/index.html.md)
- [customer](https://docs.medusajs.com/references/js-sdk/store/customer/index.html.md)
- [order](https://docs.medusajs.com/references/js-sdk/store/order/index.html.md)
- [payment](https://docs.medusajs.com/references/js-sdk/store/payment/index.html.md)
- [collection](https://docs.medusajs.com/references/js-sdk/store/collection/index.html.md)
- [product](https://docs.medusajs.com/references/js-sdk/store/product/index.html.md)
- [region](https://docs.medusajs.com/references/js-sdk/store/region/index.html.md)


# Admin Components & Layouts

In this section of the documentation, you'll learn how to implement common Medusa Admin components and layouts.

These guides are useful to build components and layouts that follow the same design conventions as the Medusa Admin. The components and layouts are built on top of the [Medusa UI package](https://docs.medusajs.com/ui/index.html.md).

Refer to the [Medusa UI documentation](https://docs.medusajs.com/ui/index.html.md) for a full list of components.

## Layouts

These layout components allow you to set the layout of your [UI routes](https://docs.medusajs.com/docs/learn/fundamentals/admin/ui-routes/index.html.md) similar to the layouts used in the Medusa Admin.

***

## Components

These components allow you to use common Medusa Admin components in your custom UI routes and widgets.


# Action Menu - Admin Components

The Medusa Admin often provides additional actions in a dropdown shown when users click a three-dot icon.

![Example of an action menu in the Medusa Admin](https://res.cloudinary.com/dza7lstvk/image/upload/v1728291319/Medusa%20Resources/action-menu_jnus6k.png)

To create a component that shows this menu in your customizations, create the file `src/admin/components/action-menu.tsx` with the following content:

```tsx title="src/admin/components/action-menu.tsx"
import { 
  DropdownMenu,
  IconButton,
  clx,
} from "@medusajs/ui"
import { EllipsisHorizontal } from "@medusajs/icons"
import { Link } from "react-router-dom"

export type Action = {
  icon: React.ReactNode
  label: string
  disabled?: boolean
} & (
  | {
      to: string
      onClick?: never
    }
  | {
      onClick: () => void
      to?: never
    }
)

export type ActionGroup = {
  actions: Action[]
}

export type ActionMenuProps = {
  groups: ActionGroup[]
}

export const ActionMenu = ({ groups }: ActionMenuProps) => {
  return (
    <DropdownMenu>
      <DropdownMenu.Trigger asChild>
        <IconButton size="small" variant="transparent">
          <EllipsisHorizontal />
        </IconButton>
      </DropdownMenu.Trigger>
      <DropdownMenu.Content>
        {groups.map((group, index) => {
          if (!group.actions.length) {
            return null
          }

          const isLast = index === groups.length - 1

          return (
            <DropdownMenu.Group key={index}>
              {group.actions.map((action, index) => {
                if (action.onClick) {
                  return (
                    <DropdownMenu.Item
                      disabled={action.disabled}
                      key={index}
                      onClick={(e) => {
                        e.stopPropagation()
                        action.onClick()
                      }}
                      className={clx(
                        "[&_svg]:text-ui-fg-subtle flex items-center gap-x-2",
                        {
                          "[&_svg]:text-ui-fg-disabled": action.disabled,
                        }
                      )}
                    >
                      {action.icon}
                      <span>{action.label}</span>
                    </DropdownMenu.Item>
                  )
                }

                return (
                  <div key={index}>
                    <DropdownMenu.Item
                      className={clx(
                        "[&_svg]:text-ui-fg-subtle flex items-center gap-x-2",
                        {
                          "[&_svg]:text-ui-fg-disabled": action.disabled,
                        }
                      )}
                      asChild
                      disabled={action.disabled}
                    >
                      <Link to={action.to} onClick={(e) => e.stopPropagation()}>
                        {action.icon}
                        <span>{action.label}</span>
                      </Link>
                    </DropdownMenu.Item>
                  </div>
                )
              })}
              {!isLast && <DropdownMenu.Separator />}
            </DropdownMenu.Group>
          )
        })}
      </DropdownMenu.Content>
    </DropdownMenu>
  )
}
```

The `ActionMenu` component shows a three-dots icon (or `EllipsisHorizontal`) from the [Medusa Icons package](https://docs.medusajs.com/ui/icons/overview/index.html.md) in a button.

When the button is clicked, a dropdown menu is shown with the actions passed in the props.

The component accepts the following props:

- groups: (\`object\[]\`) Groups of actions to be shown in the dropdown. Each group is separated by a divider.

  - actions: (\`object\[]\`) Actions in the group.

    - icon: (\`React.ReactNode\`)

    - label: (\`string\`) The action's text.

    - disabled: (\`boolean\`) Whether the action is shown as disabled.

    - \`to\`: (\`string\`) The link to take the user to when they click the action. This is required if \`onClick\` isn't provided.

    - \`onClick\`: (\`() => void\`) The function to execute when the action is clicked. This is required if \`to\` isn't provided.

***

## Example

Use the `ActionMenu` component in any widget or UI route.

For example, create the widget `src/admin/widgets/product-widget.tsx` with the following content:

```tsx title="src/admin/widgets/product-widget.tsx"
import { defineWidgetConfig } from "@medusajs/admin-sdk"
import { Pencil } from "@medusajs/icons"
import { Container } from "../components/container"
import { ActionMenu } from "../components/action-menu"

const ProductWidget = () => {
  return (
    <Container>
      <ActionMenu groups={[
        {
          actions: [
            {
              icon: <Pencil />,
              label: "Edit",
              onClick: () => {
                alert("You clicked the edit action!")
              },
            },
          ],
        },
      ]} />
    </Container>
  )
}

export const config = defineWidgetConfig({
  zone: "product.details.before",
})

export default ProductWidget
```

This widget also uses a [Container](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/admin-components/components/container/index.html.md) custom component.

### Use in Header

You can also use the action menu in the [Header](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/admin-components/components/header/index.html.md) component as part of its actions.

For example:

```tsx title="src/admin/widgets/product-widget.tsx"
import { defineWidgetConfig } from "@medusajs/admin-sdk"
import { Pencil } from "@medusajs/icons"
import { Container } from "../components/container"
import { Header } from "../components/header"

const ProductWidget = () => {
  return (
    <Container>
      <Header 
        title="Product Widget"
        subtitle="This is my custom product widget"
        actions={[
          {
            type: "action-menu",
            props: {
              groups: [
                {
                  actions: [
                    {
                      icon: <Pencil />,
                      label: "Edit",
                      onClick: () => {
                        alert("You clicked the edit action!")
                      },
                    },
                  ],
                },
              ],
            },
          },
        ]}
      />
    </Container>
  )
}

export const config = defineWidgetConfig({
  zone: "product.details.before",
})

export default ProductWidget
```


# Container - Admin Components

The Medusa Admin wraps each section of a page in a container.

![Example of a container in the Medusa Admin](https://res.cloudinary.com/dza7lstvk/image/upload/v1728287102/Medusa%20Resources/container_soenir.png)

To create a component that uses the same container styling in your widgets or UI routes, create the file `src/admin/components/container.tsx` with the following content:

```tsx
import { 
  Container as UiContainer,
  clx,
} from "@medusajs/ui"

type ContainerProps = React.ComponentProps<typeof UiContainer>

export const Container = (props: ContainerProps) => {
  return (
    <UiContainer {...props} className={clx(
      "divide-y p-0",
      props.className
    )} />
  )
}
```

The `Container` component re-uses the component from the [Medusa UI package](https://docs.medusajs.com/ui/components/container/index.html.md) and applies to it classes to match the Medusa Admin's design conventions.

***

## Example

Use that `Container` component in any widget or UI route.

For example, create the widget `src/admin/widgets/product-widget.tsx` with the following content:

```tsx title="src/admin/widgets/product-widget.tsx"
import { defineWidgetConfig } from "@medusajs/admin-sdk"
import { Container } from "../components/container"
import { Header } from "../components/header"

const ProductWidget = () => {
  return (
    <Container>
      <Header title="Product Widget" />
    </Container>
  )
}

export const config = defineWidgetConfig({
  zone: "product.details.before",
})

export default ProductWidget
```

This widget also uses a [Header](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/admin-components/components/header/index.html.md) custom component.


# Header - Admin Components

Each section in the Medusa Admin has a header with a title, and optionally a subtitle with buttons to perform an action.

![Example of a header in a section](https://res.cloudinary.com/dza7lstvk/image/upload/v1728288562/Medusa%20Resources/header_dtz4gl.png)

To create a component that uses the same header styling and structure, create the file `src/admin/components/header.tsx` with the following content:

```tsx title="src/admin/components/header.tsx"
import { Heading, Button, Text } from "@medusajs/ui"
import React from "react"
import { Link, LinkProps } from "react-router-dom"
import { ActionMenu, ActionMenuProps } from "./action-menu"

export type HeadingProps = {
  title: string
  subtitle?: string
  actions?: (
    {
      type: "button",
      props: React.ComponentProps<typeof Button>
      link?: LinkProps
    } |  
    {
      type: "action-menu"
      props: ActionMenuProps
    } |
    {
      type: "custom"
      children: React.ReactNode
    }
  )[]
}

export const Header = ({
  title,
  subtitle,
  actions = [],
}: HeadingProps) => {
  return (
    <div className="flex items-center justify-between px-6 py-4">
      <div>
        <Heading level="h2">{title}</Heading>
        {subtitle && (
          <Text className="text-ui-fg-subtle" size="small">
            {subtitle}
          </Text>
        )}
      </div>
      {actions.length > 0 && (
        <div className="flex items-center justify-center gap-x-2">
          {actions.map((action, index) => (
            <>
              {action.type === "button" && (
                <Button 
                  {...action.props} 
                  size={action.props.size || "small"}
                  key={index}
                >
                  <>
                    {action.props.children}
                    {action.link && <Link {...action.link} />}
                  </>
                </Button>
              )}
              {action.type === "action-menu" && (
                <ActionMenu {...action.props} />
              )}
              {action.type === "custom" && action.children}
            </>
          ))}
        </div>
      )}
    </div>
  )
}
```

The `Header` component shows a title, and optionally a subtitle and action buttons.

The component also uses the [Action Menu](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/admin-components/components/action-menu/index.html.md) custom component.

It accepts the following props:

- title: (\`string\`) The section's title.
- subtitle: (\`string\`) The section's subtitle.
- actions: (\`object\[]\`) An array of actions to show.

  - type: (\`button\` \\| \`action-menu\` \\| \`custom\`) The type of action to add.

    \- If its value is \`button\`, it'll show a button that can have a link or an on-click action.

    \- If its value is \`action-menu\`, it'll show a three dot icon with a dropdown of actions.

    \- If its value is \`custom\`, you can pass any React nodes to render.

  - props: (object)

  - children: (React.ReactNode) This property is only accepted if \`type\` is \`custom\`. Its content is rendered as part of the actions.

***

## Example

Use the `Header` component in any widget or UI route.

For example, create the widget `src/admin/widgets/product-widget.tsx` with the following content:

```tsx title="src/admin/widgets/product-widget.tsx"
import { defineWidgetConfig } from "@medusajs/admin-sdk"
import { Container } from "../components/container"
import { Header } from "../components/header"

const ProductWidget = () => {
  return (
    <Container>
      <Header 
        title="Product Widget"
        subtitle="This is my custom product widget"
        actions={[
          {
            type: "button",
            props: {
              children: "Click me",
              variant: "secondary",
              onClick: () => {
                alert("You clicked the button.")
              },
            },
          },
        ]}
      />
    </Container>
  )
}

export const config = defineWidgetConfig({
  zone: "product.details.before",
})

export default ProductWidget
```

This widget also uses a [Container](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/admin-components/components/container/index.html.md) custom component.


# Forms - Admin Components

The Medusa Admin has two types of forms:

1. Create forms, created using the [FocusModal UI component](https://docs.medusajs.com/ui/components/focus-modal/index.html.md).
2. Edit or update forms, created using the [Drawer UI component](https://docs.medusajs.com/ui/components/drawer/index.html.md).

This guide explains how to create these two form types following the Medusa Admin's conventions.

## Form Tooling

The Medusa Admin uses the following tools to build the forms:

1. [react-hook-form](https://react-hook-form.com/) to easily build forms and manage their states.
2. [Zod](https://zod.dev/) to validate the form's fields.

Both of these libraries are available in your project, so you don't have to install them to use them.

***

## Create Form

In this section, you'll build a form component to create an item of a resource.

### Full Component

```tsx title="src/admin/components/create-form.tsx"
import { 
  FocusModal,
  Heading,
  Label,
  Input,
  Button,
} from "@medusajs/ui"
import { 
  useForm, 
  FormProvider,
  Controller,
} from "react-hook-form"
import * as zod from "zod"

const schema = zod.object({
  name: zod.string(),
})

export const CreateForm = () => {
  const form = useForm<zod.infer<typeof schema>>({
    defaultValues: {
      name: "",
    },
  })

  const handleSubmit = form.handleSubmit(({ name }) => {
    // TODO submit to backend
    console.log(name)
  })

  return (
    <FocusModal>
      <FocusModal.Trigger asChild>
        <Button>Create</Button>
      </FocusModal.Trigger>
      <FocusModal.Content>
        <FormProvider {...form}>
          <form
            onSubmit={handleSubmit}
            className="flex h-full flex-col overflow-hidden"
          >
            <FocusModal.Header>
              <div className="flex items-center justify-end gap-x-2">
                  <FocusModal.Close asChild>
                    <Button size="small" variant="secondary">
                      Cancel
                    </Button>
                  </FocusModal.Close>
                  <Button type="submit" size="small">
                    Save
                  </Button>
                </div>
            </FocusModal.Header>
            <FocusModal.Body>
                <div className="flex flex-1 flex-col items-center overflow-y-auto">
                  <div className="mx-auto flex w-full max-w-[720px] flex-col gap-y-8 px-2 py-16">
                    <div>
                      <Heading className="capitalize">
                        Create Item
                      </Heading>
                    </div>
                    <div className="grid grid-cols-2 gap-4">
                      <Controller
                        control={form.control}
                        name="name"
                        render={({ field }) => {
                          return (
                            <div className="flex flex-col space-y-2">
                              <div className="flex items-center gap-x-1">
                                <Label size="small" weight="plus">
                                  Name
                                </Label>
                              </div>
                              <Input {...field} />
                            </div>
                          )
                        }}
                      />
                    </div>
                  </div>
                </div>
            </FocusModal.Body>
          </form>
        </FormProvider>
      </FocusModal.Content>
    </FocusModal>
  )
}
```

Unlike other components in this documentation, this form component isn't reusable. You have to create one for every resource that has a create form in the admin.

Start by creating the file `src/admin/components/create-form.tsx` that you'll create the form in.

### Create Validation Schema

In `src/admin/components/create-form.tsx`, create a validation schema with Zod for the form's fields:

```tsx title="src/admin/components/create-form.tsx"
import * as zod from "zod"

const schema = zod.object({
  name: zod.string(),
})
```

The form in this guide is simple, it only has a required `name` field, which is a string.

### Initialize Form

Next, you'll initialize the form using `react-hook-form`.

Add to `src/admin/components/create-form.tsx` the following:

```tsx title="src/admin/components/create-form.tsx"
// other imports...
import { useForm } from "react-hook-form"

// validation schema...

export const CreateForm = () => {
  const form = useForm<zod.infer<typeof schema>>({
    defaultValues: {
      name: "",
    },
  })

  const handleSubmit = form.handleSubmit(({ name }) => {
    // TODO submit to backend
    console.log(name)
  })

  // TODO render form
}
```

You create the `CreateForm` component. For now, it uses `useForm` from `react-hook-form` to initialize a form.

You also define a `handleSubmit` function to perform an action when the form is submitted.

You can replace the content of the function with sending a request to Medusa's routes. Refer to [this guide](https://docs.medusajs.com/docs/learn/fundamentals/admin/tips#send-requests-to-api-routes/index.html.md) for more details on how to do that.

### Render Components

You'll now add a `return` statement that renders the focus modal where the form is shown.

Replace `// TODO render form` with the following:

```tsx title="src/admin/components/create-form.tsx"
// other imports...
import { 
  FocusModal,
  Heading,
  Label,
  Input,
  Button,
} from "@medusajs/ui"
import { 
  FormProvider,
  Controller,
} from "react-hook-form"

export const CreateForm = () => {
  // ...

  return (
    <FocusModal>
      <FocusModal.Trigger asChild>
        <Button>Create</Button>
      </FocusModal.Trigger>
      <FocusModal.Content>
        <FormProvider {...form}>
          <form
            onSubmit={handleSubmit}
            className="flex h-full flex-col overflow-hidden"
          >
            <FocusModal.Header>
              <div className="flex items-center justify-end gap-x-2">
                  <FocusModal.Close asChild>
                    <Button size="small" variant="secondary">
                      Cancel
                    </Button>
                  </FocusModal.Close>
                  <Button type="submit" size="small">
                    Save
                  </Button>
                </div>
            </FocusModal.Header>
            <FocusModal.Body>
                <div className="flex flex-1 flex-col items-center overflow-y-auto">
                  <div className="mx-auto flex w-full max-w-[720px] flex-col gap-y-8 px-2 py-16">
                    <div>
                      <Heading className="capitalize">
                        Create Item
                      </Heading>
                    </div>
                    <div className="grid grid-cols-2 gap-4">
                      <Controller
                        control={form.control}
                        name="name"
                        render={({ field }) => {
                          return (
                            <div className="flex flex-col space-y-2">
                              <div className="flex items-center gap-x-1">
                                <Label size="small" weight="plus">
                                  Name
                                </Label>
                              </div>
                              <Input {...field} />
                            </div>
                          )
                        }}
                      />
                    </div>
                  </div>
                </div>
            </FocusModal.Body>
          </form>
        </FormProvider>
      </FocusModal.Content>
    </FocusModal>
  )
}
```

You render a focus modal, with a trigger button to open it.

In the `FocusModal.Content` component, you wrap the content with the `FormProvider` component from `react-hook-form`, passing it the details of the form you initialized earlier as props.

In the `FormProvider`, you add a `form` component passing it the `handleSubmit` function you created earlier as the handler of the `onSubmit` event.

In the `FocusModal.Header` component, you add buttons to save or cancel the form submission.

Finally, you render the form's components inside the `FocusModal.Body`. To render inputs, you use the `Controller` component imported from `react-hook-form`.

### Use Create Form Component

You can use the `CreateForm` component in your widget or UI route.

For example, create the widget `src/admin/widgets/product-widget.tsx` with the following content:

```tsx title="src/admin/widgets/product-widget.tsx"
import { defineWidgetConfig } from "@medusajs/admin-sdk"
import { CreateForm } from "../components/create-form"
import { Container } from "../components/container"
import { Header } from "../components/header"

const ProductWidget = () => {
  return (
    <Container>
      <Header
        title="Items"
        actions={[
          {
            type: "custom",
            children: <CreateForm />,
          },
        ]}
      />
    </Container>
  )
}

export const config = defineWidgetConfig({
  zone: "product.details.before",
})

export default ProductWidget
```

This component uses the [Container](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/admin-components/components/container/index.html.md) and [Header](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/admin-components/components/header/index.html.md) custom components.

It will add at the top of a product's details page a new section, and in its header you'll find a Create button. If you click on it, it will open the focus modal with your form.

***

## Edit Form

In this section, you'll build a form component to edit an item of a resource.

### Full Component

```tsx title="src/admin/components/edit-form.tsx"
import { 
  Drawer,
  Heading,
  Label,
  Input,
  Button,
} from "@medusajs/ui"
import { 
  useForm, 
  FormProvider,
  Controller,
} from "react-hook-form"
import * as zod from "zod"

const schema = zod.object({
  name: zod.string(),
})

export const EditForm = () => {
  const form = useForm<zod.infer<typeof schema>>({
    defaultValues: {
      name: "",
    },
  })

  const handleSubmit = form.handleSubmit(({ name }) => {
    // TODO submit to backend
    console.log(name)
  })

  return (
    <Drawer>
      <Drawer.Trigger asChild>
        <Button>Edit Item</Button>
      </Drawer.Trigger>
      <Drawer.Content>
        <FormProvider {...form}>
          <form
            onSubmit={handleSubmit}
            className="flex flex-1 flex-col overflow-hidden"
          >
          <Drawer.Header>
            <Heading className="capitalize">
              Edit Item
            </Heading>
          </Drawer.Header>
          <Drawer.Body className="flex max-w-full flex-1 flex-col gap-y-8 overflow-y-auto">
            <Controller
              control={form.control}
              name="name"
              render={({ field }) => {
                return (
                  <div className="flex flex-col space-y-2">
                    <div className="flex items-center gap-x-1">
                      <Label size="small" weight="plus">
                        Name
                      </Label>
                    </div>
                    <Input {...field} />
                  </div>
                )
              }}
            />
          </Drawer.Body>
          <Drawer.Footer>
            <div className="flex items-center justify-end gap-x-2">
              <Drawer.Close asChild>
                <Button size="small" variant="secondary">
                  Cancel
                </Button>
              </Drawer.Close>
              <Button size="small" type="submit">
                Save
              </Button>
            </div>
          </Drawer.Footer>
          </form>
        </FormProvider>
      </Drawer.Content>
    </Drawer>
  )
}
```

Unlike other components in this documentation, this form component isn't reusable. You have to create one for every resource that has an edit form in the admin.

Start by creating the file `src/admin/components/edit-form.tsx` that you'll create the form in.

### Create Validation Schema

In `src/admin/components/edit-form.tsx`, create a validation schema with Zod for the form's fields:

```tsx title="src/admin/components/edit-form.tsx"
import * as zod from "zod"

const schema = zod.object({
  name: zod.string(),
})
```

The form in this guide is simple, it only has a required `name` field, which is a string.

### Initialize Form

Next, you'll initialize the form using `react-hook-form`.

Add to `src/admin/components/edit-form.tsx` the following:

```tsx title="src/admin/components/edit-form.tsx"
// other imports...
import { useForm } from "react-hook-form"

// validation schema...

export const EditForm = () => {
  const form = useForm<zod.infer<typeof schema>>({
    defaultValues: {
      name: "",
    },
  })

  const handleSubmit = form.handleSubmit(({ name }) => {
    // TODO submit to backend
    console.log(name)
  })

  // TODO render form
}
```

You create the `EditForm` component. For now, it uses `useForm` from `react-hook-form` to initialize a form.

You also define a `handleSubmit` function to perform an action when the form is submitted.

You can replace the content of the function with sending a request to Medusa's routes. Refer to [this guide](https://docs.medusajs.com/docs/learn/fundamentals/admin/tips#send-requests-to-api-routes/index.html.md) for more details on how to do that.

### Render Components

You'll now add a `return` statement that renders the drawer where the form is shown.

Replace `// TODO render form` with the following:

```tsx title="src/admin/components/edit-form.tsx"
// other imports...
import { 
  Drawer,
  Heading,
  Label,
  Input,
  Button,
} from "@medusajs/ui"
import { 
  FormProvider,
  Controller,
} from "react-hook-form"

export const EditForm = () => {
  // ...

  return (
    <Drawer>
      <Drawer.Trigger asChild>
        <Button>Edit Item</Button>
      </Drawer.Trigger>
      <Drawer.Content>
        <FormProvider {...form}>
          <form
            onSubmit={handleSubmit}
            className="flex flex-1 flex-col overflow-hidden"
          >
          <Drawer.Header>
            <Heading className="capitalize">
              Edit Item
            </Heading>
          </Drawer.Header>
          <Drawer.Body className="flex max-w-full flex-1 flex-col gap-y-8 overflow-y-auto">
            <Controller
              control={form.control}
              name="name"
              render={({ field }) => {
                return (
                  <div className="flex flex-col space-y-2">
                    <div className="flex items-center gap-x-1">
                      <Label size="small" weight="plus">
                        Name
                      </Label>
                    </div>
                    <Input {...field} />
                  </div>
                )
              }}
            />
          </Drawer.Body>
          <Drawer.Footer>
            <div className="flex items-center justify-end gap-x-2">
              <Drawer.Close asChild>
                <Button size="small" variant="secondary">
                  Cancel
                </Button>
              </Drawer.Close>
              <Button size="small" type="submit">
                Save
              </Button>
            </div>
          </Drawer.Footer>
          </form>
        </FormProvider>
      </Drawer.Content>
    </Drawer>
  )
}
```

You render a drawer, with a trigger button to open it.

In the `Drawer.Content` component, you wrap the content with the `FormProvider` component from `react-hook-form`, passing it the details of the form you initialized earlier as props.

In the `FormProvider`, you add a `form` component passing it the `handleSubmit` function you created earlier as the handler of the `onSubmit` event.

You render the form's components inside the `Drawer.Body`. To render inputs, you use the `Controller` component imported from `react-hook-form`.

Finally, in the `Drawer.Footer` component, you add buttons to save or cancel the form submission.

### Use Edit Form Component

You can use the `EditForm` component in your widget or UI route.

For example, create the widget `src/admin/widgets/product-widget.tsx` with the following content:

```tsx title="src/admin/widgets/product-widget.tsx"
import { defineWidgetConfig } from "@medusajs/admin-sdk"
import { Container } from "../components/container"
import { Header } from "../components/header"
import { EditForm } from "../components/edit-form"

const ProductWidget = () => {
  return (
    <Container>
      <Header
        title="Items"
        actions={[
          {
            type: "custom",
            children: <EditForm />,
          },
        ]}
      />
    </Container>
  )
}

export const config = defineWidgetConfig({
  zone: "product.details.before",
})

export default ProductWidget
```

This component uses the [Container](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/admin-components/components/container/index.html.md) and [Header](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/admin-components/components/header/index.html.md) custom components.

It will add at the top of a product's details page a new section, and in its header you'll find an "Edit Item" button. If you click on it, it will open the drawer with your form.


# Section Row - Admin Components

The Medusa Admin often shows information in rows of label-values, such as when showing a product's details.

![Example of a section row in the Medusa Admin](https://res.cloudinary.com/dza7lstvk/image/upload/v1728292781/Medusa%20Resources/section-row_kknbnw.png)

To create a component that shows information in the same structure, create the file `src/admin/components/section-row.tsx` with the following content:

```tsx title="src/admin/components/section-row.tsx"
import { Text, clx } from "@medusajs/ui"

export type SectionRowProps = {
  title: string
  value?: React.ReactNode | string | null
  actions?: React.ReactNode
}

export const SectionRow = ({ title, value, actions }: SectionRowProps) => {
  const isValueString = typeof value === "string" || !value

  return (
    <div
      className={clx(
        `text-ui-fg-subtle grid grid-cols-2 items-center px-6 py-4`,
        {
          "grid-cols-[1fr_1fr_28px]": !!actions,
        }
      )}
    >
      <Text size="small" weight="plus" leading="compact">
        {title}
      </Text>

      {isValueString ? (
        <Text
          size="small"
          leading="compact"
          className="whitespace-pre-line text-pretty"
        >
          {value ?? "-"}
        </Text>
      ) : (
        <div className="flex flex-wrap gap-1">{value}</div>
      )}

      {actions && <div>{actions}</div>}
    </div>
  )
}
```

The `SectionRow` component shows a title and a value in the same row.

It accepts the following props:

- title: (\`string\`) The title to show on the left side.
- value: (\`React.ReactNode\` \\| \`string\` \\| \`null\`) The value to show on the right side.
- actions: (\`React.ReactNode\`) The actions to show at the end of the row.

***

## Example

Use the `SectionRow` component in any widget or UI route.

For example, create the widget `src/admin/widgets/product-widget.tsx` with the following content:

```tsx title="src/admin/widgets/product-widget.tsx"
import { defineWidgetConfig } from "@medusajs/admin-sdk"
import { Container } from "../components/container"
import { Header } from "../components/header"
import { SectionRow } from "../components/section-row"

const ProductWidget = () => {
  return (
    <Container>
      <Header title="Product Widget" />
      <SectionRow title="Name" value="John" />
    </Container>
  )
}

export const config = defineWidgetConfig({
  zone: "product.details.before",
})

export default ProductWidget
```

This widget also uses the [Container](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/admin-components/components/container/index.html.md) and [Header](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/admin-components/components/header/index.html.md) custom component.


# JSON View - Admin Components

Detail pages in the Medusa Admin show a JSON section to view the current page's details in JSON format.

![Example of a JSON section in the admin](https://res.cloudinary.com/dza7lstvk/image/upload/v1728295129/Medusa%20Resources/json_dtbsgm.png)

To create a component that shows a JSON section in your customizations, create the file `src/admin/components/json-view-section.tsx` with the following content:

```tsx title="src/admin/components/json-view-section.tsx"
import {
  ArrowUpRightOnBox,
  Check,
  SquareTwoStack,
  TriangleDownMini,
  XMarkMini,
} from "@medusajs/icons"
import {
  Badge,
  Container,
  Drawer,
  Heading,
  IconButton,
  Kbd,
} from "@medusajs/ui"
import Primitive from "@uiw/react-json-view"
import { CSSProperties, MouseEvent, Suspense, useState } from "react"

type JsonViewSectionProps = {
  data: object
  title?: string
}

export const JsonViewSection = ({ data }: JsonViewSectionProps) => {
  const numberOfKeys = Object.keys(data).length

  return (
    <Container className="flex items-center justify-between px-6 py-4">
      <div className="flex items-center gap-x-4">
        <Heading level="h2">JSON</Heading>
        <Badge size="2xsmall" rounded="full">
          {numberOfKeys} keys
        </Badge>
      </div>
      <Drawer>
        <Drawer.Trigger asChild>
          <IconButton
            size="small"
            variant="transparent"
            className="text-ui-fg-muted hover:text-ui-fg-subtle"
          >
            <ArrowUpRightOnBox />
          </IconButton>
        </Drawer.Trigger>
        <Drawer.Content className="bg-ui-contrast-bg-base text-ui-code-fg-subtle !shadow-elevation-commandbar overflow-hidden border border-none max-md:inset-x-2 max-md:max-w-[calc(100%-16px)]">
          <div className="bg-ui-code-bg-base flex items-center justify-between px-6 py-4">
            <div className="flex items-center gap-x-4">
              <Drawer.Title asChild>
                <Heading className="text-ui-contrast-fg-primary">
                <span className="text-ui-fg-subtle">
                  {numberOfKeys}
                </span>
                </Heading>
              </Drawer.Title>
            </div>
            <div className="flex items-center gap-x-2">
              <Kbd className="bg-ui-contrast-bg-subtle border-ui-contrast-border-base text-ui-contrast-fg-secondary">
                esc
              </Kbd>
              <Drawer.Close asChild>
                <IconButton
                  size="small"
                  variant="transparent"
                  className="text-ui-contrast-fg-secondary hover:text-ui-contrast-fg-primary hover:bg-ui-contrast-bg-base-hover active:bg-ui-contrast-bg-base-pressed focus-visible:bg-ui-contrast-bg-base-hover focus-visible:shadow-borders-interactive-with-active"
                >
                  <XMarkMini />
                </IconButton>
              </Drawer.Close>
            </div>
          </div>
          <Drawer.Body className="flex flex-1 flex-col overflow-hidden px-[5px] py-0 pb-[5px]">
            <div className="bg-ui-contrast-bg-subtle flex-1 overflow-auto rounded-b-[4px] rounded-t-lg p-3">
              <Suspense
                fallback={<div className="flex size-full flex-col"></div>}
              >
                <Primitive
                  value={data}
                  displayDataTypes={false}
                  style={
                    {
                      "--w-rjv-font-family": "Roboto Mono, monospace",
                      "--w-rjv-line-color": "var(--contrast-border-base)",
                      "--w-rjv-curlybraces-color":
                        "var(--contrast-fg-secondary)",
                      "--w-rjv-brackets-color": "var(--contrast-fg-secondary)",
                      "--w-rjv-key-string": "var(--contrast-fg-primary)",
                      "--w-rjv-info-color": "var(--contrast-fg-secondary)",
                      "--w-rjv-type-string-color": "var(--tag-green-icon)",
                      "--w-rjv-quotes-string-color": "var(--tag-green-icon)",
                      "--w-rjv-type-boolean-color": "var(--tag-orange-icon)",
                      "--w-rjv-type-int-color": "var(--tag-orange-icon)",
                      "--w-rjv-type-float-color": "var(--tag-orange-icon)",
                      "--w-rjv-type-bigint-color": "var(--tag-orange-icon)",
                      "--w-rjv-key-number": "var(--contrast-fg-secondary)",
                      "--w-rjv-arrow-color": "var(--contrast-fg-secondary)",
                      "--w-rjv-copied-color": "var(--contrast-fg-secondary)",
                      "--w-rjv-copied-success-color":
                        "var(--contrast-fg-primary)",
                      "--w-rjv-colon-color": "var(--contrast-fg-primary)",
                      "--w-rjv-ellipsis-color": "var(--contrast-fg-secondary)",
                    } as CSSProperties
                  }
                  collapsed={1}
                >
                  <Primitive.Quote render={() => <span />} />
                  <Primitive.Null
                    render={() => (
                      <span className="text-ui-tag-red-icon">null</span>
                    )}
                  />
                  <Primitive.Undefined
                    render={() => (
                      <span className="text-ui-tag-blue-icon">undefined</span>
                    )}
                  />
                  <Primitive.CountInfo
                    render={(_props, { value }) => {
                      return (
                        <span className="text-ui-contrast-fg-secondary ml-2">
                          {Object.keys(value as object).length} items
                        </span>
                      )
                    }}
                  />
                  <Primitive.Arrow>
                    <TriangleDownMini className="text-ui-contrast-fg-secondary -ml-[0.5px]" />
                  </Primitive.Arrow>
                  <Primitive.Colon>
                    <span className="mr-1">:</span>
                  </Primitive.Colon>
                  <Primitive.Copied
                    render={({ style }, { value }) => {
                      return <Copied style={style} value={value} />
                    }}
                  />
                </Primitive>
              </Suspense>
            </div>
          </Drawer.Body>
        </Drawer.Content>
      </Drawer>
    </Container>
  )
}

type CopiedProps = {
  style?: CSSProperties
  value: object | undefined
}

const Copied = ({ style, value }: CopiedProps) => {
  const [copied, setCopied] = useState(false)

  const handler = (e: MouseEvent<HTMLSpanElement>) => {
    e.stopPropagation()
    setCopied(true)

    if (typeof value === "string") {
      navigator.clipboard.writeText(value)
    } else {
      const json = JSON.stringify(value, null, 2)
      navigator.clipboard.writeText(json)
    }

    setTimeout(() => {
      setCopied(false)
    }, 2000)
  }

  const styl = { whiteSpace: "nowrap", width: "20px" }

  if (copied) {
    return (
      <span style={{ ...style, ...styl }}>
        <Check className="text-ui-contrast-fg-primary" />
      </span>
    )
  }

  return (
    <span style={{ ...style, ...styl }} onClick={handler}>
      <SquareTwoStack className="text-ui-contrast-fg-secondary" />
    </span>
  )
}
```

The `JsonViewSection` component shows a section with the "JSON" title and a button to show the data as JSON in a drawer or side window.

The `JsonViewSection` accepts a `data` prop, which is the data to show as a JSON object in the drawer.

***

## Example

Use the `JsonViewSection` component in any widget or UI route.

For example, create the widget `src/admin/widgets/product-widget.tsx` with the following content:

```tsx title="src/admin/widgets/product-widget.tsx"
import { defineWidgetConfig } from "@medusajs/admin-sdk"
import { JsonViewSection } from "../components/json-view-section"

const ProductWidget = () => {
  return <JsonViewSection data={{
    name: "John",
  }} />
}

export const config = defineWidgetConfig({
  zone: "product.details.before",
})

export default ProductWidget
```

This shows the JSON section at the top of the product page, passing it the object `{ name: "John" }`.


# Data Table - Admin Components

This component is available after [Medusa v2.4.0+](https://github.com/medusajs/medusa/releases/tag/v2.4.0).

The [DataTable component in Medusa UI](https://docs.medusajs.com/ui/components/data-table/index.html.md) allows you to display data in a table with sorting, filtering, and pagination. It's used across the Medusa Admin dashboard to showcase a list of items, such as a list of products.

![Example of a table in the product listing page](https://res.cloudinary.com/dza7lstvk/image/upload/v1728295658/Medusa%20Resources/list_ddt9zc.png)

You can use this component in your Admin Extensions to display data in a table format, especially if you're retrieving them from API routes of the Medusa application.

This guide focuses on how to use the `DataTable` component while fetching data from the backend. Refer to the [Medusa UI documentation](https://docs.medusajs.com/ui/components/data-table/index.html.md) for detailed information about the DataTable component and its different usages.

## Example: DataTable with Data Fetching

In this example, you'll create a UI widget that shows the list of products retrieved from the [List Products API Route](https://docs.medusajs.com/api/admin#products_getproducts) in a data table with pagination, filtering, searching, and sorting.

Start by initializing the columns in the data table. To do that, use the `createDataTableColumnHelper` from Medusa UI:

```tsx title="src/admin/routes/custom/page.tsx"
import {
  createDataTableColumnHelper,
} from "@medusajs/ui"
import { 
  HttpTypes,
} from "@medusajs/framework/types"

const columnHelper = createDataTableColumnHelper<HttpTypes.AdminProduct>()

const columns = [
  columnHelper.accessor("title", {
    header: "Title",
    // Enables sorting for the column.
    enableSorting: true,
    // If omitted, the header will be used instead if it's a string, 
    // otherwise the accessor key (id) will be used.
    sortLabel: "Title",
    // If omitted the default value will be "A-Z"
    sortAscLabel: "A-Z",
    // If omitted the default value will be "Z-A"
    sortDescLabel: "Z-A",
  }),
  columnHelper.accessor("status", {
    header: "Status",
    cell: ({ getValue }) => {
      const status = getValue()
      return (
        <Badge color={status === "published" ? "green" : "grey"} size="xsmall">
          {status === "published" ? "Published" : "Draft"}
        </Badge>
      )
    },
  }),
]
```

`createDataTableColumnHelper` utility creates a column helper that helps you define the columns for the data table. The column helper has an `accessor` method that accepts two parameters:

1. The column's key in the table's data.
2. An object with the following properties:
   - `header`: The column's header.
   - `cell`: (optional) By default, a data's value for a column is displayed as a string. Use this property to specify custom rendering of the value. It accepts a function that returns a string or a React node. The function receives an object that has a `getValue` property function to retrieve the raw value of the cell.
   - `enableSorting`: (optional) A boolean that enables sorting data by this column.
   - `sortLabel`: (optional) The label for the sorting button. If omitted, the `header` will be used instead if it's a string, otherwise the accessor key (id) will be used.
   - `sortAscLabel`: (optional) The label for the ascending sorting button. If omitted, the default value will be "A-Z".
   - `sortDescLabel`: (optional) The label for the descending sorting button. If omitted, the default value will be "Z-A".

Next, you'll define the filters that can be applied to the data table. You'll configure filtering by product status.

To define the filters, add the following:

```tsx title="src/admin/routes/custom/page.tsx"
// other imports...
import {
  // ...
  createDataTableFilterHelper,
} from "@medusajs/ui"

const filterHelper = createDataTableFilterHelper<HttpTypes.AdminProduct>()

const filters = [
  filterHelper.accessor("status", {
    type: "select",
    label: "Status",
    options: [
      {
        label: "Published",
        value: "published",
      },
      {
        label: "Draft",
        value: "draft",
      },
    ],
  }),
]
```

`createDataTableFilterHelper` utility creates a filter helper that helps you define the filters for the data table. The filter helper has an `accessor` method that accepts two parameters:

1. The key of a column in the table's data.
2. An object with the following properties:
   - `type`: The type of filter. It can be either:
     - `select`: A select dropdown allowing users to choose multiple values.
     - `radio`: A radio button allowing users to choose one value.
     - `date`: A date picker allowing users to choose a date.
   - `label`: The filter's label.
   - `options`: An array of objects with `label` and `value` properties. The `label` is the option's label, and the `value` is the value to filter by.

You'll now start creating the UI widget's component. Start by adding the necessary state variables:

```tsx title="src/admin/routes/custom/page.tsx"
// other imports...
import {
  // ...
  DataTablePaginationState,
  DataTableFilteringState,
  DataTableSortingState,
} from "@medusajs/ui"
import { useMemo, useState } from "react"

// ...

const limit = 15

const CustomPage = () => {
  const [pagination, setPagination] = useState<DataTablePaginationState>({
    pageSize: limit,
    pageIndex: 0,
  })
	const [search, setSearch] = useState<string>("")
	const [filtering, setFiltering] = useState<DataTableFilteringState>({})
  const [sorting, setSorting] = useState<DataTableSortingState | null>(null)

  const offset = useMemo(() => {
    return pagination.pageIndex * limit
  }, [pagination])
  const statusFilters = useMemo(() => {
    return (filtering.status || []) as ProductStatus
  }, [filtering])

  // TODO add data fetching logic
}
```

In the component, you've added the following state variables:

- `pagination`: An object of type `DataTablePaginationState` that holds the pagination state. It has two properties:
  - `pageSize`: The number of items to show per page.
  - `pageIndex`: The current page index.
- `search`: A string that holds the search query.
- `filtering`: An object of type `DataTableFilteringState` that holds the filtering state.
- `sorting`: An object of type `DataTableSortingState` that holds the sorting state.

You've also added two memoized variables:

- `offset`: How many items to skip when fetching data based on the current page.
- `statusFilters`: The selected status filters, if any.

Next, you'll fetch the products from the Medusa application. Assuming you have the JS SDK configured as explained in [this guide](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/js-sdk/index.html.md), add the following imports at the top of the file:

```tsx title="src/admin/routes/custom/page.tsx"
import { sdk } from "../../lib/config"
import { useQuery } from "@tanstack/react-query"
```

This imports the JS SDK instance and `useQuery` from [Tanstack Query](https://tanstack.com/query/latest).

Then, replace the `TODO` in the component with the following:

```tsx title="src/admin/routes/custom/page.tsx"
const { data, isLoading } = useQuery({
  queryFn: () => sdk.admin.product.list({
    limit,
    offset,
    q: search,
    status: statusFilters,
    order: sorting ? `${sorting.desc ? "-" : ""}${sorting.id}` : undefined,
  }),
  queryKey: [["products", limit, offset, search, statusFilters, sorting?.id, sorting?.desc]],
})

// TODO configure data table
```

You use the `useQuery` hook to fetch the products from the Medusa application. In the `queryFn`, you call the `sdk.admin.product.list` method to fetch the products. You pass the following query parameters to the method:

- `limit`: The number of products to fetch per page.
- `offset`: The number of products to skip based on the current page.
- `q`: The search query, if set.
- `status`: The status filters, if set.
- `order`: The sorting order, if set.

So, whenever the user changes the current page, search query, status filters, or sorting, the products are fetched based on the new parameters.

Next, you'll configure the data table. Medusa UI provides a `useDataTable` hook that helps you configure the data table. Add the following imports at the top of the file:

```tsx title="src/admin/routes/custom/page.tsx"
import {
  // ...
  useDataTable,
} from "@medusajs/ui"
import { useNavigate } from "react-router-dom"
```

Then, replace the `TODO` in the component with the following:

```tsx title="src/admin/routes/custom/page.tsx"
const navigate = useNavigate()

const table = useDataTable({
  columns,
  data: data?.products || [],
  getRowId: (row) => row.id,
  rowCount: data?.count || 0,
  isLoading,
  pagination: {
    state: pagination,
    onPaginationChange: setPagination,
  },
  search: {
    state: search,
    onSearchChange: setSearch,
  },
  filtering: {
    state: filtering,
    onFilteringChange: setFiltering,
  },
  filters,
  sorting: {
    // Pass the pagination state and updater to the table instance
    state: sorting,
    onSortingChange: setSorting,
  },
  onRowClick: (event, row) => {
    // Handle row click, for example
    navigate(`/products/${row.id}`)
  },
})

// TODO render component
```

The `useDataTable` hook accepts an object with the following properties:

- columns: (\`array\`) The columns to display in the data table. You created this using the \`createDataTableColumnHelper\` utility.
- data: (\`array\`) The products fetched from the Medusa application.
- getRowId: (\`function\`) A function that returns the unique ID of a row.
- rowCount: (\`number\`) The total number of products that can be retrieved. This is used to determine the number of pages.
- isLoading: (\`boolean\`) A boolean that indicates if the data is being fetched.
- pagination: (\`object\`) An object to configure pagination.

  - state: (\`object\`) The pagination React state variable.

  - onPaginationChange: (\`function\`) A function that updates the pagination state.
- search: (\`object\`) An object to configure searching.

  - state: (\`string\`) The search query React state variable.

  - onSearchChange: (\`function\`) A function that updates the search query state.
- filtering: (\`object\`) An object to configure filtering.

  - state: (\`object\`) The filtering React state variable.

  - onFilteringChange: (\`function\`) A function that updates the filtering state.
- filters: (\`array\`) The filters to display in the data table. You created this using the \`createDataTableFilterHelper\` utility.
- sorting: (\`object\`) An object to configure sorting.

  - state: (\`object\`) The sorting React state variable.

  - onSortingChange: (\`function\`) A function that updates the sorting state.
- onRowClick: (\`function\`) A function that allows you to perform an action when the user clicks on a row. In this example, you navigate to the product's detail page.

  - event: (\`mouseevent\`) An instance of the \[MouseClickEvent]\(https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent) object.

  - row: (\`object\`) The data of the row that was clicked.

Finally, you'll render the data table. But first, add the following imports at the top of the page:

```tsx title="src/admin/routes/custom/page.tsx"
import {
  // ...
  DataTable,
} from "@medusajs/ui"
import { SingleColumnLayout } from "../../layouts/single-column"
import { Container } from "../../components/container"
```

Aside from the `DataTable` component, you also import the [SingleColumnLayout](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/admin-components/layouts/single-column/index.html.md) and [Container](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/admin-components/components/container/index.html.md) components implemented in other Admin Component guides. These components ensure a style consistent to other pages in the admin dashboard.

Then, replace the `TODO` in the component with the following:

```tsx title="src/admin/routes/custom/page.tsx"
return (
  <SingleColumnLayout>
    <Container>
      <DataTable instance={table}>
        <DataTable.Toolbar className="flex flex-col items-start justify-between gap-2 md:flex-row md:items-center">
          <Heading>Products</Heading>
          <div className="flex gap-2">
            <DataTable.FilterMenu tooltip="Filter" />
            <DataTable.SortingMenu tooltip="Sort" />
            <DataTable.Search placeholder="Search..." />
          </div>
        </DataTable.Toolbar>
        <DataTable.Table />
        <DataTable.Pagination />
      </DataTable>
    </Container>
  </SingleColumnLayout>
)
```

You render the `DataTable` component and pass the `table` instance as a prop. In the `DataTable` component, you render a toolbar showing a heading, filter menu, sorting menu, and a search input. You also show pagination after the table.

Lastly, export the component and the UI widget's configuration at the end of the file:

```tsx title="src/admin/routes/custom/page.tsx"
// other imports...
import { defineRouteConfig } from "@medusajs/admin-sdk"
import { ChatBubbleLeftRight } from "@medusajs/icons"

// ...

export const config = defineRouteConfig({
  label: "Custom",
  icon: ChatBubbleLeftRight,
})

export default CustomPage
```

If you start your Medusa application and go to `localhost:9000/app/custom`, you'll see the data table showing the list of products with pagination, filtering, searching, and sorting functionalities.

### Full Example Code

```tsx title="src/admin/routes/custom/page.tsx"
import { defineRouteConfig } from "@medusajs/admin-sdk"
import { ChatBubbleLeftRight } from "@medusajs/icons"
import { 
  Badge,
  createDataTableColumnHelper,
  createDataTableFilterHelper,
  DataTable,
  DataTableFilteringState,
  DataTablePaginationState,
  DataTableSortingState,
  Heading,
  useDataTable,
} from "@medusajs/ui"
import { useQuery } from "@tanstack/react-query"
import { SingleColumnLayout } from "../../layouts/single-column"
import { sdk } from "../../lib/config"
import { useMemo, useState } from "react"
import { Container } from "../../components/container"
import { HttpTypes, ProductStatus } from "@medusajs/framework/types"

const columnHelper = createDataTableColumnHelper<HttpTypes.AdminProduct>()

const columns = [
  columnHelper.accessor("title", {
    header: "Title",
    // Enables sorting for the column.
    enableSorting: true,
    // If omitted, the header will be used instead if it's a string, 
    // otherwise the accessor key (id) will be used.
    sortLabel: "Title",
    // If omitted the default value will be "A-Z"
    sortAscLabel: "A-Z",
    // If omitted the default value will be "Z-A"
    sortDescLabel: "Z-A",
  }),
  columnHelper.accessor("status", {
    header: "Status",
    cell: ({ getValue }) => {
      const status = getValue()
      return (
        <Badge color={status === "published" ? "green" : "grey"} size="xsmall">
          {status === "published" ? "Published" : "Draft"}
        </Badge>
      )
    },
  }),
]

const filterHelper = createDataTableFilterHelper<HttpTypes.AdminProduct>()

const filters = [
  filterHelper.accessor("status", {
    type: "select",
    label: "Status",
    options: [
      {
        label: "Published",
        value: "published",
      },
      {
        label: "Draft",
        value: "draft",
      },
    ],
  }),
]

const limit = 15

const CustomPage = () => {
  const [pagination, setPagination] = useState<DataTablePaginationState>({
    pageSize: limit,
    pageIndex: 0,
  })
	const [search, setSearch] = useState<string>("")
	const [filtering, setFiltering] = useState<DataTableFilteringState>({})
  const [sorting, setSorting] = useState<DataTableSortingState | null>(null)

  const offset = useMemo(() => {
    return pagination.pageIndex * limit
  }, [pagination])
  const statusFilters = useMemo(() => {
    return (filtering.status || []) as ProductStatus
  }, [filtering])

  const { data, isLoading } = useQuery({
    queryFn: () => sdk.admin.product.list({
      limit,
      offset,
      q: search,
      status: statusFilters,
      order: sorting ? `${sorting.desc ? "-" : ""}${sorting.id}` : undefined,
    }),
    queryKey: [["products", limit, offset, search, statusFilters, sorting?.id, sorting?.desc]],
  })

  const table = useDataTable({
    columns,
    data: data?.products || [],
    getRowId: (row) => row.id,
    rowCount: data?.count || 0,
    isLoading,
    pagination: {
      state: pagination,
      onPaginationChange: setPagination,
    },
    search: {
	    state: search,
	    onSearchChange: setSearch,
    },
    filtering: {
      state: filtering,
      onFilteringChange: setFiltering,
    },
    filters,
    sorting: {
      // Pass the pagination state and updater to the table instance
      state: sorting,
      onSortingChange: setSorting,
    },
  })

  return (
    <SingleColumnLayout>
      <Container>
        <DataTable instance={table}>
          <DataTable.Toolbar className="flex flex-col items-start justify-between gap-2 md:flex-row md:items-center">
            <Heading>Products</Heading>
            <div className="flex gap-2">
              <DataTable.FilterMenu tooltip="Filter" />
              <DataTable.SortingMenu tooltip="Sort" />
              <DataTable.Search placeholder="Search..." />
            </div>
          </DataTable.Toolbar>
          <DataTable.Table />
          <DataTable.Pagination />
        </DataTable>
      </Container>
    </SingleColumnLayout>
  )
}

export const config = defineRouteConfig({
  label: "Custom",
  icon: ChatBubbleLeftRight,
})

export default CustomPage
```


# Table - Admin Components

If you're using [Medusa v2.4.0+](https://github.com/medusajs/medusa/releases/tag/v2.4.0), it's recommended to use the [Data Table](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/admin-components/components/data-table/index.html.md) component instead as it provides features for sorting, filtering, pagination, and more with a simpler API.

You can use the `Table` component from Medusa UI to display data in a table. It's mostly recommended for simpler tables.

To create a component that shows a table with pagination, create the file `src/admin/components/table.tsx` with the following content:

```tsx title="src/admin/components/table.tsx"
import { useMemo } from "react"
import { Table as UiTable } from "@medusajs/ui"

export type TableProps = {
  columns: {
    key: string
    label?: string
    render?: (value: unknown) => React.ReactNode
  }[]
  data: Record<string, unknown>[]
  pageSize: number
  count: number
  currentPage: number
  setCurrentPage: (value: number) => void
}

export const Table = ({
  columns,
  data,
  pageSize,
  count,
  currentPage,
  setCurrentPage,
}: TableProps) => {
  const pageCount = useMemo(() => {
    return Math.ceil(count / pageSize)
  }, [data, pageSize])

  const canNextPage = useMemo(() => {
    return currentPage < pageCount - 1
  }, [currentPage, pageCount])
  const canPreviousPage = useMemo(() => {
    return currentPage - 1 >= 0
  }, [currentPage])

  const nextPage = () => {
    if (canNextPage) {
      setCurrentPage(currentPage + 1)
    }
  }

  const previousPage = () => {
    if (canPreviousPage) {
      setCurrentPage(currentPage - 1)
    }
  }

  return (
    <div className="flex h-full flex-col overflow-hidden !border-t-0">
      <UiTable>
        <UiTable.Header>
          <UiTable.Row>
            {columns.map((column, index) => (
              <UiTable.HeaderCell key={index}>
                {column.label || column.key}
              </UiTable.HeaderCell>
            ))}
          </UiTable.Row>
        </UiTable.Header>
        <UiTable.Body>
          {data.map((item, index) => {
            const rowIndex = "id" in item ? item.id as string : index
            return (
              <UiTable.Row key={rowIndex}>
                {columns.map((column, index) => (
                  <UiTable.Cell key={`${rowIndex}-${index}`}>
                    <>
                      {column.render && column.render(item[column.key])}
                      {!column.render && (
                        <>{item[column.key] as string}</>
                      )}
                    </>
                  </UiTable.Cell>
                ))}
              </UiTable.Row>
            )
          })}
        </UiTable.Body>
      </UiTable>
      <UiTable.Pagination
        count={count}
        pageSize={pageSize}
        pageIndex={currentPage}
        pageCount={pageCount}
        canPreviousPage={canPreviousPage}
        canNextPage={canNextPage}
        previousPage={previousPage}
        nextPage={nextPage}
      />
    </div>
  )
}
```

The `Table` component uses the component from the [UI package](https://docs.medusajs.com/ui/components/table/index.html.md), with additional styling and rendering of data.

It accepts the following props:

- columns: (\`object\[]\`) The table's columns.

  - key: (\`string\`) The column's key in the passed \`data\`

  - label: (\`string\`) The column's label shown in the table. If not provided, the \`key\` is used.

  - render: (\`(value: unknown) => React.ReactNode\`) By default, the data is shown as-is in the table. You can use this function to change how the value is rendered. The function receives the value is a parameter and returns a React node.
- data: (\`Record\<string, unknown>\[]\`) The data to show in the table for the current page. The keys of each object should be in the \`columns\` array.
- pageSize: (\`number\`) The number of items to show per page.
- count: (\`number\`) The total number of items.
- currentPage: (\`number\`) A zero-based index indicating the current page's number.
- setCurrentPage: (\`(value: number) => void\`) A function used to change the current page.

***

## Example

Use the `Table` component in any widget or UI route.

For example, create the widget `src/admin/widgets/product-widget.tsx` with the following content:

```tsx title="src/admin/widgets/product-widget.tsx"
import { defineWidgetConfig } from "@medusajs/admin-sdk"
import { StatusBadge } from "@medusajs/ui"
import { Table } from "../components/table"
import { useState } from "react"
import { Container } from "../components/container"

const ProductWidget = () => {
  const [currentPage, setCurrentPage] = useState(0)

  return (
    <Container>
      <Table
        columns={[
          {
            key: "name",
            label: "Name",
          },
          {
            key: "is_enabled",
            label: "Status",
            render: (value: unknown) => {
              const isEnabled = value as boolean

              return (
                <StatusBadge color={isEnabled ? "green" : "grey"}>
                  {isEnabled ? "Enabled" : "Disabled"}
                </StatusBadge>
              )
            },
          },
        ]}
        data={[
          {
            name: "John",
            is_enabled: true,
          },
          {
            name: "Jane",
            is_enabled: false,
          },
        ]}
        pageSize={2}
        count={2}
        currentPage={currentPage}
        setCurrentPage={setCurrentPage}
      />
    </Container>
  )
}

export const config = defineWidgetConfig({
  zone: "product.details.before",
})

export default ProductWidget
```

This widget also uses the [Container](../container.mdx) custom component.

***

## Example With Data Fetching

This section shows you how to use the `Table` component when fetching data from the Medusa application's API routes.

Assuming you've set up the JS SDK as explained in [this guide](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/js-sdk/index.html.md), create the UI route `src/admin/routes/custom/page.tsx` with the following content:

```tsx title="src/admin/routes/custom/page.tsx" collapsibleLines="1-10" expandButtonLabel="Show Imports" highlights={tableExampleHighlights}
import { defineRouteConfig } from "@medusajs/admin-sdk"
import { ChatBubbleLeftRight } from "@medusajs/icons"
import { useQuery } from "@tanstack/react-query"
import { SingleColumnLayout } from "../../layouts/single-column"
import { Table } from "../../components/table"
import { sdk } from "../../lib/config"
import { useMemo, useState } from "react"
import { Container } from "../../components/container"
import { Header } from "../../components/header"

const CustomPage = () => {
  const [currentPage, setCurrentPage] = useState(0)
  const limit = 15
  const offset = useMemo(() => {
    return currentPage * limit
  }, [currentPage])

  const { data } = useQuery({
    queryFn: () => sdk.admin.product.list({
      limit,
      offset,
    }),
    queryKey: [["products", limit, offset]],
  })

  // TODO display table
}

export const config = defineRouteConfig({
  label: "Custom",
  icon: ChatBubbleLeftRight,
})

export default CustomPage
```

In the `CustomPage` component, you define:

- A state variable `currentPage` that stores the current page of the table.
- A `limit` variable, indicating how many items to retrieve per page
- An `offset` memoized variable indicating how many items to skip before the retrieved items. It's calculated as a multiplication of `currentPage` and `limit`.

Then, you use `useQuery` from [Tanstack Query](https://tanstack.com/query/latest) to retrieve products using the JS SDK. You pass `limit` and `offset` as query parameters, and you set the `queryKey`, which is used for caching and revalidation, to be based on the key `products`, along with the current limit and offset. So, whenever the `offset` variable changes, the request is sent again to retrieve the products of the current page.

You can change the query to send a request to a custom API route as explained in [this guide](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/js-sdk#send-requests-to-custom-routes/index.html.md).

Do not install Tanstack Query as that will cause unexpected errors in your development. If you prefer installing it for better auto-completion in your code editor, make sure to install `v5.64.2` as a development dependency.

`useQuery` returns an object containing `data`, which holds the response fields including the products and pagination fields.

Then, to display the table, replace the `TODO` with the following:

```tsx
return (
  <SingleColumnLayout>
    <Container>
      <Header title="Products" />
      {data && (
        <Table
          columns={[
            {
              key: "id",
              label: "ID",
            },
            {
              key: "title",
              label: "Title",
            },
          ]}
          data={data.products as any}
          pageSize={data.limit}
          count={data.count}
          currentPage={currentPage}
          setCurrentPage={setCurrentPage}
        />
      )}
    </Container>
  </SingleColumnLayout>
)
```

Aside from the `Table` component, this UI route also uses the [SingleColumnLayout](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/admin-components/layouts/single-column/index.html.md), [Container](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/admin-components/components/container/index.html.md), and [Header](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/admin-components/components/header/index.html.md) custom component.

If `data` isn't `undefined`, you display the `Table` component passing it the following props:

- `columns`: The columns to show. You only show the product's ID and title.
- `data`: The rows of the table. You pass it the `products` property of `data`.
- `pageSize`: The maximum number of items per page. You pass it the `count` property of `data`.
- `currentPage` and `setCurrentPage`: The current page and the function to change it.

To test it out, log into the Medusa Admin and open `http://localhost:9000/app/custom`. You'll find a table of products with pagination.


# Single Column Layout - Admin Components

The Medusa Admin has pages with a single column of content.

This doesn't include the sidebar, only the main content.

![An example of an admin page with a single column](https://res.cloudinary.com/dza7lstvk/image/upload/v1728286605/Medusa%20Resources/single-column.png)

To create a layout that you can use in UI routes to support one column of content, create the component `src/admin/layouts/single-column.tsx` with the following content:

```tsx title="src/admin/layouts/single-column.tsx"
export type SingleColumnLayoutProps = {
  children: React.ReactNode
}

export const SingleColumnLayout = ({ children }: SingleColumnLayoutProps) => {
  return (
    <div className="flex flex-col gap-y-3">
      {children}
    </div>
  )
}
```

The `SingleColumnLayout` accepts the content in the `children` props.

***

## Example

Use the `SingleColumnLayout` component in your UI routes that have a single column. For example:

```tsx title="src/admin/routes/custom/page.tsx" highlights={[["9"]]}
import { defineRouteConfig } from "@medusajs/admin-sdk"
import { ChatBubbleLeftRight } from "@medusajs/icons"
import { Container } from "../../components/container"
import { SingleColumnLayout } from "../../layouts/single-column"
import { Header } from "../../components/header"

const CustomPage = () => {
  return (
    <SingleColumnLayout>
      <Container>
        <Header title="Custom Page" />
      </Container>
    </SingleColumnLayout>
  )
}

export const config = defineRouteConfig({
  label: "Custom",
  icon: ChatBubbleLeftRight,
})

export default CustomPage
```

This UI route also uses a [Container](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/admin-components/components/container/index.html.md) and a [Header]() custom components.


# Two Column Layout - Admin Components

The Medusa Admin has pages with two columns of content.

This doesn't include the sidebar, only the main content.

![An example of an admin page with two columns](https://res.cloudinary.com/dza7lstvk/image/upload/v1728286690/Medusa%20Resources/two-column_sdnkg0.png)

To create a layout that you can use in UI routes to support two columns of content, create the component `src/admin/layouts/two-column.tsx` with the following content:

```tsx title="src/admin/layouts/two-column.tsx"
export type TwoColumnLayoutProps = {
  firstCol: React.ReactNode
  secondCol: React.ReactNode
}

export const TwoColumnLayout = ({ 
  firstCol,
  secondCol,
}: TwoColumnLayoutProps) => {
  return (
    <div className="flex flex-col gap-x-4 gap-y-3 xl:flex-row xl:items-start">
      <div className="flex w-full flex-col gap-y-3">
          {firstCol}
        </div>
        <div className="flex w-full max-w-[100%] flex-col gap-y-3 xl:mt-0 xl:max-w-[440px]">
          {secondCol}
        </div>
    </div>
  )
}
```

The `TwoColumnLayout` accepts two props:

- `firstCol` indicating the content of the first column.
- `secondCol` indicating the content of the second column.

***

## Example

Use the `TwoColumnLayout` component in your UI routes that have a single column. For example:

```tsx title="src/admin/routes/custom/page.tsx" highlights={[["9"]]}
import { defineRouteConfig } from "@medusajs/admin-sdk"
import { ChatBubbleLeftRight } from "@medusajs/icons"
import { Container } from "../../components/container"
import { Header } from "../../components/header"
import { TwoColumnLayout } from "../../layouts/two-column"

const CustomPage = () => {
  return (
    <TwoColumnLayout
      firstCol={
        <Container>
          <Header title="First Column" />
        </Container>
      }
      secondCol={
        <Container>
          <Header title="Second Column" />
        </Container>
      }
    />
  )
}

export const config = defineRouteConfig({
  label: "Custom",
  icon: ChatBubbleLeftRight,
})

export default CustomPage
```

This UI route also uses [Container](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/admin-components/components/container/index.html.md) and [Header]() custom components.


# Service Factory Reference

This section of the documentation provides a reference of the methods generated for services extending the service factory (`MedusaService`), and how to use them.

Learn more about the service factory in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/modules/service-factory/index.html.md).

## Method Names

Generated method names are of the format `{operationName}_{dataModelName}`, where:

- `{operationName}` is the name of the operation. For example, `create`.
- `{dataModelName}` is the pascal-case version of the data model's key that's passed in the object parameter of `MedusaService`. The name is pluralized for all operations except for the `retrieve` operation.

Some examples of method names:

- `createPosts`
- `createMyPosts`
- `retrievePost`
- `listPosts`

***

## Methods Reference

The reference uses only the operation name to refer to the method.


# create Method - Service Factory Reference

This method creates one or more records of the data model.

## Create One Record

```ts
const post = await postModuleService.createPosts({
  name: "My Post",
  published_at: new Date(),
  metadata: {
     external_id: "1234",
  },
})
```

If an object is passed of the method, an object of the created record is also returned.

***

## Create Multiple Records

```ts
const posts = await postModuleService.createPosts([
  {
    name: "My Post",
    published_at: new Date(),
  },
  {
    name: "My Other Post",
    published_at: new Date(),
  },
])
```

If an array is passed of the method, an array of the created records is also returned.


# delete Method - Service Factory Reference

This method deletes one or more records.

## Delete One Record

```ts
await postModuleService.deletePosts("123")
```

To delete one record, pass its ID as a parameter of the method.

***

## Delete Multiple Records

```ts
await postModuleService.deletePosts([
  "123",
  "321",
])
```

To delete multiple records, pass an array of IDs as a parameter of the method.

***

## Delete Records Matching Filters

```ts
await postModuleService.deletePosts({
  name: "My Post",
})
```

To delete records matching a set of filters, pass an object of filters as a parameter.

Learn more about accepted filters in [this documentation](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/service-factory-reference/tips/filtering/index.html.md).


# listAndCount Method - Service Factory Reference

This method retrieves a list of records with the total count.

## Retrieve List of Records

```ts
const [posts, count] = await postModuleService.listAndCountPosts()
```

If no parameters are passed, the method returns an array with two items:

1. The first is an array of the first `15` records retrieved.
2. The second is the total count of records.

***

## Filter Records

```ts
const [posts, count] = await postModuleService.listAndCountPosts({
  id: ["123", "321"],
})
```

### Parameters

To retrieve records matching a set of filters, pass an object of the filters as a first parameter.

Learn more about accepted filters in [this documentation](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/service-factory-reference/tips/filtering/index.html.md).

### Returns

The method returns an array with two items:

1. The first is an array of the first `15` records retrieved matching the specified filters.
2. The second is the total count of records matching the specified filters.

***

## Retrieve Relations

This applies to relations between data models of the same module. To retrieve linked records of different modules, use [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md).

```ts
const [posts, count] = await postModuleService.listAndCountPosts({}, {
  relations: ["author"],
})
```

### Parameters

To retrieve records with their relations, pass as a second parameter an object having a `relations` property. Its value is an array of relation names.

### Returns

The method returns an array with two items:

1. The first is an array of the first `15` records retrieved.
2. The second is the total count of records.

***

## Select Properties

```ts
const [posts, count] = await postModuleService.listAndCountPosts({}, {
  select: ["id", "name"],
})
```

### Parameters

By default, retrieved records have all their properties. To select specific properties to retrieve, pass in the second object parameter a `select` property.

`select`'s value is an array of property names to retrieve.

### Returns

The method returns an array with two items:

1. The first is an array of the first `15` records retrieved.
2. The second is the total count of records.

***

## Paginate Relations

```ts
const [posts, count] = await postModuleService.listAndCountPosts({}, {
  take: 20,
  skip: 10,
})
```

### Parameters

To paginate the returned records, the second object parameter accepts the following properties:

- `take`: a number indicating how many records to retrieve. By default, it's `15`.
- `skip`: a number indicating how many records to skip before the retrieved records. By default, it's `0`.

### Returns

The method returns an array with two items:

1. The first is an array of the records retrieved. The number of records is less than or equal to `take`'s value.
2. The second is the total count of records.

***

## Sort Records

```ts
const [posts, count] = await postModuleService.listAndCountPosts({}, {
  order: {
    name: "ASC",
  },
})
```

### Parameters

To sort records by one or more properties, pass to the second object parameter the `order` property. Its value is an object whose keys are the property names, and values can either be:

- `ASC` to sort by this property in the ascending order.
- `DESC` to sort by this property in the descending order.

### Returns

The method returns an array with two items:

1. The first is an array of the first `15` records retrieved.
2. The second is the total count of records.


# list Method - Service Factory Reference

This method retrieves a list of records.

## Retrieve List of Records

```ts
const posts = await postModuleService.listPosts()
```

If no parameters are passed, the method returns an array of the first `15` records.

***

## Filter Records

```ts
const posts = await postModuleService.listPosts({
  id: ["123", "321"],
})
```

### Parameters

To retrieve records matching a set of filters, pass an object of the filters as a first parameter.

Learn more about accepted filters in [this documentation](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/service-factory-reference/tips/filtering/index.html.md).

### Returns

The method returns an array of the first `15` records matching the filters.

***

## Retrieve Relations

This applies to relations between data models of the same module. To retrieve linked records of different modules, use [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md).

```ts
const posts = await postModuleService.listPosts({}, {
  relations: ["author"],
})
```

### Parameters

To retrieve records with their relations, pass as a second parameter an object having a `relations` property. `relations`'s value is an array of relation names.

### Returns

The method returns an array of the first `15` records matching the filters.

***

## Select Properties

```ts
const posts = await postModuleService.listPosts({}, {
  select: ["id", "name"],
})
```

### Parameters

By default, retrieved records have all their properties. To select specific properties to retrieve, pass in the second object parameter a `select` property.

`select`'s value is an array of property names to retrieve.

### Returns

The method returns an array of the first `15` records matching the filters.

***

## Paginate Relations

```ts
const posts = await postModuleService.listPosts({}, {
  take: 20,
  skip: 10,
})
```

### Parameters

To paginate the returned records, the second object parameter accepts the following properties:

- `take`: a number indicating how many records to retrieve. By default, it's `15`.
- `skip`: a number indicating how many records to skip before the retrieved records. By default, it's `0`.

### Returns

The method returns an array of records. The number of records is less than or equal to `take`'s value.

***

## Sort Records

```ts
const posts = await postModuleService.listPosts({}, {
  order: {
    name: "ASC",
  },
})
```

### Parameters

To sort records by one or more properties, pass to the second object parameter the `order` property. Its value is an object whose keys are the property names, and values can either be:

- `ASC` to sort by this property in the ascending order.
- `DESC` to sort by this property in the descending order.

### Returns

The method returns an array of the first `15` records matching the filters.


# retrieve Method - Service Factory Reference

This method retrieves one record of the data model by its ID.

## Retrieve a Record

```ts
const post = await postModuleService.retrievePost("123")
```

### Parameters

Pass the ID of the record to retrieve.

### Returns

The method returns the record as an object.

***

## Retrieve a Record's Relations

This applies to relations between data models of the same module. To retrieve linked records of different modules, use [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md).

```ts
const post = await postModuleService.retrievePost("123", {
  relations: ["author"],
})
```

### Parameters

To retrieve the data model with relations, pass as a second parameter of the method an object with the property `relations`. `relations`'s value is an array of relation names.

### Returns

The method returns the record as an object.

***

## Select Properties to Retrieve

```ts
const post = await postModuleService.retrievePost("123", {
  select: ["id", "name"],
})
```

### Parameters

By default, all of the record's properties are retrieved. To select specific ones, pass in the second object parameter a `select` property. Its value is an array of property names.

### Returns

The method returns the record as an object.


# softDelete Method - Service Factory Reference

This method soft deletes one or more records of the data model.

## Soft Delete One Record

```ts
const deletedPosts = await postModuleService.softDeletePosts(
  "123"
)
```

### Parameters

To soft delete a record, pass its ID as a parameter of the method.

### Returns

The method returns an object, whose keys are of the format `{camel_case_data_model_name}_id`, and their values are arrays of soft-deleted records' IDs.

For example, the returned object of the above example is:

```ts
deletedPosts = {
  post_id: ["123"],
}
```

***

## Soft Delete Multiple Records

```ts
const deletedPosts = await postModuleService.softDeletePosts([
  "123",
  "321",
])
```

### Parameters

To soft delete multiple records, pass an array of IDs as a parameter of the method.

### Returns

The method returns an object, whose keys are of the format `{camel_case_data_model_name}_id`, and their values are arrays of soft-deleted records' IDs.

For example, the returned object of the above example is:

```ts
deletedPosts = {
  post_id: [
    "123",
    "321",
  ],
}
```

***

## Soft Delete Records Matching Filters

```ts
const deletedPosts = await postModuleService.softDeletePosts({
  name: "My Post",
})
```

### Parameters

To soft delete records matching a set of filters, pass an object of filters as a parameter.

Learn more about accepted filters in [this documentation](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/service-factory-reference/tips/filtering/index.html.md).

### Returns

The method returns an object, whose keys are of the format `{camel_case_data_model_name}_id`, and their values are arrays of soft-deleted records' IDs.

For example, the returned object of the above example is:

```ts
deletedPosts = {
  post_id: ["123"],
}
```


# update Method - Service Factory Reference

This method updates one or more records of the data model.

## Update One Record

```ts
const post = await postModuleService.updatePosts({
  id: "123",
  name: "My Post",
})
```

### Parameters

To update one record, pass an object that at least has an `id` property, identifying the ID of the record to update.

You can pass in the same object any other properties to update.

### Returns

The method returns the updated record as an object.

***

## Update Multiple Records

```ts
const posts = await postModuleService.updatePosts([
  {
    id: "123",
    name: "My Post",
  },
  {
    id: "321",
    published_at: new Date(),
  },
])
```

### Parameters

To update multiple records, pass an array of objects. Each object has at least an `id` property, identifying the ID of the record to update.

You can pass in each object any other properties to update.

### Returns

The method returns an array of objects of updated records.

***

## Update Records Matching a Filter

```ts
const posts = await postModuleService.updatePosts({
  selector: {
    name: "My Post",
  },
  data: {
    published_at: new Date(),
  },
})
```

### Parameters

To update records that match specified filters, pass as a parameter an object having two properties:

- `selector`: An object of filters that a record must match to be updated.
- `data`: An object of the properties to update in every record that match the filters in `selector`.

In the example above, you update the `published_at` property of every post record whose name is `My Post`.

Learn more about accepted filters in [this documentation](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/service-factory-reference/tips/filtering/index.html.md).

### Returns

The method returns an array of objects of updated records.

***

## Multiple Record Updates with Filters

```ts
const posts = await postModuleService.updatePosts([
  {
    selector: {
      name: "My Post",
    },
    data: {
      published_at: new Date(),
    },
  },
  {
    selector: {
      name: "Another Post",
    },
    data: {
      metadata: {
        external_id: "123",
      },
    },
  },
])
```

### Parameters

To update records matching different sets of filters, pass an array of objects, each having two properties:

- `selector`: An object of filters that a record must match to be updated.
- `data`: An object of the properties to update in every record that match the filters in `selector`.

In the example above, you update the `published_at` property of post records whose name is `My Post`, and update the `metadata` property of post records whose name is `Another Post`.

Learn more about accepted filters in [this documentation](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/service-factory-reference/tips/filtering/index.html.md).

### Returns

The method returns an array of objects of updated records.


# restore Method - Service Factory Reference

This method restores one or more records of the data model that were [soft-deleted](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/service-factory-reference/methods/soft-delete/index.html.md).

## Restore One Record

```ts
const restoredPosts = await postModuleService.restorePosts("123")
```

### Parameters

To restore one record, pass its ID as a parameter of the method.

### Returns

The method returns an object, whose keys are of the format `{camel_case_data_model_name}_id`, and their values are arrays of restored records' IDs.

For example, the returned object of the above example is:

```ts
restoredPosts = {
  post_id: ["123"],
}
```

***

## Restore Multiple Records

```ts
const restoredPosts = await postModuleService.restorePosts([
  "123",
  "321",
])
```

### Parameters

To restore multiple records, pass an array of IDs as a parameter of the method.

### Returns

The method returns an object, whose keys are of the format `{camel_case_data_model_name}_id`, and their values are arrays of restored records' IDs.

For example, the returned object of the above example is:

```ts
restoredPosts = {
  post_id: [
    "123",
    "321",
  ],
}
```

***

## Restore Records Matching Filters

```ts
const restoredPosts = await postModuleService.restorePosts({
  name: "My Post",
})
```

### Parameters

To restore records matching a set of filters, pass an object of fitlers as a parameter of the method.

Learn more about accepted filters in [this documentation](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/service-factory-reference/tips/filtering/index.html.md).

### Returns

The method returns an object, whose keys are of the format `{camel_case_data_model_name}_id`, and their values are arrays of restored records' IDs.

For example, the returned object of the above example is:

```ts
restoredPosts = {
  post_id: [
    "123",
  ],
}
```


# Filter Records - Service Factory Reference

Many of the service factory's generated methods allow passing filters to perform an operation, such as to update or delete records matching the filters.

This guide provides examples of using filters.

The `list` method is used in the example snippets of this reference, but you can use the same filtering mechanism with any method that accepts filters.

***

## Match Exact Value

```ts
const posts = await postModuleService.listPosts({
  title: "My Post 2",
})
```

If you pass a property with its value, only records whose properties exactly match the value are selected.

In the example above, only posts having the title `My Post 2` are retrieved.

***

## Doesn't Match Exact Value

```ts
const posts = await postModuleService.listPosts({
  title: {
    $ne: "My Post",
  },
})
```

To find records with a property that doesn't match a value, pass an object with a `$ne` property. Its value is the value that a record's property shouldn't match.

In the example above, only posts that don't have the title `My Post` are retrieved.

***

## Match Multiple Values

```ts
const posts = await postModuleService.listPosts({
  views: [
    50,
    100,
  ],
})
```

To find records with a property matching multiple values, pass an array of those values as the property's value in the filter.

In the example above, only posts having either `50` or `100` views are retrieved.

***

## Don't Match Multiple Values

```ts
const posts = await postModuleService.listPosts({
  title: {
    $nin: [
      "My Post",
    ],
  },
})
```

To find records with a property that doesn't match one or more values, pass an object with a `$nin` property. Its value is an array of multiple values that a record's property shouldn't match.

In the example above, only posts that don't have the title `My Post` are retrieved.

***

## Match Text Like Value

This filter only applies to text-like properties, including `text`, `id`, and `enum` properties.

```ts
const posts = await postModuleService.listPosts({
  title: {
    $like: "My%",
  },
})
```

To perform a `like` filter on a record's property, set the property's value to an object with a `$like` property. Its value is the string to use when applying the `like` filter.

The example above matches all posts whose title starts with `My`.

***

## Filter by Null or Not Null

To retrieve records with a property that is `null`, set the property's value to `null`.

For example:

```ts
const posts = await postModuleService.listPosts({
  published_at: null,
})
```

In the example above, only posts that have a `null` publish date are retrieved.

On the other hand, to retrieve records with a property that isn't `null`, set the property's value to an object with a `$ne` property.

For example:

```ts
const posts = await postModuleService.listPosts({
  published_at: {
    $ne: null,
  },
})
```

In the example above, only posts that have a publish date are retrieved.

***

## Apply Range Filters

This filter only applies to the `number` and `dateTime` properties.

```ts
const posts = await postModuleService.listPosts({
  published_at: {
    $lt: new Date(),
  },
})
```

To filter a record's property to be within a range, set the property's value to an object with any of the following properties:

1. `$lt`: The property's value must be less than the supplied value.
2. `$lte`: The property's value must be less than or equal to the supplied value.
3. `$gt`: The property's value must be greater than the supplied value.
4. `$gte`: The property's value must be greater than or equal the supplied value.

In the example above, only posts whose `published_at` property is before the current date and time are retrieved.

### Example: Retrieve Posts Published Today

```ts
const startToday = new Date()
startToday.setHours(0, 0, 0, 0)

const endToday = new Date()
endToday.setHours(23, 59, 59, 59)

const posts = await postModuleService.listPosts({
  published_at: {
    $gte: startToday,
    $lte: endToday,
  },
})
```

The `dateTime` property also stores the time. So, when matching for an exact day, you must set a range filter to be between the beginning and end of the day.

In this example, you retrieve the current date twice: once to set its time to `00:00:00`, and another to set its time `23:59:59`. Then, you retrieve posts whose `published_at` property is between `00:00:00` and `23:59:59` of today.

### Example: Range Filter on Number Property

```ts
const posts = await postModuleService.listPosts({
  views: {
    $gte: 50,
    $lte: 100,
  },
})
```

In the example above, only posts with `views` between `50` and `100` are retrieved.

***

## Apply Or Condition

```ts
const posts = await postModuleService.listPosts({
  $or: [
    {
      title: "My Post",
    },
    {
      published_at: {
        $lt: new Date(),
      },
    },
  ],
})
```

To use an `or` condition, pass to the filter object the `$or` property, whose value is an array of filters.

In the example above, posts whose title is `My Post` or their `published_at` date is less than the current date and time are retrieved.

***

## Apply And Condition

```ts
const posts = await postModuleService.listPosts({
  $and: [
    {
      title: "My Post",
    },
    {
      published_at: {
        $lt: new Date(),
      },
    },
  ],
})
```

To use an `and` condition, pass to the filter object the `$and` property, whose value is an array of filters.

In the example above, only posts whose title is `My Post` and their `published_at` date is less than the current date and time are retrieved.

***

## Complex Filters Example

```ts
const posts = await postModuleService.listPosts({
  $or: [
    {
      $and: [
        { views: { $gte: 50 } },
        { 
          published_at: {
            $gte: new Date(new Date().getFullYear(), 0, 1),
          },
        },
      ],
    },
    {
      title: {
        $like: "%Featured%",
      },
    },
  ],
})
```

In the example above, posts are retrieved if they meet either of the following conditions:

1. The post has at least `50` views and was published after the beginning of the current year.
2. The post's title contains the word `Featured`.

By combining `and` and `or` conditions, you can create complex filters to retrieve records that meet specific criteria.

***

## Supported Filter Operators List

The following operators are supported by the service factory filtering mechanism:

|Operator|Description|
|---|---|
|Comparison Operators|
|\`$eq\`|Matches values that are equal to a specified value.|
|\`$ne\`|Matches values that are not equal to a specified value.|
|\`$in\`|Matches any of the values specified in an array.|
|\`$nin\`|Matches none of the values specified in an array.|
|\`$like\`|Matches values containing a specified substring.|
|\`$lt\`|Matches values that are less than a specified value.|
|\`$lte\`|Matches values that are less than or equal to a specified value.|
|\`$gt\`|Matches values that are greater than a specified value.|
|\`$gte\`|Matches values that are greater than or equal to a specified value.|
|\`$re\`|Matches values that match a specified regular expression.|
|\`$ilike\`|Matches values containing a specified substring, case-insensitive.|
|\`$fulltext\`|Performs a full-text search on a text property.|
|\`$overlap\`|Matches values that have overlapping elements with the specified array.|
|\`$contains\`|Performs an |
|\`$contained\`|Performs an |
|Logical Operators|
|\`$and\`|Joins two or more conditions with a logical AND.|
|\`$or\`|Joins two or more conditions with a logical OR.|
|\`$not\`|Inverts the logic of a condition. For example, |




<H3 className="!mt-0">Just Getting Started?</H3>

Check out the [Medusa v2 Documentation](https://docs.medusajs.com/docs/index.html.md).

<H3 className="!mt-0">Medusa JS SDK</H3>

To use Medusa's JS SDK library, install the following packages in your project (not required for admin customizations):

```bash
npm install @medusajs/js-sdk@latest @medusajs/types@latest
```

Learn more about the JS SDK and how to configure it in [this documentation](https://docs.medusajs.com/resources/js-sdk/index.html.md).

### Download Full Reference

Download this reference as an OpenApi YAML file. You can import this file to tools like Postman and start sending requests directly to your Medusa application.




## Admin API Reference

- [GET /admin/api-keys](https://docs.medusajs.com/api/admin#api-keys_getapikeys)
- [POST /admin/api-keys](https://docs.medusajs.com/api/admin#api-keys_postapikeys)
- [GET /admin/api-keys/{id}](https://docs.medusajs.com/api/admin#api-keys_getapikeysid)
- [POST /admin/api-keys/{id}](https://docs.medusajs.com/api/admin#api-keys_postapikeysid)
- [DELETE /admin/api-keys/{id}](https://docs.medusajs.com/api/admin#api-keys_deleteapikeysid)
- [POST /admin/api-keys/{id}/revoke](https://docs.medusajs.com/api/admin#api-keys_postapikeysidrevoke)
- [POST /admin/api-keys/{id}/sales-channels](https://docs.medusajs.com/api/admin#api-keys_postapikeysidsaleschannels)
- [GET /admin/campaigns](https://docs.medusajs.com/api/admin#campaigns_getcampaigns)
- [POST /admin/campaigns](https://docs.medusajs.com/api/admin#campaigns_postcampaigns)
- [GET /admin/campaigns/{id}](https://docs.medusajs.com/api/admin#campaigns_getcampaignsid)
- [POST /admin/campaigns/{id}](https://docs.medusajs.com/api/admin#campaigns_postcampaignsid)
- [DELETE /admin/campaigns/{id}](https://docs.medusajs.com/api/admin#campaigns_deletecampaignsid)
- [POST /admin/campaigns/{id}/promotions](https://docs.medusajs.com/api/admin#campaigns_postcampaignsidpromotions)
- [GET /admin/claims](https://docs.medusajs.com/api/admin#claims_getclaims)
- [POST /admin/claims](https://docs.medusajs.com/api/admin#claims_postclaims)
- [GET /admin/claims/{id}](https://docs.medusajs.com/api/admin#claims_getclaimsid)
- [POST /admin/claims/{id}/cancel](https://docs.medusajs.com/api/admin#claims_postclaimsidcancel)
- [POST /admin/claims/{id}/claim-items](https://docs.medusajs.com/api/admin#claims_postclaimsidclaimitems)
- [POST /admin/claims/{id}/claim-items/{action_id}](https://docs.medusajs.com/api/admin#claims_postclaimsidclaimitemsaction_id)
- [DELETE /admin/claims/{id}/claim-items/{action_id}](https://docs.medusajs.com/api/admin#claims_deleteclaimsidclaimitemsaction_id)
- [POST /admin/claims/{id}/inbound/items](https://docs.medusajs.com/api/admin#claims_postclaimsidinbounditems)
- [POST /admin/claims/{id}/inbound/items/{action_id}](https://docs.medusajs.com/api/admin#claims_postclaimsidinbounditemsaction_id)
- [DELETE /admin/claims/{id}/inbound/items/{action_id}](https://docs.medusajs.com/api/admin#claims_deleteclaimsidinbounditemsaction_id)
- [POST /admin/claims/{id}/inbound/shipping-method](https://docs.medusajs.com/api/admin#claims_postclaimsidinboundshippingmethod)
- [POST /admin/claims/{id}/inbound/shipping-method/{action_id}](https://docs.medusajs.com/api/admin#claims_postclaimsidinboundshippingmethodaction_id)
- [DELETE /admin/claims/{id}/inbound/shipping-method/{action_id}](https://docs.medusajs.com/api/admin#claims_deleteclaimsidinboundshippingmethodaction_id)
- [POST /admin/claims/{id}/outbound/items](https://docs.medusajs.com/api/admin#claims_postclaimsidoutbounditems)
- [POST /admin/claims/{id}/outbound/items/{action_id}](https://docs.medusajs.com/api/admin#claims_postclaimsidoutbounditemsaction_id)
- [DELETE /admin/claims/{id}/outbound/items/{action_id}](https://docs.medusajs.com/api/admin#claims_deleteclaimsidoutbounditemsaction_id)
- [POST /admin/claims/{id}/outbound/shipping-method](https://docs.medusajs.com/api/admin#claims_postclaimsidoutboundshippingmethod)
- [POST /admin/claims/{id}/outbound/shipping-method/{action_id}](https://docs.medusajs.com/api/admin#claims_postclaimsidoutboundshippingmethodaction_id)
- [DELETE /admin/claims/{id}/outbound/shipping-method/{action_id}](https://docs.medusajs.com/api/admin#claims_deleteclaimsidoutboundshippingmethodaction_id)
- [POST /admin/claims/{id}/request](https://docs.medusajs.com/api/admin#claims_postclaimsidrequest)
- [DELETE /admin/claims/{id}/request](https://docs.medusajs.com/api/admin#claims_deleteclaimsidrequest)
- [GET /admin/collections](https://docs.medusajs.com/api/admin#collections_getcollections)
- [POST /admin/collections](https://docs.medusajs.com/api/admin#collections_postcollections)
- [GET /admin/collections/{id}](https://docs.medusajs.com/api/admin#collections_getcollectionsid)
- [POST /admin/collections/{id}](https://docs.medusajs.com/api/admin#collections_postcollectionsid)
- [DELETE /admin/collections/{id}](https://docs.medusajs.com/api/admin#collections_deletecollectionsid)
- [POST /admin/collections/{id}/products](https://docs.medusajs.com/api/admin#collections_postcollectionsidproducts)
- [GET /admin/currencies](https://docs.medusajs.com/api/admin#currencies_getcurrencies)
- [GET /admin/currencies/{code}](https://docs.medusajs.com/api/admin#currencies_getcurrenciescode)
- [GET /admin/customer-groups](https://docs.medusajs.com/api/admin#customer-groups_getcustomergroups)
- [POST /admin/customer-groups](https://docs.medusajs.com/api/admin#customer-groups_postcustomergroups)
- [GET /admin/customer-groups/{id}](https://docs.medusajs.com/api/admin#customer-groups_getcustomergroupsid)
- [POST /admin/customer-groups/{id}](https://docs.medusajs.com/api/admin#customer-groups_postcustomergroupsid)
- [DELETE /admin/customer-groups/{id}](https://docs.medusajs.com/api/admin#customer-groups_deletecustomergroupsid)
- [POST /admin/customer-groups/{id}/customers](https://docs.medusajs.com/api/admin#customer-groups_postcustomergroupsidcustomers)
- [GET /admin/customers](https://docs.medusajs.com/api/admin#customers_getcustomers)
- [POST /admin/customers](https://docs.medusajs.com/api/admin#customers_postcustomers)
- [GET /admin/customers/{id}](https://docs.medusajs.com/api/admin#customers_getcustomersid)
- [POST /admin/customers/{id}](https://docs.medusajs.com/api/admin#customers_postcustomersid)
- [DELETE /admin/customers/{id}](https://docs.medusajs.com/api/admin#customers_deletecustomersid)
- [GET /admin/customers/{id}/addresses](https://docs.medusajs.com/api/admin#customers_getcustomersidaddresses)
- [POST /admin/customers/{id}/addresses](https://docs.medusajs.com/api/admin#customers_postcustomersidaddresses)
- [GET /admin/customers/{id}/addresses/{address_id}](https://docs.medusajs.com/api/admin#customers_getcustomersidaddressesaddress_id)
- [POST /admin/customers/{id}/addresses/{address_id}](https://docs.medusajs.com/api/admin#customers_postcustomersidaddressesaddress_id)
- [DELETE /admin/customers/{id}/addresses/{address_id}](https://docs.medusajs.com/api/admin#customers_deletecustomersidaddressesaddress_id)
- [POST /admin/customers/{id}/customer-groups](https://docs.medusajs.com/api/admin#customers_postcustomersidcustomergroups)
- [GET /admin/draft-orders](https://docs.medusajs.com/api/admin#draft-orders_getdraftorders)
- [POST /admin/draft-orders](https://docs.medusajs.com/api/admin#draft-orders_postdraftorders)
- [GET /admin/draft-orders/{id}](https://docs.medusajs.com/api/admin#draft-orders_getdraftordersid)
- [POST /admin/draft-orders/{id}](https://docs.medusajs.com/api/admin#draft-orders_postdraftordersid)
- [POST /admin/draft-orders/{id}/convert-to-order](https://docs.medusajs.com/api/admin#draft-orders_postdraftordersidconverttoorder)
- [POST /admin/draft-orders/{id}/edit](https://docs.medusajs.com/api/admin#draft-orders_postdraftordersidedit)
- [DELETE /admin/draft-orders/{id}/edit](https://docs.medusajs.com/api/admin#draft-orders_deletedraftordersidedit)
- [POST /admin/draft-orders/{id}/edit/confirm](https://docs.medusajs.com/api/admin#draft-orders_postdraftordersideditconfirm)
- [POST /admin/draft-orders/{id}/edit/items](https://docs.medusajs.com/api/admin#draft-orders_postdraftordersidedititems)
- [POST /admin/draft-orders/{id}/edit/items/item/{item_id}](https://docs.medusajs.com/api/admin#draft-orders_postdraftordersidedititemsitemitem_id)
- [POST /admin/draft-orders/{id}/edit/items/{action_id}](https://docs.medusajs.com/api/admin#draft-orders_postdraftordersidedititemsaction_id)
- [DELETE /admin/draft-orders/{id}/edit/items/{action_id}](https://docs.medusajs.com/api/admin#draft-orders_deletedraftordersidedititemsaction_id)
- [POST /admin/draft-orders/{id}/edit/promotions](https://docs.medusajs.com/api/admin#draft-orders_postdraftordersideditpromotions)
- [DELETE /admin/draft-orders/{id}/edit/promotions](https://docs.medusajs.com/api/admin#draft-orders_deletedraftordersideditpromotions)
- [POST /admin/draft-orders/{id}/edit/request](https://docs.medusajs.com/api/admin#draft-orders_postdraftordersideditrequest)
- [POST /admin/draft-orders/{id}/edit/shipping-methods](https://docs.medusajs.com/api/admin#draft-orders_postdraftordersideditshippingmethods)
- [POST /admin/draft-orders/{id}/edit/shipping-methods/method/{method_id}](https://docs.medusajs.com/api/admin#draft-orders_postdraftordersideditshippingmethodsmethodmethod_id)
- [DELETE /admin/draft-orders/{id}/edit/shipping-methods/method/{method_id}](https://docs.medusajs.com/api/admin#draft-orders_deletedraftordersideditshippingmethodsmethodmethod_id)
- [POST /admin/draft-orders/{id}/edit/shipping-methods/{action_id}](https://docs.medusajs.com/api/admin#draft-orders_postdraftordersideditshippingmethodsaction_id)
- [DELETE /admin/draft-orders/{id}/edit/shipping-methods/{action_id}](https://docs.medusajs.com/api/admin#draft-orders_deletedraftordersideditshippingmethodsaction_id)
- [GET /admin/exchanges](https://docs.medusajs.com/api/admin#exchanges_getexchanges)
- [POST /admin/exchanges](https://docs.medusajs.com/api/admin#exchanges_postexchanges)
- [GET /admin/exchanges/{id}](https://docs.medusajs.com/api/admin#exchanges_getexchangesid)
- [POST /admin/exchanges/{id}/cancel](https://docs.medusajs.com/api/admin#exchanges_postexchangesidcancel)
- [POST /admin/exchanges/{id}/inbound/items](https://docs.medusajs.com/api/admin#exchanges_postexchangesidinbounditems)
- [POST /admin/exchanges/{id}/inbound/items/{action_id}](https://docs.medusajs.com/api/admin#exchanges_postexchangesidinbounditemsaction_id)
- [DELETE /admin/exchanges/{id}/inbound/items/{action_id}](https://docs.medusajs.com/api/admin#exchanges_deleteexchangesidinbounditemsaction_id)
- [POST /admin/exchanges/{id}/inbound/shipping-method](https://docs.medusajs.com/api/admin#exchanges_postexchangesidinboundshippingmethod)
- [POST /admin/exchanges/{id}/inbound/shipping-method/{action_id}](https://docs.medusajs.com/api/admin#exchanges_postexchangesidinboundshippingmethodaction_id)
- [DELETE /admin/exchanges/{id}/inbound/shipping-method/{action_id}](https://docs.medusajs.com/api/admin#exchanges_deleteexchangesidinboundshippingmethodaction_id)
- [POST /admin/exchanges/{id}/outbound/items](https://docs.medusajs.com/api/admin#exchanges_postexchangesidoutbounditems)
- [POST /admin/exchanges/{id}/outbound/items/{action_id}](https://docs.medusajs.com/api/admin#exchanges_postexchangesidoutbounditemsaction_id)
- [DELETE /admin/exchanges/{id}/outbound/items/{action_id}](https://docs.medusajs.com/api/admin#exchanges_deleteexchangesidoutbounditemsaction_id)
- [POST /admin/exchanges/{id}/outbound/shipping-method](https://docs.medusajs.com/api/admin#exchanges_postexchangesidoutboundshippingmethod)
- [POST /admin/exchanges/{id}/outbound/shipping-method/{action_id}](https://docs.medusajs.com/api/admin#exchanges_postexchangesidoutboundshippingmethodaction_id)
- [DELETE /admin/exchanges/{id}/outbound/shipping-method/{action_id}](https://docs.medusajs.com/api/admin#exchanges_deleteexchangesidoutboundshippingmethodaction_id)
- [POST /admin/exchanges/{id}/request](https://docs.medusajs.com/api/admin#exchanges_postexchangesidrequest)
- [DELETE /admin/exchanges/{id}/request](https://docs.medusajs.com/api/admin#exchanges_deleteexchangesidrequest)
- [GET /admin/fulfillment-providers](https://docs.medusajs.com/api/admin#fulfillment-providers_getfulfillmentproviders)
- [GET /admin/fulfillment-providers/{id}/options](https://docs.medusajs.com/api/admin#fulfillment-providers_getfulfillmentprovidersidoptions)
- [DELETE /admin/fulfillment-sets/{id}](https://docs.medusajs.com/api/admin#fulfillment-sets_deletefulfillmentsetsid)
- [POST /admin/fulfillment-sets/{id}/service-zones](https://docs.medusajs.com/api/admin#fulfillment-sets_postfulfillmentsetsidservicezones)
- [GET /admin/fulfillment-sets/{id}/service-zones/{zone_id}](https://docs.medusajs.com/api/admin#fulfillment-sets_getfulfillmentsetsidservicezoneszone_id)
- [POST /admin/fulfillment-sets/{id}/service-zones/{zone_id}](https://docs.medusajs.com/api/admin#fulfillment-sets_postfulfillmentsetsidservicezoneszone_id)
- [DELETE /admin/fulfillment-sets/{id}/service-zones/{zone_id}](https://docs.medusajs.com/api/admin#fulfillment-sets_deletefulfillmentsetsidservicezoneszone_id)
- [POST /admin/fulfillments](https://docs.medusajs.com/api/admin#fulfillments_postfulfillments)
- [POST /admin/fulfillments/{id}/cancel](https://docs.medusajs.com/api/admin#fulfillments_postfulfillmentsidcancel)
- [POST /admin/fulfillments/{id}/shipment](https://docs.medusajs.com/api/admin#fulfillments_postfulfillmentsidshipment)
- [GET /admin/inventory-items](https://docs.medusajs.com/api/admin#inventory-items_getinventoryitems)
- [POST /admin/inventory-items](https://docs.medusajs.com/api/admin#inventory-items_postinventoryitems)
- [POST /admin/inventory-items/location-levels/batch](https://docs.medusajs.com/api/admin#inventory-items_postinventoryitemslocationlevelsbatch)
- [GET /admin/inventory-items/{id}](https://docs.medusajs.com/api/admin#inventory-items_getinventoryitemsid)
- [POST /admin/inventory-items/{id}](https://docs.medusajs.com/api/admin#inventory-items_postinventoryitemsid)
- [DELETE /admin/inventory-items/{id}](https://docs.medusajs.com/api/admin#inventory-items_deleteinventoryitemsid)
- [GET /admin/inventory-items/{id}/location-levels](https://docs.medusajs.com/api/admin#inventory-items_getinventoryitemsidlocationlevels)
- [POST /admin/inventory-items/{id}/location-levels](https://docs.medusajs.com/api/admin#inventory-items_postinventoryitemsidlocationlevels)
- [POST /admin/inventory-items/{id}/location-levels/batch](https://docs.medusajs.com/api/admin#inventory-items_postinventoryitemsidlocationlevelsbatch)
- [POST /admin/inventory-items/{id}/location-levels/{location_id}](https://docs.medusajs.com/api/admin#inventory-items_postinventoryitemsidlocationlevelslocation_id)
- [DELETE /admin/inventory-items/{id}/location-levels/{location_id}](https://docs.medusajs.com/api/admin#inventory-items_deleteinventoryitemsidlocationlevelslocation_id)
- [GET /admin/invites](https://docs.medusajs.com/api/admin#invites_getinvites)
- [POST /admin/invites](https://docs.medusajs.com/api/admin#invites_postinvites)
- [POST /admin/invites/accept](https://docs.medusajs.com/api/admin#invites_postinvitesaccept)
- [GET /admin/invites/{id}](https://docs.medusajs.com/api/admin#invites_getinvitesid)
- [DELETE /admin/invites/{id}](https://docs.medusajs.com/api/admin#invites_deleteinvitesid)
- [POST /admin/invites/{id}/resend](https://docs.medusajs.com/api/admin#invites_postinvitesidresend)
- [GET /admin/notifications](https://docs.medusajs.com/api/admin#notifications_getnotifications)
- [GET /admin/notifications/{id}](https://docs.medusajs.com/api/admin#notifications_getnotificationsid)
- [POST /admin/order-edits](https://docs.medusajs.com/api/admin#order-edits_postorderedits)
- [DELETE /admin/order-edits/{id}](https://docs.medusajs.com/api/admin#order-edits_deleteordereditsid)
- [POST /admin/order-edits/{id}/confirm](https://docs.medusajs.com/api/admin#order-edits_postordereditsidconfirm)
- [POST /admin/order-edits/{id}/items](https://docs.medusajs.com/api/admin#order-edits_postordereditsiditems)
- [POST /admin/order-edits/{id}/items/item/{item_id}](https://docs.medusajs.com/api/admin#order-edits_postordereditsiditemsitemitem_id)
- [POST /admin/order-edits/{id}/items/{action_id}](https://docs.medusajs.com/api/admin#order-edits_postordereditsiditemsaction_id)
- [DELETE /admin/order-edits/{id}/items/{action_id}](https://docs.medusajs.com/api/admin#order-edits_deleteordereditsiditemsaction_id)
- [POST /admin/order-edits/{id}/request](https://docs.medusajs.com/api/admin#order-edits_postordereditsidrequest)
- [POST /admin/order-edits/{id}/shipping-method](https://docs.medusajs.com/api/admin#order-edits_postordereditsidshippingmethod)
- [POST /admin/order-edits/{id}/shipping-method/{action_id}](https://docs.medusajs.com/api/admin#order-edits_postordereditsidshippingmethodaction_id)
- [DELETE /admin/order-edits/{id}/shipping-method/{action_id}](https://docs.medusajs.com/api/admin#order-edits_deleteordereditsidshippingmethodaction_id)
- [GET /admin/orders](https://docs.medusajs.com/api/admin#orders_getorders)
- [GET /admin/orders/{id}](https://docs.medusajs.com/api/admin#orders_getordersid)
- [POST /admin/orders/{id}](https://docs.medusajs.com/api/admin#orders_postordersid)
- [POST /admin/orders/{id}/archive](https://docs.medusajs.com/api/admin#orders_postordersidarchive)
- [POST /admin/orders/{id}/cancel](https://docs.medusajs.com/api/admin#orders_postordersidcancel)
- [GET /admin/orders/{id}/changes](https://docs.medusajs.com/api/admin#orders_getordersidchanges)
- [POST /admin/orders/{id}/complete](https://docs.medusajs.com/api/admin#orders_postordersidcomplete)
- [POST /admin/orders/{id}/credit-lines](https://docs.medusajs.com/api/admin#orders_postordersidcreditlines)
- [POST /admin/orders/{id}/fulfillments](https://docs.medusajs.com/api/admin#orders_postordersidfulfillments)
- [POST /admin/orders/{id}/fulfillments/{fulfillment_id}/cancel](https://docs.medusajs.com/api/admin#orders_postordersidfulfillmentsfulfillment_idcancel)
- [POST /admin/orders/{id}/fulfillments/{fulfillment_id}/mark-as-delivered](https://docs.medusajs.com/api/admin#orders_postordersidfulfillmentsfulfillment_idmarkasdelivered)
- [POST /admin/orders/{id}/fulfillments/{fulfillment_id}/shipments](https://docs.medusajs.com/api/admin#orders_postordersidfulfillmentsfulfillment_idshipments)
- [GET /admin/orders/{id}/line-items](https://docs.medusajs.com/api/admin#orders_getordersidlineitems)
- [GET /admin/orders/{id}/preview](https://docs.medusajs.com/api/admin#orders_getordersidpreview)
- [POST /admin/orders/{id}/transfer](https://docs.medusajs.com/api/admin#orders_postordersidtransfer)
- [POST /admin/orders/{id}/transfer/cancel](https://docs.medusajs.com/api/admin#orders_postordersidtransfercancel)
- [POST /admin/payment-collections](https://docs.medusajs.com/api/admin#payment-collections_postpaymentcollections)
- [DELETE /admin/payment-collections/{id}](https://docs.medusajs.com/api/admin#payment-collections_deletepaymentcollectionsid)
- [POST /admin/payment-collections/{id}/mark-as-paid](https://docs.medusajs.com/api/admin#payment-collections_postpaymentcollectionsidmarkaspaid)
- [GET /admin/payments](https://docs.medusajs.com/api/admin#payments_getpayments)
- [GET /admin/payments/payment-providers](https://docs.medusajs.com/api/admin#payments_getpaymentspaymentproviders)
- [GET /admin/payments/{id}](https://docs.medusajs.com/api/admin#payments_getpaymentsid)
- [POST /admin/payments/{id}/capture](https://docs.medusajs.com/api/admin#payments_postpaymentsidcapture)
- [POST /admin/payments/{id}/refund](https://docs.medusajs.com/api/admin#payments_postpaymentsidrefund)
- [GET /admin/plugins](https://docs.medusajs.com/api/admin#plugins_getplugins)
- [GET /admin/price-lists](https://docs.medusajs.com/api/admin#price-lists_getpricelists)
- [POST /admin/price-lists](https://docs.medusajs.com/api/admin#price-lists_postpricelists)
- [GET /admin/price-lists/{id}](https://docs.medusajs.com/api/admin#price-lists_getpricelistsid)
- [POST /admin/price-lists/{id}](https://docs.medusajs.com/api/admin#price-lists_postpricelistsid)
- [DELETE /admin/price-lists/{id}](https://docs.medusajs.com/api/admin#price-lists_deletepricelistsid)
- [POST /admin/price-lists/{id}/prices/batch](https://docs.medusajs.com/api/admin#price-lists_postpricelistsidpricesbatch)
- [POST /admin/price-lists/{id}/products](https://docs.medusajs.com/api/admin#price-lists_postpricelistsidproducts)
- [GET /admin/price-preferences](https://docs.medusajs.com/api/admin#price-preferences_getpricepreferences)
- [POST /admin/price-preferences](https://docs.medusajs.com/api/admin#price-preferences_postpricepreferences)
- [GET /admin/price-preferences/{id}](https://docs.medusajs.com/api/admin#price-preferences_getpricepreferencesid)
- [POST /admin/price-preferences/{id}](https://docs.medusajs.com/api/admin#price-preferences_postpricepreferencesid)
- [DELETE /admin/price-preferences/{id}](https://docs.medusajs.com/api/admin#price-preferences_deletepricepreferencesid)
- [GET /admin/product-categories](https://docs.medusajs.com/api/admin#product-categories_getproductcategories)
- [POST /admin/product-categories](https://docs.medusajs.com/api/admin#product-categories_postproductcategories)
- [GET /admin/product-categories/{id}](https://docs.medusajs.com/api/admin#product-categories_getproductcategoriesid)
- [POST /admin/product-categories/{id}](https://docs.medusajs.com/api/admin#product-categories_postproductcategoriesid)
- [DELETE /admin/product-categories/{id}](https://docs.medusajs.com/api/admin#product-categories_deleteproductcategoriesid)
- [POST /admin/product-categories/{id}/products](https://docs.medusajs.com/api/admin#product-categories_postproductcategoriesidproducts)
- [GET /admin/product-tags](https://docs.medusajs.com/api/admin#product-tags_getproducttags)
- [POST /admin/product-tags](https://docs.medusajs.com/api/admin#product-tags_postproducttags)
- [GET /admin/product-tags/{id}](https://docs.medusajs.com/api/admin#product-tags_getproducttagsid)
- [POST /admin/product-tags/{id}](https://docs.medusajs.com/api/admin#product-tags_postproducttagsid)
- [DELETE /admin/product-tags/{id}](https://docs.medusajs.com/api/admin#product-tags_deleteproducttagsid)
- [GET /admin/product-types](https://docs.medusajs.com/api/admin#product-types_getproducttypes)
- [POST /admin/product-types](https://docs.medusajs.com/api/admin#product-types_postproducttypes)
- [GET /admin/product-types/{id}](https://docs.medusajs.com/api/admin#product-types_getproducttypesid)
- [POST /admin/product-types/{id}](https://docs.medusajs.com/api/admin#product-types_postproducttypesid)
- [DELETE /admin/product-types/{id}](https://docs.medusajs.com/api/admin#product-types_deleteproducttypesid)
- [GET /admin/product-variants](https://docs.medusajs.com/api/admin#product-variants_getproductvariants)
- [GET /admin/products](https://docs.medusajs.com/api/admin#products_getproducts)
- [POST /admin/products](https://docs.medusajs.com/api/admin#products_postproducts)
- [POST /admin/products/batch](https://docs.medusajs.com/api/admin#products_postproductsbatch)
- [POST /admin/products/export](https://docs.medusajs.com/api/admin#products_postproductsexport)
- [POST /admin/products/import](https://docs.medusajs.com/api/admin#products_postproductsimport)
- [POST /admin/products/import/{transaction_id}/confirm](https://docs.medusajs.com/api/admin#products_postproductsimporttransaction_idconfirm)
- [GET /admin/products/{id}](https://docs.medusajs.com/api/admin#products_getproductsid)
- [POST /admin/products/{id}](https://docs.medusajs.com/api/admin#products_postproductsid)
- [DELETE /admin/products/{id}](https://docs.medusajs.com/api/admin#products_deleteproductsid)
- [GET /admin/products/{id}/options](https://docs.medusajs.com/api/admin#products_getproductsidoptions)
- [POST /admin/products/{id}/options](https://docs.medusajs.com/api/admin#products_postproductsidoptions)
- [GET /admin/products/{id}/options/{option_id}](https://docs.medusajs.com/api/admin#products_getproductsidoptionsoption_id)
- [POST /admin/products/{id}/options/{option_id}](https://docs.medusajs.com/api/admin#products_postproductsidoptionsoption_id)
- [DELETE /admin/products/{id}/options/{option_id}](https://docs.medusajs.com/api/admin#products_deleteproductsidoptionsoption_id)
- [GET /admin/products/{id}/variants](https://docs.medusajs.com/api/admin#products_getproductsidvariants)
- [POST /admin/products/{id}/variants](https://docs.medusajs.com/api/admin#products_postproductsidvariants)
- [POST /admin/products/{id}/variants/batch](https://docs.medusajs.com/api/admin#products_postproductsidvariantsbatch)
- [POST /admin/products/{id}/variants/inventory-items/batch](https://docs.medusajs.com/api/admin#products_postproductsidvariantsinventoryitemsbatch)
- [GET /admin/products/{id}/variants/{variant_id}](https://docs.medusajs.com/api/admin#products_getproductsidvariantsvariant_id)
- [POST /admin/products/{id}/variants/{variant_id}](https://docs.medusajs.com/api/admin#products_postproductsidvariantsvariant_id)
- [DELETE /admin/products/{id}/variants/{variant_id}](https://docs.medusajs.com/api/admin#products_deleteproductsidvariantsvariant_id)
- [POST /admin/products/{id}/variants/{variant_id}/inventory-items](https://docs.medusajs.com/api/admin#products_postproductsidvariantsvariant_idinventoryitems)
- [POST /admin/products/{id}/variants/{variant_id}/inventory-items/{inventory_item_id}](https://docs.medusajs.com/api/admin#products_postproductsidvariantsvariant_idinventoryitemsinventory_item_id)
- [DELETE /admin/products/{id}/variants/{variant_id}/inventory-items/{inventory_item_id}](https://docs.medusajs.com/api/admin#products_deleteproductsidvariantsvariant_idinventoryitemsinventory_item_id)
- [GET /admin/promotions](https://docs.medusajs.com/api/admin#promotions_getpromotions)
- [POST /admin/promotions](https://docs.medusajs.com/api/admin#promotions_postpromotions)
- [GET /admin/promotions/rule-attribute-options/{rule_type}](https://docs.medusajs.com/api/admin#promotions_getpromotionsruleattributeoptionsrule_type)
- [GET /admin/promotions/rule-value-options/{rule_type}/{rule_attribute_id}](https://docs.medusajs.com/api/admin#promotions_getpromotionsrulevalueoptionsrule_typerule_attribute_id)
- [GET /admin/promotions/{id}](https://docs.medusajs.com/api/admin#promotions_getpromotionsid)
- [POST /admin/promotions/{id}](https://docs.medusajs.com/api/admin#promotions_postpromotionsid)
- [DELETE /admin/promotions/{id}](https://docs.medusajs.com/api/admin#promotions_deletepromotionsid)
- [POST /admin/promotions/{id}/buy-rules/batch](https://docs.medusajs.com/api/admin#promotions_postpromotionsidbuyrulesbatch)
- [POST /admin/promotions/{id}/rules/batch](https://docs.medusajs.com/api/admin#promotions_postpromotionsidrulesbatch)
- [POST /admin/promotions/{id}/target-rules/batch](https://docs.medusajs.com/api/admin#promotions_postpromotionsidtargetrulesbatch)
- [GET /admin/promotions/{id}/{rule_type}](https://docs.medusajs.com/api/admin#promotions_getpromotionsidrule_type)
- [GET /admin/refund-reasons](https://docs.medusajs.com/api/admin#refund-reasons_getrefundreasons)
- [POST /admin/refund-reasons](https://docs.medusajs.com/api/admin#refund-reasons_postrefundreasons)
- [GET /admin/refund-reasons/{id}](https://docs.medusajs.com/api/admin#refund-reasons_getrefundreasonsid)
- [POST /admin/refund-reasons/{id}](https://docs.medusajs.com/api/admin#refund-reasons_postrefundreasonsid)
- [DELETE /admin/refund-reasons/{id}](https://docs.medusajs.com/api/admin#refund-reasons_deleterefundreasonsid)
- [GET /admin/regions](https://docs.medusajs.com/api/admin#regions_getregions)
- [POST /admin/regions](https://docs.medusajs.com/api/admin#regions_postregions)
- [GET /admin/regions/{id}](https://docs.medusajs.com/api/admin#regions_getregionsid)
- [POST /admin/regions/{id}](https://docs.medusajs.com/api/admin#regions_postregionsid)
- [DELETE /admin/regions/{id}](https://docs.medusajs.com/api/admin#regions_deleteregionsid)
- [GET /admin/reservations](https://docs.medusajs.com/api/admin#reservations_getreservations)
- [POST /admin/reservations](https://docs.medusajs.com/api/admin#reservations_postreservations)
- [GET /admin/reservations/{id}](https://docs.medusajs.com/api/admin#reservations_getreservationsid)
- [POST /admin/reservations/{id}](https://docs.medusajs.com/api/admin#reservations_postreservationsid)
- [DELETE /admin/reservations/{id}](https://docs.medusajs.com/api/admin#reservations_deletereservationsid)
- [GET /admin/return-reasons](https://docs.medusajs.com/api/admin#return-reasons_getreturnreasons)
- [POST /admin/return-reasons](https://docs.medusajs.com/api/admin#return-reasons_postreturnreasons)
- [GET /admin/return-reasons/{id}](https://docs.medusajs.com/api/admin#return-reasons_getreturnreasonsid)
- [POST /admin/return-reasons/{id}](https://docs.medusajs.com/api/admin#return-reasons_postreturnreasonsid)
- [DELETE /admin/return-reasons/{id}](https://docs.medusajs.com/api/admin#return-reasons_deletereturnreasonsid)
- [GET /admin/returns](https://docs.medusajs.com/api/admin#returns_getreturns)
- [POST /admin/returns](https://docs.medusajs.com/api/admin#returns_postreturns)
- [GET /admin/returns/{id}](https://docs.medusajs.com/api/admin#returns_getreturnsid)
- [POST /admin/returns/{id}](https://docs.medusajs.com/api/admin#returns_postreturnsid)
- [POST /admin/returns/{id}/cancel](https://docs.medusajs.com/api/admin#returns_postreturnsidcancel)
- [POST /admin/returns/{id}/dismiss-items](https://docs.medusajs.com/api/admin#returns_postreturnsiddismissitems)
- [POST /admin/returns/{id}/dismiss-items/{action_id}](https://docs.medusajs.com/api/admin#returns_postreturnsiddismissitemsaction_id)
- [DELETE /admin/returns/{id}/dismiss-items/{action_id}](https://docs.medusajs.com/api/admin#returns_deletereturnsiddismissitemsaction_id)
- [POST /admin/returns/{id}/receive-items](https://docs.medusajs.com/api/admin#returns_postreturnsidreceiveitems)
- [POST /admin/returns/{id}/receive-items/{action_id}](https://docs.medusajs.com/api/admin#returns_postreturnsidreceiveitemsaction_id)
- [DELETE /admin/returns/{id}/receive-items/{action_id}](https://docs.medusajs.com/api/admin#returns_deletereturnsidreceiveitemsaction_id)
- [POST /admin/returns/{id}/receive](https://docs.medusajs.com/api/admin#returns_postreturnsidreceive)
- [DELETE /admin/returns/{id}/receive](https://docs.medusajs.com/api/admin#returns_deletereturnsidreceive)
- [POST /admin/returns/{id}/receive/confirm](https://docs.medusajs.com/api/admin#returns_postreturnsidreceiveconfirm)
- [POST /admin/returns/{id}/request-items](https://docs.medusajs.com/api/admin#returns_postreturnsidrequestitems)
- [POST /admin/returns/{id}/request-items/{action_id}](https://docs.medusajs.com/api/admin#returns_postreturnsidrequestitemsaction_id)
- [DELETE /admin/returns/{id}/request-items/{action_id}](https://docs.medusajs.com/api/admin#returns_deletereturnsidrequestitemsaction_id)
- [POST /admin/returns/{id}/request](https://docs.medusajs.com/api/admin#returns_postreturnsidrequest)
- [DELETE /admin/returns/{id}/request](https://docs.medusajs.com/api/admin#returns_deletereturnsidrequest)
- [POST /admin/returns/{id}/shipping-method](https://docs.medusajs.com/api/admin#returns_postreturnsidshippingmethod)
- [POST /admin/returns/{id}/shipping-method/{action_id}](https://docs.medusajs.com/api/admin#returns_postreturnsidshippingmethodaction_id)
- [DELETE /admin/returns/{id}/shipping-method/{action_id}](https://docs.medusajs.com/api/admin#returns_deletereturnsidshippingmethodaction_id)
- [GET /admin/sales-channels](https://docs.medusajs.com/api/admin#sales-channels_getsaleschannels)
- [POST /admin/sales-channels](https://docs.medusajs.com/api/admin#sales-channels_postsaleschannels)
- [GET /admin/sales-channels/{id}](https://docs.medusajs.com/api/admin#sales-channels_getsaleschannelsid)
- [POST /admin/sales-channels/{id}](https://docs.medusajs.com/api/admin#sales-channels_postsaleschannelsid)
- [DELETE /admin/sales-channels/{id}](https://docs.medusajs.com/api/admin#sales-channels_deletesaleschannelsid)
- [POST /admin/sales-channels/{id}/products](https://docs.medusajs.com/api/admin#sales-channels_postsaleschannelsidproducts)
- [GET /admin/shipping-options](https://docs.medusajs.com/api/admin#shipping-options_getshippingoptions)
- [POST /admin/shipping-options](https://docs.medusajs.com/api/admin#shipping-options_postshippingoptions)
- [GET /admin/shipping-options/{id}](https://docs.medusajs.com/api/admin#shipping-options_getshippingoptionsid)
- [POST /admin/shipping-options/{id}](https://docs.medusajs.com/api/admin#shipping-options_postshippingoptionsid)
- [DELETE /admin/shipping-options/{id}](https://docs.medusajs.com/api/admin#shipping-options_deleteshippingoptionsid)
- [POST /admin/shipping-options/{id}/rules/batch](https://docs.medusajs.com/api/admin#shipping-options_postshippingoptionsidrulesbatch)
- [GET /admin/shipping-profiles](https://docs.medusajs.com/api/admin#shipping-profiles_getshippingprofiles)
- [POST /admin/shipping-profiles](https://docs.medusajs.com/api/admin#shipping-profiles_postshippingprofiles)
- [GET /admin/shipping-profiles/{id}](https://docs.medusajs.com/api/admin#shipping-profiles_getshippingprofilesid)
- [POST /admin/shipping-profiles/{id}](https://docs.medusajs.com/api/admin#shipping-profiles_postshippingprofilesid)
- [DELETE /admin/shipping-profiles/{id}](https://docs.medusajs.com/api/admin#shipping-profiles_deleteshippingprofilesid)
- [GET /admin/stock-locations](https://docs.medusajs.com/api/admin#stock-locations_getstocklocations)
- [POST /admin/stock-locations](https://docs.medusajs.com/api/admin#stock-locations_poststocklocations)
- [GET /admin/stock-locations/{id}](https://docs.medusajs.com/api/admin#stock-locations_getstocklocationsid)
- [POST /admin/stock-locations/{id}](https://docs.medusajs.com/api/admin#stock-locations_poststocklocationsid)
- [DELETE /admin/stock-locations/{id}](https://docs.medusajs.com/api/admin#stock-locations_deletestocklocationsid)
- [POST /admin/stock-locations/{id}/fulfillment-providers](https://docs.medusajs.com/api/admin#stock-locations_poststocklocationsidfulfillmentproviders)
- [POST /admin/stock-locations/{id}/fulfillment-sets](https://docs.medusajs.com/api/admin#stock-locations_poststocklocationsidfulfillmentsets)
- [POST /admin/stock-locations/{id}/sales-channels](https://docs.medusajs.com/api/admin#stock-locations_poststocklocationsidsaleschannels)
- [GET /admin/stores](https://docs.medusajs.com/api/admin#stores_getstores)
- [GET /admin/stores/{id}](https://docs.medusajs.com/api/admin#stores_getstoresid)
- [POST /admin/stores/{id}](https://docs.medusajs.com/api/admin#stores_poststoresid)
- [GET /admin/tax-providers](https://docs.medusajs.com/api/admin#tax-providers_gettaxproviders)
- [GET /admin/tax-rates](https://docs.medusajs.com/api/admin#tax-rates_gettaxrates)
- [POST /admin/tax-rates](https://docs.medusajs.com/api/admin#tax-rates_posttaxrates)
- [GET /admin/tax-rates/{id}](https://docs.medusajs.com/api/admin#tax-rates_gettaxratesid)
- [POST /admin/tax-rates/{id}](https://docs.medusajs.com/api/admin#tax-rates_posttaxratesid)
- [DELETE /admin/tax-rates/{id}](https://docs.medusajs.com/api/admin#tax-rates_deletetaxratesid)
- [POST /admin/tax-rates/{id}/rules](https://docs.medusajs.com/api/admin#tax-rates_posttaxratesidrules)
- [DELETE /admin/tax-rates/{id}/rules/{rule_id}](https://docs.medusajs.com/api/admin#tax-rates_deletetaxratesidrulesrule_id)
- [GET /admin/tax-regions](https://docs.medusajs.com/api/admin#tax-regions_gettaxregions)
- [POST /admin/tax-regions](https://docs.medusajs.com/api/admin#tax-regions_posttaxregions)
- [GET /admin/tax-regions/{id}](https://docs.medusajs.com/api/admin#tax-regions_gettaxregionsid)
- [POST /admin/tax-regions/{id}](https://docs.medusajs.com/api/admin#tax-regions_posttaxregionsid)
- [DELETE /admin/tax-regions/{id}](https://docs.medusajs.com/api/admin#tax-regions_deletetaxregionsid)
- [POST /admin/uploads](https://docs.medusajs.com/api/admin#uploads_postuploads)
- [GET /admin/uploads/{id}](https://docs.medusajs.com/api/admin#uploads_getuploadsid)
- [DELETE /admin/uploads/{id}](https://docs.medusajs.com/api/admin#uploads_deleteuploadsid)
- [GET /admin/users](https://docs.medusajs.com/api/admin#users_getusers)
- [GET /admin/users/me](https://docs.medusajs.com/api/admin#users_getusersme)
- [GET /admin/users/{id}](https://docs.medusajs.com/api/admin#users_getusersid)
- [POST /admin/users/{id}](https://docs.medusajs.com/api/admin#users_postusersid)
- [DELETE /admin/users/{id}](https://docs.medusajs.com/api/admin#users_deleteusersid)
- [GET /admin/workflows-executions](https://docs.medusajs.com/api/admin#workflows-executions_getworkflowsexecutions)
- [GET /admin/workflows-executions/{id}](https://docs.medusajs.com/api/admin#workflows-executions_getworkflowsexecutionsid)
- [POST /admin/workflows-executions/{workflow_id}/run](https://docs.medusajs.com/api/admin#workflows-executions_postworkflowsexecutionsworkflow_idrun)
- [POST /admin/workflows-executions/{workflow_id}/steps/failure](https://docs.medusajs.com/api/admin#workflows-executions_postworkflowsexecutionsworkflow_idstepsfailure)
- [POST /admin/workflows-executions/{workflow_id}/steps/success](https://docs.medusajs.com/api/admin#workflows-executions_postworkflowsexecutionsworkflow_idstepssuccess)
- [GET /admin/workflows-executions/{workflow_id}/subscribe](https://docs.medusajs.com/api/admin#workflows-executions_getworkflowsexecutionsworkflow_idsubscribe)
- [GET /admin/workflows-executions/{workflow_id}/{transaction_id}](https://docs.medusajs.com/api/admin#workflows-executions_getworkflowsexecutionsworkflow_idtransaction_id)
- [GET /admin/workflows-executions/{workflow_id}/{transaction_id}/{step_id}/subscribe](https://docs.medusajs.com/api/admin#workflows-executions_getworkflowsexecutionsworkflow_idtransaction_idstep_idsubscribe)
- [POST /auth/session](https://docs.medusajs.com/api/admin#auth_postsession)
- [DELETE /auth/session](https://docs.medusajs.com/api/admin#auth_deletesession)
- [POST /auth/token/refresh](https://docs.medusajs.com/api/admin#auth_postadminauthtokenrefresh)
- [POST /auth/user/{auth_provider}](https://docs.medusajs.com/api/admin#auth_postactor_typeauth_provider)
- [POST /auth/user/{auth_provider}/callback](https://docs.medusajs.com/api/admin#auth_postactor_typeauth_providercallback)
- [POST /auth/user/{auth_provider}/register](https://docs.medusajs.com/api/admin#auth_postactor_typeauth_provider_register)
- [POST /auth/user/{auth_provider}/reset-password](https://docs.medusajs.com/api/admin#auth_postactor_typeauth_providerresetpassword)
- [POST /auth/user/{auth_provider}/update](https://docs.medusajs.com/api/admin#auth_postactor_typeauth_providerupdate)


## Store API Reference

- [POST /auth/customer/{auth_provider}](https://docs.medusajs.com/api/store#auth_postactor_typeauth_provider)
- [POST /auth/customer/{auth_provider}/callback](https://docs.medusajs.com/api/store#auth_postactor_typeauth_providercallback)
- [POST /auth/customer/{auth_provider}/register](https://docs.medusajs.com/api/store#auth_postactor_typeauth_provider_register)
- [POST /auth/customer/{auth_provider}/reset-password](https://docs.medusajs.com/api/store#auth_postactor_typeauth_providerresetpassword)
- [POST /auth/customer/{auth_provider}/update](https://docs.medusajs.com/api/store#auth_postactor_typeauth_providerupdate)
- [POST /auth/session](https://docs.medusajs.com/api/store#auth_postsession)
- [DELETE /auth/session](https://docs.medusajs.com/api/store#auth_deletesession)
- [POST /auth/token/refresh](https://docs.medusajs.com/api/store#auth_postadminauthtokenrefresh)
- [POST /store/carts](https://docs.medusajs.com/api/store#carts_postcarts)
- [GET /store/carts/{id}](https://docs.medusajs.com/api/store#carts_getcartsid)
- [POST /store/carts/{id}](https://docs.medusajs.com/api/store#carts_postcartsid)
- [POST /store/carts/{id}/complete](https://docs.medusajs.com/api/store#carts_postcartsidcomplete)
- [POST /store/carts/{id}/customer](https://docs.medusajs.com/api/store#carts_postcartsidcustomer)
- [POST /store/carts/{id}/line-items](https://docs.medusajs.com/api/store#carts_postcartsidlineitems)
- [POST /store/carts/{id}/line-items/{line_id}](https://docs.medusajs.com/api/store#carts_postcartsidlineitemsline_id)
- [DELETE /store/carts/{id}/line-items/{line_id}](https://docs.medusajs.com/api/store#carts_deletecartsidlineitemsline_id)
- [POST /store/carts/{id}/promotions](https://docs.medusajs.com/api/store#carts_postcartsidpromotions)
- [DELETE /store/carts/{id}/promotions](https://docs.medusajs.com/api/store#carts_deletecartsidpromotions)
- [POST /store/carts/{id}/shipping-methods](https://docs.medusajs.com/api/store#carts_postcartsidshippingmethods)
- [POST /store/carts/{id}/taxes](https://docs.medusajs.com/api/store#carts_postcartsidtaxes)
- [GET /store/collections](https://docs.medusajs.com/api/store#collections_getcollections)
- [GET /store/collections/{id}](https://docs.medusajs.com/api/store#collections_getcollectionsid)
- [GET /store/currencies](https://docs.medusajs.com/api/store#currencies_getcurrencies)
- [GET /store/currencies/{code}](https://docs.medusajs.com/api/store#currencies_getcurrenciescode)
- [POST /store/customers](https://docs.medusajs.com/api/store#customers_postcustomers)
- [GET /store/customers/me](https://docs.medusajs.com/api/store#customers_getcustomersme)
- [POST /store/customers/me](https://docs.medusajs.com/api/store#customers_postcustomersme)
- [GET /store/customers/me/addresses](https://docs.medusajs.com/api/store#customers_getcustomersmeaddresses)
- [POST /store/customers/me/addresses](https://docs.medusajs.com/api/store#customers_postcustomersmeaddresses)
- [GET /store/customers/me/addresses/{address_id}](https://docs.medusajs.com/api/store#customers_getcustomersmeaddressesaddress_id)
- [POST /store/customers/me/addresses/{address_id}](https://docs.medusajs.com/api/store#customers_postcustomersmeaddressesaddress_id)
- [DELETE /store/customers/me/addresses/{address_id}](https://docs.medusajs.com/api/store#customers_deletecustomersmeaddressesaddress_id)
- [GET /store/orders](https://docs.medusajs.com/api/store#orders_getorders)
- [GET /store/orders/{id}](https://docs.medusajs.com/api/store#orders_getordersid)
- [POST /store/orders/{id}/transfer/accept](https://docs.medusajs.com/api/store#orders_postordersidtransferaccept)
- [POST /store/orders/{id}/transfer/cancel](https://docs.medusajs.com/api/store#orders_postordersidtransfercancel)
- [POST /store/orders/{id}/transfer/decline](https://docs.medusajs.com/api/store#orders_postordersidtransferdecline)
- [POST /store/orders/{id}/transfer/request](https://docs.medusajs.com/api/store#orders_postordersidtransferrequest)
- [POST /store/payment-collections](https://docs.medusajs.com/api/store#payment-collections_postpaymentcollections)
- [POST /store/payment-collections/{id}/payment-sessions](https://docs.medusajs.com/api/store#payment-collections_postpaymentcollectionsidpaymentsessions)
- [GET /store/payment-providers](https://docs.medusajs.com/api/store#payment-providers_getpaymentproviders)
- [GET /store/product-categories](https://docs.medusajs.com/api/store#product-categories_getproductcategories)
- [GET /store/product-categories/{id}](https://docs.medusajs.com/api/store#product-categories_getproductcategoriesid)
- [GET /store/product-tags](https://docs.medusajs.com/api/store#product-tags_getproducttags)
- [GET /store/product-tags/{id}](https://docs.medusajs.com/api/store#product-tags_getproducttagsid)
- [GET /store/product-types](https://docs.medusajs.com/api/store#product-types_getproducttypes)
- [GET /store/product-types/{id}](https://docs.medusajs.com/api/store#product-types_getproducttypesid)
- [GET /store/products](https://docs.medusajs.com/api/store#products_getproducts)
- [GET /store/products/{id}](https://docs.medusajs.com/api/store#products_getproductsid)
- [GET /store/regions](https://docs.medusajs.com/api/store#regions_getregions)
- [GET /store/regions/{id}](https://docs.medusajs.com/api/store#regions_getregionsid)
- [GET /store/return-reasons](https://docs.medusajs.com/api/store#return-reasons_getreturnreasons)
- [GET /store/return-reasons/{id}](https://docs.medusajs.com/api/store#return-reasons_getreturnreasonsid)
- [POST /store/returns](https://docs.medusajs.com/api/store#returns_postreturns)
- [GET /store/shipping-options](https://docs.medusajs.com/api/store#shipping-options_getshippingoptions)
- [POST /store/shipping-options/{id}/calculate](https://docs.medusajs.com/api/store#shipping-options_postshippingoptionsidcalculate)


# Introduction

Primitives for building Medusa applications.

Welcome to Medusa UI, a React implementation of the Medusa design system.
It is a collection of components, hooks, utility functions, icons, and [Tailwind CSS](https://tailwindcss.com/) classes that can be used to build
a consistent user interface across the Medusa Admin and other Medusa applications.

## Packages

***

Medusa UI is split into multiple packages. Each package is published to npm
and can be installed separately.

- `@medusajs/ui` - React components, hooks, and utility functions used
  in Medusa UI.
- `@medusajs/ui-preset` - Tailwind CSS preset containing all the classes
  used in Medusa UI.
- `@medusajs/icons` - Icons used in Medusa UI.

## About

***

At its core, Medusa UI is a styled and slightly opinionated implementation of [Radix Primitives](https://www.radix-ui.com/primitives).
Our team have also referenced the fantastic [shadcn/ui](https://ui.shadcn.com/) for inspiration in certain implementations.

Our team strongly believe in keeping the components simple and
composable, much like Medusa's foundation. This allows you to build whatever you need. Our team have tried to avoid overloading
the component API and, instead, leveraged the native HTML API, which gets implemented
and respected accordingly, and passed to the underlying elements.


# Installation

How to install and setup Medusa UI.



# Alert

A component for displaying important messages.

```tsx
import { Alert } from "@medusajs/ui"

export default function AlertDemo() {
  return <Alert>You are viewing Medusa docs.</Alert>
}

```

## Usage

***

```tsx
import { Alert } from "@medusajs/ui"
```

```tsx
<Alert>Here's a message</Alert>
```

## API Reference

***

### Alert Props

This component is based on the div element and supports all of its props

- variant: (union) The variant of the alert Default: "info"
- dismissible: (boolean) Whether the alert is dismissible Default: false

## Examples

***

### Success Alert

```tsx
import { Alert } from "@medusajs/ui"

export default function AlertSuccess() {
  return <Alert variant="success">Data updated successfully!</Alert>
}

```

### Warning Alert

```tsx
import { Alert } from "@medusajs/ui"

export default function AlertWarning() {
  return <Alert variant="warning">Be careful!</Alert>
}

```

### Error Alert

```tsx
import { Alert } from "@medusajs/ui"

export default function AlertError() {
  return <Alert variant="error">An error occured while updating data.</Alert>
}

```

### Dismissable Alert

```tsx
import { Alert } from "@medusajs/ui"

export default function AlertDismissable() {
  return <Alert dismissible={true}>You are viewing Medusa docs.</Alert>
}

```


# Avatar

An image element with a fallback for representing the user.

```tsx
import { Avatar } from "@medusajs/ui"

export default function AvatarDemo() {
  return (
    <Avatar
      src="https://avatars.githubusercontent.com/u/10656202?v=4"
      fallback="M"
    />
  )
}

```

## Usage

***

```tsx
import { Avatar } from "@medusajs/ui"
```

```tsx
<Avatar
  src="https://avatars.githubusercontent.com/u/10656202?v=4"
  fallback="M"
/>
```

## API Reference

***

### Avatar Props

This component is based on the \[Radix UI Avatar]\(https://www.radix-ui.com/primitives/docs/components/avatar) primitive.

- src: (string) The URL of the image used in the Avatar.
- fallback: (string) Text to show in the avatar if the URL provided in \`src\` can't be opened.
- variant: (union) The style of the avatar. Default: "rounded"
- size: (union) The size of the avatar's border radius. Default: "base"


# Badge

Displays a badge

```tsx
import { Badge } from "@medusajs/ui"

export default function BadgeDemo() {
  return <Badge>Badge</Badge>
}

```

## Usage

***

```tsx
import { Badge } from "@medusajs/ui"
```

```tsx
<Badge>Badge</Badge>
```

## API Reference

***

### Badge Props

This component is based on the \`div\` element and supports all of its props

- asChild: (boolean) Whether to remove the wrapper \`span\` element and use the
  passed child element instead. Default: false
- size: (union) The badge's size. Default: "base"
- rounded: (union) The style of the badge's border radius. Default: "base"
- color: (union) The badge's color. Default: "grey"

## Examples

***

### Grey

```tsx
import { Badge } from "@medusajs/ui"

export default function BadgeGrey() {
  return <Badge color="grey">Badge</Badge>
}

```

### Red

```tsx
import { Badge } from "@medusajs/ui"

export default function BadgeRed() {
  return <Badge color="red">Badge</Badge>
}

```

### Green

```tsx
import { Badge } from "@medusajs/ui"

export default function BadgeGreen() {
  return <Badge color="green">Badge</Badge>
}

```

### Blue

```tsx
import { Badge } from "@medusajs/ui"

export default function BadgeBlue() {
  return <Badge color="blue">Badge</Badge>
}

```

### Orange

```tsx
import { Badge } from "@medusajs/ui"

export default function BadgeOrange() {
  return <Badge color="orange">Badge</Badge>
}

```

### Purple

```tsx
import { Badge } from "@medusajs/ui"

export default function BadgePurple() {
  return <Badge color="purple">Badge</Badge>
}

```

### Small

```tsx
import { Badge } from "@medusajs/ui"

export default function BadgeSmall() {
  return <Badge size="small">Badge</Badge>
}

```

### Large

```tsx
import { Badge } from "@medusajs/ui"

export default function BadgeLarge() {
  return <Badge size="large">Badge</Badge>
}

```

### Rounded Full

```tsx
import { Badge } from "@medusajs/ui"

export default function BadgeRoundedFull() {
  return <Badge rounded="full">Badge</Badge>
}

```

### Rounded Base

```tsx
import { Badge } from "@medusajs/ui"

export default function BadgeRoundedBase() {
  return <Badge rounded="base">Badge</Badge>
}

```


# Button

Displays a button

```tsx
import { Button } from "@medusajs/ui"

export default function ButtonDemo() {
  return <Button>Button</Button>
}

```

## Usage

***

```tsx
import { Button } from "@medusajs/ui"
```

```tsx
<Button>Button</Button>
```

## API Reference

***

### Button Props

This component is based on the \`button\` element and supports all of its props

- isLoading: (boolean) Whether to show a loading spinner. Default: false
- asChild: (boolean) Whether to remove the wrapper \`button\` element and use the
  passed child element instead. Default: false
- variant: (union) The button's style. Default: "primary"
- size: (union) The button's size. Default: "base"

## Examples

***

### Primary

```tsx
import { Button } from "@medusajs/ui"

export default function ButtonPrimary() {
  return <Button>Button</Button>
}

```

### Secondary

```tsx
import { Button } from "@medusajs/ui"

export default function ButtonSecondary() {
  return <Button variant="secondary">Button</Button>
}

```

### Transparent

```tsx
import { Button } from "@medusajs/ui"

export default function ButtonTransparent() {
  return <Button variant="transparent">Button</Button>
}

```

### Danger

```tsx
import { Button } from "@medusajs/ui"

export default function ButtonDanger() {
  return <Button variant="danger">Button</Button>
}

```

### Disabled

```tsx
import { Button } from "@medusajs/ui"

export default function ButtonDisabled() {
  return <Button disabled={true}>Button</Button>
}

```

### With Icon

```tsx
import { PlusMini } from "@medusajs/icons"
import { Button } from "@medusajs/ui"

export default function ButtonWithIcon() {
  return (
    <Button>
      Button <PlusMini />
    </Button>
  )
}

```

### Loading

```tsx
import { Button } from "@medusajs/ui"

export default function ButtonLoading() {
  return <Button isLoading={true}>Button</Button>
}

```


# Calendar

A calendar component that allows picking of a single date or a range of dates.

```tsx
import { Calendar } from "@medusajs/ui"
import * as React from "react"

export default function CalendarDemo() {
  const [date, setDate] = React.useState<Date | null>()

  return <Calendar value={date} onChange={setDate} />
}

```

## Usage

***

```tsx
import { Calendar } from "@medusajs/ui"
```

```tsx
<Calendar />
```

## API Reference

***

### Calendar Props

Calendar component used to select a date.
Its props are based on \[React Aria Calendar]\(https://react-spectrum.adobe.com/react-aria/Calendar.html#calendar-1).

- autoFocus: (boolean) Whether to automatically focus the calendar when it mounts.
- defaultFocusedValue: (DateValue) The date that is focused when the calendar first mounts (uncountrolled).
- errorMessage: (ReactNode) An error message to display when the selected value is invalid.
- focusedValue: (DateValue) Controls the currently focused date within the calendar.
- isDisabled: (boolean) Whether the calendar is disabled.
- isInvalid: (boolean) Whether the current selection is invalid according to application logic.
- isReadOnly: (boolean) Whether the calendar value is immutable.
- onFocusChange: (signature) Handler that is called when the focused date changes.
- pageBehavior: (PageBehavior) Controls the behavior of paging. Pagination either works by advancing the visible page by visibleDuration (default) or one unit of visibleDuration.
- validationState: (ValidationState) Whether the current selection is valid or invalid according to application logic.


# Checkbox

A control that allows the user to toggle between checked and not checked.

```tsx
import { Checkbox, Label } from "@medusajs/ui"

export default function CheckboxDemo() {
  return (
    <div className="flex items-center space-x-2">
      <Checkbox id="billing-shipping" />
      <Label htmlFor="billing-shipping">
        Billing address same as shipping address
      </Label>
    </div>
  )
}

```

## Usage

***

```tsx
import { Checkbox } from "@medusajs/ui"
```

```tsx
<Checkbox />
```

## API Reference

***

### Checkbox Props

This component is based on the \[Radix UI Checkbox]\(https://www.radix-ui.com/primitives/docs/components/checkbox) primitive.



## Examples

***

### Default

```tsx
import { Checkbox, Label } from "@medusajs/ui"

export default function CheckboxDefault() {
  return (
    <div className="flex items-center space-x-2">
      <Checkbox id="billing-shipping-default" />
      <Label htmlFor="billing-shipping-default">
        Billing address same as shipping address
      </Label>
    </div>
  )
}

```

### Checked

```tsx
import { Checkbox, Label } from "@medusajs/ui"

export default function CheckboxChecked() {
  return (
    <div className="flex items-center space-x-2">
      <Checkbox id="billing-shipping-checked" checked={true} />
      <Label htmlFor="billing-shipping-checked">
        Billing address same as shipping address
      </Label>
    </div>
  )
}

```

### Disabled

```tsx
import { Checkbox, Label } from "@medusajs/ui"

export default function CheckboxDisabled() {
  return (
    <div className="flex items-center space-x-2">
      <Checkbox id="billing-shipping-disabled" disabled={true} />
      <Label htmlFor="billing-shipping-disabled">
        Billing address same as shipping address
      </Label>
    </div>
  )
}

```

### Indeterminate

```tsx
import { Checkbox, Label } from "@medusajs/ui"

export default function CheckboxIndeterminate() {
  return (
    <div className="flex items-center space-x-2">
      <Checkbox id="billing-shipping-indeterminate" checked={"indeterminate"} />
      <Label htmlFor="billing-shipping-indeterminate">
        Billing address same as shipping address
      </Label>
    </div>
  )
}

```


# Code Block

Allows you to render highlighted code snippets

```tsx
import { CodeBlock, Label } from "@medusajs/ui"

const snippets = [
  {
    label: "cURL",
    language: "bash",
    code: `curl 'http://localhost:9000/store/products/PRODUCT_ID'\n -H 'x-publishable-key: YOUR_API_KEY'`,
    hideLineNumbers: true,
  },
  {
    label: "Medusa JS SDK",
    language: "jsx",
    code: `// Install the JS SDK in your storefront project: @medusajs/js-sdk\n\nimport Medusa from "@medusajs/js-sdk"\n\nconst medusa = new Medusa({\n  baseUrl: import.meta.env.NEXT_PUBLIC_BACKEND_URL || "/",\n  publishableKey: process.env.NEXT_PUBLIC_MEDUSA_PAK\n})\nconst { product } = await medusa.store.products.retrieve("prod_123")\nconsole.log(product.id)`,
  },
]

export default function CodeBlockDemo() {
  return (
    <div className="w-full">
      <CodeBlock snippets={snippets}>
        <CodeBlock.Header>
          <CodeBlock.Header.Meta>
            <Label weight={"plus"}>/product-detail.js</Label>
          </CodeBlock.Header.Meta>
        </CodeBlock.Header>
        <CodeBlock.Body />
      </CodeBlock>
    </div>
  )
}

```

## Usage

***

```tsx
import { CodeBlock } from "@medusajs/ui"
```

```tsx
<CodeBlock 
  snippets={[
    { 
      language: "tsx", 
      label: "Label", 
      code: "import { useProduct } from \"medusa-react\"",
    },
  ]}
>
  <CodeBlock.Header />
  <CodeBlock.Body />
</CodeBlock>
```

## API Reference

***

### CodeBlock Props

This component is based on the \`div\` element and supports all of its props

- snippets: (Array) The code snippets.

### CodeBlock.Header Props

This component is based on the \`div\` element and supports all of its props

- hideLabels: (boolean) Whether to hide the code snippets' labels. Default: false

### CodeBlock.Header.Meta Props

This component is based on the \`div\` element and supports all of its props



### CodeBlock.Body Props

This component is based on the \`div\` element and supports all of its props



## Usage Outside Medusa Admin

***

If you're using the `CodeBlock` component in a project other than the Medusa Admin, make sure to include the `TooltipProvider` somewhere up in your component tree, as the `CodeBlock.Header` component uses a [Tooltip](https://docs.medusajs.com/components/tooltip#usage-outside-medusa-admin/index.html.md):

```tsx
<TooltipProvider>
  <CodeBlock 
    snippets={[
      { 
        language: "tsx", 
        label: "Label", 
        code: "import { useProduct } from \"medusa-react\"",
      },
    ]}
  >
    <CodeBlock.Header />
    <CodeBlock.Body />
  </CodeBlock>
</TooltipProvider>
```

## Examples

***

### Single snippet

If you want to only show a code sample for one language or API, you can choose to hide the snippet labels:

```tsx
import { CodeBlock, Label } from "@medusajs/ui"

const snippets = [
  {
    label: "Medusa JS SDK",
    language: "jsx",
    code: `// Install the JS SDK in your storefront project: @medusajs/js-sdk\n\nimport Medusa from "@medusajs/js-sdk"\n\nconst medusa = new Medusa({\n  baseUrl: import.meta.env.NEXT_PUBLIC_BACKEND_URL || "/",\n  publishableKey: process.env.NEXT_PUBLIC_MEDUSA_PAK\n})\nconst { product } = await medusa.store.products.retrieve("prod_123")\nconsole.log(product.id)`,
  },
]

export default function CodeBlockSingle() {
  return (
    <div className="w-full">
      <CodeBlock snippets={snippets}>
        <CodeBlock.Header hideLabels={true}>
          <CodeBlock.Header.Meta>
            <Label weight={"plus"}>/product-detail.js</Label>
          </CodeBlock.Header.Meta>
        </CodeBlock.Header>
        <CodeBlock.Body />
      </CodeBlock>
    </div>
  )
}

```

### No Header

You could also choose to omit the header entirely:

```tsx
import { CodeBlock } from "@medusajs/ui"

const snippets = [
  {
    label: "Medusa JS SDK",
    language: "jsx",
    code: `// Install the JS SDK in your storefront project: @medusajs/js-sdk\n\nimport Medusa from "@medusajs/js-sdk"\n\nconst medusa = new Medusa({\n  baseUrl: import.meta.env.NEXT_PUBLIC_BACKEND_URL || "/",\n  publishableKey: process.env.NEXT_PUBLIC_MEDUSA_PAK\n})\nconst { product } = await medusa.store.products.retrieve("prod_123")\nconsole.log(product.id)`,
  },
]

export default function CodeBlockNoHeader() {
  return (
    <div className="w-full">
      <CodeBlock snippets={snippets}>
        <CodeBlock.Body />
      </CodeBlock>
    </div>
  )
}

```

### No Line Numbers

```tsx
import { CodeBlock, Label } from "@medusajs/ui"

const snippets = [
  {
    label: "Medusa JS SDK",
    language: "jsx",
    code: `// Install the JS SDK in your storefront project: @medusajs/js-sdk\n\nimport Medusa from "@medusajs/js-sdk"\n\nconst medusa = new Medusa({\n  baseUrl: import.meta.env.NEXT_PUBLIC_BACKEND_URL || "/",\n  publishableKey: process.env.NEXT_PUBLIC_MEDUSA_PAK\n})\nconst { product } = await medusa.store.products.retrieve("prod_123")\nconsole.log(product.id)`,
    hideLineNumbers: true,
  },
]

export default function CodeBlockNoLines() {
  return (
    <div className="w-full">
      <CodeBlock snippets={snippets}>
        <CodeBlock.Header>
          <CodeBlock.Header.Meta>
            <Label weight={"plus"}>/product-detail.js</Label>
          </CodeBlock.Header.Meta>
        </CodeBlock.Header>
        <CodeBlock.Body />
      </CodeBlock>
    </div>
  )
}

```

### No Copy Button

```tsx
import { CodeBlock, Label } from "@medusajs/ui"

const snippets = [
  {
    label: "Medusa JS SDK",
    language: "jsx",
    code: `// Install the JS SDK in your storefront project: @medusajs/js-sdk\n\nimport Medusa from "@medusajs/js-sdk"\n\nconst medusa = new Medusa({\n  baseUrl: import.meta.env.NEXT_PUBLIC_BACKEND_URL || "/",\n  publishableKey: process.env.NEXT_PUBLIC_MEDUSA_PAK\n})\nconst { product } = await medusa.store.products.retrieve("prod_123")\nconsole.log(product.id)`,
    hideCopy: true,
  },
]

export default function CodeBlockNoCopy() {
  return (
    <div className="w-full">
      <CodeBlock snippets={snippets}>
        <CodeBlock.Header>
          <CodeBlock.Header.Meta>
            <Label weight={"plus"}>/product-detail.js</Label>
          </CodeBlock.Header.Meta>
        </CodeBlock.Header>
        <CodeBlock.Body />
      </CodeBlock>
    </div>
  )
}

```


# Command Bar

Display a command bar with a list of commands

```tsx
import { Checkbox, CommandBar, Label, Text } from "@medusajs/ui"
import * as React from "react"

export default function CommandBarDemo() {
  const [selected, setSelected] = React.useState<boolean>(false)

  return (
    <div className="flex justify-center gap-y-2 flex-col">
      <div className="flex items-center gap-x-2">
        <Checkbox
          checked={selected}
          onCheckedChange={(checked) =>
            setSelected(checked === true ? true : false)
          }
        />
        <Label>Item One</Label>
      </div>
      <div><Text size="small" className="text-ui-fg-muted">Check the box to view the command bar</Text></div>
      <CommandBar open={selected}>
        <CommandBar.Bar>
          <CommandBar.Value>1 selected</CommandBar.Value>
          <CommandBar.Seperator />
          <CommandBar.Command
            action={() => {
              alert("Delete")
            }}
            label="Delete"
            shortcut="d"
          />
          <CommandBar.Seperator />
          <CommandBar.Command
            action={() => {
              alert("Edit")
            }}
            label="Edit"
            shortcut="e"
          />
        </CommandBar.Bar>
      </CommandBar>
    </div>
  )
}

```

## Usage

***

```tsx
import { CommandBar } from "@medusajs/ui"
```

```tsx
<CommandBar open={open}>
  <CommandBar.Bar>
    <CommandBar.Value>{count} selected</CommandBar.Value>
    <CommandBar.Seperator />
    <CommandBar.Command
      action={onDelete}
      label="Delete"
      shortcut="d"
    />
    <CommandBar.Seperator />
    <CommandBar.Command
      action={onEdit}
      label="Edit"
      shortcut="e"
    />
  </CommandBar.Bar>
</CommandBar>
```

## API Reference

***

### CommandBar Props

The root component of the command bar. This component manages the state of the command bar.

- open: (boolean) Whether to open (show) the command bar. Default: false
- onOpenChange: (signature) Specify a function to handle the change of \`open\`'s value.
- defaultOpen: (boolean) Whether the command bar is open by default. Default: false
- disableAutoFocus: (boolean) Whether to disable focusing automatically on the command bar when it's opened. Default: true

### CommandBar.Bar Props

The bar component of the command bar. This component is used to display the commands.



### CommandBar.Value Props

The value component of the command bar. This component is used to display a value,
such as the number of selected items which the commands will act on.



### CommandBar.Seperator Props

The seperator component of the command bar. This component is used to display a seperator between commands.



### CommandBar.Command Props

The command component of the command bar. This component is used to display a command, as well as registering the keyboad shortcut.

- action: (signature) The function to execute when the command is triggered.
- label: (string) The command's label.
- shortcut: (string) The command's shortcut


# Command

Renders an unhighlighted code block, useful for one-liners or API Routes

```tsx
import { Badge, Command } from "@medusajs/ui"

export default function CommandDemo() {
  return (
    <div className="w-full">
      <Command>
        <Badge color="green">Get</Badge>
        <code>localhost:9000/store/products</code>
        <Command.Copy
          content="localhost:9000/store/products"
          className="ml-auto"
        />
      </Command>
    </div>
  )
}

```

## Usage

***

```tsx
import { Command } from "@medusajs/ui"
```

```tsx
<Command>
  <code>yarn add @medusajs/ui</code>
</Command>
```

## API Reference

***

### Command Props

This component is based on the div element and supports all of its props



## Usage Outside Medusa Admin

***

If you're using the `Command` component in a project other than the Medusa Admin, make sure to include the `TooltipProvider` somewhere up in your component tree, as the `Command.Copy` component uses a [Tooltip](https://docs.medusajs.com/components/tooltip#usage-outside-medusa-admin/index.html.md):

```tsx
<TooltipProvider>
  <Command>
    <code>yarn add @medusajs/ui</code>
    <Command.Copy
      content="yarn add @medusajs/ui"
      className="ml-auto"
    />
  </Command>
</TooltipProvider>
```


# Container

A box used to organise content.

```tsx
import { Container } from "@medusajs/ui"

export default function ContainerDemo() {
  return <Container>Content</Container>
}

```

## Usage

***

```tsx
import { Container } from "@medusajs/ui"
```

```tsx
<Container>Container</Container>
```

## API Reference

***

### Container Props

This component is based on the \`div\` element and supports all of its props



## Examples

***

### In a layout

```tsx
import { Container, Heading } from "@medusajs/ui"

export default function ContainerLayout() {
  return (
    <div className="flex h-full w-full">
      <div className="border-ui-border-base w-full max-w-[216px] border-r p-4">
        <Heading level="h3">Menubar</Heading>
      </div>
      <div className="flex w-full flex-col gap-y-2 px-8 pb-8 pt-6">
        <Container>
          <Heading>Section 1</Heading>
        </Container>
        <Container>
          <Heading>Section 2</Heading>
        </Container>
        <Container>
          <Heading>Section 3</Heading>
        </Container>
      </div>
    </div>
  )
}

```


# Copy

Displays a tooltipped button that puts string contents into the user's clipboard.

```tsx
import { Copy } from "@medusajs/ui"

export default function CopyDemo() {
  return <Copy content="yarn add @medusajs/ui" />
}

```

## Usage

***

```tsx
import { Copy } from "@medusajs/ui"
```

```tsx
<Copy content="yarn add @medusajs/ui" />
```

## API Reference

***

### Copy Props

This component is based on the \`button\` element and supports all of its props

- content: (string) The content to copy.
- variant: (union) The variant of the copy button. Default: "default"
- asChild: (boolean) Whether to remove the wrapper \`button\` element and use the
  passed child element instead. Default: false

## Usage Outside Medusa Admin

***

If you're using the `Copy` component in a project other than the Medusa Admin, make sure to include the `TooltipProvider` somewhere up in your component tree, as the `Copy` component uses a [Tooltip](https://docs.medusajs.com/components/tooltip#usage-outside-medusa-admin/index.html.md):

```tsx
<TooltipProvider>
  <Copy content="yarn add @medusajs/ui" />
</TooltipProvider>
```

## Examples

***

### With custom display

```tsx
import { Code, Copy } from "@medusajs/ui"

export default function CopyDemo() {
  return (
    <Copy content="yarn add @medusajs/ui">
      <Code>yarn add @medusajs/ui</Code>
    </Copy>
  )
}

```

### As child

Using the `asChild` prop, you can render the `<Copy/>` as it's child. This is useful if you want to render a custom button, to prevent rendering a button inside a button.

```tsx
import { PlusMini } from "@medusajs/icons"
import { Copy, IconButton, Text } from "@medusajs/ui"

export default function CopyAsChild() {
  return (
    <div className="flex items-center gap-x-2">
      <Text>Copy command</Text>
      <Copy content="yarn add @medusajs/ui" asChild>
        <IconButton>
          <PlusMini />
        </IconButton>
      </Copy>
    </div>
  )
}

```


# Currency Input

Input component for currency values

```tsx
import { CurrencyInput } from "@medusajs/ui"

export default function CurrencyInputDemo() {
  return (
    <div className="max-w-[250px]">
      <CurrencyInput symbol="$" code="usd" />
    </div>
  )
}

```

## Usage

***

```tsx
import { CurrencyInput } from "@medusajs/ui"
```

```tsx
<CurrencyInput symbol="$" code="usd" />
```

## API Reference

***

### CurrencyInput Props

This component is based on the input element and supports all of its props

- symbol: (string) The symbol to show in the input.
- code: (string) The currency code to show in the input.
- size: (union) The input's size. Default: "base"
- allowDecimals: (boolean) Allow decimals

  Default = true
- allowNegativeValue: (boolean) Allow user to enter negative value

  Default = true
- className: (string) Class names
- customInput: (ElementType) Custom component

  Default = \<input/>
- decimalScale: (number) Specify decimal scale for padding/trimming

  Example:
  &#x20; 1.5 -> 1.50
  &#x20; 1.234 -> 1.23
- decimalSeparator: (string) Separator between integer part and fractional part of value.

  This cannot be a number
- decimalsLimit: (number) Limit length of decimals allowed

  Default = 2
- defaultValue: (union) Default value if not passing in value via props
- disableAbbreviations: (boolean) Disable abbreviations (m, k, b)

  Default = false
- disabled: (boolean) Disabled

  Default = false
- disableGroupSeparators: (boolean) Disable auto adding separator between values eg. 1000 -> 1,000

  Default = false
- fixedDecimalLength: (number) Value will always have the specified length of decimals

  Example:
  &#x20; 123 -> 1.23

  Note: This formatting only happens onBlur
- formatValueOnBlur: (boolean) When set to false, the formatValueOnBlur flag disables the application of the \_\_onValueChange\_\_ function
  specifically on blur events. If disabled or set to false, the onValueChange will not trigger on blur.
  Default = true
- groupSeparator: (string) Separator between thousand, million and billion

  This cannot be a number
- id: (string) Component id
- intlConfig: (IntlConfig) International locale config, examples:
  &#x20; \{ locale: 'ja-JP', currency: 'JPY' }
  &#x20; \{ locale: 'en-IN', currency: 'INR' }

  Any prefix, groupSeparator or decimalSeparator options passed in
  will override Intl Locale config
- maxLength: (number) Maximum characters the user can enter
- onValueChange: (signature) Handle change in value
- placeholder: (string) Placeholder if there is no value
- step: (number) Incremental value change on arrow down and arrow up key press
- transformRawValue: (signature) Transform the raw value form the input before parsing

## Examples

***

### Base

```tsx
import { CurrencyInput } from "@medusajs/ui"

export default function CurrencyInputBase() {
  return (
    <div className="max-w-[250px]">
      <CurrencyInput size="base" symbol="$" code="usd" />
    </div>
  )
}

```

### Small

```tsx
import { CurrencyInput } from "@medusajs/ui"

export default function CurrencyInputSmall() {
  return (
    <div className="max-w-[250px]">
      <CurrencyInput size="small" symbol="$" code="usd" />
    </div>
  )
}

```


# DataTable

A Table component with advanced functionalities like pagination, filtering, and more.

The `DataTable` component is useful if you're displaying large data with functionalities like pagination, filtering, sorting, and searching. It's also the recommended table component to use when creating customizations in the Medusa Admin.

This component is available after Medusa UI v4.0.4 (or Medusa v2.4.0). It is built on top of the [Table](https://docs.medusajs.com/components/table/index.html.md) component. If you want a table with more control over its styling and functionality, use that component instead.

## Simple Example

***

```tsx
import { createDataTableColumnHelper, useDataTable, DataTable, Heading } from "@medusajs/ui"

const products = [
  {
    id: "1",
    title: "Shirt",
    price: 10
  },
  {
    id: "2",
    title: "Pants",
    price: 20
  }
]

const columnHelper = createDataTableColumnHelper<typeof products[0]>()

const columns = [
  columnHelper.accessor("title", {
    header: "Title",
    enableSorting: true,
  }),
  columnHelper.accessor("price", {
    header: "Price",
    enableSorting: true,
  }),
]

export default function ProductTable () {
  const table = useDataTable({
    columns,
    data: products,
    getRowId: (product) => product.id,
    rowCount: products.length,
    isLoading: false,
  })
  
  return (
    <DataTable instance={table}>
	    <DataTable.Toolbar className="flex flex-col items-start justify-between gap-2 md:flex-row md:items-center">
	      <Heading>Products</Heading>
      </DataTable.Toolbar>
      <DataTable.Table />
    </DataTable>
  )
}
```

## Usage

***

You import the `DataTable` component from `@medusajs/ui`.

```tsx
import { 
  DataTable,
} from "@medusajs/ui"
```

### Columns Preparation

Before using the `DataTable` component, you need to prepare its columns using the `createDataTableColumnHelper` utility:

```tsx
import {
  // ...
  createDataTableColumnHelper,
} from "@medusajs/ui"

const data = [
  {
    id: "1",
    title: "Shirt",
    price: 10
  },
  // other data...
]

const columnHelper = createDataTableColumnHelper<typeof data[0]>()

const columns = [
  columnHelper.accessor("title", {
    header: "Title",
    enableSorting: true,
  }),
  columnHelper.accessor("price", {
    header: "Price",
  }),
]
```

The `createDataTableColumnHelper` utility is a function that returns a helper used to generates column configurations for the `DataTable` component.

For each column in the table, use the `accessor` method of the column helper to specify configurations for a specific column. The `accessor` method accepts the column's key in the table's data as the first parameter, and an object with the following properties as the second parameter:

- `header`: The table header text for the column.
- `enableSorting`: (optional) A boolean that indicates whether data in the table can be sorted by this column. More on sorting in [this section](#configure-sorting-in-datatable).

### Create Table Instance

The `DataTable` component expects a table instance created using the `useDataTable` hook. Import that hook from `@medusajs/ui`:

```tsx
import {
  // ...
  useDataTable,
} from "@medusajs/ui"
```

Then, inside the component that will render `DataTable`, create a table instance using the `useDataTable` hook:

```tsx
export default function ProductTable () {
  const table = useDataTable({
    columns,
    data,
    getRowId: (product) => product.id,
    rowCount: data.length,
    isLoading: false,
  })
}
```

The `useDataTable` hook accepts an object with the following properties:

- `columns`: An array of column configurations generated using the `createDataTableColumnHelper` utility.
- `data`: The data to be displayed in the table.
- `getRowId`: A function that returns the unique identifier of a row. The identifier must be a string.
- `rowCount`: The total number of rows in the table. If you're fetching data from the Medusa application with pagination or filters, this will be the total count, not the count of the data returned in the current page.
- `isLoading`: A boolean that indicates whether the table is loading data. This is useful when loading data from the Medusa application for the first time or in between pages.

### Render DataTable

Finally, render the `DataTable` component with the table instance created using the `useDataTable` hook:

```tsx
export default function ProductTable () {
  // ...
  return (
    <DataTable instance={table}>
      <DataTable.Toolbar className="flex flex-col items-start justify-between gap-2 md:flex-row md:items-center">
        <Heading>Products</Heading>
      </DataTable.Toolbar>
      <DataTable.Table />
    </DataTable>
  )
}
```

In the `DataTable` component, you pass the following child components:

1. `DataTable.Toolbar`: The toolbar component shown at the top of the table. You can also add buttons for custom actions.
2. `DataTable.Table`: The table component that renders the data.

Refer to the examples later on this page to learn how to add pagination, filtering, and other functionalities using the `DataTable` component.

## API Reference

***

### DataTable Props

This component creates a data table with filters, pagination, sorting, and more.
It's built on top of the \`Table\` component while expanding its functionality.
The \`DataTable\` is useful to create tables similar to those in the Medusa Admin dashboard.

- instance: (UseDataTableReturn) The instance returned by the \`useDataTable\` hook.
- children: (ReactReactNode) The children of the component.
- className: (string) Additional classes to pass to the wrapper \`div\` of the component.

### DataTable.Table Props

This component renders the table in a data table, supporting advanced features.

- emptyState: (signature) The empty state to display when the table is empty.

### DataTable.Pagination Props

This component adds a pagination component and functionality to the data table.

- translations: (ReactComponentProps\["translations"]) The translations for strings in the pagination component.

### DataTable.FilterMenu Props

This component adds a filter menu to the data table, allowing users
to filter the table's data.

- tooltip: (string) The tooltip to show when hovering over the filter menu.

### DataTable.Search Props

This component adds a search input to the data table, allowing users
to search through the table's data.

- autoFocus: (boolean) If true, the search input will be focused on mount.
- className: (string) Additional classes to pass to the search input.
- placeholder: (string) The placeholder text to show in the search input.

### DataTable.CommandBar Props

This component adds a command bar to the data table, which is used
to show commands that can be executed on the selected rows.

- selectedLabel: (union) The label to show when items are selected. If a function is passed,&#x20;
  it will be called with the count of selected items.

### DataTable.SortingMenu Props

This component adds a sorting menu to the data table, allowing users
to sort the table's data.

- tooltip: (string) The tooltip to show when hovering over the sorting menu.

## Example with Data Fetching

***

Refer to [this Admin Components guide](https://docs.medusajs.com/resources/admin-components/components/data-table/index.html.md) for an example on using the `DataTable` component with data fetching from the Medusa application.

## Handle Row Click

***

```tsx
import { createDataTableColumnHelper, useDataTable, DataTable, Heading } from "@medusajs/ui"

const products = [
  {
    id: "1",
    title: "Shirt",
    price: 10
  },
  {
    id: "2",
    title: "Pants",
    price: 20
  }
]

const columnHelper = createDataTableColumnHelper<typeof products[0]>()

const columns = [
  columnHelper.accessor("title", {
    header: "Title",
    enableSorting: true,
  }),
  columnHelper.accessor("price", {
    header: "Price",
    enableSorting: true,
  }),
]

export default function ProductTable () {
  const table = useDataTable({
    columns,
    data: products,
    getRowId: (product) => product.id,
    rowCount: products.length,
    isLoading: false,
    onRowClick: (event, row) => {
      alert(`You clicked row #${row.id}`)
    }
  })
  
  return (
    <DataTable instance={table}>
	    <DataTable.Toolbar className="flex flex-col items-start justify-between gap-2 md:flex-row md:items-center">
	      <Heading>Products</Heading>
      </DataTable.Toolbar>
      <DataTable.Table />
    </DataTable>
  )
}
```

In many cases, you want to perform an action when a row is clicked. Most commonly, you may want to open the details page of the row when it's clicked.

For bulk actions, such as deleting multiple rows, use the [Command Bar](#perform-bulk-actions-on-datatable-rows) instead.

The `useDataTable` hook accepts an `onRowClick` property that you can use to handle row clicks:

```tsx
const navigate = useNavigate()

const table = useDataTable({
  // ...
  onRowClick(event, row) {
    navigate(`/author/${row.id}`)
  },
})
```

The value of `onRowClick` is a function that accepts two parameters:

- `event`: An instance of the [MouseClickEvent](https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent) object.
- `row`: The data of the row that was clicked.

In the above example, you use a `navigate` function, retrieved through the `useNavigate` hook from `react-router-dom`, to navigate to the details page of the row that was clicked.

## Configure Cell Rendering

***

```tsx
import { createDataTableColumnHelper, useDataTable, DataTable, Heading, Badge } from "@medusajs/ui"

const products = [
  {
    id: "1",
    title: "Shirt",
    price: 10,
    is_active: true
  },
  {
    id: "2",
    title: "Pants",
    price: 20,
    is_active: false
  }
]

const columnHelper = createDataTableColumnHelper<typeof products[0]>()

const columns = [
  columnHelper.accessor("title", {
    header: "Title",
    enableSorting: true,
  }),
  columnHelper.accessor("price", {
    header: "Price",
    enableSorting: true,
  }),
  columnHelper.accessor("is_active", {
    header: "Status",
    cell: ({ getValue }) => {
      const isActive = getValue()
      return (
        <Badge color={isActive ? "green" : "grey"} size="xsmall">
          {isActive ? "Active" : "Inactive"}
        </Badge>
      )
    }
  })
]

export default function ProductTable () {
  const table = useDataTable({
    columns,
    data: products,
    getRowId: (product) => product.id,
    rowCount: products.length,
    isLoading: false,
  })
  
  return (
    <DataTable instance={table}>
	    <DataTable.Toolbar className="flex flex-col items-start justify-between gap-2 md:flex-row md:items-center">
	      <Heading>Products</Heading>
      </DataTable.Toolbar>
      <DataTable.Table />
    </DataTable>
  )
}
```

The `accessor` method of the `createDataTableColumnHelper` utility accepts a `cell` property that you can use to customize the rendering of the cell content.

For example:

```tsx
const products = [
  {
    id: "1",
    title: "Shirt",
    price: 10,
    is_active: true
  },
  {
    id: "2",
    title: "Pants",
    price: 20,
    is_active: true
  }
]

const columnHelper = createDataTableColumnHelper<typeof products[0]>()

const columns = [
  columnHelper.accessor("is_active", {
    header: "Status",
    cell: ({ getValue }) => {
      const isActive = getValue()
      return (
        <Badge color={isActive ? "green" : "grey"}>
          {isActive ? "Active" : "Inactive"}
        </Badge>
      )
    }
  }),
  // ...
]
```

The `cell` property's value is a function that returns a string or a React node to be rendered in the cell. The function receives as a parameter an object having a `getValue` property to get the raw value of the cell.

## Configure Search in DataTable

***

```tsx
import { createDataTableColumnHelper, useDataTable, DataTable, Heading } from "@medusajs/ui"
import { useMemo, useState } from "react"

const products = [
  {
    id: "1",
    title: "Shirt",
    price: 10
  },
  {
    id: "2",
    title: "Pants",
    price: 20
  }
]

const columnHelper = createDataTableColumnHelper<typeof products[0]>()

const columns = [
  columnHelper.accessor("title", {
    header: "Title",
    enableSorting: true,
  }),
  columnHelper.accessor("price", {
    header: "Price",
    enableSorting: true,
  }),
]

export default function ProductTable () {
	const [search, setSearch] = useState<string>("")

  const shownProducts = useMemo(() => {
    return products.filter((product) => product.title.toLowerCase().includes(search.toLowerCase()))
  }, [search]) 
  
  const table = useDataTable({
    columns,
    data: shownProducts,
    getRowId: (product) => product.id,
    rowCount: products.length,
    isLoading: false,
    // Pass the state and onSearchChange to the table instance.
    search: {
	    state: search,
	    onSearchChange: setSearch
    }
  })
  
  return (
    <DataTable instance={table}>
	    <DataTable.Toolbar className="flex flex-col items-start justify-between gap-2 md:flex-row md:items-center">
	      <Heading>Products</Heading>
	      {/* This component renders the search bar */}
	      <DataTable.Search placeholder="Search..." />
      </DataTable.Toolbar>
      <DataTable.Table />
    </DataTable>
  )
}
```

The object passed to the `useDataTable` hook accepts a `search` property that you can use to enable and configure the search functionality in the `DataTable` component:

```tsx
// `useState` imported from `React`
const [search, setSearch] = useState("")

const table = useDataTable({
  // ...
  search: {
    state: search,
    onSearchChange: setSearch
  }
})
```

`search` accepts the following properties:

- `state`: The search query string. This must be a React state variable, as its value will be used for the table's search input.
- `onSearchChange`: A function that updates the search query string. Typically, this would be the setter function of the state variable, but you can also perform custom actions if necessary.

Next, you must implement the search filtering. For example, if you're retrieving data from the Medusa application, you pass the search query to the API to filter the data.

For example, when using a simple array as in the example above, this is how you filter the data by the search query:

```tsx
const [search, setSearch] = useState<string>("")

const shownProducts = useMemo(() => {
  return products.filter((product) => product.title.toLowerCase().includes(search.toLowerCase()))
}, [search]) 

const table = useDataTable({
  columns,
  data: shownProducts,
  getRowId: (product) => product.id,
  rowCount: products.length,
  isLoading: false,
  // Pass the state and onSearchChange to the table instance.
  search: {
    state: search,
    onSearchChange: setSearch
  }
})
```

Then, render the `DataTable.Search` component as part of the `DataTable`'s children:

```tsx
return (
  <DataTable instance={table}>
    <DataTable.Toolbar className="flex flex-col items-start justify-between gap-2 md:flex-row md:items-center">
      <Heading>Products</Heading>
      {/* This component renders the search bar */}
      <DataTable.Search placeholder="Search..." />
    </DataTable.Toolbar>
    <DataTable.Table />
  </DataTable>
)
```

This will show a search input at the top of the table, in the data table's toolbar.

## Configure Pagination in DataTable

***

```tsx
import { DataTable, Heading, createDataTableColumnHelper, useDataTable, type DataTablePaginationState } from "@medusajs/ui"
import { useMemo, useState } from "react"

const products = [
  {
    id: "1",
    title: "Shirt",
    price: 10
  },
  {
    id: "2",
    title: "Pants",
    price: 20
  },
  {
    id: "3",
    title: "Hat",
    price: 15
  },
  {
    id: "4",
    title: "Socks",
    price: 5
  },
  {
    id: "5",
    title: "Shoes",
    price: 50
  },
  {
    id: "6",
    title: "Jacket",
    price: 100
  },
  {
    id: "7",
    title: "Scarf",
    price: 25
  },
  {
    id: "8",
    title: "Gloves",
    price: 12
  },
  {
    id: "9",
    title: "Belt",
    price: 18
  },
  {
    id: "10",
    title: "Sunglasses",
    price: 30
  },
  {
    id: "11",
    title: "Watch",
    price: 200
  },
  {
    id: "12",
    title: "Tie",
    price: 20
  },
  {
    id: "13",
    title: "Sweater",
    price: 40
  },
  {
    id: "14",
    title: "Jeans",
    price: 60
  },
  {
    id: "15",
    title: "Shorts",
    price: 25
  },
  {
    id: "16",
    title: "Blouse",
    price: 35
  },
  {
    id: "17",
    title: "Dress",
    price: 80
  }
]

const columnHelper = createDataTableColumnHelper<typeof products[0]>()

const columns = [
  columnHelper.accessor("title", {
    header: "Title",
    enableSorting: true,
  }),
  columnHelper.accessor("price", {
    header: "Price",
    enableSorting: true,
  }),
]

const PAGE_SIZE = 10;

export default function ProductTable () {
  const [pagination, setPagination] = useState<DataTablePaginationState>({
    pageSize: PAGE_SIZE,
    pageIndex: 0,
  })

  const shownProducts = useMemo(() => {
    return products.slice(
      pagination.pageIndex * pagination.pageSize,
      (pagination.pageIndex + 1) * pagination.pageSize
    )
  }, [pagination])

  const table = useDataTable({
    data: shownProducts,
    columns,
    rowCount: products.length,
    getRowId: (product) => product.id,
    pagination: {
      // Pass the pagination state and updater to the table instance
      state: pagination,
      onPaginationChange: setPagination,
    },
    isLoading: false,
  });

  return (
      <DataTable instance={table}>
        <DataTable.Toolbar>
          <Heading>Products</Heading>
        </DataTable.Toolbar>
				<DataTable.Table />
        {/** This component will render the pagination controls **/}
        <DataTable.Pagination />
      </DataTable>
  );
};
```

The object passed to the `useDataTable` hook accepts a `pagination` property that you can use to enable and configure the pagination functionality in the `DataTable` component.

First, import the `DataTablePaginationState` type from `@medusajs/ui`:

```tsx
import {
  // ...
  DataTablePaginationState,
} from "@medusajs/ui"
```

Then, create a state variable to manage the pagination:

```tsx
const [pagination, setPagination] = useState<DataTablePaginationState>({
  pageSize: 15,
  pageIndex: 0,
})
```

The pagination state variable of type `DataTablePaginationState` is an object with the following properties:

- `pageSize`: The number of rows to display per page.
- `pageIndex`: The current page index. It's zero-based, meaning the first page would be `0`.

Next, pass the pagination object to the `useDataTable` hook:

```tsx
const table = useDataTable({
  // ...
  pagination: {
    state: pagination,
    onPaginationChange: setPagination,
  },
})
```

`pagination` accepts the following properties:

- `state`: The pagination state object. This must be a React state variable of type `DataTablePaginationState`.
- `onPaginationChange`: A function that updates the pagination state object. Typically, this would be the setter function of the state variable, but you can also perform custom actions if necessary.

You must also implement the pagination logic, such as fetching data from the Medusa application with the pagination parameters.

For example, when using a simple array as in the example above, this is how you paginate the data:

```tsx
const [pagination, setPagination] = useState<DataTablePaginationState>({
  pageSize: PAGE_SIZE,
  pageIndex: 0,
})

const shownProducts = useMemo(() => {
  return products.slice(
    pagination.pageIndex * pagination.pageSize,
    (pagination.pageIndex + 1) * pagination.pageSize
  )
}, [pagination])

const table = useDataTable({
  data: shownProducts,
  columns,
  rowCount: products.length,
  getRowId: (product) => product.id,
  pagination: {
    // Pass the pagination state and updater to the table instance
    state: pagination,
    onPaginationChange: setPagination,
  },
  isLoading: false,
});
```

Finally, render the `DataTable.Pagination` component as part of the `DataTable`'s children:

```tsx
return (
  <DataTable instance={table}>
    <DataTable.Toolbar>
      <Heading>Products</Heading>
    </DataTable.Toolbar>
    <DataTable.Table />
    {/** This component will render the pagination controls **/}
    <DataTable.Pagination />
  </DataTable>
)
```

This will show the pagination controls at the end of the table.

## Configure Filters in DataTable

***

```tsx
import { DataTable, DataTableFilteringState, Heading, createDataTableColumnHelper, createDataTableFilterHelper, useDataTable } from "@medusajs/ui"
import { useMemo, useState } from "react"

const products = [
  {
    id: "1",
    title: "Shirt",
    price: 10
  },
  {
    id: "2",
    title: "Pants",
    price: 20
  }
]

const columnHelper = createDataTableColumnHelper<typeof products[0]>()

const columns = [
  columnHelper.accessor("title", {
    header: "Title",
    enableSorting: true,
  }),
  columnHelper.accessor("price", {
    header: "Price",
    enableSorting: true,
  }),
]

const filterHelper = createDataTableFilterHelper<typeof products[0]>()

const filters = [
  filterHelper.accessor("title", {
    type: "select",
    label: "Title",
    options: products.map((product) => ({
      label: product.title,
      value: product.title.toLowerCase()
    }))
  }),
]

export default function ProductTable () {
	const [filtering, setFiltering] = useState<DataTableFilteringState>({})

  const shownProducts = useMemo(() => {
    return products.filter((product) => {
      return Object.entries(filtering).every(([key, value]) => {
        if (!value) {
          return true
        }
        if (typeof value === "string") {
          // @ts-ignore
          return product[key].toString().toLowerCase().includes(value.toString().toLowerCase())
        }
        if (Array.isArray(value)) {
          // @ts-ignore
          return value.includes(product[key].toLowerCase())
        }
        if (typeof value === "object") {
          // @ts-ignore
          const date = new Date(product[key])
          let matching = false
          if ("$gte" in value && value.$gte) {
            matching = date >= new Date(value.$gte as number)
          }
          if ("$lte" in value && value.$lte) {
            matching = date <= new Date(value.$lte as number)
          }
          if ("$lt" in value && value.$lt) {
            matching = date < new Date(value.$lt as number)
          }
          if ("$gt" in value && value.$gt) {
            matching = date > new Date(value.$gt as number)
          }
          return matching
        }
      })
    })
  }, [filtering])

  const table = useDataTable({
    data: shownProducts,
    columns,
    getRowId: (product) => product.id,
    rowCount: products.length,
    isLoading: false,
    filtering: {
      state: filtering,
      onFilteringChange: setFiltering,
    },
    filters
  });

  return (
    <DataTable instance={table}>
      <DataTable.Toolbar className="flex justify-between items-center">
        <Heading>Products</Heading>
        {/** This component will render a menu that allows the user to choose which filters to apply to the table data. **/}
        <DataTable.FilterMenu tooltip="Filter" />
      </DataTable.Toolbar>
	    <DataTable.Table />
    </DataTable>
  );
};
```

The object passed to the `useDataTable` hook accepts a `filters` property that you can use to enable and configure the filtering functionality in the `DataTable` component.

First, add the following imports from the `@medusajs/ui` package:

```tsx
import {
  // ...
  createDataTableFilterHelper,
  DataTableFilteringState,
} from "@medusajs/ui"
```

The `createDataTableFilterHelper` utility is a function that returns a helper function to generate filter configurations for the `DataTable` component. The `DataTableFilteringState` type is an object that represents the filtering state of the table.

Then, create the filters using the `createDataTableFilterHelper` utility:

Create the filters outside the component rendering the `DataTable` component.

```tsx
const filterHelper = createDataTableFilterHelper<typeof products[0]>()

const filters = [
  filterHelper.accessor("title", {
    type: "select",
    label: "Title",
    options: products.map((product) => ({
      label: product.title,
      value: product.title.toLowerCase()
    }))
  }),
]
```

The filter helper returned by `createDataTableFilterHelper` has an `accessor` method that accepts the column's key in the data as the first parameter, and an object with the following properties as the second parameter:

- `type`: The type of filter. It can be either:
  - `select`: A select dropdown filter.
  - `radio`: A radio button filter.
  - `date`: A date filter.
- `label`: The label text for the filter.
- `options`: If the filter type is `select` or `radio`, an array of dropdown options. Each option has a `label` and `value` property.

Refer to [this section](#filtering-date-values) to learn how to use date filters.

Next, in the component rendering the `DataTable` component, create a state variable to manage the filtering, and pass the filters to the `useDataTable` hook:

```tsx
const [filtering, setFiltering] = useState<DataTableFilteringState>({})

const table = useDataTable({
  // ...
  filters,
  filtering: {
    state: filtering,
    onFilteringChange: setFiltering,
  },
})
```

You create a `filtering` state variable of type `DataTableFilteringState` to manage the filtering state. You can also set initial filters as explained in [this section](#initial-filter-values).

The `useDataTable` hook accepts the following properties for filtering:

- `filters`: An array of filter configurations generated using the `createDataTableFilterHelper` utility.
- `filtering`: An object with the following properties:
  - `state`: The filtering state object. This must be a React state variable of type `DataTableFilteringState`.
  - `onFilteringChange`: A function that updates the filtering state object. Typically, this would be the setter function of the state variable, but you can also perform custom actions if necessary.

You must also implement the logic of filtering the data based on the filter conditions, such as sending the filter conditions to the Medusa application when fetching data.

For example, when using a simple array as in the example above, this is how you filter the data based on the filter conditions:

```tsx
const [filtering, setFiltering] = useState<DataTableFilteringState>({})

const shownProducts = useMemo(() => {
  return products.filter((product) => {
    return Object.entries(filtering).every(([key, value]) => {
      if (!value) {
        return true
      }
      if (typeof value === "string") {
        // @ts-ignore
        return product[key].toString().toLowerCase().includes(value.toString().toLowerCase())
      }
      if (Array.isArray(value)) {
        // @ts-ignore
        return value.includes(product[key].toLowerCase())
      }
      if (typeof value === "object") {
        // @ts-ignore
        const date = new Date(product[key])
        let matching = false
        if ("$gte" in value && value.$gte) {
          matching = date >= new Date(value.$gte as number)
        }
        if ("$lte" in value && value.$lte) {
          matching = date <= new Date(value.$lte as number)
        }
        if ("$lt" in value && value.$lt) {
          matching = date < new Date(value.$lt as number)
        }
        if ("$gt" in value && value.$gt) {
          matching = date > new Date(value.$gt as number)
        }
        return matching
      }
    })
  })
}, [filtering])

const table = useDataTable({
  data: shownProducts,
  columns,
  getRowId: (product) => product.id,
  rowCount: products.length,
  isLoading: false,
  filtering: {
    state: filtering,
    onFilteringChange: setFiltering,
  },
  filters
})
```

When filters are selected, the `filtering` state object will contain the filter conditions, where the key is the column key and the value can be:

- `undefined` if the user is still selecting the value.
- A string if the filter type is `radio`, as the user can choose only one value.
- An array of strings if the filter type is `select`, as the user can choose multiple values.
- An object with the filter conditions if the filter type is `date`. The filter conditions for dates are explained more in [this section](#filtering-date-values).

Finally, render the `DataTable.FilterMenu` component as part of the `DataTable`'s children:

```tsx
return (
  <DataTable instance={table}>
    <DataTable.Toolbar className="flex justify-between items-center">
      <Heading>Products</Heading>
      {/** This component will render a menu that allows the user to choose which filters to apply to the table data. **/}
      <DataTable.FilterMenu tooltip="Filter" />
    </DataTable.Toolbar>
    <DataTable.Table />
  </DataTable>
)
```

This will show a filter menu at the top of the table, in the data table's toolbar.

### Filtering Date Values

```tsx
import { DataTable, DataTableFilteringState, Heading, createDataTableColumnHelper, createDataTableFilterHelper, useDataTable } from "@medusajs/ui"
import { useMemo, useState } from "react"

const products = [
  {
    id: "1",
    title: "Shirt",
    price: 10,
    created_at: new Date()
  },
  {
    id: "2",
    title: "Pants",
    price: 20,
    created_at: new Date("2026-01-01")
  }
]

const columnHelper = createDataTableColumnHelper<typeof products[0]>()

const columns = [
  columnHelper.accessor("title", {
    header: "Title",
    enableSorting: true,
  }),
  columnHelper.accessor("price", {
    header: "Price",
    enableSorting: true,
  }),
  columnHelper.accessor("created_at", {
    header: "Created At",
    cell: ({ getValue }) => {
      return getValue().toLocaleString()
    }
  }),
]

const filterHelper = createDataTableFilterHelper<typeof products[0]>()

const filters = [
  filterHelper.accessor("created_at", {
    type: "date",
    label: "Created At",
    format: "date",
    formatDateValue: (date) => date.toLocaleString(),
    rangeOptionStartLabel: "From",
    rangeOptionEndLabel: "To",
    rangeOptionLabel: "Between",
    options: [
      {
        label: "Today",
        value: {
          $gte: new Date(new Date().setHours(0, 0, 0, 0)).toString(),
          $lte: new Date(new Date().setHours(23, 59, 59, 999)).toString()
        }
      },
      {
        label: "Yesterday",
        value: {
          $gte: new Date(new Date().setHours(0, 0, 0, 0) - 24 * 60 * 60 * 1000).toString(),
          $lte: new Date(new Date().setHours(0, 0, 0, 0)).toString()
        }
      },
      {
        label: "Last Week",
        value: {
          $gte: new Date(new Date().setHours(0, 0, 0, 0) - 7 * 24 * 60 * 60 * 1000).toString(),
          $lte: new Date(new Date().setHours(0, 0, 0, 0)).toString()
        },
      },
    ]
  }),
]

export default function ProductTable () {
	const [filtering, setFiltering] = useState<DataTableFilteringState>({})

  const shownProducts = useMemo(() => {
    return products.filter((product) => {
      return Object.entries(filtering).every(([key, value]) => {
        if (!value) {
          return true
        }
        if (typeof value === "string") {
          // @ts-ignore
          return product[key].toString().toLowerCase().includes(value.toString().toLowerCase())
        }
        if (Array.isArray(value)) {
          // @ts-ignore
          return value.includes(product[key].toLowerCase())
        }
        if (typeof value === "object") {
          // @ts-ignore
          const date = new Date(product[key])
          let matching = false
          if ("$gte" in value && value.$gte) {
            matching = date >= new Date(value.$gte as number)
          }
          if ("$lte" in value && value.$lte) {
            matching = date <= new Date(value.$lte as number)
          }
          if ("$lt" in value && value.$lt) {
            matching = date < new Date(value.$lt as number)
          }
          if ("$gt" in value && value.$gt) {
            matching = date > new Date(value.$gt as number)
          }
          return matching
        }
      })
    })
  }, [filtering])

  const table = useDataTable({
    data: shownProducts,
    columns,
    getRowId: (product) => product.id,
    rowCount: products.length,
    isLoading: false,
    filtering: {
      state: filtering,
      onFilteringChange: setFiltering,
    },
    filters
  });

  return (
    <DataTable instance={table}>
      <DataTable.Toolbar className="flex justify-between items-center">
        <Heading>Products</Heading>
        {/** This component will render a menu that allows the user to choose which filters to apply to the table data. **/}
        <DataTable.FilterMenu tooltip="Filter" />
      </DataTable.Toolbar>
	    <DataTable.Table />
    </DataTable>
  );
};
```

Consider your data has a `created_at` field that contains date values. To filter the data based on date values, you can add a `date` filter using the filter helper:

```tsx
const filters = [
  // ...
  filterHelper.accessor("created_at", {
    type: "date",
    label: "Created At",
    format: "date",
    formatDateValue: (date) => date.toLocaleString(),
    rangeOptionStartLabel: "From",
    rangeOptionEndLabel: "To",
    rangeOptionLabel: "Between",
    options: [
      {
        label: "Today",
        value: {
          $gte: new Date(new Date().setHours(0, 0, 0, 0)).toString(),
          $lte: new Date(new Date().setHours(23, 59, 59, 999)).toString()
        }
      },
      {
        label: "Yesterday",
        value: {
          $gte: new Date(new Date().setHours(0, 0, 0, 0) - 24 * 60 * 60 * 1000).toString(),
          $lte: new Date(new Date().setHours(0, 0, 0, 0)).toString()
        }
      },
      {
        label: "Last Week",
        value: {
          $gte: new Date(new Date().setHours(0, 0, 0, 0) - 7 * 24 * 60 * 60 * 1000).toString(),
          $lte: new Date(new Date().setHours(0, 0, 0, 0)).toString()
        },
      },
    ]
  }),
]
```

When the filter type is `date`, the filter configuration object passed as a second parameter to the `accessor` method accepts the following properties:

- `format`: The format of the date value. It can be either `date` to filter by dates, or `datetime` to filter by dates and times.
- `formatDateValue`: A function that formats the date value when displaying it in the filter options.
- `rangeOptionStartLabel`: (optional) The label for the start date input in the range filter.
- `rangeOptionEndLabel`: (optional) The label for the end date input in the range filter.
- `rangeOptionLabel`: (optional) The label for the range filter option.
- `options`: By default, the filter will allow the user to filter between two dates. You can also set this property to an array of filter options to quickly choose from. Each option has a `label` and `value` property. The `value` property is an object that represents the filter condition. In this example, the filter condition is an object with a `$gte` property that specifies the date that the data should be greater than or equal to. Allowed properties are:
  - `$gt`: Greater than.
  - `$lt`: Less than.
  - `$lte`: Less than or equal to.
  - `$gte`: Greater than or equal to.

When the user selects a date filter option, the `filtering` state object will contain the filter conditions, where the key is the column key and the value is an object with the filter conditions. You must handle the filter logic as explained earlier.

For example, when using a simple array as in the example above, this is how you filter the data based on the date filter conditions:

```tsx
const shownProducts = useMemo(() => {
  return products.filter((product) => {
    return Object.entries(filtering).every(([key, value]) => {
      if (!value) {
        return true
      }
      // other types checks...
      if (typeof value === "object") {
        // @ts-ignore
        const date = new Date(product[key])
        let matching = false
        if ("$gte" in value && value.$gte) {
          matching = date >= new Date(value.$gte as number)
        }
        if ("$lte" in value && value.$lte) {
          matching = date <= new Date(value.$lte as number)
        }
        if ("$lt" in value && value.$lt) {
          matching = date < new Date(value.$lt as number)
        }
        if ("$gt" in value && value.$gt) {
          matching = date > new Date(value.$gt as number)
        }
        return matching
      }
    })
  })
}, [filtering])
```

### Initial Filter Values

```tsx
import { DataTable, DataTableFilteringState, Heading, createDataTableColumnHelper, createDataTableFilterHelper, useDataTable } from "@medusajs/ui"
import { useMemo, useState } from "react"

const products = [
  {
    id: "1",
    title: "Shirt",
    price: 10
  },
  {
    id: "2",
    title: "Pants",
    price: 20
  }
]

const columnHelper = createDataTableColumnHelper<typeof products[0]>()

const columns = [
  columnHelper.accessor("title", {
    header: "Title",
    enableSorting: true,
  }),
  columnHelper.accessor("price", {
    header: "Price",
    enableSorting: true,
  }),
]

const filterHelper = createDataTableFilterHelper<typeof products[0]>()

const filters = [
  filterHelper.accessor("title", {
    type: "select",
    label: "Title",
    options: products.map((product) => ({
      label: product.title,
      value: product.title.toLowerCase()
    }))
  }),
]

export default function ProductTable () {
	const [filtering, setFiltering] = useState<DataTableFilteringState>({
    title: ["shirt"]
  })

  const shownProducts = useMemo(() => {
    return products.filter((product) => {
      return Object.entries(filtering).every(([key, value]) => {
        if (!value) {
          return true
        }
        if (typeof value === "string") {
          // @ts-ignore
          return product[key].toString().toLowerCase().includes(value.toString().toLowerCase())
        }
        if (Array.isArray(value)) {
          // @ts-ignore
          return value.includes(product[key].toLowerCase())
        }
        if (typeof value === "object") {
          // @ts-ignore
          const date = new Date(product[key])
          let matching = false
          if ("$gte" in value && value.$gte) {
            matching = date >= new Date(value.$gte as number)
          }
          if ("$lte" in value && value.$lte) {
            matching = date <= new Date(value.$lte as number)
          }
          if ("$lt" in value && value.$lt) {
            matching = date < new Date(value.$lt as number)
          }
          if ("$gt" in value && value.$gt) {
            matching = date > new Date(value.$gt as number)
          }
          return matching
        }
      })
    })
  }, [filtering])

  const table = useDataTable({
    data: shownProducts,
    columns,
    getRowId: (product) => product.id,
    rowCount: products.length,
    isLoading: false,
    filtering: {
      state: filtering,
      onFilteringChange: setFiltering,
    },
    filters
  });

  return (
    <DataTable instance={table}>
      <DataTable.Toolbar className="flex justify-between items-center">
        <Heading>Products</Heading>
        {/** This component will render a menu that allows the user to choose which filters to apply to the table data. **/}
        <DataTable.FilterMenu tooltip="Filter" />
      </DataTable.Toolbar>
	    <DataTable.Table />
    </DataTable>
  );
};
```

If you want to set initial filter values, you can set the initial state of the `filtering` state variable:

```tsx
const [filtering, setFiltering] = useState<DataTableFilteringState>({
  title: ["shirt"]
})
```

The user can still change the filter values, but the initial values will be applied when the table is first rendered.

## Configure Sorting in DataTable

***

```tsx
import { DataTable, DataTableSortingState, Heading, createDataTableColumnHelper, useDataTable } from "@medusajs/ui"
import { useMemo, useState } from "react"

const products = [
  {
    id: "1",
    title: "Shirt",
    price: 10
  },
  {
    id: "2",
    title: "Pants",
    price: 20
  }
]

const columnHelper = createDataTableColumnHelper<typeof products[0]>()

const columns = [
  columnHelper.accessor("title", {
    header: "Title",
    // Enables sorting for the column.
    enableSorting: true,
    // If omitted, the header will be used instead if it's a string, 
    // otherwise the accessor key (id) will be used.
    sortLabel: "Title",
    // If omitted the default value will be "A-Z"
    sortAscLabel: "A-Z",
    // If omitted the default value will be "Z-A"
    sortDescLabel: "Z-A",
  }),
  columnHelper.accessor("price", {
    header: "Price",
  }),
]

export default function ProductTable () {
  const [sorting, setSorting] = useState<DataTableSortingState | null>(null);

  const shownProducts = useMemo(() => {
    if (!sorting) {
      return products
    }
    return products.slice().sort((a, b) => {
      // @ts-ignore
      const aVal = a[sorting.id]
      // @ts-ignore
      const bVal = b[sorting.id]
      if (aVal < bVal) {
        return sorting.desc ? 1 : -1
      }
      if (aVal > bVal) {
        return sorting.desc ? -1 : 1
      }
      return 0
    })
  }, [sorting])

  const table = useDataTable({
    data: shownProducts,
    columns,
    getRowId: (product) => product.id,
    rowCount: products.length,
    sorting: {
      // Pass the pagination state and updater to the table instance
      state: sorting,
      onSortingChange: setSorting,
    },
    isLoading: false,
  })

  return (
    <DataTable instance={table}>
      <DataTable.Toolbar className="flex justify-between items-center">
        <Heading>Products</Heading>
        {/** This component will render a menu that allows the user to choose which column to sort by and in what direction. **/}
        <DataTable.SortingMenu tooltip="Sort" />
      </DataTable.Toolbar>
	    <DataTable.Table />
    </DataTable>
  );
};
```

The object passed to the `useDataTable` hook accepts a `sorting` property that you can use to enable and configure the sorting functionality in the `DataTable` component.

First, in the `columns` array created by the columns helper, specify for the sortable columns the following properties:

```tsx
const columns = [
  columnHelper.accessor("title", {
    header: "Title",
    // Enables sorting for the column.
    enableSorting: true,
    // If omitted, the header will be used instead if it's a string, 
    // otherwise the accessor key (id) will be used.
    sortLabel: "Title",
    // If omitted the default value will be "A-Z"
    sortAscLabel: "A-Z",
    // If omitted the default value will be "Z-A"
    sortDescLabel: "Z-A",
  }),
]
```

The `accessor` method of the helper function accepts the following properties for sorting:

- `enableSorting`: A boolean that indicates whether data in the table can be sorted by this column.
- `sortLabel`: The label text for the sort button in the column header. If omitted, the `header` will be used instead if it's a string, otherwise the accessor key (id) will be used.
- `sortAscLabel`: The label text for the ascending sort button. If omitted, the default value will be `A-Z`.
- `sortDescLabel`: The label text for the descending sort button. If omitted, the default value will be `Z-A`.

Next, in the component rendering the `DataTable` component, create a state variable to manage the sorting, and pass the sorting object to the `useDataTable` hook:

```tsx
import {
  // ...
  DataTableSortingState
} from "@medusajs/ui"

export default function ProductTable () {
  const [sorting, setSorting] = useState<DataTableSortingState | null>(null);

  const table = useDataTable({
    // ...
    sorting: {
      state: sorting,
      onSortingChange: setSorting,
    },
  })

  // ...
}
```

You create a state variable of type `DataTableSortingState` to manage the sorting state. You can also set initial sorting values as explained in [this section](#initial-sort-values).

The `sorting` object passed to the `useDataTable` hook accepts the following properties:

- `state`: The sorting state object. This must be a React state variable of type `DataTableSortingState`.
- `onSortingChange`: A function that updates the sorting state object. Typically, this would be the setter function of the state variable, but you can also perform custom actions if necessary.

You must also implement the sorting logic, such as sending the sorting conditions to the Medusa application when fetching data.

For example, when using a simple array as in the example above, this is how you sort the data based on the sorting conditions:

```tsx
const [sorting, setSorting] = useState<DataTableSortingState | null>(null);

const shownProducts = useMemo(() => {
  if (!sorting) {
    return products
  }
  return products.slice().sort((a, b) => {
    // @ts-ignore
    const aVal = a[sorting.id]
    // @ts-ignore
    const bVal = b[sorting.id]
    if (aVal < bVal) {
      return sorting.desc ? 1 : -1
    }
    if (aVal > bVal) {
      return sorting.desc ? -1 : 1
    }
    return 0
  })
}, [sorting])

const table = useDataTable({
  data: shownProducts,
  columns,
  getRowId: (product) => product.id,
  rowCount: products.length,
  sorting: {
    // Pass the pagination state and updater to the table instance
    state: sorting,
    onSortingChange: setSorting,
  },
  isLoading: false,
})
```

The `sorting` state object has the following properties:

- `id`: The column key to sort by.
- `desc`: A boolean that indicates whether to sort in descending order.

Finally, render the `DataTable.SortingMenu` component as part of the `DataTable`'s children:

```tsx
return (
  <DataTable instance={table}>
    <DataTable.Toolbar className="flex justify-between items-center">
      <Heading>Products</Heading>
      {/** This component will render a menu that allows the user to choose which column to sort by and in what direction. **/}
      <DataTable.SortingMenu tooltip="Sort" />
    </DataTable.Toolbar>
    <DataTable.Table />
  </DataTable>
)
```

This will show a sorting menu at the top of the table, in the data table's toolbar.

### Initial Sort Values

```tsx
import { DataTable, DataTableSortingState, Heading, createDataTableColumnHelper, useDataTable } from "@medusajs/ui"
import { useMemo, useState } from "react"

const products = [
  {
    id: "1",
    title: "Shirt",
    price: 10
  },
  {
    id: "2",
    title: "Pants",
    price: 20
  }
]

const columnHelper = createDataTableColumnHelper<typeof products[0]>()

const columns = [
  columnHelper.accessor("title", {
    header: "Title",
    // Enables sorting for the column.
    enableSorting: true,
    // If omitted, the header will be used instead if it's a string, 
    // otherwise the accessor key (id) will be used.
    sortLabel: "Title",
    // If omitted the default value will be "A-Z"
    sortAscLabel: "A-Z",
    // If omitted the default value will be "Z-A"
    sortDescLabel: "Z-A",
  }),
  columnHelper.accessor("price", {
    header: "Price",
  }),
]

export default function ProductTable () {
  const [sorting, setSorting] = useState<DataTableSortingState | null>({
    id: "title",
    desc: false,
  })

  const shownProducts = useMemo(() => {
    if (!sorting) {
      return products
    }
    return products.slice().sort((a, b) => {
      // @ts-ignore
      const aVal = a[sorting.id]
      // @ts-ignore
      const bVal = b[sorting.id]
      if (aVal < bVal) {
        return sorting.desc ? 1 : -1
      }
      if (aVal > bVal) {
        return sorting.desc ? -1 : 1
      }
      return 0
    })
  }, [sorting])

  const table = useDataTable({
    data: shownProducts,
    columns,
    getRowId: (product) => product.id,
    rowCount: products.length,
    sorting: {
      // Pass the pagination state and updater to the table instance
      state: sorting,
      onSortingChange: setSorting,
    },
    isLoading: false,
  })

  return (
    <DataTable instance={table}>
      <DataTable.Toolbar className="flex justify-between items-center">
        <Heading>Products</Heading>
        {/** This component will render a menu that allows the user to choose which column to sort by and in what direction. **/}
        <DataTable.SortingMenu tooltip="Sort" />
      </DataTable.Toolbar>
	    <DataTable.Table />
    </DataTable>
  );
};
```

If you want to set initial sort values, you can set the initial state of the `sorting` state variable:

```tsx
const [sorting, setSorting] = useState<DataTableSortingState | null>({
  id: "title",
  desc: false,
})
```

The user can still change the sort values, but the initial values will be applied when the table is first rendered.

## Perform Bulk Actions on DataTable Rows

***

```tsx
import { DataTable, DataTableRowSelectionState, Heading, createDataTableColumnHelper, createDataTableCommandHelper, useDataTable } from "@medusajs/ui"
import { useState } from "react"

let products = [
  {
    id: "1",
    title: "Shirt",
    price: 10,
  },
  {
    id: "2",
    title: "Pants",
    price: 20,
  }
]

const columnHelper = createDataTableColumnHelper<typeof products[0]>()

const columns = [
  // Commands requires a select column.
  columnHelper.select(),
  columnHelper.accessor("title", {
    header: "Title",
    enableSorting: true,
  }),
  columnHelper.accessor("price", {
    header: "Price",
    enableSorting: true,
  }),
]

const commandHelper = createDataTableCommandHelper()

const useCommands = () => {
  return [
    commandHelper.command({
      label: "Delete",
      shortcut: "D",
      action: async (selection) => {
        const productsToDeleteIds = Object.keys(selection)

        alert(`You deleted product(s) with IDs: ${productsToDeleteIds.join()}`)
      }
    })
  ]
}

export default function ProductTable () {
	const [rowSelection, setRowSelection] = useState<DataTableRowSelectionState>({})

  const commands = useCommands()

  const instance = useDataTable({
    data: products,
    columns,
    getRowId: (product) => product.id,
    rowCount: products.length,
    isLoading: false,
    commands,
    rowSelection: {
      state: rowSelection,
      onRowSelectionChange: setRowSelection,
    },
  });

  return (
    <DataTable instance={instance}>
      <DataTable.Toolbar className="flex justify-between items-center">
        <Heading>Products</Heading>
      </DataTable.Toolbar>
	    <DataTable.Table />
      {/** This component will the command bar when the user has selected at least one row. **/}
	    <DataTable.CommandBar selectedLabel={(count) => `${count} selected`} />
    </DataTable>
  );
};
```

The object passed to the `useDataTable` hook accepts a `commands` object property that you can use to add custom actions to the `DataTable` component.

First, add the following imports from `@medusajs/ui`:

```tsx
import {
  // ...
  createDataTableCommandHelper,
  DataTableRowSelectionState,
} from "@medusajs/ui"
```

The `createDataTableCommandHelper` utility is a function that returns a helper function to generate command configurations for the `DataTable` component. The `DataTableRowSelectionState` type is an object that represents the row selection state of the table.

Then, in the `columns` array created by the columns helper, add a `select` column:

```tsx
const columns = [
  // Commands requires a select column.
  columnHelper.select(),
  // ...
]
```

The `select` method of the helper function adds a select column to the table. This column will render checkboxes in each row to allow the user to select rows.

Next, create the commands using the `createDataTableCommandHelper` utility:

Create the commands outside the component rendering the `DataTable` component.

```tsx
const commandHelper = createDataTableCommandHelper()

const useCommands = () => {
  return [
    commandHelper.command({
      label: "Delete",
      shortcut: "D",
      action: async (selection) => {
        const productsToDeleteIds = Object.keys(selection)

        // TODO remove products from the server
      }
    })
  ]
}
```

The `createDataTableCommandHelper` utility is a function that returns a helper function to generate command configurations for the `DataTable` component.

You create a function that returns an array of command configurations. This is useful if the command's action requires initializing other functions or hooks.

The `command` method of the helper function accepts the following properties:

- `label`: The label text for the command.
- `shortcut`: The keyboard shortcut for the command. This shortcut only works when rows are selected in the table.
- `action`: A function that performs the action when the command is executed. The function receives the selected rows as an object, where the key is the row's `id` field and the value is a boolean indicating that the row is selected. You can send a request to the server within this function to perform the action.

Then, in the component rendering the `DataTable` component, create a state variable to manage the selected rows, and pass the commands to the `useDataTable` hook:

```tsx
const [rowSelection, setRowSelection] = useState<DataTableRowSelectionState>({})

const commands = useCommands()

const instance = useDataTable({
  data: products,
  columns,
  getRowId: (product) => product.id,
  rowCount: products.length,
  isLoading: false,
  commands,
  rowSelection: {
    state: rowSelection,
    onRowSelectionChange: setRowSelection,
  },
})
```

You create a state variable of type `DataTableRowSelectionState` to manage the selected rows. You also retrieve the commands by calling the `useCommand` function.

The `useDataTable` hook accepts the following properties for commands:

- `commands`: An array of command configurations generated using the `createDataTableCommandHelper` utility.
- `rowSelection`: An object that enables selecting rows in the table. It accepts the following properties:
  - `state`: The row selection state object. This must be a React state variable of type `DataTableRowSelectionState`.
  - `onRowSelectionChange`: A function that updates the row selection state object. Typically, this would be the setter function of the state variable, but you can also perform custom actions if necessary.

Finally, render the `DataTable.CommandBar` component as part of the `DataTable`'s children:

```tsx
return (
  <DataTable instance={instance}>
    <DataTable.Toolbar className="flex justify-between items-center">
      <Heading>Products</Heading>
    </DataTable.Toolbar>
    <DataTable.Table />
    {/** This component will the command bar when the user has selected at least one row. **/}
    <DataTable.CommandBar selectedLabel={(count) => `${count} selected`} />
  </DataTable>
)
```

This will show a command bar when the user has selected at least one row in the table.


# Date Picker

A date picker component with range and presets.

```tsx
import { DatePicker } from "@medusajs/ui"

export default function DatePickerDemo() {
  return (
    <div className="w-[250px]">
      <DatePicker />
    </div>
  )
}

```

## Usage

***

```tsx
import { DatePicker } from "@medusajs/ui"
```

```tsx
<DatePicker />
```

## API Reference

***

### DatePicker Props

- aria-describedby: (string) Identifies the element (or elements) that describes the object.
- aria-details: (string) Identifies the element (or elements) that provide a detailed, extended description for the object.
- aria-label: (string) Defines a string value that labels the current element.
- aria-labelledby: (string) Identifies the element (or elements) that labels the current element.
- autoFocus: (boolean) Whether the element should receive focus on render.
- defaultOpen: (boolean) Whether the overlay is open by default (uncontrolled).
- description: (ReactNode) A description for the field. Provides a hint such as specific requirements for what to choose.
- errorMessage: (union) An error message for the field.
- hideTimeZone: (boolean) Whether to hide the time zone abbreviation.
- hourCycle: (union) Whether to display the time in 12 or 24 hour format. By default, this is determined by the user's locale.
- id: (string) The element's unique identifier. See \[MDN]\(https://developer.mozilla.org/en-US/docs/Web/HTML/Global\_attributes/id).
- isDisabled: (boolean) Whether the input is disabled.
- isInvalid: (boolean) Whether the input value is invalid.
- isOpen: (boolean) Whether the overlay is open by default (controlled).
- isReadOnly: (boolean) Whether the input can be selected but not changed by the user.
- isRequired: (boolean) Whether user input is required on the input before form submission.
- label: (ReactNode) The content to display as the label.
- name: (string) The name of the input element, used when submitting an HTML form. See \[MDN]\(https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input#htmlattrdefname).
- onBlur: (signature) Handler that is called when the element loses focus.
- onFocus: (signature) Handler that is called when the element receives focus.
- onFocusChange: (signature) Handler that is called when the element's focus status changes.
- onKeyDown: (signature) Handler that is called when a key is pressed.
- onKeyUp: (signature) Handler that is called when a key is released.
- onOpenChange: (signature) Handler that is called when the overlay's open state changes.
- pageBehavior: (PageBehavior) Controls the behavior of paging. Pagination either works by advancing the visible page by visibleDuration (default) or one unit of visibleDuration.
- placeholderValue: (union) A placeholder date that influences the format of the placeholder shown when no value is selected. Defaults to today's date at midnight.
- shouldForceLeadingZeros: (boolean) Whether to always show leading zeros in the month, day, and hour fields.
  By default, this is determined by the user's locale.
- validate: (signature) A function that returns an error message if a given value is invalid.
  Validation errors are displayed to the user when the form is submitted
  if \`validationBehavior="native"\`. For realtime validation, use the \`isInvalid\`
  prop instead.
- validationBehavior: (union) Whether to use native HTML form validation to prevent form submission
  when the value is missing or invalid, or mark the field as required
  or invalid via ARIA.


# Drawer

A triggerable drawer that overlaps whatever page it's on.

```tsx
import { Button, Drawer, Text } from "@medusajs/ui"

export default function DrawerDemo() {
  return (
    <Drawer>
      <Drawer.Trigger asChild>
        <Button>Edit Variant</Button>
      </Drawer.Trigger>
      <Drawer.Content>
        <Drawer.Header>
          <Drawer.Title>Edit Variant</Drawer.Title>
        </Drawer.Header>
        <Drawer.Body className="p-4">
          <Text>This is where you edit the variant&apos;s details</Text>
        </Drawer.Body>
        <Drawer.Footer>
          <Drawer.Close asChild>
            <Button variant="secondary">Cancel</Button>
          </Drawer.Close>
          <Button>Save</Button>
        </Drawer.Footer>
      </Drawer.Content>
    </Drawer>
  )
}

```

## Usage

***

```tsx
import { Drawer } from "@medusajs/ui"
```

```tsx
<Drawer>
  <Drawer.Trigger>Trigger</Drawer.Trigger>
  <Drawer.Content>
    <Drawer.Header>
      <Drawer.Title>Drawer Title</Drawer.Title>
    </Drawer.Header>
    <Drawer.Body>Body</Drawer.Body>
    <Drawer.Footer>Footer</Drawer.Footer>
  </Drawer.Content>
</Drawer>
```

## API Reference

***

### Drawer Props

This component is based on the \[Radix UI Dialog]\(https://www.radix-ui.com/primitives/docs/components/dialog) primitives.



### Drawer.Trigger Props

This component is used to create the trigger button that opens the drawer.
It accepts props from the \[Radix UI Dialog Trigger]\(https://www.radix-ui.com/primitives/docs/components/dialog#trigger) component.



### Drawer.Content Props

This component wraps the content of the drawer.
It accepts props from the \[Radix UI Dialog Content]\(https://www.radix-ui.com/primitives/docs/components/dialog#content) component.

- overlayProps: (ReactComponentPropsWithoutRef) Props for the overlay component.
  It accepts props from the \[Radix UI Dialog Overlay]\(https://www.radix-ui.com/primitives/docs/components/dialog#overlay) component.
- portalProps: (ReactComponentPropsWithoutRef) Props for the portal component that wraps the drawer content.
  It accepts props from the \[Radix UI Dialog Portal]\(https://www.radix-ui.com/primitives/docs/components/dialog#portal) component.

### Drawer.Header Props

This component is used to wrap the header content of the drawer.
This component is based on the \`div\` element and supports all of its props.



### Drawer.Title Props

This component adds an accessible title to the drawer.
It accepts props from the \[Radix UI Dialog Title]\(https://www.radix-ui.com/primitives/docs/components/dialog#title) component.



### Drawer.Body Props

This component is used to wrap the body content of the drawer.
This component is based on the \`div\` element and supports all of its props



### Drawer.Footer Props

This component is used to wrap the footer content of the drawer.
This component is based on the \`div\` element and supports all of its props.



# Dropdown Menu

Displays a menu to the user—such as a set of actions or functions—triggered by a button.

```tsx
import { EllipsisHorizontal, PencilSquare, Plus, Trash } from "@medusajs/icons"
import { DropdownMenu, IconButton } from "@medusajs/ui"

export default function DropdownMenuDemo() {
  return (
    <DropdownMenu>
      <DropdownMenu.Trigger asChild>
        <IconButton>
          <EllipsisHorizontal />
        </IconButton>
      </DropdownMenu.Trigger>
      <DropdownMenu.Content>
        <DropdownMenu.Item className="gap-x-2">
          <PencilSquare className="text-ui-fg-subtle" />
          Edit
        </DropdownMenu.Item>
        <DropdownMenu.Item className="gap-x-2">
          <Plus className="text-ui-fg-subtle" />
          Add
        </DropdownMenu.Item>
        <DropdownMenu.Separator />
        <DropdownMenu.Item className="gap-x-2">
          <Trash className="text-ui-fg-subtle" />
          Delete
        </DropdownMenu.Item>
      </DropdownMenu.Content>
    </DropdownMenu>
  )
}

```

## Usage

***

```tsx
import { DropdownMenu } from "@medusajs/ui"
```

```tsx
<DropdownMenu>
  <DropdownMenu.Trigger>Trigger</DropdownMenu.Trigger>
  <DropdownMenu.Content>
    <DropdownMenu.Item>Edit</DropdownMenu.Item>
    <DropdownMenu.Item>Add</DropdownMenu.Item>
    <DropdownMenu.Item>Delete</DropdownMenu.Item>
  </DropdownMenu.Content>
</DropdownMenu>
```

## API Reference

***

### DropdownMenu Props

This component is based on the \[Radix UI Dropdown Menu]\(https://www.radix-ui.com/primitives/docs/components/dropdown-menu) primitive.



### DropdownMenu.Shortcut Props

This component is based on the \`span\` element and supports all of its props



### DropdownMenu.Hint Props

This component is based on the \`span\` element and supports all of its props



## Examples

***

### Sorting

Implementing collection sorting choices using a Dropdown Menu:

```tsx
import { EllipsisHorizontal } from "@medusajs/icons"
import { DropdownMenu, IconButton } from "@medusajs/ui"
import React from "react"

type SortingState = "asc" | "desc" | "alpha" | "alpha-reverse" | "none"

export default function DropdownMenuSorting() {
  const [sort, setSort] = React.useState<SortingState>("none")

  return (
    <div className="flex flex-col items-center gap-y-2">
      <DropdownMenu>
        <DropdownMenu.Trigger asChild>
          <IconButton>
            <EllipsisHorizontal />
          </IconButton>
        </DropdownMenu.Trigger>
        <DropdownMenu.Content className="w-[300px]">
          <DropdownMenu.RadioGroup
            value={sort}
            onValueChange={(v) => setSort(v as SortingState)}
          >
            <DropdownMenu.RadioItem value="none">
              No Sorting
            </DropdownMenu.RadioItem>
            <DropdownMenu.Separator />
            <DropdownMenu.RadioItem value="alpha">
              Alphabetical
              <DropdownMenu.Hint>A-Z</DropdownMenu.Hint>
            </DropdownMenu.RadioItem>
            <DropdownMenu.RadioItem value="alpha-reverse">
              Reverse Alphabetical
              <DropdownMenu.Hint>Z-A</DropdownMenu.Hint>
            </DropdownMenu.RadioItem>
            <DropdownMenu.RadioItem value="asc">
              Created At - Ascending
              <DropdownMenu.Hint>1 - 30</DropdownMenu.Hint>
            </DropdownMenu.RadioItem>
            <DropdownMenu.RadioItem value="desc">
              Created At - Descending
              <DropdownMenu.Hint>30 - 1</DropdownMenu.Hint>
            </DropdownMenu.RadioItem>
          </DropdownMenu.RadioGroup>
        </DropdownMenu.Content>
      </DropdownMenu>
      <div>
        <pre className=" text-sm">Sorting: {sort}</pre>
      </div>
    </div>
  )
}

```


# Focus Modal

A modal component that spans the whole screen

```tsx
import { Button, FocusModal, Heading, Input, Label, Text } from "@medusajs/ui"

export default function FocusModalDemo() {
  return (
    <FocusModal>
      <FocusModal.Trigger asChild>
        <Button>Edit Variant</Button>
      </FocusModal.Trigger>
      <FocusModal.Content>
        <FocusModal.Header>
          <Button>Save</Button>
        </FocusModal.Header>
        <FocusModal.Body className="flex flex-col items-center py-16">
          <div className="flex w-full max-w-lg flex-col gap-y-8">
            <div className="flex flex-col gap-y-1">
              <Heading>Create API key</Heading>
              <Text className="text-ui-fg-subtle">
                Create and manage API keys. You can create multiple keys to
                organize your applications.
              </Text>
            </div>
            <div className="flex flex-col gap-y-2">
              <Label htmlFor="key_name" className="text-ui-fg-subtle">
                Key name
              </Label>
              <Input id="key_name" placeholder="my_app" />
            </div>
          </div>
        </FocusModal.Body>
      </FocusModal.Content>
    </FocusModal>
  )
}

```

## Usage

***

```tsx
import { FocusModal } from "@medusajs/ui"
```

```tsx
<FocusModal>
  <FocusModal.Trigger>Trigger</FocusModal.Trigger>
  <FocusModal.Content>
    <FocusModal.Header>Title</FocusModal.Header>
    <FocusModal.Body>Content</FocusModal.Body>
  </FocusModal.Content>
</FocusModal>
```

### API Reference

***

### FocusModal Props

This component is based on the \[Radix UI Dialog]\(https://www.radix-ui.com/primitives/docs/components/dialog) primitives.

- defaultOpen: (boolean) Whether the modal is opened by default.
- open: (boolean) Whether the modal is opened.
- onOpenChange: (signature) A function to handle when the modal is opened or closed.

### FocusModal.Trigger Props

This component is used to create the trigger button that opens the modal.
It accepts props from the \[Radix UI Dialog Trigger]\(https://www.radix-ui.com/primitives/docs/components/dialog#trigger) component.



### FocusModal.Content Props

This component wraps the content of the modal.
It accepts props from the \[Radix UI Dialog Content]\(https://www.radix-ui.com/primitives/docs/components/dialog#content) component.



### FocusModal.Header Props

This component is used to wrap the header content of the modal.
This component is based on the \`div\` element and supports all of its props



### FocusModal.Body Props

This component is used to wrap the body content of the modal.
This component is based on the \`div\` element and supports all of its props



### FocusModal.Footer Props

This component is used to wrap the footer content of the modal.
This component is based on the \`div\` element and supports all of its props



## Example: Control Open State

***

```tsx
import { useState } from "react"

const MyModal = () => {
  const [open, setOpen] = useState(false)

  return (
    <FocusModal 
      open={open} 
      onOpenChange={setOpen}
    >
      <FocusModal.Trigger>Trigger</FocusModal.Trigger>
      <FocusModal.Content>
        <FocusModal.Header>Title</FocusModal.Header>
        <FocusModal.Body>Content</FocusModal.Body>
      </FocusModal.Content>
    </FocusModal>
  )
}
```


# Heading

Text component used for page titles and other headers

```tsx
import { Heading } from "@medusajs/ui"

export default function HeadingDemo() {
  return (
    <div className="flex flex-col items-center">
      <Heading level="h1">This is an H1 heading</Heading>
      <Heading level="h2">This is an H2 heading</Heading>
      <Heading level="h3">This is an H3 heading</Heading>
    </div>
  )
}

```

## Usage

***

```tsx
import { Heading } from "@medusajs/ui"
```

```tsx
<Heading>A Title</Heading>
```

## API Reference

***

### Heading Props

This component is based on the heading element (\`h1\`, \`h2\`, etc...) depeneding on the specified level
and supports all of its props

- level: (union) The heading level which specifies which heading element is used. Default: "h1"


# Icon Badge

Displays an icon badge

```tsx
import { BuildingTax } from "@medusajs/icons"
import { IconBadge } from "@medusajs/ui"

export default function IconBadgeDemo() {
  return (
    <IconBadge>
      <BuildingTax />
    </IconBadge>
  )
}

```

## Usage

***

```tsx
import { IconBadge } from "@medusajs/ui"
import { BuildingTax } from "@medusajs/icons"
```

```tsx
<IconBadge>
  <BuildingTax />
</IconBadge>
```

## API Reference

***

### IconBadge Props

This component is based on the \`span\` element and supports all of its props

- asChild: (boolean) Whether to remove the wrapper \`span\` element and use the
  passed child element instead. Default: false
- color: (union) The badge's color. Default: "grey"
- size: (union) The badge's size. Default: "base"

## Examples

***

### Grey

```tsx
import { BuildingTax } from "@medusajs/icons"
import { IconBadge } from "@medusajs/ui"

export default function IconBadgeGrey() {
  return (
    <IconBadge color="grey">
      <BuildingTax />
    </IconBadge>
  )
}

```

### Red

```tsx
import { BuildingTax } from "@medusajs/icons"
import { IconBadge } from "@medusajs/ui"

export default function IconBadgeRed() {
  return (
    <IconBadge color="red">
      <BuildingTax />
    </IconBadge>
  )
}

```

### Green

```tsx
import { BuildingTax } from "@medusajs/icons"
import { IconBadge } from "@medusajs/ui"

export default function IconBadgeGreen() {
  return (
    <IconBadge color="green">
      <BuildingTax />
    </IconBadge>
  )
}

```

### Blue

```tsx
import { BuildingTax } from "@medusajs/icons"
import { IconBadge } from "@medusajs/ui"

export default function IconBadgeBlue() {
  return (
    <IconBadge color="blue">
      <BuildingTax />
    </IconBadge>
  )
}

```

### Orange

```tsx
import { BuildingTax } from "@medusajs/icons"
import { IconBadge } from "@medusajs/ui"

export default function IconBadgeOrange() {
  return (
    <IconBadge color="orange">
      <BuildingTax />
    </IconBadge>
  )
}

```

### Purple

```tsx
import { BuildingTax } from "@medusajs/icons"
import { IconBadge } from "@medusajs/ui"

export default function IconBadgePurple() {
  return (
    <IconBadge color="purple">
      <BuildingTax />
    </IconBadge>
  )
}

```

### Base

```tsx
import { BuildingTax } from "@medusajs/icons"
import { IconBadge } from "@medusajs/ui"

export default function IconBadgeBase() {
  return (
    <IconBadge size="base">
      <BuildingTax />
    </IconBadge>
  )
}

```

### Large

```tsx
import { BuildingTax } from "@medusajs/icons"
import { IconBadge } from "@medusajs/ui"

export default function IconBadgeLarge() {
  return (
    <IconBadge size="large">
      <BuildingTax />
    </IconBadge>
  )
}

```


# Icon Button

Displays an icon button

```tsx
import { PlusMini } from "@medusajs/icons"
import { IconButton } from "@medusajs/ui"

export default function IconButtonDemo() {
  return (
    <IconButton>
      <PlusMini />
    </IconButton>
  )
}

```

## Usage

***

```tsx
import { IconButton } from "@medusajs/ui"
import { Plus } from "@medusajs/icons"
```

```tsx
<IconButton>
  <Plus />
</IconButton>
```

## API Reference

***

### IconButton Props

This component is based on the \`button\` element and supports all of its props

- asChild: (boolean) Whether to remove the wrapper \`button\` element and use the
  passed child element instead. Default: false
- isLoading: (boolean) Whether to show a loading spinner. Default: false
- variant: (union) The button's style. Default: "primary"
- size: (union) The button's size. Default: "base"

## Examples

***

### Primary

```tsx
import { PlusMini } from "@medusajs/icons"
import { IconButton } from "@medusajs/ui"

export default function IconButtonPrimary() {
  return (
    <IconButton variant="primary">
      <PlusMini />
    </IconButton>
  )
}

```

### Transparent

```tsx
import { PlusMini } from "@medusajs/icons"
import { IconButton } from "@medusajs/ui"

export default function IconButtonTransparent() {
  return (
    <IconButton variant="transparent">
      <PlusMini />
    </IconButton>
  )
}

```

### Base

```tsx
import { PlusMini } from "@medusajs/icons"
import { IconButton } from "@medusajs/ui"

export default function IconButtonBase() {
  return (
    <IconButton size="base">
      <PlusMini />
    </IconButton>
  )
}

```

### Large

```tsx
import { PlusMini } from "@medusajs/icons"
import { IconButton } from "@medusajs/ui"

export default function IconButtonLarge() {
  return (
    <IconButton size="large">
      <PlusMini />
    </IconButton>
  )
}

```

### X-Large

```tsx
import { PlusMini } from "@medusajs/icons"
import { IconButton } from "@medusajs/ui"

export default function IconButtonXLarge() {
  return (
    <IconButton size="xlarge">
      <PlusMini />
    </IconButton>
  )
}

```

### Disabled

```tsx
import { PlusMini } from "@medusajs/icons"
import { IconButton } from "@medusajs/ui"

export default function IconButtonDisabled() {
  return (
    <IconButton disabled>
      <PlusMini />
    </IconButton>
  )
}

```

### Loading

```tsx
import { PlusMini } from "@medusajs/icons"
import { IconButton } from "@medusajs/ui"

export default function IconButtonLoading() {
  return (
    <IconButton isLoading>
      <PlusMini />
    </IconButton>
  )
}

```


# Inline Tip

A component for displaying a note or tip inline.

```tsx
import { InlineTip } from "@medusajs/ui"

export default function InlineTipDemo() {
  return (
    <InlineTip
      label="Tip"
    >
      Medusa UI is a package of React components to be used in Medusa Admin customizations.
    </InlineTip>
  )
}
```

## Usage

***

```tsx
import { InlineTip } from "@medusajs/ui"
```

```tsx
<InlineTip
  label="This is a tip"
>
  <button>Hover me</button>
</InlineTip>
```

## API Reference

***

### InlineTip Props

This component is based on the \`div\` element and supports all of its props.

- label: (string) The label to display in the tip.
- variant: (union) The variant of the tip. Default: "info"

## Examples

***

### Success Inline Tip

```tsx
import { InlineTip } from "@medusajs/ui"

export default function InlineTipSuccess() {
  return (
    <InlineTip
      label="Success"
      variant="success"
    >
      Product created successfully!
    </InlineTip>
  )
}
```

### Warning Inline Tip

```tsx
import { InlineTip } from "@medusajs/ui"

export default function InlineTipWarning() {
  return (
    <InlineTip
      label="Warning"
      variant="warning"
    >
      This action cannot be undone.
    </InlineTip>
  )
}
```

### Error Inline Tip

```tsx
import { InlineTip } from "@medusajs/ui"

export default function InlineTipError() {
  return (
    <InlineTip
      label="Error"
      variant="error"
    >
      An error occurred. Please try again.
    </InlineTip>
  )
}
```


# Input

Renders a form input field

```tsx
import { Input } from "@medusajs/ui"

export default function InputDemo() {
  return (
    <div className="w-[250px]">
      <Input placeholder="Sales Channel Name" id="sales-channel-name" />
    </div>
  )
}

```

## Usage

***

```tsx
import { Input } from "@medusajs/ui"
```

```tsx
<Input placeholder="Placeholder" id="input-id" />
```

## API Reference

***

### Input Props

This component is based on the \`input\` element and supports all of its props

- size: (union) The input's size. Default: "base"

## Examples

***

### Password

```tsx
import { Input } from "@medusajs/ui"

export default function InputPassword() {
  return (
    <div className="w-[250px]">
      <Input id="password" type="password" defaultValue="supersecret" />
    </div>
  )
}

```

### Search

```tsx
import { Input } from "@medusajs/ui"

export default function InputSearch() {
  return (
    <div className="w-[250px]">
      <Input placeholder="Search" id="search-input" type="search" />
    </div>
  )
}

```

### Disabled

```tsx
import { Input } from "@medusajs/ui"

export default function InputDisabled() {
  return (
    <div className="w-[250px]">
      <Input placeholder="Disabled" id="disabled-input" disabled />
    </div>
  )
}

```

### Small

```tsx
import { Input } from "@medusajs/ui"

export default function InputSmall() {
  return (
    <div className="w-[250px]">
      <Input placeholder="First name" id="first-name" size="small" />
    </div>
  )
}

```

### Error state

You can leverage the native `aria-invalid` property to show an error state on your input:

```tsx
import { Input } from "@medusajs/ui"

export default function InputError() {
  return (
    <div className="w-[250px]">
      <Input
        placeholder="Sales Channel Name"
        id="sales-channel-name"
        aria-invalid={true}
      />
    </div>
  )
}

```


# Kbd

Renders a badge-styled kbd element

```tsx
import { Kbd } from "@medusajs/ui"

export default function KbdDemo() {
  return <Kbd>⌘ + K</Kbd>
}

```

## Usage

***

```tsx
import { Kbd } from "@medusajs/ui"
```

```tsx
<Kbd>Ctrl + Shift + A</Kbd>
```

## API Reference

***

### Kbd Props

This component is based on the \`kbd\` element and supports all of its props



# Label

Renders an accessible label associated with controls.

```tsx
import { Label } from "@medusajs/ui"

export default function LabelDemo() {
  return <Label>Regular label</Label>
}

```

## Usage

***

```tsx
import { Label } from "@medusajs/ui"
```

```tsx
<Label>Label</Label>
```

## API Reference

***

### Label Props

This component is based on the \[Radix UI Label]\(https://www.radix-ui.com/primitives/docs/components/label) primitive.

- size: (union) The label's size. Default: "base"
- weight: (union) The label's font weight. Default: "regular"

## Examples

***

### Base Regular

```tsx
import { Label } from "@medusajs/ui"

export default function LabelBaseRegular() {
  return (
    <Label size="base" weight="regular">
      Label
    </Label>
  )
}

```

### Base Plus

```tsx
import { Label } from "@medusajs/ui"

export default function LabelBasePlus() {
  return (
    <Label size="base" weight="plus">
      Label
    </Label>
  )
}

```

### Large Regular

```tsx
import { Label } from "@medusajs/ui"

export default function LabelLargeRegular() {
  return (
    <Label size="large" weight="regular">
      Label
    </Label>
  )
}

```

### Large Plus

```tsx
import { Label } from "@medusajs/ui"

export default function LabelLargePlus() {
  return (
    <Label size="large" weight="plus">
      Label
    </Label>
  )
}

```

### Small Regular

```tsx
import { Label } from "@medusajs/ui"

export default function LabelSmallRegular() {
  return (
    <Label size="small" weight="regular">
      Label
    </Label>
  )
}

```

### Small Plus

```tsx
import { Label } from "@medusajs/ui"

export default function LabelSmallPlus() {
  return (
    <Label size="small" weight="plus">
      Label
    </Label>
  )
}

```

### X-Small Regular

```tsx
import { Label } from "@medusajs/ui"

export default function LabelXSmallRegular() {
  return (
    <Label size="xsmall" weight="regular">
      Label
    </Label>
  )
}

```

### X-Small Plus

```tsx
import { Label } from "@medusajs/ui"

export default function LabelXSmallPlus() {
  return (
    <Label size="xsmall" weight="plus">
      Label
    </Label>
  )
}

```


# Progress Accordion

A set of expandable content panels, specifically designed for implementing multi-step tasks

```tsx
import { ProgressAccordion, Text } from "@medusajs/ui"

export default function ProgressAccordionDemo() {
  return (
    <div className="w-full px-4">
      <ProgressAccordion type="single">
        <ProgressAccordion.Item value="general">
          <ProgressAccordion.Header>General</ProgressAccordion.Header>
          <ProgressAccordion.Content>
            <div className="pb-6">
              <Text size="small">
                Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed
                ornare, tortor nec commodo ultrices, diam leo porttitor eros,
                eget ultricies mauris nisl nec nisl. Donec quis magna euismod,
                lacinia ipsum id, varius velit.
              </Text>
            </div>
          </ProgressAccordion.Content>
        </ProgressAccordion.Item>
        <ProgressAccordion.Item value="shipping">
          <ProgressAccordion.Header>Shipping</ProgressAccordion.Header>
          <ProgressAccordion.Content>
            <div className="pb-6">
              <Text size="small">
                Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed
                ornare, tortor nec commodo ultrices, diam leo porttitor eros,
                eget ultricies mauris nisl nec nisl. Donec quis magna euismod,
                lacinia ipsum id, varius velit.
              </Text>
            </div>
          </ProgressAccordion.Content>
        </ProgressAccordion.Item>
      </ProgressAccordion>
    </div>
  )
}

```

## Usage

***

```tsx
import { ProgressAccordion } from "@medusajs/ui"
```

```tsx
<ProgressAccordion type="single">
  <ProgressAccordion.Item value="general">
    <ProgressAccordion.Header>
      General
    </ProgressAccordion.Header>
    <ProgressAccordion.Content>
       {/* Content */}
    </ProgressAccordion.Content>
  </ProgressAccordion.Item>
  <ProgressAccordion.Item value="shipping">
    <ProgressAccordion.Header>
      Shipping
    </ProgressAccordion.Header>
    <ProgressAccordion.Content>
        {/* Content */}
    </ProgressAccordion.Content>
  </ProgressAccordion.Item>
</ProgressAccordion>
```

## API Reference

***

### ProgressAccordion Props

This component is based on the \[Radix UI Accordion]\(https://radix-ui.com/primitives/docs/components/accordion) primitves.



### ProgressAccordion.Header Props

- status: (union) The current status. Default: "not-started"

## Examples

***

### Single

```tsx
import { ProgressAccordion, Text } from "@medusajs/ui"

export default function ProgressAccordionSingle() {
  return (
    <div className="w-full px-4">
      <ProgressAccordion type="single">
        <ProgressAccordion.Item value="general">
          <ProgressAccordion.Header>General</ProgressAccordion.Header>
          <ProgressAccordion.Content>
            <div className="pb-6">
              <Text size="small">
                Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed
                ornare, tortor nec commodo ultrices, diam leo porttitor eros,
                eget ultricies mauris nisl nec nisl. Donec quis magna euismod,
                lacinia ipsum id, varius velit.
              </Text>
            </div>
          </ProgressAccordion.Content>
        </ProgressAccordion.Item>
        <ProgressAccordion.Item value="shipping">
          <ProgressAccordion.Header>Shipping</ProgressAccordion.Header>
          <ProgressAccordion.Content>
            <div className="pb-6">
              <Text size="small">
                Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed
                ornare, tortor nec commodo ultrices, diam leo porttitor eros,
                eget ultricies mauris nisl nec nisl. Donec quis magna euismod,
                lacinia ipsum id, varius velit.
              </Text>
            </div>
          </ProgressAccordion.Content>
        </ProgressAccordion.Item>
      </ProgressAccordion>
    </div>
  )
}

```

### Multiple

```tsx
import { ProgressAccordion, Text } from "@medusajs/ui"

export default function ProgressAccordionSingle() {
  return (
    <div className="w-full px-4">
      <ProgressAccordion type="multiple">
        <ProgressAccordion.Item value="general">
          <ProgressAccordion.Header>General</ProgressAccordion.Header>
          <ProgressAccordion.Content>
            <div className="pb-6">
              <Text size="small">
                Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed
                ornare, tortor nec commodo ultrices, diam leo porttitor eros,
                eget ultricies mauris nisl nec nisl. Donec quis magna euismod,
                lacinia ipsum id, varius velit.
              </Text>
            </div>
          </ProgressAccordion.Content>
        </ProgressAccordion.Item>
        <ProgressAccordion.Item value="shipping">
          <ProgressAccordion.Header>Shipping</ProgressAccordion.Header>
          <ProgressAccordion.Content>
            <div className="pb-6">
              <Text size="small">
                Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed
                ornare, tortor nec commodo ultrices, diam leo porttitor eros,
                eget ultricies mauris nisl nec nisl. Donec quis magna euismod,
                lacinia ipsum id, varius velit.
              </Text>
            </div>
          </ProgressAccordion.Content>
        </ProgressAccordion.Item>
      </ProgressAccordion>
    </div>
  )
}

```


# Progress Tabs

A set of layered content panels, known as tabs, specifically designed for implementing multi-step tasks

```tsx
import { ProgressTabs, Text } from "@medusajs/ui"

export default function ProgressTabsDemo() {
  return (
    <div className="w-full px-4">
      <ProgressTabs defaultValue="general">
        <div className="border-b border-ui-border-base">
          <ProgressTabs.List>
            <ProgressTabs.Trigger value="general">General</ProgressTabs.Trigger>
            <ProgressTabs.Trigger value="shipping">
              Shipping
            </ProgressTabs.Trigger>
            <ProgressTabs.Trigger value="payment">Payment</ProgressTabs.Trigger>
          </ProgressTabs.List>
        </div>
        <div className="mt-2">
          <ProgressTabs.Content value="general">
            <Text size="small">
              At ACME, we&apos;re dedicated to providing you with an exceptional
              shopping experience. Our wide selection of products caters to your
              every need, from fashion to electronics and beyond. We take pride
              in our commitment to quality, customer satisfaction, and timely
              delivery. Our friendly customer support team is here to assist you
              with any inquiries or concerns you may have. Thank you for
              choosing ACME as your trusted online shopping destination.
            </Text>
          </ProgressTabs.Content>
          <ProgressTabs.Content value="shipping">
            <Text size="small">
              Shipping is a crucial part of our service, designed to ensure your
              products reach you quickly and securely. Our dedicated team works
              tirelessly to process orders, carefully package items, and
              coordinate with reliable carriers to deliver your purchases to
              your doorstep. We take pride in our efficient shipping process,
              guaranteeing your satisfaction with every delivery.
            </Text>
          </ProgressTabs.Content>
          <ProgressTabs.Content value="payment">
            <Text size="small">
              Our payment process is designed to make your shopping experience
              smooth and secure. We offer a variety of payment options to
              accommodate your preferences, from credit and debit cards to
              online payment gateways. Rest assured that your financial
              information is protected through advanced encryption methods.
              Shopping with us means you can shop with confidence, knowing your
              payments are safe and hassle-free.
            </Text>
          </ProgressTabs.Content>
        </div>
      </ProgressTabs>
    </div>
  )
}

```

## Usage

***

```tsx
import { ProgressTabs } from "@medusajs/ui"
```

```tsx
<ProgressTabs defaultValue="general">
  <ProgressTabs.List>
    <ProgressTabs.Trigger value="general">
      General
    </ProgressTabs.Trigger>
    <ProgressTabs.Trigger value="shipping">
        Shipping
    </ProgressTabs.Trigger>
    <ProgressTabs.Trigger value="payment">
      Payment
    </ProgressTabs.Trigger>
  </ProgressTabs.List>
  <ProgressTabs.Content value="general">
    {/* Content */}
  </ProgressTabs.Content>
    <ProgressTabs.Content value="shipping">
       {/* Content */}
    </ProgressTabs.Content>
    <ProgressTabs.Content value="payment">
        {/* Content */}
  </ProgressTabs.Content>
</ProgressTabs>
```

## API Reference

***

### ProgressTabs Props

This component is based on the \[Radix UI Tabs]\(https://radix-ui.com/primitives/docs/components/tabs) primitves.

- activationMode: (union) Whether a tab is activated automatically or manually.
- defaultValue: (string) The value of the tab to select by default, if uncontrolled
- dir: (Direction) The direction of navigation between toolbar items.
- onValueChange: (signature) A function called when a new tab is selected
- orientation: (union) The orientation the tabs are layed out.
  Mainly so arrow navigation is done accordingly (left & right vs. up & down)
- value: (string) The value for the selected tab, if controlled

### ProgressTabs.ProgressIndicator Props

- status: (union) The current status.


# Prompt

Displays a dialog triggered by a button

```tsx
import { Button, Prompt } from "@medusajs/ui"

export default function PromptDemo() {
  return (
    <Prompt>
      <Prompt.Trigger asChild>
        <Button>Open</Button>
      </Prompt.Trigger>
      <Prompt.Content>
        <Prompt.Header>
          <Prompt.Title>Delete something</Prompt.Title>
          <Prompt.Description>
            Are you sure? This cannot be undone.
          </Prompt.Description>
        </Prompt.Header>
        <Prompt.Footer>
          <Prompt.Cancel>Cancel</Prompt.Cancel>
          <Prompt.Action>Delete</Prompt.Action>
        </Prompt.Footer>
      </Prompt.Content>
    </Prompt>
  )
}

```

## Usage

***

```tsx
import { Prompt } from "@medusajs/ui"
```

```tsx
<Prompt>
  <Prompt.Trigger>Trigger</Prompt.Trigger>
  <Prompt.Content>
    <Prompt.Header>
      <Prompt.Title>Title</Prompt.Title>
      <Prompt.Description>Description</Prompt.Description>
    </Prompt.Header>
    <Prompt.Footer>
      <Prompt.Cancel>Cancel</Prompt.Cancel>
      <Prompt.Action>Delete</Prompt.Action>
    </Prompt.Footer>
  </Prompt.Content>
</Prompt>
```

## API Reference

***

### Prompt Props

This component is based on the \[Radix UI Alert Dialog]\(https://www.radix-ui.com/primitives/docs/components/alert-dialog) primitives.

- variant: (union) The variant of the prompt. Default: "danger"

### Prompt.Header Props

This component is based on the \`div\` element and supports all of its props



### Prompt.Footer Props

This component is based on the \`div\` element and supports all of its props



# Radio Group

A set of checkable buttons—known as radio buttons—where no more than one of the buttons can be checked at a time.

```tsx
import { Label, RadioGroup } from "@medusajs/ui"

export default function RadioGroupDemo() {
  return (
    <RadioGroup>
      <div className="flex items-center gap-x-3">
        <RadioGroup.Item value="1" id="radio_1" />
        <Label htmlFor="radio_1" weight="plus">
          Radio 1
        </Label>
      </div>
      <div className="flex items-center gap-x-3">
        <RadioGroup.Item value="2" id="radio_2" />
        <Label htmlFor="radio_2" weight="plus">
          Radio 2
        </Label>
      </div>
      <div className="flex items-center gap-x-3">
        <RadioGroup.Item value="3" id="radio_3" />
        <Label htmlFor="radio_3" weight="plus">
          Radio 3
        </Label>
      </div>
    </RadioGroup>
  )
}

```

## Usage

***

```tsx
import { RadioGroup } from "@medusajs/ui"
```

```tsx
<RadioGroup>
  <RadioGroup.Item value="1" id="radio_1" />
  <RadioGroup.Item value="2" id="radio_2" />
  <RadioGroup.Item value="3" id="radio_3" />
</RadioGroup>
```

## API Reference

***

### RadioGroup Props

This component is based on the \[Radix UI Radio Group]\(https://www.radix-ui.com/primitives/docs/components/radio-group) primitives.



## Examples

***

### With Descriptions

```tsx
import { Label, RadioGroup, Text } from "@medusajs/ui"

export default function RadioGroupDescriptions() {
  return (
    <RadioGroup>
      <div className="flex items-start gap-x-3">
        <RadioGroup.Item value="1" id="radio_1_descriptions" />
        <div className="flex flex-col gap-y-0.5">
          <Label htmlFor="radio_1_descriptions" weight="plus">
            Radio 1
          </Label>
          <Text className="text-ui-fg-subtle">
            The quick brown fox jumps over the lazy dog.
          </Text>
        </div>
      </div>
      <div className="flex items-start gap-x-3">
        <RadioGroup.Item value="2" id="radio_2_descriptions" />
        <div className="flex flex-col gap-y-0.5">
          <Label htmlFor="radio_2_descriptions" weight="plus">
            Radio 2
          </Label>
          <Text className="text-ui-fg-subtle">
            The quick brown fox jumps over the lazy dog.
          </Text>
        </div>
      </div>
      <div className="flex items-start gap-x-3">
        <RadioGroup.Item value="3" id="radio_3_descriptions" />
        <div className="flex flex-col gap-y-0.5">
          <Label htmlFor="radio_3_descriptions" weight="plus">
            Radio 3
          </Label>
          <Text className="text-ui-fg-subtle">
            The quick brown fox jumps over the lazy dog.
          </Text>
        </div>
      </div>
    </RadioGroup>
  )
}

```

### With a disabled item

```tsx
import { Label, RadioGroup } from "@medusajs/ui"

export default function RadioGroupDisabled() {
  return (
    <RadioGroup>
      <div className="flex items-center gap-x-3">
        <RadioGroup.Item value="1" id="radio_1_disabled" />
        <Label htmlFor="radio_1_disabled" weight="plus">
          Radio 1
        </Label>
      </div>
      <div className="flex items-center gap-x-3">
        <RadioGroup.Item value="2" id="radio_2_disabled" />
        <Label htmlFor="radio_2_disabled" weight="plus">
          Radio 2
        </Label>
      </div>
      <div className="flex items-center gap-x-3">
        <RadioGroup.Item value="3" id="radio_3_disabled" disabled={true} />
        <Label htmlFor="radio_3_disabled" weight="plus">
          Radio 3
        </Label>
      </div>
    </RadioGroup>
  )
}

```


# Select

Displays a list of options for the user to pick from—triggered by a button.

```tsx
import { Select } from "@medusajs/ui"

export default function SelectDemo() {
  return (
    <div className="w-[256px]">
      <Select>
        <Select.Trigger>
          <Select.Value placeholder="Select a currency" />
        </Select.Trigger>
        <Select.Content>
          {currencies.map((item) => (
            <Select.Item key={item.value} value={item.value}>
              {item.label}
            </Select.Item>
          ))}
        </Select.Content>
      </Select>
    </div>
  )
}

const currencies = [
  {
    value: "eur",
    label: "EUR",
  },
  {
    value: "usd",
    label: "USD",
  },
  {
    value: "dkk",
    label: "DKK",
  },
]

```

## Usage

***

```tsx
import { Select } from "@medusajs/ui"
```

```tsx
<Select>
  <Select.Trigger>
    <Select.Value placeholder="Placeholder" />
  </Select.Trigger>
  <Select.Content>
    {items.map((item) => (
      <Select.Item key={item.value} value={item.value}>
        {item.label}
      </Select.Item>
    ))}
  </Select.Content>
</Select>
```

## API Reference

***

### Select Props

This component is based on \[Radix UI Select]\(https://www.radix-ui.com/primitives/docs/components/select).
It also accepts all props of the HTML \`select\` component.

- size: (union) The select's size. Default: "base"

### Select.Trigger Props

The trigger that toggles the select.
It's based on \[Radix UI Select Trigger]\(https://www.radix-ui.com/primitives/docs/components/select#trigger).



### Select.Value Props

Displays the selected value, or a placeholder if no value is selected.
It's based on \[Radix UI Select Value]\(https://www.radix-ui.com/primitives/docs/components/select#value).



### Select.Group Props

Groups multiple items together.



### Select.Label Props

Used to label a group of items.
It's based on \[Radix UI Select Label]\(https://www.radix-ui.com/primitives/docs/components/select#label).



### Select.Item Props

An item in the select. It's based on \[Radix UI Select Item]\(https://www.radix-ui.com/primitives/docs/components/select#item)
and accepts its props.



### Select.Content Props

The content that appears when the select is open.
It's based on \[Radix UI Select Content]\(https://www.radix-ui.com/primitives/docs/components/select#content).

- position: (union) Whether to show the select items below (\`popper\`) or over (\`item-aligned\`) the select input. Default: "popper"
- sideOffset: (number) The distance of the content pop-up in pixels from the select input. Only available when position is set to popper. Default: 8
- collisionPadding: (union) The distance in pixels from the boundary edges where collision detection should occur. Only available when position is set to popper. Default: 24

## Examples

***

### Small

```tsx
import { Select } from "@medusajs/ui"

export default function SelectSmall() {
  return (
    <div className="w-[256px]">
      <Select size="small">
        <Select.Trigger>
          <Select.Value placeholder="Select a currency" />
        </Select.Trigger>
        <Select.Content>
          {currencies.map((item) => (
            <Select.Item key={item.value} value={item.value}>
              {item.label}
            </Select.Item>
          ))}
        </Select.Content>
      </Select>
    </div>
  )
}

const currencies = [
  {
    value: "eur",
    label: "EUR",
  },
  {
    value: "usd",
    label: "USD",
  },
  {
    value: "dkk",
    label: "DKK",
  },
]

```

### Item-Aligned Position

```tsx
import { Select } from "@medusajs/ui"

export default function SelectItemAligned() {
  return (
    <div className="w-[256px]">
      <Select>
        <Select.Trigger>
          <Select.Value placeholder="Select a currency" />
        </Select.Trigger>
        <Select.Content position="item-aligned">
          {currencies.map((item) => (
            <Select.Item key={item.value} value={item.value}>
              {item.label}
            </Select.Item>
          ))}
        </Select.Content>
      </Select>
    </div>
  )
}

const currencies = [
  {
    value: "eur",
    label: "EUR",
  },
  {
    value: "usd",
    label: "USD",
  },
  {
    value: "dkk",
    label: "DKK",
  },
]

```

### Disabled

```tsx
import { Select } from "@medusajs/ui"

export default function SelectDemo() {
  return (
    <div className="w-[256px]">
      <Select disabled>
        <Select.Trigger>
          <Select.Value placeholder="Select a currency" />
        </Select.Trigger>
        <Select.Content>
          {currencies.map((item) => (
            <Select.Item key={item.value} value={item.value}>
              {item.label}
            </Select.Item>
          ))}
        </Select.Content>
      </Select>
    </div>
  )
}

const currencies = [
  {
    value: "eur",
    label: "EUR",
  },
  {
    value: "usd",
    label: "USD",
  },
  {
    value: "dkk",
    label: "DKK",
  },
]

```

### Grouped Items

```tsx
import { Select } from "@medusajs/ui"

export default function SelectDemo() {
  return (
    <div className="w-[256px]">
      <Select>
        <Select.Trigger>
          <Select.Value placeholder="Select a currency" />
        </Select.Trigger>
        <Select.Content>
          {data.map((group) => (
            <Select.Group key={group.label}>
              <Select.Label>{group.label}</Select.Label>
              {group.items.map((item) => (
                <Select.Item key={item.value} value={item.value}>
                  {item.label}
                </Select.Item>
              ))}
            </Select.Group>
          ))}
        </Select.Content>
      </Select>
    </div>
  )
}

const data = [
  {
    label: "Shirts",
    items: [
      {
        value: "dress-shirt-solid",
        label: "Solid Dress Shirt",
      },
      {
        value: "dress-shirt-check",
        label: "Check Dress Shirt",
      },
    ],
  },
  {
    label: "T-Shirts",
    items: [
      {
        value: "v-neck",
        label: "V-Neck",
      },
      {
        value: "crew-neck",
        label: "Crew Neck",
      },
      {
        value: "henley",
        label: "Henley",
      },
    ],
  },
]

```

### Controlled

```tsx
import { Select } from "@medusajs/ui"
import * as React from "react"

export default function SelectDemo() {
  const [value, setValue] = React.useState<string | undefined>()

  return (
    <div className="w-[256px]">
      <Select onValueChange={setValue} value={value}>
        <Select.Trigger>
          <Select.Value placeholder="Select a currency" />
        </Select.Trigger>
        <Select.Content>
          {currencies.map((item) => (
            <Select.Item key={item.value} value={item.value}>
              {item.label}
            </Select.Item>
          ))}
        </Select.Content>
      </Select>
    </div>
  )
}

const currencies = [
  {
    value: "eur",
    label: "EUR",
  },
  {
    value: "usd",
    label: "USD",
  },
  {
    value: "dkk",
    label: "DKK",
  },
]

```


# Status Badge

Displays a status badge

```tsx
import { StatusBadge } from "@medusajs/ui"

export default function StatusBadgeDemo() {
  return <StatusBadge>Status</StatusBadge>
}

```

## Usage

***

```tsx
import { StatusBadge } from "@medusajs/ui"
```

```tsx
<StatusBadge color="green">Active</StatusBadge>
```

## API Reference

***

### StatusBadge Props

This component is based on the span element and supports all of its props

- color: (union) The status's color. Default: "grey"

## Examples

***

### Grey

```tsx
import { StatusBadge } from "@medusajs/ui"

export default function StatusBadgeGrey() {
  return <StatusBadge color="grey">Status</StatusBadge>
}

```

### Red

```tsx
import { StatusBadge } from "@medusajs/ui"

export default function StatusBadgeRed() {
  return <StatusBadge color="red">Status</StatusBadge>
}

```

### Green

```tsx
import { StatusBadge } from "@medusajs/ui"

export default function StatusBadgeGreen() {
  return <StatusBadge color="green">Status</StatusBadge>
}

```

### Blue

```tsx
import { StatusBadge } from "@medusajs/ui"

export default function StatusBadgeBlue() {
  return <StatusBadge color="blue">Status</StatusBadge>
}

```

### Orange

```tsx
import { StatusBadge } from "@medusajs/ui"

export default function StatusBadgeOrange() {
  return <StatusBadge color="orange">Status</StatusBadge>
}

```

### Purple

```tsx
import { StatusBadge } from "@medusajs/ui"

export default function StatusBadgePurple() {
  return <StatusBadge color="purple">Status</StatusBadge>
}

```


# Switch

A control that allows the user to toggle between checked and not checked.

```tsx
import { Label, Switch } from "@medusajs/ui"

export default function SwitchDemo() {
  return (
    <div className="flex items-center gap-x-2">
      <Switch id="manage-inventory" />
      <Label htmlFor="manage-inventory">Manage Inventory</Label>
    </div>
  )
}

```

## Usage

***

```tsx
import { Switch } from "@medusajs/ui"
```

```tsx
<Switch />
```

## API Reference

***

### Switch Props

This component is based on the \[Radix UI Switch]\(https://www.radix-ui.com/primitives/docs/components/switch) primitive.

- size: (union) The switch's size. Default: "base"

## Examples

***

### Small

```tsx
import { Label, Switch } from "@medusajs/ui"

export default function SwitchSmall() {
  return (
    <div className="flex items-center gap-x-2">
      <Switch id="manage-inventory-small" size="small" />
      <Label htmlFor="manage-inventory-small" size="small">
        Manage Inventory
      </Label>
    </div>
  )
}

```

### Disabled

```tsx
import { Label, Switch } from "@medusajs/ui"

export default function SwitchDisabled() {
  return (
    <div className="flex items-center gap-x-2">
      <Switch id="manage-inventory-disabled" disabled={true} />
      <Label htmlFor="manage-inventory-disabled">Manage Inventory</Label>
    </div>
  )
}

```

### Checked

```tsx
import { Label, Switch } from "@medusajs/ui"

export default function SwitchChecked() {
  return (
    <div className="flex items-center gap-x-2">
      <Switch id="manage-inventory-checked" checked={true} />
      <Label htmlFor="manage-inventory-checked">Manage Inventory</Label>
    </div>
  )
}

```

### Checked Disabled

```tsx
import { Label, Switch } from "@medusajs/ui"

export default function SwitchCheckedDisabled() {
  return (
    <div className="flex items-center gap-x-2">
      <Switch
        id="manage-inventory-checked-disabled"
        checked={true}
        disabled={true}
      />
      <Label htmlFor="manage-inventory-checked-disabled">
        Manage Inventory
      </Label>
    </div>
  )
}

```


# Table

A Table component for displaying data.

If you're looking to add a table in your Medusa Admin Extensions with features like pagination and filters, refer to the [DataTable](https://docs.medusajs.com/components/data-table/index.html.md) component instead.

```tsx
import { Table } from "@medusajs/ui"

type Order = {
  id: string
  displayId: number
  customer: string
  email: string
  amount: number
  currency: string
}

const fakeData: Order[] = [
  {
    id: "order_6782",
    displayId: 86078,
    customer: "Jill Miller",
    email: "32690@gmail.com",
    amount: 493,
    currency: "EUR",
  },
  {
    id: "order_46487",
    displayId: 42845,
    customer: "Sarah Garcia",
    email: "86379@gmail.com",
    amount: 113,
    currency: "JPY",
  },
  {
    id: "order_8169",
    displayId: 39129,
    customer: "Josef Smith",
    email: "89383@gmail.com",
    amount: 43,
    currency: "USD",
  },
  {
    id: "order_67883",
    displayId: 5548,
    customer: "Elvis Jones",
    email: "52860@gmail.com",
    amount: 840,
    currency: "GBP",
  },
  {
    id: "order_61121",
    displayId: 87668,
    customer: "Charles Rodriguez",
    email: "45675@gmail.com",
    amount: 304,
    currency: "GBP",
  },
]

export default function TableDemo() {
  return (
    <Table>
      <Table.Header>
        <Table.Row>
          <Table.HeaderCell>#</Table.HeaderCell>
          <Table.HeaderCell>Customer</Table.HeaderCell>
          <Table.HeaderCell>Email</Table.HeaderCell>
          <Table.HeaderCell className="text-right">Amount</Table.HeaderCell>
          <Table.HeaderCell></Table.HeaderCell>
        </Table.Row>
      </Table.Header>
      <Table.Body>
        {fakeData.map((order) => {
          return (
            <Table.Row
              key={order.id}
              className="[&_td:last-child]:w-[1%] [&_td:last-child]:whitespace-nowrap"
            >
              <Table.Cell>{order.displayId}</Table.Cell>
              <Table.Cell>{order.customer}</Table.Cell>
              <Table.Cell>{order.email}</Table.Cell>
              <Table.Cell className="text-right">
                {new Intl.NumberFormat("en-US", {
                  style: "currency",
                  currency: order.currency,
                }).format(order.amount)}
              </Table.Cell>
              <Table.Cell className="text-ui-fg-muted">
                {order.currency}
              </Table.Cell>
            </Table.Row>
          )
        })}
      </Table.Body>
    </Table>
  )
}

```

## Usage

***

```tsx
import { Table } from "@medusajs/ui"
```

```tsx
<Table>
  <Table.Header>
    <Table.Row>
      <Table.HeaderCell>#</Table.HeaderCell>
      <Table.HeaderCell>Customer</Table.HeaderCell>
      <Table.HeaderCell>Email</Table.HeaderCell>
    </Table.Row>
  </Table.Header>
  <Table.Body>
    <Table.Row>
      <Table.Cell>1</Table.Cell>
      <Table.Cell>Emil Larsson</Table.Cell>
      <Table.Cell>emil2738@gmail.com</Table.Cell>
    </Table.Row>
  </Table.Body>
</Table>
```

## API Reference

***

### Table Props

This component is based on the table element and its various children:

\- \`Table\`: \`table\`
\- \`Table.Header\`: \`thead\`
\- \`Table.Row\`: \`tr\`
\- \`Table.HeaderCell\`: \`th\`
\- \`Table.Body\`: \`tbody\`
\- \`Table.Cell\`: \`td\`

Each component supports the props or attributes of its equivalent HTML element.



### Table.Pagination Props

This component is based on the \`div\` element and supports all of its props

- count: (number) The total number of items.
- pageSize: (number) The number of items per page.
- pageIndex: (number) The current page index.
- pageCount: (number) The total number of pages.
- canPreviousPage: (boolean) Whether there's a previous page that can be navigated to.
- canNextPage: (boolean) Whether there's a next page that can be navigated to.
- translations: (signature) An optional object of words to use in the pagination component.
  Use this to override the default words, or translate them into another language. Default: \{
  &#x20; of: "of",
  &#x20; results: "results",
  &#x20; pages: "pages",
  &#x20; prev: "Prev",
  &#x20; next: "Next",
  }
- previousPage: (signature) A function that handles navigating to the previous page.
  This function should handle retrieving data for the previous page.
- nextPage: (signature) A function that handles navigating to the next page.
  This function should handle retrieving data for the next page.

## Examples

***

### Table with Pagination

```tsx
import { Table } from "@medusajs/ui"
import { useMemo, useState } from "react"

type Order = {
  id: string
  displayId: number
  customer: string
  email: string
  amount: number
  currency: string
}

export default function TableDemo() {
  const fakeData: Order[] = useMemo(
    () => [
      {
        id: "order_6782",
        displayId: 86078,
        customer: "Jill Miller",
        email: "32690@gmail.com",
        amount: 493,
        currency: "EUR",
      },
      {
        id: "order_46487",
        displayId: 42845,
        customer: "Sarah Garcia",
        email: "86379@gmail.com",
        amount: 113,
        currency: "JPY",
      },
      {
        id: "order_8169",
        displayId: 39129,
        customer: "Josef Smith",
        email: "89383@gmail.com",
        amount: 43,
        currency: "USD",
      },
      {
        id: "order_67883",
        displayId: 5548,
        customer: "Elvis Jones",
        email: "52860@gmail.com",
        amount: 840,
        currency: "GBP",
      },
      {
        id: "order_61121",
        displayId: 87668,
        customer: "Charles Rodriguez",
        email: "45675@gmail.com",
        amount: 304,
        currency: "GBP",
      },
    ],
    []
  )
  const [currentPage, setCurrentPage] = useState(0)
  const pageSize = 3
  const pageCount = Math.ceil(fakeData.length / pageSize)
  const canNextPage = useMemo(
    () => currentPage < pageCount - 1,
    [currentPage, pageCount]
  )
  const canPreviousPage = useMemo(() => currentPage - 1 >= 0, [currentPage])

  const nextPage = () => {
    if (canNextPage) {
      setCurrentPage(currentPage + 1)
    }
  }

  const previousPage = () => {
    if (canPreviousPage) {
      setCurrentPage(currentPage - 1)
    }
  }

  const currentOrders = useMemo(() => {
    const offset = currentPage * pageSize
    const limit = Math.min(offset + pageSize, fakeData.length)

    return fakeData.slice(offset, limit)
  }, [currentPage, pageSize, fakeData])

  return (
    <div className="flex gap-1 flex-col">
      <Table>
        <Table.Header>
          <Table.Row>
            <Table.HeaderCell>#</Table.HeaderCell>
            <Table.HeaderCell>Customer</Table.HeaderCell>
            <Table.HeaderCell>Email</Table.HeaderCell>
            <Table.HeaderCell className="text-right">Amount</Table.HeaderCell>
            <Table.HeaderCell></Table.HeaderCell>
          </Table.Row>
        </Table.Header>
        <Table.Body>
          {currentOrders.map((order) => {
            return (
              <Table.Row
                key={order.id}
                className="[&_td:last-child]:w-[1%] [&_td:last-child]:whitespace-nowrap"
              >
                <Table.Cell>{order.displayId}</Table.Cell>
                <Table.Cell>{order.customer}</Table.Cell>
                <Table.Cell>{order.email}</Table.Cell>
                <Table.Cell className="text-right">
                  {new Intl.NumberFormat("en-US", {
                    style: "currency",
                    currency: order.currency,
                  }).format(order.amount)}
                </Table.Cell>
                <Table.Cell className="text-ui-fg-muted">
                  {order.currency}
                </Table.Cell>
              </Table.Row>
            )
          })}
        </Table.Body>
      </Table>
      <Table.Pagination
        count={fakeData.length}
        pageSize={pageSize}
        pageIndex={currentPage}
        pageCount={fakeData.length}
        canPreviousPage={canPreviousPage}
        canNextPage={canNextPage}
        previousPage={previousPage}
        nextPage={nextPage}
      />
    </div>
  )
}

```


# Tabs

A set of layered content panels, known as tabs, that display content one panel at a time

```tsx
import { Tabs, Text } from "@medusajs/ui"

export default function TabsDemo() {
  return (
    <div className="w-full px-4">
      <Tabs defaultValue="general">
        <Tabs.List>
          <Tabs.Trigger value="general">General</Tabs.Trigger>
          <Tabs.Trigger value="shipping">Shipping</Tabs.Trigger>
          <Tabs.Trigger value="payment">Payment</Tabs.Trigger>
        </Tabs.List>
        <div className="mt-2">
          <Tabs.Content value="general">
            <Text size="small">
              At ACME, we&apos;re dedicated to providing you with an exceptional
              shopping experience. Our wide selection of products caters to your
              every need, from fashion to electronics and beyond. We take pride
              in our commitment to quality, customer satisfaction, and timely
              delivery. Our friendly customer support team is here to assist you
              with any inquiries or concerns you may have. Thank you for
              choosing ACME as your trusted online shopping destination.
            </Text>
          </Tabs.Content>
          <Tabs.Content value="shipping">
            <Text size="small">
              Shipping is a crucial part of our service, designed to ensure your
              products reach you quickly and securely. Our dedicated team works
              tirelessly to process orders, carefully package items, and
              coordinate with reliable carriers to deliver your purchases to
              your doorstep. We take pride in our efficient shipping process,
              guaranteeing your satisfaction with every delivery.
            </Text>
          </Tabs.Content>
          <Tabs.Content value="payment">
            <Text size="small">
              Our payment process is designed to make your shopping experience
              smooth and secure. We offer a variety of payment options to
              accommodate your preferences, from credit and debit cards to
              online payment gateways. Rest assured that your financial
              information is protected through advanced encryption methods.
              Shopping with us means you can shop with confidence, knowing your
              payments are safe and hassle-free.
            </Text>
          </Tabs.Content>
        </div>
      </Tabs>
    </div>
  )
}

```

## Usage

***

```tsx
import { Tabs } from "@medusajs/ui"
```

```tsx
<Tabs>
  <Tabs.List>
    <Tabs.Trigger value="1">Tab 1</Tabs.Trigger>
    <Tabs.Trigger value="2">Tab 2</Tabs.Trigger>
    <Tabs.Trigger value="3">Tab 3</Tabs.Trigger>
  </Tabs.List>
  <Tabs.Content value="1">Panel 1</Tabs.Content>
  <Tabs.Content value="2">Panel 2</Tabs.Content>
  <Tabs.Content value="3">Panel 3</Tabs.Content>
</Tabs>
```

## API Reference

***

### Tabs Props

This component is based on the \[Radix UI Tabs]\(https://radix-ui.com/primitives/docs/components/tabs) primitves

- activationMode: (union) Whether a tab is activated automatically or manually.
- defaultValue: (string) The value of the tab to select by default, if uncontrolled
- dir: (Direction) The direction of navigation between toolbar items.
- onValueChange: (signature) A function called when a new tab is selected
- orientation: (union) The orientation the tabs are layed out.
  Mainly so arrow navigation is done accordingly (left & right vs. up & down)
- value: (string) The value for the selected tab, if controlled


# Text

Displays basic text.

```tsx
import { Text } from "@medusajs/ui"

export default function TextDemo() {
  return <Text>Text</Text>
}

```

## Usage

***

```tsx
import { Text } from "@medusajs/ui"
```

```tsx
<Text>Text</Text>
```

## API Reference

***

### Text Props

This component is based on the \`p\` element and supports all of its props

- asChild: (boolean) Whether to remove the wrapper \`button\` element and use the
  passed child element instead. Default: false
- as: (union) The wrapper element to use when \`asChild\` is disabled. Default: "p"
- size: (union) The text's size. Default: "base"
- weight: (union) The text's font weight. Default: "regular"
- family: (union) The text's font family. Default: "sans"
- leading: (union) The text's line height. Default: "normal"

## Examples

***

### All variations

```tsx
import { Text } from "@medusajs/ui"

export default function TextExamples() {
  return (
    <div className="flex flex-col items-start gap-y-2">
      <Text size="base" weight="regular" family="sans">
        Base Size, Regular Weight, Sans-Serif
      </Text>
      <Text size="base" weight="plus" family="sans">
        Base Size, Plus Weight, Sans-Serif
      </Text>
      <Text size="large" weight="regular" family="sans">
        Large Size, Regular Weight, Sans-Serif
      </Text>
      <Text size="large" weight="plus" family="sans">
        Large Size, Plus Weight, Sans-Serif
      </Text>
      <Text size="xlarge" weight="regular" family="sans">
        XLarge Size, Regular Weight, Sans-Serif
      </Text>
      <Text size="xlarge" weight="plus" family="sans">
        XLarge Size, Plus Weight, Sans-Serif
      </Text>
      <Text size="base" weight="regular" family="mono">
        Base Size, Regular Weight, Mono
      </Text>
      <Text size="large" weight="regular" family="mono">
        Large Size, Regular Weight, Mono
      </Text>
      <Text size="large" weight="plus" family="mono">
        Large Size, Plus Weight, Mono
      </Text>
      <Text size="xlarge" weight="regular" family="mono">
        XLarge Size, Regular Weight, Mono
      </Text>
      <Text size="xlarge" weight="plus" family="mono">
        XLarge Size, Plus Weight, Mono
      </Text>
    </div>
  )
}

```


# Textarea

Displays a form textarea.

```tsx
import { Textarea } from "@medusajs/ui"

export default function TextAreaDemo() {
  return <Textarea placeholder="Product description ..." />
}

```

## Usage

***

```tsx
import { Textarea } from "@medusajs/ui"
```

```tsx
<Textarea />
```

## API Reference

***

### Textarea Props

This component is based on the \`textarea\` element and supports all of its props



# Toast

A succinct message that is displayed temporarily.

```tsx
import { Button, Toaster, toast } from "@medusajs/ui"

export default function ToasterDemo() {
  return (
    <>
      <Toaster />
      <Button
        onClick={() =>
          toast.info("Info", {
            description: "The quick brown fox jumps over the lazy dog.",
          })
        }
      >
        Show
      </Button>
    </>
  )
}

```

## Usage

***

Import the `toast` utility and `Toaster` component from `@medusajs/ui`:

```tsx
import { Toaster, toast } from "@medusajs/ui"
```

Then, add the `Toaster` component somewhere in your tree:

```tsx
<Toaster />
```

Finally, use the `toast` utility to display a toast:

```tsx
return (
  <Button 
    onClick={() => 
      toast.info("Toast title", {
        description: "Toast body",
      })
    }
  >
    Trigger
  </Button>
)
```

## API Reference

***

### Toast Utility Functions

The `toast` utility has the following functions to display different variants of toasts:

- `info`
- `error`
- `success`
- `warning`
- `loading`

Each of these functions accept two parameters:

1. A string indicating the title of the toast.
2. An object of props to pass to the underlying `Toast` component.

### Toast Props

### Toast Props

This component is based on the \[Sonner]\(https://sonner.emilkowal.ski/toast) toast library.

- id: (union) Optional ID of the toast.
- description: (ReactReactNode) The toast's text.
- action: (signature) The toast's action buttons.

### Toaster Props

### Toaster Props

This component is based on the \[Toaster component of the Sonner library]\(https://sonner.emilkowal.ski/toaster).

- position: (Position) The position of the created toasts. Default: "bottom-right"
- gap: (number) The gap between the toast components. Default: 12
- offset: (union) The space from the edges of the screen. Default: 24
- duration: (number) The time in milliseconds that a toast is shown before it's
  automatically dismissed.

  &#x20;Default: 4000

## Examples

***

### Dismissable Toast

```tsx
import { Button, Toaster, toast } from "@medusajs/ui"

export default function DismissableToaster() {
  return (
    <>
      <Toaster />
      <Button
        onClick={() =>
          toast.info("Info", {
            description: "The quick brown fox jumps over the lazy dog.",
            dismissable: true,
          })
        }
      >
        Show
      </Button>
    </>
  )
}

```

### Warning

```tsx
import { Button, Toaster, toast } from "@medusajs/ui"

export default function ToasterWarning() {
  return (
    <>
      <Toaster />
      <Button
        onClick={() =>
          toast.warning("Warning", {
            description: "The quick brown fox jumps over the lazy dog.",
            duration: 5000,
          })
        }
      >
        Show
      </Button>
    </>
  )
}

```

### Error

```tsx
import { Button, Toaster, toast } from "@medusajs/ui"

export default function ToasterError() {
  return (
    <>
      <Toaster />
      <Button
        onClick={() =>
          toast.error("Error", {
            description: "The quick brown fox jumps over the lazy dog.",
            duration: 5000,
          })
        }
      >
        Show
      </Button>
    </>
  )
}

```

### Success

```tsx
import { Button, Toaster, toast } from "@medusajs/ui"

export default function ToasterSuccess() {
  return (
    <>
      <Toaster />
      <Button
        onClick={() =>
          toast.success("Success", {
            description: "The quick brown fox jumps over the lazy dog.",
            duration: 5000,
          })
        }
      >
        Show
      </Button>
    </>
  )
}

```

### Loading

```tsx
import { Button, Toaster, toast } from "@medusajs/ui"

export default function ToasterLoading() {
  return (
    <>
      <Toaster />
      <Button
        onClick={() =>
          toast.loading("Loading", {
            description: "The quick brown fox jumps over the lazy dog.",
            duration: 5000,
          })
        }
      >
        Show
      </Button>
    </>
  )
}

```

### With Action

```tsx
import { Button, Toaster, toast } from "@medusajs/ui"

export default function ToasterWithAction() {
  return (
    <>
      <Toaster />
      <Button
        onClick={() =>
          toast.success("Created Product", {
            description: "The product has been created.",
            action: {
              altText: "Undo product creation",
              onClick: () => {},
              label: "Undo",
            },
            duration: 10000,
          })
        }
      >
        Show
      </Button>
    </>
  )
}

```


# Tooltip

A popup that displays information related to an element when the element receives keyboard focus or the mouse hovers over it.

```tsx
import { InformationCircleSolid } from "@medusajs/icons"
import { Tooltip } from "@medusajs/ui"

export default function TooltipDemo() {
  return (
    <Tooltip content="The quick brown fox jumps over the lazy dog.">
      <InformationCircleSolid />
    </Tooltip>
  )
}

```

## Usage

```tsx
import { Tooltip, TooltipProvider } from "@medusajs/ui"
```

```tsx
<Tooltip content="Tooltip content">Trigger</Tooltip>
```

## API Reference

***

### Tooltip Props

This component is based on the \[Radix UI Tooltip]\(https://www.radix-ui.com/primitives/docs/components/tooltip) primitive.

- maxWidth: (number) The maximum width of the tooltip. Default: 220
- aria-label: (string) A more descriptive label for accessibility purpose
- delayDuration: (number) The duration from when the pointer enters the trigger until the tooltip gets opened. This will
  override the prop with the same name passed to Provider.
- forceMount: (literal) Used to force mounting when more control is needed. Useful when
  controlling animation with React animation libraries.
- onEscapeKeyDown: (signature) Event handler called when the escape key is down.
  Can be prevented.
- onPointerDownOutside: (signature) Event handler called when the a \`pointerdown\` event happens outside of the \`Tooltip\`.
  Can be prevented.

## Usage Outside Medusa Admin

***

If you're using the `Tooltip` component in a project other than the Medusa Admin, make sure to include the `TooltipProvider` somewhere up in your component tree:

```tsx
<TooltipProvider>
  <Tooltip content="Tooltip content">Trigger</Tooltip>
</TooltipProvider>
```

### TooltipProvider Reference

***

### TooltipProvider Props

- delayDuration: (number) The duration from when the pointer enters the trigger until the tooltip gets opened. Default: 100
- skipDelayDuration: (number) How much time a user has to enter another trigger without incurring a delay again. Default: 300
- disableHoverableContent: (boolean) When \`true\`, trying to hover the content will result in the tooltip closing as the pointer leaves the trigger.


# Medusa Admin Extension

How to install and use Medusa UI for building Admin extensions.

## Installation

***

The `@medusajs/ui` package is a already installed as a dependency of the `@medusajs/admin` package. Due to this you can simply import the package and use it in your local Admin extensions.

If you are building a Admin extension as part of a Medusa plugin, you can install the package as a dependency of your plugin.

```bash
npm install @medusajs/ui
```

## Configuration

***

The configuration of the UI package is handled by the `@medusajs/admin` package. Therefore, you do not need to any additional configuration to use the UI package in your Admin extensions.


# Standalone Project

How to install and use Medusa UI in a standalone project.

## Installation

***

Medusa UI is a React UI library and while it's intended for usage within Medusa projects, it can also be used in any React project.

### Install Medusa UI

Install the React UI library with the following command:

```bash
npm install @medusajs/ui
```

### Configuring Tailwind CSS

The components are styled using Tailwind CSS, and in order to use them, you will need to install Tailwind CSS in your project as well.
For more information on how to install Tailwind CSS, please refer to the [Tailwind CSS documentation](https://tailwindcss.com/docs/installation).

All of the classes used for Medusa UI are shipped as a Tailwind CSS customization.
You can install it with the following command:

```bash
npm install @medusajs/ui-preset
```

After you have installed Tailwind CSS and the Medusa UI preset, you need to add the following to your `tailwind.config.js`file:

```tsx
module.exports = {
  presets: [require("@medusajs/ui-preset")],
  // ...
}
```

In order for the styles to be applied correctly to the components, you will also need to ensure that
`@medusajs/ui` is included in the content field of your `tailwind.config.js` file:

```tsx
module.exports = {
  content: [
    // ...
    "./node_modules/@medusajs/ui/dist/**/*.{js,jsx,ts,tsx}",
  ],
  // ...
}
```

If you are working within a monorepo, you may need to add the path to the `@medusajs/ui` package in your `tailwind.config.js` like so:

```tsx
const path = require("path")

const uiPath = path.resolve(
  require.resolve("@medusajs/ui"),
  "../..",
  "\*_/_.{js,jsx,ts,tsx}"
)

module.exports = {
  content: [
    // ...
    uiPath,
  ],
  // ...
}

```

## Start building

***

You are now ready to start building your application with Medusa UI. You can import the components like so:

```tsx
import { Button, Drawer } from "@medusajs/ui"
```

## Updating UI Packages

***

Medusa's design-system packages, including `@medusajs/ui`, `@medusajs/ui-preset`, and `@medusajs/ui-icons`, are versioned independently. However, they're still part of the latest Medusa release. So, you can browse the [release notes](https://github.com/medusajs/medusa/releases) to see if there are any breaking changes to these packages.

To update these packages, update their version in your `package.json` file and re-install dependencies. For example:

```bash
npm install @medusajs/ui
```


# clx

Utility function for working with classNames.

## Usage

***

The `clx` function is a utility function for working with classNames. It is built using [clsx](https://www.npmjs.com/package/clsx) and [tw-merge](https://www.npmjs.com/package/tw-merge) and is intended to be used with [Tailwind CSS](https://tailwindcss.com/).

```tsx
import { clx } from "@medusajs/ui"

type BoxProps = {
  className?: string
  children: React.ReactNode
  mt: "sm" | "md" | "lg"
}

const Box = ({ className, children, mt }: BoxProps) => {
  return (
    <div
      className={clx(
        "flex items-center justify-center",
        {
          "mt-4": mt === "sm",
          "mt-8": mt === "md",
          "mt-12": mt === "lg",
        },
        className
      )}
    >
      {children}
    </div>
  )
}

```

In the above example the utility is used to apply a base style, a margin top that is dependent on the `mt` prop and a custom className.
The Box component accepts a `className` prop that is merged with the other classNames, and the underlying usage of `tw-merge` ensures that all Tailwind CSS classes are merged without style conflicts.
