bKash and Nagad checkout in a React store: what actually ships
By Visual Friends Editorial · Published · Updated · 7 min read
Every ecommerce project we build in Bangladesh has the same three payment methods: bKash, Nagad and cash on delivery. The merchant handbooks make each one sound like a single API call. In practice, the code that keeps orders consistent lives between the gateway and your database, and that's where most stores break under real traffic.
bKash Tokenized Checkout: three round trips, not one.
The bKash Tokenized Checkout flow looks like a single 'pay now' button to the customer. On the server it's three round trips: grant token, create payment, and execute payment after the customer authenticates on bKash. The failure the merchant handbook doesn't emphasise is that the customer can close the bKash sheet after step two. Your order is created in bKash's system, but 'execute' never fires. If your React app treats 'payment created' as a paid order, you'll ship products that nobody paid for. We only mark an order 'paid' after the execute call returns a transaction ID and we've stored it against the order row.
Nagad's checksum and the callback that isn't.
Nagad's flow is superficially similar but hides the payment status inside a signed callback URL. The customer is redirected back to your site with an encrypted payload that you have to verify against Nagad's public key on the server. Two things bite: the redirect can arrive after the browser has already navigated away, and the payload is signed but not idempotent — a browser refresh on the callback page will hit your verify endpoint twice. We guard both with a payment_records table keyed on (order_id, gateway, gateway_ref) with a unique constraint, so the second hit is a no-op instead of a double credit.
The order state machine we keep on the server.
React state is fine for the checkout form. The order itself lives in a state machine on the server, not in the client. The states we use on every store:
- pending — order row exists, payment not initiated.
- awaiting_gateway — customer sent to bKash or Nagad, waiting on callback.
- paid — gateway confirmed and a payment_record exists.
- packed — warehouse marked items ready.
- shipped — courier accepted the parcel with a tracking ID.
- delivered — courier reported successful delivery or COD collection.
- returned or cancelled — terminal states with a stored reason.
Every transition is a database write with an audit row. The React client re-reads state from the server and never assumes it.
COD is still the default. Plan for the phone call.
Cash on delivery is 60–75% of orders on the stores we've launched, not 10%. That means your checkout has to accept COD as a first-class method, your admin panel has to expose a phone verification step, and your courier integration has to handle 'customer unreachable' as a normal state, not an error. We integrate with Steadfast, RedX and Pathao APIs depending on the merchant, and every store ships with a phone-verify screen that runs before the order is dispatched. Skipping it doubles the return rate.
One admin dashboard, three payment methods.
The final piece is the admin surface. We put bKash, Nagad and COD orders in the same list, sorted by payment state, with the payment_record reference visible on the row. A single 'mark packed' action moves an order forward regardless of gateway. Merchants don't want three separate dashboards; they want one place to run the day.