Skip to content

Integrate a Performance Group

Build a fixed 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 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.

The complete transaction

  1. Your serverCreates the fixed run

    It creates compatible dated Events, then creates and activates one Performance Group for the exact included 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 included performance.

  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. The group coordinator makes the all-or-nothing inventory decision.

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

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],
  },
});

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.

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.

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,
      }),
    });
  },
});

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.

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.
  • 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