Picker API Reference
Complete API reference for the SeatSquirrel Picker
Constructor Options
new SeatSquirrel.StatelessPicker(options)
Shared Callbacks (Both Modes)
These options work in both standalone and iframe SDK modes.
| Option | Type | Required | Description |
|---|---|---|---|
onReady | () => void | No | Called when picker is ready. |
onLayoutLoaded | () => void | No | Called when loadLayout() succeeds. Pure confirmation signal — no payload. |
onSelectionChanged | (data: SelectionData) => void | No | Called when selections change. data.lastSelection is the item just selected/modified, or null on deselect/clear. |
onComplete | (data: SelectionData) => void | No | Important: Called when user clicks to finalise their selection. This is the most important callback — where you handle the user's final selections and send them to your backend for processing. |
onError | (error: { message: string, code: string, details?: string }) => void | No | Called when an error occurs. |
requirePricingCategory | boolean | No | When true, entities without a valid pricing category are treated as unavailable. Default: true. Can also be set via loadLayout options, setPricingCategories, or setAvailability by-pricing-category mode. |
smartSeating | boolean | No | When true, the picker highlights live any empty seat the current selection would strand (row ends count as walls) and disables Proceed with a hint until the user fixes the selection. Sandwich orphans (an empty seat between two of the user's own picks) are always flagged; other single-empty configurations are flagged only when a better placement exists. Row seats only. No blocking modal. Default: false. |
ui | { fullscreenButton?: boolean; pricingCategoryFilter?: boolean } | No | UI configuration. ui.fullscreenButton: false hides the built-in fullscreen toggle (default true); you can still trigger fullscreen programmatically via setFullscreen() when the button is hidden. ui.pricingCategoryFilter: false hides the built-in "Filter categories" control — a dropdown of the pricing categories in the current view whose checkboxes fade non-matching bookables so a booker can find a price tier (auto-hidden when a view has fewer than 2 categories). Default: true (shown). |
onFullscreenChange | (isFullscreen: boolean) => void | No | Called whenever the picker enters or exits viewport-fill fullscreen. |
onRequestSectionContent | (sectionId: string) => SectionContent | { content, initialAvailability? } | Promise<...> | No | Sections only. Lazily provide one Section's contents when the user drills into it. Required for sectioned layouts — without it, layout.sections is dropped at load (one-time warning) and only the parent scope renders. |
onSectionChange | (activeSectionId: string | null) => void | No | Sections only. Fires when the user drills into a Section (its id) or back to the parent (null). Push fresh availability for the now-active section here — re-entry serves cached content. |
iFrame SDK Only
These options only apply when using the iframe SDK. They are not available in standalone mode.
| Option | Type | Required | Description |
|---|---|---|---|
container | string | HTMLElement | Yes | CSS selector or DOM element for the iframe |
baseUrl | string | No | Location of the SeatSquirrel app (default: https://seatsquirrel.com) |
pickerUrl | string | No | Full URL override for the picker page. Takes precedence over baseUrl. Useful for servers requiring .html extensions (e.g., https://example.com/picker.html) |
Methods
Summary
| Method | Parameters | Returns |
|---|---|---|
loadLayout(layoutData, options?) | layoutData: LayoutOutput, options? | Standalone: { success: boolean, availabilityErrors?: string[] } iFrame: Promise<{ success: boolean, availabilityErrors?: string[] }> |
isPickerReady() | — | { pickerReady: boolean } |
setAvailability({ mode, ... }) | { mode, ... } | { success: boolean } |
setPricingCategories({ mode, ... }) | { mode, pricingCategories } | Standalone: { success: boolean } iFrame: Promise<{ success: boolean }> |
getPricingCategories() | — | Standalone: { pricingCategories: PricingCategory[] } iFrame: Promise<{ pricingCategories: PricingCategory[] }> |
assignPricingCategories(input) | AssignPricingCategoriesInput | Standalone: { success, errors? } iFrame: Promise<{ success, errors? }> |
getSelections() | — | SelectionData |
getTotals() | — | { count: number, amount: number } |
getLayout({ mode }) | { mode: 'current' | 'last-saved' } | Standalone: { layout: LayoutOutput | null } iFrame: Promise<{ layout: LayoutOutput | null }> |
setSelections(input, options?) | SelectionData or SetSelectionsInput, { mode? } | Standalone: SetSelectionsResult iFrame: Promise<SetSelectionsResult> |
prefetchSections(sectionIds?) | string[] (optional) | Promise<{ loaded: string[], failed: string[] }> |
clearSelections() | — | { success: boolean } |
setAllowProceed(allow) | boolean | Standalone: { success: boolean } iFrame: void |
setSeatStatuses(input) | SetSeatStatusesInput | Standalone only: { success: boolean, errors?: string[] } |
showToast(options) | ShowToastOptions | Standalone: { success: boolean } iFrame: Promise<{ actionClicked: boolean }> |
showAlertDialog(options) | ShowAlertDialogOptions | { success: boolean } |
showNotice(options) | ShowNoticeOptions | { success: boolean, noticeId: string } |
removeNotice(noticeId) | string | { success: boolean } |
setFullscreen(value) | boolean | { success: boolean } |
destroy() | — | void (iFrame SDK only) |
loadLayout(layoutData, options?)
Description
Load a layout into the picker for seat selection. This must be called before users can select seats.
Parameters
layoutData(LayoutOutput): The layout JSON from the designer or your databaseoptions(Object, optional):
Older layout exports that used layout.sections for label groups and sectionId on rows/areas/tables are accepted and
normalized to the current layout.types/typeId format during load.
| Property | Type | Required | Description |
|---|---|---|---|
requirePricingCategory | boolean | No | When true, entities without a valid pricing category are treated as unavailable. Default: true. Pass false to allow uncategorized entities to remain selectable. |
initialAvailability | SetAvailabilityInput | No | Apply availability atomically with the layout load to prevent a flash of default (all-available) state. Accepts the same input as setAvailability() — any of the 5 modes. Applied after the layout is set but before onLayoutLoaded fires. |
Returns
{ success: boolean, availabilityErrors?: string[] } — success refers to the layout load itself. availabilityErrors
is only present if initialAvailability was provided and had issues (the layout still loads and onLayoutLoaded still
fires).
In standalone mode this is synchronous. In iframe SDK mode it returns a Promise.
Example
// Standalone mode (synchronous)
// By default, requirePricingCategory is true — entities without a valid pricing category are unavailable
const { success } = SeatSquirrel.picker.loadLayout(layoutData);
// Opt out of pricing enforcement — uncategorized entities remain selectable
const { success } = SeatSquirrel.picker.loadLayout(layoutData, {
requirePricingCategory: false,
});
// Load with initial availability (prevents flash of default state)
const { success, availabilityErrors } = SeatSquirrel.picker.loadLayout(layoutData, {
initialAvailability: {
mode: 'by-id',
rowSeats: { 'seat-1': true, 'seat-2': false },
areas: { 'area-1': 50 },
},
});
if (availabilityErrors) {
console.warn('Availability issues:', availabilityErrors);
}
// Load with initial availability using pricing categories
const result = SeatSquirrel.picker.loadLayout(layoutData, {
initialAvailability: {
mode: 'by-pricing-category',
available: ['category-vip', 'category-standard'],
},
});// Iframe SDK mode (async)
const { success } = await picker.loadLayout(layoutData);
// Opt out of pricing enforcement
const { success } = await picker.loadLayout(layoutData, { requirePricingCategory: false });
// Load with initial availability (prevents flash)
const { success, availabilityErrors } = await picker.loadLayout(layoutData, {
initialAvailability: {
mode: 'by-slug',
rowSeats: { 'row-a/seat-1': true, 'row-a/seat-2': false },
},
});setAvailability({ mode, ... })
Description
Set which seats and areas are available. The mode parameter controls how items are identified:
'by-id'— Use machine-generated IDs to identify seats, areas, table seats, and tables.'by-slug'— Use user-defined slugs. This is the recommended mode as slugs are stable, human-readable identifiers that you control.'dangerously-by-labels'— Use display labels. Use with caution — labels can clash if duplicated across sections. Prefer'by-slug'for reliable lookups.'by-pricing-category'— Bulk-set availability based on pricing category IDs. Entities are resolved to available/unavailable based on their assigned pricing categories (including row→seat and table→tableSeat inheritance).'by-pricing-category-slug'— Same as'by-pricing-category', but using pricing category slugs instead of IDs.
Sections: 'by-slug' and 'dangerously-by-labels' resolve identifiers against the parent layout plus every
already-visited (cached) section, so pushing slug-keyed availability for the active section from onSectionChange
works. Writes remain scoped to the view the visitor is currently looking at (see
Notes & limitations): resolved ids outside that scope are ignored, exactly like 'by-id'.
Slugs/labels in never-visited sections cannot be resolved and produce a conversion error. When a slug or label matches
both a parent-layout entity and a cached-section entity, the parent entity wins, so parent-only pushes stay
deterministic regardless of which sections have been visited; an ambiguity error is only raised for label clashes
within the same tier (pass typeId to resolve those, or to target the section entity explicitly).
Parameters
options(Object):
| Property | Type | Required | Description |
|---|---|---|---|
mode | 'by-id' | 'by-slug' | 'dangerously-by-labels' | 'by-pricing-category' | 'by-pricing-category-slug' | Yes | How to identify items |
Additional properties for mode 'by-id':
| Property | Type | Required | Description |
|---|---|---|---|
rowSeats | Record<string, boolean> | No | Map of seat ID to boolean (true = available) |
areas | Record<string, number | boolean> | No | Map of area ID to available count (number), or boolean (true = 1, false = 0) |
tableSeats | Record<string, boolean> | No | Map of table seat ID to boolean (true = available) |
tables | Record<string, boolean> | No | Map of table ID to boolean (true = available) |
Additional properties for mode 'by-slug': (AvailabilityBySlug)
Object with rowSeats, areas, tableSeats, and tables maps keyed by slug.
Additional properties for mode 'dangerously-by-labels':
(AvailabilityByLabels)
Object with rowSeats, areas, tableSeats, and tables arrays.
Additional properties for modes 'by-pricing-category' / 'by-pricing-category-slug':
| Property | Type | Required | Description |
|---|---|---|---|
available | string[] | No | Category IDs (or slugs) whose entities should be available. Unlisted categories → entities unavailable. This is a full reset. |
unavailable | string[] | No | Category IDs (or slugs) whose entities should be unavailable. Only flips an entity to unavailable when all of its effective category IDs are in this list. Unlisted → unchanged. This is a delta. |
requirePricingCategory | boolean | No | When provided, updates the requirePricingCategory picker option. When true, uncategorized entities are treated as unavailable. |
When both available and unavailable are provided, available is processed first (full reset), then unavailable
overrides: entities whose entire set of effective category IDs are in the unavailable list are flipped to
unavailable.
Uncategorized entities (empty pricingCategoryIds) are always left unchanged by these modes, unless
requirePricingCategory is also set to true (which makes them unavailable at the rendering level).
Returns
{ success: boolean, errors?: string[] }
Example
// By ID
picker.setAvailability({
mode: 'by-id',
rowSeats: {
'row-1234567890_abc123-seat-0_1234567890': true,
'row-1234567890_abc123-seat-1_1234567890': false,
},
areas: {
'area-1234567890_abc123': 50, // number for count
'area-9876543210_def456': false, // boolean for single-customer areas (false=0, true=1)
},
tableSeats: {
'table-1234567890_abc123-seat-0_1234567890': true,
},
tables: {
'table-1234567890_abc123': false,
'table-1234567891_def456': true,
},
});
// By slug (recommended)
picker.setAvailability({
mode: 'by-slug',
rowSeats: {
'row-a-seat-1': false,
'row-a-seat-2': true,
},
areas: {
'standing-room': 50,
'vip-lounge': 0, // number: 0 = sold out
'private-booth': false, // boolean: false = unavailable (same as 0)
},
tableSeats: {
'table-1-seat-1': false,
},
tables: {
'vip-table-1': false,
'table-5': true,
},
});
// By pricing category — only "standard" category entities are available (full reset)
picker.setAvailability({
mode: 'by-pricing-category',
available: ['price_abc123'], // Standard category ID
});
// By pricing category — make "vip" entities unavailable (delta, others unchanged)
picker.setAvailability({
mode: 'by-pricing-category',
unavailable: ['price_def456'], // VIP category ID
});
// By pricing category — combined: available sets baseline, unavailable overrides
picker.setAvailability({
mode: 'by-pricing-category',
available: ['price_abc123', 'price_def456'], // Standard + VIP available
unavailable: ['price_abc123'], // Then override: Standard-only entities → unavailable
});
// By pricing category slug — same as above but using slugs
picker.setAvailability({
mode: 'by-pricing-category-slug',
available: ['standard', 'vip'],
});
// With requirePricingCategory — uncategorized entities also become unavailable
picker.setAvailability({
mode: 'by-pricing-category',
available: ['price_abc123'],
requirePricingCategory: true,
});
// Dangerously by labels (use with caution)
picker.setAvailability({
mode: 'dangerously-by-labels',
rowSeats: [
{ rowLabel: 'A', seatLabel: '1', isAvailable: false },
{ rowLabel: 'A', seatLabel: '2', isAvailable: true },
],
areas: [
{ areaLabel: 'Standing Room', availableCount: 50 },
{ areaLabel: 'Private Booth', availableCount: false }, // boolean: false = 0
],
tableSeats: [
{ tableLabel: 'Table 1', seatLabel: '1', isAvailable: false },
{ tableLabel: 'Table 1', seatLabel: '2', isAvailable: true },
],
tables: [
{ tableLabel: 'VIP Table', isAvailable: false },
{ tableLabel: 'Table 5', isAvailable: true },
],
});setPricingCategories({ mode, ... })
Description
Set or update pricing categories in the picker. The mode parameter controls how the categories are applied:
'replace'— Replace all pricing categories with new data. This mode replaces all category properties (id,label,slug,price,color,customerNotes). Existing seat-to-category assignments are preserved. Entities assigned to omitted categories automatically render as unavailable and are blocked from selection (the "rendering safety net" — no additionalsetAvailabilitycall needed). This relies on therequirePricingCategorysetting (defaulttruefromloadLayout).'update-by-id'— Partially update existing categories by ID. Merges provided fields into matching categories while leaving unmentioned categories untouched. All-or-nothing: if any ID doesn't match an existing category, the entire call fails with no changes applied. This is also the way to do price-only updates — just pass{ id, price }.'update-by-slug'— Partially update existing categories by slug. Merges provided fields into matching categories while leaving unmentioned categories untouched. All-or-nothing: if any slug doesn't match an existing category, the entire call fails with no changes applied. This is also the way to do price-only updates — just pass{ slug, price }.
Note: When using
'update-by-slug'mode, theslugfield is used only for matching — it is not merged into the category. To change a category's slug, use'update-by-id'mode and passslugas an update field.
Parameters
options(Object):
| Property | Type | Required | Description |
|---|---|---|---|
mode | 'replace' | 'update-by-id' | 'update-by-slug' | Yes | How to apply the pricing categories |
pricingCategories | Array | Yes | Array of pricing category objects (shape depends on mode) |
requirePricingCategory | boolean | No | Override whether entities must have a valid pricing category to be selectable. If omitted, inherits the current setting (default true from loadLayout). |
Category Object Properties (mode: 'replace'):
| Property | Type | Required | Description |
|---|---|---|---|
id | string | Yes | Unique identifier |
label | string | Yes | Display label |
slug | string | Yes | URL-friendly identifier |
price | number | Yes | Price in cents |
color | string | Yes | Hex color code |
customerNotes | string | No | Optional notes visible to customers |
Update Object Properties (mode: 'update-by-id'):
| Property | Type | Required | Description |
|---|---|---|---|
id | string | Yes | ID of the category to update (must match existing) |
label | string | No | New display label |
slug | string | No | New slug |
price | number | No | New price in cents |
color | string | No | New hex color |
customerNotes | string | No | New customer notes |
Update Object Properties (mode: 'update-by-slug'):
| Property | Type | Required | Description |
|---|---|---|---|
slug | string | Yes | Slug of the category to match (used for lookup only, not merged) |
label | string | No | New display label |
price | number | No | New price in cents |
color | string | No | New hex color |
customerNotes | string | No | New customer notes |
Returns
{ success: boolean } — In standalone mode this is synchronous. In iframe SDK mode it returns a
Promise<{ success: boolean }>.
Example
// Replace all categories (standalone, synchronous)
const { success } = SeatSquirrel.picker.setPricingCategories({
mode: 'replace',
pricingCategories: [
{ id: 'cat-1', label: 'Standard', slug: 'standard', price: 5000, color: '#3B82F6' },
{ id: 'cat-2', label: 'VIP', slug: 'vip', price: 10000, color: '#EF4444', customerNotes: 'Includes drinks' },
],
});
// Replace all categories (iframe SDK, async)
const { success } = await picker.setPricingCategories({
mode: 'replace',
pricingCategories: [
{ id: 'cat-1', label: 'Standard', slug: 'standard', price: 5000, color: '#3B82F6' },
{ id: 'cat-2', label: 'VIP', slug: 'vip', price: 10000, color: '#EF4444', customerNotes: 'Includes drinks' },
],
});
// Partially update by ID
const { success } = SeatSquirrel.picker.setPricingCategories({
mode: 'update-by-id',
pricingCategories: [
{ id: 'price_abc123', price: 7500, label: 'Standard (updated)' },
{ id: 'price_def456', color: '#10B981' },
],
});
// Partially update by slug
const { success } = await picker.setPricingCategories({
mode: 'update-by-slug',
pricingCategories: [
{ slug: 'standard', price: 7500, label: 'Standard (updated)' },
{ slug: 'vip', color: '#10B981' },
],
});
// Price-only update (pass only the price field — other properties are preserved)
SeatSquirrel.picker.setPricingCategories({
mode: 'update-by-slug',
pricingCategories: [
{ slug: 'standard', price: 4000 },
{ slug: 'vip', price: 8000 },
],
});
// Tier filtering: only offer VIP and Standard for this performance.
// Entities assigned to other categories (e.g., Budget) become unavailable
// because requirePricingCategory defaults to true via loadLayout.
SeatSquirrel.picker.setPricingCategories({
mode: 'replace',
pricingCategories: [
{ id: 'cat-1', label: 'Standard', slug: 'standard', price: 5000, color: '#3B82F6' },
{ id: 'cat-2', label: 'VIP', slug: 'vip', price: 10000, color: '#EF4444' },
],
});getPricingCategories()
Description
Get the current pricing categories from the loaded layout. Returns the full category objects including id, label,
slug, price, color, and customerNotes.
Returns
{ pricingCategories: PricingCategory[] } — In standalone mode this is synchronous. In iframe SDK mode it returns a
Promise<{ pricingCategories: PricingCategory[] }>.
Example
// Standalone mode (synchronous)
const { pricingCategories } = SeatSquirrel.picker.getPricingCategories();
// Iframe SDK mode (async)
const { pricingCategories } = await picker.getPricingCategories();
pricingCategories.forEach(cat => {
console.log(cat.label, cat.price, cat.color);
});assignPricingCategories(input)
Description
Assign pricing categories to layout objects (rows, seats, areas, tables, table seats) programmatically. This is useful when pricing categories are managed externally and need to be mapped to layout objects after loading.
Atomic semantics: If any assignment fails validation, none are applied. All errors are returned in the errors
array.
The mode parameter controls how objects and categories are identified:
'by-id'— Reference objects and categories by their internal UUIDs.'by-slug'— Reference objects and categories by their user-defined slugs. This is the recommended mode.'dangerously-by-labels'— Reference objects by human-readable labels (can be ambiguous across sections). Categories are always referenced by slug in this mode.
Parameters
input(AssignPricingCategoriesInput):
| Property | Type | Required | Description |
|---|---|---|---|
mode | 'by-id' | 'by-slug' | 'dangerously-by-labels' | Yes | How to identify objects and categories |
Additional properties for mode 'by-id':
| Property | Type | Required | Description |
|---|---|---|---|
rows | Record<string, string[]> | No | Map of row ID to category IDs |
rowSeats | Record<string, string[]> | No | Map of seat ID to category IDs |
areas | Record<string, string[]> | No | Map of area ID to category IDs |
tables | Record<string, string[]> | No | Map of table ID to category IDs |
tableSeats | Record<string, string[]> | No | Map of table seat ID to category IDs |
Additional properties for mode 'by-slug':
Same shape as 'by-id', but keys are slugs (e.g., { 'row-a': ['vip', 'standard'] }).
Additional properties for mode 'dangerously-by-labels':
| Property | Type | Required | Description |
|---|---|---|---|
rows | Array<{ typeId?, rowLabel, pricingCategorySlugs }> | No | Row assignments by label |
rowSeats | Array<{ typeId?, rowLabel, seatLabel, pricingCategorySlugs }> | No | Seat assignments by label |
areas | Array<{ typeId?, areaLabel, pricingCategorySlugs }> | No | Area assignments by label |
tables | Array<{ typeId?, tableLabel, pricingCategorySlugs }> | No | Table assignments by label |
tableSeats | Array<{ typeId?, tableLabel, seatLabel, pricingCategorySlugs }> | No | Table seat assignments by label |
Returns
AssignPricingCategoriesResult
{ success: boolean, errors?: string[] } — In standalone mode this is synchronous. In iframe SDK mode it returns a
Promise.
Example
// Assign by slug (recommended)
const result = SeatSquirrel.picker.assignPricingCategories({
mode: 'by-slug',
rows: {
'row-a': ['vip', 'standard'],
'row-b': ['standard'],
},
areas: {
'vip-lounge': ['vip'],
},
});
// Assign by ID
const result = await picker.assignPricingCategories({
mode: 'by-id',
rows: {
'row-uuid-1': ['cat-uuid-1', 'cat-uuid-2'],
'row-uuid-2': ['cat-uuid-2'],
},
});
// Dangerously by labels (use with caution)
const result = SeatSquirrel.picker.assignPricingCategories({
mode: 'dangerously-by-labels',
rows: [
{ rowLabel: 'A', pricingCategorySlugs: ['vip', 'standard'] },
{ typeId: 'type-uuid', rowLabel: 'A', pricingCategorySlugs: ['economy'] },
],
});
if (!result.success) {
console.error('Assignment errors:', result.errors);
}setSelections(input, options?)
Description
Programmatically set selections in the picker. Use this to restore a previous selection state (e.g., from a saved cart) or to pre-select items on behalf of the user.
The engine validates each item against the loaded layout — checking that the entity exists, is available, and that the
pricing category is valid when the entity has pricing categories assigned. Uncategorized bookable entities can be
selected with categoryId: null / omitted categorySlug. Valid items are applied even when some items fail
(best-effort semantics).
Sections: items are resolved against the parent layout plus every section the visitor has already drilled into
(their contents are cached), regardless of which scope is currently active — so you can restore a visitor's earlier
in-section picks from the parent view. A seat in a section that has never been visited cannot be resolved and fails
with entity-not-found; setSelections never triggers a section-content fetch. To restore in-section selections
before the visitor has drilled anywhere, call prefetchSections() first, then apply.
Selecting into a non-active section does not change what the visitor is viewing — the seat renders selected when they
next drill in. Availability and category validation use the section content's current (last-pushed) state.
Two input modes:
by-id(default) — Pass fullSelectionItemobjects with machine UUIDs. Backward compatible with the legacy input shape.by-slug— Pass simplifiedSlugSelectionItemobjects with human-readable slugs. The engine resolves slugs to IDs and enriches the fullSelectionIteminternally. Recommended for integrations that don't track internal UUIDs.
The options.mode parameter controls how new selections interact with existing ones:
'replace'(default) — Clears all existing selections before applying the new ones.'merge'— Adds to existing selections without clearing.
After selections are applied, onSelectionChanged fires automatically with the updated state.
Parameters
input: EitherSelectionData(legacy/by-id) orSetSelectionsInput(discriminated bymode):
| Input Mode | Shape | Description |
|---|---|---|
by-id (default) | { selections: SelectionItem[] } or { mode: 'by-id', selections: SelectionItem[] } | Full SelectionItem objects with machine IDs |
by-slug | { mode: 'by-slug', selections: SlugSelectionItem[] } | Simplified objects with human-readable slugs |
options(Object, optional):
| Property | Type | Required | Description |
|---|---|---|---|
mode | 'replace' | 'merge' | No | How to apply selections (default: 'replace') |
Returns
SetSelectionsResult
{ success: boolean, applied: SelectionItem[], failed: SetSelectionsFailedItem[] } — In standalone mode this is
synchronous. In iframe SDK mode it returns a Promise.
successistrueonly if all items were applied (no failures).appliedcontains the items that were successfully selected.failedcontains each failed item with areasonexplaining why.
Example — by-slug mode
// Standalone mode — pre-select items using slugs
const result = SeatSquirrel.picker.setSelections({
mode: 'by-slug',
selections: [
{ type: 'row-seat', slug: 'row-a-seat-1', categorySlug: 'standard' },
{ type: 'area', slug: 'vip-lounge', categorySlug: 'vip', quantity: 2 },
{ type: 'table-seat', slug: 'table-1-seat-3', categorySlug: 'premium' },
{ type: 'whole-table', tableSlug: 'table-5', categorySlug: 'standard', quantity: 4 },
],
});
if (!result.success) {
result.failed.forEach(f => {
console.warn(`Failed: ${f.selection.slug || f.selection.id} — ${f.reason}`);
});
}
// Iframe SDK mode — by-slug (async)
const result = await picker.setSelections({
mode: 'by-slug',
selections: [
{ type: 'row-seat', slug: 'row-a-seat-1', categorySlug: 'standard' },
{ type: 'row-seat', slug: 'row-b-seat-5', categorySlug: null },
],
});Example — by-id mode
// Standalone mode — restore a saved cart (replace mode, default)
const result = SeatSquirrel.picker.setSelections({
selections: [
{ id: 'seat-id-1', type: 'row-seat', categoryId: 'cat-1' },
{ id: 'seat-id-2', type: 'row-seat', categoryId: null },
{ id: 'area-id-1', type: 'area', categoryId: 'cat-2', quantity: 2 },
],
});
if (!result.success) {
result.failed.forEach(f => {
console.warn(`Failed: ${f.selection.id} — ${f.reason}`);
});
}
// Merge mode — add to existing selections
const mergeResult = SeatSquirrel.picker.setSelections(
{
selections: [{ id: 'seat-id-3', type: 'row-seat', categoryId: 'cat-1' /* ...other fields */ }],
},
{ mode: 'merge' }
);
// Iframe SDK mode (async)
const result = await picker.setSelections({
selections: savedCart.selections,
lastSelection: null,
totals: { count: 0, amount: 0 },
});
console.log('Applied:', result.applied.length, 'Failed:', result.failed.length);prefetchSections(sectionIds?)
Description
Fetch and cache Section contents ahead of drill-in, without changing what the visitor is viewing. Each section's
contents are requested through the same channel as a drill-in (standalone: the onRequestSectionContent config
callback; iframe SDK: the requestSectionContent round-trip), so it requires section support to be configured.
Use this before setSelections when restoring selections that live inside sections the visitor hasn't drilled into
yet — without it, those items fail with entity-not-found. Typical restore flow:
- Apply your selections with
setSelections(..., { mode: 'merge' }). - If any items failed and the layout has sections,
await picker.prefetchSections(). - Re-apply just the failed items in
mergemode.
Already-cached sections resolve immediately (they count as loaded and are not re-fetched), so calling this repeatedly
is cheap. A prefetch that's racing a visitor drill-in shares the same fetch — no duplicate requests. Fetches run
through a small concurrency pool (6 at a time), so large sectioned layouts don't burst-load every section at once.
Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
sectionIds | string[] | No | Section ids to prefetch. Omit to prefetch every section in the layout |
Returns
Promise<{ loaded: string[], failed: string[] }> — loaded lists sections whose contents are now cached (including
ones that already were); failed lists unknown ids and sections whose content fetch failed.
Example
// Standalone mode — restore a saved cart that spans sections
let result = SeatSquirrel.picker.setSelections(savedCart, { mode: 'merge' });
if (result.failed.length > 0) {
await SeatSquirrel.picker.prefetchSections();
result = SeatSquirrel.picker.setSelections(
{ selections: result.failed.map(f => f.selection) },
{ mode: 'merge' }
);
}
// Iframe SDK mode — prefetch specific sections up front
const { loaded, failed } = await picker.prefetchSections(['section-id-1', 'section-id-2']);clearSelections()
Description
Clear all selected seats and areas.
Returns
{ success: boolean }
Example
picker.clearSelections();setAllowProceed(allow)
Description
Enable or disable the proceed button. When disabled, the button is greyed out regardless of whether items are selected. Useful for enforcing external validation (e.g., terms acceptance) before allowing the user to proceed.
The proceed button is enabled by default.
Parameters
allow(boolean):trueto enable the proceed button,falseto disable it.
Returns
Standalone: { success: boolean } — iFrame: void (fire-and-forget).
Example
// Disable the proceed button
picker.setAllowProceed(false);
// Re-enable it after external validation passes
picker.setAllowProceed(true);setSeatStatuses(input)
Standalone only, and only in admin mode. Calling it without
admin.enabledreturns{ success: false }and firesonErrorwith codeADMIN_MODE_REQUIRED.
Description
Push per-bookable operational status for a back-office seating plan — what a staff member sees, not a customer. Each bookable gets one of four statuses, which drives its colour on the canvas and the contents of its hover tooltip:
| Status | Colour | Selectable by an administrator |
|---|---|---|
available | pricing category | yes |
sold | red | no |
held | amber | no |
blocked | slate | yes — so it can be unblocked |
Bookables with no entry read as available.
Admin mode is enabled by adding an admin block to window.SeatSquirrelConfig:
window.SeatSquirrelConfig = {
admin: {
enabled: true,
// optional: override any of the three non-available fills (#RRGGBB only)
statusColors: { blocked: '#6B7280' },
},
onLayoutLoaded: () => loadStatuses(),
};Alongside the colours, admin mode replaces the pricing tooltip with a status/booking one, and drops per-item prices, the cart total and the Proceed button — the host page owns whatever bulk action the selection feeds.
Caution: Status is the single source of truth for colour and selectability in admin mode. Do not also call
setAvailability()— the two will disagree.
Statuses are held alongside the layout, never inside it, so getLayout() output
is unaffected and an administrator can never save operational state into a
layout. loadLayout() clears them.
Parameters
input(Object):mode('by-id'): the only supported mode.apply('replace' | 'merge', optional):'replace'(default) drops every previously-pushed status;'merge'keeps ids absent from this payload.rowSeats/tableSeats/areas/tables(Record<string, SeatStatusEntry>, optional): entries keyed by bookable id.
SeatStatusEntry:
status('available' | 'sold' | 'held' | 'blocked')tooltip(string[], optional): lines rendered under the label in the hover tooltip. Rendered as plain text, never as HTML.
Returns
{ success: boolean, errors?: string[] }. Entries with an unrecognised status
are dropped and reported in errors while the rest still apply — a partially
refreshed plan beats one silently showing stale colours. On layouts without
Sections, ids that don't exist in the loaded layout are dropped and reported the
same way (the usual cause is sending slugs or labels where bookable ids are
expected); layouts with Sections skip this check, since section bookables can't
be enumerated until their content is fetched. Passing anything other than an
id-keyed object for a group (an array, for example) fails the whole call with
onError code INVALID_FORMAT.
Example
function loadStatuses() {
fetch(`/api/admin/seat-state/${eventId}`)
.then(r => r.json())
.then(state => {
picker.setSeatStatuses({ mode: 'by-id', rowSeats: state.rowSeats });
});
}
// state.rowSeats looks like:
// {
// "seat_abc": { status: "sold", tooltip: ["Confirmed", "J. Smith", "Barcode 1234"] },
// "seat_def": { status: "blocked", tooltip: ["Blocked"] },
// "seat_ghi": { status: "available" }
// }showToast(options)
Description
Show a toast notification inside the picker UI.
Parameters
options(Object):
| Property | Type | Required | Description |
|---|---|---|---|
title | string | Yes | Main toast message |
variant | string | No | 'info', 'warning', 'error', or 'success' (default: 'info') |
description | string | No | Secondary description text |
action | Object | No | Action button: { label: string, onClick?: function }. Per-toast onClick is called when the action button is clicked |
Returns
- Standalone mode:
{ success: boolean } - iFrame SDK mode:
Promise<{ actionClicked: boolean }>— resolves when the toast is dismissed or the action button is clicked
Example
picker.showToast({ title: 'Seats reserved!', variant: 'success' });
// Per-toast action callback (works in both modes)
const { actionClicked } = await picker.showToast({
title: 'Availability changed',
variant: 'info',
description: 'Some seats are no longer available.',
action: {
label: 'Refresh',
onClick: function () {
fetchLatestAvailability().then(a => picker.setAvailability({ mode: 'by-slug', ...a }));
},
},
});
console.log('User clicked action:', actionClicked);Action handling by mode:
- Standalone mode —
action.onClickruns directly when the button is clicked. - iFrame SDK mode —
action.onClickis called via correlation IDs when the action button is clicked. The returned Promise resolves with{ actionClicked: true }if the action was clicked, or{ actionClicked: false }if the toast was dismissed.
showAlertDialog(options)
Description
Show a modal alert dialog inside the picker UI. The dialog blocks interaction and requires the user to explicitly confirm or cancel.
Note: Only one alert dialog can be shown at a time. Calling
showAlertDialog()while a dialog is open cancels the previous one. In both modes,onConfirmandonCancelwork the same way. In iframe mode, the SDK matches callbacks via internal correlation IDs transparently.
Parameters
options(Object):
| Property | Type | Required | Description |
|---|---|---|---|
title | string | Yes | Dialog title text |
description | string | No | Dialog body/description text |
confirmText | string | No | Confirm button label (default: 'OK') |
cancelText | string | No | Cancel button label (default: 'Cancel') |
variant | string | No | 'info', 'warning', 'error', or 'success' — each with distinct icon and color (default: 'info') |
mode | string | No | 'alert' (OK button only) or 'confirm' (OK/Cancel buttons) (default: 'alert') |
onConfirm | function | No | Called when the user clicks the confirm button |
onCancel | function | No | Called when the user clicks the cancel button |
Returns
{ success: boolean }
Example
picker.showAlertDialog({
title: 'Seat unavailable',
description: 'This seat was just booked by another customer.',
mode: 'alert',
variant: 'error',
onConfirm: function () {
fetchLatestAvailability().then(a => picker.setAvailability({ mode: 'by-slug', ...a }));
},
});
picker.showAlertDialog({
title: 'Session expired',
description: 'Your reservation has expired. Please start again.',
mode: 'alert',
variant: 'error',
onConfirm: function () {
window.location.reload();
},
});showNotice(options)
Description
Show a persistent, non-blocking notice banner in the picker UI. Notices remain visible until dismissed by the user or
removed via removeNotice(). One notice per position — calling showNotice() with the same position replaces the
existing notice (the old notice's onDismiss is not called on replacement).
Parameters
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
options.position | string | Yes | — | 'stage-top', 'cart-top', or 'cart-before-proceed' |
options.variant | string | No | 'info' | 'info', 'warning', 'error', or 'success' |
options.title | string | Yes | — | Main notice message |
options.description | string | No | — | Optional secondary text |
options.dismissable | boolean | No | true | Whether the user can dismiss via X button |
options.action | object | No | — | { label: string, onClick?: () => void } — optional action button |
options.onDismiss | function | No | — | Called when user dismisses (not called on API removal or replacement) |
Positions:
stage-top— Banner overlaying the top of the canvas stage. Use for high-visibility alerts (e.g. "Booking closes in 5 min").cart-top— Top of the selection panel, below the header. Use for selection-context notices (e.g. "Max 4 seats per booking").cart-before-proceed— In the cart footer, above the Proceed button. Use for checkout-blocking warnings (e.g. "Select at least 2 seats").
Returns
{ success: boolean, noticeId: string } — noticeId can be passed to removeNotice().
Example
// Show a warning over the stage
const { noticeId } = picker.showNotice({
position: 'stage-top',
variant: 'warning',
title: 'Booking closes in 5 minutes',
dismissable: false,
});
// Show an info notice at the top of the cart
picker.showNotice({
position: 'cart-top',
variant: 'info',
title: 'Maximum 4 seats per booking',
});
// Show an error with an action button before the Proceed button
picker.showNotice({
position: 'cart-before-proceed',
variant: 'error',
title: 'Please select at least 2 seats',
action: {
label: 'Help',
onClick: function () {
console.log('Help clicked');
},
},
});
// Replace the stage-top notice (same position replaces)
picker.showNotice({
position: 'stage-top',
variant: 'success',
title: 'Seats reserved!',
});removeNotice(noticeId)
Description
Remove a notice by its ID. The notice's onDismiss callback is not called when removed via API — only when the user
clicks the X button.
Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
noticeId | string | Yes | The noticeId returned by showNotice() |
Returns
{ success: boolean } — true if the notice was found and removed.
Example
const { noticeId } = picker.showNotice({
position: 'stage-top',
variant: 'info',
title: 'Loading availability...',
dismissable: false,
});
// Later, remove it programmatically
picker.removeNotice(noticeId);getSelections()
Description
Get the current selections synchronously from cached state.
Returns
(SelectionData) { selections, totals } — current selections and totals
Example
const { selections, totals } = picker.getSelections();
console.log(`${totals.count} items selected, $${totals.amount / 100}`);getTotals()
Description
Get the last known totals without triggering a callback.
Returns
(Object) { count: number, amount: number }
Example
const totals = picker.getTotals();
console.log(`${totals.count} items, $${totals.amount / 100}`);getLayout({ mode })
Description
Get the layout by mode. The mode parameter controls what is returned:
'current'— Returns the real-time layout state with current availability and user selections reflected on all seats, areas, tables, and table seats.'last-saved'— Returns the layout snapshot from whenloadLayout()was called (before any user selections). Returnsnullif no layout has been loaded.
Explicit isAvailable on every seat
In 'current' mode, every seat and table seat has an explicit isAvailable boolean — always true or false, never
undefined or omitted.
Parameters
input(Object):
| Property | Type | Required | Description |
|---|---|---|---|
mode | 'current' | 'last-saved' | Yes | Which layout snapshot to return |
Returns
{ layout: LayoutOutput | null } — In standalone mode this is synchronous. In iframe SDK mode it returns a
Promise<{ layout: LayoutOutput | null }>.
Example
// Standalone mode (synchronous)
const { layout } = SeatSquirrel.picker.getLayout({ mode: 'current' });
// Iframe SDK mode (async)
const { layout } = await picker.getLayout({ mode: 'current' });
if (layout) {
layout.layout.rows.forEach(row => {
row.rowSeats.forEach(seat => {
console.log(seat.label, seat.isAvailable);
});
});
}
// Get the last loaded snapshot
const { layout: savedLayout } = picker.getLayout({ mode: 'last-saved' });
if (savedLayout) {
console.log('Last loaded:', savedLayout);
}isPickerReady()
Description
Check if the picker is ready to receive commands.
Returns
{ pickerReady: boolean } — Whether the picker is ready
setFullscreen(value)
Description
Expand the picker to fill the browser viewport, or restore it. This drives the same state as the built-in fullscreen
button and respects the ui.fullscreenButton option — calling it imperatively still works when the button is hidden.
This is viewport-fill fullscreen, not the browser Fullscreen API: the picker covers the browser window via CSS,
browser chrome stays visible, and no permissions chain is required. Pressing Escape always exits.
In iframe SDK mode, the SDK resizes the iframe element on the host page and locks document.body scroll until exit. In
standalone mode, the page root is expanded directly.
Parameters
value(boolean):trueto enter fullscreen,falseto exit
Returns
{ success: boolean }
Example
document.getElementById('my-fullscreen-button').addEventListener('click', () => {
picker.setFullscreen(true);
});destroy()
iFrame SDK only. Not available in standalone mode — standalone cleanup is handled automatically by the page lifecycle.
Description
Destroy the picker instance. Removes the iframe from the DOM, detaches the PostMessage event listener, and clears all internal state (pending requests, selections, callback maps). Call this when you no longer need the picker — for example, when a modal closes or the user navigates away in a SPA.
Returns
void
Example
picker.destroy();Sections (drill-in layouts)
A Section is a frame on the parent canvas holding its own nested sub-layout (rows, areas, tables). Parent-view
sections show a small "Click to view" hint when the frame is large enough. Visitors click a section to drill in,
select within it, then drill back out — by clicking the grey spotlight (the dimmed area outside the active frame) or the
"Back to layout" pill; clicking another section's frame switches straight into it. While drilled in, the picker fits the
camera to the section and dims everything outside the active frame with a light grey spotlight. This keeps large venues
responsive: the layout you pass to
loadLayout carries only lightweight section shells (frame, thumbnail, counts),
and your host streams each section's full contents lazily, on drill-in.
Two callbacks enable Sections (both modes):
onRequestSectionContent(sectionId)— return that section's contents. May be async. Required for sectioned layouts; omit it and sections are stripped at load (one-time console warning) while the parent scope still renders.onSectionChange(activeSectionId)— fires on drill-in (the id) or back out (null). Push fresh availability for the active section here.
Return shapes for onRequestSectionContent
| Shape | When to use |
|---|---|
SectionContent | The content's entities already carry isAvailable / availableCount. |
{ content, initialAvailability } | Bake availability atomically with the content so it paints correct on first render. initialAvailability.mode: 'by-id' and 'by-slug' are applied automatically (slugs resolve against this section's own entities; unknown slugs are skipped with a console warning); for other modes, set availability on the content yourself. |
SectionContentPayload | The { type: 'seatSquirrelSectionContent', content, ... } wire envelope (e.g. proxied from the multi-tenant /api/layouts/[id]/sections/[sectionId] route). |
Throw or reject to abort the drill-in — the loading overlay clears and the visitor stays in the parent scope.
Example
const picker = new SeatSquirrel.StatelessPicker({
container: '#picker-container',
onRequestSectionContent: async sectionId => {
const res = await fetch(`/api/my-sections/${sectionId}`);
const content = await res.json(); // a SectionContent
return {
content,
initialAvailability: {
mode: 'by-id',
rowSeats: { 'sectionA-seat-1': false, 'sectionA-seat-2': true },
areas: { 'sectionA-area-1': 30 },
},
};
},
// Re-entry serves cached content — re-push availability for the active section.
onSectionChange: activeSectionId => {
if (activeSectionId) {
picker.setAvailability({ mode: 'by-id', rowSeats: latestFor(activeSectionId) });
}
},
onReady: () => picker.loadLayout(parentLayout, { initialAvailability: rootAvailability }),
});Notes & limitations
- Availability is scoped.
setAvailabilitywrites only into the scope the visitor is currently viewing (the active section when drilled in, the parent otherwise). Sections the visitor isn't looking at keep their last-known availability until re-entered. Slug/label resolution spans the parent plus every cached section (so'by-slug'works for the active section), but the write scoping above still applies. setSelectionsresolves cached sections. Items may target bookables in any section the visitor has already drilled into, from any scope — useful for restoring earlier in-section picks. Never-visited sections fail withentity-not-found(no content fetch is triggered), and applying a selection into a non-active section doesn't change what the visitor is viewing; the seat renders selected on their next drill-in.- Selections inside sections are included in
onSelectionChanged/onComplete/getSelections()with their labels — and each selection item carries the container Section'ssectionId/sectionLabel/sectionSlug(allnullfor top-level bookables) — and ingetLayout({ mode: 'current' })underlayout.sectionContents. - iframe latency. The iframe SDK fetches section contents over PostMessage; a request that gets no reply within ~15s rejects and clears the loading overlay.
Next Steps
- Standalone Quick Start — Direct JavaScript API without iFrame
- iFrame Quick Start — Embed via SDK
- Designer API Reference — Designer SDK documentation
- Types Reference — All shared data structures