SeatSquirrel
DeploymentSelf-Hosted Mode

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.

OptionTypeRequiredDescription
onReady() => voidNoCalled when picker is ready.
onLayoutLoaded() => voidNoCalled when loadLayout() succeeds. Pure confirmation signal — no payload.
onSelectionChanged(data: SelectionData) => voidNoCalled when selections change. data.lastSelection is the item just selected/modified, or null on deselect/clear.
onComplete(data: SelectionData) => voidNoImportant: 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 }) => voidNoCalled when an error occurs.
requirePricingCategorybooleanNoWhen 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.
smartSeatingbooleanNoWhen 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 }NoUI 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) => voidNoCalled whenever the picker enters or exits viewport-fill fullscreen.
onRequestSectionContent(sectionId: string) => SectionContent | { content, initialAvailability? } | Promise<...>NoSections 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) => voidNoSections 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.

OptionTypeRequiredDescription
containerstring | HTMLElementYesCSS selector or DOM element for the iframe
baseUrlstringNoLocation of the SeatSquirrel app (default: https://seatsquirrel.com)
pickerUrlstringNoFull 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

MethodParametersReturns
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)AssignPricingCategoriesInputStandalone: { 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)booleanStandalone: { success: boolean }
iFrame: void
setSeatStatuses(input)SetSeatStatusesInputStandalone only: { success: boolean, errors?: string[] }
showToast(options)ShowToastOptionsStandalone: { 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 database
  • options (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.

PropertyTypeRequiredDescription
requirePricingCategorybooleanNoWhen true, entities without a valid pricing category are treated as unavailable. Default: true. Pass false to allow uncategorized entities to remain selectable.
initialAvailabilitySetAvailabilityInputNoApply 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):
PropertyTypeRequiredDescription
mode'by-id' | 'by-slug' | 'dangerously-by-labels' | 'by-pricing-category' | 'by-pricing-category-slug'YesHow to identify items

Additional properties for mode 'by-id':

PropertyTypeRequiredDescription
rowSeatsRecord<string, boolean>NoMap of seat ID to boolean (true = available)
areasRecord<string, number | boolean>NoMap of area ID to available count (number), or boolean (true = 1, false = 0)
tableSeatsRecord<string, boolean>NoMap of table seat ID to boolean (true = available)
tablesRecord<string, boolean>NoMap 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':

PropertyTypeRequiredDescription
availablestring[]NoCategory IDs (or slugs) whose entities should be available. Unlisted categories → entities unavailable. This is a full reset.
unavailablestring[]NoCategory 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.
requirePricingCategorybooleanNoWhen 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 additional setAvailability call needed). This relies on the requirePricingCategory setting (default true from loadLayout).
  • '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, the slug field is used only for matching — it is not merged into the category. To change a category's slug, use 'update-by-id' mode and pass slug as an update field.

Parameters

  • options (Object):
PropertyTypeRequiredDescription
mode'replace' | 'update-by-id' | 'update-by-slug'YesHow to apply the pricing categories
pricingCategoriesArrayYesArray of pricing category objects (shape depends on mode)
requirePricingCategorybooleanNoOverride 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'):

PropertyTypeRequiredDescription
idstringYesUnique identifier
labelstringYesDisplay label
slugstringYesURL-friendly identifier
pricenumberYesPrice in cents
colorstringYesHex color code
customerNotesstringNoOptional notes visible to customers

Update Object Properties (mode: 'update-by-id'):

PropertyTypeRequiredDescription
idstringYesID of the category to update (must match existing)
labelstringNoNew display label
slugstringNoNew slug
pricenumberNoNew price in cents
colorstringNoNew hex color
customerNotesstringNoNew customer notes

Update Object Properties (mode: 'update-by-slug'):

PropertyTypeRequiredDescription
slugstringYesSlug of the category to match (used for lookup only, not merged)
labelstringNoNew display label
pricenumberNoNew price in cents
colorstringNoNew hex color
customerNotesstringNoNew 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

PropertyTypeRequiredDescription
mode'by-id' | 'by-slug' | 'dangerously-by-labels'YesHow to identify objects and categories

Additional properties for mode 'by-id':

PropertyTypeRequiredDescription
rowsRecord<string, string[]>NoMap of row ID to category IDs
rowSeatsRecord<string, string[]>NoMap of seat ID to category IDs
areasRecord<string, string[]>NoMap of area ID to category IDs
tablesRecord<string, string[]>NoMap of table ID to category IDs
tableSeatsRecord<string, string[]>NoMap 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':

PropertyTypeRequiredDescription
rowsArray<{ typeId?, rowLabel, pricingCategorySlugs }>NoRow assignments by label
rowSeatsArray<{ typeId?, rowLabel, seatLabel, pricingCategorySlugs }>NoSeat assignments by label
areasArray<{ typeId?, areaLabel, pricingCategorySlugs }>NoArea assignments by label
tablesArray<{ typeId?, tableLabel, pricingCategorySlugs }>NoTable assignments by label
tableSeatsArray<{ typeId?, tableLabel, seatLabel, pricingCategorySlugs }>NoTable 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 full SelectionItem objects with machine UUIDs. Backward compatible with the legacy input shape.
  • by-slug — Pass simplified SlugSelectionItem objects with human-readable slugs. The engine resolves slugs to IDs and enriches the full SelectionItem internally. 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 ModeShapeDescription
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):
PropertyTypeRequiredDescription
mode'replace' | 'merge'NoHow 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.

  • success is true only if all items were applied (no failures).
  • applied contains the items that were successfully selected.
  • failed contains each failed item with a reason explaining 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:

  1. Apply your selections with setSelections(..., { mode: 'merge' }).
  2. If any items failed and the layout has sections, await picker.prefetchSections().
  3. Re-apply just the failed items in merge mode.

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

ParameterTypeRequiredDescription
sectionIdsstring[]NoSection 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): true to enable the proceed button, false to 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.enabled returns { success: false } and fires onError with code ADMIN_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:

StatusColourSelectable by an administrator
availablepricing categoryyes
soldredno
heldamberno
blockedslateyes — 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):
PropertyTypeRequiredDescription
titlestringYesMain toast message
variantstringNo'info', 'warning', 'error', or 'success' (default: 'info')
descriptionstringNoSecondary description text
actionObjectNoAction 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 modeaction.onClick runs directly when the button is clicked.
  • iFrame SDK modeaction.onClick is 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, onConfirm and onCancel work the same way. In iframe mode, the SDK matches callbacks via internal correlation IDs transparently.

Parameters

  • options (Object):
PropertyTypeRequiredDescription
titlestringYesDialog title text
descriptionstringNoDialog body/description text
confirmTextstringNoConfirm button label (default: 'OK')
cancelTextstringNoCancel button label (default: 'Cancel')
variantstringNo'info', 'warning', 'error', or 'success' — each with distinct icon and color (default: 'info')
modestringNo'alert' (OK button only) or 'confirm' (OK/Cancel buttons) (default: 'alert')
onConfirmfunctionNoCalled when the user clicks the confirm button
onCancelfunctionNoCalled 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

ParameterTypeRequiredDefaultDescription
options.positionstringYes'stage-top', 'cart-top', or 'cart-before-proceed'
options.variantstringNo'info''info', 'warning', 'error', or 'success'
options.titlestringYesMain notice message
options.descriptionstringNoOptional secondary text
options.dismissablebooleanNotrueWhether the user can dismiss via X button
options.actionobjectNo{ label: string, onClick?: () => void } — optional action button
options.onDismissfunctionNoCalled 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

ParameterTypeRequiredDescription
noticeIdstringYesThe 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 when loadLayout() was called (before any user selections). Returns null if 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):
PropertyTypeRequiredDescription
mode'current' | 'last-saved'YesWhich 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): true to enter fullscreen, false to 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

ShapeWhen to use
SectionContentThe 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.
SectionContentPayloadThe { 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. setAvailability writes 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.
  • setSelections resolves 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 with entity-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's sectionId / sectionLabel / sectionSlug (all null for top-level bookables) — and in getLayout({ mode: 'current' }) under layout.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

On this page