Handling Razorpay Refunds and Failed Payments in Node.js
Learn how to process refunds, handle failed payments, and manage payment disputes with Razorpay in your Node.js application.
Handling Razorpay Refunds and Failed Payments
Payments can fail, and customers may request refunds. This guide covers handling both scenarios in Node.js.
Processing Refunds
// Full refund router.post("/refund/:paymentId", userAuth, authorizeAdmin, asyncHandler(async (req, res) => { const { paymentId } = req.params; const { reason } = req.body; // Find the payment const payment = await Payment.findOne({ paymentId }); if (!payment) throw new AppError("Payment not found", 404); if (payment.status !== "paid") throw new AppError("Cannot refund unpaid order", 400); // Process refund on Razorpay const refund = await razorpay.payments.refund(paymentId, { amount: payment.amount, // Full refund (in paise) notes: { reason: reason, userId: payment.userId.toString() } }); // Update database payment.status = "refunded"; payment.refundId = refund.id; payment.refundAmount = refund.amount; payment.refundReason = reason; await payment.save(); // Revoke premium access await User.findByIdAndUpdate(payment.userId, { isPremium: false, premiumExpiresAt: new Date() }); res.json({ message: "Refund processed", refundId: refund.id }); }) ); // Partial refund router.post("/refund/partial/:paymentId", userAuth, authorizeAdmin, asyncHandler(async (req, res) => { const { paymentId } = req.params; const { amount, reason } = req.body; const refund = await razorpay.payments.refund(paymentId, { amount: amount * 100, // Partial amount in paise notes: { reason } }); // Update database const payment = await Payment.findOne({ paymentId }); payment.refundId = refund.id; payment.refundAmount = (payment.refundAmount || 0) + refund.amount; if (payment.refundAmount >= payment.amount) { payment.status = "refunded"; } await payment.save(); res.json({ message: "Partial refund processed", refundId: refund.id }); }) );
Handling Failed Payments
// Webhook handler for failed payments const handlePaymentFailed = async (payment) => { const dbPayment = await Payment.findOne({ orderId: payment.order_id }); if (dbPayment) { dbPayment.status = "failed"; dbPayment.failureReason = payment.error_description || "Payment failed"; dbPayment.failureCode = payment.error_code; await dbPayment.save(); // Notify the user const user = await User.findById(dbPayment.userId); await sendEmail(user.email, "paymentFailed", { firstName: user.firstName, amount: dbPayment.amount / 100, reason: payment.error_description }); } }; // API endpoint to retry failed payment router.post("/retry/:orderId", userAuth, asyncHandler(async (req, res) => { const { orderId } = req.params; const payment = await Payment.findOne({ orderId, userId: req.user._id }); if (!payment) throw new AppError("Order not found", 404); if (payment.status !== "failed") throw new AppError("Order is not in failed state", 400); // Create a new order for retry const newOrder = await razorpay.orders.create({ amount: payment.amount, currency: payment.currency, receipt: `retry_${Date.now()}`, notes: { userId: req.user._id.toString(), originalOrderId: orderId } }); // Update the payment record payment.orderId = newOrder.id; payment.status = "created"; await payment.save(); res.json({ orderId: newOrder.id, amount: newOrder.amount }); }));
Refund Webhooks
// In the webhook handler case "refund.processed": await handleRefundProcessed(event.payload.refund.entity); break; case "refund.failed": await handleRefundFailed(event.payload.refund.entity); break; const handleRefundProcessed = async (refund) => { const payment = await Payment.findOne({ paymentId: refund.payment_id }); if (payment) { payment.status = "refunded"; payment.refundId = refund.id; payment.refundedAt = new Date(); await payment.save(); // Notify user await sendEmail(payment.userId.email, "refundProcessed", { amount: refund.amount / 100 }); } };
Updating the Payment Model for Refunds
// Add refund fields to payment schema const paymentSchema = new mongoose.Schema({ // ... existing fields ... refundId: String, refundAmount: Number, // in paise refundReason: String, refundedAt: Date, failureReason: String, failureCode: String });
The Takeaway
Handling refunds and failed payments involves: processing full or partial refunds via razorpay.payments.refund(), updating the payment status in the database, revoking premium access for full refunds, handling refund webhooks (refund.processed, refund.failed), handling failed payment webhooks (payment.failed), notifying users of failures, and providing a retry endpoint for failed payments. Always update the database status to reflect the current state.
Use razorpay.payments.refund(paymentId, { amount: payment.amount, notes: { reason } }). For a partial refund, specify a smaller amount. Update the payment status to 'refunded' in the database, store the refund ID, and revoke premium access if applicable.
Handle the payment.failed webhook: update the payment status to 'failed', store the failure reason and code, notify the user via email, and provide a retry endpoint that creates a new order. Users can retry the payment with the new order ID.
Handle refund.processed (refund completed successfully update status and notify user) and refund.failed (refund could not be processed investigate and retry). Always verify the webhook signature before processing.
Create a new Razorpay order with the same amount: razorpay.orders.create({ amount, currency, receipt: 'retry_' + Date.now() }). Update the payment record with the new order ID and status 'created'. Return the new order ID to the frontend for a new checkout attempt.
For full refunds, yes revoke premium access by setting isPremium to false. For partial refunds, typically no the user keeps premium for the current period. Always notify the user via email about the refund status and any access changes.
Ready to master Node.js completely?
Want to upskill yourself, crack your next interview, and get your dream job? Join our comprehensive course to dive deeper with high-quality video tutorials, solve interview questions, and a premium community.
Master Node.js
Want to upskill yourself, crack your next interview, and get your dream job? Join our comprehensive course.

