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:
- Create a chart (the reusable venue layout).
- Publish the chart so events can render it.
- Create an event against the published chart.
- Render the SDK in the browser.
- Hold a seat server-side when the buyer commits to a selection.
- Extend the hold at checkout to start the payment window.
- Book the seat at checkout.
- 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_xxxThe 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
eventKeyand samechartKey→200with the existing event (no duplicate is created); eventKeyalready 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/sdkimport 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.
| Field | Default | What 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. |
showHoldCountdown | false | When 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):
| Setting | Default | Source |
|---|---|---|
Browse-hold (initial POST /hold, no ttlSeconds) | 900s | server |
Payment-window (extend on POST /book / /extend) | 600s | server |
SDK holdDurationSeconds | none (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.