Learn how CampusBite integrates direct UPI payments with dynamic link generation and multiple payment app support.
CampusBite uses a direct-to-store UPI payment model where customers pay directly to the store’s UPI ID. The platform generates UPI deep links that work across all major UPI apps in India.
Unlike traditional payment gateways, CampusBite implements a zero-commission model:
1
Customer initiates checkout
System generates UPI payment links with the exact amount and payment reference.
2
Customer pays via UPI app
Customer opens their preferred UPI app (Google Pay, PhonePe, Paytm, etc.) and completes payment.
3
Customer submits transaction ID
After payment, customer enters their UPI transaction ID in CampusBite.
4
Store verifies payment
Store employee manually verifies the payment in their UPI app and confirms in CampusBite.
This model eliminates payment gateway fees (typically 1.5-3%), allowing stores to keep 100% of their revenue. The trade-off is manual payment verification by store employees.
Merchant code (mc):
If you set the UPI_MERCHANT_CODE environment variable to a 4-digit code, it will be included in UPI links. This can provide better categorization in some UPI apps.Callback URL (url):
If you set APP_URL or FRONTEND_URL, it will be included as a callback URL. Some UPI apps may redirect users back to this URL after payment.
Compatibility links don’t include transaction references, making it harder to match payments to specific orders. Use these only when the standard link fails to open in the customer’s UPI app.
On iOS and web browsers, the generic upi:// link will open the default UPI app or show a list of installed apps. The app-specific intent URIs only work on Android.
The system prevents the same transaction ID from being used for multiple orders:
backend/src/controllers/orderController.js
if (normalizedTransactionId) { const duplicateTransactionOrder = await Order.findOne({ transaction_id: normalizedTransactionId, }).lean(); if (duplicateTransactionOrder) { return res.status(409).json({ success: false, message: "This transaction ID is already linked to another order. Please check the ID and try again.", }); }}
Store employees verify payments manually using this workflow:
1
Customer pays and submits order
Customer completes UPI payment and creates an order with the transaction ID.
2
Store receives payment notification
Store receives the payment in their UPI app (Google Pay, PhonePe, etc.).
3
Store checks payment details
Store employee verifies:
Payment amount matches order total
Transaction ID matches (if provided)
Payment reference appears in the transaction note (if app shows it)
4
Store confirms in CampusBite
Store employee updates payment status to “success” in the CampusBite dashboard.
5
Order proceeds to preparation
Order status automatically changes to “accepted” and store can begin preparation.
backend/src/controllers/orderController.js
export const updatePaymentStatus = async (req, res, next) => { try { const { id } = req.params; const userId = req.user.id; const { paymentStatus, transactionId } = req.body; if (!paymentStatus || !["pending", "success", "failed"].includes(paymentStatus)) { return res.status(400).json({ success: false, message: "Valid payment status is required (pending, success, failed).", }); } const order = await Order.findById(id) .populate("store_id") .populate("user_id", "name email no_show_count trust_tier"); if (!order) { return res.status(404).json({ success: false, message: "Order not found.", }); } if (order.store_id.owner_id.toString() !== userId) { return res.status(403).json({ success: false, message: "You are not authorized to update this order.", }); } const normalizedTransactionId = transactionId?.trim().toUpperCase(); if (normalizedTransactionId) { const duplicateTransactionOrder = await Order.findOne({ transaction_id: normalizedTransactionId, _id: { $ne: order._id }, }).lean(); if (duplicateTransactionOrder) { return res.status(409).json({ success: false, message: "This transaction ID is already linked to another order.", }); } order.transaction_id = normalizedTransactionId; } order.payment_status = paymentStatus; if (paymentStatus === "failed") { order.order_status = "cancelled"; order.cancelled_at = new Date(); order.cancellation_reason = "payment_failed"; } await order.save(); if (paymentStatus === "success") { await sendOrderStatusUpdate( order.user_id.email, order.user_id.name, formatOrder(order), "accepted", ); pushOrderNotification(order.user_id._id, order, "accepted"); } res.json({ success: true, message: paymentStatus === "success" && order.order_status === "placed" ? "Payment confirmed. Waiting for customer commitment before preparation." : "Payment status updated successfully.", data: { order: formatOrder(order, { store: order.store_id, customer: order.user_id }), }, }); } catch (error) { next(error); }};
The manual verification model works well for campus environments where stores and customers have an ongoing relationship and transaction volumes are manageable.