You have Medusa v2 running with Razorpay payments. Prepaid orders flow through. Then your first customer from Tier 2 India asks: "Do you have cash on delivery?" In India, COD is not a legacy payment method — it is the default. Depending on the city tier and product category, 25–65% of e-commerce orders are COD. Medusa v2 has no built-in COD payment provider and no Shiprocket fulfillment module. The community plugin Hemann55/medusa-fulfillment-shiprocket exists for v1 but was never ported. This guide builds both from scratch: a COD payment provider that creates orders without collecting payment, a Shiprocket fulfillment module that pushes orders to Shiprocket's API, and the webhook pipeline that keeps delivery status and RTO handling in sync.
Need Razorpay for prepaid orders?
This guide covers the COD side. For UPI, cards, and netbanking, see the Razorpay payment provider guide. For self-hosting Medusa on affordable infrastructure, see the Coolify + Hetzner deployment guide.
Cash on delivery creates a fundamentally different order lifecycle. There is no payment at checkout — the customer promises to pay when the package arrives. Your store ships product on trust, eats both forward and return shipping costs when the customer refuses delivery, and waits days for the courier to remit collected cash. Every Indian e-commerce operator knows this, but Medusa's payment architecture assumes payment happens before fulfillment. I am Manoj, commerce and ERP implementation lead at Mith Tech in Bengaluru, and wiring Indian fulfillment logistics into headless commerce stacks is what we do every week.
Why COD cannot be an afterthought
If you have built an Indian e-commerce store and skipped COD, your conversion rate is artificially suppressed. COD is not about customers who lack online payment access — UPI penetration is high. COD is about trust. A customer buying from a brand they have never heard of, on a website they found through Instagram ads, will choose COD to protect themselves from scams and mismatched product quality.
The industry data is consistent: COD accounts for roughly 60–65% of orders in fashion/apparel, 40–50% in electronics, and 25–35% in beauty and personal care. Tier 1 city rates are lower (25–35%) because repeat customers build trust. Tier 2 and Tier 3 rates are higher (45–65%) because brand awareness is lower.
The operational cost is also consistent: RTO rates for COD orders are 25–30% nationally, climbing to 40% in Tier 3 cities. Each RTO costs you both forward and return shipping — typically ₹60–160 per order depending on weight, zone, and courier. The RTO Cost Calculator below lets you model this for your order volume.
The COD order lifecycle — different from prepaid
A prepaid order follows a straightforward path: customer pays → store fulfils → courier delivers. COD inverts this. The store fulfils on trust, the courier collects cash, the courier remits cash to Shiprocket, and Shiprocket remits to your bank account. Your payment is "captured" days after the customer receives the product.
Click through each stage to see what happens on the Medusa side and the Shiprocket side — from checkout through delivery, cash collection, and the RTO path when a customer refuses.
Checkout (COD selected)
Customer selects COD at checkout. No payment is collected. Medusa creates the order with payment_status = 'awaiting'.
Medusa side
completeCartWorkflow runs with manual/system payment provider. Payment session status = 'pending'. Order created with amount_due = total.
Shiprocket side
Not involved yet.
The simulator above shows the key divergence: after "delivered", the payment in Medusa is still awaiting. It moves to captured only after Shiprocket's settlement cycle (T+2 to T+8 depending on the courier). Your reconciliation process needs to account for this gap — you have shipped product but not received payment, and the settlement is not instantaneous.
The cost of RTO
Every COD order that gets refused at the doorstep costs you forward shipping, return shipping, product handling, and opportunity cost (inventory was locked while the order was in transit). The RTO Cost Calculator models this for your business:
Enter your monthly COD order volume, average order value, and expected RTO rate to see the financial impact of COD returns on your margins.
RTO orders
125
25% of 500
RTO shipping cost
₹12,500
Forward + return
Total shipping cost
₹31,250
10.4% of revenue
Delivered revenue
₹300,000
375 orders
If the calculator shows shipping costs eating more than 8–10% of your revenue, consider these mitigations:
- OTP-verified COD. Send an OTP to the customer's phone before confirming a COD order. This reduces fake orders by 30–40%.
- COD with advance. Collect ₹50–100 via UPI at checkout, refundable against the COD amount. Filters out low-intent orders.
- Pincode-level COD blocking. Track RTO rates by pincode. Disable COD for pincodes with >40% RTO rate.
- COD to prepaid conversion. After order confirmation, send a WhatsApp message with a prepaid payment link offering a ₹50 discount. 15–25% of customers convert.
Building the COD + Shiprocket stack
The implementation has four files: the COD payment provider module, the Shiprocket fulfillment module, the tracking webhook route, and the module registration in medusa-config.ts.
Four-tab code recipe covering the COD payment provider, Shiprocket fulfillment service, tracking webhook handler, and medusa-config.ts registration. Copy each file into your Medusa v2 project.
// src/modules/cod/service.ts
import {
AbstractPaymentProvider,
PaymentSessionStatus,
} from "@medusajs/framework/utils"
class CodPaymentProvider extends AbstractPaymentProvider {
static identifier = "cod"
async initiatePayment(input: {
amount: number
currency_code: string
context: Record<string, unknown>
}) {
// COD: no payment is collected at checkout
// Create a session with status "pending"
return {
id: `cod_${Date.now()}`,
data: {
method: "cash_on_delivery",
amount: input.amount,
currency: input.currency_code,
},
}
}
async getPaymentStatus(): Promise<PaymentSessionStatus> {
// COD payments are always "pending" until
// delivery agent collects cash
return PaymentSessionStatus.AUTHORIZED
}
async authorizePayment() {
// Auto-authorize — the "payment" is the promise to pay
return {
status: PaymentSessionStatus.AUTHORIZED,
data: { authorized_at: new Date().toISOString() },
}
}
async capturePayment(paymentData: Record<string, unknown>) {
// Called when COD cash is remitted to your bank
return {
...paymentData,
captured_at: new Date().toISOString(),
}
}
async refundPayment() {
// COD refunds are manual (bank transfer to customer)
return { status: "refunded" }
}
async cancelPayment() {
return { status: "cancelled" }
}
}
export default CodPaymentProviderCreate the COD payment provider module
Create src/modules/cod/service.ts extending AbstractPaymentProvider. The key difference from a standard payment provider: initiatePayment creates a session without collecting any payment. authorizePayment auto-authorises because COD "authorisation" is the customer promising to pay. capturePayment is called manually (or via a scheduled job) when Shiprocket remits the COD amount.
Create the Shiprocket fulfillment module
Create src/modules/shiprocket/service.ts extending AbstractFulfillmentProviderService. The module authenticates with Shiprocket's API using email/password credentials, creates orders with payment_method: "COD", and returns the AWB number for tracking. Store the awb_code and courier_name on the fulfillment metadata.
Register both modules
Add the COD and Shiprocket modules to medusa-config.ts. The COD module takes no options (it does not connect to any external service). The Shiprocket module takes email, password, and pickup_location from environment variables.
Build the tracking webhook
Create src/api/hooks/shiprocket/route.ts. Shiprocket sends status updates via webhook with status codes: 6 (shipped/out for delivery), 7 (delivered), 8 (cancelled), 9 (RTO initiated), 10 (RTO delivered). Map each code to the corresponding Medusa fulfillment and order state. Note that order.completed never fires reliably in some courier integrations (GitHub #10611) — use the tracking webhook as the source of truth.
Handle the RTO path
When status 9 or 10 arrives, cancel the order in Medusa, restock the inventory, and log the shipping cost. The inventory restock is critical — without it, your available stock decreases permanently for every RTO. Also verify the stock location: Medusa can deduct from the wrong location (GitHub #10658), so always specify location_id explicitly.
Test the full lifecycle
Create a test COD order. Verify it appears in Shiprocket with payment_method: COD. Simulate delivery by calling your webhook endpoint with status 7. Confirm the fulfillment transitions to "delivered" and the payment remains "awaiting" (cash collected but not yet remitted). Then simulate COD remittance by manually capturing the payment. Finally, test the RTO path: webhook with status 10, verify inventory is restocked.
What to know before you commit
Shiprocket API authentication uses email/password. There is no OAuth or API key — you store your Shiprocket account credentials in environment variables. The token expires after 24 hours, so your fulfillment module must handle re-authentication. The code recipe handles this with a lazy token fetch.
Draft orders bypass stock checks in some configurations (GitHub #14106). If you use draft orders for COD (creating the order before the customer confirms), verify that inventory is decremented at order creation, not at fulfillment.
COD amount handling. Shiprocket needs the order total in rupees, not paise. Medusa stores amounts in the smallest currency unit (paise for INR). Divide by 100 when sending to Shiprocket, or your COD collection will be 100× too high.
Courier selection matters. Shiprocket's auto-assign picks the cheapest courier by default. For COD, reliability matters more than cost — a failed delivery attempt on a COD order is a near-certain RTO. Override the auto-selection for high-value COD orders to use premium couriers (Delhivery, Blue Dart) with better first-attempt delivery rates.
+How do I add COD (cash on delivery) to Medusa v2?
Build a custom payment provider module extending AbstractPaymentProvider. The initiatePayment method creates a session without collecting payment. authorizePayment auto-authorises. capturePayment is called when the courier remits the collected cash. Register the module in medusa-config.ts. There is no official COD module for Medusa v2.
+How do I integrate Shiprocket with Medusa v2?
Build a fulfillment provider module extending AbstractFulfillmentProviderService. Authenticate with Shiprocket's API (POST /v1/external/auth/login with email/password), create orders via POST /v1/external/orders/create/adhoc, and handle delivery status via webhook callbacks. The community plugin Hemann55/medusa-fulfillment-shiprocket exists for v1 but was never ported to v2.
+What is the RTO rate for COD orders in India?
The national average RTO rate for COD orders is 25–30%. Tier 1 cities (Mumbai, Delhi, Bengaluru) are lower at 15–20%. Tier 2 cities (Jaipur, Lucknow, Indore) run 25–35%. Tier 3 cities and rural areas reach 35–40%. Fashion and apparel have the highest RTO rates; electronics and essentials have the lowest.
+How long does Shiprocket COD settlement take?
Shiprocket remits COD amounts to your bank account on a T+2 to T+8 cycle depending on the courier partner. Delhivery and Blue Dart typically settle in T+2 to T+3. Ecom Express and DTDC may take T+5 to T+8. You can opt for early COD remittance (Shiprocket charges 1.5–2% for this) to improve cash flow.
+Does Shiprocket charge for RTO shipments?
Yes. You pay both forward and return shipping for RTO orders. Forward shipping is ₹30–80 depending on weight and zone. Return shipping is ₹30–80 (same rate tier). So each RTO costs ₹60–160 in shipping alone, plus packaging and product handling costs. This is why reducing RTO rate is critical for COD profitability.
+How do I reduce COD RTO rates?
Four proven methods: (1) OTP verification before confirming COD orders (reduces fake orders by 30–40%), (2) COD with advance payment of ₹50–100 via UPI (filters low-intent buyers), (3) pincode-level COD blocking for high-RTO areas, (4) post-order prepaid conversion via WhatsApp/SMS payment link with a ₹50 discount (15–25% convert). Implement these in your Medusa order workflow, not in the payment provider.
+Can I use Medusa's built-in fulfillment for Indian shipping?
Medusa's built-in fulfillment provider (manual) only marks items as shipped — it does not connect to any courier API. For India, you need a fulfillment module that integrates with Shiprocket, Delhivery, or a similar logistics aggregator. Shiprocket is the most common choice because it aggregates 17+ courier partners and provides a single API for order creation, tracking, and COD management.
+What Medusa version does this guide apply to?
This guide targets Medusa v2.18.0 (latest stable as of August 2026). The AbstractPaymentProvider and AbstractFulfillmentProviderService interfaces have been stable since v2.0. The COD and Shiprocket module patterns work across v2.x releases. Webhook handling may need adjustment if Medusa changes its API route body parsing.
COD is operationally harder than prepaid — the payment comes after shipping, RTO eats margins, and reconciliation is a weekly chore. But in India, skipping COD means losing more than half your addressable market. The order flow simulator and RTO calculator above show the exact lifecycle and cost structure. If the numbers work for your margins, the code recipes give you a working COD + Shiprocket stack for Medusa v2 in an afternoon.
Wiring COD + Shiprocket into your Medusa store?
We build the full India fulfilment stack for headless commerce — COD, Shiprocket, RTO mitigation, and COD reconciliation. If you are stuck on the Shiprocket webhook pipeline or need to reduce RTO rates, we will scope it honestly.