SeatStudio
Partner Integration Guide

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.

v1.1 Last updated 2026-07-12 · Regions: us · ca · br · eu · uk · za · kr · au (see all)


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:

PartWhat it isWhere it runs
Seating chartA 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 APIA 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.

You never deal with availability storage or live-update transport — the chart keeps itself current on its own. You work with two clean surfaces: the chart component and the booking API.

How it works

flow
 ┌───────────────────── 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                   │
 └────────────────────────────────────────────────────────────────────┘
  1. Render. Mount the chart with an event code and your ticket/price config. It draws the map and shows what's available.
  2. 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.
  3. Confirm. Your server takes payment and calls book with the selected objects and the hold token.
  4. Expire. Holds that are never confirmed are released automatically, so seats never get stuck.

Core concepts

TermMeaning
MapThe layout of a venue: floors, sectors, seat rows, standing areas. Built once in the dashboard and reused across events.
EventA single date/session that sells tickets. Identified by its event codeset 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.
SectorA subdivision of the map, referenced by its sector codeset this to your own sector ID (the identifier the sector already has in your platform). Either seated (assigned seats) or general admission.
SeatAn 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.
ObjectA seat or a GA amount. The API works on lists of objects.
StatusfreereservedByToken (held) → booked (sold); plus blocked (manually withheld).
Hold tokenGroups one reservation. The chart creates it on the first hold and reports it; you pass it to book.
API tokenYour secret server-side key for the booking API. From the dashboard. Never expose it in the browser.
RegionEach 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):

StatusMeaningSelectable in the chart?
freeAvailableyes
reservedByTokenTemporarily held by someoneno (yours show as selected)
bookedSoldno
blockedManually withheldno
The held state's wire value is 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:

  1. Create an account and note your region (see the table below), shown in Settings → Integration.
  2. 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.
  3. 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.
  4. For general admission sectors, set their capacity (in the dashboard or with PUT /sectors).
  5. Copy your API token from Settings → Integration — the Bearer token for every backend call.
Every 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:

setup
region      = us | ca | br | eu | uk | za | kr | au
eventCode   = SHOW-2026-001
apiToken    = <secret, server-side only>
API base    = https://api-<region>.seatstudio.io

Regions

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.

regionLocationAPI baseChart library (import URL)
usGlobal / United States (Ohio)https://api-us.seatstudio.iohttps://map.seatstudio.io/seatstudio-viewer.js
caUS West / N. Californiahttps://api-ca.seatstudio.iohttps://map-ca.seatstudio.io/seatstudio-viewer.js
brBrazil / São Paulohttps://api-br.seatstudio.iohttps://map-br.seatstudio.io/seatstudio-viewer.js
euEurope / Frankfurthttps://api-eu.seatstudio.iohttps://map-eu.seatstudio.io/seatstudio-viewer.js
ukUnited Kingdom / Londonhttps://api-uk.seatstudio.iohttps://map-uk.seatstudio.io/seatstudio-viewer.js
zaAfrica / Cape Townhttps://api-za.seatstudio.iohttps://map-za.seatstudio.io/seatstudio-viewer.js
krAsia / South Korea (Seoul)https://api-kr.seatstudio.iohttps://map-kr.seatstudio.io/seatstudio-viewer.js
auOceania / Australia (Sydney)https://api-au.seatstudio.iohttps://map-au.seatstudio.io/seatstudio-viewer.js
Two things are region-specific: the API base (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)

html
<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:

bash
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.

html
<!-- 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:

html
<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.

The library is built per region — the bundle you load already points at its region's API. That's why the import URL is region-specific and config.region is optional. With a bundler, import mount from the module build instead (see Examples).

6.2 mount() reference

signature
SeatStudioViewer.mount(target, options) → handle
ArgumentTypeDescription
targetstring | ElementFirst positional arg. A CSS selector or DOM element to render into. Throws if not found.
options.configobjectRequired. Event + ticket configuration (§6.3).
options.onSelectionChangefunctionCalled with the selection object on every change.
options.onReserveCallfunctionOptional. Telemetry for each reservation call: { label, ok, status, req, data }.
options.onSelectRequestfunctionOptional. Fires only when config.hideTicketModal is true — render your own ticket picker from its payload (§6.8).
options.localestringUI language: pt-BR, en, or es. Default pt-BR.
options.floorIdstringOptional. Initial floor for multi-level venues.
options.mapobjectOptional. A full inline map JSON — render without loading the map from the event (offline/demo).
options.apiBasestringOptional. Override the regional API base (defaults to the bundle's region).

Returns a handle with methods to control the chart:

MethodDescription
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

json
{
  "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" }
        }
      ]
    }
  ]
}
FieldTypeDescription
eventCodestringRequired. The event's code — your own internal event ID. The chart loads the map and live availability from it.
regionstringOptional. Your account region (us/br/eu/kr/au). The regional bundle already targets its region, so set this only to override.
showCartboolShow the chart's built-in cart panel.
showSectorsListboolShow a side list of sectors with a "from" price.
hideTicketModalboolOptional (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[]arrayCommercial config per sector. A sector with no entry here is not selectable.
sectors[].sectorCodestringThe sector code — your own internal sector ID (matches the map and API sector).
sectors[].soldOutboolMark the whole sector as sold out (greyed, not selectable).
sectors[].maxSelectionnumberMax items a customer may pick in this sector.
sectors[].lotIdstringOptional. Default batch/phase id applied to the sector's items.
sectors[].lotTitlestringOptional. Default batch/phase display name for the sector.
sectors[].tickets[]arrayTicket types offered for this sector (§6.4).

Optional display flags — sensible defaults, override only if needed:

FieldDefaultEffect
showNavigatorauto (off on touch)Zoom / pan navigator controls.
showMinimaptrueMinimap overview.
minimapFromZoomZoom level at which the minimap appears.
seatDotsUntilZoomRender seats as dots until this zoom level (perf on large maps).
zoomOnFloorChangeAuto-fit zoom when the floor changes.
entryAnimationPlay the intro animation on mount.
allowFullscreentrueShow the fullscreen toggle.
debugfalseDebug HUD overlay.

6.4 Ticket types & prices

FieldTypeDescription
codestringThe ticket type's code — your own internal ticket-type ID (e.g. full, half). Returned as ticketCode in the selection.
titlestringDisplay name (e.g. Standard).
descriptionstringOptional sub-label (e.g. Students).
lotIdstringOptional batch/phase id.
lotTitlestringOptional batch/phase display name.
price.amountnumberInteger, in minor units (cents). 15000 = 150.00.
price.currencystringISO currency code (BRL, USD, …).
Prices, batches and limits are yours. The chart only displays them and returns them in the selection — pricing and charging happen on your side.

6.5 The selection object

onSelectionChange receives the full current selection on every change:

json
{
  "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" }
    }
  ]
}
FieldDescription
holdTokenIdentifies the current reservation. Send it to your backend for book.
countTotal number of tickets (GA quantities included).
totalSum of item prices (amount in minor units).
items[].typeseat or ga.
items[].sectorCodeThe sector code → the sector field in the API.
items[].seatIdThe chart's internal seat id (seats only). Use with handle.deselectSeat(seatId); do not send it to the API.
items[].seatLabelThe seat label → the seat field in the API (seats only).
items[].quantityThe amount (GA only).

Map a selection to API objects with one helper:

javascript
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:

GA quantities are tracked in the cart but not auto-held. To guard standing inventory during a long checkout, place a server-side hold for the GA objects.

6.7 Examples (HTML, React & PHP)

Plain HTML — see the full tutorial above.

React (bundler):

jsx
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:

javascript
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
<?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
<?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.

javascript
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.

basics
Base URL:  https://api-<region>.seatstudio.io     (us | ca | br | eu | uk | za | kr | au)
Auth:      Authorization: Bearer <api token>
Body:      application/json

7.1 Authentication

Every backend call sends your API token as a Bearer token:

header
Authorization: Bearer eyJ…

7.2 The object model

Every action takes an objects array. Each object is a seat or a GA amount:

json
"objects": [
  { "sector": "plateia", "seat": "A12" },   // seat: sector + seat
  { "sector": "pista",   "quantity": 2 }     // GA:   sector + quantity (default 1)
]

7.3 The booking lifecycle

lifecycle
   free ──hold──▶ reserved ──book──▶ booked
    ▲   ◀─release-hold─┘    ◀──release──┘
    │
    └────────── (holds auto-expire back to free)
   blocked ◀── change-object-status (manual)
ActionEffectTypical caller
holdfree → reservedchart (auto) / your server (GA guard)
release-holdreserved → freechart (auto) / your server
bookreserved or free → bookedyour server (after payment)
releasebooked → freeyour server (refund/cancel)
change-object-statusseat → 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:

json
{
  "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)
}

GET Retrieve event availability

GET /events/{eventKey}/status

Full snapshot: every non-free seat plus GA counters.

200 response
{
  "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.

Seats not listed are 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).

request
{
  "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.

request
{
  "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.

request
{
  "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
}
You can book directly from 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.

request
{ "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.

request
{ "objects": [{ "sector": "plateia", "seat": "A12" }], "status": "blocked", "holdToken": "9f3c…" }

PUT Set GA capacity

PUT /events/{eventKey}/sectors

Set the capacity of general admission sectors. Do this when you publish the event.

request
{ "sectors": [{ "sector": "pista", "capacity": 1000 }, { "sector": "vip", "capacity": 200 }] }

Response: 204 No Content.

GA capacity is not derived automatically — set it here, or 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}

200 response
{ "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.

200 response
{ "holdToken": "9f3c…", "expiresAt": 1780000900000 }

Errors

HTTPMeaningWhat to do
200OK — may include a partial failed[]Always inspect failed[].
204OK, no body (PUT /sectors)
401Missing/invalid tokenCheck the Authorization header.
402Account has no active billing period (only on hold / book)Resolve billing; calls resume.
403Token used against the wrong region, or a viewer-scoped token calling a backend-only actionUse https://api-<your-region>.seatstudio.io with your backend token.
404Unknown event codeCheck the event code.
422Validation failed — a required field is missing or invalidFix the payload (see the message).

Error bodies are { "message": "…", "status": <int> }, except 402, which returns { "error": "payment_required", "message": "…" }.

Common failed[].reason values:

reasonWhen
not available to holdseat already reserved/booked/blocked
not available to booksame, on confirm
not held by this tokenrelease-hold with a token that doesn't hold it
could not releaseobject wasn't in the expected state
general admission capacity not set for sectorGA sector missing PUT /sectors
changeStatus not supported for general admissionchange-object-status on GA
Overselling is impossible. Concurrent attempts on the same seat → the loser lands in 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:

How to use it:

  1. 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: ushttps://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).
  2. Set eventCode (and region) to a real event of yours and click Apply.
  3. Click seats in the chart → watch the selection and the hold appear.
  4. In the backend console, pick Book → review the pre-filled body → Send.
  5. Copy the generated cURL as the starting point for your server code.
The standalone page is plain HTML + vanilla JS with no build step — read its source as a copy-paste integration reference.

Best practices

Quick reference

Bases

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

MethodPath
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> }

Statusfree · reservedByToken · booked · blocked

ChartSeatStudioViewer.mount(el, { config, onSelectionChange })

Seat Studio · Partner Integration Guide · v1.1