Registration/Booking widget
This widget is used to place the registration / booking form on your website. You can place as many instances of this widget as you wish, there is no limit on this. Each form can have many of its settings overridden per instance. This allows you to pre-filter the classes that are listed in the form, or change its appearance and behaviour based on the use case you're solving.
Installation
WordPress
When your WordPress plugin is installed, just head to Settings > Zooza and from the dropdown of pages, select a page where you want the form to appear.
Shortcodes
You can also use shortcodes to place the form anywhere within the page. More configuration options for shortcodes is described below in their respective sections.
[zooza type="registration"]
Wix
In Wix editor, click on Zooza widget. In the Settings panel, enter the api key and as a widget choose Registration.
Embed code
Place the following snippet directly into the <body> of your page, where you want the booking form to appear.
| Placeholder | Description | Example Value |
|---|---|---|
YOUR_API_KEY | Replace with the API key found in the application under Publish > Widget. Appears twice. | abc123xyz |
ZOOZA_API_URL | Replace with the Zooza API URL for your region: Europe: https://api.zooza.app, UK: https://uk.api.zooza.app, UAE: https://asia.api.zooza.app | https://api.zooza.app |
<script data-version='v1' data-widget-id='zooza' id='YOUR_API_KEY' type='text/javascript'>
( function() {
function async_load(){
document.body.setAttribute('data-zooza-api-url', 'ZOOZA_API_URL');
var s = document.createElement('script'); s.type = 'text/javascript'; s.async = true;
s.src = document.body.getAttribute('data-zooza-api-url') +
'/widgets/v1/?type=registration&ref=' + encodeURIComponent( window.location.href );
var embedder = document.getElementById( 'YOUR_API_KEY' );
embedder.parentNode.insertBefore( s, embedder );
}
if ( window.attachEvent ) {
window.attachEvent( 'onload', async_load );
} else {
window.addEventListener( 'load', async_load, false );
}
} )();
</script>
Settings
These settings are managed within the Zooza's main application Publish > Widget > Registration Form.
URL
Probably the most important setting there is. This will let Zooza know where your widget resides so that it can redirect your customers to it when necessary.
Availability
This will affect how the availability information is shown for classes.
| Value | Description | Example Value |
|---|---|---|
| Do not show | Availability information will be hidden | n/a |
| Current status | This will show exact real-time numbered value, thus revealing how many users are already booked out of available capacity | 10/10 |
| Text information (Default) | This will show text version of the above | Last spot available |
Submit button
| Value | Description | Example Value |
|---|---|---|
| Register (Default) | Default option | Register |
| Register with obligation to pay | More intensive version of the default value | Register with obligation to pay |
If you need custom text, refer to the translations section of this reference.
Price display for payment schedules
When the widget displays a list of available classes in this overview, it also lists the class price. If you sell courses that require recurring billing, you often don't want to show your customers the total price of the class at this point as it may be irrelevant at this stage. You can turn it off with this setting.
| Value | Description | Example Value |
|---|---|---|
| Total course price (default) | Default option | 300 € |
| Hide price | If class has payment schedule enabled the price won't be listed | n/a |
Course list display
By default, the widget renders the list of available courses as a <select> dropdown. If you have a small number of courses and want to make them more visually prominent, you can switch to a built-in tile grid. The grid is unstyled beyond a minimal layout — your site's CSS controls the look.
| Value | Description | Example Value |
|---|---|---|
| Dropdown (Default) | Courses appear as a single <select>. Best when you have many courses or want the most compact form. | Course A / Course B / … |
| Grid | Courses appear as tiles in a configurable column grid. Each tile shows the course name, the first 5 lines of its description, and a Select button. | 2 × 2 grid of tiles |
The setting above is the tenant-wide default. Individual pages can override it via window.ZOOZA or URL query — useful when one landing page wants a grid and another wants the dropdown without flipping the tenant setting.
- JavaScript
- URL Query
<script>
window.ZOOZA = {
course_list_display: 'grid'
}
</script>
?course_list_display=grid
Accepted values: 'select', 'grid'. Anything else is ignored and the tenant setting is used.
If the built-in grid doesn't match your design, register the render_course_tile callback to take over rendering of each tile. The widget falls back to the value of this setting whenever the callback isn't registered. Registering render_course_tile forces grid mode regardless of the tenant setting or any per-page override.
Course grid columns
Only applies when Course list display is set to Grid. Picks the number of columns the grid renders. Use 1 for a single-column list, 2–4 for a wider layout. The setting persists even when you switch the display back to Dropdown, so toggling between modes doesn't lose your column preference.
| Value | Description | Example Value |
|---|---|---|
| 1 (Default) | Single column — equivalent to a vertical list. | Stacked tiles |
| 2 | Two columns. | 2 × N grid |
| 3 | Three columns. | 3 × N grid |
| 4 | Four columns. | 4 × N grid |
The setting above is the tenant-wide default. Individual pages can override it via window.ZOOZA or URL query.
- JavaScript
- URL Query
<script>
window.ZOOZA = {
course_list_columns: 3
}
</script>
?course_list_columns=3
Accepted values: 1, 2, 3, 4. Values outside this range fall back to 1.
Transfer the website visitor to the form
This is useful if the registration form is not on the top of the page and users will need to scroll to see it. By default this is turned off.
Hide field for discount codes
This will hide the discount code form field in the booking form. By default this is turned on.
Use CSS
This will load default Zooza styling. By default this is turned on. Typically you only want to override couple of styles but if you want, you can turn this off and create your own styling from scratch. However we recommend downloading the default styling and go from there, instead of building everything from scratch.
You can download the default css from this URL:
API_URL/widgets/v1/css/?widget=YOUR_API_KEY&type=registration
See valid options for API_URL and YOUR_API_KEY above.
Initialisation options
In general, you can customise how the widget is initialized either via JavaScript or via URL query parameters. Due to legacy reasons, some of the parameters are defined into document.zooza and some into window.ZOOZA. It is always shown along each property which way to use. However the concept stays the same: insert the script tag before the embed code.
The widget gives you three ways to control how courses and classes appear:
- Admin Settings (with per-page override) — Course list display and Course grid columns. Tenant-wide defaults, also overridable per page via
window.ZOOZA/ URL. course_list_collapse_on_select/schedule_list_collapse_on_select— JS or URL-query flags for keeping all tiles visible after a selection.render_course_tile/render_schedule_tilecallbacks — return the inside of a single tile; the widget keeps owning the shell, click delegation, and collapse behaviour.
Callbacks always win over admin settings — registering render_course_tile forces grid mode even if Course list display is set to Dropdown.
filter_courses
Type: Array, String
This will allow you to limit which courses are shown in the booking form.
| Value | Description | Example Value |
|---|---|---|
YOUR_COURSE_ID | Array of course ids. | [ 123, 1234 ] For WordPress see note in its tab. |
- JavaScript
- WordPress
<script>
document.zooza = {
filter_courses: [ YOUR_COURSE_ID ]
}
</script>
Enter ids as a string delimited by pipe: 123|123
[zooza type="registration" filter_courses="YOUR_COURSE_ID"]
course_ids
Type: Array, String
This is the same as filter_courses but instead it is defined like this:
<script>
document.zooza = {
course_ids: [ YOUR_COURSE_ID ]
}
</script>
course_id
Type: Integer
Use this in URL query to filter classes by course. Only single course ID is allowed. You can combine this with schedule_id and place_id to preselect concrete class.
| Value | Description | Example Value |
|---|---|---|
YOUR_COURSE_ID | Course ID. | 123 |
https://sample-site.com/registration?course_id=YOUR_COURSE_ID
filter_places
Type: Array, String
This will limit the displayed classes based on given location(s). This filter uses place id thus doesn't allow for filtering to a classroom level.
| Value | Description | Example Value |
|---|---|---|
YOUR_PLACE_ID | Array of place ids. | [ 123, 1234 ] For WordPress see note in its tab. |
- JavaScript
- WordPress
<script>
document.zooza = {
filter_places: [ YOUR_PLACE_ID ]
}
</script>
Enter ids as a string delimited by pipe: 123|123
[zooza type="registration" filter_places="YOUR_PLACE_ID"]
place_id
Type: String
Use this in URL query to filter classes by place. Only single place ID is allowed. You can combine this with schedule_id and course_id to preselect concrete class.
| Value | Description | Example Value |
|---|---|---|
YOUR_PID | PID consists of place_id and a room_id. They are concatenated using underscore. If you don't want to specify room, just use zero (123_0) | 123_123 |
https://sample-site.com/registration?place_id=YOUR_PID
schedule_id
Type: Integer
Use this in URL query to filter classes by their ID. Only single class ID is allowed. You NEED to combine this with place_id and course_id to preselect concrete class. All three parameters need to be present if you want to select concrete class.
| Value | Description | Example Value |
|---|---|---|
YOUR_CLASS_ID | Class id | 123 |
https://sample-site.com/registration?schedule_id=YOUR_SCHEDULE_ID
labels_in
Type: Array of strings, or pipe-delimited string
Show only courses tagged with at least one of the listed labels. Values are label names as shown in the admin UI — case-sensitive, including spaces and punctuation (e.g. "Summer 2026", "Kids — beginners"). Label ids are not accepted here; the legacy id-based label filter is a separate, untouched mechanism.
Composes with labels_not_in (AND), and with course_ids / filter_places / schedule_id (also AND). An empty array, empty string, or omitted value means no filter.
This option works alongside the tile-grid course list and the render_course_tile callback — label filtering is applied first, then the surviving courses are rendered in whatever display mode you've chosen.
Private (admin-only) labels never match for embedders. There is no client-visible difference between "label doesn't exist" and "label exists but is private" — both produce zero matches. If a label is unpublished after the widget is embedded, the courses tagged with it will silently disappear from the list.
- JavaScript
- URL Query
<script>
window.ZOOZA = {
labels_in: [ "Summer 2026", "Kids — beginners" ]
}
</script>
?labels_in=Summer 2026|Kids — beginners
labels_not_in
Type: Array of strings, or pipe-delimited string
Hide courses tagged with any of the listed labels. Values are label names as shown in the admin UI — case-sensitive, including spaces and punctuation. Label ids are not accepted.
Composes with labels_in (AND), and with course_ids / filter_places / schedule_id. An empty array, empty string, or omitted value means no filter.
- JavaScript
- URL Query
<script>
window.ZOOZA = {
labels_not_in: [ "Archived", "Internal" ]
}
</script>
?labels_not_in=Archived|Internal
metadata_in
Type: Object — per-key value is an array of strings or pipe-delimited string
Show only courses whose metadata matches at least one of the listed values for each listed key. Keys and values are matched verbatim against the company's metadata catalogue — case-sensitive, including whitespace and punctuation. Multiple keys compose as AND across keys (a course must satisfy every listed key); within a single key the listed values match as OR (the course's value for that key must equal one of them).
Composes with metadata_not_in (AND), with labels_in / labels_not_in, and with the existing id-based filters course_ids / filter_places / schedule_id (also AND). An empty per-key value drops that key from the wire; an empty top-level config means no metadata filter is applied.
This option works alongside the tile-grid course list and the render_course_tile callback — metadata filtering is applied first, then the surviving courses are rendered. The same data is exposed on course.metadata inside the per-tile callback, so you can both filter and display from the same source.
The widget URL-encodes metadata keys and values when constructing the api request from the JavaScript form, so &, =, +, #, spaces, and punctuation are all safe to use in window.ZOOZA.metadata_in. The URL Query form is not auto-encoded — if you write the URL by hand (or template it server-side), encode &, =, #, and + yourself, the same way you would for any query parameter.
Private (admin-only) metadata keys never match for embedders. There is no client-visible difference between "key doesn't exist" and "key exists but is private" — both produce zero matches for that key. If a key is unpublished after the widget is embedded, the courses tagged with it will silently fall out of the list.
- JavaScript
- URL Query
<script>
window.ZOOZA = {
metadata_in: {
color: [ "red", "blue" ],
age_band: "6-8"
}
}
</script>
?metadata_in[color]=red|blue&metadata_in[age_band]=6-8
metadata_not_in
Type: Object — per-key value is an array of strings or pipe-delimited string
Hide courses whose metadata matches any of the listed values for the listed keys. Match rules are identical to metadata_in — keys and values verbatim, case-sensitive, multiple keys compose as AND (a course must avoid every match), values within a single key match as OR.
Composes with metadata_in (AND), with labels_in / labels_not_in, and with the existing id-based filters course_ids / filter_places / schedule_id. An empty per-key value drops that key from the wire; an empty top-level config means no metadata filter is applied.
- JavaScript
- URL Query
<script>
window.ZOOZA = {
metadata_not_in: {
archived: [ "true" ]
}
}
</script>
?metadata_not_in[archived]=true
course_list_collapse_on_select
Type: Boolean
By default, when a customer picks a course from the grid, the other tiles are hidden so the form can focus on the next step. The selected tile gets a "back to all courses" affordance the customer can use to change their mind. Set this to false to keep every tile visible at all times — useful when you want customers to compare courses side by side as they progress.
Only meaningful when Course list display is set to Grid. In Dropdown mode the setting has no effect.
| Value | Description | Example Value |
|---|---|---|
true (Default) | After a customer picks a course, other tiles are hidden until they pick "back to all courses". | Focused single-tile view |
false | All tiles stay visible after a selection; the chosen tile is highlighted with a selected class. | Side-by-side comparison |
- JavaScript
- URL Query
<script>
window.ZOOZA = {
course_list_collapse_on_select: false
}
</script>
?course_list_collapse_on_select=false
schedule_list_collapse_on_select
Type: Boolean
By default, after the customer picks a class from the schedule list, the other classes are hidden and the chosen one stays on screen with a "change" link. Set this to false to keep all classes visible — useful when customers should be able to scan the full list as they fill in the form, or when the form sits beside a fixed schedule overview.
| Value | Description | Example Value |
|---|---|---|
true (Default) | After a class is picked, the other tiles collapse. The chosen tile shows a "change" link. | Focused single-tile view |
false | All tiles remain visible after a selection. | Full schedule stays on screen |
- JavaScript
- URL Query
<script>
window.ZOOZA = {
schedule_list_collapse_on_select: false
}
</script>
?schedule_list_collapse_on_select=false
ps (Payment schedule type)
Type: String
Use this setting either in combination with f or as a standalone preset to preselect first instance of available payment schedules by type.
Allowed values:
| Value | Label |
|---|---|
single_payment | Single payment |
in_advance | In advance |
by_attendance | By attendance |
pay_as_you_go | Pay as you go |
- JavaScript
- URL Query
<script>
window.ZOOZA = {
ps: 'pay_as_you_go'
}
</script>
https://sample-site.com/registration?ps=pay_as_you_go
f (Payment schedule frequency)
Type: String
Use this setting either in combination with ps or as a standalone preset to preselect first instance of available payment schedules by frequency.
Allowed values:
| Value | Label |
|---|---|
monthly | Monthly |
quarterly | Quarterly |
half_yearly | Half yearly |
yearly | Yearly |
after_events | After events |
absolute | Absolute |
segments | Blocks |
- JavaScript
- URL Query
<script>
window.ZOOZA = {
f: 'yearly'
}
</script>
https://sample-site.com/registration?f=yearly
multi_step_registration
Type: Bool
By default the registration form appears as a single step form, that reveals its parts as user navigates through a selection. You can turn this form into multiple steps:
- Course/Class selection
- Personal information
- Product options (optional)
- Payment options
- JavaScript
- URL Query
- WordPress
<script>
window.ZOOZA = {
multi_step_form: true
}
</script>
https://sample-site.com/registration?multi_step_form=true
[zooza type="registration" multistep="true"]
preferred_currency
Type: String (Three letter ISO 4217 code)
This settings will override default currency shown in the form which is based on your Zooza account's region. Even though the form will display prices for your currency, in order for this to work, your currency also needs to be set up on the course level for every course shown in the booking form. This setting can be done in: Courses > COURSE > Settings > Price settings > Additional currencies
This enables you to sell your classes in different currency than defined by your region. If you want to collect money for all bookings in the same currency consider changing the region. This setting is intended for cases where you want to allow dual currency regime on a subset of your products.
| Value | Description | Example Value |
|---|---|---|
CODE | Three letter ISO 4217 currency code | CZK |
<script>
window.ZOOZA = {
preferred_currency: CODE
}
</script>
print_debug
Type: Bool
This will print additional debug information to the browser's console. It is especially useful for tracking translations if you want to replace some of the default texts.
<script>
window.ZOOZA = {
print_debug: true|false
}
</script>
translations
Type: Object
If you want to replace any of the text used in the booking form, you can do that by providing your own custom translations.
Within Zooza's course settings there are plenty of customisation options especially for input fields regarding personal information. You can change them directly in the Courses > COURSE > Settings > Online registration or Courses > COURSE > Settings > Additional fields without any need for coding.
On top of that, please bear in mind that by providing custom translations you are changing the text value for all products/texts displayed on this widget's instance. Therefore if possible, stay away from changing the meaning of the text or omitting useful information as this may hinder your conversion rates.
| Placeholder | Description | Example Value |
|---|---|---|
KEY | Translation key that appears in the booking form | registration.step_select_course |
TRANSLATION | Your custom text that will replace the default translation | Select course |
<script>
window.ZOOZA = {
translations: {
KEY : TRANSLATION
}
}
</script>
Enable print_debug mode and open your browser's console. You will see printouts like this: Text Key: notifications.title Translation: Notifications. The Text key is the key used by application and Translation is its default translation. Notice that keys appear as you progress through the form. You can identify each key by searching for the Translation in the console.
Example
<script>
window.ZOOZA = {
translations: {
'registration.step_select_course' : 'Select course',
'overview.your_discounts' : 'Your discounts',
'course_detail.accordion_free_events' : 'Free lessons',
}
}
</script>
registration_display_mode
Type: Enum
By default, if class supports more registration modes (e.g. Trial registrations or Segments), registration widget will present customer with all available options. If you want to limit this, you can do that using this property.
Example usages:
- Your class contains blocks and you only want your customers to select the blocks
- You offer trial lessons on a landing page for new customers. And you place a second instance of the widget in the members area where you'll offer more options
| Value | Description | Example Value |
|---|---|---|
REGISTRATION_DISPLAY_MODE | Enum of your choice | trials_only |
<script>
window.ZOOZA = {
registration_display_mode: REGISTRATION_DISPLAY_MODE
}
</script>
lang
Type: String (BCP 47 locale code)
Language of the widget is defined by your Zooza account. You can change this language in Settings > General > Language. However, you can override widget's language by setting its lang property to one of supported languages.
| Value | Description | Example Value |
|---|---|---|
LANGUAGE_CODE | Language code of your choice | en-EN |
- URL Query
- HTML
https://sample-site.com/registration?lang=LANGUAGE_CODE
<html lang="LANGUAGE_CODE">
</html>
Although the widget's language is mainly determined by language set in your account, it is not the only determinant. The widget's language is determined in this order:
- URL Query
langparameter langparameter read from browser'sLocalStorage()(this is set when user opts for a language using language selector at the bottom of the widget)langattribute ofhtmltag- language set in the Zooza
If you have multilingual website and you are using a content management system (such as WordPress) then you don't need to set up anything as these systems will typically set current page's language html attribute and the widget will automatically use this prior selecting the language defined in your account (as described in box above).
Analytics
Registration widget will trigger following events for Google Tag Manager's DataLayer and Meta's Pixel:
| Event | Description | Event data |
|---|---|---|
zooza_event_form_loaded | Triggered when the registration form is displayed to the user | zooza_form_loaded: true |
zooza_event_form_submit_start | Triggered immediately after the submit button is clicked, before the request is sent | zooza_total_price: Total value of the booking (including future payments); zooza_currency: Currency; zooza_pay_now: Amount to pay now |
zooza_event_form_submit_done | Triggered after the response is received from the server | zooza_event_form_submit_done: true |
zooza_event_form_error | Triggered if the response contains any errors | |
zooza_event_form_submit_thank_you | Triggered if registration is successful and no further steps are needed | zooza_form_submit_thank_you: true |
zooza_event_form_submit_go_to_payment | Triggered if the user is taken to the payment step | zooza_form_submit_go_to_payment: true |
zooza_event_payment_response | Triggered when the user returns from the payment gateway | zooza_payment_response (string) ok or fail |
zooza_event_confirm_registration | Triggered when the user clicks the confirmation link in the email | zooza_registration_id (int) |
zooza_event_enroll_started | Triggered when the user proceeds after a trial lesson | zooza_enroll_started: true |
zooza_event_reenroll_started | Triggered when the user proceeds after a trial lesson | zooza_reenroll_started: true |
zooza_event_accept_waitlist | Triggered when a user accepts a spot from a waitlist notification (for make-up lessons) | zooza_accept_waitlist: true |
zooza_event_turn_off_notifications | Triggered when the user disables notifications in Full courses | zooza_turn_off_notifications: true |
zooza_event_turn_off_upcoming_events_notifications | Triggered when the user disables reminders in Open courses | zooza_turn_off_upcoming_events_notifications: true |
zooza_event_cancel_event | Triggered when the user cancels a session via the email link | zooza_cancel_event: true |
zooza_event_form_submit_error | Triggered when form on submit returns an error (e.g. booking failed due to an error) | zooza_event_form_submit_error (bool) |
zooza_event_share_link_opened | Triggered when Share link is used | zooza_share_link_opened (bool) |
Each event will also pass event data, which is an object with one or multiple properties. The one that will likely interest you most is the zooza_event_form_submit_start event, which also sends the value of the order to analytics.
Guides
URL Hashtag-Based Tracking (For Registration Confirmation)
Besides event-based triggers, Zooza also supports conversion tracking via URL fragments (hashtags).
You'll see these appear after key steps in the registration flow. They can be used in Google Analytics, Meta Ads, or other platforms for measuring conversion milestones.
First Phase – Submitted Form
| Hashtag | Description |
|---|---|
#done | Registration email sent. At this point, the user is recorded in Zooza and this counts as a conversion. |
#registration_failed | Form submission failed. |
Second Phase – Confirmed Email
| Hashtag | Description |
|---|---|
#confirm_registration | Triggered when the user confirms their registration via email. |
#confirm_registration_thank_you | Triggered when the user reaches the final "Thank You" screen (registration complete). |
Notes on hashtag tracking
- Dynamic URLs: Zooza registration links often include query parameters like
?course_id=123&place_id=456. Always match usingURL contains #YOUR_HASHTAG. - Use Regex for Flexibility: For more robust tracking across locations or courses, use regular expressions in your rules.
- Tracking per Location: Hashtags don't carry location data. If you need to track conversions per branch or subpage, combine hashtags with UTM parameters or URL path logic.
- Thank You Page Tip:
#confirm_registration_thank_youonly appears when the registration is fully completed. This helps distinguish between users who just clicked the email vs. those who reached the final step.
Events and callbacks
You can hook into widget lifecycle events using the callback property.
schedule_registration_options_loaded
This callback is called when registration options are loaded and allows you to manipulate the DOM or widget state. This event happens immediately after a class is selected and before any further actions. Although it is possible to manipulate the DOM, it is generally better and safer to intercept the data model.
Params
The params object contains following attributes:
| Attribute | Description |
|---|---|
el | jQuery element with registration options (.zooza_trial_registration_wrapper) |
ask_for_trial | Bool. Whether currently selected class does contain trial sessions |
ask_for_segment | Bool. Whether currently selected class does contain Blocks |
window.ZOOZA = {
callback: {
schedule_registration_options_loaded: ( params ) => {
console.info( 'registration options loaded', params );
},
},
};
schedule_list_rendered
Fires immediately after the widget renders the schedule list and appends it to the DOM. Use this hook when you need to run your own DOM work over the rendered tile collection — adding badges or section separators, attaching analytics, observing visibility, anything that needs the tiles to already be on the page.
The callback fires on every render of the schedule list, including re-renders that follow a place change. Make sure your handler is idempotent — guard against double-attaching listeners or duplicating injected nodes (the practical example below shows the pattern).
Params
The callback receives a single object argument with the following attributes:
| Attribute | Description |
|---|---|
el | The .zooza_schedules container as a plain DOM Element (not a jQuery object). Holds the prepended filter and all rendered .zooza_schedules_schedule[data-schedule_id] tiles. |
schedules | Array of schedule objects in render order. The array order lines up 1:1 with the rendered tiles, so schedules[ i ] matches the i-th tile inside el. Each entry exposes the same stable members documented for render_schedule_tile's schedule arg (id, get_date_formatted(), get_start_formatted(), get_end_formatted(), get_price(), get_capacity_formatted()). |
course | The currently selected course. Same stable shape as the course arg in render_course_tile (id, name, description, course_type, registration_type, metadata). |
el is a plain DOM ElementUnlike schedule_registration_options_loaded (which passes el as a jQuery object for legacy reasons), schedule_list_rendered passes el as a plain DOM Element. Embedders without jQuery on their page can use it directly — querySelectorAll, insertBefore, classList, etc. If you do have jQuery available, wrap with $( el ) if you prefer.
Minimal example — log every render
window.ZOOZA = {
callback: {
schedule_list_rendered: ( { el, schedules, course } ) => {
const tiles = el.querySelectorAll( '.zooza_schedules_schedule' );
console.info( `Rendered ${ tiles.length } schedules for course "${ course.name }"` );
},
},
};
Practical example — week separators between schedule groups
A common pattern: the schedule list contains several weekly groups whose names start with a numeric prefix ("1. 22 JUNE - 3 JULY 2026 // 09.00-10.10", "1. 22 JUNE - 3 JULY 2026 // 10.30-11.40", …, "2. 06 JULY - 17 JULY 2026 // 09.00-10.10", …). The example below walks the rendered tiles, detects when the leading group number changes, and inserts a separator before the first tile of each new group.
The handler is idempotent: it removes any separators it injected on a previous render before walking the tiles, so a re-render after a place change does not stack duplicates.
window.ZOOZA = {
callback: {
schedule_list_rendered: ( { el, schedules } ) => {
// The callback fires on every render (including re-renders after a
// place change), so clear any separators we injected previously.
el.querySelectorAll( '.my-week-separator' ).forEach( ( node ) => node.remove() );
const tiles = el.querySelectorAll( '.zooza_schedules_schedule' );
let last_group = null;
tiles.forEach( ( tile, index ) => {
const schedule = schedules[ index ];
const match = schedule.name && schedule.name.match( /^(\d+)\./ );
if ( ! match ) {
return;
}
const group_number = match[ 1 ];
if ( group_number === last_group ) {
return;
}
last_group = group_number;
const separator = document.createElement( 'div' );
separator.className = 'my-week-separator';
separator.textContent = schedule.name;
tile.parentNode.insertBefore( separator, tile );
} );
},
},
};
The widget exposes schedule.name on each entry alongside the stable getters listed in render_schedule_tile — it is the human-readable label the widget itself uses on the tile.
render_course_tile
Returns the HTML that goes inside a single course tile. The widget owns everything else: the <div class="zooza_courses_course" data-course_id="<id>"> wrapper, the grid layout, the "Select" CTA, the collapse-on-select, and the "back to all courses" affordance. Register this callback when you want a different look for the courses than the built-in default — your own headings, images, marketing copy, prices, badges, anything.
Registering this callback forces the widget into grid mode even if Course list display is set to Dropdown — the hidden <select name="zooza_courses"> stays in the DOM as the source of truth, but the visible UI is your tiles.
Params
The course parameter exposes the following stable fields. Anything else is internal and may change between minor versions without a deprecation cycle.
| Field | Type | Note |
|---|---|---|
id | Number | Course id |
name | String | Display name |
description | String | May be empty |
course_type | String | Matches the API enum (course, event, online_event, photography, …) |
registration_type | String | single, full2, or open |
metadata | Array | Public metadata entries on the course (see entry shape below). Empty array if the course has no public metadata. The same keys you can filter on with metadata_in / metadata_not_in. |
Each entry in course.metadata exposes:
| Field | Type | Note |
|---|---|---|
key | String | Metadata key as authored in admin (case-sensitive). |
value | String | Number | Boolean | Object | Typed per value_type; the api casts before serialising. |
value_type | String | One of string, int, bool, or json. Branch on this if you need exhaustive type handling. |
Each course has at most one entry per key, so course.metadata.find( m => m.key === 'color' ) is safe.
Return
A string, DOM Node, or jQuery object representing the inside of the tile. Whatever you return is inserted into the widget-built tile wrapper. Anything else (e.g. undefined) is treated as "no content" and the tile gets only the auto-appended Select CTA.
Minimal example — name and description
The smallest thing that works. The widget auto-appends a default Select CTA at the bottom of the tile because the returned content does not include a .zooza_courses_course_select element — so the customer can advance with no extra wiring.
window.ZOOZA = {
callback: {
render_course_tile: ( course ) => `
<h3>${ course.name }</h3>
<p>${ ( course.description || '' ).slice( 0, 160 ) }</p>
`,
},
};
Reading metadata — typed values rendered as badges
Metadata entries on course.metadata are an array of { key, value, value_type }. A small helper that returns the value for a given key (or null if absent) keeps tile templates readable. The widget always returns a value typed per value_type — string, int, bool, or json — so no parsing on your side. See the Params table for the stable contract.
window.ZOOZA = {
callback: {
render_course_tile: ( course ) => {
const get_meta = ( key ) => {
const entry = ( course.metadata || [] ).find( ( m ) => m.key === key );
return entry ? entry.value : null;
};
const price = get_meta( 'price' ); // value_type: 'string', e.g. "from 34 EUR/month"
const size = get_meta( 'size' ); // value_type: 'int', e.g. 10
const price_badge = price !== null
? `<span class="my-badge my-badge-price">${ price }</span>`
: '';
const size_badge = size !== null
? `<span class="my-badge my-badge-size">Group size: ${ size }</span>`
: '';
return `
<h3>${ course.name }</h3>
<div class="my-meta-row">${ price_badge }${ size_badge }</div>
<p>${ ( course.description || '' ).slice( 0, 160 ) }</p>
`;
},
},
};
The same keys you read here are the ones you can filter on with metadata_in / metadata_not_in — filter to a subset of courses, then render the surviving courses' metadata in your own design language.
Custom CTA — embedder provides the Select element
If you want the Select CTA inline (different copy, different position, different styling), include any element with class="zooza_courses_course_select" in the returned HTML. The widget detects it and skips its auto-append. You don't write a click handler — the widget's delegated click on .zooza_courses_course_select reads the course id from the tile wrapper and runs the standard flow.
window.ZOOZA = {
callback: {
render_course_tile: ( course ) => `
<header>
<h3>${ course.name }</h3>
<button type="button" class="zooza_courses_course_select">Reserve this course</button>
</header>
<p class="my-course-summary">${ ( course.description || '' ).slice( 0, 200 ) }</p>
`,
},
};
The widget delegates click on these classes — your tile content can include any element carrying them, and the widget reads data-course_id from the closest tile wrapper, never from the click target. No event listeners on your side, no jQuery, no manual selection wiring.
| Class | Action |
|---|---|
zooza_courses_course_select | Pick this course |
zooza_courses_course_change | Back to all courses (collapse mode only) |
If your tile content does not include zooza_courses_course_select, the widget auto-appends a default Select CTA so the customer can always advance.
render_schedule_tile
Mirror of render_course_tile for the schedule list. Returns the HTML that goes inside a single <div class="zooza_schedules_schedule" data-schedule_id="<id>"> tile. The widget owns the wrapper, the grid layout, the Choose CTA, collapse-on-select, and the change affordance.
Params
The callback receives ( schedule, course ). Anything not listed below is internal and may change between minor versions without a deprecation cycle.
schedule exposes:
| Member | Returns | Note |
|---|---|---|
id | Number | Schedule id |
get_date_formatted() | String | Locale-formatted date / date range |
get_start_formatted() | String | Start datetime, locale-formatted |
get_end_formatted() | String | End datetime, locale-formatted |
get_price() | String | Currency-formatted price (e.g. "10,00 €") |
get_capacity_formatted() | String | Localised capacity copy (e.g. "Spots available", "Full") |
course is the currently selected course model — same shape as the course parameter passed to render_course_tile.
Return
A string, DOM Node, or jQuery object — same as render_course_tile. Whatever you return is inserted into the widget-built tile wrapper.
Minimal example — date and price
window.ZOOZA = {
callback: {
render_schedule_tile: ( schedule, course ) => `
<strong>${ schedule.get_date_formatted() }</strong>
<span>${ schedule.get_price() }</span>
`,
},
};
Custom CTA — embedder provides the Choose element
Same pattern as courses: include class="zooza_schedules_schedule_select" on any element to take over CTA copy, position, or styling. Optionally include class="zooza_schedules_schedule_change" if you want the "change selected class" affordance inline (the widget normally injects it on the selected tile in collapse mode).
window.ZOOZA = {
callback: {
render_schedule_tile: ( schedule, course ) => `
<div class="my-schedule-info">
<strong>${ schedule.get_date_formatted() }</strong>
<span>${ schedule.get_capacity_formatted() }</span>
</div>
<button type="button" class="zooza_schedules_schedule_select">Reserve this date</button>
`,
},
};
The widget delegates click on these classes — your tile content can include any element carrying them, and the widget reads data-schedule_id from the closest tile wrapper, never from the click target. No event listeners on your side, no jQuery, no manual selection wiring.
| Class | Action |
|---|---|
zooza_schedules_schedule_select | Pick this class |
zooza_schedules_schedule_change | Change selected class (collapse mode only) |
If your tile content does not include zooza_schedules_schedule_select, the widget auto-appends a default Choose CTA so the customer can always advance.
Data model
Registration widget exposes its model within document.ZOOZA.model property. This allows you to override some logical steps decided by the application.
Remember that when you override default behaviour of the model, you can break the widget's functionality. On top of that, the validations that are performed on the backend will not respect if you override something in the model.
Most common/useful model methods:
| Method | Description |
|---|---|
document.ZOOZA.model.print() | Will print current state of the model |
document.ZOOZA.model.selected_course() | Will return currently selected course data |
document.ZOOZA.model.selected_schedule() | Will return currently selected class data |
Examples
Multi step registration and filtered courses with custom translation
window.ZOOZA = {
course_ids: [948], // Array of course IDs to filter or preselect
multi_step_form: true, // Use a single-step or multi-step registration form
translations: {
'registration.step_select_course': 'Select a program',
},
};