Skip to content

Integrate a Performance Group

Build a fixed or flexible-date multi-performance checkout with one safe seat hold, your existing payment flow, and per-performance fulfilment.

Updated View as Markdown

Use a Performance Group when a buyer purchases one 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.

This page owns the end-to-end Performance Group workflow. Compare all three inventory models before committing to it, then use the Performance Group picker for buyer UI and the Performance Groups API for exact trusted operations.

Open the public Performance Groups buyer demo to test the multi-performance selection and hold handoff before following the end-to-end server flow below.

The complete transaction

  1. Your serverCreates the run and inclusion policy

    It creates compatible dated Events, then activates one Performance Group as fixed or flexible_dates.

  2. Your serverMints browser access

    The browser receives only a short-lived, exact-origin group session token.

  3. BuyerSelects seats for the run

    PerformanceGroupPicker creates one all-or-nothing hold across every fixed date or the available dates selected for a partial seat.

  4. Your backendInspects and charges

    Your checkout receives the opaque group hold identity, validates the allocations, and runs payment.

  5. SeatLayerBooks the complete group

    One stable booking reference commits the group or returns a recoverable failure without partially booking a date.

  6. Your applicationIssues each performance ticket

    Your normal fulfilment and customer-support process remains responsible for the resulting tickets.

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. SeatLayer makes the all-or-nothing inventory decision for the whole group.

Before you begin

Complete server authentication and environment setup 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.

server/seatlayer-json.tsts
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 even when its hold policy is flexible_dates. 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 run, chooses its inclusion policy, reviews its compatibility result, then activates the exact displayed revision.

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

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.

Omit inclusionMode or pass "fixed" when every date is mandatory. Use "flexible_dates" only when your offer permits a partial seat to be sold for the dates where it is available. The buyer cannot override this server-owned policy.

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 run purchase.

server/group-access.tsts
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. With flexible_dates and same_seat, a partial seat shows its date-by-date availability and can be selected for the available dates without opening or loading another chart.

browser/checkout-step.tsts
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,
        performanceKeys: handoff.performanceKeys, // display/context only
      }),
    });
  },
});

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.

server/checkout-start.tsts
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. For a flexible hold, this authoritative allocation list is also the final set of dates to charge and fulfil; do not reconstruct it from the group member list or trust the browser-posted performanceKeys.

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.

server/confirm-inventory.tsts
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.
  • Create a flexible_dates group, make one seat unavailable on one date, and prove the partial seat can be held for the remaining dates from the same chart.
  • Clear that partial selection and prove the full-date availability view is restored without another chart load.
  • 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 and Performance Group picker reference.

Navigation

Type to search…

↑↓ navigate↵ selectEsc close