Bonaventure OgetoBy Bonaventure Ogeto|

Build a Hotel Booking and Deposit System

Build a hotel booking system by managing room availability in a calendar data model, collecting a deposit via Paystack at booking confirmation, tracking the outstanding balance, and collecting the remainder on checkout. Use Paystack webhooks to confirm deposit payment before reserving the room, and the Refund API for cancellations.

Room Availability and Booking Flow

Tables: rooms (hotel_id, type, price_per_night, capacity, amenities), bookings (room_id, guest_email, check_in, check_out, nights, total_amount, deposit_amount, balance_due, status, paystack_deposit_ref, paystack_balance_ref), blocked_dates (room_id, date — one row per blocked night).

async function checkAvailability(roomId, checkIn, checkOut) {
  var nights = Math.ceil((new Date(checkOut) - new Date(checkIn)) / 86400000);
  var dates = [];
  for (var i = 0; i < nights; i++) {
    var d = new Date(checkIn);
    d.setDate(d.getDate() + i);
    dates.push(d.toISOString().split('T')[0]);
  }

  var blocked = await db.query(
    'SELECT date FROM blocked_dates WHERE room_id = $1 AND date = ANY($2::date[])',
    [roomId, dates]
  );
  return blocked.length === 0;
}

async function createBooking(guestEmail, roomId, checkIn, checkOut) {
  var available = await checkAvailability(roomId, checkIn, checkOut);
  if (!available) throw new Error('Room not available for those dates');

  var room = await db.rooms.findById(roomId);
  var nights = Math.ceil((new Date(checkOut) - new Date(checkIn)) / 86400000);
  var total = room.price_per_night * nights;
  var deposit = Math.ceil(total * 0.3); // 30% deposit

  var booking = await db.bookings.create({
    room_id: roomId, guest_email: guestEmail,
    check_in: checkIn, check_out: checkOut, nights,
    total_amount: total, deposit_amount: deposit,
    balance_due: total - deposit, status: 'pending',
  });

  // Initialize deposit payment
  var res = await fetch('https://api.paystack.co/transaction/initialize', {
    method: 'POST',
    headers: { Authorization: 'Bearer ' + process.env.PAYSTACK_SECRET_KEY, 'Content-Type': 'application/json' },
    body: JSON.stringify({
      email: guestEmail, amount: deposit, currency: 'NGN',
      reference: 'DEPOSIT-' + booking.id + '-' + Date.now(),
      metadata: { booking_id: booking.id, payment_type: 'deposit' },
    }),
  });
  var data = await res.json();
  return { booking, authorizationUrl: data.data.authorization_url };
}

Deposit Confirmation and Checkout Balance

// Webhook: confirm deposit and block dates
if (event.event === 'charge.success') {
  var meta = event.data.metadata;
  var booking = await db.bookings.findById(meta.booking_id);

  if (meta.payment_type === 'deposit' && booking.status === 'pending') {
    await db.bookings.update(booking.id, { status: 'confirmed', paystack_deposit_ref: event.data.reference });

    // Block each night of the booking
    var dates = getDatesInRange(booking.check_in, booking.check_out);
    for (var date of dates) {
      await db.blockedDates.create({ room_id: booking.room_id, date });
    }
  }

  if (meta.payment_type === 'balance') {
    await db.bookings.update(booking.id, { status: 'fully_paid', paystack_balance_ref: event.data.reference });
  }
}

// Collect balance at checkout
async function collectCheckoutBalance(bookingId) {
  var booking = await db.bookings.findById(bookingId);
  if (booking.status !== 'confirmed') throw new Error('Booking not confirmed');

  var res = await fetch('https://api.paystack.co/transaction/initialize', {
    method: 'POST',
    headers: { Authorization: 'Bearer ' + process.env.PAYSTACK_SECRET_KEY, 'Content-Type': 'application/json' },
    body: JSON.stringify({
      email: booking.guest_email, amount: booking.balance_due, currency: 'NGN',
      metadata: { booking_id: bookingId, payment_type: 'balance' },
    }),
  });
  return (await res.json()).data.authorization_url;
}

Learn More

See build a photography booking and deposit flow for the same deposit pattern applied to service bookings.

Sign up for the McTaba newsletter

Key Takeaways

  • Collect a deposit (30-50%) at booking to secure the reservation; balance on checkout.
  • Only confirm the reservation after the deposit webhook fires — do not block rooms for unpaid bookings.
  • Store check-in and check-out dates with availability logic to prevent double-booking.
  • Implement a cancellation policy: free cancellation 48+ hours out, 50% deposit forfeit 24-48 hours, no refund within 24 hours.
  • Use Paystack subaccounts if you run a multi-property platform where each hotel receives their own settlement.

Frequently Asked Questions

How do I prevent double-bookings between two concurrent requests?
Use a database transaction with a row-level lock on the blocked_dates table. When inserting blocked dates, wrap in a transaction and use SELECT FOR UPDATE on the room row. If two requests try to book the same dates simultaneously, one will wait for the lock and then see the dates are already blocked. PostgreSQL handles this naturally with proper transaction isolation.
How do I handle the cancellation refund based on timing?
On cancellation, calculate how far in advance the guest is cancelling (hours until check-in). Apply your policy: more than 48 hours = full deposit refund, 24-48 hours = 50% deposit refund, less than 24 hours = no refund. Use the Paystack Refund API with the deposit transaction reference and the refund amount. Log the refund in your bookings table with reason and amount.
Can I let multiple hotels use the same platform with their own bank accounts?
Yes. Create a Paystack subaccount for each hotel. When initializing the deposit or balance payment, include the hotel's subaccount code. Paystack routes the hotel's share to their bank account at settlement and your platform keeps the commission. The guest pays one total amount; Paystack handles the split.

Ready to build real-world apps?

Join the McTaba Labs full-stack marathon (4 months full-time · 6 months part-time). Learn M-Pesa, USSD, and WhatsApp engineering while shipping 8 production apps.

Apply to the McTaba Marathon