Skip to main content

Overview

Popup Store supports two payment methods:
  1. UPI Direct - Customer pays directly via UPI, uploads proof (2% discount)
  2. Razorpay - Full payment gateway with cards, wallets, etc. (2-3% fee)

Payment Method Comparison

FeatureUPI DirectRazorpay
Fee0% (2% discount to customer)2-3% (paid by customer)
VerificationManual (seller verifies proof)Automatic
SettlementDirect to seller UPI2-3 business days
SupportedUPI onlyCards, UPI, Wallets, NetBanking
Best ForHigh-trust, repeat customersNew customers, international

UPI Direct Flow

1. Customer Checkout

2. Payment Proof Upload

The customer pays via their UPI app, then uploads a screenshot:
// Upload payment proof to Supabase Storage
const { data, error } = await supabase.storage
  .from('payment-proofs')
  .upload(`${orderId}/${Date.now()}.jpg`, file);

// Update order with proof URL
await supabase
  .from('orders')
  .update({
    payment_proof_url: data.path,
    status: 'proof_uploaded'
  })
  .eq('id', orderId);

3. Seller Verification

Seller receives email with payment proof:
Subject: New Order #ORD-2024-001

Payment Method: UPI Direct
Amount: ₹2,598 (₹2,650 - 2% discount)

[View Payment Proof]
[Verify & Approve]
Seller clicks “Verify & Approve” in iOS app:
func verifyPayment(orderId: String) async {
    try await supabase
        .from("orders")
        .update(["status": "paid"])
        .eq("id", orderId)
        .execute()

    // Send confirmation email to customer
    await sendOrderConfirmation(orderId)
}

Razorpay Flow

1. Initialize Payment

import Razorpay from 'razorpay';

const razorpay = new Razorpay({
  key_id: process.env.RAZORPAY_KEY_ID,
  key_secret: process.env.RAZORPAY_KEY_SECRET
});

// Create Razorpay order
const order = await razorpay.orders.create({
  amount: finalAmount * 100, // Amount in paise
  currency: 'INR',
  receipt: `order_${orderId}`,
  notes: {
    storeId: store.id,
    orderId: orderId
  }
});

2. Frontend Checkout

const options = {
  key: process.env.NEXT_PUBLIC_RAZORPAY_KEY_ID,
  amount: order.amount,
  currency: order.currency,
  name: store.business_name,
  description: `Order #${orderNumber}`,
  order_id: order.id,
  handler: async (response) => {
    // Payment successful
    await verifyPayment({
      razorpay_order_id: response.razorpay_order_id,
      razorpay_payment_id: response.razorpay_payment_id,
      razorpay_signature: response.razorpay_signature
    });
  },
  prefill: {
    name: buyerName,
    email: buyerEmail,
    contact: buyerPhone
  },
  theme: {
    color: store.brand_color
  }
};

const razorpayInstance = new window.Razorpay(options);
razorpayInstance.open();

3. Webhook Verification

Razorpay sends webhooks for payment events:
api/webhooks/razorpay/route.ts
import crypto from 'crypto';

export async function POST(req: Request) {
  const body = await req.text();
  const signature = req.headers.get('x-razorpay-signature');

  // Verify webhook signature
  const expectedSignature = crypto
    .createHmac('sha256', process.env.RAZORPAY_WEBHOOK_SECRET)
    .update(body)
    .digest('hex');

  if (signature !== expectedSignature) {
    return Response.json({ error: 'Invalid signature' }, { status: 400 });
  }

  const event = JSON.parse(body);

  if (event.event === 'payment.captured') {
    // Update order status
    await supabase
      .from('orders')
      .update({
        status: 'paid',
        razorpay_payment_id: event.payload.payment.entity.id
      })
      .eq('razorpay_order_id', event.payload.payment.entity.order_id);

    // Send confirmation emails
    await sendOrderConfirmation(orderId);
  }

  return Response.json({ received: true });
}

Fee Calculation

UPI Direct (2% Discount)

const subtotal = 2650; // Cart total
const discount = subtotal * 0.02; // 2% discount
const finalAmount = subtotal - discount; // ₹2,598

Razorpay (2-3% Fee)

const subtotal = 2650;
const checkoutFee = subtotal * 0.025; // 2.5% average
const finalAmount = subtotal + checkoutFee; // ₹2,716.25
The fee is charged to the customer, not deducted from the seller’s payout.

Configuration

Razorpay Setup

  1. Create Razorpay Account: razorpay.com
  2. Get API Keys: Dashboard → Settings → API Keys
  3. Add to Environment:
.env.local
NEXT_PUBLIC_RAZORPAY_KEY_ID=rzp_test_xxxxx
RAZORPAY_KEY_SECRET=xxxxx
RAZORPAY_WEBHOOK_SECRET=xxxxx
  1. Configure Webhook:
    • URL: https://mypopup.shop/api/webhooks/razorpay
    • Events: payment.captured, payment.failed
    • Secret: (copy from Razorpay dashboard)

UPI Direct Setup

Seller configures UPI VPA in iOS app:
struct PaymentSettingsView: View {
    @State private var upiId: String = ""

    var body: some View {
        Form {
            Section("UPI Direct") {
                TextField("UPI ID", text: $upiId)
                    .textInputAutocapitalization(.never)
                    .keyboardType(.emailAddress)

                Text("Example: seller@paytm")
                    .font(.caption)
                    .foregroundColor(.secondary)
            }

            Button("Save") {
                saveUpiSettings()
            }
        }
    }
}

Security

Payment Verification

Razorpay Signature Verification:
function verifyRazorpaySignature(
  orderId: string,
  paymentId: string,
  signature: string
): boolean {
  const body = `${orderId}|${paymentId}`;

  const expectedSignature = crypto
    .createHmac('sha256', process.env.RAZORPAY_KEY_SECRET)
    .update(body)
    .digest('hex');

  return signature === expectedSignature;
}

UPI Proof Validation

Manual verification checklist for sellers:
  • Amount matches order total
  • UPI transaction ID is visible
  • Timestamp is recent (within 1 hour of order)
  • Recipient name/UPI ID matches store’s UPI
Never auto-approve UPI payments without manual verification. Fraudulent screenshots are common!

Order Status Lifecycle

Status Definitions

StatusDescriptionNext Action
payment_pendingWaiting for paymentCustomer pays
proof_uploadedUPI proof submittedSeller verifies
paidPayment confirmedSeller processes
processingOrder being preparedSeller ships
shippedPackage dispatchedDelivery
deliveredCustomer receivedComplete
cancelledOrder cancelledRefund (if paid)

Email Notifications

Order Confirmation (Customer)

Sent when payment is confirmed:
await resend.emails.send({
  from: '[email protected]',
  to: buyerEmail,
  subject: `Order Confirmation - ${store.business_name}`,
  react: OrderConfirmationEmail({
    orderNumber,
    items,
    totalAmount,
    shippingAddress,
    paymentMethod
  })
});

New Order Alert (Seller)

Sent immediately when order is placed:
await resend.emails.send({
  from: '[email protected]',
  to: store.owner_email,
  subject: `New Order #${orderNumber}`,
  react: NewOrderEmail({
    orderNumber,
    customerName: buyerName,
    items,
    totalAmount,
    paymentMethod,
    paymentProofUrl // if UPI Direct
  })
});

Testing

Razorpay Test Mode

Use test credentials for development:
# Test Key ID
rzp_test_xxxxxxxxxxxxxx

# Test Cards
4111 1111 1111 1111 (Visa - Success)
5555 5555 5555 4444 (Mastercard - Success)
4000 0000 0000 0002 (Declined)

# Test UPI
success@razorpay (Success)
failure@razorpay (Failed)

UPI Direct Testing

Create a test order flow:
  1. Use a dummy UPI screenshot
  2. Upload to dev environment
  3. Verify seller can see proof in iOS app
  4. Approve/reject and check status updates

Troubleshooting

Check:
  • API keys are correct (test vs live)
  • Webhook is configured with correct URL
  • Webhook secret matches environment variable
  • Order amount is in paise (multiply by 100)
Common issues:
  • File size > 5MB (compress image)
  • Storage bucket permissions (check RLS policies)
  • Invalid file type (only .jpg, .png, .webp)
  • Network timeout (increase timeout limit)
This is a webhook delay. Check:
  • Webhook logs in Razorpay dashboard
  • Your webhook endpoint is reachable
  • Signature verification is passing
  • Database update query is running