Event Website Development on 1C-Bitrix
An event website is a transactional system, not a brochure. Registration, ticket sales, email notifications, QR-code generation for check-in, attendee dashboards — all of this runs on the sale, catalog, and subscribe modules plus a set of carefully structured info blocks. Get the data architecture wrong at the start, and six months later when the second conference launches, you discover that the "Events" info block holds speakers, schedule slots, and tickets in a single flat structure. Refactoring on production means migrating data, rewriting component templates, and rebuilding the cart logic.
Info Block Architecture
Four info blocks minimum. You can collapse schedule into events, but that falls apart the moment you need multi-day conferences with parallel tracks.
"Events" info block (type events) — the primary entity. Properties:
-
DATE_START,DATE_END— Date/time type. Used in filtering and sorting; an index onb_iblock_element_propertybyIBLOCK_PROPERTY_ID+VALUE_DATEis required -
VENUE— element link to a "Venues" info block or Highload block containing address, coordinates, hall capacity -
CAPACITY— integer, maximum attendees. Validated during checkout via theOnSaleOrderBeforeSavedhandler -
STATUS— list: "Registration Open", "Registration Closed", "Completed", "Cancelled" -
STREAM_URL— string, broadcast link (YouTube/Vimeo). Displayed only to authenticated users with a paid ticket -
SCHEMA_ORG— computed at render time inresult_modifier.php, not stored in the database
"Speakers" info block (type speakers) — separate from events. One speaker presents at multiple events; binding via a multiple "Element link" (E) property. Properties: PHOTO (file), BIO (HTML/text), COMPANY, POSITION, SOCIAL_LINKS (multiple string).
"Schedule" info block (type schedule) — one element per time slot. Properties:
-
EVENT_ID— link to event -
SPEAKER_ID— multiple link to speakers -
TIME_START,TIME_END— time within the day -
HALL— track/room for parallel sessions -
SLOT_TYPE— list: "Talk", "Workshop", "Break", "Networking"
"Tickets" info block (type tickets) — connected to the catalog module. Each ticket type is an info block element attached as a product in the trade catalog. Properties: EVENT_ID, TICKET_TYPE (Standard, VIP, Online), AVAILABLE_SEATS, VALID_UNTIL.
Why not Highload blocks for everything? Speakers and schedule hold tens to hundreds of records. Standard info blocks give you news.list, news.detail components and the admin visual editor out of the box. Highload makes sense for venues (if there are 500+) and for the check-in log (thousands of QR scan records).
Ticket Sales — the Core Complexity
A ticket is a product in the Bitrix catalog, but with non-standard logic: date binding, quantity limits, and it's nominative (tied to a specific attendee).
Product = element in the "Tickets" info block, connected to the trade catalog via CCatalog::Add(). Each ticket type for each event is a separate product. A 500-person conference with three ticket types means three catalog products, each with its own stock quantity.
Inventory management enforces capacity limits. A warehouse is created in the catalog module; each ticket product's quantity equals the available seats. When an order is placed, the sale module decrements stock automatically. When stock reaches zero, the "Buy" button switches to "Sold Out" — checked via CCatalogProduct::GetByID() in the component template.
Order flow in detail:
- User selects an event and ticket type
- Add to cart via
\Bitrix\Sale\Basket::addItem()— not the legacyCSaleBasket::Add() - During checkout — a custom step in
sale.order.ajaxwith fields: attendee name, email, phone, company. These values are stored as order properties (\Bitrix\Sale\Order::setField()) and basket properties - Payment via a gateway: YooKassa, Stripe, or PayPal. Configured in the
salemodule → "Payment Systems" → handleryookassaor a custom handler atlocal/php_interface/include/sale_payment/ - On successful payment — the
OnSalePaymentEntitySavedevent fires. The handler generates a QR code, sends the ticket email, and creates a CRM lead
Real-time capacity enforcement. Standard inventory works at order level, but time passes between adding to cart and paying. To avoid selling ticket 501 for a 500-seat event:
- The
OnSaleOrderBeforeSavedhandler checks current stock and rejects the order if no seats remain - Soft reservation via
CCatalogProduct::Update()decrementsQUANTITYon add-to-cart. An agent restores it after 30 minutes if the order is not completed
Promo codes — via catalog.discount. A discount rule with condition "Coupon code = X", discount type — percentage or fixed amount. In the sale.order.ajax template, add a coupon input field; application through \Bitrix\Sale\Discount::setBasketItemDiscount(). For early-bird pricing — a discount with a date condition: active until a specific date, deactivates automatically.
Registration and CRM Integration
Free event registration bypasses the sale module entirely. A custom component project:event.register with a form:
- Frontend validation (JS) and backend validation (in
component.php) - Record in a "Registrations" Highload block:
EVENT_ID,USER_ID,STATUS,REGISTERED_AT - CRM lead creation via REST API:
CRest::call('crm.lead.add', [...])or directly through thecrmmodule if Bitrix24 is deployed on the same server
For paid events, registration happens automatically on payment. The OnSalePaymentEntitySaved handler creates the Highload block record and updates the CRM lead status.
Attendee dashboard is built on sale.personal.section with a custom template:
- List of registered events (Highload block query by
USER_ID) - Order history and tickets (stock
sale.personal.order) - Ticket download with QR code (PDF generated via
TCPDForDompdf) - Broadcast access (
STREAM_URLlink appears 30 minutes before start) - Post-event attendance certificate (PDF with data from the Highload block)
QR Code and Check-In
On successful payment, a QR code is generated. Content — a unique order hash: md5($orderId . $userId . $salt). The hash is stored in the order property QR_HASH.
QR generation uses the endroid/qr-code Composer package, loaded in local/php_interface/. The QR is embedded in the PDF ticket and sent by email.
At the venue — a mobile app or a web page at /check-in/ with a camera scanner (the html5-qrcode library). The scanned hash is sent as a POST request to /api/checkin/. The controller checks:
- Does an order with this
QR_HASHexist - Is the order paid (
\Bitrix\Sale\Order::isPaid()) - Has the ticket already been used (
CHECKED_INorder property)
On success — the order property is updated, a record is written to the check-in Highload block, and a 200 response with attendee data is returned.
Event Calendar
Two approaches:
Custom component + FullCalendar.js. The project:event.calendar component returns a JSON array of events via AJAX. On the frontend — FullCalendar with eventSources pointing to /api/events/calendar/. Each event object contains title, start, end, url (link to the detail page), color (by event type). JSON response caching — 10 minutes with tagged cache on the events info block.
Stock iblock.element.list component with a custom template rendering a date grid. Easier to maintain but less interactive — no month/week/day switching without custom JS.
For event series (annual conference) — filtering by info block section. Each year = section, events inside = elements.
Countdown Timer and Live Streaming
The timer is a JS component that reads DATE_START from a data attribute. Server time is passed via <?= time() ?> for synchronization. No external API — pure setInterval calculating the difference.
Streaming — a YouTube/Vimeo iframe wrapped in an access-controlled component. project:event.stream checks in component.php:
- Is the user authenticated
- Does the user have a paid order with an "Online" or "VIP" ticket for this event
- Has the event ended
If the check fails — redirect to the ticket purchase page.
Email Notifications
The subscribe module plus main module mail events:
-
Registration confirmation — mail event
EVENT_REGISTER_CONFIRM, template in Settings → Mail Events -
Ticket after payment —
EVENT_TICKET_PAID, with PDF attachment -
24-hour reminder — a
CAgentthat selects events whereDATE_START= tomorrow and sends emails to registered attendees viaCEvent::Send() -
Schedule change notification —
OnAfterIBlockElementUpdatehandler on the Schedule info block, sends updates to event attendees
SEO: Schema.org Event Markup
In the event detail page's result_modifier.php, a JSON-LD block is generated:
{
"@context": "https://schema.org",
"@type": "Event",
"name": "...",
"startDate": "2025-09-15T10:00:00+03:00",
"endDate": "2025-09-15T18:00:00+03:00",
"location": {
"@type": "Place",
"name": "...",
"address": "..."
},
"offers": {
"@type": "Offer",
"price": "...",
"priceCurrency": "USD",
"availability": "https://schema.org/InStock",
"validFrom": "..."
},
"performer": [...]
}
Output via $APPLICATION->AddHeadString() in the <head>. The availability field updates dynamically: InStock when tickets are available, SoldOut when stock is zero.
Development Stages
- Planning (1-2 weeks) — info block structure, data schema, page wireframes, registration and payment workflow documentation
- Design (1-2 weeks) — page layouts: event catalog, detail page, schedule, attendee dashboard, check-in screen
- Backend (2-4 weeks) — info blocks, trade catalog, registration and payment components, gateway integration, QR generation
- Frontend (2-3 weeks) — component templates, calendar, countdown timer, responsive layouts, check-in scanner page
- Integrations (1-2 weeks) — CRM, email notifications, streaming embed
- Testing (1-2 weeks) — functional, payment in sandbox mode, load testing (simulating 500 concurrent purchases)
- Launch (3-5 days) — deployment, monitoring, validation on a real event
| Project Scope | Estimated Timeline |
|---|---|
| Single event, registration without payment | 3-5 weeks |
| Event with ticket sales and QR check-in | 6-9 weeks |
| Multi-event platform with attendee dashboard | 8-12 weeks |
Timelines depend on the number of ticket types, external integrations, and attendee dashboard requirements.







