Skip to main content

Documentation

No results found.
Features

Event Ticketing

Event Ticketing sells tickets to events straight from the event page: paid or free ticket types, capacity limits that can't oversell, an embedded Stripe checkout, emailed tickets with QR codes, one-tap door check-in from any phone, and atte...

Event Ticketing sells tickets to events straight from the event page: paid or free ticket types, capacity limits that can't oversell, an embedded Stripe checkout, emailed tickets with QR codes, one-tap door check-in from any phone, and attendee exports. Every buyer lands in the CRM, so the person who bought a ticket today is reachable by every follow-up tool the site has.


The problem

An event's tickets usually live on Eventbrite or a Facebook event — a third-party brand between the business and its own audience, a per-ticket fee on every sale, and an attendee list that belongs to someone else's platform. The website announces the event; the money and the relationship happen elsewhere.

The fix

A self-contained feature module under app/Features/EventTicketing/ puts ticket sales on the site's own event pages: ticket types with prices, capacity, and sales windows attach to any event from the Events Calendar; visitors buy (or RSVP free) without leaving the page flow; tickets with QR codes are emailed instantly; and door staff check people in by pointing a phone camera at the QR. Buyers create CRM contacts automatically (source Tickets) and completed orders record a ticket_order Analytics goal with the revenue.

Like every addon, it's structured as a feature module gated by feature:event_ticketing middleware. The service provider boots unconditionally; routes 404 and the sidebar entry hides when the feature is off. Toggling it on runs the module's migrations. Nothing is materialized into the page tree — the buy UI is a feature-gated section of the event detail layout that renders only while the feature is enabled.

What visitors see

On an event page with tickets on sale, a Get Tickets section lists each ticket type with its price ("Free" or the amount) — with the regular price struck through and an "Early bird until…" note while an early-bird tier is live — a low-stock note when 10 or fewer seats remain, and a Sold out badge (with a Join waitlist link) when none do. An optional promo code field validates live and shows the discount. The visitor picks quantities, enters name and email, and:

  • Free tickets (RSVP) issue instantly — no payment step.
  • Paid tickets go to an embedded Stripe checkout. The selected seats are held while they pay and released automatically if checkout is abandoned, so a slow payer can't lose their seats and an abandoner can't block a sellout.
  • Recurring events (weekly classes, monthly meetups) add a date picker — the chosen occurrence is stamped on the order and its tickets.

Every completed order gets:

  • A confirmation email with the order summary, the ticket codes, an attached .ics calendar invite, and a button to the ticket page.
  • A ticket page (tokenized link, no login) showing each ticket as a card: the door code, a QR code, its live status, a Change attendee control (reassign a ticket to the friend actually using it — the door list shows the new name), and — when wallet credentials are configured — Add to Apple Wallet / Save to Google Wallet buttons. The email link always comes back here — it's the buyer's ticket wallet at the door.

Mixed orders work naturally: two paid GA tickets and a free kids' ticket are one checkout, one email, three QR codes.

QR check-in at the door

Each ticket's QR encodes a check-in URL inside the dashboard — so door staff need no special app: point any phone camera at the QR, log in once as a Manager, and the check-in page opens with the event, ticket type, and buyer name and one big Check in button.

  • Checking in is atomic — two staffers scanning the same ticket at the same moment can't both admit it.
  • A second scan shows a loud "Already checked in" warning with when it was first scanned.
  • Refunded (void) tickets show a red "do not admit" rejection.
  • Mistakes are one tap to undo, and every ticket can also be checked in manually from the attendee list (search by code, name, or email).

The camera scanner

Dashboard → Ticketing → Scan turns any staff phone into a dedicated scanner: a live camera view (native BarcodeDetector — Chrome/Edge/Android; requires HTTPS) reads ticket QRs continuously and checks each in on the spot with a green/amber/red result card, a running session tally (checked in / duplicates / rejected), and today's check-ins per staffer. Scans made while the venue wifi drops are queued in the browser and synced automatically when the connection returns. Manual code entry sits under the camera for browsers that can't scan in-page. Per-staff totals also show on each event's manage page (every check-in records who scanned it).

What the dashboard gets

Three pages under Dashboard → Ticketing (Manager and up; Settings is Admin-only):

  • Events & Tickets — every event with ticket types, showing sold/capacity, check-ins, and revenue at a glance, plus a picker to start selling tickets on another event, and the promo codes manager (percent or amount off, optional per-event scope, expiry, and redemption caps — redemptions count completed orders only). Each event's manage page holds the ticket type editor (name, price, early-bird price + deadline, capacity, max per order, sales window, on/off), stat tiles, per-staff check-in totals, the attendee list with search and check-in/undo, the event's orders, and an Add to Marketing action that pushes every buyer into an "Attendees: {event}" CRM group + the Marketing subscribers (campaign-targetable via the crm_group audience).
  • Orders — all orders across events with status tabs and search. The detail drawer shows line items, ticket codes and statuses, and the Stripe payment reference, with four actions: Resend confirmation, Transfer to a different email (repoints the order and resends the tickets), Open ticket page, and Refund (full refund through Stripe — the tickets are voided, their seats reopen for sale, and the waitlist notifier emails anyone waiting).
  • Settings — currency (defaults to the shop's), checkout hold minutes, the Stripe webhook signing secret, sender name/email overrides, an optional owner-notification address for every new order, and the wallet pass credentials (Google Wallet issuer id + service-account JSON; Apple Wallet pass certificate .p12 + password + Pass Type/Team IDs + Apple's WWDR PEM — all optional, the buttons only appear once configured).

Attendees export to CSV per event (code, type, attendee, buyer, status, check-in time, event date, order, paid) — spreadsheet-injection-hardened like every other export in the CMS.

Capacity & holds — why it can't oversell

Capacity is claimed when checkout starts, not when payment lands: starting a checkout atomically reserves the requested seats against the ticket type's counter, and the reservation expires together with the Stripe checkout session (default 35 minutes, configurable). Whichever of the webhook or the return page lands first completes the order — the other is a no-op. If checkout is abandoned, the expiry webhook (with a five-minute cron sweep as backstop) deletes the pending order and releases its seats. Two buyers racing for the last seat can never both win, and a paid order can never arrive to find its seats gone.

Stripe setup

Ticketing shares the site-wide Stripe keys from Settings → API Keys (the same pair Ecommerce, Booking, and Donations use). The only ticketing-specific piece is the webhook: create an endpoint in the Stripe dashboard pointing at /tickets/stripe/webhook (the Settings page shows the full URL), subscribe it to checkout.session.completed, checkout.session.expired, and charge.refunded, and paste its signing secret into Dashboard → Ticketing → Settings. Free tickets work with no Stripe configuration at all.

Gift cards, account credit & points

With Gift Cards and/or Loyalty enabled, the ticket picker gains a gift-card field, an account-credit toggle and a points field, under the promo-code input. Credits and a promo code are mutually exclusive — applying a promo stands the credits down with a message saying so (Stripe allows one discount per session, and keeping the same rule here means one rule to explain). Credits are measured against the selected tickets' total, ride into Stripe as a one-off coupon, and are only captured once the order completes; a full refund returns them. A 50c minimum stays card-payable, so a fully covered order still passes through Stripe rather than taking the free-ticket path. See gift-cards.md.

The member account page

Signed-in visitors get /members/event-tickets (the Event Tickets pill in the account nav, plus a card on the account dashboard). It lists every completed or refunded ticket order bought with the account's email, newest event first, and links each one to the tokenized order page — which stays the only place tickets, QR codes and wallet passes render. Abandoned pending orders are excluded. See memberships.md → The account area.

Waitlists

When a ticket type sells out, the widget offers Join waitlist (name + email, per occurrence for recurring events, idempotent per email). Whenever seats come back — a refund voided tickets, an abandoned hold expired, or the team raised the capacity — the tickets:notify-waitlist sweep (LazyCron, every 5 minutes) emails everyone waiting, exactly once each, with a link back to the event page. First come, first served: the waitlist notifies, it doesn't reserve.

Limitations

  • Recurring events share one capacity pool across occurrences. A capacity-50 ticket type on a weekly class means 50 tickets total, not 50 per week. The chosen occurrence is stamped on each order for the attendee list and calendar invite.
  • QR codes live on the linked ticket page, not inside the email — most email clients can't render them inline. The email's button (and the codes it lists) always lead back to the ticket page.
  • Refunds are full-order from the dashboard UI. Partial refunds issued directly in Stripe are still recorded correctly (the order shows the refunded amount) but don't void tickets until the full amount is refunded.
  • Event pages materialized before this addon shipped don't have the tickets section yet — add the Event Tickets row from the design library (Page Layouts → Detail) to the event detail page once via the page editor. Newer event-detail layouts include the section automatically.
  • Camera scanning needs HTTPS and a BarcodeDetector browser (Chrome/Edge/Android). Elsewhere, the per-ticket QR still opens the check-in page in any camera app, and the scan page's manual entry always works.
  • Apple Wallet passes require an Apple Developer account (a Pass Type ID certificate); Google Wallet requires a Wallet Console issuer account. Both are bring-your-own-credentials — without them the ticket page simply doesn't show wallet buttons.
  • Promo discounts collapse the Stripe line items into one discounted line (Stripe won't accept negative lines); the itemized breakdown stays on the order and the tickets.