Overview
Event ticketing platforms need to handle:- Multiple ticket types and pricing tiers
- Early bird and promotional pricing
- Capacity limits and sold-out management
- QR code ticket generation
- Attendee check-in and validation
- Refund processing
Architecture
Implementation
1. Event and Ticket Setup
Define your event structure:Copy
// event-types.ts
interface Event {
id: string;
name: string;
description: string;
venue: string;
date: Date;
endDate?: Date;
imageUrl: string;
organizerId: string;
status: 'draft' | 'published' | 'soldout' | 'completed' | 'cancelled';
ticketTypes: TicketType[];
}
interface TicketType {
id: string;
eventId: string;
name: string;
description: string;
price: number;
quantity: number;
sold: number;
maxPerOrder: number;
salesStart: Date;
salesEnd: Date;
benefits: string[];
}
// Example event
const sampleEvent: Event = {
id: 'evt_concert_2024',
name: 'Afrobeats Night 2024',
description: 'The biggest Afrobeats concert of the year',
venue: 'Palais des Congrès, Brazzaville',
date: new Date('2024-12-31T20:00:00'),
imageUrl: '/events/afrobeats-2024.jpg',
organizerId: 'org_123',
status: 'published',
ticketTypes: [
{
id: 'tt_vip',
eventId: 'evt_concert_2024',
name: 'VIP',
description: 'Front row seats with backstage access',
price: 50000,
quantity: 100,
sold: 45,
maxPerOrder: 4,
salesStart: new Date('2024-10-01'),
salesEnd: new Date('2024-12-30'),
benefits: ['Front row seats', 'Backstage access', 'Meet & greet', 'Free drinks']
},
{
id: 'tt_standard',
eventId: 'evt_concert_2024',
name: 'Standard',
description: 'General admission',
price: 15000,
quantity: 500,
sold: 320,
maxPerOrder: 10,
salesStart: new Date('2024-10-01'),
salesEnd: new Date('2024-12-30'),
benefits: ['General admission', 'Standing area']
},
{
id: 'tt_early',
eventId: 'evt_concert_2024',
name: 'Early Bird',
description: 'Limited early bird pricing',
price: 10000,
quantity: 200,
sold: 200, // Sold out
maxPerOrder: 5,
salesStart: new Date('2024-09-01'),
salesEnd: new Date('2024-09-30'),
benefits: ['General admission', 'Early entry']
}
]
};
2. Ticket Purchase Flow
Handle ticket orders:Copy
import Yabetoo from '@yabetoo/sdk-js';
import { v4 as uuidv4 } from 'uuid';
const yabetoo = new Yabetoo(process.env.YABETOO_SECRET_KEY!);
interface OrderItem {
ticketTypeId: string;
quantity: number;
}
interface Order {
id: string;
eventId: string;
items: OrderItem[];
totalAmount: number;
buyerInfo: {
firstName: string;
lastName: string;
email: string;
phone: string;
};
status: 'pending' | 'paid' | 'cancelled' | 'refunded';
paymentIntentId?: string;
createdAt: Date;
}
async function createTicketOrder(
eventId: string,
items: OrderItem[],
buyerInfo: Order['buyerInfo']
) {
const event = await db.events.findById(eventId);
if (!event) throw new Error('Event not found');
// Validate availability
for (const item of items) {
const ticketType = event.ticketTypes.find(t => t.id === item.ticketTypeId);
if (!ticketType) throw new Error(`Ticket type ${item.ticketTypeId} not found`);
const available = ticketType.quantity - ticketType.sold;
if (item.quantity > available) {
throw new Error(`Only ${available} tickets available for ${ticketType.name}`);
}
if (item.quantity > ticketType.maxPerOrder) {
throw new Error(`Maximum ${ticketType.maxPerOrder} tickets per order for ${ticketType.name}`);
}
// Check sales period
const now = new Date();
if (now < ticketType.salesStart || now > ticketType.salesEnd) {
throw new Error(`Sales not open for ${ticketType.name}`);
}
}
// Calculate total
const totalAmount = items.reduce((sum, item) => {
const ticketType = event.ticketTypes.find(t => t.id === item.ticketTypeId)!;
return sum + (ticketType.price * item.quantity);
}, 0);
// Create order
const order: Order = {
id: uuidv4(),
eventId,
items,
totalAmount,
buyerInfo,
status: 'pending',
createdAt: new Date()
};
// Reserve tickets temporarily (15 min hold)
await reserveTickets(order);
// Create payment intent
const intent = await yabetoo.payments.create({
amount: totalAmount,
currency: 'XAF',
description: `Tickets for ${event.name}`,
metadata: {
orderId: order.id,
eventId,
eventName: event.name,
eventDate: event.date.toISOString(),
ticketDetails: JSON.stringify(items),
buyerEmail: buyerInfo.email,
buyerPhone: buyerInfo.phone,
type: 'event_ticket'
}
});
order.paymentIntentId = intent.id;
await db.orders.create(order);
return { order, paymentIntent: intent };
}
3. QR Code Ticket Generation
Generate unique QR codes for each ticket:Copy
import QRCode from 'qrcode';
import crypto from 'crypto';
interface Ticket {
id: string;
orderId: string;
eventId: string;
ticketTypeId: string;
ticketTypeName: string;
holderName: string;
qrCode: string;
qrCodeData: string;
status: 'valid' | 'used' | 'cancelled';
usedAt?: Date;
}
async function generateTickets(order: Order, event: Event): Promise<Ticket[]> {
const tickets: Ticket[] = [];
for (const item of order.items) {
const ticketType = event.ticketTypes.find(t => t.id === item.ticketTypeId)!;
for (let i = 0; i < item.quantity; i++) {
const ticketId = `TKT-${Date.now()}-${crypto.randomBytes(4).toString('hex').toUpperCase()}`;
// Create secure QR code data
const qrData = {
ticketId,
eventId: event.id,
ticketTypeId: item.ticketTypeId,
timestamp: Date.now(),
signature: crypto
.createHmac('sha256', process.env.TICKET_SECRET!)
.update(`${ticketId}:${event.id}`)
.digest('hex')
};
const qrCodeData = JSON.stringify(qrData);
const qrCodeImage = await QRCode.toDataURL(qrCodeData, {
width: 300,
margin: 2,
color: { dark: '#001C6B', light: '#FFFFFF' }
});
const ticket: Ticket = {
id: ticketId,
orderId: order.id,
eventId: event.id,
ticketTypeId: item.ticketTypeId,
ticketTypeName: ticketType.name,
holderName: `${order.buyerInfo.firstName} ${order.buyerInfo.lastName}`,
qrCode: qrCodeImage,
qrCodeData,
status: 'valid'
};
tickets.push(ticket);
}
}
// Save tickets to database
await db.tickets.createMany(tickets);
return tickets;
}
4. Webhook Handler
Process ticket purchases:Copy
app.post('/webhooks/yabetoo', async (req, res) => {
const event = req.body;
if (event.type === 'payment_intent.succeeded') {
const { metadata } = event.data;
if (metadata.type === 'event_ticket') {
const order = await db.orders.findById(metadata.orderId);
// Update order status
await db.orders.update(order.id, { status: 'paid' });
// Confirm ticket reservations
await confirmTicketReservations(order);
// Update sold count
for (const item of order.items) {
await db.ticketTypes.incrementSold(item.ticketTypeId, item.quantity);
}
// Generate tickets
const eventData = await db.events.findById(order.eventId);
const tickets = await generateTickets(order, eventData);
// Send tickets to buyer
await sendTicketsByEmail(order.buyerInfo.email, tickets, eventData);
await sendTicketsBySMS(order.buyerInfo.phone, tickets, eventData);
// Check if event is sold out
await checkSoldOutStatus(order.eventId);
}
}
if (event.type === 'payment_intent.failed') {
const { metadata } = event.data;
if (metadata.type === 'event_ticket') {
// Release reserved tickets
await releaseTicketReservations(metadata.orderId);
await db.orders.update(metadata.orderId, { status: 'cancelled' });
}
}
res.json({ received: true });
});
5. Ticket Validation (Check-in)
Validate tickets at the event entrance:Copy
interface ValidationResult {
valid: boolean;
ticket?: Ticket;
message: string;
ticketType?: string;
}
async function validateTicket(qrCodeData: string): Promise<ValidationResult> {
try {
const data = JSON.parse(qrCodeData);
// Verify signature
const expectedSignature = crypto
.createHmac('sha256', process.env.TICKET_SECRET!)
.update(`${data.ticketId}:${data.eventId}`)
.digest('hex');
if (data.signature !== expectedSignature) {
return { valid: false, message: 'Invalid ticket signature' };
}
// Find ticket
const ticket = await db.tickets.findById(data.ticketId);
if (!ticket) {
return { valid: false, message: 'Ticket not found' };
}
if (ticket.status === 'used') {
return {
valid: false,
ticket,
message: `Ticket already used at ${ticket.usedAt?.toLocaleString()}`
};
}
if (ticket.status === 'cancelled') {
return { valid: false, ticket, message: 'Ticket has been cancelled' };
}
// Mark as used
await db.tickets.update(ticket.id, {
status: 'used',
usedAt: new Date()
});
return {
valid: true,
ticket,
message: 'Ticket validated successfully',
ticketType: ticket.ticketTypeName
};
} catch (error) {
return { valid: false, message: 'Invalid QR code format' };
}
}
// Check-in API endpoint
app.post('/api/check-in', async (req, res) => {
const { qrCodeData } = req.body;
const result = await validateTicket(qrCodeData);
res.json(result);
});
6. Refund Processing
Handle ticket refunds:Copy
async function processRefund(orderId: string, reason: string) {
const order = await db.orders.findById(orderId);
if (order.status !== 'paid') {
throw new Error('Order is not eligible for refund');
}
const event = await db.events.findById(order.eventId);
// Check refund policy (e.g., no refunds 48 hours before event)
const hoursUntilEvent = (event.date.getTime() - Date.now()) / (1000 * 60 * 60);
if (hoursUntilEvent < 48) {
throw new Error('Refunds not available within 48 hours of event');
}
// Cancel all tickets
await db.tickets.updateMany(
{ orderId },
{ status: 'cancelled' }
);
// Release ticket inventory
for (const item of order.items) {
await db.ticketTypes.decrementSold(item.ticketTypeId, item.quantity);
}
// Create disbursement for refund
const disbursement = await yabetoo.disbursements.create({
amount: order.totalAmount,
currency: 'XAF',
firstName: order.buyerInfo.firstName,
lastName: order.buyerInfo.lastName,
paymentMethodData: {
type: 'momo',
momo: {
msisdn: order.buyerInfo.phone,
country: 'cg',
operatorName: 'mtn' // Detect from original payment
}
}
});
// Update order
await db.orders.update(orderId, {
status: 'refunded',
refundReason: reason,
refundId: disbursement.id
});
// Notify buyer
await sendRefundConfirmation(order.buyerInfo.email, order, disbursement);
return disbursement;
}
Ticket Purchase Flow
1
Browse Events
User browses available events and selects one.
2
Select Tickets
User chooses ticket type and quantity.
3
Enter Details
User provides contact information (name, email, phone).
4
Payment
User pays via mobile money (MTN or Airtel).
5
Receive Tickets
User receives QR code tickets via email and SMS.
6
Event Day
User presents QR code at venue for scanning and entry.
Best Practices
Ticket Reservation
Ticket Reservation
Hold tickets for 15 minutes during checkout to prevent overselling.
Fraud Prevention
Fraud Prevention
Use cryptographic signatures on QR codes to prevent counterfeiting.
Offline Check-in
Offline Check-in
Support offline ticket validation with periodic sync for remote venues.
Capacity Alerts
Capacity Alerts
Send alerts to organizers when tickets are 80%, 95%, and 100% sold.
Email Template Example
Copy
const ticketEmailTemplate = (buyer: any, tickets: Ticket[], event: Event) => `
<h1>Your Tickets for ${event.name}</h1>
<p>Dear ${buyer.firstName},</p>
<p>Thank you for your purchase! Here are your tickets:</p>
<div style="background: #f5f5f5; padding: 20px; margin: 20px 0;">
<h2>${event.name}</h2>
<p><strong>Date:</strong> ${event.date.toLocaleDateString()}</p>
<p><strong>Venue:</strong> ${event.venue}</p>
<p><strong>Tickets:</strong> ${tickets.length}</p>
</div>
${tickets.map(ticket => `
<div style="border: 1px solid #ddd; padding: 15px; margin: 10px 0;">
<h3>${ticket.ticketTypeName}</h3>
<p>Ticket ID: ${ticket.id}</p>
<img src="${ticket.qrCode}" alt="QR Code" width="200" />
</div>
`).join('')}
<p>Present this QR code at the entrance for entry.</p>
`;