Back to BlogIndustry Guides

Cash on Delivery at Scale: The Engineering Behind COD Ecommerce

Rupak Amin

Founder & Lead Engineer, RAITHub

13 min read

Cash on delivery works at scale when each order is a state machine, not a "paid" flag: a COD order only becomes revenue when the courier's remittance arrives and matches it. That means explicit order states, courier status sync, remittance reconciliation, return handling and rules for when to ask for an advance. Couriers charge for collecting cash too: Pathao Courier applies a 1% COD charge.

If you would rather have it built for you, see how RAITHub would build this below.

Why is cash on delivery harder to engineer than prepaid checkout?

Because the money is collected by someone else, days later, and sometimes not at all. A card payment is confirmed before you ship. A COD order is a promise that turns into cash only after a courier delivers, collects, deducts its fees and pays you in a batch.

Off-the-shelf platforms leave most of that gap to you. Shopify shows manual-payment orders such as COD as unpaid until you mark them paid on the order page. WooCommerce sets COD orders to "Processing" and tells the store owner to confirm the cash was collected before setting an order to "Complete". At a few orders a day, a person can do that. At a few hundred, the spreadsheet stops matching the bank account, and nobody knows which parcels the cash is missing for.

Three things make COD expensive when they are not engineered:

  • Cash in transit. Delivered parcels whose cash has not yet reached you. It is real money owed to you, and it needs a ledger line, not a guess.
  • Failed deliveries. A refused or unreachable parcel still costs the outbound delivery fee and, often, a return fee. Pathao lists returns at 50% of the delivery charge outside its Dhaka Metro zone (Pathao rate card).
  • Stock in limbo. Items on their way back are neither sellable nor sold. If the system treats them as either, inventory drifts.

Buy, build or hire: do you need custom COD engineering at all?

Not always. Here is the honest split.

RouteWhat you get for CODChoose this whenWhere it breaks
Hosted platform: Shopify (Basic from $19 a month billed yearly, $25 monthly)COD as a manual payment method; orders stay unpaid until a staff member marks them paidLow volume, one courier, and someone who can reconcile by hand each weekMarking paid is manual; courier statuses and remittances live in another system unless an app bridges them
Template or plugin: WooCommerce (free core plugin) with its built-in COD gatewayCOD limited by shipping method; orders sit in "Processing" until you confirm collectionYou already run WordPress and want control over hosting and pluginsEach courier and remittance format needs a plugin or custom code; return states are not modelled by default
Custom buildYour own order state machine, courier adapters, remittance import, reconciliation and return flowsCOD is a large share of orders, you use several couriers, or you sell through multiple sellers or warehousesWeeks of engineering, and you own the integrations when a courier changes its API

If you already run a hosted store and COD is the only pain, fix that part first. The wider decision is in Shopify limitations and when to go custom and the pillar guide building a marketplace or B2B store.

What order states does a cash-on-delivery order need?

More than "pending" and "paid". A COD order has a delivery life and a money life, and the second one finishes later than the first. These are the states that cover both.

StateMeaningWhat the system does on entry
placedCustomer checked out with CODReserve stock; apply advance-payment rules
awaiting_advanceAn advance is required before shippingSend a payment link; expire the reservation after a set time
confirmedOrder accepted, advance paid if one was requiredRelease to the warehouse queue
handed_to_courierParcel booked and picked upStore the courier's consignment ID; start status sync
delivered / partially_deliveredCourier reports collection of all or part of the cashRecord cash in transit for the collected amount; start return flow for refused items
delivery_failedRefused, unreachable or wrong addressQueue a reattempt or start the return
returning / returnedParcel coming back / received at the warehouseHold stock as "in return"; restock only after inspection
settledCourier remittance received and matchedMove the amount from cash in transit to settled revenue
cancelledCancelled before handoverRelease stock; refund any advance

The rule that matters most: "delivered" is not "paid". Delivered means the courier says it holds your cash. Settled means the cash is in your account and matches. Reporting revenue from delivery events alone is how stores discover a gap months later.

A small TypeScript state machine enforces which moves are allowed, so a late or duplicate courier event cannot drag an order backwards:

export type CodState =
  | 'placed' | 'awaiting_advance' | 'confirmed' | 'handed_to_courier'
  | 'out_for_delivery' | 'delivered' | 'partially_delivered'
  | 'delivery_failed' | 'returning' | 'returned' | 'settled' | 'cancelled'

const allowed: Record<CodState, readonly CodState[]> = {
  placed: ['awaiting_advance', 'confirmed', 'cancelled'],
  awaiting_advance: ['confirmed', 'cancelled'],
  confirmed: ['handed_to_courier', 'cancelled'],
  handed_to_courier: ['out_for_delivery', 'delivery_failed', 'returning'],
  out_for_delivery: ['delivered', 'partially_delivered', 'delivery_failed'],
  delivered: ['settled'],
  partially_delivered: ['returning', 'settled'],
  delivery_failed: ['out_for_delivery', 'returning'],
  returning: ['returned'],
  returned: ['settled'], // settles the delivery and return fees
  settled: [],
  cancelled: [],
}

export function canMove(from: CodState, to: CodState): boolean {
  return allowed[from].includes(to)
}

export function nextState(from: CodState, to: CodState): CodState {
  if (from === to) return from // duplicate event: ignore, don't fail
  if (!canMove(from, to)) {
    throw new Error('Illegal COD transition: ' + from + ' to ' + to)
  }
  return to
}

Apply it in the database as well as in code, so two workers cannot both move the same order: update orders set state = $2 where id = $1 and state = $3, and treat zero updated rows as "someone else got there first". Pair it with the reservation pattern in how to prevent stock overselling, because COD orders hold stock for days.

How do you integrate couriers without losing track of parcels?

Put each courier behind an adapter that does three jobs: book a consignment, report status, and import remittances. The rest of the system only ever sees your own states.

  • Booking. Send the order with the exact amount to collect, and store the courier's consignment ID against the order. Make booking idempotent with your order ID as the merchant reference, so a retry does not create two parcels.
  • Status sync. Accept webhooks where the courier offers them, and poll on a schedule regardless, because webhooks get lost. Save every raw event before acting on it.
  • Status mapping. Each courier uses its own vocabulary for the same events. Map them to your states in one table per courier, and send anything unrecognised to a review queue instead of guessing.
  • Amount changes. If the customer accepts only some items, the courier collects less. Record the collected amount from the courier, not the order total.

Do not trust a courier status as proof of cash. It is a claim that the remittance has to confirm, exactly as a payment redirect is a hint that a webhook has to confirm. The same pattern for online payments is in the bKash and SSLCommerz integration guide.

How do you reconcile COD remittances from a courier?

Import every remittance statement, match each line to an order by consignment ID, and check that the amount equals what was collected minus the courier's fees. Anything that does not match goes to a named person's queue.

A courier usually pays a batch: many parcels, one transfer, with delivery charges, COD charges and return fees already deducted. Your job is to explain the transfer line by line. Using Pathao's published rate card, a same-city parcel of 500 g to 1 kg costs 70 taka to deliver, and the COD charge is 1% (Pathao rate card). A 1,500-taka order would then reconcile like this:

LineAmount (taka)Source
Cash collected from the customer1,500.00Courier delivery event and statement
Delivery charge−70.00Rate card for zone and weight
COD charge at 1%−15.00Rate card
Expected net remittance1,415.00Your calculation, before the statement arrives

Illustrative only: rates change, VAT may apply, and your contract may differ, so read the fee rules from configuration per courier, never from code. Store every amount as integer poisha (one taka is 100 poisha) so the sums are exact.

Reconciliation then has four outcomes for each line: matched, amount differs, order not found, or order delivered but missing from every statement after the expected payout window. The last one is the expensive one, and only a scheduled ageing report finds it. Integer money, idempotent imports and exception queues are covered in depth in payments engineering in practice.

How should failed deliveries and returns be handled?

As first-class flows with their own states, costs and stock rules, not as cancellations. A returned parcel is a cost to record and a stock movement to inspect.

  • Reattempt before return. Allow a set number of reattempts, with a message to the customer each time, then start the return automatically.
  • Charge the cost where it belongs. Record the outbound fee and any return fee against the order, so margin reports show what failed deliveries really cost.
  • Restock after inspection. Move items from "in return" to sellable only when the warehouse scans and checks them. Damaged items go to a write-off.
  • Partial delivery. When a customer keeps some items, the kept lines settle and the refused lines follow the return flow. Each line needs its own state, or partial delivery becomes guesswork.

When should a store ask for an advance payment on a COD order?

When the order would be costly to lose if it came back. Base the rules on the order and the delivery, written down and shown to the customer at checkout, so they are predictable and fair.

Common order-based rules:

  • The order total is above a set threshold.
  • The delivery zone has high delivery and return fees.
  • The item is made to order, a pre-order, or bulky to ship back.
  • The quantity of one item is unusually large for a retail order.

Keep the rules in configuration with an effective date, and record which rule applied to each order, so support staff can explain a decision and you can change a threshold without a deploy.

How does partial COD work?

The customer pays part of the order online, often the delivery fee, and the rest in cash at the door. Engineering-wise, it is one order with two payments against it, and each payment has its own life.

  • Two payment records. An online advance through a gateway such as bKash, Nagad or SSLCommerz, and a COD amount for the courier to collect. The courier is told only the balance.
  • Refund rules for the advance. If the order is cancelled before handover, refund it. If delivery fails, the advance may cover the fee you already paid. State the rule at checkout.
  • Reconciliation from two sides. The advance reconciles against the gateway settlement; the balance against the courier remittance. The order is settled only when both are.

How long does it take to add COD engineering to an existing store yourself?

For an experienced developer with a custom store, roughly 2–4 weeks for the state machine, one courier adapter, a remittance import and a basic exceptions screen. Each further courier adds its own mapping and statement format.

The main risk of doing it yourself is marking orders paid from courier delivery events without reconciling the cash. It looks finished until the first month where the bank balance and the sales report disagree, and nobody can say by how much or why.

Why RAITHub for this

  • COD running in production. TheSkinProof, the founder's own venture built and run by RAITHub, takes cash on delivery alongside bKash, Nagad and SSLCommerz. See the TheSkinProof case study.
  • Built for warehouse reality. TheSkinProof has 5 portals, including warehouse and seller portals, across 217 API endpoints.
  • Tested like money code. 750+ automated tests on TheSkinProof; duplicate events, out-of-order statuses and partial deliveries get named tests that run in CI.
  • Several payment rails behind one design. PadhAI, also built by RAITHub, was built with 9 payment gateways.

The courier-specific details above are engineering guidance drawn from published rate cards; your couriers' APIs and contracts decide the specifics.

When you don't need us

  • You ship a handful of COD orders a day with one courier. Shopify or WooCommerce plus a weekly reconciliation in a spreadsheet is enough.
  • Your courier or an app already reconciles remittances for you and the numbers match the bank. Keep it.
  • You want a native mobile app. RAITHub builds web apps and PWAs only.
  • You need tax or accounting advice on how COD revenue is recognised. That is general information here; confirm with your adviser.

How RAITHub would build this

  • Scope:
    • A COD order state machine enforced in code and in PostgreSQL, with stock reservations and line-level partial delivery
    • Courier adapters for your couriers: idempotent booking, webhooks plus scheduled polling, status mapping with a review queue
    • Remittance import and reconciliation in integer poisha, with an ageing report for cash in transit
    • Return and reattempt flows with inspection-based restock
    • Configurable advance-payment and partial-COD rules, with online advances through your payment gateways
  • Timeline: a new store MVP with COD built in, 4–6 weeks fixed scope. Adding COD engineering and several couriers to an existing platform is backend and API work, at 6–12 weeks. If an existing COD flow is already losing track of cash, a code rescue takes 2–4 weeks.
  • What you receive: automated tests and CI, handover docs and runbooks (including a missing-remittance runbook), and full IP under NDA.
  • Next step: a free 15-minute technical audit, then a written fixed quote. Bring a sample remittance statement from each courier. Book the free 15-minute audit.

More on the sector is on the ecommerce page, and the integration service is API and backend development.

Frequently asked questions

How does cash on delivery work in ecommerce?

The customer orders without paying, the courier collects cash at delivery, deducts its delivery and COD charges, and remits the balance to the store in a batch. The store's system has to track each order from checkout to that remittance.

What does a courier charge for cash on delivery?

It varies by courier and contract. Pathao Courier and Steadfast Courier both publish a 1% COD charge, on top of delivery fees by zone and weight.

When should a COD order be marked as paid?

When the courier's remittance has arrived and matched the order, not when the courier reports delivery. Delivery means the courier holds your cash; settlement means you do.

What is partial COD?

The customer pays part of the order online, often the delivery fee, and the rest in cash at the door. The system records two payments against one order and settles the order only when both reconcile.

How do you handle returned COD parcels?

Give them their own states: returning, then returned. Record the delivery and return fees against the order, and restock items only after the warehouse inspects them.

Can Shopify handle cash on delivery?

Yes, as a manual payment method. Shopify shows those orders as unpaid until you mark them paid, so courier status sync and remittance reconciliation need an app or your own process.

Does RAITHub run cash on delivery in production?

Yes, in TheSkinProof, the founder's own venture, which takes COD alongside bKash, Nagad and SSLCommerz and runs under 750+ automated tests.

Cash on deliveryCOD ecommerceCourier integrationReconciliationOrder state machinebKashEcommerce

Ready to discuss your project?

Book a free 15-minute technical audit with our engineering team.