Booking sessions

A booking session is a server-held cart for one event. The seats and general-admission capacity it holds belong to it until it is confirmed, cancelled or expires, so a buyer can reload the page without losing their selection and two buyers can never be sold the same seat. The public endpoints need no API key, which lets the booking page drive the flow from the browser while your server keeps the Secret API key for the final confirm.

When to use which

  • Booking sessions — interactive checkout driven from the browser. The page opens a session, holds and releases seats as the buyer clicks, checks out, and your server confirms after payment.
  • Standard sale flow — server-side integrations that hold seats with lock during payment and sell them with sale after confirmation, using the Secret API key and keeping the buyer’s cart on their own side.
  • Direct sale — non-interactive flows with nobody holding the seats: back-office sales, comp tickets, imports.

Lifecycle

State Holds inventory Reached by
ACTIVE yes create; every lock and unlock keeps it here
PENDING_PAYMENT yes checkout; the cart is frozen and the payment grace period starts
CONFIRMED no, sold private confirm; every held line becomes SOLD in one transaction
CANCELLED no public or private cancel
EXPIRED no the hold time (ACTIVE) or the grace period (PENDING_PAYMENT) ran out

The defaults are a fifteen-minute hold and a ten-minute payment grace; both are operator settings. The public session payloads carry expiresAt, expiresInSeconds and serverTime, so a page can run its own countdown without trusting the browser clock; create answers expiresAt and serverTime. The private session payload carries expiresAt and expiresInSeconds, and the list summaries carry expiresInSeconds alone.

Enabling sessions for an organisation

Every public session call, get and cancel included, answers 403 with NOT_ENABLED until the organisation enables public sessions:

PUT /api/private/management/v2.0/session-config/
X-API-Key: {organizationToken}
Content-Type: application/json

{ "publicSessionsEnabled": true }

The private endpoints below work regardless of this setting.

Public endpoints

Base path /api/public/v2.0/session/. There is no API key: the session id returned by create is the credential for every later call. Treat it as a secret and store it where a page reload can find it again, for example in sessionStorage. Like the other public endpoints, these are not part of the published OpenAPI document; this page and the renderer’s BookingSessionClient are the reference.

Method Path Purpose
POST / Open a session. Body { "eventId": "...", "publicKey": "..." }, header Idempotency-Key required. Repeating the same key returns the same session.
GET /{id} State, cart with captured prices, total and the expiry countdown.
POST /{id}/lock Hold seats and general-admission capacity. Seats are additive and tolerate ids the session already owns; a GA capacity is the desired total for that area, so repeats are safe. All-or-nothing.
POST /{id}/unlock Release part of what the session holds. Owner-scoped and idempotent: lines the session does not hold are a no-op.
POST /{id}/checkout Lock anything in the (optional) body that is not held yet, then freeze the cart and start the payment grace. A repeat on a frozen session returns the same payload.
POST /{id}/cancel Release the session. From ACTIVE it releases everything; a session already frozen for payment requires the body { "startOver": true }.

create answers with the session id, the expiry, the server time, maxSeats (the cap on what one session may hold, counted as seats plus general-admission capacity) and ttlSeconds (the hold time the countdown restarts from).

lock, unlock and checkout take a selection; each list holds at most 1000 entries:

{
  "seats": [1001, 1002],
  "groupOfSeats": [{ "id": 77, "capacity": 2 }]
}

Every other public call answers the session:

{
  "sessionId": "2c1f0e3a-...",
  "eventId": "8f4b...",
  "state": "ACTIVE",
  "cart": {
    "seats": [{ "id": 1001, "priceId": 12, "priceName": "Stalls" }],
    "groupOfSeats": [{ "id": 77, "capacity": 2, "priceId": 14, "priceName": "Standing" }]
  },
  "total": 145.0,
  "expiresAt": "2026-09-11T10:15:00",
  "expiresInSeconds": 842,
  "serverTime": "2026-09-11T10:00:58"
}

Prices are captured when a line is locked and carried on the cart from then on.

Private endpoints

Base path /api/private/v2.0/session/, authenticated with X-API-Key: {organizationToken} — the Secret API key from the Editor login response, sent verbatim.

Method Path Purpose
GET / List sessions for an event, newest first. Query eventId (required), state (optional), limit (default 50, capped at 200), offset (default 0).
GET /{id} The full session: id, eventId, state, cart, reference, total, expiresAt, expiresInSeconds, createdAt. The private payloads name the session id and carry no serverTime.
POST /{id}/confirm Convert every held line to SOLD atomically and store the order reference. Body { "reference": "..." } (optional, 255 characters). Valid from PENDING_PAYMENT; a repeat on a CONFIRMED session returns the stored result.
POST /{id}/cancel Release everything the session holds and mark it CANCELLED. Repeats on a CANCELLED or EXPIRED session return the current state.

The list answers summaries: id, state, cartSize (seats plus GA capacity), total, createdAt, expiresInSeconds.

Errors

Errors use the Problem Details body the v2 API returns everywhere, with errorCode and, where noted, extra members.

Status errorCode When Extra members
403 NOT_ENABLED Any public call, get and cancel included, for an organisation that has not enabled public sessions
404 ENTITY_NOT_FOUND Unknown session id, or an eventId and publicKey pair that does not resolve on create
409 SEAT_CONFLICT A lock or checkout line could not be acquired, or a held line can no longer be sold on confirm; the whole call was rolled back conflicts: { seats: [...], groupOfSeats: [...] }
409 SESSION_FROZEN lock or unlock on a session frozen for payment state
409 SESSION_DEAD lock, unlock or checkout on a CONFIRMED, CANCELLED or EXPIRED session, or on one whose hold has run out; cancel on a CONFIRMED session; confirm on a CANCELLED or EXPIRED one state
409 SESSION_NOT_CHECKED_OUT confirm on a session that has not been checked out state
409 START_OVER_REQUIRED Public cancel on a frozen session without startOver: true state
422 EMPTY_CART checkout on a session that holds nothing
429 CAP_SEATS lock or checkout that would take the session past maxSeats maxSeats

get never fails on a dead session: it answers 200 with the stored state and expiresInSeconds: 0, so a page that reads a session back after a reload must check it before reusing the id. A hold that has lapsed still reads ACTIVE until the sweep drains it, and a session frozen for payment refuses lock, so reusing a stored id for further holds needs state ACTIVE and expiresInSeconds above zero. A repeat confirm on a CONFIRMED session and a repeat cancel on a CANCELLED or EXPIRED session also answer 200 with the stored state.

Sessions and the other booking endpoints

A seat held by a live session is refused by lock, sale and directsale with 409 and errorCode: SESSION_HELD; the body’s conflicts.seats names the seats. The seat is reported rather than skipped, so a direct sale cannot silently drop part of an order. An integration that does not use sessions is unaffected.

From the browser

@seatmap.pro/renderer exports a client for the public surface. The session id is stored across reloads; the Secret API key never reaches the page.

import {
  BookingSessionClient,
  BookingSessionError,
  selectionFromCart,
} from '@seatmap.pro/renderer';

const sessions = new BookingSessionClient({ baseUrl: 'https://booking.seatmap.pro', publicKey });

const stored = sessionStorage.getItem('seatmap-session');
const existing = stored ? await sessions.get(stored).catch(() => null) : null;
const reusable = existing !== null && existing.state === 'ACTIVE' && existing.expiresInSeconds > 0;
const sessionId = reusable ? existing.sessionId : (await sessions.create(eventId)).sessionId;
sessionStorage.setItem('seatmap-session', sessionId);

try {
  await sessions.lock(sessionId, selectionFromCart(cart));
} catch (e) {
  if (e instanceof BookingSessionError && e.code === 'SEAT_CONFLICT') {
    showTaken(e.conflicts?.seats ?? []);
  }
}

const frozen = await sessions.checkout(sessionId);
startCountdown(frozen.expiresInSeconds);

selectionFromCart turns the renderer’s cart into the selection lock takes; selectionFromSession builds the selection that releases everything a session still holds, for unlock. To restore the map after a reload, read the session back and mark the seats in session.cart on the renderer. Every method rejects with BookingSessionError, which carries the HTTP status, the errorCode and, on a refused hold, the conflicting seats and areas.

After payment, confirm from your server:

POST /api/private/v2.0/session/{sessionId}/confirm
X-API-Key: {organizationToken}
Content-Type: application/json

{ "reference": "ORDER-2026-000123" }

Operator settings

The hold time, the payment grace, the seat cap per session and the expiry sweep are settings on the booking service, listed under Booking sessions in the booking service configuration. In the Helm chart, global.bookingSessions.enabled serves or withholds the endpoints altogether; see the Helm chart documentation.

Type a feature, setting, endpoint or SDK method.