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.
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.
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