SeatBuilderSeatBuilder Docs

Integration walkthrough

End-to-end integration — create a chart, publish it, run an event, render the SDK, hold a seat, book it, and verify the webhook delivery.

Integration walkthrough

This page walks through the canonical end-to-end SeatBuilder flow on one long scroll page so you can copy each step into your own integration:

  1. Create a chart (the reusable venue layout).
  2. Publish the chart so events can render it.
  3. Create an event against the published chart.
  4. Render the SDK in the browser.
  5. Hold a seat server-side when the buyer commits to a selection.
  6. Extend the hold at checkout to start the payment window.
  7. Book the seat at checkout.
  8. Receive and verify the webhook delivery on your backend.

All requests are authenticated with an X-Api-Key header — see Authentication for the key model.

1. Create a chart

Send a POST /api/v1/charts with the chart name. The draftVersion object is optional and accepts the editor's chart payload (sections, rows, tables, GA areas, decorative shapes). Most integrators omit it here and use the visual editor to build the layout.

POST /api/v1/charts HTTP/1.1
Host: seatbuilder.org
X-Api-Key: sk_live_xxx
Content-Type: application/json

{ "name": "Demo Venue" }
{
  "chartKey": "ch_01HXYZ...",
  "name": "Demo Venue",
  "draftVersion": null,
  "publishedVersion": null,
  "createdAt": "2026-05-17T12:00:00Z"
}

See Charts API for the full payload reference.

2. Publish the chart

Events can only render charts that have a publishedVersion. Promote the current draft with POST /api/v1/charts/{chartKey}/publish — no body.

POST /api/v1/charts/ch_01HXYZ.../publish HTTP/1.1
Host: seatbuilder.org
X-Api-Key: sk_live_xxx

The response echoes the chart with publishedVersion populated. From this point on, re-publishing the chart updates the layout live for every existing event that references this chart (see Versioning for the snapshot policy).

3. Create an event

Each event has independent seat availability. Pass the published chartKey and a human-readable name.

POST /api/v1/events HTTP/1.1
Host: seatbuilder.org
X-Api-Key: sk_live_xxx
Content-Type: application/json

{
  "chartKey": "ch_01HXYZ...",
  "name": "Opening night, 2026-09-12"
}
{
  "eventKey": "evt_01HXYZ...",
  "chartKey": "ch_01HXYZ...",
  "name": "Opening night, 2026-09-12",
  "createdAt": "2026-05-17T12:00:01Z"
}

See Events API for the full payload reference.

Multiple performances of the same show

Create one event per performance, all from the same chart. Events share nothing but the layout: holding or booking A-1 on one event never changes A-1 on another, and a general-admission area's remaining on one event is not reduced by sales on another.

Events read the chart's current published version. They don't pin the version they were created from, so republishing the chart updates the layout of every event bound to it.

To protect tickets already sold, POST /charts/{chartKey}/publish is refused with 409 publish_affects_reservations when the new draft would do any of these to a booked or held label, on any event of the chart:

  • remove it;
  • turn it from a seat into a GA area, or back;
  • change its category;
  • shrink a GA area below its booked + held quantity.

failed[] lists each affected label with its eventKey. GET /charts/{chartKey}/publish-check returns the same list without publishing. ?force=true publishes anyway. Existing holds and bookings are kept as they are: a booked seat whose label was removed still shows as booked in /status, but it's no longer on the map or in /objects.

To change the layout of one performance only, first move its event to its own chart with POST /events/{eventKey}/actions/move-to-new-chart-copy, then edit and publish the new chart (the chartKey in the response). The other events keep the original chart. The dashboard offers the same action on the event page, and both send an event.chart_changed webhook so you can update the chart you store for that event.

To create performances from a background job, pass your own eventKey (1–64 characters, A-Z a-z 0-9 _ -, unique across all workspaces and environments). The call is then safe to retry:

  • same eventKey and same chartKey → 200 with the existing event (no duplicate is created);
  • eventKey already used by a different chart, environment or workspace → 409 event_key_conflict.
POST /api/v1/events HTTP/1.1
Host: seatbuilder.org
X-Api-Key: sk_live_xxx
Content-Type: application/json

{ "chartKey": "ch_01HXYZ...", "eventKey": "tg-prod-session-44778" }

DELETE /api/v1/events/{eventKey} removes a performance you no longer need. It's refused with 409 event_has_reservations while any seat is booked or held; expired holds don't count.

Creating events counts toward your plan's event limit (402 plan_limit_reached on the Free plan). For bulk creation, see Rate limits.

4. Render the SDK

Mount the SDK in the buyer's browser via a <script> tag, npm, or the React component — pick whichever fits your stack. The publicKey is a pk_* (publishable) key — safe to expose. See Render a seat map for the standalone recipe, or copy the snippet from Quickstart.

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_01HXYZ...',
    container: 'seats-container',
    apiUrl: 'https://seatbuilder.org',
    maxSelectedObjects: 4,
    holdDurationSeconds: 300, // EXAMPLE value only (not a default); omit → server default 900s
    showHoldCountdown: false, // keep the built-in banner off (default)
    onObjectSelected: ({ objectLabel, holdToken }) => {
      console.log('Selected', objectLabel, 'with hold', holdToken);
    },
  });
</script>

npm

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

const chart = SeatBuilder.render({
  publicKey: 'pk_live_xxx',
  eventKey: 'evt_01HXYZ...',
  container: 'seats-container',
  apiUrl: 'https://seatbuilder.org',
  // A bare number is a total cap; pass { total?, perCategory? } for
  // category-aware caps (see Render a seat map). When a per-category
  // cap fires, onSelectionInvalid carries a `categoryKey`.
  maxSelectedObjects: 4,
  // Deferred-countdown config (both opt-in — see below):
  holdDurationSeconds: 300, // EXAMPLE value only (not a default); omit → server default 900s
  showHoldCountdown: false, // keep the built-in banner off (default)
  onObjectSelected: ({ objectLabel, holdToken }) => {
    // Persist `holdToken` alongside the buyer's cart — you'll use it
    // for the hold, extend, and book calls below.
    console.log('Selected', objectLabel, 'with hold', holdToken);
  },
});

React

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

export function SeatMap({ eventKey }: { eventKey: string }) {
  return (
    <SeatBuilderChart
      publicKey="pk_live_xxx"
      eventKey={eventKey}
      apiUrl="https://seatbuilder.org"
      maxSelectedObjects={4}
      holdDurationSeconds={300}
      showHoldCountdown={false}
      style={{ height: 600 }}
      onObjectSelected={(e) => console.log('Selected', e.objectLabel, 'with hold', e.holdToken)}
    />
  );
}

See React & Next.js for the App Router pattern and the full props reference.

Render your own countdown (SDK config)

The SDK exposes exactly two opt-in config fields that control hold lifetime and the built-in countdown. Both have safe defaults — existing integrations behave unchanged except that the built-in countdown banner no longer auto-shows.

FieldDefaultWhat it does
holdDurationSeconds—Browse-hold lifetime in seconds, passed as ttlSeconds on the initial /hold. No client default — omit to keep the server default (900s). 300 (seen in examples) is only an illustrative shorter value, not a default.
showHoldCountdownfalseWhen true, re-enables the legacy built-in 2-minute warning banner + auto-deselect timer. Leave false to own the visible countdown on your checkout page.

Hold-TTL defaults (single authoritative set — see the canonical table in the SDK reference):

SettingDefaultSource
Browse-hold (initial POST /hold, no ttlSeconds)900sserver
Payment-window (extend on POST /book / /extend)600sserver
SDK holdDurationSecondsnone (omit → server 900s)client

The 300 in the code example above is an EXAMPLE value only — there is no 300 default anywhere.

With the default showHoldCountdown: false, the SDK no longer auto-shows a countdown banner, and an expired browse-hold reconciles automatically in the holder's view via the realtime seat.hold_expired broadcast (no polling). The integrator owns the visible timer on the checkout page, driven by the holdExpiresAt returned from /seats/extend (step 6 below). See the full flow in Hold seats and confirm at checkout.

Choose a holdDurationSeconds shorter than your payment window (ttlSeconds on /extend, default 600s) so abandoned selections free up faster than active checkouts.

5. Hold a seat (server-side)

The SDK call in step 4 already places a transient hold inside the buyer's session. When the buyer adds the seat to a server-side cart, mirror the hold to your backend with POST /api/v1/events/{eventKey}/seats/hold so the seat remains locked even if the buyer's tab closes.

POST /api/v1/events/evt_01HXYZ.../seats/hold HTTP/1.1
Host: seatbuilder.org
X-Api-Key: sk_live_xxx
Content-Type: application/json

{
  "objectLabels": ["A-12", "A-13"],
  "holdToken": "<the holdToken from onObjectSelected>"
}
{
  "chartKey": "chart_8a2b1c",
  "eventKey": "evt_q3-2026-jazz-night",
  "holdToken": "7f6c3a91-b1ef-4d0a-9c84-3f12a8e0b5d2",
  "holdExpiresAt": "2026-10-02T18:55:00.000Z",
  "objects": [
    { "objectLabel": "A-12", "status": "held" },
    { "objectLabel": "A-13", "status": "held" }
  ]
}

See Seats API for the full payload reference and the hold-conflict / hold-expired error envelopes.

6. Extend the hold at checkout

When the buyer advances to the payment screen, refresh the browse holds to the longer payment window with POST /api/v1/events/{eventKey}/seats/extend, passing the chart's holdToken. This is a batch, partial-success call (it never returns 409): seats still held by the token come back in extended[] with a fresh holdExpiresAt; any seat the token no longer holds comes back in failed[] with a generic not_held_by_token reason.

POST /api/v1/events/evt_01HXYZ.../seats/extend HTTP/1.1
Host: seatbuilder.org
X-Api-Key: sk_live_xxx
Content-Type: application/json

{
  "holdToken": "<the chart holdToken>",
  "objectLabels": ["A-12"],
  "ttlSeconds": 600
}
{
  "holdExpiresAt": "2026-10-02T18:55:00.000Z",
  "extended": [
    { "objectLabel": "A-12" }
  ],
  "failed": []
}

Render your visible payment countdown from the returned holdExpiresAt (the SDK does not show it when showHoldCountdown is false — see SDK config). Drop any failed[] seats and re-prompt the buyer for those before booking.

7. Book seats

At checkout, promote the holds to permanent bookings with a single all-or-nothing call to POST /api/v1/events/{eventKey}/seats/book. Pass every seat label in objectLabels[] and the shared holdToken; all the labels must currently be held by that token. If any one of them is no longer held, the whole request fails with 409 and nothing is booked.

POST /api/v1/events/evt_01HXYZ.../seats/book HTTP/1.1
Host: seatbuilder.org
X-Api-Key: sk_live_xxx
Content-Type: application/json

{
  "objectLabels": ["A-12", "A-13"],
  "holdToken": "<same holdToken>",
  "extraData": { "orderId": "ord_98a2" }
}

Success returns 200 with a booked[] array — one object per confirmed seat:

{
  "booked": [
    { "chartKey": "chart_8a2b1c", "eventKey": "evt_01HXYZ...", "objectLabel": "A-12",
      "status": "booked", "bookedAt": "2026-05-17T12:09:00Z" },
    { "chartKey": "chart_8a2b1c", "eventKey": "evt_01HXYZ...", "objectLabel": "A-13",
      "status": "booked", "bookedAt": "2026-05-17T12:09:00Z" }
  ]
}

If a seat is no longer held, the call returns 409 not_held_by_token with a failed[] array — nothing was booked. If the token did hold it but the hold had expired, it's 410 hold_expired instead:

{
  "statusCode": 409,
  "error": "Conflict",
  "message": "One or more seats are no longer held by this token",
  "code": "not_held_by_token",
  "failed": [{ "objectLabel": "A-13", "reason": "not_held_by_token" }]
}

See Error model for the full envelope and code table.

The end-to-end e-commerce variant — including 409 (one or more seats lost) recovery — lives in Hold seats and confirm at checkout.

8. Receive the webhook

Register a webhook endpoint in the dashboard (or via POST /api/v1/webhooks) for seat.booked. Each delivery carries a SeatBuilder-Signature header in the Stripe-style format t=<unix_seconds>,v1=<hex>. Compute HMAC-SHA256 over `${t}.${rawBody}` with your endpoint secret and reject mismatches.

The full verification recipe — including replay protection — lives in Verify a webhook.

Integration walkthrough — SeatBuilder Docs