How to Generate a PDF Ticket or Boarding Pass Automatically
Event platforms generate tickets, but they're branded with the platform's chrome. If you're running a conference, a workshop, a transit system, or any booking flow on your own infrastructure, you need to generate tickets yourself.
Here's how to render a professional PDF ticket — with QR code, barcode, seat info, and event details — triggered on booking confirmation.
Event ticket template
function renderEventTicketHtml({ ticket }) {
const {
eventName, eventDate, eventTime, venue, venueAddress,
attendeeName, ticketId, ticketType, seatSection, seatRow, seatNumber,
orderId, doorTime,
} = ticket;
return `<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<style>
@page { size: 8.5in 3.5in; margin: 0; }
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
width: 8.5in;
height: 3.5in;
font-family: -apple-system, 'Segoe UI', sans-serif;
display: flex;
overflow: hidden;
}
/* Left panel — event info */
.main {
flex: 1;
background: #111;
color: white;
padding: 0.4in 0.4in;
display: flex;
flex-direction: column;
justify-content: space-between;
position: relative;
overflow: hidden;
}
.main::after {
content: '';
position: absolute;
top: -60px; right: -60px;
width: 200px; height: 200px;
border-radius: 50%;
background: rgba(255,255,255,0.04);
}
.ticket-type {
font-size: 10px;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 2px;
color: rgba(255,255,255,0.5);
}
.event-name {
font-size: 28px;
font-weight: 900;
line-height: 1.1;
margin-top: 8px;
}
.event-meta {
display: flex;
gap: 24px;
margin-top: 12px;
}
.meta-item .label {
font-size: 9px;
text-transform: uppercase;
letter-spacing: 1px;
color: rgba(255,255,255,0.4);
margin-bottom: 2px;
}
.meta-item .value { font-size: 13px; font-weight: 600; }
/* Middle divider — perforated look */
.divider {
width: 2px;
background: repeating-linear-gradient(
to bottom,
#333 0px, #333 8px,
transparent 8px, transparent 14px
);
position: relative;
flex-shrink: 0;
}
.divider::before, .divider::after {
content: '';
position: absolute;
left: 50%;
transform: translateX(-50%);
width: 20px;
height: 20px;
background: white;
border-radius: 50%;
}
.divider::before { top: -10px; }
.divider::after { bottom: -10px; }
/* Right panel — stub */
.stub {
width: 2.2in;
background: white;
padding: 0.35in 0.3in;
display: flex;
flex-direction: column;
justify-content: space-between;
}
.attendee-name { font-size: 15px; font-weight: 800; color: #111; }
.seat-info { }
.seat-label { font-size: 9px; text-transform: uppercase; letter-spacing: 1px; color: #999; margin-bottom: 2px; }
.seat-value { font-size: 22px; font-weight: 900; color: #111; }
.qr-placeholder {
background: #f0f0f0;
border: 1px solid #ddd;
display: flex;
align-items: center;
justify-content: center;
font-size: 9px;
color: #999;
text-align: center;
padding: 8px;
border-radius: 4px;
}
.ticket-id {
font-family: monospace;
font-size: 9px;
color: #999;
letter-spacing: 1px;
text-align: center;
margin-top: 4px;
}
</style>
</head>
<body>
<div class="main">
<div>
<div class="ticket-type">${ticketType}</div>
<div class="event-name">${eventName}</div>
</div>
<div class="event-meta">
<div class="meta-item">
<div class="label">Date</div>
<div class="value">${eventDate}</div>
</div>
<div class="meta-item">
<div class="label">Time</div>
<div class="value">${eventTime}</div>
</div>
<div class="meta-item">
<div class="label">Doors</div>
<div class="value">${doorTime}</div>
</div>
</div>
<div>
<div style="font-size:11px;color:rgba(255,255,255,0.5);margin-bottom:2px">${venue}</div>
<div style="font-size:10px;color:rgba(255,255,255,0.35)">${venueAddress}</div>
</div>
</div>
<div class="divider"></div>
<div class="stub">
<div>
<div style="font-size:9px;text-transform:uppercase;letter-spacing:1px;color:#999;margin-bottom:2px">Attendee</div>
<div class="attendee-name">${attendeeName}</div>
</div>
${seatSection ? `
<div class="seat-info">
<div class="seat-label">Section / Row / Seat</div>
<div class="seat-value">${seatSection} · ${seatRow} · ${seatNumber}</div>
</div>` : ""}
<div>
<!-- In production: render a real QR code with a library like qrcode -->
<div class="qr-placeholder" style="width:80px;height:80px;font-size:8px">
QR<br>${ticketId}
</div>
<div class="ticket-id">${ticketId}</div>
</div>
</div>
</body>
</html>`;
}
Add a real QR code
import QRCode from "qrcode";
async function renderTicketWithQR(ticket) {
// Generate QR code as data URL
const qrDataUrl = await QRCode.toDataURL(
`https://yourapp.com/tickets/verify/${ticket.ticketId}`,
{ width: 120, margin: 1 }
);
const html = renderEventTicketHtml({ ticket })
.replace(
`<!-- In production: render a real QR code with a library like qrcode -->
<div class="qr-placeholder" style="width:80px;height:80px;font-size:8px">
QR<br>${ticket.ticketId}
</div>`,
`<img src="${qrDataUrl}" width="80" height="80" style="border-radius:4px" />`
);
return html;
}
Generate PDF on booking confirmation
async function issueTicket(booking) {
const ticketId = `TKT-${Date.now().toString(36).toUpperCase()}`;
const html = await renderTicketWithQR({
eventName: booking.event.name,
eventDate: new Date(booking.event.startsAt).toLocaleDateString("en-US", {
weekday: "long", month: "long", day: "numeric", year: "numeric",
}),
eventTime: new Date(booking.event.startsAt).toLocaleTimeString("en-US", {
hour: "numeric", minute: "2-digit",
}),
doorTime: new Date(booking.event.doorsAt).toLocaleTimeString("en-US", {
hour: "numeric", minute: "2-digit",
}),
venue: booking.event.venue.name,
venueAddress: booking.event.venue.address,
attendeeName: booking.attendee.name,
ticketType: booking.ticketType,
seatSection: booking.seat?.section,
seatRow: booking.seat?.row,
seatNumber: booking.seat?.number,
ticketId,
orderId: booking.orderId,
});
const res = await fetch("https://pagebolt.dev/api/v1/pdf", {
method: "POST",
headers: {
"x-api-key": process.env.PAGEBOLT_API_KEY,
"Content-Type": "application/json",
},
body: JSON.stringify({ html }),
});
const pdf = Buffer.from(await res.arrayBuffer());
// Store and email
await uploadToS3(pdf, `tickets/${ticketId}.pdf`);
await db.tickets.create({ data: { ticketId, bookingId: booking.id } });
await emailTicket({ booking, pdf, ticketId });
return { ticketId, pdf };
}
Stripe webhook handler
app.post("/webhooks/stripe", express.raw({ type: "application/json" }), async (req, res) => {
const event = stripe.webhooks.constructEvent(
req.body,
req.headers["stripe-signature"],
process.env.STRIPE_WEBHOOK_SECRET
);
if (event.type === "payment_intent.succeeded") {
const booking = await db.bookings.findOne({
where: { stripePaymentIntentId: event.data.object.id },
include: { event: { include: { venue: true } }, attendee: true, seat: true },
});
if (booking) await issueTicket(booking);
}
res.json({ received: true });
});
Multi-ticket order (batch)
async function issueOrderTickets(order) {
const tickets = await Promise.all(
order.bookings.map((booking) => issueTicket(booking))
);
// Combine all tickets into one PDF for download
// (merge PDFs client-side or use a PDF merge library)
return tickets;
}
The ticket renders at 8.5×3.5 inches — standard event ticket dimensions, printable on any laser or inkjet. QR code links to your own verification endpoint so scanning works offline.
Try it free — 100 requests/month, no credit card. → Get started in 2 minutes
Top comments (0)