---
title: "Performance Group picker"
description: "Let a buyer choose seats across a fixed run of performances, hold every allocation together, and hand the group to your checkout."
---

`PerformanceGroupPicker` is the complete buyer seating step for a **fixed** run
of two to eight performances that share one published seating chart. It is
available in `@seatlayer/js` `0.64.0` and later.

It is not a season-ticket, subscription, pass, or buyer-selectable date bundle.
The buyer purchases every performance shown by the picker. Your application
still owns the offer, checkout, payment, commercial order, ticket delivery, and
support.

<Aside type="caution" title="Use a Performance Group only for fixed inclusion">
  Every performance must use the same published chart, venue, timezone,
  currency, environment, and Platform checkout ownership. A buyer cannot pick
  two dates from five in this version. Keep that offer in your commerce system
  until a separate Season/Package product exists.
</Aside>

## Choose the selection mode

| Mode | Buyer experience | Use it when |
|---|---|---|
| `same_seat` | The buyer selects once; those exact seats are held for every included performance. | The run promises the same seat each night. |
| `per_performance` | The buyer chooses an equal number of seats on each dated tab; seats may differ by performance. | The buyer needs the same party size but not the same physical seats. |

Both modes create **one all-or-nothing group hold**. The browser never makes a
separate hold for each Event.

## Before you render

Your trusted backend must create the Performance Group, activate it, and mint a
short-lived `bsg_…` buyer-access session for the exact browser origin. See the
[Performance Groups API](/server-api/performance-groups) for that server-side
setup.

Return only `{ token, expiresAt }` to the browser. Do not put the token in a
URL, persistent browser storage, analytics, or logs.

## Same seat for every performance

```ts title="browser/group-picker.ts"
import { PerformanceGroupPicker } from "@seatlayer/js";

const picker = new PerformanceGroupPicker({
  container: "#seat-picker",
  performanceGroup: "pg_opening_weekend",
  buyerAccessTokenProvider: async () => {
    const response = await fetch("/api/seatlayer/group-access", {
      method: "POST",
      credentials: "same-origin",
      cache: "no-store",
    });
    if (!response.ok) throw new Error("Unable to open seat selection");
    return response.json(); // { token: "bsg_…", expiresAt }
  },
  onCheckout: (handoff) => {
    // This starts your checkout. It does not book inventory yet.
    startCheckout({
      performanceGroup: handoff.performanceGroup.key,
      holdId: handoff.holdId,
      operationId: handoff.operationId,
    });
  },
  onError: (error) => reportSeatSelectionError(error),
});

await picker.render();
```

The picker presents the complete date list, one shared chart, an accessible
selection summary, and one **Hold seats for all performances** action. When the
hold completes, the primary action becomes **Continue to checkout**.

## Different seats on each date

Use `per_performance` only when your offer permits different seats. The picker
reuses one chart and shows dated tabs; it requires the configured party size on
every performance before the hold action becomes available.

```ts title="browser/per-performance-picker.ts"
const picker = new PerformanceGroupPicker({
  container: "#seat-picker",
  performanceGroup: "pg_three-night-run",
  selectionMode: "per_performance",
  numberOfPlacesToSelect: 2,
  buyerAccessTokenProvider: getGroupBuyerAccess,
  onCheckout: (handoff) => {
    beginCheckout({
      holdId: handoff.holdId,
      operationId: handoff.operationId,
    });
  },
});

await picker.render();
```

For example, a two-person purchase must have two seats selected on each tab.
The buyer cannot hold two seats on Friday and one on Saturday.

## Constructor options

| Option | Type | Notes |
|---|---|---|
| `container` | `string \| HTMLElement` | Required inline mount target. |
| `performanceGroup` | `string` | Required active `pg_…` key. Do not pass an Event key. |
| `selectionMode` | `"same_seat" \| "per_performance"` | Defaults to `same_seat`. This is an offer decision made before the picker opens. |
| `numberOfPlacesToSelect` | `number` | Required and at least one for `per_performance`; each tab must contain this many places. |
| `buyerAccessTokenProvider` | `({ reason }) => Promise<{ token, expiresAt }>` | Preferred group-and-origin-bound credential refresh path. |
| `buyerAccessToken` | `string \| { token, expiresAt }` | Non-refreshing alternative; prefer the provider. |
| `holdTtlMs` | `number` | Requested common group hold duration; the server clamps it. |
| `initialHoldId` + `initialOperationId` | `string` pair | Restore only a matched group hold/operation pair. Supply both or neither. |
| `restoreHold` | `boolean` | Defaults to `true`; group storage is separate from single-Event holds. |
| `locale`, `messages`, `languages`, `theme`, `colorblindSafe` | Presentation options | Use the same accessibility and branding controls as `SeatPicker`. |
| `enable3D`, `max3DSeats`, `seatView` | Presentation options | The canonical shared chart is rendered once. |
| `maxSelection`, `selectedObjects`, `selectableObjects`, `selectionValidators` | Selection options | In per-performance mode, exact party size is still required for every date. |

`event`, custom `transport`, Best Available, runtime pricing overrides, and
hosted checkout are intentionally unavailable. Passing a group through
`SeatPicker` or calling one Event's hold route would make the purchase unsafe.

## Checkout handoff

`onHold` and `onCheckout` receive this group-specific object:

```ts title="PerformanceGroupCheckoutHandoff.ts"
interface PerformanceGroupCheckoutHandoff {
  operationId: string;
  holdId: string;
  expiresAt: number;
  performanceGroup: PerformanceGroupDescriptor;
  selectionMode: "same_seat" | "per_performance";
  seatLabels: string[];
  allocations?: Array<{
    eventKey: string;
    seatLabels: string[];
  }>;
}
```

Treat `seatLabels` and `allocations` as buyer display data only. Send the
opaque `holdId` and `operationId` to your backend; it must inspect the group
hold before calculating a charge or booking inventory.

The handoff deliberately never includes child Event hold IDs, authoritative
prices, payment data, or ticket information.

## Hold, recovery, and release

| Buyer state | Picker behavior | Host behavior |
|---|---|---|
| Selecting | Seats are candidates only. | Do not create an order yet. |
| Securing seats | Interaction is locked while the single group operation resolves. | Keep the buyer on the picker; do not start a second hold. |
| Held | Every performance is protected by one common expiry. | Start checkout using the opaque handoff. |
| Conflict | Nothing is presented as held after safe cleanup. | Explain that availability changed and let the buyer choose again. |
| Recovery | The picker checks the same operation after a lost response. | Do not create another operation or guess the outcome. |
| Expired or released | Selection resets and availability refreshes. | Clear the in-progress checkout/cart reference. |

Call `picker.destroy()` when the host route unmounts. The picker clears its
timers, chart instance, and polling connection. It never books inventory;
booking belongs to your server.

Continue with the [end-to-end integration guide](/integrations/performance-groups)
and [group hold inspection and booking](/server-api/performance-groups).