Tickets
Scan and validate event tickets with the Storefront SDK
Overview
The tickets resource provides methods for event ticket scanning and validation. It is used by event staff scanner apps to authenticate, look up tickets, and mark them as used at venues.
Scanner flow:
- Fetch available events with
tickets.events() - Authenticate with a PIN via
tickets.validatePin() - Scan QR codes and consume tickets with
tickets.use()
Tickets are created automatically when a customer purchases a ticket product. Each ticket has a unique UUID code embedded in a QR code that is emailed to the customer.
Methods
tickets.events(options?)
List events that have scanner PIN codes configured. Used to populate the event selector in a scanner app.
const { events } = await storefront.tickets.events();Returns: Promise<TicketEventsResponse>
interface TicketEventsResponse {
events: TicketEvent[];
}
interface TicketEvent {
id: string;
name: string;
location: string;
startDate: string | null; // null = ongoing event
endDate: string | null;
}tickets.validatePin(eventId, pin, options?)
Validate a scanner PIN code for a specific event. Must be called before scanning to verify staff authorization.
const { success, event } = await storefront.tickets.validatePin(
'clx123event',
'1234'
);
if (success) {
console.log(`Authenticated for: ${event.name}`);
}Parameters:
| Param | Type | Required | Description |
|---|---|---|---|
eventId | string | Yes | Event ID to authenticate for |
pin | string | Yes | Scanner PIN code |
Returns: Promise<TicketValidatePinResponse>
interface TicketValidatePinResponse {
success: boolean;
event: TicketEvent;
}Throws: ValidationError if PIN is incorrect or event not found
tickets.get(code, options?)
Fetch ticket details by its unique code. Read-only lookup that does not modify the ticket.
const { ticket } = await storefront.tickets.get('550e8400-e29b-41d4-a716-446655440000');
console.log(ticket.status); // "VALID"
console.log(ticket.productName); // "Festival Day Pass"
console.log(ticket.usedCount); // 0Parameters:
| Param | Type | Required | Description |
|---|---|---|---|
code | string | Yes | Unique ticket code (from QR scan) |
Returns: Promise<TicketGetResponse>
Throws: NotFoundError if ticket not found
tickets.use(code, eventId, options?)
Use (consume) a ticket for a specific event. This is the core scanning endpoint.
const result = await storefront.tickets.use(
'550e8400-e29b-41d4-a716-446655440000',
'clx123event'
);
if (result.success) {
console.log('Entry granted!');
console.log(`Uses: ${result.ticket.usedCount}/${result.ticket.maxUses || '∞'}`);
} else {
console.log(`Denied: ${result.message}`);
}Parameters:
| Param | Type | Required | Description |
|---|---|---|---|
code | string | Yes | Ticket code from QR scan |
eventId | string | Yes | Event ID being scanned at |
Returns: Promise<TicketUseResponse>
interface TicketUseResponse {
success: boolean;
message: string;
ticket: PurchasedTicket;
}Important: A success: false response is not an exception. The ticket was found but could not be used (wrong event, expired, already used). Always check the success field.
Throws: NotFoundError if ticket code does not exist
Response Types
PurchasedTicket
interface PurchasedTicket {
id: string;
code: string; // QR code value (UUID)
status: TicketStatus;
ticketEventId: string | null;
firstName: string | null; // Holder name (if requiresHolder)
lastName: string | null;
customerEmail: string;
productName: string | null; // Snapshot at purchase
eventName: string | null; // Snapshot at purchase
eventDate: string | null; // Snapshot at purchase
price: number | null; // In cents
usedCount: number;
maxUses: number; // 0 = unlimited
validFrom: string;
validUntil: string | null; // null = no expiry
lastUsedAt: string | null;
createdAt: string;
}
type TicketStatus = "VALID" | "USED" | "EXPIRED" | "CANCELLED";Ticket Status Meanings
| Status | Description |
|---|---|
VALID | Ticket can be used |
USED | Ticket has reached its maximum uses |
EXPIRED | Ticket's validUntil date has passed |
CANCELLED | Ticket was cancelled (e.g., refund) |
Usage Limits
maxUses | Behavior |
|---|---|
0 | Unlimited uses (season pass / day pass) |
1 | Single entry (status becomes USED after one scan) |
N | Multi-use (N entries allowed) |
Ticket Products in the Storefront
Ticket products are regular products with a ticketInfo field. This field is included on the product detail endpoint (products.getBySlug()), not on listings.
const product = await storefront.products.getBySlug('summer-festival-pass');
if (product.ticketInfo) {
// This is a ticket product
console.log(product.ticketInfo.requiresHolder); // true/false
console.log(product.ticketInfo.maxUses); // 1 = single entry
}interface TicketInfoSummary {
id: string;
requiresHolder: boolean; // Whether holder names are required at checkout
maxUses: number; // 0 = unlimited, 1 = single entry
}Ticket Checkout
When purchasing ticket products, the checkout has special behavior:
- No shipping required — Ticket-only orders skip shipping entirely. Pass
shipmentMethod: null. - Holder names — If
ticketInfo.requiresHolderistrue, you must provideticketHoldersin the checkout params (one name per ticket quantity). - Sales window — Tickets can only be purchased within their
salesStart/salesEndwindow. The API returnsTICKET_SALES_NOT_STARTEDorTICKET_SALES_ENDEDerrors.
Checkout with Ticket Holders
import { randomUUID } from "crypto";
const orderId = randomUUID();
const { url } = await storefront.checkout.stripe({
customerData: {
first_name: "Matti",
last_name: "Meikalainen",
email: "matti@example.fi",
address: "Mannerheimintie 1",
postal_code: "00100",
city: "Helsinki",
phone: "+358401234567",
},
shipmentMethod: null, // No shipping for ticket-only orders
orderId,
successUrl: `${process.env.NEXT_PUBLIC_BASE_URL}/payment/success/${orderId}`,
cancelUrl: `${process.env.NEXT_PUBLIC_BASE_URL}/payment/cancel/${orderId}`,
ticketHolders: {
"product_id_here": [
{ firstName: "Matti", lastName: "Meikalainen" },
{ firstName: "Maija", lastName: "Meikalainen" },
],
},
}, { cartId });Checkout Error Codes for Tickets
| Code | Description |
|---|---|
TICKET_SALES_NOT_STARTED | Ticket sales haven't started yet |
TICKET_SALES_ENDED | Ticket sales have ended |
TICKET_HOLDER_REQUIRED | Holder names missing for a requiresHolder ticket |
Examples
Scanner App — Complete Flow
import { createStorefrontClient, NotFoundError } from "@putiikkipalvelu/storefront-sdk";
const client = createStorefrontClient({
apiKey: process.env.STOREFRONT_API_KEY!,
baseUrl: process.env.STOREFRONT_API_URL!,
});
// 1. List available events
const { events } = await client.tickets.events();
// → Display event selector to staff
// 2. Authenticate with PIN
const selectedEventId = events[0].id;
const { success } = await client.tickets.validatePin(selectedEventId, "1234");
if (!success) {
console.error("Invalid PIN");
return;
}
// 3. Scan tickets (called on each QR code scan)
async function handleScan(qrCode: string) {
try {
const result = await client.tickets.use(qrCode, selectedEventId);
if (result.success) {
// Show green checkmark
return {
allowed: true,
name: `${result.ticket.firstName ?? ""} ${result.ticket.lastName ?? ""}`.trim(),
uses: `${result.ticket.usedCount}/${result.ticket.maxUses || "∞"}`,
};
} else {
// Show red X with reason
return {
allowed: false,
reason: result.message,
status: result.ticket.status,
};
}
} catch (error) {
if (error instanceof NotFoundError) {
return { allowed: false, reason: "Unknown ticket" };
}
throw error;
}
}Detecting Ticket Products on Storefront
import { storefront } from "@/lib/storefront";
export default async function ProductPage({ params }: { params: { slug: string } }) {
const product = await storefront.products.getBySlug(params.slug);
const isTicket = !!product.ticketInfo;
return (
<div>
<h1>{product.name}</h1>
<p>{(product.price / 100).toFixed(2)} €</p>
{isTicket && (
<p>
{product.ticketInfo!.maxUses === 1
? "Single entry ticket"
: product.ticketInfo!.maxUses === 0
? "Unlimited entry pass"
: `${product.ticketInfo!.maxUses}-use ticket`}
</p>
)}
</div>
);
}Error Handling
import { NotFoundError, ValidationError, StorefrontError } from "@putiikkipalvelu/storefront-sdk";
// Scanning errors
try {
const result = await storefront.tickets.use(code, eventId);
} catch (error) {
if (error instanceof NotFoundError) {
// Ticket code doesn't exist
return { error: "Unknown ticket" };
}
if (error instanceof StorefrontError) {
return { error: error.message };
}
throw error;
}
// Checkout errors for ticket products
try {
const { url } = await storefront.checkout.stripe(params, options);
} catch (error) {
if (error instanceof ValidationError) {
if (error.message.includes("TICKET_SALES_NOT_STARTED")) {
return { error: "Ticket sales haven't started yet" };
}
if (error.message.includes("TICKET_SALES_ENDED")) {
return { error: "Ticket sales have ended" };
}
if (error.message.includes("TICKET_HOLDER_REQUIRED")) {
return { error: "Please fill in holder names for all tickets" };
}
}
throw error;
}