---
title: "SeatPicker reference"
description: "Complete buyer widget options, callbacks, methods, checkout handoff, modal mode, 3D, holds, and responsive behavior."
---

`SeatPicker` is the complete buyer experience: live map, confirmation, tier/GA
controls, selection tray, hold countdown, best available, 2D/3D, view from seat,
mobile bottom sheet, sold-out/closed states, and checkout handoff. For how it
sits beside the server and mobile packages, see the
[seat map API overview](https://seatlayer.io/developers/).

```js title="browser/picker.js"
import { SeatPicker } from "@seatlayer/js";

const picker = new SeatPicker({
  container: "#picker",
  event: "ev_9f3a",
  publicKey: "pk_test_…",
  onCheckout: (_hold, _seats, handoff) => {
    startCheckout({ holdId: handoff.holdId });
  },
});

await picker.render();
```

For a public Platform event, `publicKey` creates one direct, origin-bound,
Public-sale-only bootstrap. The response carries chart geometry and compact
inventory status together; its short-lived bearer stays in memory. Login,
presale, partner, and channel inventory instead use `buyerAccessTokenProvider`
or `buyerAccessToken` from your authenticated backend. Either explicit buyer
credential takes precedence over `publicKey`.

## Choose the surface

| | `SeatPicker` | `SeatingChart` |
|---|---|---|
| Map canvas | Included | Included |
| Cart/tray, CTA, timer, states | Included | Host builds |
| Seat confirmation and 360 view | Included | Host builds |
| Modal helper | `SeatPicker.open()` | No |
| Best for | Complete, styleable buyer flow | Fully custom cart and controls |

For a buyer purchasing a fixed multi-performance run, use
[`PerformanceGroupPicker`](/buyer-sdk/performance-groups) instead. Do not pass
an Event array to `SeatPicker` or create several event holds in the browser.

## Constructor options

| Option | Type | Default/notes |
|---|---|---|
| `container` | `string \| HTMLElement` | Required inline; omit for `open()` |
| `event` | `string` | Required event key |
| `apiBase` | `string` | `https://api.seatlayer.io` |
| `transport` | `PickerTransport` | Built-in public transport; useful for controlled adapters/tests |
| `operationalTelemetry` | `boolean` | Omit for built-in production-only timing, `true` to opt in for another API/custom transport, or `false` to disable |
| `publicKey` | `string` | Publishable key for direct, exact-origin, Public-sale-only Platform bootstrap |
| `buyerAccessTokenProvider` | `({ reason }) => Promise<{ token, expiresAt }>` | Refreshable private/login/presale/channel access from your authenticated backend; takes precedence over `publicKey` |
| `buyerAccessToken` | `string \| { token, expiresAt }` | Non-refreshing escape hatch; prefer the provider |
| `maxSelection` | `number` | `10` |
| `selectedObjects` | `string[]` | Initial object ids or public labels, applied after live availability loads |
| `selectableObjects` | `string[] \| null` | Buyer-selectable allow-list; omit/null for every free object |
| `numberOfPlacesToSelect` | `number` | Require exactly this many seated/table/GA guest units before checkout |
| `selectionValidators` | `PickerSelectionValidator[]` | Optional minimum-count, consecutive-seat, and no-orphan sale guards |
| `locale` | `string` | Browser language → `en`; built-in `en`, `es`, `de`, `fr` |
| `messages` | `Record<string,string>` | Per-message copy overrides |
| `currency` | `string` | `USD` fallback; event/org data wins |
| `colorblindSafe` | `boolean` | `false` |
| `initialView` | `RendererViewMode` | Use `flat`; legacy perspective values are deprecated |
| `enable3D` | `boolean` | `true` when WebGL2 is available |
| `max3DSeats` | `number` | `60,000` desktop; `30,000` on small/low-core devices; host overrides are accepted |
| `onBuyerViewChange` | `({ view, seatId? }) => void` | Map/3D entry, exit, and targeted-seat changes |
| `onAnalytics` | `(event, props) => void` | Optional host-owned Preview load/3D event sink; separate from automatic anonymous load diagnostics |
| `hideBadge` | `boolean` | Deprecated, no effect: the badge follows the account's white-label add-on |
| `hideEventDetails` | `boolean` | Hide duplicate host/event identity while inline; fullscreen restores context |
| `theme` | `SeatPickerTheme` | Partial host overrides |
| `pricing` | `SeatPickerPricing` | Buyer display overrides; not server ledger authority |
| `holdTtlMs` | `number` | Fallback hold duration; an event-dashboard setting takes priority |
| `initialHoldId` | `string` | Verify/restore an existing event hold |
| `restoreHold` | `boolean` | `true`; uses per-event session storage |
| `readOnly` | `boolean` | Render live inventory without selection, holds, or checkout |
| `confirmSelection` | `boolean` | `true` |
| `seatView` | `boolean` | `true` |
| `checkout` | `"handoff" \| "hosted"` | `handoff`; hosted is an explicit managed-commerce opt-in |
| `returnUrl` | `string` | Validated hosted-checkout return location |

## Callbacks

| Callback | Signature | Meaning |
|---|---|---|
| `onCheckout` | `(hold, seats, handoff) => void` | Hold succeeded and buyer chose checkout |
| `onCheckoutUnavailable` | `({ reason, handoff }) => void` | Hosted checkout cannot run; the hold remains available for host handoff |
| `onOrderConfirmed` | `(order) => void` | Hosted payment was confirmed |
| `onBooked` | `(handoff) => void` | Realtime state confirms this handed-off hold was booked |
| `onOfferAvailabilityChange` | `(availability) => void` | Active ticket-release/offer display state changed |
| `onSelectionChange` | `(seats) => void` | Selection changes |
| `onSelectionValidityChange` | `(state) => void` | Exact-count state after each selection change |
| `onSelectionValid` / `onSelectionInvalid` | callback | Exact-count validity transition |
| `onSelectionLimit` | `(max) => void` | A selection attempt reached the active cap |
| `onHoldChange` | `(hold, seats, handoff) => void` | Hold created/restored/extended/partially released/released |
| `onHoldExpired` | `() => void` | Current hold expired and widget reset |
| `onHoldRestored` | `(hold, seats, handoff) => void` | Prior hold verified and restored |
| `onClose` | `() => void` | Modal closes |
| `onAccessExpired` | `(state) => void` | Buyer access session expired; state says whether refresh recovered it |
| `onAccessUnavailable` | `(state) => void` | Scoped private inventory is no longer available |
| `onSelectedObjectUnavailable` | `(state) => void` | A selected, unheld object left this buyer's projection |
| `onError` | `(error) => void` | Render/transport operation fails |

Use the third `onCheckout` argument for new integrations:

```ts
interface CheckoutHandoff {
  holdId: string;
  expiresAt: number;
  currency: string;
  lineItems: CheckoutLineItem[];
  total: number;
}

interface CheckoutLineItem {
  label: string;
  displayLabel?: string;
  displayType?: string;
  objectId: string;
  objectType: "seat" | "booth" | "ga" | "table";
  categoryKey: string;
  tierId: string | null;
  unitPrice: number;
  currency: string;
  quantity: number;
}
```

The browser handoff is excellent display context, but payment still uses a fresh
server hold inspection.

### Scoped channel prices need no browser configuration

When `buyerAccessTokenProvider` returns a session scoped to a priced sales
channel, `SeatPicker` receives only that session's compact price projection and
uses it for seats, tables, booths, GA quantities, ticket tiers, totals, and the
checkout handoff. You do not pass a channel id or duplicate the channel price
book in browser options.

The hold endpoint resolves the same channel price independently and freezes it
with `channelPricingVersion`. Continue to treat the inspected hold as the
checkout authority. `setPricing()` remains a host-side presentation override;
it cannot replace or weaken the server's channel price.

## Public methods

| Method | Result |
|---|---|
| `render()` | `Promise<this>` |
| `close()` | Close modal or destroy inline picker |
| `getSelection()` | Current `PickerSeat[]` |
| `selectObjects(objects)` / `deselectObjects(objects)` | Select/deselect by engine id or public label |
| `clearSelection()` | Clear every unheld selection |
| `selectCategories(keys)` / `deselectCategories(keys)` | Change every selectable object in named categories |
| `setSelectableObjects(objects)` | Replace the buyer allow-list without remounting |
| `setMaxSelection(max)` | Change the active order cap without remounting |
| `getSelectionValidity()` | Rule state, including typed `violations`, held lines, and pending GA tickets |
| `setMapTheme(map)` | Repaint the canvas map without remounting or losing selection |
| `setEventDetailsHidden(hidden)` | Hide/show duplicate host event identity in place |
| `setPricing(pricing)` | Repaint buyer display prices without changing server-authoritative hold prices |
| `isColorblindSafe()` | Current accessible palette state |
| `setColorblindSafe(on)` | Change accessible palette state |
| `setViewMode(mode)` / `getViewMode()` | Legacy 2D renderer mode compatibility |
| `getBuyerView()` | `"map" \| "venue3d"` |
| `setBuyerView(view, { flyToSeatId?, resetView? })` | Switch buyer surface, run a targeted 3D tour, or reset the venue overview |
| `getCurrentHold()` | `HoldResult \| null` |
| `resumeHold(holdId)` | Verify and restore a hold |
| `removeHeldTicket(label)` | Remove one held item and keep the rest |
| `bestAvailable(qty, categoryKey?, options?)` | Hold a server-selected group |
| `release()` | Release current hold and reset |
| `refreshAccess()` | Ask the configured provider for a fresh buyer-access session |
| `destroy()` | Remove DOM, socket, listeners, and timers |

For a fixed-size purchase flow, set `numberOfPlacesToSelect`. For guided buyer
selection, add one or more local rules:

```js
selectionValidators: [
  { type: "minimumSelectedPlaces", minimum: 2 },
  { type: "consecutiveSeats" },
  { type: "noOrphanSeats" },
]
```

`consecutiveSeats` requires one uninterrupted run in the same row and ticket
category. `noOrphanSeats` prevents a choice from newly stranding one free seat
between unavailable neighbours. The built-in CTA stays disabled and shows
localized guidance until every rule is satisfied. `onSelectionValidityChange`
exposes `{ isValid, count, required, remaining, seats, violations }` for custom
host chrome. The full picker counts seated places, grouped-table guest
quantities, held lines, and pending GA quantities together.

## Modal mode

```js
const picker = await SeatPicker.open({
  event: "ev_9f3a",
  onCheckout: (_hold, _seats, handoff) => {
    startCheckout({ holdId: handoff.holdId });
  },
  onClose: () => restoreHostUI(),
});

picker.close();
```

The modal traps focus, closes through Escape/scrim/close button, restores scroll
and prior focus, and fires `onClose`.

## Holds and restore

The picker persists the current hold id in `sessionStorage` by default. On a
return visit it verifies that hold against the same event before showing it as
owned.

Set `restoreHold: false` when your router/cart owns persistence, then supply
`initialHoldId` or call `resumeHold()`. Clear host cart state when
`onHoldChange(null, …)` fires.

The built-in countdown offers an extension in the final minute and resets on
confirmed extension. Server limits and renewal caps remain authoritative.

## Buyer 3D and analytics

{/* MAINTAINER RELEASE GATE: publish operationalTelemetry documentation only
    with the matching SDK runtime. */}

`enable3D` controls whether the Map/3D affordance is offered. The 3D module is
lazy-loaded only after use and falls back to the complete 2D experience when
WebGL2 is unavailable.

Use `setBuyerView("venue3d", { flyToSeatId })` for a guided tour and
`onAnalytics` to forward Preview journey events to your own system.

With the built-in transport and `apiBase: "https://api.seatlayer.io"`, the picker
separately sends one anonymous operational load timing after a successful render.
Custom transports, other API origins, and localhost stay silent unless
`operationalTelemetry: true`; `false` always opts out. The request goes only to
the configured `apiBase`, omits credentials and referrer, and never includes an
access token. It is not routed through `onAnalytics`.

See [3D buyer view](/buyer-sdk/3d-view) and
[buyer analytics](/buyer-sdk/analytics).

## Responsive behavior

Layout responds to the picker container, not only the viewport:

- wide containers show a side panel and docked details;
- narrow containers use a bottom sheet and touch-first map controls; and
- fullscreen uses the native API or a platform fallback.

Give the container a definite height. Test the actual embed width, mobile browser
chrome, keyboard, reduced motion, and colorblind-safe mode.

## Lifecycle checklist

- [ ] Construct once per mounted host view.
- [ ] Await `render()` and expose failures through `onError`.
- [ ] Send only `holdId` to the booking server boundary.
- [ ] Clear host state when the hold disappears.
- [ ] Use stable order ids for booking retries.
- [ ] Call `destroy()` during application unmount.
- [ ] Test inline/modal, narrow/wide, 2D/no-WebGL, expiry, and conflict paths.

Continue with [holds and checkout](/buyer-sdk/holds-and-checkout),
[customization](/customization/buyer-experience), or the
[SeatingChart reference](/buyer-sdk/seating-chart).