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
Your serverCreates the fixed run
It creates compatible dated Events, then creates and activates one Performance Group for the exact included dates.
Your serverMints browser access
The browser receives only a short-lived, exact-origin group session token.
BuyerSelects seats for the run
PerformanceGroupPicker creates one all-or-nothing hold across every included performance.
Your backendInspects and charges
Your checkout receives the opaque group hold identity, validates the allocations, and runs payment.
SeatLayerBooks the complete group
One stable booking reference commits the group or returns a recoverable failure without partially booking a date.
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.
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.
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.
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.
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.
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.
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
bookActionIdandbookingRef. - 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.