Skip to main content
Skip to main content

How to Create a Fulfillment Provider

In this document, you’ll learn how to create a fulfillment provider in the Medusa backend and the methods you must implement in it. If you’re unfamiliar with the Shipping architecture in Medusa, make sure to check out the overview first.

Overview

A fulfillment provider is the shipping provider used to fulfill orders and deliver them to customers. An example of a fulfillment provider is FedEx.

By default, a Medusa Backend has a manual fulfillment provider which has minimal implementation. It allows you to accept orders and fulfill them manually. However, you can integrate any fulfillment provider into Medusa, and your fulfillment provider can interact with third-party shipping providers.

A fulfillment provider is a service that extends the AbstractFulfillmentService and implements its methods. So, adding a fulfillment provider is as simple as creating a service file in src/services. The file's name is the fulfillment provider's class name as a slug and without the word Service. For example, if you're creating a MyFulfillmentService class, the file name is src/services/my-fulfillment.ts.

src/services/my-fulfillment.ts
import { AbstractFulfillmentService } from "@medusajs/medusa"

class MyFulfillmentService extends AbstractFulfillmentService {
// methods here...
}

export default MyFulfillmentService

Identifier Property

The FulfillmentProvider entity has 2 properties: identifier and is_installed. The identifier property in the fulfillment provider service is used when the fulfillment provider is added to the database.

The value of this property is also used to reference the fulfillment provider throughout Medusa. For example, it is used to add a fulfillment provider to a region.

class MyFulfillmentService extends AbstractFulfillmentService {
static identifier = "my-fulfillment"

// ...
}

constructor

You can use the constructor of your fulfillment provider to access the different services in Medusa through dependency injection.

You can also use the constructor to initialize your integration with the third-party provider. For example, if you use a client to connect to the third-party provider’s APIs, you can initialize it in the constructor and use it in other methods in the service. Additionally, if you’re creating your fulfillment provider as an external plugin to be installed on any Medusa backend and you want to access the options added for the plugin, you can access it in the constructor.

Example

class MyFulfillmentService extends AbstractFulfillmentService {
// ...
constructor(container, options) {
super(container)
// you can access options here

// you can also initialize a client that
// communicates with a third-party service.
this.client = new Client(options)
}
// ...
}

Parameters

containerRecord<string, unknown>Required
An instance of MedusaContainer that allows you to access other resources, such as services, in your Medusa backend.
configRecord<string, unknown>
If this fulfillment provider is created in a plugin, the plugin's options are passed in this parameter.

Methods

getFulfillmentOptions

This method is used when retrieving the list of fulfillment options available in a region, particularly by the List Fulfillment Options API Route. For example, if you’re integrating UPS as a fulfillment provider, you might support two fulfillment options: UPS Express Shipping and UPS Access Point. Each of these options can have different data associated with them.

Example

class MyFulfillmentService extends AbstractFulfillmentService {
// ...
async getFulfillmentOptions(): Promise<any[]> {
return [
{
id: "my-fulfillment",
},
{
id: "my-fulfillment-dynamic",
},
]
}
}

Returns

PromisePromise<any[]>Required
The list of fulfillment options. These options don't have any required format. Later on, these options can be used when creating a shipping option, such as when using the Create Shipping Option API Route. The chosen fulfillment option, which is one of the items in the array returned by this method, will be set in the data object of the shipping option.

validateFulfillmentData

This method is called when a shipping method is created. This typically happens when the customer chooses a shipping option during checkout, when a shipping method is created for an order return, or in other similar cases. The shipping option and its data are validated before the shipping method is created.

You can use the provided parameters to validate the chosen shipping option. For example, you can check if the data object passed as a second parameter includes all data needed to fulfill the shipment later on.

If any of the data is invalid, you can throw an error. This error will stop Medusa from creating a shipping method and the error message will be returned as a result of the API Route.

Example

class MyFulfillmentService extends AbstractFulfillmentService {
// ...
async validateFulfillmentData(
optionData: Record<string, unknown>,
data: Record<string, unknown>,
cart: Cart
): Promise<Record<string, unknown>> {
if (data.id !== "my-fulfillment") {
throw new Error("invalid data")
}

return {
...data,
}
}
}

Parameters

optionDataShippingOptionDataRequired
The data object of the shipping option selected when creating the shipping method.
The data object passed in the body of the request.
cartCartRequired
The customer's cart details. It may be empty if the shipping method isn't associated with a cart, such as when it's associated with a claim.

Returns

PromisePromise<Record<string, unknown>>Required
The data that will be stored in the data property of the shipping method to be created. Make sure the value you return contains everything you need to fulfill the shipment later on. The returned value may also be used to calculate the price of the shipping method if it doesn't have a set price. It will be passed along to the calculatePrice method.

validateOption

Once the admin creates the shipping option, the data of the shipping option will be validated first using this method. This method is called when the Create Shipping Option API Route is used.

Example

For example, you can use this method to ensure that the id in the data object is correct:

class MyFulfillmentService extends AbstractFulfillmentService {
// ...
async validateOption(
data: Record<string, unknown>
): Promise<boolean> {
return data.id === "my-fulfillment"
}
}

Parameters

dataShippingOptionDataRequired
the data object that is sent in the body of the request, basically, the data object of the shipping option. You can use this data to validate the shipping option before it is saved.

Returns

PromisePromise<boolean>Required
Whether the fulfillment option is valid. If the returned value is false, an error is thrown and the shipping option will not be saved.

canCalculate

This method is used to determine whether a shipping option is calculated dynamically or flat rate. It is called if the price_type of the shipping option being created is set to calculated.

Example

class MyFulfillmentService extends AbstractFulfillmentService {
// ...
async canCalculate(
data: Record<string, unknown>
): Promise<boolean> {
return data.id === "my-fulfillment-dynamic"
}
}

Parameters

dataShippingOptionDataRequired
The data object of the shipping option being created. You can use this data to determine whether the shipping option should be calculated or not. This is useful if the fulfillment provider you are integrating has both flat rate and dynamically priced fulfillment options.

Returns

PromisePromise<boolean>Required
If this method returns true, that means that the price can be calculated dynamically and the shipping option can have the price_type set to calculated. The amount property of the shipping option will then be set to null. The amount will be created later when the shipping method is created on checkout using the calculatePrice method. If the method returns false, an error is thrown as it means the selected shipping option is invalid and it can only have the flat_rate price type.

calculatePrice

This method is used in different places, including:

  1. When the shipping options for a cart are retrieved during checkout. If a shipping option has their price_type set to calculated, this method is used to set the amount of the returned shipping option.
  2. When a shipping method is created. If the shipping option associated with the method has their price_type set to calculated, this method is used to set the price attribute of the shipping method in the database.
  3. When the cart's totals are calculated.

Example

An example of calculating the price based on some custom logic:

class MyFulfillmentService extends AbstractFulfillmentService {
// ...
async calculatePrice(
optionData: Record<string, unknown>,
data: Record<string, unknown>,
cart: Cart
): Promise<number> {
return cart.items.length * 1000
}
}

If your fulfillment provider does not provide any dynamically calculated rates you can return any static value or throw an error. For example:

class MyFulfillmentService extends AbstractFulfillmentService {
// ...
async calculatePrice(
optionData: Record<string, unknown>,
data: Record<string, unknown>,
cart: Cart
): Promise<number> {
throw new Error("Method not implemented.")
}
}

Parameters

optionDataShippingOptionDataRequired
The data object of the selected shipping option.
A data object that is different based on the context it's used in:
  1. If the price is being calculated for the list of shipping options available for a cart, it's the data object of the shipping option.
  2. If the price is being calculated when the shipping method is being created, it's the data returned by the validateFulfillmentData method used during the shipping method creation.
  3. If the price is being calculated while calculating the cart's totals, it will be the data object of the cart's shipping method.
cartCartRequired
Either the Cart or the Order object.

Returns

PromisePromise<number>Required
Used to set the price of the shipping method or option, based on the context the method is used in.

createFulfillment

This method is used when a fulfillment is created for an order, a claim, or a swap.

Example

Here is a basic implementation of createFulfillment for a fulfillment provider that does not interact with any third-party provider to create the fulfillment:

class MyFulfillmentService extends AbstractFulfillmentService {
// ...
async createFulfillment(
data: Record<string, unknown>,
items: LineItem[],
order: Order,
fulfillment: Fulfillment
) {
// No data is being sent anywhere
// No data to be stored in the fulfillment's data object
return {}
}
}

Parameters

dataShippingMethodDataRequired
The data object of the shipping method associated with the resource, such as the order. You can use it to access the data specific to the shipping option. This is based on your implementation of previous methods.
itemsLineItem[]Required
The line items in the order to be fulfilled. The admin can choose all or some of the items to fulfill.
orderOrderRequired
The details of the created resource, which is either an order, a claim, or a swap:
  • If the resource the fulfillment is being created for is a claim, the is_claim property in the object will be true.
  • If the resource the fulfillment is being created for is a swap, the is_swap property in the object will be true.
  • Otherwise, the resource is an order.
fulfillmentFulfillmentRequired
The fulfillment being created.

Returns

PromisePromise<FulfillmentProviderData>Required
The data that will be stored in the data attribute of the created fulfillment.

cancelFulfillment

This method is called when a fulfillment is cancelled by the admin. This fulfillment can be for an order, a claim, or a swap.

Example

This is the basic implementation of the method for a fulfillment provider that doesn't interact with a third-party provider to cancel the fulfillment:

class MyFulfillmentService extends FulfillmentService {
// ...
async cancelFulfillment(
fulfillment: Record<string, unknown>
): Promise<any> {
return {}
}
}

Parameters

fulfillmentFulfillmentProviderDataRequired
The data attribute of the fulfillment being canceled

Returns

PromisePromise<any>Required
The method isn't expected to return any specific data.

createReturn

Fulfillment providers can also be used to return products. A shipping option can be used for returns if the is_return property is true or if an admin creates a Return Shipping Option from the settings. This method is used when the admin creates a return request for an order, creates a swap for an order, or when the customer creates a return of their order. The fulfillment is created automatically for the order return.

Example

This is the basic implementation of the method for a fulfillment provider that does not contact with a third-party provider to fulfill the return:

class MyFulfillmentService extends AbstractFulfillmentService {
// ...
async createReturn(
returnOrder: CreateReturnType
): Promise<Record<string, unknown>> {
return {}
}
}

Parameters

returnOrderCreateReturnTypeRequired
the return that the fulfillment is being created for.

Returns

PromisePromise<Record<string, unknown>>Required
Used to set the value of the shipping_data attribute of the return being created.

getFulfillmentDocuments

This method is used to retrieve any documents associated with a fulfillment. This method isn't used by default in the backend, but you can use it for custom use cases such as allowing admins to download these documents.

Example

class MyFulfillmentService extends FulfillmentService {
// ...
async getFulfillmentDocuments(
data: Record<string, unknown>
): Promise<any> {
// assuming you contact a client to
// retrieve the document
return this.client.getFulfillmentDocuments()
}
}

Parameters

The data attribute of the fulfillment that you're retrieving the documents for.

Returns

PromisePromise<any>Required
There are no restrictions on the returned response. If your fulfillment provider doesn't provide this functionality, you can leave the method empty or through an error.

getReturnDocuments

This method is used to retrieve any documents associated with a return. This method isn't used by default in the backend, but you can use it for custom use cases such as allowing admins to download these documents.

Example

class MyFulfillmentService extends FulfillmentService {
// ...
async getReturnDocuments(
data: Record<string, unknown>
): Promise<any> {
// assuming you contact a client to
// retrieve the document
return this.client.getReturnDocuments()
}
}

Parameters

dataRecord<string, unknown>Required
The data attribute of the return that you're retrieving the documents for.

Returns

PromisePromise<any>Required
There are no restrictions on the returned response. If your fulfillment provider doesn't provide this functionality, you can leave the method empty or through an error.

getShipmentDocuments

This method is used to retrieve any documents associated with a shipment. This method isn't used by default in the backend, but you can use it for custom use cases such as allowing admins to download these documents.

Example

class MyFulfillmentService extends FulfillmentService {
// ...
async getShipmentDocuments(
data: Record<string, unknown>
): Promise<any> {
// assuming you contact a client to
// retrieve the document
return this.client.getShipmentDocuments()
}
}

Parameters

dataRecord<string, unknown>Required
The data attribute of the shipment that you're retrieving the documents for.

Returns

PromisePromise<any>Required
There are no restrictions on the returned response. If your fulfillment provider doesn't provide this functionality, you can leave the method empty or through an error.

retrieveDocuments

This method is used to retrieve any documents associated with an order and its fulfillments. This method isn't used by default in the backend, but you can use it for custom use cases such as allowing admins to download these documents.

Example

class MyFulfillmentService extends FulfillmentService {
// ...
async retrieveDocuments(
fulfillmentData: Record<string, unknown>,
documentType: "invoice" | "label"
): Promise<any> {
// assuming you contact a client to
// retrieve the document
return this.client.getDocuments()
}
}

Parameters

fulfillmentDataRecord<string, unknown>Required
The data attribute of the order's fulfillment.
documentType"label" | "invoice"Required
The type of document to retrieve.

Returns

PromisePromise<any>Required
There are no restrictions on the returned response. If your fulfillment provider doesn't provide this functionality, you can leave the method empty or through an error.

Test Implementation

Note

If you created your fulfillment provider in a plugin, refer to this guide on how to test plugins.

After finishing your fulfillment provider implementation:

1. Run the build command in the root of your Medusa backend:

npm run build

2. Start the backend with the develop command:

npx medusa develop

3. Enable your fulfillment provider in one or more regions. You can do that either using the Admin APIs or the Medusa Admin.

4. To test out your fulfillment provider implementation, create a cart and complete an order. You can do that either using the Next.js starter or using Medusa's APIs and clients.

Was this section helpful?