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 Cinema Ticketing System
Bonaventure OgetoBy Bonaventure Ogeto|Updated July 22, 2026

Build a Cinema Ticketing System

Build a cinema ticketing system by managing seats with a reserved state that times out unpaid reservations, collecting payment via Paystack before confirming the ticket, and generating a QR code with the booking reference for door scanning. Use webhooks to confirm payment before marking tickets as confirmed.

In this article

  1. 1. Seat Reservation with Timeout
  2. 2. Payment and Ticket Generation
  3. 3. Learn More

Seat Reservation with Timeout

Tables: movies, showtimes (movie_id, hall, date, time, seat_map), seats (showtime_id, row, number, category, price, status, reserved_by, reserved_until), bookings (customer_email, showtime_id, seat_ids[], total, status, paystack_ref), tickets (booking_id, seat_id, qr_code).

async function reserveSeats(customerId, showtimeId, seatIds) {
  // Check all seats are available
  var seats = await db.seats.findMany(seatIds);
  var unavailable = seats.filter(s => s.status !== 'available');
  if (unavailable.length > 0) throw new Error('Some seats are no longer available');

  var reservedUntil = new Date(Date.now() + 10 * 60 * 1000); // 10 min

  // Atomic update using transaction
  await db.transaction(async (trx) => {
    for (var seatId of seatIds) {
      await trx.seats.update(seatId, {
        status: 'reserved',
        reserved_by: customerId,
        reserved_until: reservedUntil,
      });
    }
  });

  var total = seats.reduce((sum, s) => sum + s.price, 0);
  return { total, reservedUntil };
}

// Cron job: release expired reservations every minute
async function releaseExpiredReservations() {
  await db.seats.updateWhere(
    { status: 'reserved', reserved_until_lt: new Date() },
    { status: 'available', reserved_by: null, reserved_until: null }
  );
}

Payment and Ticket Generation

var QRCode = require('qrcode');

async function initiatePayment(customerId, customerEmail, showtimeId, seatIds) {
  var seats = await db.seats.findMany(seatIds);
  var total = seats.reduce((sum, s) => sum + s.price, 0);
  var reference = 'TKT-' + Date.now();

  var booking = await db.bookings.create({
    customer_email: customerEmail, showtime_id: showtimeId,
    seat_ids: seatIds, total, 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: customerEmail, amount: total, currency: 'NGN',
      reference, metadata: { booking_id: booking.id },
    }),
  });
  return (await res.json()).data.authorization_url;
}

// Webhook: confirm booking and generate tickets
if (event.event === 'charge.success') {
  var bookingId = event.data.metadata.booking_id;
  var booking = await db.bookings.findById(bookingId);

  await db.bookings.update(bookingId, { status: 'confirmed' });
  await db.seats.updateMany(booking.seat_ids, { status: 'sold' });

  // Generate one QR code per seat
  for (var seatId of booking.seat_ids) {
    var ticketRef = bookingId + '-' + seatId;
    var qrCode = await QRCode.toDataURL(ticketRef); // base64 PNG
    await db.tickets.create({ booking_id: bookingId, seat_id: seatId, qr_code: qrCode, ref: ticketRef });
  }

  await sendTicketEmail(booking.customer_email, bookingId);
}

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 ticketed webinar platform for the same payment-before-access pattern applied to online events.

Sign up for the McTaba newsletter

Key Takeaways

  • ✓Reserve seats for 10 minutes when a customer starts checkout — release them if payment does not complete.
  • ✓Never confirm a ticket until the payment webhook fires — the redirect can be manipulated.
  • ✓Generate a unique QR code per ticket that encodes the booking reference for door validation.
  • ✓Store seat state as available, reserved (with timeout), or sold — update atomically to prevent double-booking.
  • ✓Support group bookings: one payment, multiple seats, each getting its own ticket QR code.

Frequently Asked Questions

How do I scan QR codes at the cinema door?
Build a simple door-validation app (mobile or tablet) that scans the QR code and calls your backend endpoint /tickets/validate with the ticket reference. Your backend checks if the ticket exists, is confirmed, has not been used, and marks it as used (scanned_at timestamp). Display a green or red result to the attendant.
What happens if a payment fails but the seats are still reserved?
The seats remain reserved for the 10-minute window, then auto-release. The customer can try again. If the payment fails mid-checkout (card declined), the customer is returned to the seat selection page where they can retry or choose different seats. The reservation timer is not reset on a failed payment.
Can I offer different seat category prices (VIP, standard, economy)?
Yes. Each seat row has a category and price stored in the seats table. When a customer selects seats across categories, your backend sums the individual prices. The Paystack transaction amount is the sum of all selected seat prices. The total is displayed to the customer before they proceed to payment.

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

C2B, B2C, and B2B in Daraja Terms vs Paystack Terms

Daraja and Paystack use completely different vocabulary for the same payment operations. C2B is Paystack Checkout. B2C is Paystack Transfers. STK Push is the mobile_money charge channel. This article maps every Daraja concept to its Paystack equivalent, explains where the mapping breaks down, and clarifies what Paystack simply does not support.

Read more

Who Bears the Fee: Split Payment Fee Configuration

A detailed breakdown of how the Paystack processing fee is handled in split payments. Covers the three bearer options (account, subaccount, all), worked examples showing the exact settlement amounts under each option, unit economics analysis, and how to choose the right fee configuration for your marketplace.

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.