Build a ticketed webinar platform by creating events with capacity limits and pricing tiers, generating a unique access token for each attendee in the charge.success webhook, and using that token to authenticate entry to the live session and replay. Block access for attendees without a valid, paid token.
Tables: events (title, description, host_id, scheduled_at, timezone, price, early_bird_price, early_bird_deadline, capacity, seats_sold, status, video_platform, video_room_url), tickets (event_id, attendee_email, attendee_name, access_token, price_paid, paystack_ref, issued_at, used_for_live, used_for_replay).
var crypto = require('crypto');
function generateAccessToken() {
return crypto.randomBytes(24).toString('hex'); // 48-char unique token
}
async function getCurrentPrice(event) {
if (event.early_bird_deadline && new Date() < new Date(event.early_bird_deadline)) {
return event.early_bird_price;
}
return event.price;
}
async function purchaseTicket(attendeeEmail, attendeeName, eventId) {
var event = await db.events.findById(eventId);
if (event.status !== 'on_sale') throw new Error('Tickets not available');
if (event.seats_sold >= event.capacity) throw new Error('Event is sold out');
var price = await getCurrentPrice(event);
var reference = 'WEB-' + eventId + '-' + Date.now();
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: attendeeEmail,
amount: price,
currency: 'NGN',
reference,
metadata: { event_id: eventId, attendee_name: attendeeName, attendee_email: attendeeEmail },
}),
});
return (await res.json()).data.authorization_url;
}
// Webhook: issue ticket with access token
if (event.event === 'charge.success') {
var meta = event.data.metadata;
if (!meta.event_id) return res.sendStatus(200);
var webinarEvent = await db.events.findById(meta.event_id);
if (webinarEvent.seats_sold >= webinarEvent.capacity) {
// Race condition: sold out — refund
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: event.data.reference }) });
return res.sendStatus(200);
}
var accessToken = generateAccessToken();
await db.tickets.create({
event_id: meta.event_id,
attendee_email: meta.attendee_email,
attendee_name: meta.attendee_name,
access_token: accessToken,
price_paid: event.data.amount,
paystack_ref: event.data.reference,
issued_at: new Date(),
});
await db.events.increment(meta.event_id, 'seats_sold', 1);
await sendTicketEmail(meta.attendee_email, meta.attendee_name, webinarEvent, accessToken);
}
// Attendee joins via: /join?token=abc123
async function validateAccessToken(token, eventId, accessType) {
var ticket = await db.tickets.findByToken(token);
if (!ticket || ticket.event_id !== parseInt(eventId)) {
throw { status: 403, message: 'Invalid or expired ticket.' };
}
var webinarEvent = await db.events.findById(eventId);
if (accessType === 'live') {
var now = new Date();
var startTime = new Date(webinarEvent.scheduled_at);
var endTime = new Date(startTime.getTime() + 180 * 60000); // 3-hour window
if (now < new Date(startTime.getTime() - 15 * 60000)) throw { status: 403, message: 'Event has not started yet. Join 15 minutes before.' };
if (now > endTime) throw { status: 403, message: 'Live session has ended. Access the replay instead.' };
await db.tickets.update(ticket.id, { used_for_live: true, first_joined_at: new Date() });
}
if (accessType === 'replay') {
if (webinarEvent.status !== 'replay_available') throw { status: 403, message: 'Replay not yet available.' };
await db.tickets.update(ticket.id, { used_for_replay: true });
}
return { valid: true, room_url: webinarEvent.video_room_url };
}
// Refund before event (admin or automated)
async function refundTicket(ticketId) {
var ticket = await db.tickets.findById(ticketId);
var event = await db.events.findById(ticket.event_id);
var hoursUntilEvent = (new Date(event.scheduled_at) - new Date()) / (1000 * 60 * 60);
if (hoursUntilEvent < 24) throw new Error('Refund window has passed — less than 24 hours to event.');
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: ticket.paystack_ref }),
});
await db.tickets.update(ticketId, { status: 'refunded' });
await db.events.decrement(ticket.event_id, 'seats_sold', 1);
}
Get weekly developer tips
Join 25,000+ developers. Practical guides, job tips, and new content — straight to your inbox.
No spam. Unsubscribe anytime.
See build a cinema ticketing system for the seat reservation and QR code ticketing pattern applied to in-person events.
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