Bonaventure OgetoBy Bonaventure Ogeto|

Build a Barbershop Appointment and Payment App

Build a barbershop appointment app by managing time slots per barber, requiring a deposit (NGN 500-1,000) to confirm the booking via Paystack, and releasing the slot back to available if payment does not complete within 15 minutes. The deposit is applied toward the final bill; clients forfeit it if they no-show without 2 hours notice.

Slot Management and Booking

Tables: barbers (name, specialty, avatar_url), services (name, duration_mins, price), slots (barber_id, date, start_time, end_time, status, booking_id), bookings (client_name, client_phone, barber_id, service_id, slot_id, deposit_paid, status, paystack_ref).

async function getAvailableSlots(barberId, date, serviceDurationMins) {
  return db.slots.findAll({
    barber_id: barberId, date, status: 'available',
    duration_gte: serviceDurationMins, // only slots long enough for the service
  });
}

async function holdAndPay(clientData, barberId, serviceId, slotId) {
  var service = await db.services.findById(serviceId);
  var slot = await db.slots.findById(slotId);
  if (slot.status !== 'available') throw new Error('Slot already booked');

  var deposit = 50000; // NGN 500 in kobo
  var reference = 'BARBER-' + Date.now();

  // Hold the slot
  await db.slots.update(slotId, { status: 'held', held_until: new Date(Date.now() + 15 * 60000) });

  var booking = await db.bookings.create({
    ...clientData, barber_id: barberId, service_id: serviceId, slot_id: slotId,
    deposit_paid: deposit, status: 'pending', paystack_ref: reference,
  });

  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: clientData.email, amount: deposit, currency: 'NGN', reference,
      metadata: { booking_id: booking.id, slot_id: slotId, service: service.name },
    }),
  });
  return (await res.json()).data.authorization_url;
}

// Cron: release held slots where payment did not complete
async function releaseExpiredHolds() {
  var expiredSlots = await db.slots.findAll({ status: 'held', held_until_lt: new Date() });
  for (var slot of expiredSlots) {
    await db.slots.update(slot.id, { status: 'available', held_until: null });
    await db.bookings.update({ slot_id: slot.id, status: 'pending' }, { status: 'expired' });
  }
}

Confirmation and Appointment Reminders

if (event.event === 'charge.success') {
  var bookingId = event.data.metadata.booking_id;
  var slotId = event.data.metadata.slot_id;

  await db.bookings.update(bookingId, { status: 'confirmed' });
  await db.slots.update(slotId, { status: 'booked', booking_id: bookingId, held_until: null });

  var booking = await db.bookings.findById(bookingId);
  var slot = await db.slots.findById(slotId);

  // Send confirmation SMS
  await sendSMS(booking.client_phone, 'Booking confirmed! ' + slot.date + ' at ' + slot.start_time + '. Deposit: NGN ' + (event.data.amount / 100) + '. See you soon!');

  // Schedule reminder SMS 2 hours before appointment
  await scheduleReminder(booking.client_phone, slot.date, slot.start_time);
}

For no-show handling: if the client does not arrive within 30 minutes of their slot, mark the booking as "no_show". The deposit is forfeited (it stays in your account). Offer the barber a small compensation from the forfeited deposit as an incentive for holding the slot.

Learn More

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

Sign up for the McTaba newsletter

Key Takeaways

  • Require a small deposit (NGN 500-1,000) at booking to dramatically reduce no-shows.
  • Slot management per barber: each barber has their own calendar of available slots.
  • Auto-release booked slots after 15 minutes if payment does not complete.
  • Apply the deposit toward the final bill — clients pay only the balance at the shop.
  • For multi-branch barbershops, use Paystack subaccounts to route each branch's deposits to their account.

Frequently Asked Questions

Should the deposit be refundable if the client cancels in advance?
Offer free cancellation up to 2 hours before the appointment — full deposit refund via Paystack Refund API. Within 2 hours, the deposit is forfeited. This policy protects the barber's time while being fair to clients who have genuine reasons to cancel with enough notice.
How do I handle walk-in clients who do not book online?
Walk-in clients pay at the counter (cash or POS). Create a simple admin panel for barbers to manually mark slots as "walk-in" with cash payment noted. This keeps your slot calendar accurate for online bookings and lets you track revenue from both channels.
Can I build this for a multi-branch barbershop?
Yes. Add a branches table and link barbers and slots to branches. On the booking page, let clients select a branch first, then choose a barber at that branch. If you want each branch to receive their own payouts, create a Paystack subaccount per branch and route deposits accordingly.

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