McTaba Labs logo
Software EngineeringThe WhiteboardProjectsEventsMasterclassesWebinarsLearn
Book Call

500+ developers already in the community

Get answers, find accountability partners, and stay ahead

Join Discord →
McTaba Labs logo

Educating 1 million students in tech by 2030.

Programs

  • HighSchool Graduates Launchpad
  • AI & Business Automation
  • Corporate Training
  • McTaba Story & Mission
  • Full-Stack Curriculum
  • Course Pricing & Plans
  • Frequently Asked Questions

Resources

  • Join Discord Community
  • Tech Events in Nairobi
  • Masterclasses
  • Free Resources
  • Learn
  • Campus
  • Africa Tech Talent Report
  • Partner With Us
  • Start a Project
  • Our Work

Contact Us

  • 124 Manyani East Road, Lavington, Nairobi
  • +254727795603
  • Book a Discovery Call

Office Hours:

Mon-Fri: 9am - 2pm EAT

Sat: 10am - 12pm EAT

© 2027 McTaba Labs - A product of Edtech Industries. All rights reserved.

Privacy PolicyTerms of ServiceRefund Policy
  1. Learn
  2. Paystack for African Developers
  3. Build a Flight or Bus Ticket Booking System
Bonaventure OgetoBy Bonaventure Ogeto|Updated July 22, 2026

Build a Flight or Bus Ticket Booking System

Build a ticket booking system by modeling routes with scheduled departures, tracking seat inventory per departure, collecting payment via Paystack before confirming the booking, and issuing e-tickets after webhook confirmation. Use a seat lock with expiry during checkout to prevent double-booking under concurrent load.

In this article

  1. 1. Route and Seat Inventory
  2. 2. Webhook Confirmation and E-Ticket
  3. 3. Learn More

Route and Seat Inventory

Tables: routes (origin, destination, operator, distance_km), departures (route_id, departs_at, vehicle_type, total_seats, available_seats, base_price), bookings (departure_id, passenger_name, passenger_email, passenger_phone, seat_number, amount_paid, status, paystack_ref, ticket_code).

async function searchRoutes(origin, destination, date) {
  return db.query(
    `SELECT d.*, r.origin, r.destination, r.operator
     FROM departures d
     JOIN routes r ON d.route_id = r.id
     WHERE r.origin = $1 AND r.destination = $2
     AND DATE(d.departs_at) = $3
     AND d.available_seats > 0
     ORDER BY d.departs_at`,
    [origin, destination, date]
  );
}

async function initiateBooking(departureId, passenger, seats) {
  var departure = await db.departures.findById(departureId);
  if (departure.available_seats < seats) throw new Error('Not enough seats');

  var amount = departure.base_price * seats;
  var ticketCode = 'TKT-' + Math.random().toString(36).slice(2, 10).toUpperCase();
  var reference = 'BUS-' + Date.now();

  var booking = await db.bookings.create({
    departure_id: departureId,
    passenger_name: passenger.name,
    passenger_email: passenger.email,
    passenger_phone: passenger.phone,
    seats_count: seats,
    amount_paid: amount,
    status: 'pending',
    paystack_ref: reference,
    ticket_code: ticketCode,
  });

  // Temporarily hold seats
  await db.departures.decrement(departureId, 'available_seats', seats);

  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: passenger.email,
      amount,
      currency: 'NGN',
      reference,
      metadata: { booking_id: booking.id, route: departure.route_id, departs_at: departure.departs_at },
    }),
  });
  return (await res.json()).data.authorization_url;
}

Seats are decremented immediately on booking initiation (temporary hold). If payment does not complete within 15 minutes, a cron job restores the seats and cancels the pending booking.

Webhook Confirmation and E-Ticket

if (event.event === 'charge.success') {
  var bookingId = event.data.metadata.booking_id;
  var booking = await db.bookings.findById(bookingId);
  if (booking.status !== 'pending') return res.sendStatus(200);

  await db.bookings.update(bookingId, { status: 'confirmed', paystack_txn_id: event.data.id });
  await sendEticketEmail(booking); // PDF attachment with ticket code + QR
}

// Cancellation and refund
async function cancelBooking(bookingId, reason) {
  var booking = await db.bookings.findById(bookingId);
  var departure = await db.departures.findById(booking.departure_id);
  var hoursUntilDeparture = (new Date(departure.departs_at) - new Date()) / 3600000;

  var refundAmount = 0;
  if (hoursUntilDeparture > 48) refundAmount = booking.amount_paid;
  else if (hoursUntilDeparture > 24) refundAmount = Math.floor(booking.amount_paid * 0.5);

  if (refundAmount > 0) {
    await fetch('https://api.paystack.co/refund', {
      method: 'POST',
      headers: { Authorization: 'Bearer ' + process.env.PAYSTACK_SECRET_KEY, 'Content-Type': 'application/json' },
      body: JSON.stringify({ transaction: booking.paystack_ref, amount: refundAmount }),
    });
  }

  await db.bookings.update(bookingId, { status: 'cancelled', cancel_reason: reason });
  await db.departures.increment(booking.departure_id, 'available_seats', booking.seats_count);
}

Get weekly developer tips

Join 25,000+ developers. Practical guides, job tips, and new content — straight to your inbox.

No spam. Unsubscribe anytime.

Learn More

See build a cinema ticketing system for the same seat-locking and e-ticket pattern in a different context.

Sign up for the McTaba newsletter

Key Takeaways

  • ✓Lock seats temporarily during checkout (10-15 min) and release them if payment does not complete.
  • ✓Issue e-tickets only after the charge.success webhook — redirect confirmation is unreliable.
  • ✓Store route, departure, origin, destination, and passenger name in Paystack metadata for clean audit trails.
  • ✓For bus operators (multiple companies), use subaccounts to route each operator's revenue to their bank account.
  • ✓Handle partial refunds for cancellations based on departure proximity — full refund 48+ hours out.

Frequently Asked Questions

How do I handle the 15-minute seat hold expiry?
Run a cron job every minute that queries for bookings with status "pending" and created_at older than 15 minutes. For each expired booking, restore the available_seats count on the departure and mark the booking as "expired". The customer sees a "session expired, please try again" message.
Can I support multiple bus operators under one platform?
Yes. Create a Paystack subaccount for each operator. When initializing the booking payment, include the operator's subaccount code. The operator gets their share at settlement automatically. Your platform keeps the booking fee as commission. Each operator can have a different commission rate based on your agreements.
How do I generate PDF e-tickets?
Use PDFKit or Puppeteer on the backend to generate a PDF with the passenger name, route, departure time, seat number, ticket code, and a QR code. Email it as an attachment after the charge.success webhook fires. Also store a copy in your database or a cloud storage bucket for customer re-download.

Learn Paystack Integration

KES 2,999

The definitive guide to building payment systems with Paystack — from your first transaction to production-grade payment infrastructure across Africa. 9 modules, 47 lessons. Free preview available.

Start Paystack Course

Not ready? Create Free Account

Keep reading

Things You Can Build with Paystack: Project Guides

A complete map of real-world projects you can build using the Paystack API. From online store checkouts and subscription platforms to savings apps and solar pay-as-you-go systems, each category links to step-by-step build guides with working code.

Read more

Build a Laundry Pickup and Payment App

A build guide for a laundry pickup and delivery app that uses Paystack to collect payments. Covers order pricing by weight or item count, pickup scheduling, status tracking through the wash cycle, payment collection at order creation or delivery, and customer notifications.

Read more

Build an Affiliate Commission Engine

A build guide for an affiliate commission engine where affiliates share tracking links, earn commissions when their referrals convert (pay via Paystack), and withdraw their balance via bank transfer. Covers click tracking, last-click attribution, commission ledger, and Paystack Transfers payout.

Read more

Build a Farmers Market Ordering Platform

A build guide for a farmers market ordering platform where customers order fresh produce from multiple farmers in one cart, the platform collects the combined payment via Paystack, and each farmer receives their share via Paystack Split Payments or Transfers. Covers inventory management, order routing to individual farmers, and delivery coordination.

Read more

Reconciling M-Pesa Payments Received Through Paystack

When customers pay via M-Pesa through Paystack, you end up with two sets of records: your database and Paystack's transaction log. This article walks through the practical reconciliation workflow. How to match transactions, what to do when they disagree, how to handle missing webhooks and reversed payments, and a working reconciliation script you can adapt for your own system.

Read more

Percentage vs Flat Split Configuration on Paystack

A detailed comparison of percentage and flat split configurations on Paystack. Covers how each model works, worked margin calculations across different transaction sizes, when to use percentage vs flat, implementing per-transaction overrides, and building hybrid models that combine both approaches.

Read more

Explore related topics

M-Pesa & African Stack

M-Pesa Daraja API tutorials, USSD development, and WhatsApp Business API guides. The African Stack that global bootcamps do not teach.

Learn to Code in Kenya

Everything a Kenyan needs to know before writing their first line of code. Career paths, real costs, hardware requirements, and honest answers about what it takes.

Learning to Code

Roadmaps, language guides, and practical advice for learning to code and launching a programming career.

Breaking Into Tech

Practical, no-nonsense guides for anyone looking to start or change their career in tech. From choosing your first language to acing interviews.

The African Stack: Skills for This Market

The skills that make Kenyan developers hard to replace: M-Pesa, USSD, WhatsApp, fintech, and building for the African market.

Bonaventure Ogeto

Bonaventure Ogeto

Founder, McTaba Labs

Bonaventure is the founder of McTaba Labs, a coding bootcamp in Nairobi, Kenya. He has spent years building products for the African market and mentoring the next generation of developers across the continent.