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.
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 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:
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:
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
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
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 and buyer 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 throughonError. - Send only
holdIdto 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, customization, or the SeatingChart reference.