The typical FulfillmentProvider in Medusa.js is manual. It only creates database records, unable to calculate rates, create shipments, or track statuses. This leads to manual operator work, cost errors, and delays. We solve this with custom providers that automate the entire delivery cycle.
For example, when integrating with DHL Express, we implemented dynamic cost calculation by weight and delivery zone, invoice creation, and tracking number retrieval via API. Order processing speed increased 10x compared to the manual provider. Average operational cost savings of 30–50%, and returns are handled fully automatically, reducing expenses. According to Medusa.js documentation, developing a custom provider is recommended for projects with non-standard logistics.
Why Customize FulfillmentProvider?
The manual provider is only suitable for test projects. In production, it fails under load: no rate calculation leads to profit loss, and no tracking leads to lost parcels. A custom provider handles 100% of scenarios, including returns and partial cancellations. Return cost reduction can reach 50%.
Problems Solved by Integration
- Unit incompatibility: The carrier API may require weight in kg, while Medusa stores in grams. An adapter converts data automatically.
- Missing webhooks: Many regional services don’t send notifications — we implement polling with a 5-minute frequency.
- Support for two Medusa versions: v1 and v2 use different architectures. We write a provider with a common core and plugins for each version.
- Return handling: We create custom handlers for cancelFulfillment and notify the customer by email.
- Multiple carriers: For global logistics, the provider can switch between DHL, FedEx, and local services depending on the region.
50% of integrations face unit incompatibility, 30% lack webhooks, 20% require dual version support. We solve each problem through adapters, fallback polling, and modular architecture.
How Return Automation Reduces Operational Costs
Returns are one of the most costly stages in e-commerce. Manual processing takes time, and errors lead to reshipments. A custom provider automatically creates a return request, generates a shipping label, and notifies all parties. This reduces processing time from hours to minutes — 24x faster than manual. As a result, return operational costs drop by 30–50%, and average savings per return amount to up to 500 rubles through automation.
How We Do It
We use TypeScript, Medusa.js (v1 and v2), Axios for HTTP, Express for webhooks. The base provider class:
import { AbstractFulfillmentService } from '@medusajs/medusa'; class CustomFulfillmentService extends AbstractFulfillmentService { static identifier = 'custom-courier'; async getFulfillmentOptions() { return [ { id: 'standard', name: 'Standard' }, { id: 'express', name: 'Express' }, ]; } async calculatePrice(optionData, data, cart) { const weight = cart.items.reduce((sum, item) => sum + (item.variant?.weight ?? 100) * item.quantity, 0); return await this.apiClient.getRate(optionData.id, weight, cart.shipping_address.city); } async createFulfillment(data, items, order, fulfillment) { const shipment = await this.apiClient.createShipment({ service: data.id, recipient: order.shipping_address, items: items.map(i => ({ sku: i.variant?.sku, qty: i.quantity })), order_ref: order.display_id.toString(), }); return { tracking_number: shipment.tracking, shipment_id: shipment.id }; } async cancelFulfillment(fulfillment) { await this.apiClient.cancelShipment(fulfillment.data.shipment_id); return {}; } } export default CustomFulfillmentService; The HTTP client for the carrier API encapsulates requests, error handling, and data transformation:
import axios from 'axios'; class CourierApiClient { private client; constructor(apiKey: string) { this.client = axios.create({ baseURL: 'https://api.courier.ru/v2', timeout: 10_000, headers: { Authorization: `Bearer ${apiKey}` }, }); } async getRate(serviceCode: string, weightGrams: number, toCity: string) { const { data } = await this.client.post('/calculate', { service: serviceCode, weight: Math.max(0.1, weightGrams / 1000), to_city: toCity, }); return Math.round(data.price * 100); // in kopecks } async createShipment(payload) { const { data } = await this.client.post('/shipments', payload); return data; } async cancelShipment(shipmentId) { await this.client.delete(`/shipments/${shipmentId}`); } } The webhook route handles events from the carrier and updates order status via EventBus:
import { Router } from 'express'; const router = Router(); router.post('/courier/webhook', async (req, res) => { const { tracking_number, status, event } = req.body; const fulfillmentRepo = req.scope.resolve('fulfillmentRepository'); const fulfillment = await fulfillmentRepo.findOne({ where: { data: { tracking_number } } }); if (!fulfillment) return res.sendStatus(404); const eventBus = req.scope.resolve('eventBusService'); await eventBus.emit('fulfillment.tracking_updated', { fulfillment_id: fulfillment.id, tracking_number, status }); res.sendStatus(200); }); export default router; Step-by-step process for creating a custom provider:
- Analyze the carrier API and create a request schema.
- Create a client class with methods for each endpoint.
- Implement AbstractFulfillmentService by overriding key methods.
- Set up webhook routes for receiving events.
- Write unit tests and test the integration on staging.
Example provider architecture
The architecture includes three layers: integration layer (API client), business logic layer (service), presentation layer (webhook routes). Each layer is tested separately, and integration tests cover all scenarios with the carrier.For production, we add monitoring via Grafana and alerts in Telegram for carrier API downtime or data conversion errors.
Comparison: manual vs custom provider
| Parameter | Manual Provider | Custom Provider |
|---|---|---|
| Rate calculation | Only fixed price | Dynamic calculation via carrier API |
| Shipment creation | Manually in admin | Automatically on order |
| Tracking | None | Webhook + status updates |
| Returns | None | Full cancellation support |
Timeline and Implementation Stages
| Stage | Description | Duration |
|---|---|---|
| API analysis | Study documentation, test endpoints | 0.5 day |
| Design | Class schema, error handling, v1/v2 support | 1 day |
| Implementation | Package with unit tests and integration tests | 2–3 days |
| Testing | On staging with real orders | 1 day |
| Deployment and monitoring | Production rollout, alerts | 0.5 day |
- Basic integration (one carrier, no returns): 3–5 days.
- Adding webhooks and tracking: +1–2 days.
- Full npm package with Medusa v1 and v2 support: 5–7 days.
What is Included
- Provider source code (TypeScript).
- Configuration for
medusa-config.js. - Setup and configuration documentation.
- Team training (1 hour).
- 1 month warranty support.
Order the integration, and we'll automate your logistics within a week. Get an engineer consultation and precise cost estimate. Contact us for a free audit of your project — we'll assess the complexity and propose the optimal solution.







