Repository navigation
v2.0.0-rc.3
Pre-releaseHi.Events v2.0.0-rc.3
Important
Upgrading a self-hosted install from v1.x or an earlier release candidate? Back up your database, then pull and migrate. See Upgrading.
The third release candidate of v2. Features marked (Enterprise) need a licence for production use (see Enterprise features and licensing). Everything else is free to self-host.
What's new
Selling
- Reserved seating (Enterprise): build seat maps in a visual designer and let buyers choose their own seats, with best available, wheelchair spaces and companion seats.
- Box office (Enterprise): sell and check people in at the door from any browser. Take cash, card on Stripe Terminal readers or comps, with PIN sign-in for staff.
- Product purchases view: see who bought each product, with search, totals and an export, and filter orders by product.
- Refunds on manual orders: paid orders you created by hand can now be refunded.
- Grouped ticket emails: each email address gets one email with all of its tickets. Before, only the first ticket for a shared address was sent.
- Invoice PDFs support Chinese and Japanese, and are much smaller.
Event pages and embeds
- Lineup and schedule: speaker and artist profiles you reuse across events, stages, billing tiers and a drag-and-drop timetable, shown on the event page.
- Run sheets: import people and slots from CSV or XLSX, export to XLSX, or print.
- Customisable homepage sections: reorder, hide and rename the sections of event and organizer pages, and add text, gallery, FAQ, video and sponsor sections.
- New embeds: a button that opens tickets in a pop-up, a lineup, a schedule and an organizer events list.
- Saved widgets: change an embedded widget without touching your site, and see its views, orders and revenue.
- New widget editor with a live preview and install guides for HTML, WordPress, Webflow, Squarespace, Wix, Shopify, Framer and React/Next.js.
- More widget options: show only some products or categories, pin a date, apply a promo code or set the language. Affiliate, promo code and UTM parameters on your page carry through to checkout.
Teams and sign-in
- Team access controls: a read-only Viewer role, per-member switches for financials and refunds, and members limited to specific organizers or events.
- Two-factor authentication with an authenticator app, recovery codes and trusted devices. Admins can require it for everyone on the account.
- Social login with Google, Microsoft or your own OpenID Connect provider (Okta, Keycloak, Authentik and others).
- Enterprise single sign-on (Enterprise): connect an account to its own identity provider, verify your email domains and require SSO for your team.
API and integrations
- API keys for Zapier, CRMs and your own scripts: read-only or read and write, never more access than your own, with an expiry you choose.
- In-app API reference under Settings → API keys, with error codes, rate limits and a backward-compatibility promise.
Around the app
- One settings area at
/settingsfor your profile, security, team, API keys and account, with an unsaved-changes bar. - Redesigned event creation, homepage and ticket designers, organizer and event homepages, products table and date picker. Modals go full screen on phones.
- First day of the week setting for organizers, used by every calendar (thanks @cornelisonc).
- Japanese translation (thanks @Tt0x).
Security and self-hosting
- Security fixes for cross-site request forgery, cross-origin API reads, stored XSS in email previews and more, affecting earlier self-hosted versions. See Security fixes.
- Spam and abuse protection: rate limits on sign-up, login, orders and promo codes, optional Cloudflare Turnstile, and stricter checks on names.
- Superadmin dashboard rebuilt, with platform commission, top events and accounts per currency, recent signups and a "needs attention" row.
- Email delivery tracking: every sent email is recorded, and with Postmark you can see deliveries, bounces, opens and clicks.
Upgrading
Hi.Events Cloud is upgraded for you. On a self-hosted install, back up, pull and migrate. New settings are optional and only needed for the features that use them.
From v1.x
Go straight to this release: you don't need to install rc.1 or rc.2 first. v2 needs PHP 8.3 or higher with the intl and gd extensions, and Node 22 to build the frontend. The Docker images already include them.
- Back up your database. If something goes wrong, restore it and switch back to your previous version.
docker compose exec -T postgres pg_dump -U postgres -F c hi-events > hievents-backup.dump
- Pull and migrate.
- Docker from a clone of this repo (the README install):
git fetch --tags && git checkout v2.0.0-rc.3, thendocker compose up -d --buildindocker/all-in-one. Migrations run before the app starts. - Images from Docker Hub: release candidates aren't tagged
latest, so set your image tags tov2.0.0-rc.3, then rundocker compose pull && docker compose up -d. The all-in-one image migrates on start. With separate backend and frontend images, rundocker compose run --rm backend php artisan migrate --forcebeforeup -d. - Manual install: pull the code, run
composer install --no-dev --optimize-autoloaderandphp artisan migrate --forceinbackend/, build the frontend withyarn install --frozen-lockfile && yarn build(yarn, not npm), then restart your app server and queue workers.
- Docker from a clone of this repo (the README install):
- Add the new variables for any features you want, and check the After upgrading list.
Migrations take seconds on most installs and a few minutes with hundreds of thousands of attendees. On a busy site, stop the app before the backup (leave the database running) and let any separate queue worker empty its queue, so the backup holds every order. A restore loses anything written after the backup, so decide whether to roll back before the upgraded site takes new orders.
What changes for v1 installs:
- Each existing event becomes a single-date event. Buyers see no difference.
- Stripe Connect and VAT settings move from the account to each organizer. Existing connections carry over, so nobody has to reconnect.
- Every event gets a Default check-in list with a public link that shows only attendee names and check-in status. Check-in lists tied to one date become all-dates lists.
- API integrations: some endpoints and fields changed. See Breaking changes below and in the rc.1 notes.
From rc.1 or rc.2
Follow the same steps. Requirements haven't changed since rc.1.
After upgrading
- CORS. The API now only answers the site in
APP_FRONTEND_URL(and itswww.or bare-domain counterpart) by default. If the dashboard loads but API calls fail with CORS errors, fixAPP_FRONTEND_URLor list your origins inCORS_ALLOWED_ORIGINS. The all-in-one image isn't affected. - Proxies. Behind more than one proxy (Cloudflare in front of nginx, for example), list every hop in
APP_TRUSTED_PROXIES, such as127.0.0.1,172.16.0.0/12,cloudflare. Otherwise every buyer shares one rate limit. - All-in-one
docker-compose.yml. It only passes the variables it lists. If you keep your own copy, bring over the newenvironment:lines from the latest file, includingDB_CONNECTION=pgsql.APP_PLATFORM_SUPPORT_EMAIL,API_DOCS_ENABLED,OCCURRENCES_QUEUE_NAME,POSTMARK_WEBHOOK_SECRET,AWS_SESSION_TOKENand the spam check settings aren't listed, so add those yourself if you use them. - Scheduler and queue worker. Both need to be running (the all-in-one image runs them). They handle account deletion, recurring dates, SSO domain checks and API key expiry.
- Stripe webhooks should send
payment_intent.succeeded,payment_intent.payment_failed,charge.succeeded,charge.updated,charge.refunded,refund.created,refund.updated,account.updated,payout.paid,payout.updatedand, for box office card readers,terminal.reader.action_failed. - Several app servers? Use a shared cache such as Redis. 2FA, SSO and box office sign-ins are kept in the cache.
If a migration fails, the all-in-one container exits rather than start half-migrated, which looks like a restart loop. docker compose logs shows why. Fix the cause and run it again. If a migration stopped on a lock timeout, first drop any half-built index listed by SELECT indexrelid::regclass FROM pg_index WHERE NOT indisvalid;.
New environment variables
All optional. Add one only if you use the feature it controls.
| Variable | Default | What it does |
|---|---|---|
APP_LICENCE_KEY |
unset | Unlocks Enterprise features. development unlocks them for testing. |
SSO_* |
unset | Social login with Google, Microsoft or an OpenID Connect provider. See Single sign-on. |
TURNSTILE_SECRET_KEY / VITE_TURNSTILE_SITE_KEY |
unset | Cloudflare Turnstile on sign-up. Set both or neither. |
APP_TRUSTED_PROXIES |
* |
Proxies allowed to set X-Forwarded-For. cloudflare adds Cloudflare's ranges. |
APP_SSR_SHARED_SECRET |
unset | Set on backend and frontend when the SSR server can't be a trusted proxy. |
APP_REGISTER_RATE_LIMIT_PER_HOUR, APP_PUBLIC_ORDER_RATE_LIMIT_PER_MINUTE, APP_PUBLIC_PROMO_CODE_RATE_LIMIT_PER_MINUTE, APP_PUBLIC_PAYMENT_INTENT_RATE_LIMIT_PER_HOUR |
10, 60, 10, 120 |
Per-IP limits on sign-ups, orders, promo code lookups and card payments. |
APP_SEAT_MAP_MAX_SEATS |
3000 |
Largest seat map that can be saved. |
VITE_COOKIE_CONSENT_ENABLED, VITE_COOKIE_CONSENT_DOMAIN, VITE_COOKIE_CONSENT_TEXT |
false |
Cookie consent banner. |
APP_EVENT_SPAM_CHECK_*, ANTHROPIC_API_KEY |
off | Checks newly published events for spam with Claude and holds suspicious ones for review. |
GOOGLE_MAPS_API_KEY |
unset | Address autocomplete. |
API_DOCS_ENABLED |
false |
Interactive API docs at /docs/api. |
OCCURRENCES_QUEUE_NAME |
unset | A separate queue for generating recurring dates. Add it to your worker's --queue= list. |
POSTMARK_WEBHOOK_SECRET |
unset | Postmark delivery tracking. |
AWS_SESSION_TOKEN |
unset | Session token for SES credentials. |
Also set APP_PLATFORM_SUPPORT_EMAIL to a real address, since the public /compliance endpoint now shows it.
In detail
Reserved seating (Enterprise)
Sell specific seats instead of quantities.
- Seat maps are built per organizer in a visual designer: rows and blocks (straight or curved, with aisles), tables, standing zones, a stage, labels, multiple areas, and seat numbering options. Start from scratch or from a template. Mark seats as wheelchair spaces, add notes, and group seats into price bands.
- Attach a map to an event and link each price band to one or more ticket types. The event gets its own copy of the map, so later edits to the venue map are reviewed and synced, never applied silently. Seats that have been sold can't be removed or unlinked.
- Buyers pick seats on an inline map, a full-screen picker, or an accessible list view that works with the keyboard. "Best available" finds adjacent seats. Standing zones only ask how many. Companion seats can only be bought together with a wheelchair space.
- Seat rules per event: a maximum number of seats per order, a rule that refuses to leave single empty seats stranded, and whether buyers may change their own seat later.
- Holds use the normal checkout reservation. A seat taken by someone else while choosing is reported and can be replaced. If a hold runs out, the seat goes back on sale; a payment that lands afterwards still completes if nobody took the seat, and is refunded if someone did.
- Recurring events use one map with availability per date.
- For organizers: a live seat sales map, holding seats back on one or several dates, moving an attendee to another seat, adding an attendee straight into a chosen seat, a seat sales report per price band, and printable sales and house maps.
- Seats show everywhere a ticket does: tickets, order and ticket emails (and a
{{ order.seats }}Liquid token), checkout summaries, check-in search and scans, the attendees table and the attendee export.
Seated tickets don't offer a waitlist. Maps are limited to 20 price bands and, by default, 3,000 seats (APP_SEAT_MAP_MAX_SEATS).
Box office (Enterprise)
Sell and check people in at the door from any browser.
- Every event gets a default box office, and you can add more, for example one per entrance. Each has its own URL, a 6-digit PIN shown once when it's created or reset, optional activation and expiry dates, and can be limited to specific products or a single date. A box office without a PIN can't be opened by staff.
- Staff sign in with the PIN. Five wrong PINs lock the device out for 15 minutes, and resetting the PIN ends every open staff session. A signed-in organizer can open the door without the PIN and hand it over to staff. Sessions last up to 24 hours.
- Selling: tap products into a cart, take cash (with change due), card, comp, or a payment taken elsewhere with a reference. Free tickets complete without a payment step. Each box office decides whether staff can apply discounts, override prices and ask checkout questions.
- Card payments run on Stripe Terminal readers (Reader S700, S710 or WisePOS E) paired under the organizer's payout settings. A reader collecting a payment for another live sale is never taken over; the second device can wait, pick another reader or take cash. Readers never ask for a tip.
- Walk-up buyers don't need an email address. If they give one, tickets are emailed. Sales without one can be renamed later from the dashboard.
- Scan tab checks existing attendees in, and Orders tab finds, reopens, voids and resends door sales. A sale in progress survives a reload, and one left open on another device can be abandoned from the Orders tab.
- Seated events: set a ticket count and let best available seat the party together, or tap seats on the door map. Held-back seats need a confirmation before they're sold.
- Reporting: a sales summary per box office by payment type, operator and day. Door sales are labelled in the orders table and can be filtered by operator, and the orders export gains Sales Channel, Tender, Operator, Amount Tendered, Change Due and Reference columns.
Card readers need the Stripe webhook event terminal.reader.action_failed (see After upgrading).
Team access controls
Give staff access to only what they work on.
- Roles are now sets of permissions: Admin, Organizer and a new read-only Viewer. Per Organizer or Viewer, admins can turn can see financials and can issue refunds on or off. Admins always have full access.
- Restrict an Organizer or Viewer to specific organizers or events. An organizer grant covers all of its events, including future ones.
- Members without financial access don't see revenue, order totals, prices or money in stats and reports.
- Read-only members don't get the edit and create controls, and the API refuses their changes with
403 READ_ONLY_ROLE. - A Team tab on every event and organizer shows who has access, and lets you invite someone straight into that scope. Members can opt in to new-order emails (off by default), and the owner is emailed when someone else adds or invites an admin.
- The team page was redesigned with search, a role filter, and each member's access, 2FA status and last activity.
Existing members keep whole-account access with their current role. Check-in list links stay shareable door credentials, so a Viewer with the link can still check people in.
Two-factor authentication
- Turn on authenticator-app 2FA under Settings → Security, with 10 recovery codes and an option to trust a device for 30 days. You're emailed when 2FA is turned on or off, and when a recovery code is used.
- Account admins can require 2FA for every member of the account from Settings → Team. Members who haven't set it up are asked to before they can do anything else.
- Admins can reset 2FA for a member who belongs only to their account; superadmins can reset anyone. On a self-hosted install you can also run
php artisan user:reset-two-factor {email}. - Ten wrong codes lock 2FA sign-in for 15 minutes.
Single sign-on
Social login (core). Sign in, sign up and accept invitations with Google, Microsoft, or one OpenID Connect provider of your choice (Okta, Keycloak, Authentik, Zitadel and others), configured through SSO_* environment variables. The redirect URI to register with each provider is <APP_FRONTEND_URL>/sso/callback. Link or unlink providers under Settings → Security. Local 2FA still applies. Run php artisan sso:status to check your setup; see docker/all-in-one/README.md for a walkthrough.
- Provider requests are limited to public HTTPS addresses, for this provider and for enterprise connections. Set
SSO_ALLOW_PRIVATE_ISSUERS=trueonly on a single-tenant install with an internal identity provider (an internal Keycloak, for example), where you trust every account admin. SSO_PASSWORD_LOGIN_DISABLED=trueturns off password sign-in for everyone except superadmins. Before you set it, make sure people can get in: they must have linked single sign-on from their profile, or the provider must vouch for their email (SSO_OIDC_TRUST_EMAIL=truefor your own OIDC provider). If the provider is down, unset the variable and restart to let people back in with their password.
Enterprise connections (Enterprise). Each account can connect its own identity provider (Okta, Microsoft Entra ID, Google Workspace or any OIDC provider) under Settings → General → Single sign-on, with a guided setup and a test sign-in. Included in every Enterprise licence.
- Verify your email domains with a DNS TXT record. People on a verified domain are sent to your provider from the "Sign in with SSO" button, and can be added to the account on first sign-in. Domains are re-checked daily, so this needs the scheduler, a queue worker and outbound DNS. A domain can belong to only one account;
php artisan sso:verify-domainmoves a disputed domain on a self-hosted install. - Require SSO for the account, with a preview and per-member exemptions. It needs an active connection, a verified domain and 2FA on the owner. Turning it on signs affected members out everywhere and deletes their API keys for that account; they can't reset a password, and nobody can sign up with an email on your domains. Members the connection can't sign in yet, such as people who own another Hi.Events account and haven't connected it, keep their password sign-in, sessions and API keys; the preview and the Team page both show them as Password. Sessions through the connection last 12 hours by default (15 minutes to 7 days).
- Connect it to your own login. Any active member, the owner included, can connect the account's connection under Settings → Security after confirming their password. The owner can then sign in through the identity provider day to day. Connecting never adds anyone to an account or changes their role. A member who owns another account is covered by Require SSO once they connect it.
- Platform superadmin logins can't connect to, sign in through, or confirm their identity with an account's connection; they keep password sign-in.
- If you change the provider's issuer, client ID or endpoints, the client secret must be entered again, so the stored secret is never sent to a new provider.
- The owner is never required to use SSO and can always sign in with password and 2FA as a break-glass; every admin is emailed when they do.
- Client secrets are encrypted with
APP_KEY. If you rotate it, keep the old key inAPP_PREVIOUS_KEYS, or every enterprise sign-in breaks. php artisan sso:unlink-identityremoves a person's single sign-on links (for a compromised or replaced identity provider account) and signs them out everywhere.- Removing someone from your identity provider doesn't remove them from the team or revoke their API keys. Remove them under Settings → Team as well.
API keys and an in-app API reference
- Create API keys under Settings → API keys for Zapier, CRMs and your own scripts. A key acts as you, in one account, with your role and access, and never more. Keys can be read-only or read and write, and expire after 7 days to a year (90 days by default), or never. Creating one needs a verified email and confirming your identity. View-only members can only create read-only keys, and each member can have up to 25.
- Keys can't manage sign-in, the team, payouts, Stripe Connect, card readers, VAT settings, API keys or account settings (
403 API_KEY_NOT_ALLOWED). The API reference marks these endpoints "Not available to API keys". - A key stops working when you revoke it, an admin revokes it (you're emailed), you leave the account, or your password is reset. Removing a sign-in method from your profile revokes your keys in every account. Turning on Require SSO, or changing, switching off or deleting an enterprise connection, revokes the keys for that account.
- You're emailed when a key is created, and 7 days before it expires for keys that last 14 days or more.
- The full key is shown once. Only a hash and the last four characters are stored. Keys share the per-user API rate limit (180 requests a minute by default).
- Settings → API keys → API reference shows the documented API inside the app, including error codes, rate limits and a backward-compatibility promise for the endpoints keys can call. It works without
API_DOCS_ENABLED.
Lineup and schedule
For festivals, conferences and club nights.
- People: organizer-level profiles for speakers, artists and hosts (photos, tagline, bio, links), reused across events from a new People page.
- Event lineup and timetable: billing tiers, role labels, hidden acts, stages or rooms, and a drag-and-drop timetable. Late sets stay on their night: schedule days run from 05:00 to 05:00.
- On the event page: a lineup grid and a schedule timeline with a stage filter, profile and session details, add to calendar, and
#session-<id>deep links. The lineup is added to the event's search engine data. - Run sheets: export to XLSX, print, or import people and slots from CSV, XLSX or pasted rows. Every row is previewed before anything is saved, and re-importing an export changes nothing.
- Order and ticket emails can include lineup highlights, also available as
lineup.*Liquid tokens. - Duplicating an event copies its lineup.
- Imports take up to 500 people and 1,000 slots at a time.
Customisable homepage sections
Event and organizer pages are now built from sections you can reorder, show, hide and rename: about, lineup, schedule, location and organizer, plus new text, gallery, FAQ, video (YouTube or Vimeo) and sponsor sections. The tickets section (events, on organizer pages) is always shown. Existing pages render exactly as before until you change them.
Saved widgets and new embed types
- New embeds alongside the ticket widget: a Button that opens tickets in a pop-up, Lineup, Schedule, and an organizer Events list (list, grid or compact, with a category filter and an optional past events tab).
- Saved widgets: save a widget per event or organizer, and later edits go live without touching your site. Each saved widget shows its views, orders and revenue, and orders record which widget they came from.
- More options: show only some products or categories, extra event sections, a pinned date for recurring events, a promo code, pop-up or new-tab checkout, and a language. Affiliate, promo code and UTM parameters on the host page are carried into checkout, and the host page receives checkout events for its own analytics.
- A rebuilt editor with a live preview and install guides for HTML, WordPress, Webflow, Squarespace, Wix, Shopify, Framer and React/Next.js. Sections hidden on the event page show a Hidden badge and can't be turned on in the widget, and sections with no content show an Empty badge, as in the homepage designer.
widget.jsis now compressed and cached, and loads iframes lazily. Existing embed snippets keep working.
Settings and design
- One settings area at
/settings: Profile and Security for you; General, Organizers, Team, API keys and Taxes and fees for the account. Forms show an unsaved-changes bar. Old/account/*and/manage/profileURLs redirect. - Every create and edit modal shares one layout with a sticky footer, and goes full screen on phones.
- Redesigned event creation, homepage and ticket designers, organizer and event homepages, the products table and the date picker.
- A purchases view on each product shows who bought it, with search, totals and an export. Orders can be filtered by product.
Security fixes
These affect earlier self-hosted versions.
- Cross-site request forgery. The login cookie was
SameSite=None, so a form on any website could make a signed-in organizer's browser send POST requests to the API. It is nowSameSite=Laxwhenever the frontend and API are on the same site. - Cross-origin reads. With
CORS_ALLOWED_ORIGINS=*(the old default), any website could read API responses as a signed-in organizer. See the CORS change below. - Stored XSS in the email template preview, where Liquid filters could build markup after purification. Rendered bodies are purified again and the preview is sandboxed.
- Ids scoped to their parent. Product prices, email templates, waitlist offer prices, order edits and manual-attendee tax ids are now checked against the event or account being edited.
- Invitations. Revoking an invitation no longer soft-deletes the invitee's whole user, and other accounts can no longer rename a user.
- Validation cost. Duplicate checks on large lists grew quadratically, so a single request could tie up the server. They are now linear.
- Server rendering mixed up visitors. Pages rendered at the same moment could be fetched with another visitor's sign-in or language, so, for example, an organizer's unpublished events could appear on a public organizer page served to someone else. Each request now has its own context. Affects server-rendered deployments up to rc.2.
- SSR logs no longer print request headers, which included login tokens.
- Clickjacking. App pages (sign-in, dashboard, settings, admin) and the homepage designer previews can no longer be framed by other sites, whatever the letter case or URL encoding of the path. Event, widget and checkout pages are unchanged.
Spam and abuse protection
- Sign-up is limited per IP address and per email (
APP_REGISTER_RATE_LIMIT_PER_HOUR, default 10), with optional Cloudflare Turnstile (TURNSTILE_SECRET_KEYandVITE_TURNSTILE_SITE_KEY). - Login is limited to 10 attempts a minute per email, and password resets to 5 an hour per email.
- Per-IP limits on order creation (
APP_PUBLIC_ORDER_RATE_LIMIT_PER_MINUTE, default 60), promo code lookups (APP_PUBLIC_PROMO_CODE_RATE_LIMIT_PER_MINUTE, default 10) and payment intents (APP_PUBLIC_PAYMENT_INTENT_RATE_LIMIT_PER_HOUR, default 120). - A payment is cancelled after 5 failed card attempts, and the buyer is asked to start a new order.
- Person names reject links, email addresses, markup and long digit runs. Names already stored stay editable.
- Emails for a draft or under-review event carry no event content unless every recipient is a verified member of the account.
For instance operators
- Superadmin dashboard rebuilt: platform commission, activity, top events and accounts split per currency, upcoming events by size, recent signups and a "needs attention" row. Account pages can impersonate users and schedule or immediately run an account deletion; flagged events can be deleted along with their account.
- Email delivery tracking. Every sent email is recorded (type and provider message id only, no recipient details). With Postmark you can receive delivery, bounce, complaint, open and click events at
/public/webhooks/email-delivery/postmark(POSTMARK_WEBHOOK_SECRET), shown on a new admin page and as a status on each message recipient. - Lifecycle emails that nudge stuck organizers are SaaS mode only and do nothing on self-hosted installs.
Everything else
- Grouped ticket emails. Each email address now gets one email listing all of its tickets. Previously, when several tickets in an order shared an address, only the first was emailed. Custom Liquid ticket templates still send one email per attendee.
- Refunds on manually created orders. Paid manual orders can now be refunded (recorded as offline refunds), and cancelling one no longer leaves its amount in gross sales.
- Double payments for the same order, for example from two browser tabs, are refunded automatically.
- Invoice PDFs render Chinese and Japanese characters, and are much smaller. Korean, Thai and Hindi are not covered yet.
- The webhook logs modal shows the latest deliveries instead of an arbitrary ten (thanks @zigman79).
- On phones, form fields no longer zoom the page when tapped, and quantity +/- buttons are always visible.
- Report date ranges, recurrence weekday and month names, status filters and social link names now follow the selected language. Sign-in pages no longer throw hydration errors in production builds.
- Stripe refunds. A second partial refund of the same amount within 24 hours (for example, refunding the second of two equal tickets) now goes through instead of sticking at Refund pending, and retrying a failed refund creates a new one. A refund Stripe is still processing stays Refund pending instead of showing as Refund failed, and a second refund can't start until it settles.
- Marked as paid emails attach the invoice marked Paid, not the earlier Unpaid copy.
- Change seat on a reserved-seating order page only appears when the change can be made: attendee self-service is on, the guest hasn't checked in, and the date hasn't started or been cancelled.
- The organizer setting Enable attendee self-service by default now saves.
- Composing from the Messages page asks you to choose recipients instead of failing, and Message Attendees from a ticket's menu opens with that ticket selected.
APP_CDN_URLnow works with cached config (php artisan optimize); before, image URLs fell back to the storage disk URL.- All-in-one image: generating recurring dates, exporting question answers and recording failed jobs no longer fail with a 500 when
DB_CONNECTIONisn't set (the job tables defaulted to MySQL). On older images, add- DB_CONNECTION=pgsqlto the service environment indocker-compose.ymlas a workaround. - API: creating or editing a question without
product_ids, or a tax or fee without adescription, no longer returns a 500, and an unknown questionbelongs_tovalue returns a 422 instead of a 500. - Translations that showed raw placeholders such as
{minutos}or{secondes}were fixed in Spanish, French, Portuguese, Vietnamese, Chinese and Slovak, along with a few mistranslations ("Conectar faixa" for Connect Stripe in Brazilian Portuguese). - Server-rendered pages now set
<html lang>to the visitor's language, and dates and prices are formatted in that language on the server too, so non-English pages no longer re-render on load. - Resetting a password now signs that user out of every session and revokes their API keys.
- Frontend builds are faster: a patched
pofileand SWC instead of Babel. - The Playwright suite now has 141 spec files, up from 73 in rc.2, and CI runs it in full against both a self-hosted and a SaaS-mode stack.
Breaking changes
CORS defaults to your frontend
An unset or * CORS_ALLOWED_ORIGINS now means "only the site in APP_FRONTEND_URL", instead of every site. The default also allows the www. or bare-domain counterpart of that URL, and https:// when APP_FRONTEND_URL is http://. If APP_FRONTEND_URL is empty, the old allow-everything behaviour is kept.
The all-in-one image serves the frontend and API from one origin and is unaffected. If your frontend is reachable on other origins (another subdomain or port), list them all in CORS_ALLOWED_ORIGINS, comma-separated. The embeddable widget is an iframe and is unaffected.
Login cookie
The token cookie is SameSite=Lax when the frontend and API are on the same site (the all-in-one image, example.com with api.example.com, localhost ports). Installs that serve them from different registrable domains keep a SameSite=None cookie so login keeps working, but don't get the CSRF protection; move them under one domain to get it.
API changes
- Offline payment instructions need Manage payments. Changing an event's
offline_payment_instructionsthroughPUTorPATCH /events/{event_id}/settingsnow needs the Manage payments permission (Admins by default) and a signed-in session. API keys, including an Admin's, and Organizer-role members get a422on that field when the request would change the saved instructions, including clearing them (aPUTthat leaves the field out clears it). Re-sending the saved instructions unchanged, or leaving the field out of aPATCH, works as before, so integrations that read the settings and write them back are unaffected. - Deprecated:
POST /events/{event_id}/attendees/{attendee_public_id}/check_in. It never recorded the check-in on a check-in list or sentcheckin.*webhooks; use the event's check-in lists instead. Its behaviour is unchanged, except thatcheck_outfor an attendee who was never checked in returns409instead of a500. - Removed
POST /events/{event_id}/orders/{order_id}/message. It could not work (it passed an order id where the message job expected a message). UsePOST /events/{event_id}/messages. - Attendee
emailcan benullin API responses and webhook payloads, for box office sales where the buyer gave no email. - Name validation is stricter on checkout, attendee edits, waitlist joins, sign-up and invitations: names containing links, email addresses,
< > @ { }or phone-number-like digit runs return422. - Password login with 2FA. When the user has 2FA on,
POST /auth/loginreturnstoken: nulland a challenge to complete atPOST /auth/login/two-factor. Scripts that sign in with a password should switch to an API key. - Session tokens only in the
Authorizationheader or the login cookie. A JWT passed as?token=in the URL or as atokenbody field is no longer accepted. Integrations should use an API key (Authorization: Bearer hie_…). - Session tokens can be revoked. A password reset, changes to a user's single sign-on links, and Require SSO revoke that user's existing sessions (
401 SESSION_REVOKED), and a deactivated team member is refused at once. Tokens issued before the upgrade stay valid. - Money fields are omitted, not
null, for members and API keys without can see financials: order and order item totals and prices, event and date sales stats, affiliatetotal_sales_grossand box officegross_sales. Ordershort_idalso needs edit access. Money filters and sorts are ignored. Reports, invoices, product purchases, VAT settings, the fee preview, webhook management and the order, affiliate and product-purchase exports return403 PERMISSION_REQUIRED. Existing members keep their access, so nothing changes until you restrict someone. - Other users' private fields.
pending_email,marketing_opted_in_at,has_passwordandhas_pending_email_changeare only returned for your own user. - Child ids are checked against their parent. An id that belongs to another event or account (a price, a product, an email template, a waitlist offer price, a tax) now returns
404or422instead of being accepted or silently ignored./users/{id}and/accounts/{id}only accept numeric ids. - Public check-in attendee detail (
GET /public/check-in-lists/{id}/attendees/{attendee}/detail) no longer returnsorder.total_grossororder.currency. - New
error_codevalues:SESSION_REVOKED,SSO_REQUIRED,SSO_SESSION_EXPIRED,TWO_FACTOR_CHALLENGE_EXPIREDandAPI_KEY_READ_ONLY(401);API_KEY_NOT_ALLOWED,READ_ONLY_ROLE,PERMISSION_REQUIRED,ACCESS_RESTRICTED,TWO_FACTOR_SETUP_REQUIRED,ACCOUNT_PENDING_DELETION,IMPERSONATION_NOT_ALLOWEDandFEATURE_UNAVAILABLE(403);INVITATION_SIGN_IN_REQUIRED(409); and the box office codesSESSION_EXPIRED,UNAVAILABLE,SALE_EXPIREDandREADER_UNAVAILABLE. The in-app API reference lists them all with their statuses. - Additive fields:
seat_uidandseat_labelon attendees,band_keyon order items,box_office_*fields on orders,has_seat_mapon events,price_before_discount_including_taxes_and_feeson public product prices,schedule_dayson public lineup entries, andis_checked_inon attendees in the public order payload. Webhook events are unchanged, and webhook payloads always carry full prices whoever triggered them.
Behaviour changes
- End dates default to the end of the day. Picking only a date in an end or expiry field (sale end, promo code expiry, check-in list and box office expiry, event or date end) now sets 11:59 PM instead of midnight, so the period runs through the chosen day. Start fields still default to midnight; saved values don't change.
- Stripe refund emails wait for Stripe. When "Send refund notification email" is ticked, or a cancellation also refunds, the buyer's email is sent once Stripe confirms the refund succeeded, not when it is requested. If the refund fails, no email is sent. Offline and manual refunds still email straight away. On self-hosted installs without Stripe refund webhooks (
refund.created,refund.updated) the email is no longer sent; those installs already leave Stripe refunds at Refund pending. - Webhooks are retried. A failed delivery (non-2xx, connection error or a timeout over 3 seconds) is retried twice more, after about 10 and 100 seconds, and each attempt shows in the webhook's logs. Delivery is now at-least-once and its order isn't guaranteed, so receivers should de-duplicate on
event_type, the payload id andevent_sent_at, which stay the same across retries. Very large payloads (over about 120 KB, for example an order with hundreds of attendees) are still sent once without retries. - Test messages only go to you. "Send as test" now emails only the sender for every recipient type, sends immediately even if "Schedule for later" is selected, and stays a test if it is held for review. Before, a test to a single buyer or to "Order owners with a specific product" also reached the real buyers.
is_testonPOST /events/{event_id}/messagesnow applies to everymessage_type. - Reactivating an attendee checks availability. Activating a cancelled attendee now checks the ticket's quantity (per date where set), its shared capacity and the date's capacity, and returns a
422when there is no room instead of overselling. - Draft event previews in an embedded widget. A signed-in organizer no longer sees a draft event through a widget embedded on another site, because the login cookie isn't sent there. Preview drafts in the app.
- Ordering on unpublished events is limited to members of the account that owns the event, and is refused while the event is under review. Waitlist joins and contacting the organizer need a live event.
- Email changes are confirmed at the new address, and the old address gets a notice. Changing your email needs your password or a fresh single sign-on, and members of an account that requires SSO can't move their email off its verified domains.
- Team invitations need a verified account. Someone who already has a Hi.Events login now signs in and joins (
409 INVITATION_SIGN_IN_REQUIRED) instead of setting a new password from the invitation. Invitations to an account that requires SSO go through single sign-on. Account and sign-in emails no longer greet people by name. - Settings moved to
/settings/*. Old/account/*and/manage/profilelinks redirect. - New-order emails to team members are opt-in per member, separate from the event's organizer notification setting.
- First day of the week defaults to Monday for every organizer. Organizers whose calendars should start on Sunday can change it in the organizer settings.
- Prices follow the app language. Prices are formatted in the visitor's selected language rather than the browser's locale.
- Event messages are marked Sent once the email has actually been sent, and Failed only after every retry.
- Exports. The orders export gains six columns at the end and the attendees export gains Sales Channel at the end. For seated events the attendees export also gains a Seat column before the question columns.
- Late payments. A payment that lands on an expired or cancelled order is still refunded and the buyer is still emailed, but it no longer sets the order's refund status, changes event statistics or sends the
order.refundedwebhook, because the sale was never counted. - Embedded widgets. Old snippets that hardcode
data-hievents-locale="en"withwidget-version="1.0"now follow the visitor's language. Widget iframes load lazily and aredisplay: block. - Every event has a box office row, created by migration for existing events. It does nothing until a PIN is set or an organizer opens it, and needs a licence to be used.
- Public compliance endpoint.
GET /complianceon the frontend (andGET /public/complianceon the API) returnsAPP_PLATFORM_SUPPORT_EMAIL, the licence status and whether the attribution terms are met. SetAPP_PLATFORM_SUPPORT_EMAILto a real address. - Google Tag Manager. On self-hosted installs, only account admins can add a new container, and API keys never can.
- Outbound links from organizer and event content are
nofollow ugc, and pages carrynoindexwhen indexing is turned off. Widget pages (/widget/*) sendX-Robots-Tag: noindex, so search engines don't list them separately from the page they're embedded on.
Enterprise features and licensing
Reserved seating, the box office and enterprise single sign-on connections are Enterprise features. Their code lives in backend/ee/ and frontend/src/ee/, ships in this repository, and is covered by ee/LICENCE rather than the AGPL. Using them for real events needs a licence key from https://hi.events/licensing, set in APP_LICENCE_KEY. A key unlocks the Enterprise features its plan includes, and also removes the "Powered by Hi.Events" notice. Development and testing are free: APP_LICENCE_KEY=development unlocks all three on a test install. Without a key they stay locked and nothing else changes.
Everything else in this release is core and free to self-host, including social login, two-factor authentication, team access controls and API keys. Without a licence, the "Powered by Hi.Events" attribution requirement in the root LICENCE is unchanged.
How licences work
- Licence status, expiry and the features in use are shown in a Licence dialog in the avatar menu, and on a new superadmin Licence page.
- A licence includes the features of its plan. Setting up a feature your licence doesn't include is refused, and the Licence dialog names what isn't included and how to add it.
- Licences that cover a yearly sales volume show it on the superadmin Licence page, next to the instance's net sales by currency for the current and previous licence year.
- A licence has a 30-day grace period after it expires. Once it lapses, setting up anything new for an Enterprise feature is refused with
403 FEATURE_UNAVAILABLE, and the setup pages show a "Setup is locked" notice. What is already set up can still be viewed and removed, and online ticket sales for events with reserved seating keep working. New box office sign-ins are refused, so open box office sessions end within 24 hours. - Enterprise SSO connections keep signing people in after a lapse, including where SSO is required, so you can wind them down. They can be switched off or deleted, but not changed or switched back on until the licence is renewed.
- The Licence dialog lists, for each Enterprise feature you use, what keeps working and what stops. The licence text in
backend/ee/LICENCEandfrontend/src/ee/LICENCEsays the same and names enterprise single sign-on as a licensed feature. - An invalid key is logged once a day rather than on every request.
What's Changed
✨ Features
- Box office and reserved seating by @daveearley in #1372
- Two-factor authentication by @daveearley in #1379
- Product purchases view and export by @daveearley in #1380
- Grouped ticket emails by @daveearley in #1386
- Superadmin dashboard overhaul, account and spam deletion actions by @daveearley in #1388
- Email delivery tracking with provider webhooks by @daveearley in #1390
- Lifecycle emails for organizers by @daveearley in #1394
- Team access controls: scoped members, Viewer role and permissions by @daveearley in #1404
- Event lineup and schedule, and customisable homepage sections by @daveearley in #1406
- Saved widgets, new embed types and a defaults-first widget editor by @max-1-max in #1410
- Single sign-on: social login and per-account enterprise connections by @daveearley in #1411
- API keys and an in-app API reference by @daveearley in #1412
- Organizer setting for the first day of the week by @cornelisonc in #1374
- Japanese language support by @Tt0x in #1385
🐛 Fixes
- Webhook logs modal showed stale entries instead of the latest by @zigman79 in #1371
- Mobile checkout zoom, and always show quantity +/- controls by @daveearley in #1377
- Chinese and Japanese characters on invoice PDFs by @daveearley in #1383
- Account hard deletion blocked by event_spam_checks foreign keys by @daveearley in #1392
- Stray table markup in the lifecycle email footer by @daveearley in #1397
- Draft-event emails failing in the queue by @daveearley in #1407
- Untranslated SSR labels and #418 hydration errors by @daveearley in #1416
🔧 Improvements
- V2 UI design enhancements by @daveearley in #1376
- Harden sign-up, email and ordering paths against spam abuse by @daveearley in #1393
- Send lifecycle emails as plain personal emails by @daveearley in #1399
- Link email deliveries to message recipients and lifecycle nudges by @daveearley in #1402
- Redesign account settings and align create/edit modals by @max-1-max in #1408
- Faster frontend builds: patch pofile, switch to SWC by @daveearley in #1415
- Release hardening for v2.0.0-rc.3 by @max-1-max in #1421
- Licence plans, white label and covered sales by @daveearley in #1425
🧰 Development
- Amp orb setup, resume and services by @daveearley in #1387
- Run the full E2E suite against self-hosted and SaaS, fix flaky specs by @daveearley in #1396
- Move agent instructions to AGENTS.md with path-scoped rules by @max-1-max in #1409
- Update the README for 2.0 and translate it into every supported language by @daveearley in #1422
Commits without a pull request
- Allow refunds on manually created orders (303f4cc0c)
- Mark Stripe refunds as pending before the refund is created (2c18a3dfc)
- Move enterprise SSO into ee behind a licence (b30bc05cd)
- Flag widget sections the event page hides (967763639)
- Default the box office feature flag to off (8c582a232)
- Let logged-out users open the lifecycle email unsubscribe page (03ff193e7)
- Show image insert errors on the upload tab (ef28ae10d)
New Contributors
- @zigman79 made their first contribution in #1371
- @cornelisonc made their first contribution in #1374
- @Tt0x made their first contribution in #1385
- @max-1-max made their first contribution in #1408
Full Changelog: v2.0.0-rc.2...v2.0.0-rc.3
Open questions for Dave
Not release-note text. Remove this section before publishing.
- Login cookie wording. Your working draft says installs on different registrable domains "will lose cookie login".
AuthCookieSameSite::forRequest(#1372) still sendsSameSite=Nonefor cross-site requests, andAuthCookieSameSiteTestpins that, so the notes say those installs keep working but don't get the CSRF fix. ee/LICENCEfinal review. The lapse sentences were reworded to match the code (door sign-ins stop, online sales and SSO sign-in continue); the rest still needs your legal review, and the README doesn't mentionee/.- IdP offboarding. The line about removing people from your identity provider (now under Enterprise connections) describes current behaviour. Keep it.
- Main-only PRs. #1389 and #1400 were merged straight into
mainand repeat work from develop PRs. The list uses the develop PRs; GitHub's generated list will include both if the tag is cut frommain. - @max-1-max is listed as a new contributor. Keep that, or credit those PRs differently?
- Public
/complianceendpoint publishesAPP_PLATFORM_SUPPORT_EMAIL, the licence status and whether branding was removed without a licence. Keep it in public notes? APP_LICENCE_KEY=developmentis mentioned because the Licence dialog does. Fine to advertise?- Cloud-only items (lifecycle emails, Postmark delivery tracking) are kept to a few lines. Cut if you prefer.
- Licence plans. #1425 split the plan into
own_events(white label only) andplatform(white label, seating, box office, enterprise SSO), with an optional covered sales volume. The notes describe plans generically. Name them here, or leave that to /pricing while it's reworked? Keys issued with the oldenterpriseplan are now rejected ("The licence key is for an unknown plan"), so reissue any you've handed out before tagging. Cloud keys (plancloud) are unaffected. - Publish rc.3 as a pre-release. The Docker Hub step says release candidates aren't tagged
latest, which only holds for pre-releases (post-release-push-images.yml). - rc.1 notes give a broken all-in-one command.
docker compose pull && docker compose up -ddoesn't upgrade a repo clone, because the compose file builds from source. Edit the rc.1 notes to point v1 users here? - v1.11.1 → rc.3 hasn't been rehearsed. The July staging rehearsal predates every rc.2 and rc.3 migration.
- All-in-one compose gaps. It doesn't pass
APP_PLATFORM_SUPPORT_EMAIL,API_DOCS_ENABLED,OCCURRENCES_QUEUE_NAME,POSTMARK_WEBHOOK_SECRET,AWS_SESSION_TOKENor the spam check settings. Worth adding at least the support email before tagging, since the notes ask everyone to set it.