GatePass — Event Tickets & QR Check-in for WooCommerce

Sell event tickets through WooCommerce. Every paid attendee receives a unique ticket ID and a cryptographically-random QR code by email (HTML + PDF), visible in My Account → My Tickets. Staff check people in with a mobile scanner; every QR is validated server-side, once, even when several gates scan the same code at the same instant.

The plugin lives in gatepass/. Zip that folder to install.

Table of contents

  1. Requirements
  2. Installation
  3. Five-minute setup
  4. How the lifecycle works
  5. Admin screens
  6. Shortcodes & URLs
  7. Staff roles
  8. Scanner
  9. Refunds, cancellations, resends
  10. Security model
  11. Database
  12. Testing report
  13. Known limitations
  14. Roadmap
  15. Troubleshooting

Requirements

Minimum
WordPress6.2
WooCommerce8.0 (HPOS and Cart/Checkout Blocks supported and declared)
PHP7.4 (tested on 8.1 – 8.3)
MySQL / MariaDB5.7 / 10.3 (GET_LOCK, IFNULL)
HTTPSRequired for the camera scanner (browsers only expose getUserMedia on secure origins)

No Composer dependencies. QR and PDF generation are implemented in-plugin (no external services, nothing leaves your server).

Installation

  1. Download / zip the gatepass directory.
  2. Plugins → Add New → Upload Plugin → activate. (Requires WooCommerce active; the plugin refuses to boot otherwise.)
  3. Activation creates 5 tables (see Database), two roles (Event Ticket Staff, Event Manager), grants all gatepass_* capabilities to Administrators, and schedules an hourly ticket-expiry cron.
  4. Go to Tickets → Settings and set organizer name, support email and brand colours.

Deactivation keeps all data. Uninstall keeps all data unless you tick Delete ALL ticket data when the plugin is uninstalled in Settings.

Five-minute setup

  1. Tickets → Events → Add event — name, ticket-ID prefix (e.g. GRBGRB-2026-000123), date, venue, capacity, banner.
  2. Tickets → Ticket Types → Add ticket type — pick the event, name (Regular / VIP / Family…), price, quantity, people per ticket, early-bird price + quantity + window, per-customer max, validity window, features, terms, custom attendee fields. Saving creates (or updates) a virtual WooCommerce product with stock management enabled. You can also link an existing product by ID.
  3. Put [gatepass_events] on a page (or just sell the WooCommerce product normally).
  4. Tickets → Staff — create scanner accounts. Give staff the scanner URL: Tickets → Scanner in wp-admin or a page containing [gatepass_scanner].
  5. Do one test purchase. Check Tickets → All Tickets, the email, the PDF, then scan it.

How the lifecycle works

Admin creates Event  →  Admin creates Ticket Type  →  WooCommerce product synced
  →  Customer buys (cart/checkout/payment: 100% WooCommerce)
  →  Order reaches Processing/Completed (configurable)
  →  gp_tickets rows created (one per attendee, or one per group ticket)
  →  128-bit random token + human ticket ID per row
  →  QR = https://site/?gp_verify=<token>   (no personal data in the QR)
  →  Email (HTML + PDF attachment), email_status recorded
  →  Customer sees tickets in My Account → My Tickets (QR, PDF, status)
  →  Staff scans → POST /wp-json/gatepass/v1/check-in
  →  Atomic UPDATE … WHERE status='active'  →  exactly one scan wins
  →  Ticket = Used; every attempt logged in gp_checkins
  →  Refund / cancel → tickets Refunded / Cancelled → QR rejected
  →  Admin sees everything in All Tickets → ticket detail (scan log + history)

Idempotency. Generation runs under a MySQL GET_LOCK('gatepass_gen_<order_id>') and compares existing rows per order item against the paid quantity, so duplicate webhooks, page refreshes, or processing → completed transitions never create duplicates.

Early bird. Price is decided server-side when the item is added to the cart and re-asserted on every cart calculation (woocommerce_before_calculate_totals), using sold + early_qty and the date window. Add-to-cart validation blocks quantities beyond remaining inventory, event capacity, or per-customer limits (counted across previous paid orders for logged-in users / same billing email).

Group tickets. Ticket type option One QR admits the whole group (one row, people = N, optional partial check-in 2/4 → 4/4) or One QR per person (N rows).

Admin screens

ScreenWhat it does
DashboardSold / checked-in / unused / remaining / attendees / revenue / today's check-ins, hourly check-in chart, per-type table. Filter by event.
EventsCRUD, capacity, banner.
Ticket TypesCRUD; syncs WooCommerce product. Shows sold, early-bird remaining.
All TicketsSearch (name / email / phone / ticket ID / order / token), filter by event, type, status, date. Bulk: resend, cancel, block, reactivate, regenerate QR, mark checked-in, delete. CSV export. Create ticket manually (walk-in; optionally creates a completed WC order).
Ticket detailFull record, live QR, verify URL, actions, transfer/edit attendee, every scan attempt, full history log.
ScannerEmbedded mobile scanner.
Check-insChronological log of every scan attempt (valid and rejected), CSV.
ReportsSales by day, staff audit (scans / valid / rejected / first / last), top customers, CSV.
StaffCreate staff / manager accounts; revoke access.
SettingsPrefix, branding, generation statuses, refund-restore behaviour, attendee-name collection, email templates, PDF attach, scanner sound/vibration/auto-next, manual lookup, rate limit, expired acceptance, admin alerts, low-stock threshold, uninstall behaviour.

WooCommerce integration points: product edit screen gets a Ticket tab (links to type), order edit screen lists the order's tickets, My Account gets a My Tickets endpoint, order emails/thank-you page list ticket IDs.

Shortcodes & URLs

[gatepass_events]All published events with their ticket types (banner, date, price, early-bird badge, remaining bar, sold-out state).
[gatepass_tickets event_id="3"]Ticket types for one event.
[gatepass_my_tickets]Same as My Account → My Tickets, for a custom page.
[gatepass_verify]Public token lookup form.
[gatepass_scanner]Full-page mobile scanner (requires gatepass_scan).
/?gp_verify=<token>Public read-only status page (never changes state).
/?gp_download=<token>PDF ticket.
/?gp_qr=<token>SVG QR.
/my-account/my-tickets/Customer ticket wallet.
POST /wp-json/gatepass/v1/check-in`{tokencode, count?}{result, message, ticket}` — staff only, nonce + capability, rate-limited.
GET /wp-json/gatepass/v1/lookup?q=Manual lookup by ticket ID / order / phone / email — staff only.
GET /wp-json/gatepass/v1/stats?event=Live counters.
GET /wp-json/gatepass/v1/verify/<token>Public minimal status.

Staff roles

CapabilityAdministratorEvent ManagerEvent Ticket Staff
gatepass_scan (scanner, check-in, lookup, stats)
gatepass_manual_checkin
gatepass_manage_tickets (resend, cancel, block, transfer)
gatepass_view_reports
gatepass_manage_staff
gatepass_manage_events (events, ticket types)
gatepass_manage_settings
WooCommerce orders, prices, refunds

Staff never see full phone numbers in scan responses (masked), never see payment data.

Scanner

Refunds, cancellations, resends

EventResult
Order → Cancelled / Failed / Pending / On-hold (from a paid status)All order tickets → Cancelled. QR rejected. Stock freed by WooCommerce.
Full refundAll tickets → Refunded.
Partial refund of N itemsThe N most recent unused tickets for that item → Refunded (used tickets are never voided by a partial refund).
Refunded/cancelled order restored to Processing/CompletedSetting reactivate originals (default off): re-issues new tickets with new tokens so an already-leaked QR stays dead; on: flips originals back to Active.
Resend / Regenerate QRResend reuses the same ticket + token. Regenerate issues a new token (old QR dead) and emails it. Never creates a new ticket row.
Email failureemail_status = failed, logged, admin alerted (if enabled), ticket still valid and visible in My Account; resend from admin.
Validity window passedHourly cron marks Active tickets past valid_untilExpired (or the scanner rejects live if cron is late).

Security model

Database

TablePurpose
{prefix}gp_eventsEvents (name, code, dates, venue, capacity, media, status).
{prefix}gp_ticket_typesTypes (pricing, early bird, inventory, limits, group mode, validity, custom fields JSON, product link).
{prefix}gp_ticketsOne row per ticket. Unique ticket_code, unique token. Indexed on order, item, event, type, status, email, phone, created_at.
{prefix}gp_checkinsEvery scan attempt (result, reason, staff, IP, device).
{prefix}gp_logsAudit trail (generated, emailed, resent, cancelled, refunded, transferred, regenerated, deleted…).

Schema is versioned (gatepass_db_version) and upgraded with dbDelta on load. Orders are only ever accessed through wc_get_order() / WC_Order CRUD — HPOS safe.

Testing report

Run php tests/run.php (no WordPress needed). Current result: 28 passed, 0 failed.

AreaTestResult
QR encoderPayloads 15 → 540 bytes (versions 2 → 18) encoded, rasterised with rsvg-convert, decoded with zbarimg; byte-exact round trip
QR encoderDeterministic output
Tokens5,000 tokens unique, 32 lowercase hex
Scanner payload parsingRaw token, ?gp_verify= URL, REST verify URL, upper-case token, garbage
PDFValid header/trailer, xref offsets, stream length, parsed by pdfinfo, rendered by pdftoppm (visual check of layout + QR)
SettingsDefault generation statuses = processing+completed; keep data on uninstall
PHPphp -l on every file; PHP 8.1

Behaviours verified by code review / design (require a live WooCommerce install to exercise end-to-end):

ScenarioHandling
Duplicate order hooks / refreshPer-order GET_LOCK + per-item quantity diff → no duplicates
Two devices scan same QR simultaneouslySingle conditional UPDATE; loser gets already_used with first scan's time + staff
Multiple tickets in one orderN rows, N tokens, N QRs (or one group row)
Early bird exhausted mid-cartPrice re-evaluated on every totals calculation, so the cart shows regular price before payment
Inventory / capacity exhaustedAdd-to-cart and checkout validation + WooCommerce stock
Refund / cancelTickets voided; scanner rejects with reason
Camera deniedClear message + manual lookup remains available
Invalid / expired / wrong-event QRRejected with specific reason, logged
Staff without capabilityREST 403; admin pages hidden
Email failureStatus tracked, resend available, ticket still valid
Cache / CDNVerify + download endpoints send Cache-Control: no-store; REST is uncached by WordPress
Thousands of ticketsIndexed tables, paginated admin (50/page), counters via aggregate SQL

Not yet exercised in a live store in this iteration: WooCommerce Blocks checkout attendee-field capture (classic checkout fields are used; Blocks users get attendee names from billing details), real SMTP delivery, and a physical-camera scan. See Known limitations.

Known limitations

Roadmap

P1 (next): attendee fields in Checkout Block via the Additional Fields API · per-event staff assignment · Apple/Google Wallet passes · WhatsApp/SMS delivery (Twilio) · bulk ticket import · seating charts. P2: rotating QR in the customer wallet page · offline PWA with signed manifests and conflict resolution · device fingerprint anti-fraud · affiliate tracking via WooCommerce coupons · analytics dashboards.

Troubleshooting

SymptomFix
Tickets not generatedCheck Settings → Generate tickets when order becomes includes the status your gateway uses. Order note shows GatePass: generated N tickets.
Camera doesn't startSite must be HTTPS; check browser permission; use manual lookup meanwhile.
rest_cookie_invalid_nonceStaff session expired — reload the scanner page.
Emails not arrivingConfigure an SMTP plugin; check ticket email_status and resend.
QR in PDF blurryIt's vector — that's the viewer's preview. Print or zoom.