KiwiEvents
A complete event management and ticketing solution for WordPress. Create events, sell tickets (free & paid via WooCommerce), generate QR code tickets, scan at the door, track attendees, and view sales dashboards.
by KiwiEvents · github.com/richardmosqu/kiwi-events-plugin-wordpress · website
Install
No release zip yet. The repository archive installs, but the folder name will carry the branch suffix and updates will not flow:
wp plugin install https://github.com/richardmosqu/kiwi-events-plugin-wordpress/archive/refs/heads/main.zipA complete event management and ticketing plugin for WordPress. Create events, sell tickets (free or paid via WooCommerce), issue QR-coded tickets, scan them at the door, manage attendees, group reservations and promoters, and read sales dashboards — all inside wp-admin, with self-service portals for organizers and promoters on the front end.
| Plugin version | 2.2.0 — the Version: header and KE_VERSION must always match |
| Schema version | 2.6.0 (KE_DB_VERSION) — migrations run automatically on load |
| Requires | WordPress 6.0+, PHP 8.0+, MySQL 5.7+ |
| Optional | WooCommerce (required only for paid tickets) |
| License | GPL-2.0+ |
| Text domain | kiwi-events |
| REST namespace | ke/v1 |
Table of contents
- Features
- Installation
- Quick start
- Shortcodes
- Front-end URLs
- Settings reference
- Data model
- REST API
- Project structure
- Asset cache-busting constants
- Deployment
- Development notes
- Troubleshooting
- Companion plugins
- Changelog
- Support
Features
Events
Custom post type ke_event with start/end dates, venue and address, organizer, categories and tags, status (active, cancelled, postponed, …), a per-event hero background image, "extras" widgets, and an editable slug that keeps 301 redirects from every previous URL (wp_ke_event_slug_history).
Events are authored through a multi-step event builder in wp-admin rather than the default post editor, and can be duplicated in one click.
Tickets
Ticket types carry a price, a quota and an active/inactive flag. Every individual ticket is its own row with a unique sha256 code and a generated QR image. Tickets are issued automatically when a WooCommerce order completes, or through the plugin's own free-checkout path when the event has no paid types.
- Per-person purchase limits, enforced server-side.
- Configurable service fee (flat, percentage, or per-event override).
- Ticket PDFs built with TCPDF, with the QR embedded.
- Resend, cancel, or bulk-update tickets from the attendees screen.
Emergency attendees ("Ticket error")
An admin-only switch in Settings → Events → Asistentes de emergencia. When it is on, the Add Attendee dialog gains a third type, Ticket error, for repairing a sale that went wrong.
The ticket is issued in full — QR, PDF, email to the attendee, valid at the door — but it is not a sale:
- It never increments
quantity_sold, so it contributes nothing to the organizer's sold count, revenue, daily chart, check-in rate, activity feed, attendee list, CSV or PDF report. Every organizer-facing query carriesKE_Tickets::exclude_error_sql(). - Capacity does not apply. A sold-out type still accepts one, because the sale it repairs already happened. The admin's own ticket card shows the true head count (
51/50with a(+1)annotation); the organizer still sees50. - It is gated on
manage_options, notmanage_kiwi_events— staff and organizers can hold the latter, and this ticket exists precisely to stay out of the organizer's view. - Cancelling or deleting one does not hand a seat back, because it never took one.
Rows carry wp_ke_tickets.is_error, and their synthetic order uses payment_method = 'admin_error'.
Scheduled ticket sales & waitlist
Each event can declare when its ticket sales open — a date, a time, and the timezone that time is written in (defaults to the event timezone, then the site timezone). Until that moment the public event page replaces the whole purchase block with a large "Boletos disponibles a partir del (día) (hora)" notice and a live countdown.
- Visitors can leave their email to be notified. When the moment arrives, a cron sweep queues one "ya están a la venta" email per person with a link straight to the event.
- The gate is enforced on all three purchase paths — free checkout, WooCommerce add-to-cart, and the cart re-validation before payment — so the notice is never merely cosmetic.
- Sign-ups are deduped by a unique
(event, email)index, rate-limited per IP, and protected by a honeypot field. They are listed and exportable from Kiwi Events → Waitlist. - The open/closed decision is computed at render time, never from cron, so tickets appear on their own the first time the page is built after the opening moment. The countdown re-checks the server before reloading, so an edge-cached page can't strand a buyer.
Customer ticket wallet
The [kiwi_tickets_purchase] shortcode renders a logged-in user's tickets grouped by event across Upcoming / Past / Cancelled tabs, with a QR modal and a per-ticket PDF download. The PDF endpoint re-verifies ownership against wp_ke_orders.user_id on the server — the nonce establishes identity, never authorization.
Door scanner
A public scanning page with live camera capture (jsQR) and REST validation. Access is gated by a per-organizer password, and the UI moves through a strict sequential state flow (pick event → authenticate → scan → result) so a scanner left open can't drift into an ambiguous state. The camera stream persists between scans instead of being torn down each time.
Group reservations
Table/group reservations per event, running in parallel with ticket sales. Each event can be tickets-only, reservations-only, or hybrid. Reservations are either auto-confirmed or held for venue approval, with availability checks, a no-show cron, and a wp-admin page that exports to CSV and PDF.
Organizers
Taxonomy-backed organizers get:
- A self-service dashboard at
/organizer/{slug}(password-authenticated, no WordPress account needed) with sales, attendees, reservations, highlights and email templates. - A public profile at
/organizers/{slug}. - Statistics and a downloadable PDF sales report.
Promoters
Full referral system: URL attribution via ?ke_promo, click tracking, commissions with a configurable refund policy, a promoter portal at /promoter/{slug}, promoter lists with CSV import, notification emails, an admin audit trail, and a visible attribution surface (badge, checkout summary line, email paragraph, WooCommerce order meta).
Highlights (event stories)
CPT ke_highlight — Instagram-style stories attached to an event. Organizers manage them from their dashboard through a REST CRUD that enforces ownership, with hardened image uploads. They render as a hero row on the public event page and open in a full-screen viewer.
Community board
CPT ke_board_event — activities submitted by logged-in users, with no ticketing attached. Submissions go through admin moderation; approved items appear in a public carousel with likes (wp_ke_board_likes, unique index + GET_LOCK rate limiting), native comments restricted to logged-in users, and a configurable "Trending" label.
Testimonials
Per-event testimonials built on native WordPress comments, with a freshly-fetched REST nonce so submissions survive full-page caching.
Emails
Confirmation emails with ticket PDFs attached, a send queue (wp_ke_email_log), per-organizer templates, admin notifications, and a test-send button in Settings.
Dashboards
Sales, revenue and attendee charts in wp-admin, plus an opt-in dark mode for Kiwi admin pages driven by --kiwi-* design tokens.
Installation
- Copy the plugin folder to
wp-content/plugins/kiwi-events/— including thevendor/directory (see the warning below). - Activate KiwiEvents in wp-admin → Plugins. All tables (
wp_ke_orders,wp_ke_tickets,wp_ke_reservations, the promoter tables, …) are created and migrated automatically, keyed onKE_DB_VERSION. - (Optional) Activate WooCommerce for paid tickets — the integration registers itself whenever WooCommerce is active.
- Configure Events → Settings (see Settings reference).
⚠️ vendor/ is mandatory in production
The Composer dependencies — TCPDF (ticket and report PDFs) and chillerlan/php-qrcode — are committed to the repository on purpose. Managed hosts such as WordPress.com do not run Composer, so the plugin must be deployed with vendor/ bundled. Without it, every ticket PDF (wallet download or email attachment) fails with tcpdf_missing.
To regenerate them in development:
composer install --no-dev
composer.json must not reintroduce a classmap over includes/, admin/ or public/. The plugin loads its own classes with require_once, and a self-classmap causes "class already in use" fatals against the class_exists() guards in the FPDF-based reports.
⚠️ Deploy by Replace, never by Delete
Do not delete the plugin to reinstall it. An earlier uninstall.php dropped every table and metadata row on deletion; it has been removed and must never come back, but the habit still matters on hosts that run uninstall routines. Upload the new ZIP and choose Replace current with uploaded.
Quick start
- Events → Add New Event. Fill in title, dates, venue, description and featured image in the builder.
- Add ticket types (free or paid) with prices and quotas.
- Assign an organizer and an event category.
- Publish, then place
[kiwi_events]on a page to list events and[kiwi_tickets_purchase]on a "My tickets" page. - Set the organizer's scanner password (Organizers screen) and open the scanner page on a phone at the door.
- Buy one test ticket end-to-end and confirm the confirmation email arrives with the PDF attached.
Shortcodes
| Shortcode | Description |
|---|---|
[kiwi_events] |
Main event listing / carousel |
[kiwi_event] |
A single event |
[kiwi_checkout] |
Ticket checkout |
[kiwi_events_carousel] |
Event carousel, filterable by category |
[kiwi_events_list] |
Plain event list |
[kiwi_events_calendar] |
Month calendar of events |
[kiwi_organizers] |
Organizer directory |
[kiwi_tickets_purchase] |
Customer "My tickets" wallet (login required) |
[kiwi_board] |
Public community-board carousel (approved activities) |
[kiwi_create_board] |
Board submission form (login required; moderated) |
[kiwi-promoter-dashboard] |
Promoter mini-dashboard |
Front-end URLs
| Path | Purpose |
|---|---|
/organizer/{slug} |
Organizer self-service dashboard (password) |
/organizers/{slug} |
Public organizer profile |
/promoter/{slug} |
Promoter portal |
/board/{slug} |
Single community-board activity |
?ke_promo={code} |
Promoter attribution, persisted in session |
These rely on rewrite rules. register_post_type() only adds rules in memory — WordPress persists them on an explicit flush, which normally happens only on activation, and plugin updates never re-run activation. Bump KE_REWRITE_VERSION whenever you add a CPT or change a slug; the init priority-99 guard then flushes exactly once per version (and self-heals if the board rules go missing despite the version saying otherwise).
Settings reference
Events → Settings has nine tabs:
| Tab | What lives there |
|---|---|
| General | Core behaviour and access control (login/register URLs, logged-out message) |
| Payments | Service fee, currency handling, WooCommerce wiring |
| Emails | Templates, sender, admin notifications, test send |
| Events | Per-person ticket limits, expiry behaviour, extras |
| Organizers | Organizer defaults and scanner passwords |
| Promoters | Commission rates and refund policy |
| Categories | Event category management |
| Board | Community board on/off, moderation, "Trending" label |
| Advanced | Diagnostics and maintenance |
Data model
Post types & taxonomies
| Object | Type | Notes |
|---|---|---|
ke_event |
CPT | The event itself |
ke_highlight |
CPT | Event stories, owned by an organizer |
ke_board_event |
CPT | Community activity, moderated, no ticketing |
ke_organizer |
Taxonomy | Organizers |
ke_event_category |
Taxonomy | Event categories |
ke_event_tag |
Taxonomy | Event tags |
Tables
| Table | Contents |
|---|---|
wp_ke_ticket_types |
Ticket types per event (price, quota, active flag) |
wp_ke_orders |
Orders, including user_id — the authorization source of truth |
wp_ke_tickets |
One row per individual ticket (unique code, status, check-in) |
wp_ke_reservations |
Group/table reservations |
wp_ke_waitlist |
Ticket-sales waitlist sign-ups (unique index per event+email) |
wp_ke_event_slug_history |
Previous slugs, powering 301 redirects |
wp_ke_email_log |
Email queue and send log |
wp_ke_board_likes |
Board likes (unique index per user+post) |
wp_ke_promoters |
Promoter records |
wp_ke_event_promoters |
Promoter ↔ event assignments |
wp_ke_promoter_clicks |
Referral click tracking |
wp_ke_promoter_commissions |
Earned commissions and refund state |
wp_ke_promoter_lists / _list_members |
Promoter lists and membership |
wp_ke_promoter_admin_audit |
Audit trail for admin actions on promoters |
Migrations are driven by KE_DB_VERSION and run from KE_Activator::maybe_upgrade() on plugins_loaded, so existing installs pick up new columns without a manual reactivate. A failed migration surfaces as an admin notice.
REST API
Namespace ke/v1 (one legacy promoter route lives under kiwi-events/v1). Grouped by area:
Events & tickets
GET /events
GET /events/{id}
POST /events/save
GET /events/check-slug
POST /events/{id}/duplicate
GET /events/{id}/attendees
POST /events/{id}/attendees/add
GET /events/{id}/checkin-stats
POST /events/{id}/ticket-types/{type_id}/toggle-active
GET /calendar-events
POST /checkout
GET /events/{id}/sale-status # public: is the scheduled sale open yet
POST /events/{id}/waitlist # public: join the waitlist (rate-limited)
GET /tickets/{id}
POST /tickets/{id}/update-status
POST /tickets/{id}/resend-email
POST /tickets/bulk
GET /tickets/validate/{code}
POST /orders/{id}/resend-admin-notification
GET /my-tickets/{id}/pdf # ownership-verified server-side
Reservations
GET /reservations
GET /events/{id}/reservations/availability
POST /admin/reservations/{id}/{approve|decline|check-in|cancel}
Organizers
POST /organizer/auth
POST /organizer/{slug}/logout
GET /organizer/{slug}/stats
GET /organizer/{slug}/activity
GET /organizer/{slug}/attendees
GET /organizer/{slug}/last-sale
GET /organizer/{slug}/reservations
POST /organizer/{slug}/reservations/{id}/{approve|decline|check-in|cancel}
GET /organizer/{slug}/export/csv
GET /organizer/{slug}/export/pdf
GET /organizer/{slug}/highlights
* /organizer/{slug}/highlights/{id}
GET /organizers/{id}/password-meta
POST /organizers/{id}/update-password
* /organizers/{id}/templates[/{tpl_id}]
Scanner
POST /scanner/auth
GET /scanner/active-events
Board & testimonials
POST /board/submit
POST /board/like/{id}
GET /events/{id}/testimonials
DELETE /events/{id}/testimonials/{comment_id}
Dashboard, settings & promoters
GET /dashboard/stats
GET /dashboard/chart-data
* /settings[/access|/board|/fees|/fees/{id}|/notifications|/ui]
POST /settings/test-notification
POST /promoter-login
GET /users/search
GET kiwi-events/v1/promoters/event/{id}/activity
Project structure
kiwi-events.php Bootstrap: constants, requires, boot sequence
includes/ Core: post types, tickets, orders, REST API (ke/v1),
WooCommerce, QR, PDF, email, promoters, reservations,
board, highlights
admin/ wp-admin pages (dashboard, attendees, reservations,
promoters, board moderation, event builder, settings)
admin/views/ One PHP view per admin screen
admin/css|js/ Admin assets (--kiwi-* design tokens)
public/ Front end: shortcodes, views, CSS/JS (--kep-* tokens)
templates/email|pdf/ Email and PDF templates
plugins/ Optional companion plugins (see below)
vendor/ Composer dependencies (TCPDF, php-qrcode) — deployed
Asset cache-busting constants
KE_VERSION moves slowly, so fast-iterating areas get their own cache-bust constant. This matters on WordPress.com's edge cache: bump the constant and purge the cache when you deploy CSS/JS.
| Constant | Area |
|---|---|
KE_SCANNER_ASSETS_VER |
Door scanner |
KE_BUILDER_ASSETS_VER |
Event builder (admin) |
KE_TOKENS_ASSETS_VER |
Admin design tokens (ke-admin-tokens.css) |
KE_ADMIN_CSS_VER / KE_ADMIN_JS_VER |
General admin CSS / JS |
KE_PORTAL_ASSETS_VER |
Promoter portal |
KE_WALLET_ASSETS_VER |
Customer ticket wallet |
KE_BOARD_ASSETS_VER |
Community board |
KE_WAITLIST_ASSETS_VER |
Scheduled-sales notice + waitlist form |
KE_REWRITE_VERSION |
Rewrite rules (triggers a one-time flush) |
There is also KE_PROMOTER_DEBUG — diagnostic logging for promoter attribution. Flip it to true only while investigating a missing-commission report; while enabled it leaves a banner in wp-admin so it can't be forgotten.
Deployment
composer install --no-dev
cd .. && zip -r kiwi-events.zip kiwi-events/ \
-x "*.DS_Store" -x "*/.git/*" -x "*/.agents/*" -x "*/.claude/*" \
-x "*/skills/*" -x "*/.vscode/*"
Then in wp-admin → Plugins → Add New → Upload Plugin, choose the ZIP and Replace current with uploaded. Bump the relevant asset constant and purge the host's edge cache if you changed CSS or JS.
Development notes
- PHP 8 + REST: never pass a PHP internal function as a string in a route's
validate_callback/sanitize_callback(is_numeric,absint, …). WordPress invokes them with three arguments and PHP 8 raisesArgumentCountError. Use a closure:static function ( $value ) { return is_numeric( $value ); }. - Front-end styling: use the
--kep-*tokens defined inpublic/css/ke-public.css; never hardcode Kiwi green on the front end. Every new public surface needs the WordPress.com button-override guard (follow the pattern inke-promoter-portal.css). - Admin styling: use the
--kiwi-*tokens fromadmin/css/ke-admin-tokens.css, which also carries the:root[data-theme="dark"]remap for dark mode. - Event dates: reuse
KE_Shortcodes::event_is_expired()— it is fail-open and handles the mixed formats stored in_ke_event_date_start/_end. Do not write new date comparisons. - Authorization: any endpoint serving ticket data must verify ownership against
wp_ke_orders.user_idserver-side. A nonce proves identity, not permission. - Event builder JS:
#ke-resv-bodyuses class-based show/hide plus an inline vanilla-JS fallback handler, so the reservations panel still toggles if the main builder bundle fails to load. - Extras: stored in
_ke_event_extras; the template mapping renders testimonials last, always. - Audit/backfill tooling: ship read-only reports, never auto-charge or auto-delete buttons.
Troubleshooting
| Symptom | Fix |
|---|---|
| Plugin won't activate | Confirm PHP ≥ 8.0; check the PHP error log |
tcpdf_missing on ticket PDFs |
vendor/ was not deployed — re-upload with it included |
404 on /organizer/…, /board/… |
Bump KE_REWRITE_VERSION, or re-save Settings → Permalinks |
| CSS/JS changes not visible | Bump the area's asset constant and purge the host edge cache |
| QR codes not generating | Enable the GD or Imagick PHP extension |
| Emails not sending | Install an SMTP plugin (e.g. WP Mail SMTP) and use Settings → Emails → test send |
| Wrong version in the plugins list | The Version: header and KE_VERSION have drifted — set both |
| Testimonial comments rejected | Page cache served a stale nonce; the REST nonce refresh handles this — verify it is not stripped by an optimizer |
Companion plugins
plugins/ holds optional, separately-activated helpers:
ke-sold-audit— read-only sold-vs-attendees diagnostic. Reports discrepancies; it never writes.ke-yappy-fee-fix— Yappy gateway fee reconciliation, with its ownKE_FEE_DEBUGflag.
Changelog
Unreleased
- Emergency attendees ("Ticket error"): admin-only switch in Settings → Events. Issues a complete, scannable ticket that never counts as a sale and is invisible to the organizer, and that can be issued against a sold-out ticket type. New
wp_ke_tickets.is_errorcolumn (KE_DB_VERSION→ 2.8.0). - Scheduled ticket sales: per-event opening date/time with its own timezone, configured in the event builder (step 3 → Venta programada). Stored in
_ke_event_sales_scheduleand owned byKE_Sales_Schedule. - Waitlist: public email capture on the pre-sale notice (
wp_ke_waitlist), a 5-minute release sweep (KE_Waitlist_Cron, hookke_waitlist_release_sweep), thetickets_on_saleemail template, and a read-only Waitlist admin page with CSV export. - Purchase gating added to all three choke points: free checkout,
KE_WooCommerce::add_to_cart(), andwoocommerce_check_cart_items. POST /checkoutnow rejects a ticket type that does not belong to the postedevent_id(ticket_event_mismatch). The request body used to decide every per-event rule; the ticket-type row is the authority.KE_DB_VERSION→ 2.7.0 (createswp_ke_waitlist).
2.2.0
- Birthday extras reworked as an event extras widget (previously a step-3 CTA plus modal), rendering directly below the tickets.
- Highlights (event stories): new
ke_highlightCPT, REST CRUD with organizer-ownership checks, hardened uploads, an event-builder selector, a public hero row and a full-screen story viewer. - Community board: centralized and self-healing rewrite flush, email notifications, redesigned moderation queue.
- Scanner: public access with a per-organizer password, sequential state flow, persistent camera.
- Testimonials: fresh REST nonce so comments survive page caching.
- Sold-audit tool (
plugins/ke-sold-audit) — read-only sold-vs-attendees diagnostic. - Removed
uninstall.php, which wiped every table and metadata row when the plugin was deleted. - Aligned the
Version:header withKE_VERSION— the header had been frozen at 1.0.0 while the code shipped 2.x.
2.1.x
- Attendees/Reservations exports stream on
admin_initinstead of duringrender(). - Logged-out gates always resolve the login URL from Access Control settings.
- Per-event hero background image with a fixed darkening gradient.
- Customer ticket wallet
[kiwi_tickets_purchase], promoters admin, PDF/QR fixes.
2.0.x
- Community board
[kiwi_board]/[kiwi_create_board]with moderation. - Composer dependencies vendored: TCPDF 6.11.3 + chillerlan/php-qrcode 5.0.5.
Support
Questions, bug reports, feature ideas or anything else about the plugin — get in touch:
Richard Mosqueda · remosquedaf@gmail.com
Before writing in, it helps to include:
- Your WordPress and PHP versions, and the plugin version (
KE_VERSION). - Whether WooCommerce is active.
- The relevant entry from your PHP error log, if there is one.
- The steps that reproduce the problem.
Worth checking Troubleshooting first — the most common issues (missing vendor/, stale edge cache, unflushed rewrite rules) are covered there with fixes.
License
GPL-2.0+ — see http://www.gnu.org/licenses/gpl-2.0.txt.