npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

medusa-shiprocket-fulfillment-sbl

v0.0.30

Published

Shiprocket Fulfillment Provider Plugin for MedusaJS 2

Readme

medusa-shiprocket-fulfillment-sbl

Medusa v2 Shiprocket fulfillment provider plugin with shipping rate calculation, shipment creation, serviceability API, webhook automation, and optional tracking-event persistence.

Plugin Overview

medusa-shiprocket-fulfillment-sbl integrates Shiprocket logistics into Medusa:

  • Registers a fulfillment provider (identifier = shiprocket) for rates and fulfillment lifecycle.
  • Calculates rates from pickup/delivery pincodes and order item weights.
  • Creates Shiprocket orders, assigns AWB, and attaches label/manifest/invoice URLs.
  • Exposes storefront serviceability endpoint.
  • Exposes carrier tracking webhook endpoint to auto-handle shipped/delivered updates.
  • Adds admin utilities for shipment documents and tracking events.
  • Includes optional shipment_tracking module for append-only tracking scan history with deduplication.
  • Includes a scheduled token-refresh job.

Built for Medusa v2 (package uses Medusa framework/module APIs, workflows/core-flows integration, and admin-sdk widget extension).

Installation & Setup

1) Install package

npm install medusa-shiprocket-fulfillment-sbl
# or
yarn add medusa-shiprocket-fulfillment-sbl

2) Register fulfillment provider and plugin

Recommended (single credential source): set email / password only on the fulfillment provider; the plugin reads them automatically.

import { defineConfig } from "@medusajs/framework/utils"

export default defineConfig({
  modules: [
    {
      resolve: "@medusajs/medusa/fulfillment",
      options: {
        providers: [
          {
            resolve: "medusa-shiprocket-fulfillment-sbl",
            id: "shiprocket",
            options: {
              email: "[email protected]",
              password: "your_password",
              pickup_location: "Primary",
              pickup_pincode: "110001", // optional explicit pickup override
              cod: "false",
              apiTimeoutMs: 30000,
            },
          },
        ],
      },
    },
  ],
  plugins: [
    {
      resolve: "medusa-shiprocket-fulfillment-sbl",
      options: {
        // shiprocket.email/password optional when set on fulfillment provider above
        webhookSecret: "change-me",
        apiTimeoutMs: 30000,
      },
    },
  ],
})

Before (duplicated credentials — avoid):

plugins: [
  {
    resolve: "medusa-shiprocket-fulfillment-sbl",
    options: {
      shiprocket: { email: "...", password: "..." }, // duplicated
      webhookSecret: "...",
    },
  },
],
modules: [/* same email/password repeated in provider options */]

3) Optional module registration (shipment tracking events)

{
  resolve: "medusa-shiprocket-fulfillment-sbl/modules/shipment-tracking"
}

4) Run migrations

npx medusa db:migrate

Required migrations from this plugin:

  • shipment_tracking_event (only if optional tracking module is registered)

Configuration (config.ts / plugin options)

This plugin resolves options via src/utils/resolve-options.ts and type definitions in src/types/plugin-options.ts.

Plugin options (plugins[].options)

| Option | Type | Required | Default | Description | |---|---|---|---|---| | shiprocket.email | string | No* | — | Shiprocket account email. Required on plugin or fulfillment provider. | | shiprocket.password | string | No | — | Shiprocket account password. Required on plugin or fulfillment provider. | | shiprocket.extra_estimated_delivery_days | number \| string | No | resolved to 1 if all sources unset | Extra days added to summarized multi-courier serviceability estimated_delivery_days. | | shiprocket.estimated_delivery_days | number \| string | No | same as above | Alias for extra-days buffer. | | extra_estimated_delivery_days | number \| string | No | same as above | Top-level alias for same extra-days buffer. | | webhookSecret | string | No | — | Secret used by /carrier-tracking/webhook. Required if webhook endpoint should be functional. | | publishableKey | string | No | — | Optional config field available to webhook handler. | | apiTimeoutMs | number | No | 30000 | HTTP timeout for Shiprocket API requests (milliseconds). |

Fulfillment provider options (modules -> providers[].options)

| Option | Type | Required | Default | Description | |---|---|---|---|---| | email | string | Yes* | — | Shiprocket email. Can be omitted on plugin when set here. | | password | string | Yes | — | Shiprocket password. *Can be omitted on plugin when set here. | | pickup_location | string | No | provider-side default usage | Pickup location nickname used in order creation / serviceability fallback. | | pickup_pincode | string | No | — | Explicit pickup postal code override when inventory/stock location cannot be resolved. | | cod | 0 \| 1 \| "true" \| "false" | No | prepaid-like behavior | Influences provider rate calculation COD flag. | | apiTimeoutMs | number | No | 30000 | HTTP timeout for Shiprocket API requests (milliseconds). | | extra_estimated_delivery_days | number \| string | No | fallback source | Considered as fallback source for extra-days merge if plugin option is absent/non-positive. | | estimated_delivery_days | number \| string | No | fallback source | Alias fallback source for extra-days merge. |

Extra delivery-days merge behavior

Resolved in resolve-options.ts:

  • Plugin-level extra days take precedence when parsed value > 0.
  • Otherwise provider-level fallback is used.
  • Final resolveShiprocketOptions default is 1 when not specified (undefined/empty).
  • Explicit 0 is preserved when provided directly to resolveShiprocketOptions.

Complete example config

{
  modules: [
    {
      resolve: "@medusajs/medusa/fulfillment",
      options: {
        providers: [
          {
            resolve: "medusa-shiprocket-fulfillment-sbl",
            id: "shiprocket",
            options: {
              email: "[email protected]",
              password: "secret",
              pickup_location: "Primary",
              cod: 0,
              estimated_delivery_days: 1,
            },
          },
        ],
      },
    },
    {
      resolve: "medusa-shiprocket-fulfillment-sbl/modules/shipment-tracking",
    },
  ],
  plugins: [
    {
      resolve: "medusa-shiprocket-fulfillment-sbl",
      options: {
        shiprocket: {
          email: "[email protected]",
          password: "secret",
          extra_estimated_delivery_days: 2,
        },
        webhookSecret: "shiprocket-webhook-secret",
        publishableKey: "pk_...",
      },
    },
  ],
}

Environment Variables

No runtime process.env.* reads were found in this plugin’s src code.

| Variable | Used directly in src runtime code? | Notes | |---|---|---| | None | No | Values are expected to be passed through Medusa config options. |

REST APIs / Routes

Store routes

GET /store/shiprocket/serviceability

  • Auth requirement: Store route context (typically publishable key based on host app middleware)
  • Query parameters:

| Param | Type | Required | Description | |---|---|---|---| | pincode | string | Yes | Delivery pincode. | | variant_id | string | Yes | Variant id with required shipping dimensions. | | pickup_pincode | string | No | Override pickup pincode; skips pickup resolution from inventory/stock location. | | cod | string/int | No | 0 or 1, default 0. | | declared_value | integer-like string | No | Non-negative integer. | | order_id | integer-like string | No | Positive integer (Shiprocket order id). | | express_delivery | boolean-like string | No | true, 1, or yes to include a nested express summary alongside standard fields. Default: single summary object. |

  • Core behavior:

    • Loads variant dimensions (weight, length, width, height) via query graph.
    • Weight converted from grams to kg for Shiprocket request.
    • Resolves pickup from variant inventory location levels and stock location address postal codes when pickup_pincode absent.
    • Calls Shiprocket serviceability API through ShiprocketClient.
  • Response behavior:

    • Default (express_delivery absent or false): single summary object (HTTP 200), never the raw Shiprocket payload.
    • express_delivery=true: nested object — top-level fields are the standard tier (surface couriers and couriers with missing is_surface, max rate/days); express is the air tier (explicit is_surface: false, fastest/min-days) or null when no air couriers exist.
    • Breaking change: storefronts that parsed the full Shiprocket JSON for zero/one courier results must migrate to this schema.

| Field | Description | |---|---| | rate | Courier rate as a string. Single courier: that row's rate. Multiple couriers: maximum rate. No couriers: "0". | | estimated_delivery_days | Delivery days as a string (before ETD formatting). Single courier: that row's days. Multiple couriers: maximum days. No couriers: "0". Includes configured extra buffer (extra_estimated_delivery_days, default +1 day). | | etd | Estimated delivery date, formatted like Shiprocket (e.g. "June 28, 2026"). Computed from today in Asia/Kolkata plus estimated_delivery_days. For zero couriers, this is today's date in Asia/Kolkata. | | cod_available | Whether COD is available for this route. Derived from Shiprocket response/courier COD fields in the same API call. Fallback: true when the request used cod=1 and at least one courier is returned. |

When express_delivery=true, nested express uses the same field names:

| Field | Description | |---|---| | express | null when no air couriers; otherwise { rate, estimated_delivery_days, etd, cod_available } for the fastest air option. | | express.rate | Rate from the fastest air courier (minimum delivery days; tie → lowest rate). | | express.estimated_delivery_days | Delivery days for the fastest air courier, plus extra buffer. | | express.etd | ETD computed from express.estimated_delivery_days. | | express.cod_available | COD availability inferred from air couriers only. |

Example (default — serviceable):

{
  "rate": "89",
  "estimated_delivery_days": "6",
  "etd": "June 28, 2026",
  "cod_available": true
}

Example (express_delivery=true):

{
  "rate": "89",
  "estimated_delivery_days": "6",
  "etd": "June 28, 2026",
  "cod_available": true,
  "express": {
    "rate": "120",
    "estimated_delivery_days": "2",
    "etd": "June 24, 2026",
    "cod_available": true
  }
}

Example (express_delivery=true, no air couriers):

{
  "rate": "89",
  "estimated_delivery_days": "6",
  "etd": "June 28, 2026",
  "cod_available": true,
  "express": null
}

Example (not serviceable — zero couriers):

{
  "rate": "0",
  "estimated_delivery_days": "0",
  "etd": "June 22, 2026",
  "cod_available": false
}

GET /store/plugin

  • Health route, returns HTTP 200.

Carrier webhook route

POST /carrier-tracking/webhook

  • Auth requirement: shared secret validation against configured webhookSecret

  • Accepted secret sources:

    • Authorization: Bearer <secret>
    • x-shiprocket-secret: <secret>
    • x-api-key: <secret>
    • query ?secret= or ?token=
  • Body validation:

    • Requires at least one of: order_id, shipment_id, awb, channel_order_id
    • Reads status from status / shipment_status / current_status
    • Accepts passthrough payload fields (.passthrough())
  • Workflow behavior:

    • Resolves Medusa order/fulfillment using lookup utility (awb prioritized).
    • Normalizes status (shipped / delivered coarse states).
    • For shipped: ensures shipment exists (createShipmentWorkflow) if needed.
    • For delivered: ensures shipment exists then marks delivered (markOrderFulfillmentAsDeliveredWorkflow).
    • Persists webhook metadata into fulfillment metadata.
    • If optional tracking module registered, ingests scan history and emits shipment.tracking.updated on latest milestone change.
  • Query params:

| Param | Type | Default | Description | |---|---|---|---| | shiprocket_only | string | true | false allows broader fulfillment matching beyond Shiprocket provider id. |

Admin routes

POST /admin/orders/:id/fulfillments/:fulfillment_id/shiprocket-documents

  • Auth requirement: Admin JWT (admin namespace route)
  • Body: none
  • Behavior:
    • Resolves order and fulfillment via remote query.
    • Ensures fulfillment belongs to Shiprocket provider and has shipment_id.
    • Generates label via Shiprocket API.
  • Success response:
    • { label_url: string, manifest_url: string, invoice_url: string } — empty string for any document that failed to generate
  • Persists non-empty URLs to fulfillment.data via updateFulfillment
  • Possible errors:
    • 400, 404, 502 depending on missing data/provider mismatch/generation failure.

GET /admin/orders/:id/shipment-tracking-events

  • Auth requirement: Admin JWT
  • Behavior:
    • Validates order exists.
    • If shipment_tracking module registered: returns sorted raw event rows.
    • If not registered: returns 501 with registration guidance.

GET /admin/plugin

  • Health route, returns HTTP 200.

Important API examples

# Store serviceability
curl "http://localhost:9000/store/shiprocket/serviceability?pincode=110001&variant_id=variant_123" \
  -H "x-publishable-api-key: <PUBLISHABLE_KEY>"
# Shiprocket webhook
curl -X POST "http://localhost:9000/carrier-tracking/webhook" \
  -H "Content-Type: application/json" \
  -H "x-api-key: <WEBHOOK_SECRET>" \
  -d '{
    "order_id": "12345",
    "shipment_id": "67890",
    "status": "delivered",
    "awb": "AWB123456789"
  }'
# Admin generate label
curl -X POST "http://localhost:9000/admin/orders/order_123/fulfillments/ful_123/shiprocket-documents" \
  -H "Authorization: Bearer <ADMIN_JWT>"
# Admin shipment tracking events
curl "http://localhost:9000/admin/orders/order_123/shipment-tracking-events" \
  -H "Authorization: Bearer <ADMIN_JWT>"

Services

ShipRocketFulfillmentProviderService

Provider class extending AbstractFulfillmentProviderService, identifier = shiprocket.

Key methods:

  • getFulfillmentOptions()
    • Returns Standard Shipping, Express Shipping, and Return Shipping.
  • canCalculate(...)
    • Always true.
  • calculatePrice(...)
    • Computes package weight from context items, reads delivery_tier from shipping option provider data (standard | express), and calls Shiprocket serviceability with unified selectCourierForTier() (standard = min rate among surface couriers; express = fastest air courier).
  • createFulfillment(...)
    • Resolves payment method, selects courier by delivery_tier, creates Shiprocket order, assigns AWB with explicit courier_id, generates documents.
  • cancelFulfillment(data)
    • Cancels Shiprocket order by order_id.
  • createReturnFulfillment(fulfillment)
    • Creates return order flow.
  • getFulfillmentDocuments(data)
    • Generates invoice.
  • getShipmentDocuments(data)
    • Generates label.
  • validateFulfillmentData(...)
    • Stores delivery_tier (standard | express) and namespaced external_id (pending:fulfillment:<id>).
  • validateOption(data)
    • Checks external_id presence.
  • checkServiceability(...)
    • Wrapper around client serviceability call.

ShiprocketClient

Shiprocket API wrapper with token management and request methods:

  • calculate(data) — uses selectCourierForTier() (not cheapest-of-all)
  • checkServiceability(data)
  • create(fulfillment, items, order, paymentMethod)
  • cancel(orderId)
  • getTrackingInfo(trackingNumber)
  • getPickupLocations()
  • getPickupPincodeByName(locationName)
  • getPickupLocationNames()
  • createReturn(fulfillment)
  • createDocuments(fulfillment)
  • generateLabel({ shipment_id, order_id? })
  • generateInvoice(fulfillment)
  • dispose()

Includes axios interceptor for automatic token refresh on 401.

ShipmentTrackingModuleService (optional module)

Provides normalized tracking ingestion and timeline retrieval:

  • ingestShiprocketPayload(...)
  • getLatestNormalizedForFulfillment(fulfillmentId)
    • Computes latest milestone.
  • deriveCoarseWorkflowFromNormalized(...)
  • getTimelineForOrder(orderId, { audience })
  • listRawEventsForOrder(orderId)
  • maxNormalizedFromPayload(payload)

Utility services/helpers

  • resolveShiprocketPaymentMethod(...)
    • Determines Prepaid vs COD using provider data, order metadata, and payment graph fallback.
    • Logs shiprocket.payment_method.fallback_prepaid when defaulting to Prepaid (check provider logs).
  • resolveShiprocketPaymentMethodWithSource(...)
    • Same resolver; returns { method, source } for observability.
  • findOrderAndFulfillmentByShiprocketIds(...)
    • Robust fulfillment resolution from webhook identifiers.
  • shipment-tracking-normalize.ts
    • Status normalization, dedupe hashing, customer visibility filtering.

Workflows & Steps (Medusa v2)

No custom createWorkflow / createStep workflows are defined in this plugin source.

⚠️ Note: The webhook uses Medusa core flows (createShipmentWorkflow, markOrderFulfillmentAsDeliveredWorkflow) rather than custom plugin workflows.

Subscribers / Event Hooks

order-placed-shiprocket-cod-metadata

  • Event: OrderWorkflowEvents.PLACED
  • Behavior:
    • Detects COD from order payments via query graph.
    • Persists order metadata hints:
      • is_cod: true
      • shiprocket_payment_method: "COD"
    • Fails silently to avoid checkout/order-placement disruption.

Jobs

refresh-shiprocket-token

  • Schedule: 0 0 */8 * *
  • Calls ShiprocketClient.refreshToken() on the shared client to force a fresh login.
  • File: src/jobs/refresh-shiprocket-token.ts

CI / branch protection

Coverage thresholds apply to src/utils/** and src/providers/shiprocket/** (excluding the large HTTP client implementation file, which is covered by dedicated client unit tests). CI runs npm test -- --coverage. Enable Require status checks for Test medusa-shiprocket-fulfillment-sbl on your default branch.

Admin UI / Widgets

ShiprocketTrackingWidget

  • Placement: order.details.side.after
  • File: src/admin/widgets/printables.tsx
  • Displays:
    • active Shiprocket fulfillments for order
    • shipment id / AWB / status
    • label/manifest/invoice actions
  • Interactions:
    • “Download Label”:
      • uses existing label URL, or
      • calls admin document-generation route if missing
    • opens manifest/invoice links when available

Models & Entities

Optional model: shipment_tracking_event

Defined in src/modules/shipment-tracking/models/shipment-tracking-event.ts.

| Field | Type | Nullable | |---|---|---| | id | id | No | | fulfillment_id | text | No | | order_id | text | No | | awb | text | Yes | | courier_name | text | Yes | | activity | text | No | | normalized_status | text | No | | location | text | Yes | | event_time | text | No | | raw_payload | text | Yes | | provider | text | No (default shiprocket) | | dedupe_hash | text | No |

Indexes/constraints:

  • unique partial index on dedupe_hash (deleted_at IS NULL)
  • index on order_id
  • index on fulfillment_id

Relationships:

  • Stores order_id and fulfillment_id references (no explicit FK constraints to Medusa core tables in migration).

Use Cases & Examples

  1. Shiprocket calculated shipping at checkout

    • Use provider in fulfillment module; calculatePrice returns live shipping amount from Shiprocket serviceability.
  2. Product-page pincode serviceability checks

    • Call GET /store/shiprocket/serviceability with variant_id and destination pincode for ETD/rate preview.
  3. Automated shipment state sync from carrier events

    • Configure Shiprocket webhook to POST /carrier-tracking/webhook to auto-mark shipped/delivered.
  4. Admin label recovery for failed/missing document generation

    • Use POST /admin/orders/:id/fulfillments/:fulfillment_id/shiprocket-documents to generate label URL on demand.
  5. Carrier scan-history timeline storage and retrieval

    • Register optional shipment_tracking module and query GET /admin/orders/:id/shipment-tracking-events.

Express delivery setup

Express uses the same Shiprocket provider with two shipping options (standard + express). Checkout price and fulfillment courier selection both use selectCourierForTier() — no silent fallback to standard when express is unavailable.

Where to configure (Medusa Admin)

Go to Settings → Locations & Shipping (or your stock location) → Create Shipping Option for the Shiprocket fulfillment provider.

Create two shipping options using the form fields below. Medusa Admin does not expose a “provider data” JSON editor on this screen — that is expected.

Standard shipping option

| Admin field | Value | |---|---| | Price type | Calculated (required — Fixed bypasses Shiprocket rate logic) | | Name | e.g. Standard Shipping | | Fulfillment provider | shiprocket (your plugin provider id) | | Fulfillment option | Standard Shipping | | Enable in store | On |

Express shipping option

| Admin field | Value | |---|---| | Price type | Calculated | | Name | e.g. Express Shipping | | Fulfillment provider | shiprocket | | Fulfillment option | Express Shipping | | Enable in store | On |

Also ensure pickup_location in plugin options matches a Shiprocket pickup with a valid pincode (used for courier selection at fulfill time).

Do you need { "delivery_tier": "express" } JSON?

Usually no. Selecting Fulfillment option → Express Shipping is enough.

When you pick a fulfillment option in Admin, Medusa stores that option on the shipping option’s data field (from getFulfillmentOptions()), typically:

{ "id": "Express Shipping", "name": "Express Shipping", "is_return": false }

The plugin resolves tier from that automatically (id / name === "Express Shipping") or from explicit delivery_tier: "express".

| What | Do you set it manually? | |---|---| | Shipping option — pick Fulfillment option Express Shipping | Yes, in Admin (required) | | Shipping option data{ "delivery_tier": "express" } | Optional (fallback if tier resolution fails) | | Fulfillment datadelivery_tier, requested_courier_id, etc. | No — plugin writes these at checkout / fulfill |

You never paste JSON into fulfillment data in Admin.

Optional: explicit delivery_tier on the shipping option

Add this only if express orders still behave as standard (wrong rate/courier, or fulfillment.data.delivery_tier is not "express" after fulfill). Medusa Admin often has no JSON field — use the Admin API after creating the option:

curl -X POST "http://localhost:9000/admin/shipping-options/<EXPRESS_OPTION_ID>" \
  -H "Authorization: Bearer <ADMIN_JWT>" \
  -H "Content-Type: application/json" \
  -d '{ "data": { "delivery_tier": "express" } }'

Use the update route/body for your Medusa version if it differs.

Staging verification (express)

  1. Place a staging order and select Express Shipping at checkout (Calculated price should reflect air-tier rate, not standard).
  2. Create fulfillment from Medusa Admin.
  3. Confirm fulfillment.data includes:
    • delivery_tier: "express"
    • requested_delivery_tier: "express"
    • requested_courier_id, actual_courier_company_id, actual_courier_name
  4. In the Shiprocket panel, confirm an air courier was assigned (not surface-only).
  5. If delivery_tier is missing or "standard", add { "delivery_tier": "express" } to the shipping option via API (see above) and retry.

Behavior

| Tier | Courier filter | Selection rule | |---|---|---| | standard | Surface + missing is_surface | Minimum rate | | express | Air only (is_surface: false) | Minimum delivery days; tie → minimum rate |

  • Hard fail: If no couriers match the tier at checkout (calculatePrice) or fulfillment (createFulfillment), the plugin throws NOT_FOUND. It never falls back to the other tier.
  • COD + express: Only air couriers that support COD are considered when payment method is COD.
  • Storefront serviceability API (GET /store/shiprocket/serviceability?express_delivery=true) is for UI preview (ETD, COD flags). Cart/checkout price comes from calculatePrice on the Express shipping option.

Fulfillment data fields (after AWB)

| Field | Description | |---|---| | delivery_tier | standard or express | | requested_delivery_tier | Tier used for courier selection | | requested_courier_id | Courier ID sent to /courier/assign/awb | | actual_courier_company_id | Courier ID from AWB response | | actual_courier_name | Courier name from AWB response |

If requested_courier_idactual_courier_company_id, logs include shiprocket.courier.mismatch with tier, postcodes, weight, and COD flag.

Go-live staging validation

Before production traffic, run one manual reconciliation on staging Shiprocket:

  1. Place a Medusa order: 1× ₹999 GST-exclusive item (18% tax), ₹50 shipping, full fulfillment.
  2. Create Shiprocket fulfillment from Medusa Admin.
  3. Compare Medusa-computed values (unit test golden reference) with the Shiprocket order panel:

| Field | Expected value | |---|---| | sub_total | 1179 (Math.round(999 × 1.18)) | | shipping_charges | 50 | | order_items[0].selling_price | 1179 | | order_items[0].tax | 18 (rate percent, not rupee amount) | | payment_method | Prepaid or COD per order payment |

  1. COD orders: confirm Shiprocket shows payment_method: COD and logs do not contain shiprocket.payment_method.fallback_prepaid.
  2. Prepaid orders: payment_method: Prepaid is expected; fallback_prepaid warn means no explicit COD signal was found (verify payment metadata if order should be COD).

Plugin unit tests encode the ₹999 + 18% GST golden payload in shiprocket-order-amounts.test.ts (buildShiprocketAdhocAmountPayload).

Fulfillment retry (wallet / AWB failures)

When Shiprocket creates a shipment but AWB assignment fails (for example low wallet balance), the plugin does not cancel the Shiprocket order anymore. The order stays active so you can recover without hitting "order is in cancelled state" on retry.

Retry from Medusa Admin (after topping up Shiprocket wallet):

  1. Open the order and create or retry fulfillment as usual.
  2. The provider first checks Medusa fulfillment data and Shiprocket List Orders for an active shipment tied to the same Medusa reference (display_id or internal order id).
  3. If an active shipment exists without AWB, it calls assign/awb only (no duplicate create/adhoc).
  4. If an active shipment already has AWB, it returns that tracking data (idempotent).
  5. If only cancelled Shiprocket rows exist (for example from older plugin versions), it creates a new adhoc order with a suffixed order_id ({baseRef}_{fulfillmentSuffix}), capped at 50 characters.

One active Shiprocket shipment per Medusa order is the target state. Suffixed ids are used only when a new Shiprocket order is required, not on every fulfillment call.

Ops notes:

  • Cancelled bare ids (e.g. display id 250 only) cannot be revived on Shiprocket; after deploy, retries use lookup + AWB assign or a new suffixed id.
  • Manual AWB in the Shiprocket panel is only needed for debugging; normal flow is wallet top-up + retry fulfillment in Medusa.
  • Fulfillment data stores order_id, shipment_id, channel_order_id, base_ref, and tracking fields for admin widgets and document routes.
  • Success logs include resolve_mode: reuse_has_awb, reuse_no_awb, or create. If duplicate Shiprocket orders appear, check whether retries logged resolve_mode: create instead of reuse — verify channel_order_id matches the Medusa order reference and that List Orders search finds the active shipment.
  • A warn log shiprocket.createFulfillment.resolve_mode_create indicates a fresh /orders/create/adhoc call (no reusable shipment found in fulfillment data or list search).

Troubleshooting

shiprocket.email is required / shiprocket.password is required

  • Cause: plugin options missing or malformed.
  • Fix: verify plugins[].options.shiprocket in medusa-config.

Serviceability fails with missing pickup postcode resolution

  • Cause: no pickup_pincode query param and variant inventory location levels lack stock location postal code.
  • Fix: pass pickup_pincode or correct stock-location address + inventory linkage.

Serviceability returns dimension/weight validation error

  • Cause: variant missing positive weight, length, width, height.
  • Fix: set required variant shipping dimensions in Medusa Admin.

Webhook returns 501 not configured

  • Cause: webhookSecret not set in plugin options.
  • Fix: set plugins[].options.webhookSecret.

Webhook returns 401

  • Cause: secret did not match accepted headers/query secret.
  • Fix: ensure Shiprocket token/header matches configured webhookSecret.

Admin tracking events route returns 501

  • Cause: optional shipment_tracking module not registered.
  • Fix: register medusa-shiprocket-fulfillment-sbl/modules/shipment-tracking and migrate DB.

No fulfillment matched in webhook payload

  • Cause: payload ids/AWB not mapping to Medusa fulfillment data.
  • Fix: verify Shiprocket webhook fields (order_id, shipment_id, awb, channel_order_id) and stored fulfillment data.

Fulfillment creation fails with missing dimensions

  • Cause: provider create requires variant dimensions and weight for all fulfillment items.
  • Fix: update variant shipping fields.

order is in cancelled state on fulfillment retry

  • Cause: a prior attempt created a Shiprocket order, AWB failed, and an older plugin version cancelled the order; retry reused the same bare order_id.
  • Fix: upgrade to a version with fulfillment retry support, top up wallet, retry fulfillment from admin. For orders stuck before upgrade, use manual AWB on any still-active Shiprocket row or wait for suffixed create after upgrade.