Add seat selection & booking to your site
Everything you need to integrate interactive seat selection and ticket booking with Seat Studio. Two surfaces: a drop-in seating chart for your frontend, and a booking API for your backend.
Introduction
Seat Studio lets your customers pick exactly where they want to sit. You design a venue map once, attach it to an event, and drop an interactive seating chart into your checkout. Selections, holds and sales stay consistent across every visitor in real time — no double-booking, no overselling.
A complete integration has two parts:
| Part | What it is | Where it runs |
|---|---|---|
| Seating chart | A drop-in JavaScript component that renders the map, lets the customer choose seats or quantities, and reports the selection back to you. Live availability is built in. | Your website (browser) |
| Booking API | A REST API to confirm sales, cancel, block seats and read availability. | Your server |
The golden rule: render and reserve in the browser, confirm on your server. The chart holds seats the moment a customer selects them; your backend turns those holds into confirmed sales after payment. Anything not confirmed is released automatically.
How it works
┌───────────────────── YOUR WEBSITE (browser) ──────────────────────┐
│ <SeatStudioViewer> — renders the map, live availability built in │
│ │ │
│ │ customer selects a seat → the chart reserves it for you │
│ ▼ │
│ onSelectionChange(selection) — you get items + holdToken + total │
└───────────────────────────────────┬────────────────────────────────┘
│ POST selection + holdToken
▼
┌────────────────────────── YOUR SERVER ────────────────────────────┐
│ take payment → POST /actions/book (Bearer <api token>) │
│ refunds / cancellations → POST /actions/release │
└────────────────────────────────────────────────────────────────────┘- Render. Mount the chart with an event code and your ticket/price config. It draws the map and shows what's available.
- Select. The customer clicks seats (or picks a quantity for standing areas). The chart holds each selection automatically and reports it via
onSelectionChange, including a hold token. - Confirm. Your server takes payment and calls
bookwith the selected objects and the hold token. - Expire. Holds that are never confirmed are released automatically, so seats never get stuck.
Core concepts
| Term | Meaning |
|---|---|
| Map | The layout of a venue: floors, sectors, seat rows, standing areas. Built once in the dashboard and reused across events. |
| Event | A single date/session that sells tickets. Identified by its event code — set this to your own event ID (the identifier the event already has in your platform), e.g. SHOW-2026-001. The chart and API are always keyed by this code. |
| Sector | A subdivision of the map, referenced by its sector code — set this to your own sector ID (the identifier the sector already has in your platform). Either seated (assigned seats) or general admission. |
| Seat | An assigned place, identified by sector + seat (a label such as A12). |
| General Admission (GA) | A standing/unassigned area sold by quantity, identified by sector + quantity. |
| Object | A seat or a GA amount. The API works on lists of objects. |
| Status | free → reservedByToken (held) → booked (sold); plus blocked (manually withheld). |
| Hold token | Groups one reservation. The chart creates it on the first hold and reports it; you pass it to book. |
| API token | Your secret server-side key for the booking API. From the dashboard. Never expose it in the browser. |
| Region | Each account belongs to one region (e.g. us, eu), chosen at sign-up and fixed. See all regions. |
Status values — used everywhere (selection, API responses):
| Status | Meaning | Selectable in the chart? |
|---|---|---|
free | Available | yes |
reservedByToken | Temporarily held by someone | no (yours show as selected) |
booked | Sold | no |
blocked | Manually withheld | no |
reservedByToken (it mirrors seats.io) — this is exactly what status / currentStatus return. The lifecycle diagrams below write it as reserved for brevity; the literal value is always reservedByToken.Getting started
Before you write any code, set these up in the Seat Studio dashboard:
- Create an account and note your region (see the table below), shown in Settings → Integration.
- Design a map and define its sectors — set each sector code to that sector's ID in your own platform (
plateia,pista, …). You'll reference these exact codes in ticket config and API calls. - Create an event, assign the map, set its event code to that event's ID in your own platform (e.g.
SHOW-2026-001), and publish it. - For general admission sectors, set their capacity (in the dashboard or with
PUT /sectors). - Copy your API token from Settings → Integration — the Bearer token for every backend call.
code is your own internal code. eventCode, sectorCode and each ticket code are your identifiers — reuse the IDs the event, its sectors and your ticket types already have in your platform. Then everything the chart reports and the API returns (eventCode, sectorCode, ticketCode, orderId) maps straight back to your own records, with no extra lookup table.You now have the three things every integration needs:
region = us | ca | br | eu | uk | za | kr | au
eventCode = SHOW-2026-001
apiToken = <secret, server-side only>
API base = https://api-<region>.seatstudio.ioRegions
Pick the region closest to your audience at sign-up. It is immutable and determines your API base URL. Everything (event code, chart, booking API) lives in that region.
region | Location | API base | Chart library (import URL) |
|---|---|---|---|
us | Global / United States (Ohio) | https://api-us.seatstudio.io | https://map.seatstudio.io/seatstudio-viewer.js |
ca | US West / N. California | https://api-ca.seatstudio.io | https://map-ca.seatstudio.io/seatstudio-viewer.js |
br | Brazil / São Paulo | https://api-br.seatstudio.io | https://map-br.seatstudio.io/seatstudio-viewer.js |
eu | Europe / Frankfurt | https://api-eu.seatstudio.io | https://map-eu.seatstudio.io/seatstudio-viewer.js |
uk | United Kingdom / London | https://api-uk.seatstudio.io | https://map-uk.seatstudio.io/seatstudio-viewer.js |
za | Africa / Cape Town | https://api-za.seatstudio.io | https://map-za.seatstudio.io/seatstudio-viewer.js |
kr | Asia / South Korea (Seoul) | https://api-kr.seatstudio.io | https://map-kr.seatstudio.io/seatstudio-viewer.js |
au | Oceania / Australia (Sydney) | https://api-au.seatstudio.io | https://map-au.seatstudio.io/seatstudio-viewer.js |
api-<region>) and the chart library you import (map-<region>). Always use the ones for your region. The chart library is built per region — the bundle you load already targets its region's API, so config.region is optional (override only). A token used against another region's API returns 403. The us URLs are unprefixed (api-us and map.seatstudio.io); every other region carries its code (map-ca, map-br, map-eu, map-uk, map-za, map-kr, map-au).Tutorial: your first integration
End to end in two snippets.
Step 1 — Render the chart (frontend)
<div id="seatmap" style="width:100%;height:640px"></div>
<!-- Import from YOUR region's URL (this example is region "br"). -->
<link rel="stylesheet" href="https://map-br.seatstudio.io/seatstudio-viewer.css" />
<script src="https://map-br.seatstudio.io/seatstudio-viewer.js"></script>
<script>
SeatStudioViewer.mount('#seatmap', {
config: {
eventCode: 'SHOW-2026-001',
showCart: true,
sectors: [
{ sectorCode: 'plateia', maxSelection: 8, tickets: [
{ code: 'full', title: 'Standard', price: { amount: 15000, currency: 'BRL' } },
{ code: 'half', title: 'Reduced', price: { amount: 7500, currency: 'BRL' } },
]},
{ sectorCode: 'pista', maxSelection: 10, tickets: [
{ code: 'full', title: 'Standard', price: { amount: 20000, currency: 'BRL' } },
]},
],
},
onSelectionChange: (selection) => {
// Fires on every change. When ready to pay, send this to your server.
console.log(selection.items, selection.holdToken, selection.total);
},
});
</script>The chart draws the map, marks sold/held seats, and reserves each seat the moment it's selected. The selection.holdToken identifies that reservation.
Step 2 — Confirm the sale (backend)
After the customer pays, your server confirms the held objects:
curl -X POST "https://api-br.seatstudio.io/events/SHOW-2026-001/actions/book" \
-H "Authorization: Bearer $SEATSTUDIO_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"holdToken": "<holdToken from the selection>",
"orderId": "order_42",
"objects": [
{ "sector": "plateia", "seat": "A12" },
{ "sector": "pista", "quantity": 2 }
]
}'A successful response with an empty failed array means the sale is confirmed. That's the whole flow.
Frontend — rendering the seating chart
6.1 Install
Add the library and its stylesheet to your page — from your region's URL (map-<region>.seatstudio.io; us is unprefixed). See the regions table.
<!-- Replace <region> with yours: br, eu, kr, au (or use map.seatstudio.io for us). -->
<link rel="stylesheet" href="https://map-<region>.seatstudio.io/seatstudio-viewer.css" />
<script src="https://map-<region>.seatstudio.io/seatstudio-viewer.js"></script>For example, Europe:
<link rel="stylesheet" href="https://map-eu.seatstudio.io/seatstudio-viewer.css" />
<script src="https://map-eu.seatstudio.io/seatstudio-viewer.js"></script>This exposes a global window.SeatStudioViewer with a mount() method.
config.region is optional. With a bundler, import mount from the module build instead (see Examples).6.2 mount() reference
SeatStudioViewer.mount(target, options) → handle| Argument | Type | Description |
|---|---|---|
target | string | Element | First positional arg. A CSS selector or DOM element to render into. Throws if not found. |
options.config | object | Required. Event + ticket configuration (§6.3). |
options.onSelectionChange | function | Called with the selection object on every change. |
options.onReserveCall | function | Optional. Telemetry for each reservation call: { label, ok, status, req, data }. |
options.onSelectRequest | function | Optional. Fires only when config.hideTicketModal is true — render your own ticket picker from its payload (§6.8). |
options.locale | string | UI language: pt-BR, en, or es. Default pt-BR. |
options.floorId | string | Optional. Initial floor for multi-level venues. |
options.map | object | Optional. A full inline map JSON — render without loading the map from the event (offline/demo). |
options.apiBase | string | Optional. Override the regional API base (defaults to the bundle's region). |
Returns a handle with methods to control the chart:
| Method | Description |
|---|---|
unmount() | Remove the chart and clean up. |
clearSelection() | Clear the whole selection. |
deselect(item) | Remove a selection item (pass one from onSelectionChange). |
deselectSeat(seatId) | Remove a seat by its seatId. |
deselectSeatBy(sector, seat) | Remove a seat by sector code + seat label. |
deselectGa(sectorCode, ticketCode) | Remove a GA line. |
pickTicket(ticketCode) | Host-managed modal: choose the ticket type for the pending seat. |
changeGa(sectorCode, ticketCode, delta) | Host-managed modal: adjust a GA quantity. |
cancelPending() | Host-managed modal: cancel the pending selection. |
6.3 The config object
{
"eventCode": "SHOW-2026-001",
"region": "br",
"showCart": true,
"showSectorsList": false,
"hideTicketModal": false,
"sectors": [
{
"sectorCode": "plateia",
"soldOut": false,
"maxSelection": 8,
"lotId": "phase-1",
"lotTitle": "Phase 1",
"tickets": [
{
"code": "full",
"title": "Standard",
"description": "Adults",
"soldOut": false,
"price": { "amount": 15000, "currency": "BRL" }
}
]
}
]
}| Field | Type | Description |
|---|---|---|
eventCode | string | Required. The event's code — your own internal event ID. The chart loads the map and live availability from it. |
region | string | Optional. Your account region (us/br/eu/kr/au). The regional bundle already targets its region, so set this only to override. |
showCart | bool | Show the chart's built-in cart panel. |
showSectorsList | bool | Show a side list of sectors with a "from" price. |
hideTicketModal | bool | Optional (default false). When true, the chart does not render its ticket picker — it calls onSelectRequest instead so you can render your own (§6.8). |
sectors[] | array | Commercial config per sector. A sector with no entry here is not selectable. |
sectors[].sectorCode | string | The sector code — your own internal sector ID (matches the map and API sector). |
sectors[].soldOut | bool | Mark the whole sector as sold out (greyed, not selectable). |
sectors[].maxSelection | number | Max items a customer may pick in this sector. |
sectors[].lotId | string | Optional. Default batch/phase id applied to the sector's items. |
sectors[].lotTitle | string | Optional. Default batch/phase display name for the sector. |
sectors[].tickets[] | array | Ticket types offered for this sector (§6.4). |
Optional display flags — sensible defaults, override only if needed:
| Field | Default | Effect |
|---|---|---|
showNavigator | auto (off on touch) | Zoom / pan navigator controls. |
showMinimap | true | Minimap overview. |
minimapFromZoom | — | Zoom level at which the minimap appears. |
seatDotsUntilZoom | — | Render seats as dots until this zoom level (perf on large maps). |
zoomOnFloorChange | — | Auto-fit zoom when the floor changes. |
entryAnimation | — | Play the intro animation on mount. |
allowFullscreen | true | Show the fullscreen toggle. |
debug | false | Debug HUD overlay. |
6.4 Ticket types & prices
| Field | Type | Description |
|---|---|---|
code | string | The ticket type's code — your own internal ticket-type ID (e.g. full, half). Returned as ticketCode in the selection. |
title | string | Display name (e.g. Standard). |
description | string | Optional sub-label (e.g. Students). |
lotId | string | Optional batch/phase id. |
lotTitle | string | Optional batch/phase display name. |
price.amount | number | Integer, in minor units (cents). 15000 = 150.00. |
price.currency | string | ISO currency code (BRL, USD, …). |
6.5 The selection object
onSelectionChange receives the full current selection on every change:
{
"eventCode": "SHOW-2026-001",
"holdToken": "9f3c…",
"count": 3,
"total": { "amount": 35000, "currency": "BRL" },
"items": [
{
"type": "seat",
"sectorCode": "plateia",
"seatId": "plateia-A4",
"seatLabel": "A4",
"ticketCode": "full",
"ticketTitle": "Standard",
"lotId": "phase-1",
"lotTitle": "Phase 1",
"price": { "amount": 15000, "currency": "BRL" }
},
{
"type": "ga",
"sectorCode": "pista",
"quantity": 2,
"ticketCode": "full",
"ticketTitle": "Standard",
"price": { "amount": 10000, "currency": "BRL" }
}
]
}| Field | Description |
|---|---|
holdToken | Identifies the current reservation. Send it to your backend for book. |
count | Total number of tickets (GA quantities included). |
total | Sum of item prices (amount in minor units). |
items[].type | seat or ga. |
items[].sectorCode | The sector code → the sector field in the API. |
items[].seatId | The chart's internal seat id (seats only). Use with handle.deselectSeat(seatId); do not send it to the API. |
items[].seatLabel | The seat label → the seat field in the API (seats only). |
items[].quantity | The amount (GA only). |
Map a selection to API objects with one helper:
function selectionToObjects(selection) {
return (selection.items || []).map(it =>
it.type === 'ga'
? { sector: it.sectorCode, quantity: it.quantity }
: { sector: it.sectorCode, seat: it.seatLabel });
}6.6 Automatic reservation
You don't call the hold API from the browser — the chart does it for you:
- Selecting a seat holds it; deselecting releases it.
- The hold is optimistic: the UI updates instantly and reverts that seat if the hold is rejected (e.g. someone else just took it).
- Holds are short-lived and refresh while the customer is active. Anything not confirmed by
bookis released automatically. - Your only server call in the happy path is
book.
hold for the GA objects.6.7 Examples (HTML, React & PHP)
Plain HTML — see the full tutorial above.
React (bundler):
import { useEffect, useRef } from 'react';
import { mount } from '@seatstudio/viewer';
export function SeatPicker({ config, onSelectionChange }) {
const ref = useRef(null);
useEffect(() => {
const handle = mount(ref.current, { config, onSelectionChange, locale: 'en' });
return () => handle.unmount();
}, [config, onSelectionChange]);
return <div ref={ref} style={{ width: '100%', height: 640 }} />;
}Checkout wiring:
let current = null;
SeatStudioViewer.mount('#seatmap', {
config,
onSelectionChange: (sel) => { current = sel; },
});
document.querySelector('#pay').addEventListener('click', async () => {
if (!current?.items.length) return;
await fetch('/api/checkout', { // YOUR endpoint
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
eventCode: current.eventCode,
holdToken: current.holdToken,
items: current.items,
}),
});
// your /api/checkout then takes payment and calls Seat Studio's `book`.
});PHP — server-rendered page. Build the chart config from your own data and print it into the page, then confirm the sale in a backend script. Note how eventCode, sectorCode and ticket code are simply your own record IDs.
<?php // event.php — render the seating chart for one of your events
$region = 'eu'; // your account region
$event = get_event_from_db($_GET['id']); // your data layer
$config = [
'eventCode' => $event->id, // your internal event ID
'showCart' => true,
'sectors' => array_map(fn($s) => [
'sectorCode' => $s->id, // your internal sector ID
'maxSelection' => 8,
'tickets' => array_map(fn($t) => [
'code' => $t->id, // your internal ticket-type ID
'title' => $t->name,
'price' => ['amount' => $t->cents, 'currency' => 'EUR'],
], $s->ticket_types),
], $event->sectors),
];
?>
<div id="seatmap" style="width:100%;height:640px"></div>
<button id="pay">Pay</button>
<link rel="stylesheet" href="https://map-<?= $region ?>.seatstudio.io/seatstudio-viewer.css" />
<script src="https://map-<?= $region ?>.seatstudio.io/seatstudio-viewer.js"></script>
<script>
SeatStudioViewer.mount('#seatmap', {
config: <?= json_encode($config, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE) ?>,
onSelectionChange: (sel) => { window.__sel = sel; },
});
document.getElementById('pay').addEventListener('click', async () => {
const sel = window.__sel;
if (!sel?.items.length) return;
await fetch('/checkout.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ eventCode: sel.eventCode, holdToken: sel.holdToken, items: sel.items }),
});
});
</script><?php // checkout.php — confirm the sale with the booking API (server-side)
$in = json_decode(file_get_contents('php://input'), true);
$region = 'eu';
$apiBase = "https://api-$region.seatstudio.io";
$apiToken = getenv('SEATSTUDIO_API_TOKEN'); // server-side only — never in the browser
// ... take payment here ...
// map the viewer selection → API objects
$objects = array_map(fn($it) =>
$it['type'] === 'ga'
? ['sector' => $it['sectorCode'], 'quantity' => $it['quantity']]
: ['sector' => $it['sectorCode'], 'seat' => $it['seatLabel']],
$in['items']);
$ch = curl_init("$apiBase/events/" . rawurlencode($in['eventCode']) . "/actions/book");
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ["Authorization: Bearer $apiToken", 'Content-Type: application/json'],
CURLOPT_POSTFIELDS => json_encode([
'objects' => $objects,
'holdToken' => $in['holdToken'] ?? null,
'orderId' => 'order_42',
]),
]);
$result = json_decode(curl_exec($ch), true);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
// 200 + empty failed[] = sale confirmed; otherwise inspect $result['failed']
if ($status === 200 && empty($result['failed'])) {
echo json_encode(['ok' => true]);
} else {
http_response_code(409);
echo json_encode(['ok' => false, 'failed' => $result['failed'] ?? null]);
}6.8 Host-managed ticket picker (optional)
By default the chart shows its own ticket-type modal when a customer clicks a seat or a GA sector. To render that step in your own UI, set config.hideTicketModal: true and provide onSelectRequest. The chart reports the pending selection and waits for you to resolve it through the handle.
const handle = SeatStudioViewer.mount('#seatmap', {
config: { ...config, hideTicketModal: true },
onSelectRequest: (req) => {
// req = { mode, sectorCode, sectorName, sectorColor,
// seatId, seatLabel, seatRow, seatNum, lotTitle, tickets }
// mode: 'seat' or 'ga'. Show your own picker, then resolve it:
myTicketPicker.open(req, {
onPick: (ticketCode) => handle.pickTicket(ticketCode),
onChangeGa:(ticketCode, delta) => handle.changeGa(req.sectorCode, ticketCode, delta),
onCancel: () => handle.cancelPending(),
});
},
onSelectionChange: (sel) => { /* confirmed items land here as usual */ },
});onSelectRequest fires only when hideTicketModal is true. The req.tickets array is exactly the sector's configured ticket types.Backend — booking & management
Your server uses the booking API to confirm sales, cancel, block seats and read availability. It is keyed by the event code and authenticated with your API token.
Base URL: https://api-<region>.seatstudio.io (us | ca | br | eu | uk | za | kr | au)
Auth: Authorization: Bearer <api token>
Body: application/json7.1 Authentication
Every backend call sends your API token as a Bearer token:
Authorization: Bearer eyJ…- Get the token from Settings → Integration in the dashboard.
- It is server-side only — never ship it to the browser or commit it.
- A token is bound to your region. Using it against another region's base URL returns
403.
7.2 The object model
Every action takes an objects array. Each object is a seat or a GA amount:
"objects": [
{ "sector": "plateia", "seat": "A12" }, // seat: sector + seat
{ "sector": "pista", "quantity": 2 } // GA: sector + quantity (default 1)
]sectoris always the sector code.- If
seatis present and non-empty → it's a seat. Otherwise → GA. quantitydefaults to1; values below1are treated as1.seatis the label shown on the map (e.g.A12,B-3).
7.3 The booking lifecycle
free ──hold──▶ reserved ──book──▶ booked
▲ ◀─release-hold─┘ ◀──release──┘
│
└────────── (holds auto-expire back to free)
blocked ◀── change-object-status (manual)| Action | Effect | Typical caller |
|---|---|---|
hold | free → reserved | chart (auto) / your server (GA guard) |
release-hold | reserved → free | chart (auto) / your server |
book | reserved or free → booked | your server (after payment) |
release | booked → free | your server (refund/cancel) |
change-object-status | seat → blocked (or custom) | your server |
A single call can mix seats and GA, and can partially succeed: applied objects are counted; conflicts are listed in failed[] (see Errors).
API reference
All paths are relative to https://api-<region>.seatstudio.io. All calls require Authorization: Bearer <api token> and Content-Type: application/json (for bodies).
Action result
Most actions return the same shape:
{
"eventKey": "SHOW-2026-001",
"changed": 2,
"failed": [
{ "sector": "plateia", "seat": "A12", "reason": "not available to book", "currentStatus": "booked" }
],
"holdToken": "9f3c…", // holds only
"holdExpiresAt": 1780000900000 // holds only (epoch ms)
}changed— number of objects the action applied.failed[]— objects that couldn't be applied, each with areasonand the object'scurrentStatus. Always inspect this, even on200.holdToken/holdExpiresAt— present only onhold.
GET Retrieve event availability
GET /events/{eventKey}/status
Full snapshot: every non-free seat plus GA counters.
{
"eventKey": "SHOW-2026-001",
"seats": [
{ "sector": "plateia", "seat": "A12", "status": "booked", "orderId": "order_42", "updatedAt": 1780000000000 }
],
"generalAdmission": [
{ "sector": "pista", "capacity": 1000, "numReserved": 3, "numBooked": 10, "available": 987 }
]
}Each seats[] entry carries sector, seat, status (reservedByToken / booked / blocked), plus holdToken, orderId and updatedAt (epoch ms) when set.
free (expired holds are treated as free and omitted). To show availability to customers, just use the chart — it stays live on its own.POST Hold (reserve)
POST /events/{eventKey}/actions/hold
Reserve objects (free → reserved).
{
"objects": [{ "sector": "plateia", "seat": "A12" }],
"holdToken": "9f3c…", // optional: extend/reuse an existing reservation
"expiresInSeconds": 900 // optional: hold lifetime
}Response 200: an action result including holdToken and holdExpiresAt. If you didn't pass a holdToken, the server creates one — keep it.
POST Release hold
POST /events/{eventKey}/actions/release-hold
Release reserved objects (reserved → free). Requires the holdToken that holds them.
{
"objects": [{ "sector": "plateia", "seat": "A12" }],
"holdToken": "9f3c…"
}POST Book (confirm sale)
POST /events/{eventKey}/actions/book
Confirm a sale (reserved or free → booked). The core backend call.
{
"objects": [
{ "sector": "plateia", "seat": "A12" },
{ "sector": "pista", "quantity": 2 }
],
"holdToken": "9f3c…", // optional: confirm a prior reservation
"orderId": "order_42" // optional: your order id, stored on the objects
}free (no prior hold) — handy for box office / POS.POST Release (cancel sale)
POST /events/{eventKey}/actions/release
Cancel a sale (booked → free), e.g. on refund.
{ "objects": [{ "sector": "plateia", "seat": "A12" }] }POST Change object status
POST /events/{eventKey}/actions/change-object-status
Set a custom status on seats (e.g. blocked to withhold from sale). Not supported for GA.
{ "objects": [{ "sector": "plateia", "seat": "A12" }], "status": "blocked", "holdToken": "9f3c…" }statusis required (any non-blank string;blockedis the common one).holdTokenis optional — pass it to also target seats currently held by that token.
PUT Set GA capacity
PUT /events/{eventKey}/sectors
Set the capacity of general admission sectors. Do this when you publish the event.
{ "sectors": [{ "sector": "pista", "capacity": 1000 }, { "sector": "vip", "capacity": 200 }] }Response: 204 No Content.
hold/book of GA objects fail with general admission capacity not set for sector.GET Retrieve a single seat
GET /events/{eventKey}/objects/{sector}/{seat}
{ "sector": "plateia", "seat": "A12", "status": "reservedByToken", "holdToken": "9f3c…", "orderId": null, "updatedAt": 1780000000000 }POST Mint a hold token
POST /hold-tokens?expiresInSeconds=900
Mint a standalone hold token (without reserving anything yet), to group several hold calls under one token.
{ "holdToken": "9f3c…", "expiresAt": 1780000900000 }Errors
| HTTP | Meaning | What to do |
|---|---|---|
200 | OK — may include a partial failed[] | Always inspect failed[]. |
204 | OK, no body (PUT /sectors) | — |
401 | Missing/invalid token | Check the Authorization header. |
402 | Account has no active billing period (only on hold / book) | Resolve billing; calls resume. |
403 | Token used against the wrong region, or a viewer-scoped token calling a backend-only action | Use https://api-<your-region>.seatstudio.io with your backend token. |
404 | Unknown event code | Check the event code. |
422 | Validation failed — a required field is missing or invalid | Fix the payload (see the message). |
Error bodies are { "message": "…", "status": <int> }, except 402, which returns { "error": "payment_required", "message": "…" }.
Common failed[].reason values:
reason | When |
|---|---|
not available to hold | seat already reserved/booked/blocked |
not available to book | same, on confirm |
not held by this token | release-hold with a token that doesn't hold it |
could not release | object wasn't in the expected state |
general admission capacity not set for sector | GA sector missing PUT /sectors |
changeStatus not supported for general admission | change-object-status on GA |
failed[]. GA beyond capacity → failed[], never an overshoot.Testing with the Sandbox
The dashboard includes a Sandbox that exercises the whole integration with no code — the same surfaces this guide documents.
Layout:
- Init config (left): the chart's config object, editable. Apply re-mounts the chart.
- Live chart (center): the mounted seating chart. Clicking seats reserves them automatically, just like in production.
- Selection (right): the live selection object.
- Backend console (bottom): an Insomnia-style panel to fire the booking API with your API token. Pick an action (
status,book,release, …) — the URL and body are pre-filled from the current selection. It shows the assembled cURL, the HTTP status, and the response.
How to use it:
- Open Settings → Sandbox (your API token is filled in automatically), or the standalone page on your region's chart host, where you paste the token:
us→https://map.seatstudio.io/external-sandbox.html; any other region →https://map-<region>.seatstudio.io/external-sandbox-<region>.html(e.g.https://map-br.seatstudio.io/external-sandbox-br.html). - Set
eventCode(andregion) to a real event of yours and click Apply. - Click seats in the chart → watch the selection and the hold appear.
- In the backend console, pick Book → review the pre-filled body → Send.
- Copy the generated cURL as the starting point for your server code.
Best practices
- Keep your API token on the server. The chart never needs it.
- Always inspect
failed[]on200responses — conflicts are per-object. - Money is integer minor units (
amountin cents) with an explicitcurrency. - Confirm with the
holdTokenfrom the selection so you book exactly what the customer held. - Release on payment failure (or just let the hold expire) to return seats quickly.
- Set GA capacity with
PUT /sectorsat publish time. - Use the event code everywhere — chart config, every API path. Never the internal map id.
- Scale: the chart serves live availability to thousands of concurrent visitors without hitting your servers; your backend only handles confirmed sales.
Quick reference
Bases
region = us | ca | br | eu | uk | za | kr | au
API base : https://api-<region>.seatstudio.io
Auth header: Authorization: Bearer <api token>
Chart lib : https://map-<region>.seatstudio.io/seatstudio-viewer.js (+ .css)
(us is unprefixed: https://map.seatstudio.io/…)Endpoints
| Method | Path |
|---|---|
| GET | /events/{eventKey}/status |
| GET | /events/{eventKey}/objects/{sector}/{seat} |
| POST | /events/{eventKey}/actions/hold |
| POST | /events/{eventKey}/actions/release-hold |
| POST | /events/{eventKey}/actions/book |
| POST | /events/{eventKey}/actions/release |
| POST | /events/{eventKey}/actions/change-object-status |
| PUT | /events/{eventKey}/sectors |
| POST | /hold-tokens?expiresInSeconds= |
Object — seat: { "sector": "<code>", "seat": "<label>" } · GA: { "sector": "<code>", "quantity": <n> }
Status — free · reservedByToken · booked · blocked
Chart — SeatStudioViewer.mount(el, { config, onSelectionChange })