Skip to main content

Fulfillment Providers

Fulfillment providers handle shipping, delivery, and order fulfillment operations. They calculate shipping prices, validate fulfillment data, create shipments, and manage returns.

Available Fulfillment Providers

Manual Fulfillment Provider

The manual fulfillment provider (@medusajs/medusa/fulfillment-manual) is a simple provider for self-managed fulfillment where you handle shipping logistics yourself. Features:
  • No external shipping integrations
  • Manual fulfillment process
  • Supports both regular fulfillment and returns
  • Does not calculate shipping prices

Installation

The manual fulfillment provider is included in the core Medusa package:
npm install @medusajs/medusa

Configuration

Configure the fulfillment provider in your medusa-config.ts:
import { defineConfig } from "@medusajs/framework/utils"

export default defineConfig({
  modules: [
    {
      resolve: "@medusajs/medusa/fulfillment",
      options: {
        providers: [
          {
            resolve: "@medusajs/medusa/fulfillment-manual",
            id: "manual",
          },
        ],
      },
    },
  ],
})

Fulfillment Provider Interface

All fulfillment providers extend AbstractFulfillmentProviderService and implement the following methods:

getFulfillmentOptions(): Promise<FulfillmentOption[]>

Returns available fulfillment options.
const options = await fulfillmentProvider.getFulfillmentOptions()
// Result:
// [
//   { id: "manual-fulfillment" },
//   { id: "manual-fulfillment-return", is_return: true }
// ]

validateFulfillmentData(optionData, data, context): Promise<any>

Validates fulfillment data before creating a fulfillment. Parameters:
  • optionData - Configuration data for the fulfillment option
  • data - Fulfillment data to validate
  • context - Additional context including cart/order data
const validatedData = await fulfillmentProvider.validateFulfillmentData(
  { option_id: "manual-fulfillment" },
  {
    tracking_number: "TRACK123",
  },
  {
    cart_id: "cart_123",
  }
)

validateOption(data): Promise<boolean>

Validates a fulfillment option configuration.
const isValid = await fulfillmentProvider.validateOption({
  id: "manual-fulfillment",
})

canCalculate(): Promise<boolean>

Indicates whether the provider can calculate shipping prices.
const canCalculate = await fulfillmentProvider.canCalculate()
// false for manual provider

calculatePrice(optionData, data, context): Promise<CalculatedShippingOptionPrice>

Calculates shipping price (not supported by manual provider). Parameters:
  • optionData - Shipping option configuration
  • data - Calculation data (items, destination, etc.)
  • context - Additional context
The manual fulfillment provider throws an error when calculatePrice is called, as it does not support price calculation.

createFulfillment(): Promise<CreateFulfillmentResult>

Creates a fulfillment (shipment).
const result = await fulfillmentProvider.createFulfillment()
// Result: { data: {}, labels: [] }
Returns:
  • data - Provider-specific fulfillment data
  • labels - Shipping labels (if applicable)

cancelFulfillment(): Promise<any>

Cancels an existing fulfillment.
await fulfillmentProvider.cancelFulfillment()

createReturnFulfillment(): Promise<CreateFulfillmentResult>

Creates a return fulfillment.
const result = await fulfillmentProvider.createReturnFulfillment()
// Result: { data: {}, labels: [] }

Using the Fulfillment Module

Access fulfillment providers through the Fulfillment Module:
import { Modules } from "@medusajs/framework/utils"

const fulfillmentModule = container.resolve(Modules.FULFILLMENT)

// Get fulfillment options
const options = await fulfillmentModule.listFulfillmentOptions(
  "manual"
)

// Create a fulfillment
const fulfillment = await fulfillmentModule.createFulfillment({
  provider_id: "manual",
  order_id: "order_123",
  items: [
    {
      line_item_id: "item_123",
      quantity: 1,
    },
  ],
  labels: [],
  data: {
    tracking_number: "TRACK123",
  },
})

Fulfillment Workflows

Use fulfillment workflows from @medusajs/core-flows:
import { createFulfillmentWorkflow } from "@medusajs/core-flows"

const { result } = await createFulfillmentWorkflow(container).run({
  input: {
    order_id: "order_123",
    provider_id: "manual",
    items: [
      {
        line_item_id: "item_123",
        quantity: 1,
      },
    ],
  },
})

Creating Custom Fulfillment Providers

Create a custom fulfillment provider by extending AbstractFulfillmentProviderService:
packages/modules/providers/fulfillment-custom/src/services/custom-fulfillment.ts
import { AbstractFulfillmentProviderService } from "@medusajs/framework/utils"
import {
  CalculatedShippingOptionPrice,
  CalculateShippingOptionPriceContext,
  CreateFulfillmentResult,
  FulfillmentOption,
  ValidateFulfillmentDataContext,
} from "@medusajs/types"

export class CustomFulfillmentService extends AbstractFulfillmentProviderService {
  static identifier = "custom-fulfillment"

  constructor(container, options) {
    super()
    this.options_ = options
    // Initialize shipping provider SDK
    this.client_ = new ShippingProviderSDK(options.apiKey)
  }

  async getFulfillmentOptions(): Promise<FulfillmentOption[]> {
    // Return available shipping options
    return [
      {
        id: "standard-shipping",
      },
      {
        id: "express-shipping",
      },
      {
        id: "return-shipping",
        is_return: true,
      },
    ]
  }

  async validateFulfillmentData(
    optionData: Record<string, unknown>,
    data: Record<string, unknown>,
    context: ValidateFulfillmentDataContext
  ): Promise<any> {
    // Validate shipping address, items, etc.
    if (!context.shipping_address) {
      throw new Error("Shipping address is required")
    }

    return data
  }

  async validateOption(data: Record<string, any>): Promise<boolean> {
    // Validate the option configuration
    const validOptions = ["standard-shipping", "express-shipping"]
    return validOptions.includes(data.id)
  }

  async canCalculate(): Promise<boolean> {
    return true
  }

  async calculatePrice(
    optionData: Record<string, unknown>,
    data: Record<string, unknown>,
    context: CalculateShippingOptionPriceContext
  ): Promise<CalculatedShippingOptionPrice> {
    // Calculate shipping price from provider
    const rate = await this.client_.getRates({
      from: context.from_address,
      to: context.shipping_address,
      items: context.items,
      service: optionData.id,
    })

    return {
      calculated_amount: rate.amount,
    }
  }

  async createFulfillment(
    data: Record<string, unknown>,
    items: any[],
    order: any,
    fulfillment: any
  ): Promise<CreateFulfillmentResult> {
    // Create shipment with provider
    const shipment = await this.client_.createShipment({
      order_id: order.id,
      items: items,
      from_address: order.warehouse_address,
      to_address: order.shipping_address,
      service: data.option_id,
    })

    return {
      data: {
        shipment_id: shipment.id,
        tracking_number: shipment.tracking_number,
      },
      labels: [
        {
          url: shipment.label_url,
          format: "pdf",
        },
      ],
    }
  }

  async cancelFulfillment(fulfillment: any): Promise<any> {
    // Cancel shipment with provider
    await this.client_.cancelShipment(fulfillment.data.shipment_id)
    return {}
  }

  async createReturnFulfillment(
    fulfillment: any
  ): Promise<CreateFulfillmentResult> {
    // Create return shipment
    const returnShipment = await this.client_.createReturnShipment({
      original_shipment_id: fulfillment.data.shipment_id,
    })

    return {
      data: {
        shipment_id: returnShipment.id,
        tracking_number: returnShipment.tracking_number,
      },
      labels: [
        {
          url: returnShipment.label_url,
          format: "pdf",
        },
      ],
    }
  }
}
Register your custom provider:
packages/modules/providers/fulfillment-custom/src/index.ts
import { ModuleProvider, Modules } from "@medusajs/framework/utils"
import { CustomFulfillmentService } from "./services/custom-fulfillment"

export default ModuleProvider(Modules.FULFILLMENT, {
  services: [CustomFulfillmentService],
})
Configure your custom provider:
medusa-config.ts
import { defineConfig } from "@medusajs/framework/utils"

export default defineConfig({
  modules: [
    {
      resolve: "@medusajs/medusa/fulfillment",
      options: {
        providers: [
          {
            resolve: "./src/modules/providers/fulfillment-custom",
            id: "custom-fulfillment",
            options: {
              apiKey: process.env.SHIPPING_PROVIDER_API_KEY,
            },
          },
        ],
      },
    },
  ],
})

Reference

  • Source: packages/modules/providers/fulfillment-manual/src/services/manual-fulfillment.ts
  • Base class: packages/core/utils/src/fulfillment/abstract-fulfillment-provider.ts
  • Types: packages/core/types/src/fulfillment/provider.ts

Next Steps

Payment Providers

Configure payment processing

Notification Providers

Send shipping confirmation emails

Build docs developers (and LLMs) love