Payload CMS cash on delivery integration
The Ecommerce Plugin now supports Cash on Delivery (COD) as a payment method through the built-in codAdapter.
This guide explains how to migrate from the default ecommerce plugin, configure the COD adapter, and update your storefront checkout flow.
Prerequisites
Create a new Payload Ecommerce application
npx create-payload-app
Select: ecommerce and Complete the installation process and start the application.
Replace the Ecommerce Plugin
The default ecommerce template installs:
"@payloadcms/plugin-ecommerce": "^3.85.1"
Remove the official package:
pnpm remove @payloadcms/plugin-ecommerce
Install the updated Ecommerce Plugin with COD support:
pnpm add @shadowmkj/plugin-ecommerce
Update Imports
Replace all imports from:
@payloadcms/plugin-ecommerce
with:
@shadowmkj/plugin-ecommerce
For example:
import { ecommercePlugin } from '@shadowmkj/plugin-ecommerce'
Configure Payment Adapters
Open your Payload configuration and register both Stripe and COD payment methods.
payments: {
paymentMethods: [
stripeAdapter({
secretKey: process.env.STRIPE_SECRET_KEY!,
publishableKey:
process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY!,
webhookSecret:
process.env.STRIPE_WEBHOOKS_SIGNING_SECRET!,
}),
codAdapter({}),
],
},
Example:
import {
ecommercePlugin,
stripeAdapter,
codAdapter,
} from '@shadowmkj/plugin-ecommerce'
plugins: [
ecommercePlugin({
payments: {
paymentMethods: [
stripeAdapter({
secretKey: process.env.STRIPE_SECRET_KEY!,
publishableKey:
process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY!,
webhookSecret:
process.env.STRIPE_WEBHOOKS_SIGNING_SECRET!,
}),
codAdapter({}),
],
},
}),
]
Configure Frontend Payment Providers
Update your Ecommerce Provider configuration.
Replace existing payment methods with:
paymentMethods={[
stripeAdapterClient({
publishableKey:
process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY || '',
}),
codAdapterClient({}),
]}
Example:
<EcommerceProvider
paymentMethods={[
stripeAdapterClient({
publishableKey:
process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY || '',
}),
codAdapterClient({}),
]}
>
{children}
</EcommerceProvider>
Update Checkout Payment Flow
Update your checkout page to support COD orders.
Replace your existing payment initialization logic with:
const initiatePaymentIntent = useCallback(
async (method: string) => {
try {
const paymentData = await initiatePayment(method, {
additionalData: {
...(email ? { customerEmail: email } : {}),
billingAddress,
shippingAddress: billingAddressSameAsShipping
? billingAddress
: shippingAddress,
},
})
if (!paymentData) return
setPaymentData(paymentData)
if (method === 'cod') {
const result = await confirmOrder('cod', {
additionalData: {
paymentIntentID: paymentData.paymentIntentID,
},
})
if (result?.orderID) {
clearCart()
const queryParams = new URLSearchParams()
if (email) {
queryParams.set('email', email)
}
if (result.accessToken) {
queryParams.set(
'accessToken',
result.accessToken,
)
}
router.push(
`/orders/${result.orderID}?${queryParams.toString()}`
)
}
}
} catch (error) {
const errorData =
error instanceof Error
? JSON.parse(error.message)
: {}
let errorMessage = 'Payment failed'
if (errorData?.cause?.code === 'OutOfStock') {
errorMessage = 'Some items are out of stock'
}
setError(errorMessage)
toast.error(errorMessage)
}
},
[
email,
billingAddress,
shippingAddress,
billingAddressSameAsShipping,
initiatePayment,
confirmOrder,
],
)
Add Cash on Delivery to Checkout
Add a payment method selector to your checkout page.
<div className="space-y-3">
<h2 className="font-medium text-3xl">
Payment Method
</h2>
<label className="flex items-center gap-2">
<input
type="radio"
checked={paymentMethod === 'stripe'}
disabled={!canGoToPayment}
onChange={() => setPaymentMethod('stripe')}
/>
Pay Online (Stripe)
</label>
<label className="flex items-center gap-2">
<input
type="radio"
checked={paymentMethod === 'cod'}
disabled={!canGoToPayment}
onChange={() => setPaymentMethod('cod')}
/>
Cash on Delivery
</label>
<Button
disabled={!canGoToPayment}
onClick={() => {
if (paymentMethod === 'stripe') {
void initiatePaymentIntent('stripe')
} else {
void initiatePaymentIntent('cod')
}
}}
>
Continue
</Button>
</div>
COD Order Flow
Customer
│
▼
Checkout
│
▼
Select Cash on Delivery
│
▼
initiatePayment('cod')
│
▼
Create COD Payment Intent
│
▼
confirmOrder('cod')
│
▼
Order Created
│
▼
Cart Cleared
│
▼
Redirect To Order Page
Payment Status
Orders created through the COD adapter are not charged immediately.
Initial order state:
{
"paymentMethod": "cod",
"paymentStatus": "pending"
}
After payment is collected during delivery:
{
"paymentMethod": "cod",
"paymentStatus": "paid"
}
Migration Checklist
- Create a Payload Ecommerce project
- Remove
@payloadcms/plugin-ecommerce - Install
@shadowmkj/plugin-ecommerce - Update all imports
- Register
codAdapter - Register
codAdapterClient - Update checkout payment flow
- Add Cash on Delivery payment option to the checkout page
- Test order creation and confirmation flow
You're Ready
After completing these steps, customers can choose Cash on Delivery during checkout and place orders without making an online payment. The Ecommerce Plugin will create the order, track the payment status, and allow payment collection during delivery.