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
lockduring payment and sell them withsaleafter 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.