---
title: "Integrate a Performance Group"
description: "Build a fixed multi-performance checkout with one safe seat hold, your existing payment flow, and per-performance fulfilment."
---

Use a Performance Group when a buyer purchases one fixed run of two to eight
dated performances that share the same seating plan. SeatLayer coordinates the
seat inventory; your Platform application keeps ownership of the offer,
checkout, payment, order, tickets, and customer support.

<Aside type="note" title="This is not a season product">
  A Performance Group includes every listed date. It does not discount prices,
  accept renewals, support exchanges, or allow the buyer to choose a date
  subset. Those are future Season/Package commerce capabilities.
</Aside>

## The complete transaction

<FlowDiagram
  label="Performance Group transaction flow"
  steps={[
    {
      actor: "Your server",
      title: "Creates the fixed run",
      detail: "It creates compatible dated Events, then creates and activates one Performance Group for the exact included dates.",
      tone: "platform",
    },
    {
      actor: "Your server",
      title: "Mints browser access",
      detail: "The browser receives only a short-lived, exact-origin group session token.",
      tone: "platform",
    },
    {
      actor: "Buyer",
      title: "Selects seats for the run",
      detail: "PerformanceGroupPicker creates one all-or-nothing hold across every included performance.",
      tone: "buyer",
    },
    {
      actor: "Your backend",
      title: "Inspects and charges",
      detail: "Your checkout receives the opaque group hold identity, validates the allocations, and runs payment.",
      tone: "payment",
    },
    {
      actor: "SeatLayer",
      title: "Books the complete group",
      detail: "One stable booking reference commits the group or returns a recoverable failure without partially booking a date.",
      tone: "seatlayer",
    },
    {
      actor: "Your application",
      title: "Issues each performance ticket",
      detail: "Your normal fulfilment and customer-support process remains responsible for the resulting tickets.",
      tone: "outcome",
    },
  ]}
/>

There is never a browser loop that asks each Event whether a seat is free. There
is never one child checkout or booking call per Event. The group coordinator
makes the all-or-nothing inventory decision.

## Before you begin

Complete [server authentication and environment setup](/start/authentication)
first. This guide uses a small server-only JSON helper; it keeps the secret key
out of the browser and turns failed SeatLayer responses into checkout errors
your application can handle deliberately.

```ts title="server/seatlayer-json.ts"
export async function seatlayerJson(path: string, init: Omit<RequestInit, "body"> & {
  body?: unknown;
} = {}) {
  const { body, headers, ...request } = init;
  const response = await fetch(`https://api.seatlayer.io${path}`, {
    ...request,
    headers: {
      authorization: `Bearer ${process.env.SEATLAYER_SECRET_KEY}`,
      "content-type": "application/json",
      ...headers,
    },
    body: body === undefined ? undefined : JSON.stringify(body),
  });

  if (!response.ok) throw new Error(`SeatLayer request failed: ${response.status}`);
  return response.status === 204 ? null : response.json();
}
```

Never import this helper into browser code or expose `SEATLAYER_SECRET_KEY` in a
public environment variable. The browser receives only the short-lived `bsg_…`
token minted in step 3.

## 1. Create compatible performances

Create one Platform Event for each date. Before creating the group, confirm all
of the following:

- the same organization and workspace;
- the same live or test mode;
- the same venue, timezone, currency, inventory model, and published chart
  snapshot;
- a start time on every performance;
- assigned, individually bookable capacity-one seats only; and
- Platform/integrator checkout ownership on every Event.

The group stores a fixed chronological member list. Once active, do not add a
late show, silently remove a cancelled show, or swap the chart. Create a new
offer/run through your operational process instead.

## 2. Create and activate the group

Your server uses its secret key to create the fixed run, reviews its compatibility
result, then activates the exact displayed revision.

```ts title="server/create-performance-group.ts"
const group = await seatlayerJson("/v1/performance-groups", {
  method: "POST",
  headers: { "Idempotency-Key": `run-${productionId}` },
  body: {
    name: "Opening weekend",
    externalRef: productionId,
    eventKeys: [fridayEventKey, saturdayEventKey, sundayEventKey],
  },
});

const activated = await seatlayerJson(
  `/v1/performance-groups/${group.performanceGroup.key}/activate`,
  {
    method: "POST",
    body: { expectedRevision: group.performanceGroup.revision },
  },
);
```

An activation can temporarily return a recoverable in-progress result. Follow
its lifecycle location and retry interval until the state is `active`; do not
create a duplicate group or change the expected revision blindly.

## 3. Mint browser access from your backend

The browser needs one short-lived group token. Your backend creates it only
after it has decided that this buyer may start this fixed-run purchase.

```ts title="server/group-access.ts"
const session = await seatlayerJson(
  `/v1/performance-groups/${groupKey}/buyer-access-sessions`,
  {
    method: "POST",
    body: {
      allowedOrigin: "https://tickets.example.com",
      includePublic: true,
      expiresInSeconds: 1800,
      maxQuantity: 4,
      buyerRef: `buyer:${buyer.id}`,
    },
  },
);

return { token: session.token, expiresAt: session.expiresAt };
```

For private allocations, send `channelIdsByEvent` keyed by every member Event.
The group token is one credential, but the server derives each Event scope; the
browser never chooses a channel.

## 4. Render the buyer picker

Use `PerformanceGroupPicker`, not an array of `SeatPicker` instances. For a
run that promises one physical seat across all dates, use the default
`same_seat` mode. For equal-size allocations that may differ by date, set
`selectionMode: "per_performance"` and an exact `numberOfPlacesToSelect`.

```ts title="browser/checkout-step.ts"
const picker = new PerformanceGroupPicker({
  container: "#seat-picker",
  performanceGroup: groupKey,
  buyerAccessTokenProvider: () => fetch("/api/group-access", { method: "POST" })
    .then((response) => response.json()),
  onCheckout: (handoff) => {
    void fetch("/api/checkout/start", {
      method: "POST",
      headers: { "content-type": "application/json" },
      body: JSON.stringify({
        groupKey: handoff.performanceGroup.key,
        holdId: handoff.holdId,
        operationId: handoff.operationId,
      }),
    });
  },
});

await picker.render();
```

## 5. Inspect before payment

When checkout begins, use the secret-key hold-inspection endpoint. Ignore
buyer-posted seats, item prices, and totals. The inspection contains the ordered
performance allocations, common expiry, configured inventory values, and exact
hold status.

```ts title="server/checkout-start.ts"
const hold = await seatlayerJson(
  `/v1/performance-groups/${groupKey}/holds/${operationId}`,
);

if (hold.hold.state !== "committed") {
  return restartSeatSelection();
}

const order = await createCommercialOrderFromTrustedAllocations(hold.hold.allocations);
await authorizePayment(order);
```

Configured inventory values are not a charge instruction. Your checkout applies
its own discounts, fees, tax, and payment policy to the trusted allocations.

## 6. Book after your payment decision

Use a new stable `bookActionId` for this one order attempt and reuse it on every
retry. Keep the same `bookingRef` for the commercial order. A `202` is not a
failure; follow the returned booking location until it reaches a terminal state.

```ts title="server/confirm-inventory.ts"
const booking = await seatlayerJson(
  `/v1/performance-groups/${groupKey}/holds/${operationId}/book`,
  {
    method: "POST",
    body: {
      bookActionId: `pg-book:${order.id}`,
      bookingRef: order.id,
    },
  },
);

if (booking.booking?.state === "book_pending") {
  return pollGroupBooking(groupKey, `pg-book:${order.id}`);
}
if (booking.booking?.state !== "booked") {
  return escalateOrCompensatePayment(order, booking);
}

await issueTicketsForEveryPerformance(order, hold.hold.allocations);
```

Never confirm the commercial order or issue tickets until group booking reaches
the terminal booked state. A terminal `book_failed` may mean partial child
settlement and requires your pre-agreed payment/support recovery path; do not
retry it with a different action ID.

## 7. Test before live use

- [ ] Create a three-date test group using one published chart.
- [ ] Run the same-seat flow and prove a competing buyer cannot take that seat
      on any included date.
- [ ] Run the per-performance flow with a different valid allocation on each
      date.
- [ ] Verify a lost browser response recovers the original group operation.
- [ ] Verify expiry and explicit release return inventory on every date.
- [ ] Verify your backend refuses expired/invalid group holds before payment.
- [ ] Verify booking retries reuse the same `bookActionId` and `bookingRef`.
- [ ] Verify ticket fulfilment creates the right entitlement for every date.
- [ ] Verify no secret key or child Event hold ID reaches browser logs.

For request and response details, read the [Performance Groups API](/server-api/performance-groups)
and [Performance Group picker reference](/buyer-sdk/performance-groups).