SeatBuilderSeatBuilder Docs
Recipes

Render a seat map

The minimum SDK call — mount the Seats chart inside a div, listen for selections.

Render a seat map

The smallest useful Seats integration is two steps: drop a <div> container into your page and call SeatBuilder.render with your public key, the event key, and the container id.

1. Add a container

Reserve a sized region of the page for the chart. The SDK fills the container — give it explicit width and height (or constrain it with flex / grid) so Konva can compute the canvas viewport. (Skip this if you're using the React component below — it renders its own container.)

<div id="seats-container" style="width: 800px; height: 600px;"></div>

2. Call render

Choose the integration style that matches your stack: a <script> tag needs no build step, npm suits bundler-driven apps, and the React component wraps render() for you.

Script tag

<script src="https://seatbuilder.org/sdk/latest/seatbuilder.iife.min.js"></script>
<script>
  const chart = SeatBuilder.render({
    publicKey: 'pk_live_xxx',
    eventKey: 'evt_xxx',
    container: 'seats-container',
    apiUrl: 'https://seatbuilder.org',
    maxSelectedObjects: 4,
    onSelectionChange: ({ selection }) => {
      // selection.objects is the full current selection;
      // selection.byCategory totals seats per category key.
      console.log('selected', selection.objects.map((o) => o.label));
      console.log('per category', selection.byCategory);
      const canCheckout = !selection.pending && selection.count > 0;
      document.querySelector('#checkout').disabled = !canCheckout;
    },
  });
</script>

npm

npm install @seatbuilder/sdk
import SeatBuilder from '@seatbuilder/sdk';

const chart = SeatBuilder.render({
  publicKey: 'pk_live_xxx',
  eventKey: 'evt_xxx',
  container: 'seats-container',
  apiUrl: 'https://seatbuilder.org',
  maxSelectedObjects: 4,
  onSelectionChange: ({ selection }) => {
    // selection.objects is the full current selection;
    // selection.byCategory totals seats per category key.
    console.log('selected', selection.objects.map((o) => o.label));
    console.log('per category', selection.byCategory);
    const canCheckout = !selection.pending && selection.count > 0;
    document.querySelector<HTMLButtonElement>('#checkout')!.disabled = !canCheckout;
  },
});

React

'use client';
import { SeatBuilderChart } from '@seatbuilder/sdk/react';

<SeatBuilderChart
  publicKey="pk_live_xxx"
  eventKey="evt_xxx"
  apiUrl="https://seatbuilder.org"
  maxSelectedObjects={4}
  style={{ height: 600 }}
  onSelectionChange={({ selection }) => {
    console.log('selected', selection.objects.map((o) => o.label));
    console.log('can checkout', !selection.pending && selection.count > 0);
  }}
/>

The React component renders its own container <div> — no separate #seats-container element needed. See React & Next.js for the full recipe.

publicKey, eventKey, apiUrl, and container are required (the React component manages its own container). apiUrl has no default — pass https://seatbuilder.org; omitting it throws. The remaining options are optional.

What you get

The rendered chart is interactive out of the box:

  • Free seats highlight on hover.
  • Click on a free seat fires onObjectSelected with the seat label and a short-lived holdToken. Stash the token — you'll need it to promote the hold to a booking server-side.
  • Held / booked seats are not selectable and visually dim.
  • maxSelectedObjects caps the buyer's selection; the SDK rejects clicks beyond the cap and fires onSelectionInvalid (no built-in toast — render your own feedback from that callback). Pass a bare number for a single total cap across all categories, or { total?, perCategory? } to cap the total and individual categories — see Category-aware caps below.

The full e-commerce variant — adding-to-cart, confirming at checkout, recovering from expired holds — is documented in Hold seats and confirm at checkout.

On phones

In a small container the SDK switches to its compact layout: tap a section to enter it, tap a seat to select it (tap it again to deselect) — a small card next to the seat shows its details without covering the map — review the selection in the tray at the bottom, and search by section, row or seat. Give the container a fixed height between 360px and about 75vh so the rest of the page still scrolls, and drive checkout from onSelectionChange.

Callbacks reference

onSelectionChange is the primary callback: it fires on every change with a complete snapshot of the whole selection. onObjectSelected / onObjectDeselected are the per-seat alternative — they fire once per seat instead of handing you the full selection.

CallbackPayloadFires when
onSelectionChange{ selection, added, removed, updated, reasons }Any selection change. selection.objects is the full current selection; enable checkout when !selection.pending && selection.count > 0.
onObjectSelected{ objectLabel, holdToken, categoryKey }A single seat is selected and successfully held. Fires once per seat.
onObjectDeselected{ objectLabel }A single seat is deselected (its hold is released best-effort).
onSelectionValid{ selectedObjectLabels }The selection changes and at least one seat is held. selectedObjectLabels is the full current selection.
onSelectionInvalid{ selectedObjectLabels, reason, categoryKey? }The selection becomes empty or a hold fails. reason is 'max_selected', 'hold_expired', or 'hold_failed'. When a per-category cap fired, categoryKey names the capped category; it is omitted when the total cap fired.
onChartRendered{ chartData }The initial chart render completes (fires once).
const chart = SeatBuilder.render({
  publicKey: 'pk_live_xxx',
  eventKey: 'evt_xxx',
  container: 'seats-container',
  apiUrl: 'https://seatbuilder.org',
  maxSelectedObjects: 4,
  onSelectionValid: ({ selectedObjectLabels }) => {
    // The complete current selection, e.g. ['A-12', 'A-13'].
    console.log('Selected so far:', selectedObjectLabels);
  },
  onSelectionInvalid: ({ reason, categoryKey }) => {
    if (reason === 'hold_expired') promptReselect();
    if (reason === 'max_selected' && categoryKey) {
      // A per-category cap fired — tell the buyer which one.
      console.log('Reached the limit for category', categoryKey);
    }
  },
});

Category-aware caps

maxSelectedObjects accepts either a bare number (a single total cap) or an object with total and/or perCategory. The perCategory keys match the category.key values from your chart. When both total and perCategory are set, both are enforced. A general-admission area counts its quantity as N toward both caps (a hold of 50 counts as 50).

const chart = SeatBuilder.render({
  publicKey: 'pk_live_xxx',
  eventKey: 'evt_xxx',
  container: 'seats-container',
  apiUrl: 'https://seatbuilder.org',
  // At most 6 seats total, with no more than 2 in the `vip` category.
  maxSelectedObjects: { total: 6, perCategory: { vip: 2 } },
  onSelectionInvalid: ({ reason, categoryKey }) => {
    if (reason === 'max_selected') {
      console.log(
        categoryKey
          ? `Limit reached for category ${categoryKey}`
          : 'Total seat limit reached',
      );
    }
  },
});

When a per-category cap is the one that blocks the click, onSelectionInvalid carries categoryKey; when the total cap blocks it, categoryKey is omitted.

Configuration reference

OptionTypeRequiredDefaultNotes
publicKeystringyes—A pk_* publishable key — safe to expose in the browser.
eventKeystringyes—The event to render seat availability for.
containerHTMLElement | stringyes—A DOM element or its id. Must have an explicit height.
apiUrlstringyes—Base URL of your SeatBuilder API. Required since @seatbuilder/sdk 1.0.0.
mobile'auto' | 'always' | 'never'no'auto'Compact (mobile) layout chosen from the container's size: under mobileBreakpoint wide, or under 900px wide and 480px tall.
mobileBreakpointnumberno640Container width (px) below which 'auto' picks compact.
mobileTraybooleannotrueBuilt-in selected-seat tray in compact. Turn off if you show your own basket.
onLayoutChange(e) => voidno—{ layout } on the initial choice and every switch.
maxSelectedObjectsnumber | { total?, perCategory? }nounlimitedCaps the buyer's selection. A bare number is the total cap; { total?, perCategory? } adds per-category caps (perCategory keys match category.key, both enforced together, GA quantity counts as N). Clicks beyond a cap surface a toast and fire onSelectionInvalid with reason: 'max_selected' (and categoryKey when a per-category cap fired).
showSeatLabelsbooleannofalseShow per-seat number labels (still hidden below the ~0.43× zoom threshold).

Instance API

SeatBuilder.render() returns a ChartInstance for imperative control:

MemberSignaturePurpose
holdTokenstringThe session hold token. Share it with your backend to book the held seats.
clearSelection() => voidDeselect all seats and release their holds.
selectObjects(labels: string[]) => Promise<void>Programmatically select (and hold) seats by label.
getSeatInfo(label: string) => SeatInfo | nullO(1) lookup of a seat or GA area's position, category and live status. null before render or for unknown labels.
getSelectedObjects() => SelectionSnapshotThe current selection snapshot (same shape onSelectionChange delivers).
destroy() => voidTear down the chart and release all resources.
fitVenue / focusSection / focusSeat / focusSeats / fitSelection—Camera control — see Camera below.

Camera

MethodDescription
fitVenue()Animate to the whole venue (returns to the overview if drilled into a section).
focusSection(sectionKey)Enter a section and frame it.
focusSeat(label)Bring one seat into view, moving as little as possible.
focusSeats(labels)Frame a group of seats (overview when they span several sections).
fitSelection()Frame the current selection; no-op when empty.
Render a seat map — SeatBuilder Docs