Spreadconnect product, shipping, order, and shipment integration for Medusa.
is a Medusa plugin developed by SHOP4FANS. It connects Medusa to Spreadconnect. It imports products, calculates shipping, submits paid orders, and records fulfillment and tracking updates.
Unofficial integration: This plugin is not affiliated with, endorsed by, or an official product of Spreadconnect. Spreadconnect is a third-party service.
This release requires Medusa 2.22.1 and Node.js 22.22.0 or later. Use the same Medusa version in the host application.
Create a stock location and a shipping profile in Medusa Admin. Use their IDs in the configuration below.
Register the plugin and fulfillment provider in . Merge these entries with your existing configuration and providers.
Required environment variables:
The token is configured separately for the plugin module and fulfillment provider because Medusa modules are isolated. The fulfillment provider does not inject the product/webhook module.
The default fulfillment provider ID is : Medusa composes it from the provider's static identifier () and configured ID (). If the provider is registered with a different , set the resulting full ID in the plugin option .
Configure the following through Medusa Admin or the corresponding Admin APIs:
The provider exposes exactly these fulfillment options:
Choose for the Standard Shipping Option and for Express. The provider rejects mismatched IDs and values.
Imports use , which defaults to . Configure the same currency and appropriate tax settings in Medusa. Product prices use Spreadconnect values without conversion.
Product creation and updates set the product to draft. An update therefore removes a published product from the storefront until you publish it again. The plugin does not select sales channels.
The plugin receives product changes through webhooks. It does not perform an initial bulk catalog import.
Use the normal Medusa Store API or Medusa JS SDK. The storefront does not need Spreadconnect credentials or custom checkout endpoints.
Before you request shipping prices, save the customer email and shipping address on the cart. Include a phone number in the shipping address. Spreadconnect requires these fields. The cart region determines the currency.
Backend workflow hooks read the saved cart email and currency. Do not pass them in shipping-method . Client values cannot override the saved cart values.
A storefront that omits phone numbers must collect and save a phone number before shipping calculation. Standard Medusa fields support this requirement. Product visibility still requires a published product, a sales channel, a linked stock location, and a matching publishable API key.
Each shipping calculation creates a temporary Spreadconnect order in state , then cancels it. Standard and Express use separate calculations. Shipping prices use major currency units and include tax. Cancellation failures cause the calculation to fail after three attempts.
The job in runs daily at . The scheduler determines the time zone. Set your worker time zone explicitly if 03:00 must match a local time.
The job reads paginated stock from Spreadconnect and updates imported variants at . It changes inventory quantities only. It does not unpublish products or change sales channels.
Zero stock sets the inventory quantity to zero. Missing or invalid stock leaves the previous quantity unchanged. The job skips unsupported inventory links, including bundles, and logs the skipped count. If a later page fails, earlier batches remain saved. The next daily run checks them again.
Live stock checks also run when a customer:
The plugin reads stored variant data and combines quantities for each SKU. A cart cannot bypass the check through client metadata. Non-Spreadconnect products use normal Medusa inventory checks.
Insufficient stock returns . An API failure or invalid stock response returns . Both use Medusa's standard HTTP 400 error response. The storefront can display these errors through its normal cart and checkout error handling.
These checks do not reserve stock at Spreadconnect. Supplier stock can change after a check. Completed-cart retries remain idempotent and do not trigger a new stock check.
The plugin handles these events:
| Event | Result |
|---|---|
| Create or update a draft product and its stock. | |
| Update or create a draft product and its stock. | |
| Delete the imported product. | |
| Create the native Medusa fulfillment for the submitted order. | |
| Write a warning for operator review. | |
| Cancel the matching fulfillment and Medusa order. | |
| Create the native Medusa shipment and tracking labels. |
and trigger payment reconciliation. Production starts only after the payment collection is complete and fully captured. Authorization or partial capture does not start production.
Submission sends a Spreadconnect order with a stable reference. The plugin saves the remote order ID on the shipping method. creates the Medusa fulfillment later. Manual fulfillment requires an existing submission and does not create another production order.
Webhook delivery uses . The endpoint verifies the HMAC and saves a database receipt before it returns . It does not call Spreadconnect during acknowledgment.
A scheduled job processes up to 50 due receipts each minute. Failed processing uses increasing retry delays, up to one hour. Identical deliveries share one receipt. Order locks prevent duplicate fulfillment and shipment changes. Product imports share a lock to protect global options.
Keep a Medusa worker with scheduled jobs enabled. For multiple backend or worker processes, configure Medusa's shared locking provider, such as Redis. A process-local lock cannot coordinate separate processes.
Stock lookup failures keep the receipt pending. They do not disable inventory management or enable backorders. A retry completes partial product imports. An older pending article receipt is ignored when a newer receipt for that article has completed. This uses receipt time, not an upstream event sequence.
An uncertain production response records . The plugin does not submit again automatically, because the first request may have succeeded. Check the remote order using its external reference. A later processed webhook can attach the remote order and continue fulfillment. Resolve a request that never reached Spreadconnect before you clear its submission marker.
Shipment processing waits until the relevant remote items have shipped. It retries when a fulfillment or tracking data is not yet available. It does not mark quantities shipped at production submission.
A remote cancellation uses Medusa's order cancellation workflow. This can refund captured payments and cancels the whole Medusa order. Use separate orders for products whose cancellation must remain independent.
Returns are not supported. Cancel an existing fulfillment through Medusa's normal cancellation flow. If Spreadconnect refuses cancellation, the provider returns an error.
Receipts retain webhook payloads, which can contain customer information. Set a retention policy for completed receipts in your deployment. Pending receipts must remain available for retries.
Tests use anonymized Spreadconnect responses in . SDK replay tests pass captured response bodies through the installed SDK without remote API calls.
installs the built tarball and its dependencies in a temporary project. It checks Medusa plugin discovery and loads the packaged backend entry points. This command requires access to the npm registry.
Run database tests against an isolated local database whose name contains :
The optional requires , , , and . The fixture must be a valid SDK JSON payload. This test creates and cancels quotes and creates a confirmed staging order. Use a staging account only.
This plugin uses the MIT License. Its dependencies retain their own licenses.
npm install s4f-spreadconnect@0.9.01import { defineConfig, loadEnv } from "@medusajs/framework/utils";2
3loadEnv(process.env.NODE_ENV || "development", process.cwd());4
5export default defineConfig({6 plugins: [7 {8 resolve: "s4f-spreadconnect",9 options: {10 baseUrl: process.env.SPREADCONNECT_BASE_URL,11 token: process.env.SPREADCONNECT_TOKEN,12 medusaUrl: `${process.env.MEDUSA_BACKEND_URL}/spreadconnect/webhooks`,13 webhookSecret: process.env.SPREADCONNECT_WEBHOOK_SECRET,14 defaultCurrencyCode: "eur",15 defaultStockLocationId: process.env.SPREADCONNECT_DEFAULT_STOCK_LOCATION_ID,16 shippingProfileId: process.env.SPREADCONNECT_SHIPPING_PROFILE_ID,17 fulfillmentProviderId: "spreadconnect_spreadconnect",18 },19 },20 ],21 modules: [22 {23 resolve: "@medusajs/medusa/fulfillment",24 options: {25 providers: [26 {27 resolve: "s4f-spreadconnect/providers/spreadconnect-fulfillment",28 id: "spreadconnect",29 options: {30 baseUrl: process.env.SPREADCONNECT_BASE_URL,31 token: process.env.SPREADCONNECT_TOKEN,32 },33 },34 ],35 },36 },37 ],38});1SPREADCONNECT_BASE_URL=https://...2SPREADCONNECT_TOKEN=...3SPREADCONNECT_WEBHOOK_SECRET=...4SPREADCONNECT_DEFAULT_STOCK_LOCATION_ID=sloc_...5SPREADCONNECT_SHIPPING_PROFILE_ID=sp_...6MEDUSA_BACKEND_URL=http://localhost:90001[2 {3 id: "spreadconnect-standard",4 name: "Spreadconnect Standard",5 integration: "spreadconnect",6 preferred_type: "STANDARD",7 is_return: false,8 },9 {10 id: "spreadconnect-express",11 name: "Spreadconnect Express",12 integration: "spreadconnect",13 preferred_type: "EXPRESS",14 is_return: false,15 },16];1await sdk.store.cart.update(cart.id, {2 email: customerEmail,3 shipping_address: {4 first_name: firstName,5 last_name: lastName,6 address_1: street,7 city,8 postal_code: postalCode,9 country_code: countryCode,10 phone,11 },12});13
14const { shipping_options } = await sdk.store.fulfillment.listCartOptions({15 cart_id: cart.id,16});17
18const { shipping_option } = await sdk.store.fulfillment.calculate(optionId, {19 cart_id: cart.id,20});21
22await sdk.store.cart.addShippingMethod(cart.id, { option_id: optionId });23// Complete your normal payment-session and checkout steps.24await sdk.store.cart.complete(cart.id);1npm test2npm run test:coverage3npm run release:check4npm run test:installSPREADCONNECT_TEST_DATABASE_URL=postgres://localhost/spreadconnect_test npm run test:integration