Skip to content

Event Tickets

Sell a fixed number of tickets for an event, give every buyer a QR code per person, and scan those codes at the door with the Badger app. This is phase 1: general admission, one occurrence per product. Allocated seating, recurring events and offline scanning are out of scope.

How it's modelled

  • An event is a product with the Event Ticket product type (eventTicket decorator). One product per occurrence: a Friday and a Saturday show are two products.
  • Ticket types are variants: Adult / Child / Concession, or Standard / VIP. A product without variants is a single ticket type.
  • Capacity is stock. Each ticket type is its own SKU, so "100 adult tickets" is 100 in stock on the Adult variant (set on the product's Stock tab). A ticket order can't oversell: the stock is taken before payment is captured, and an order for a sold-out type is refused and its card hold released (see Dispatch, Cancellations & Returns).
  • One ticket per person admitted. Quantity 3 on a line is 3 tickets, each with its own code. A "family ticket" variant can admit several people with one code (admitsPerTicket).
  • Tickets never need shipping. They can't be returned; refund the order instead, which voids them.

Each ticket is its own record (Ticket), holding ids only: the order, the line, the event and the ticket type. It holds no name or email address. The QR code reads BDGRT1: followed by a 32-character code made from the ticket id with the site's ticket secret. Only a hash of the code is stored, so a copy of the database can't be turned into working tickets.

Operator view (the admin)

Setting up an event

  1. Catalogue › Products › New product. On the Settings tab, set Types to Event Ticket. The Event panel appears:

    Field Meaning
    Starts Required before tickets can be sold. Entered in the shop's time zone (see below).
    Ends Optional. Without it, the door treats the event as running 12 hours.
    Venue Shown on the ticket and in the confirmation email.
    Check-in opens Minutes before the start that scanning opens (default 120). Earlier scans are turned away as too early.
    Sales close Minutes before the start that tickets stop selling. Leave it empty to keep selling until the event ends, so the door can still sell.
    People per ticket How many people one ticket admits (default 1). A variant can override it with its own admitsPerTicket attribute.

    Time zone. Event times are entered and shown in the shop's time zone, set under Settings › Site identity › Time zone (or manageBranding setIdentity timeZone through MCP, or by a platform operator on System › Tenants). The panel says which zone it is using ("Times are in Europe/London") with a link to the setting. A shop with no zone gets the one its language implies (en_GB is Europe/London); with neither, Europe/London is assumed and the panel shows that as a warning, so a shop elsewhere sets its zone before selling. Changing the zone later doesn't move events already saved: each keeps the moment it was saved as. The same zone sets the scan app's "today", the POS day and admin dates (ShopZones in common; see Dates and time zones).

    Dates are stored as ISO-8601 with the site's offset, in the product attributes eventStart, eventEnd, eventVenue, checkInOpensMinutesBefore, ticketSalesCloseMinutesBefore and admitsPerTicket. The Event panel owns those attributes, so the Custom attributes editor doesn't list them.

    The panel checks its fields as you type: the end must be after the start, the minutes must be whole numbers of 0 or more, and people per ticket a whole number of 1 or more. A bad field gets its error underneath, and Save takes you to it instead of saving. The server applies the same rules: if it refuses the event details, nothing is saved (not even the product's other changes), and the editor comes back with what you typed and the error on the field.

  2. Add variants for the ticket types and set the stock for each on the Stock tab.

  3. Make the product live.

Don't put the date in the product name: the product page and the listing cards show it (see Buyer view). Choosing Event Ticket for a product without a stereotype of its own moves it onto the eventProduct stereotype when you save, which carries the event details. A product you gave another stereotype keeps it; add the Event details (eventDetails) extension to that stereotype's variantDisplay area to show them there. Sites created before this existed get the stereotype from the Event details on event pages platform upgrade (Settings › Platform upgrades), which also moves their existing events onto it.

Ended events drop out of collection pages and search. To keep them listed, turn off Hide Ended Events under Settings › Store and storefront › Events (hide-ended-events). Their product pages keep working either way.

Attendance

Orders › Event tickets (/admin/tickets/events) lists every event: when it is, whether it's on sale, tickets issued and how many are checked in. Events with no start come first (they can't be sold yet), then upcoming ones, then past ones. An event's product page links to its attendance too (the Attendance button).

The attendance page for one event shows:

  • Totals: tickets issued, people (family tickets count each person), checked in, still to arrive, and void.
  • By ticket type: the same counts per type, including types since removed from the product.
  • The ticket list: order number and ticket number, ticket type, status (Not checked in, Checked in, Void), when it was checked in, by whom and on which device. Filter by status, or search by order number, the last four characters of the code, or a full scanned code.
  • Actions on each ticket:
    • Check in: a manual check-in for someone whose phone has died. It ignores the check-in window, because staff have decided to let them in.
    • Undo check-in: corrects a mis-scan.
    • Void: the ticket stops working. Voiding doesn't refund the order or put the ticket back on sale. Refund the order to give the money back, and add stock to sell the place again.
  • Download CSV: every ticket for the event (order, ticket, type, admits, status, the code's last four characters, issued, checked in, by whom, device, voided, reason), for a spreadsheet or a printed door list.

The attendance pages are for Admin and Superadmin, the roles that can see orders. Tickets hold no customer details; the only names on these pages are of the staff who checked tickets in.

Refunds and cancellations void tickets automatically: cancelling an order voids all its tickets; a full refund voids them all; a refund naming a line and a quantity voids that many of the line's tickets, unused ones first. A part refund that names no line voids nothing, so void the right tickets here by hand.

Letting staff scan

Settings › Selling › POS staff (/admin/shopSettings/pos-staff; see POS staff) sets what each person with the POS Agent role can do in the Badger app. Switch on Check in event tickets for door staff. They don't need to take payments to scan. Administrators can scan without a profile. See Mobile POS.

Through MCP

An AI agent connected to the shop's MCP server can set up and follow events:

  • manageProducts with productTypeDecorators: ["eventTicket"] and an event object (start, end, venue, checkInOpensMinutesBefore, salesCloseMinutesBefore, admitsPerTicket; "" clears a field). Creating an event ticket product needs event.start, and it defaults to not needing shipping. Unless the call passes a stereotypeId, a product that becomes an event without a stereotype of its own gets eventProduct.
  • eventDetails is configured like any other extension (extensions getSchema eventDetails, and the placement prompt section): showVenue, showDoorsOpen, showStatus and showCalendarLink, all on by default. It has no text of its own to set.
  • tickets (read): list gives every event with its tally; get (skuId) gives the tally per ticket type and a page of tickets (order number, type, status, checked-in time), filtered by status. It doesn't return the codes or their last four characters.

Buyer view

Finding an event

  • The product page shows the event details panel (eventDetails) under the price, above the ticket types:
    • When, in the shop's time zone and language: "Saturday 14 November 2026, 7:00pm to 10:30pm". With no end it shows the start only; over several days it names both days ("Friday 13 November 2026, 7:00pm to Sunday 15 November 2026, 5:00pm"); an end before 6am the next day reads as the same night ("8:00pm to 1:30am").
    • Doors open: the time check-in opens, when the check-in window is more than 0 minutes.
    • Where: the venue.
    • Ticket status: Tickets on sale, Sold out (every ticket type's stock, read from the inventory service, is at 0; untracked stock never runs out), Ticket sales have closed, or This event has ended. Once sales have closed or the event has ended, Add to basket is switched off.
    • Add to calendar: an .ics file (/events/{skuId}/calendar.ics) with the start, end, venue and a link back to the page. Not offered once the event has ended.
  • Listing cards (collection pages, search results, category pages and the product rails of the Brock-family themes) show a compact date line and the venue under the name: "Sat 14 Nov 2026, 7:00pm" or "Fri 13 Nov to Sun 15 Nov 2026". An ended event that is still listed is marked Ended.
  • Ended events are left out of collection pages and search unless the shop turns that off (see Setting up an event). Listings catch up within five minutes of an event ending; search immediately.
  • Search and category pages offer an Event date sort, soonest first, when the results include events.
  • Search engines get a schema.org Event for each event page, with an offer per ticket type (see SEO Structured Data).

Buying

  • The basket and checkout work as for any product. Tickets can't be added once sales have closed or after the event has ended.
  • After payment, the order confirmation page shows one card per ticket: the QR code, event, date and time, venue, ticket type, "Ticket 2 of 4" and the code's last four characters, for reading out if the QR won't scan.
  • The confirmation email shows the same tickets and links to the confirmation page.
  • Signed-in buyers see the same cards on the order in My account.
  • The QR image is served only to someone holding the order's confirmation link, and a voided ticket's image is no longer served.
  • Each ticket card, on the page and in the email, has Add to Apple Wallet and Add to Google Wallet buttons once the platform has those wallets set up. A button whose wallet isn't configured doesn't appear. The plain-text email lists the same links.

Door staff view (the Badger app)

  1. Sign in with a POS agent account that has Check in event tickets, and open Scan tickets on the Sell screen.
  2. Pick the event. The list shows today's events and the next two days' (the Door App Days Ahead setting under Settings › Store and storefront › Events, ticketing-app-lookahead-days, at most 31; 0 shows today's only), earliest first. An event whose check-in hasn't opened yet is listed as upcoming with the time its door opens, so staff can see it's there; scans before that time are still turned away as too early. The choice stays for the session.

    "Today" is worked out in the shop's time zone. On a shop whose zone is UTC, just after midnight in a UK summer it is still the day before, which is why the list looks ahead rather than showing today alone. 3. Scan each code with the camera or a hardware scanner. The screen shows the result in large colours: - Green, admitted, with the ticket type and how many people it admits. - Amber, already checked in, with the time and who scanned it. That's the sign of a copied ticket. - Red for a void ticket, an unknown code, or a ticket for another event (with that event's name, so the holder can be redirected). A wrong-event ticket isn't checked in. - Scans before check-in opens, or after the event has ended, are refused with the reason. 4. The header keeps a running tally (checked in / issued). Undo last reverses a mis-scan, and a manual lookup finds tickets by order number or the code's last four characters.

Two devices scanning the same ticket at the same moment can't both admit it: the check-in is one conditional update, and exactly one of them gets "admitted". The app needs a connection; without one it says the ticket wasn't checked in rather than queuing the scan. The scan API is in the API Reference under the Tickets tag.

How the storefront side is built

  • EventDisplayService (commerce-core, ticketing) builds everything a shopper sees: the panel (EventPageView), the state, the ticket types with stock, the .ics file and the cards' helper. EventDateFormatter formats dates and times in Java, so templates only print strings.
  • EventDetailsExtension (eventDetails) puts the panel on the model; the templates are templates/{nova,bootstrap}/extensions/eventDetails/eventDetails.html (the Brock family uses Nova's), styled by .nova-event-details in bdgr.css.
  • EventCardsInterceptor puts eventCards (EventCards, a formatter that makes no calls) on every page; every card includes fragments/eventCardLine with ${eventCards.of(product)}.
  • EventCalendarController serves the .ics; EventStructuredDataInterceptor adds the JSON-LD.
  • EventStereotypes owns the eventProduct stereotype; EventPagesUpgrade (2026.10-event-pages) adds it to existing sites.
  • Listings: ProductService.save copies the event's start and end into Product.eventStartsAt and eventEndsAt, which findListableBySiteAndCollectionIDs filters on. The search index has eventEndsAt (every document; non-events never end) and eventStartsAt (events only), product index revision 10; MerchandisingQueryService filters and sorts on them and falls back to listing everything on an index built before they existed.

Security notes

  • The site's ticket secret (Settings › Event tickets, ticketing-signing-secret) is created on first use and is never copied by site data transfer. Changing it makes every ticket already issued unscannable, so only do it if the secret has leaked.
  • Log lines name tickets, orders, events and sites by id only. Codes are never logged.

Wallet passes

Every ticket can go into Apple Wallet and Google Wallet. The pass shows the event, its date and time (in the shop's time zone), the venue, the ticket type, "1 of 3", how many it admits, and the same QR code as the ticket (BDGRT1:<code>, with the short code beneath it). Passes take the shop's name, its email brand colour and its logo (a PNG, JPEG or GIF logo; with an SVG logo, or none, the pass shows a plain ticket icon and the shop's name).

Link What it does
/o/{orderId}/{code}/tickets/{ticketId}.pkpass The signed Apple Wallet pass (application/vnd.apple.pkpass). iPhone and Mac Safari open it in Wallet; other browsers download it.
/o/{orderId}/{code}/tickets/{ticketId}/google-wallet Redirects to Google's "Save to Google Wallet" page. The signed link is made on demand, so the email carries a short URL.

Both are authorised like the QR image, by the order's confirmation code. A wrong code, another order's or another shop's ticket, a void ticket, and a wallet that isn't configured all get a 404.

Passes aren't updated after they're added. There's no Apple pass web service in this phase, so a pass in Apple Wallet stays as it was. That's safe: the door checks every scan on the server, so a pass for a voided ticket doesn't admit. When Google Wallet is configured, voiding a ticket (refund, cancellation or by hand) also marks its Google pass inactive, in the background; a pass the buyer never saved is skipped.

Setting up (superadmin, once for the platform)

The credentials are platform-wide: one Apple Pass Type ID and one Google issuer serve every shop. Enter them in System › Global configuration (/system/globalSettings/configuration). Each key is listed there with a description, even before it's set. A wallet's buttons appear once all of its keys are set and readable; a key that can't be read is logged as a warning naming the key, never its value.

Apple Wallet (needs a paid Apple Developer Program membership):

Key Where it comes from
apple-wallet-pass-type-id Apple Developer › Certificates, Identifiers & Profiles › Identifiers › Pass Type IDs: register one, e.g. pass.uk.co.bdgr.tickets.
apple-wallet-team-id The account's 10-character Team ID (Apple Developer › Membership details).
apple-wallet-certificate (secret) On the Pass Type ID, Create Certificate from a CSR made in Keychain Access. Install the .cer, export the certificate with its private key from Keychain Access as a .p12, and paste the output of base64 -i pass.p12.
apple-wallet-certificate-password (secret) The password chosen when exporting the .p12.
apple-wallet-wwdr-certificate Apple's WWDR intermediate that issued the certificate (G4 for certificates made since 2022), from apple.com/certificateauthority. Paste it as PEM (openssl x509 -inform der -in AppleWWDRCAG4.cer) or as base64 of the .cer.

The pass certificate lasts a year. When it expires the Apple button disappears (and a warning is logged) until a new .p12 is entered.

Google Wallet:

Key Where it comes from
google-wallet-issuer-id The issuer id in the Google Pay & Wallet Console › Google Wallet API. Until Google grants publishing access, only the console's test users can save passes.
google-wallet-service-account (secret) In a Google Cloud project with the Google Wallet API enabled: IAM & Admin › Service accounts › create one › Keys › Add key › JSON. Paste the whole JSON file. Then add the service account's email in the Pay & Wallet Console › Users, with Developer access.

Each event becomes one Google event class, <issuerId>.<siteId>-<eventProductId>, created the first time one of its tickets is saved. Google doesn't change a class that already exists, so renaming or moving an event after its first pass has been saved leaves earlier Google passes showing the old details; the QR still works.

The buttons. The badge images are static files in web-mvc/src/main/resources/resources/settBuilder/wallet/ (an SVG for the page, a 2x PNG for emails, because most mail apps don't show SVG): add-to-apple-wallet.svg/.png and add-to-google-wallet.svg/.png. Both show 48px high, Google's minimum, with 8px between them.

  • Google: the official condensed "Add to Google Wallet" button, British English (enGB_add_to_google_wallet_add-wallet-badge.svg from the Google Wallet brand guidelines); the PNG is that SVG rendered at 348×96. Don't restyle or crop it.
  • Apple: still interim artwork. Apple's badge files sit behind the Apple Wallet Marketing Artwork License Agreement, which the Apple Developer account holder accepts on the Add to Apple Wallet guidelines page. Download them there and replace both files, keeping the names: the UK English SVG as is, and a PNG rendered from it at 304×96 (e.g. rsvg-convert -w 304 -h 96). Apple supplies only SVG and EPS.

Out of scope

Phase 1 doesn't do these:

  • Allocated seating (seat maps, choosing a seat).
  • Recurring or series events: create one product per date.
  • Holding tickets in the basket: stock is taken at checkout, not when a ticket is added.
  • One capacity across ticket types (e.g. 200 people whatever the mix): capacity is per type.
  • Offline scanning.
  • Updating wallet passes after they're issued (an Apple pass web service, push updates), PDF tickets, and names of attendees on tickets.
  • Transferring or reselling tickets.