Tickets are issued, transferred, refunded, validated and scanned through the events controllers. Ticket types are the templates; tickets are the issued instances. Sales run through the Storefront's payment plumbing — every ticket type is mapped to a Storefront product variant, so promotions, taxes, and gift cards all behave like a regular order.
Endpoints
Staff
/events/:eventId/ticketsJWT/events/tickets/purchaseJWT/events/tickets/compJWT/events/tickets/pre-generateJWT/events/tickets/:ticketId/activateJWT/events/:eventId/ticketsJWT/events/tickets/:ticketId/qrJWT/events/tickets/validateJWT/events/tickets/:ticketId/transferJWT/events/tickets/:ticketId/cancelJWT/events/tickets/:ticketIdJWT/events/tickets/:ticketId/refundJWT/events/bookings/:bookingId/cancelJWT/events/bookings/:bookingId/refundJWTCustomer / public
/client/events/:eventId/ticket-typesNo auth/client/events/tickets/purchaseNo auth/client/events/tickets/confirm-orderNo auth/client/events/tickets/mineJWT/client/events/tickets/:ticketIdJWT/client/events/tickets/:ticketId/qrJWT/client/events/tickets/:ticketId/transferJWT/client/events/stripe/intentNo authTicket types
A TicketType defines a sellable tier. Create them through the data API:
await fetch('/data/event_ticket_type/create', {
method: 'POST',
headers: { orgid: 'my-org', Authorization: `Bearer ${jwt}`, 'Content-Type': 'application/json' },
body: JSON.stringify({
data: {
event: 'event-id',
name: 'General Admission',
price: 199,
currency: 'USD',
capacity: 800,
saleStart: '2026-04-01T00:00:00Z',
saleEnd: '2026-09-12T00:00:00Z',
perks: ['lunch', 'tote'],
allowReentry: true,
reentryLimit: 3, // total scans allowed (incl. first)
acceptedScanPoints: ['main-door'], // optional scan-point whitelist
visibility: 'public', // 'public' | 'private' | 'invite-only'
transferable: true,
refundPolicy: 'until-7-days', // free-form, used by your UI
},
}),
});
The corresponding Storefront product variant is created automatically by the EventTicketService so that prices, discounts and gateway payouts flow through the standard checkout pipeline.
Public ticket purchase
The public purchase endpoint takes one or more { ticketTypeId, quantity } items. It creates a booking, computes pricing, and returns either a Stripe payment intent or a PayPal order, depending on paymentGateway.
const purchase = await fetch('/client/events/tickets/purchase', {
method: 'POST',
headers: { orgid: 'my-org', 'Content-Type': 'application/json' },
body: JSON.stringify({
eventId: 'event-id',
email: '[email protected]',
name: 'Alex Reyes',
items: [
{ ticketTypeId: 'general', quantity: 2 },
{ ticketTypeId: 'workshop-a', quantity: 1 },
],
paymentGateway: 'stripe',
promoCode: 'EARLY10',
}),
}).then(r => r.json());
// purchase.bookingId, purchase.clientSecret (Stripe), purchase.amount, ...
Confirm after the gateway callback to flip booking state and issue the actual tickets:
await fetch('/client/events/tickets/confirm-order', {
method: 'POST',
headers: { orgid: 'my-org', 'Content-Type': 'application/json' },
body: JSON.stringify({
bookingId: purchase.bookingId,
paymentRef: 'pi_...',
paymentGateway: 'stripe',
}),
});
The confirm step issues one ticket per quantity, generates a fresh QR for each, sends the tickets-issued notification, and writes the booking → ticket links.
Comp tickets
Staff can issue tickets without payment for VIPs, speakers, or staff:
await fetch('/events/tickets/comp', {
method: 'POST',
headers: { orgid: 'my-org', Authorization: `Bearer ${jwt}`, 'Content-Type': 'application/json' },
body: JSON.stringify({
eventId: 'event-id',
ticketTypeId: 'vip',
holders: [
{ email: '[email protected]', name: 'Pat Lee' },
{ email: '[email protected]', name: 'Jordan Kim' },
],
note: 'Day-1 keynote speakers',
}),
});
Pre-generated tickets (venue sales)
For walk-up sales at the door without a buyer email up front:
// 1. Pre-generate a batch of unactivated tickets
await fetch('/events/tickets/pre-generate', {
method: 'POST',
headers: { orgid: 'my-org', Authorization: `Bearer ${jwt}`, 'Content-Type': 'application/json' },
body: JSON.stringify({ eventId: 'event-id', ticketTypeId: 'general', quantity: 200 }),
});
// 2. At the door, activate one when sold
await fetch(`/events/tickets/${ticketId}/activate`, {
method: 'POST',
headers: { orgid: 'my-org', Authorization: `Bearer ${jwt}`, 'Content-Type': 'application/json' },
body: JSON.stringify({
holderEmail: '[email protected]',
holderName: 'Walk-up',
payment: { amount: 199, method: 'card', ref: 'sq_...' },
}),
});
Transfer
await fetch(`/client/events/tickets/${ticketId}/transfer`, {
method: 'POST',
headers: { orgid: 'my-org', Authorization: `Bearer ${jwt}`, 'Content-Type': 'application/json' },
body: JSON.stringify({
toEmail: '[email protected]',
toName: 'New Holder',
}),
});
The original ticket is revoked, a new ticket id and QR are issued, and the recipient gets the ticket-transferred notification. Transfer is rejected if the ticket type has transferable: false.
Validate, refund, cancel
POST /events/tickets/validate accepts { qrPayload } and returns whether the ticket is currently valid for entry — without recording a check-in. Use this for door scanners that show a green/red light before the actual scan.
Refunds (POST /events/tickets/:ticketId/refund for one ticket, POST /events/bookings/:bookingId/refund for a whole booking) reverse payment via the original gateway, mark the ticket(s) as refunded, and free up capacity counters. Cancel without refund (/cancel) keeps the record but voids the QR.
The Storefront integration means a ticket purchase appears in the customer's order history alongside any physical merchandise — a single profile, a single billing surface.