Skip to content

Rules, holds, adapter contract · Browser + Node

@openseat/booking

Seat status, selection rules, hold lifecycle and adapter contracts for OpenSeat. No payment processing; inventory truth stays on your backend.

Seat status types, selection rules, hold lifecycle and adapter contracts. Framework-independent.

  • 36 functions
  • 1 class
  • 28 interfaces
  • 8 types
  • 2 constants
  • MIT
npm i @openseat/booking
first npm release pending

Install commands show the public npm names. The first npm release is pending; the packages are free and MIT licensed.

depends on @openseat/core

Import
import {
  createHoldController,
  validateSelection,
  buildCart,
  createInMemoryBookingAdapter,
} from "@openseat/booking";
import type { BookingAdapter } from "@openseat/booking";

Functions36

function

areaDisplayName

What a buyer calls a GA zone ("Fan Pit"), a zone section ("Gold", or "Section 102" for a numbered one), a booth ("Booth B8") or a whole table ("Table T3").

function areaDisplayName(
  area: Pick<BookableArea, "kind" | "label" | "objectType">,
): string;

Parameters

ParameterTypeDescription
areaPick<BookableArea, "kind" | "label" | "objectType">

Returns

string

@openseat/booking/src/labels.ts

function

bookableAreaForObject

The bookable area a clicked object belongs to: the GA area, zone section, booth or bookAsWhole table itself, the bookAsWhole table that owns a clicked seat, or the zone section that holds a clicked row, table or seat. Undefined for everything else.

function bookableAreaForObject(
  layout: LayoutDocument,
  objectId: string,
): BookableArea | undefined;

Parameters

ParameterTypeDescription
layoutLayoutDocument
objectIdstring

Returns

BookableArea | undefined

@openseat/booking/src/areas.ts

function

bookableAreaForSeat

The bookAsWhole table that owns the seat with public ID seatId, or the zone section it is in, for routing a click or key press on one of its seats to the unit that is sold. Undefined when the seat is unknown, hidden, or neither at such a table nor in a zone.

function bookableAreaForSeat(
  layout: LayoutDocument,
  seatId: string,
): BookableArea | undefined;

Parameters

ParameterTypeDescription
layoutLayoutDocument
seatIdstring

Returns

BookableArea | undefined

@openseat/booking/src/areas.ts

function

boothName

"B8" → "Booth B8"; a label that already says "Booth", "Stall" or "Stand" stays as it is.

function boothName(label: string): string;

Parameters

ParameterTypeDescription
labelstring

Returns

string

@openseat/booking/src/labels.ts

function

buildCart

Turns a basket of seats and area quantities into cart lines with human labels, unit prices, amounts and a total. Consecutive seats in a row share one line ("Stalls, Row D, Seats 1–4"); a whole table is one line ("Table T3 (8 seats)"); GA areas read "4 × Fan Pit" and booths "Booth B8". Labels are built from layout labels only and never contain seat or inventory IDs. Seat lines come first in reading order, then areas in the order given.

function buildCart(input: CartInput): Cart;

Parameters

ParameterTypeDescription
inputCartInput

input takes CartInput:

Returns

@openseat/booking/src/cart.ts

function

categoryOf

The seat's category for this event: the status record wins over the layout.

function categoryOf(
  ctx: SelectionContext,
  seatId: string,
): string | undefined;

Parameters

ParameterTypeDescription
ctxSelectionContext
seatIdstring

Returns

string | undefined

@openseat/booking/src/selection.ts

function

countTickets

Tickets in a basket of seats and area quantities: one per seat, GA place and booth, and a whole table counts its seats (its seats are not counted twice when they are also in seatIds). Unknown areas count their quantity.

function countTickets(
  layout: LayoutDocument,
  seatIds: readonly string[],
  areas?: readonly AreaQuantity[],
): number;

Parameters

ParameterTypeDescription
layoutLayoutDocument
seatIdsreadonly string[]
areasoptionalreadonly AreaQuantity[]

Default []

Returns

number

@openseat/booking/src/selection.ts

function

createHoldController

Hold lifecycle: idle → holding → held → confirming → confirmed, with release, expiry and failure branches.

After confirmed the controller is ready for the next booking: hold() starts a new one straight away (with a new idempotency key), and reset() returns to idle first for UIs that show the start state between bookings.

Expiry is server-authoritative. The controller converts the server's expiresAt to local time using the server's clock (serverTime) when the adapter provides it, so a wrong device clock does not show a wrong countdown. When the countdown ends the state becomes expired; the server will already have released, or be about to release, the seats.

function createHoldController(
  options: HoldControllerOptions,
): HoldController;

Parameters

ParameterTypeDescription
optionsHoldControllerOptions

options takes HoldControllerOptions:

Returns

@openseat/booking/src/hold.ts

function

createInMemoryBookingAdapter

A complete BookingAdapter that keeps inventory in memory, with atomic multi-seat and area holds, idempotent requests, expiry and versioned updates.

Areas (GA and zone-section places, booths, bookAsWhole tables) are known from layout and areaCapacities. A hold takes seats and area quantities together, all or nothing; release returns the capacity and confirm turns it into a booking. Remaining capacity is reported by getAreaStatuses and subscribeAreas.

For demos, tests and prototyping only. It shows the behaviour a real backend must provide; it is not one. Inventory in a browser tab cannot stop two customers in different tabs from buying the same seat.

function createInMemoryBookingAdapter(
  options: InMemoryBookingOptions,
): {
  adapter: BookingAdapter;
  setStatus: (
    seatIds: readonly string[],
    status: SeatStatusRecord["status"],
  ) => SeatStatusRecord[];
  sweep: () => void;
  getStatus: (seatId: string) => SeatStatus;
  getAreaStatus: (inventoryId: string) => AreaStatusRecord | undefined;
  takeArea: (inventoryId: string, quantity: number) => number;
  getBookings: () => readonly Booking[];
};

Parameters

ParameterTypeDescription
optionsInMemoryBookingOptions

options takes InMemoryBookingOptions:

Returns

{ adapter: BookingAdapter; setStatus: (seatIds: readonly string[], status: SeatStatusRecord["status"]) => SeatStatusRecord[]; sweep: () => void; getStatus: (seatId: string) => SeatStatus; getAreaStatus: (inventoryId: string) => AreaStatusRecord | undefined; takeArea: (inventoryId: string, quantity: number) => number; getBookings: () => readonly Booking[]; }

@openseat/booking/src/memory-adapter.ts

function

createInventoryStore

Client-side cache of host inventory. It merges snapshots and real-time updates in a way that tolerates out-of-order delivery, but it is still a cache: holds must be confirmed by the server.

function createInventoryStore(
  initial?: readonly SeatStatusRecord[],
): InventoryStore;

Parameters

ParameterTypeDescription
initialoptionalreadonly SeatStatusRecord[]

Default []

Returns

@openseat/booking/src/inventory.ts

function

describeArea

Builds the text alternative for an area sold as a unit (a zone, booth or whole table), by inventory ID: its name, category, price, "seated" or "standing" for zones, and how many places are left ("seated, 12 left"). status is the host's area status, when known; priceLabel a formatted price. Null for an unknown or hidden area.

function describeArea(
  layout: LayoutDocument,
  inventoryId: string,
  status?: Pick<AreaStatusRecord, "status" | "remaining"> | null,
  priceLabel?: string,
): AreaDescription | null;

Parameters

ParameterTypeDescription
layoutLayoutDocument
inventoryIdstring
statusoptionalPick<AreaStatusRecord, "status" | "remaining"> | null
priceLabeloptionalstring

Returns

@openseat/booking/src/describe.ts

function

describeSeat

Builds the text alternative for a seat, so canvas content is available to assistive technology.

function describeSeat(
  layout: LayoutDocument,
  seatId: string,
  statuses: StatusInput,
  selected: boolean,
): SeatDescription | null;

Parameters

ParameterTypeDescription
layoutLayoutDocument
seatIdstring
statusesStatusInput
selectedboolean

Returns

@openseat/booking/src/describe.ts

function

findNeighbourSeat

Seat to move keyboard focus to from seatId in direction. Picks the nearest seat within a 60° cone, weighting sideways offset more than distance, so Right moves along a row and Down moves to the row below even on curved or staggered layouts. Hidden seats (also those in a hidden row, table or section) are skipped. Returns null at the edge.

function findNeighbourSeat(
  layout: LayoutDocument,
  seatId: string,
  direction: Direction,
  isFocusable?: (info: SeatInfo) => boolean,
): string | null;

Parameters

ParameterTypeDescription
layoutLayoutDocument
seatIdstring
directionDirection
isFocusableoptional(info: SeatInfo) => boolean

Default () => true

Returns

string | null

@openseat/booking/src/navigation.ts

function

findOrphanSeats

Available seats that the selection would leave isolated: a single free seat whose neighbours within its row segment are each taken, selected, or the end of the segment, with at least one neighbour in the selection. Aisles end a segment, so a seat next to an aisle is not stranded by it.

function findOrphanSeats(
  selection: readonly string[],
  ctx: SelectionContext,
): string[];

Parameters

ParameterTypeDescription
selectionreadonly string[]
ctxSelectionContext

Returns

string[]

@openseat/booking/src/selection.ts

function

firstSeat

First seat in reading order, for when the map receives keyboard focus.

function firstSeat(
  layout: LayoutDocument,
  isFocusable?: (info: SeatInfo) => boolean,
): string | null;

Parameters

ParameterTypeDescription
layoutLayoutDocument
isFocusableoptional(info: SeatInfo) => boolean

Default () => true

Returns

string | null

@openseat/booking/src/navigation.ts

function

getBookableAreas

Everything in the layout sold as one unit, keyed by inventory ID: GA areas, zone sections (showSeats: false, keyed by the section ID, kind "ga" with objectType: "section"), booths with an inventoryId, and tables with bookAsWhole (keyed by the table's object ID). Hidden objects, and whole tables inside zone sections, are left out. Cached per layout object.

function getBookableAreas(
  layout: LayoutDocument,
): ReadonlyMap<string, BookableArea>;

Parameters

ParameterTypeDescription
layoutLayoutDocument

Returns

ReadonlyMap<string, BookableArea>

@openseat/booking/src/areas.ts

function

getSeatIndex

Builds (and caches per layout object) the lookup tables booking logic needs.

function getSeatIndex(layout: LayoutDocument): SeatIndex;

Parameters

ParameterTypeDescription
layoutLayoutDocument

Returns

@openseat/booking/src/seat-index.ts

function

isBookingError

function isBookingError(error: unknown): error is BookingError;

Parameters

ParameterTypeDescription
errorunknown

Returns

error is BookingError

@openseat/booking/src/types.ts

function

pruneUnavailable

Removes seats that are no longer available, for example after a real-time update.

function pruneUnavailable(
  selection: readonly string[],
  statuses: StatusInput,
): { selection: readonly string[]; removed: readonly string[] };

Parameters

ParameterTypeDescription
selectionreadonly string[]
statusesStatusInput

Returns

{ selection: readonly string[]; removed: readonly string[]; }

@openseat/booking/src/selection.ts

function

rowName

"D" → "Row D"; a label that already says "Row …" stays as it is.

function rowName(label: string): string;

Parameters

ParameterTypeDescription
labelstring

Returns

string

@openseat/booking/src/labels.ts

function

seatLabel

A seat as a buyer reads it: "Row A, Seat 10", "Table T3, Seat 2", or with the section when needed ("Stalls, Row A, Seat 10"; see SeatLabelOptions.section). Null for a seat that is not in the layout.

function seatLabel(
  layout: LayoutDocument,
  seatId: string,
  options?: SeatLabelOptions,
): string | null;

Parameters

ParameterTypeDescription
layoutLayoutDocument
seatIdstring
optionsoptionalSeatLabelOptions

Default {}

Returns

string | null

@openseat/booking/src/labels.ts

function

sectionBounds

Layout-space bounds of a section: its boundary together with everything in it (rows' seats, tables and their seats, GA areas, loose seats), since seats can sit outside a hand-drawn outline. Null when the ID is not a section or nothing in it has geometry.

function sectionBounds(
  layout: LayoutDocument,
  sectionId: string,
): Bounds | null;

Parameters

ParameterTypeDescription
layoutLayoutDocument
sectionIdstring

Returns

Bounds | null

@openseat/booking/src/areas.ts

function

sectionCategoryId

The category a section is shown in: its own categoryId, or else the most common category of the seats inside it. Undefined when neither has one.

function sectionCategoryId(
  layout: LayoutDocument,
  sectionId: string,
): string | undefined;

Parameters

ParameterTypeDescription
layoutLayoutDocument
sectionIdstring

Returns

string | undefined

@openseat/booking/src/areas.ts

function

sectionName

"102" → "Section 102"; a named section ("Stalls") stays as it is.

function sectionName(label: string): string;

Parameters

ParameterTypeDescription
labelstring

Returns

string

@openseat/booking/src/labels.ts

function

tableName

"T3" → "Table T3"; a label that already says "Table …" stays as it is.

function tableName(label: string): string;

Parameters

ParameterTypeDescription
labelstring

Returns

string

@openseat/booking/src/labels.ts

function

ticketLimitMessage

"You can buy up to 10 tickets."

function ticketLimitMessage(limit: number): string;

Parameters

ParameterTypeDescription
limitnumber

Returns

string

@openseat/booking/src/selection.ts

function

ticketsLeft

How many more tickets the buyer may add to this basket, for example the most a GA quantity picker should offer. Undefined when there is no limit.

function ticketsLeft(
  layout: LayoutDocument,
  rules: SelectionRules,
  seatIds: readonly string[],
  areas?: readonly AreaQuantity[],
): number | undefined;

Parameters

ParameterTypeDescription
layoutLayoutDocument
rulesSelectionRules
seatIdsreadonly string[]
areasoptionalreadonly AreaQuantity[]

Default []

Returns

number | undefined

@openseat/booking/src/selection.ts

function

toBookingError

Wraps anything thrown into a BookingError, preserving BookingErrors as they are.

function toBookingError(error: unknown): BookingError;

Parameters

ParameterTypeDescription
errorunknown

Returns

@openseat/booking/src/types.ts

function

toggleSeat

Applies a click on a seat. Deselecting is always allowed. Selecting is refused for unavailable seats, disallowed categories, mixing categories when not allowed, and exceeding maxSeats. Minimum count and orphans are left to validateSelection, because users fix them by clicking more.

function toggleSeat(
  selection: readonly string[],
  seatId: string,
  ctx: SelectionContext,
  rules?: SelectionRules,
  areas?: readonly AreaQuantity[],
): ToggleResult;

Parameters

ParameterTypeDescription
selectionreadonly string[]
seatIdstring
ctxSelectionContext
rulesoptionalSelectionRules

Default {}

areasoptionalreadonly AreaQuantity[]

Returns

@openseat/booking/src/selection.ts

function

toSeatVisualStates

Combines host statuses and the local selection into what the map should draw.

function toSeatVisualStates(
  statuses: ReadonlyMap<string, SeatStatusRecord | SeatStatus>,
  selection: readonly string[],
): Map<string, SeatVisualState>;

Parameters

ParameterTypeDescription
statusesReadonlyMap<string, SeatStatusRecord | SeatStatus>
selectionreadonly string[]

Returns

Map<string, SeatVisualState>

@openseat/booking/src/index.ts

function

validateSelection

Checks a selection before hold or checkout. Statuses may have changed since the seats were clicked, so this re-checks availability too.

Pass areas (GA places, booths, whole tables) to check a whole basket: they count towards the minimum and the one per-buyer limit (maxTickets, else maxSeats), with messages in tickets ("You can buy up to 10 tickets."). Without areas, seat-only behaviour and messages are unchanged.

function validateSelection(
  selection: readonly string[],
  ctx: SelectionContext,
  rules?: SelectionRules,
  areas?: readonly AreaQuantity[],
): SelectionValidation;

Parameters

ParameterTypeDescription
selectionreadonly string[]
ctxSelectionContext
rulesoptionalSelectionRules

Default {}

areasoptionalreadonly AreaQuantity[]

Returns

@openseat/booking/src/selection.ts

function

wholeTableSeatIds

Public seat IDs of a bookable bookAsWhole table, by its inventory ID (the table's object ID). Empty for GA areas, booths, hidden tables and unknown IDs.

function wholeTableSeatIds(
  layout: LayoutDocument,
  inventoryId: string,
): readonly string[];

Parameters

ParameterTypeDescription
layoutLayoutDocument
inventoryIdstring

Returns

readonly string[]

@openseat/booking/src/areas.ts

Classes1

class

BookingError

Normalised error from any adapter. Adapters should throw this, not raw HTTP errors.

class BookingError extends Error

Extends Error: its public members apply here too.

Members

MemberTypeDescription
codeBookingErrorCode
statusoptionalnumber

HTTP status, when there was a response.

unavailableSeatIdsoptionalreadonly string[]

For SEATS_UNAVAILABLE: which of the requested seats were taken.

unavailableAreaIdsoptionalreadonly string[]

For AREA_SOLD_OUT: inventory IDs of the areas that could not be held.

retryableboolean

Whether retrying the same request (with the same idempotency key) can succeed.

detailsoptionalunknown
constructorconstructor(code: BookingErrorCode, message: string, extra?: { status?: number; unavailableSeatIds?: readonly string[]; unavailableAreaIds?: readonly string[]; retryable?: boolean; details?: unknown; cause?: unknown; })

Every member is readonly.

@openseat/booking/src/types.ts

Interfaces28

interface

AreaDescription

interface AreaDescription

Members

MemberTypeDescription
namestring

What the buyer calls it: "Gold", "Section 102", "Booth B8", "Table T3".

seating"seated" | "standing" | null

"seated" or "standing" for zones; null for booths and tables.

availabilitystring | null

Availability in words: "12 left", "sold out", "unavailable", or null when unknown.

textstring

Full sentence for screen readers: "Gold, Gold tier, ₹1,500, seated, 12 left".

Every member is readonly.

@openseat/booking/src/describe.ts

interface

AreaQuantity

A quantity of something sold as a unit rather than seat by seat: places in a general-admission (standing) area, a booth, or a bookAsWhole table. inventoryId is the area's or booth's inventoryId, or the table's object ID, as listed by getBookableAreas. Booths and whole tables have quantity 1.

interface AreaQuantity

Members

MemberTypeDescription
inventoryIdstring
quantitynumber

Whole number of units, at least 1.

Every member is readonly.

@openseat/booking/src/types.ts

interface

AreaStatusRecord

Availability of a GA area, booth or whole table, reported by the host. status summarises it for the map: "available" while anything is left, "blocked" when it is not for sale, otherwise "held" or "booked".

interface AreaStatusRecord

Members

MemberTypeDescription
inventoryIdstring
statusSeatStatus
remainingnumber

Units that can still be held: 0 when sold out or not for sale.

capacityoptionalnumber

Total units for this event, when the host reports it.

versionoptionalnumber

Monotonic version, with the same meaning as on seat records.

Every member is readonly.

@openseat/booking/src/types.ts

interface

BookableArea

Something sold as one unit rather than seat by seat: a zone (a GA area, or a section shown as a zone with showSeats: false, both sold by quantity up to capacity), a booth with an inventory key, or a table marked bookAsWhole. The SDK only reports clicks on these; the host shows its own quantity picker and owns the inventory.

interface BookableArea

Members

MemberTypeDescription
objectIdstring

Layout object ID of the area, zone section, booth or table.

inventoryIdstring

Host inventory key: the GA area's or booth's inventoryId, the table's object ID, or a zone section's own ID.

kind"ga" | "booth" | "table"

"ga" for every zone (GA areas and zone sections): sold by quantity.

objectTypeoptional"ga" | "section"

"section" for a zone section; absent for every other area.

seatedoptionalboolean

True for a seated zone (a GA area with seating: "seated" and every zone section): the buyer picks a quantity and is seated at the venue. Absent for standing zones, booths and tables.

labelstring
capacitynumber

GA capacity; a zone section's capacity, else its seat count; 1 for a booth; the number of seats for a table (its capacity when it has none).

categoryIdoptionalstring

The object's category; for a table, the most common category of its seats; for a zone section, its own, else its seats' most common one.

sectionIdoptionalstring

Every member is readonly.

@openseat/booking/src/areas.ts

interface

Booking

interface Booking

Members

MemberTypeDescription
bookingIdstring
seatIdsreadonly string[]
areasoptionalreadonly AreaQuantity[]

Area quantities in the booking. Optional, since adapters written before areas existed leave it out.

Every member is readonly.

@openseat/booking/src/types.ts

interface

BookingAdapter

interface BookingAdapter extends InventoryAdapter

Extends InventoryAdapter

Members

MemberTypeDescription
holdSeatsmethod(input: HoldInput, options?: RequestOptions) => Promise<Hold>

Holds all seats and area quantities, or none. Rejects with SEATS_UNAVAILABLE (listing the seats in unavailableSeatIds) when any seat is taken, AREA_SOLD_OUT (listing unavailableAreaIds) when an area has too few places left, and LIMIT_EXCEEDED when the request is over the per-buyer ticket limit.

confirmHoldmethod(input: { readonly holdId: string; readonly paymentReference: string; readonly idempotencyKey?: string; }, options?: RequestOptions) => Promise<Booking>

Converts a hold into a booking after the host's payment flow succeeds. paymentReference is whatever your payment provider returns; the server must verify it. The SDK never handles card data.

releaseHoldmethod(holdId: string, options?: RequestOptions) => Promise<void>
getHoldoptionalmethod(holdId: string, options?: RequestOptions) => Promise<Hold>

Optional: current hold state, for recovering after a reload.

getStatusesmethod(input: { readonly eventId: string; readonly seatIds?: readonly string[]; }, options?: RequestOptions) => Promise<SeatStatusRecord[]>

From InventoryAdapter

subscribeoptionalmethod(eventId: string, listener: (changes: SeatStatusRecord[]) => void, onError?: (error: BookingError) => void) => () => void

Optional real-time updates. Return an unsubscribe function. Updates are hints that make the map fresher; they do not make it authoritative.

From InventoryAdapter

getAreaStatusesoptionalmethod(input: { readonly eventId: string; readonly inventoryIds?: readonly string[]; }, options?: RequestOptions) => Promise<AreaStatusRecord[]>

Optional: availability of GA areas, booths and whole tables (remaining capacity and status). Adapters without it simply do not report areas.

From InventoryAdapter

subscribeAreasoptionalmethod(eventId: string, listener: (changes: AreaStatusRecord[]) => void, onError?: (error: BookingError) => void) => () => void

Optional real-time area updates (remaining capacity), like subscribe for seats.

From InventoryAdapter

Every member is readonly.

Example

adapter.ts
import type { Booking, BookingAdapter, Hold, SeatStatusRecord } from "@openseat/booking";

async function json<T>(res: Promise<Response>): Promise<T> {
  return (await res).json() as Promise<T>;
}

// Your API, your rules. (If it follows the reference contract, use createRestAdapter instead.)
export const adapter: BookingAdapter = {
  getStatuses: ({ eventId }) => json<SeatStatusRecord[]>(fetch(`/api/events/${eventId}/seats`)),
  holdSeats: (input) =>
    json<Hold>(fetch(`/api/events/${input.eventId}/holds`, { method: "POST", body: JSON.stringify(input) })),
  confirmHold: (input) =>
    json<Booking>(fetch(`/api/holds/${input.holdId}/confirm`, { method: "POST", body: JSON.stringify(input) })),
  releaseHold: async (holdId) => {
    await fetch(`/api/holds/${holdId}`, { method: "DELETE" });
  },
};

Your backend: the REST contract

@openseat/booking/src/types.ts

interface

Cart

interface Cart

Members

MemberTypeDescription
linesreadonly CartLine[]
ticketCountnumber

Tickets in the cart: the sum of line quantities.

totalnumber | null

Sum of line amounts, rounded to 2 decimal places; null when any price is unknown. 0 for an empty cart.

skippednumber

Seats and areas left out because they are not in the layout (or are hidden), or had an invalid quantity.

Every member is readonly.

@openseat/booking/src/cart.ts

interface

CartInput

interface CartInput

Members

MemberTypeDescription
layoutLayoutDocument
seatIdsreadonly string[]

Selected seats, by public seat ID. All seats of a bookAsWhole table make one table line.

areasoptionalreadonly AreaQuantity[]

GA places, booths and whole tables, by inventory ID.

priceOf(categoryId: string) => number | undefined

Price of one ticket in a category, or undefined when it is not known.

statusesoptionalStatusInput

Event statuses, so event-level categories in status records win over the layout's.

sectionoptionalSeatLabelOptions["section"]

When seat lines start with the section. Default "auto": when the layout has more than one section.

Every member is readonly.

@openseat/booking/src/cart.ts

interface

CartLine

interface CartLine

Members

MemberTypeDescription
kind"seats" | "table" | "ga" | "booth"
labelstring

What the buyer sees: "Stalls, Row D, Seats 1–4", "Table T3 (8 seats)", "4 × Fan Pit", "Booth B8". Never contains IDs.

quantitynumber

Tickets on this line: seats, GA places, 1 for a booth, the seats of a table.

categoryIdoptionalstring
categoryNameoptionalstring

The category's display name, for example "VIP".

unitPricenumber | null

Price of one ticket on this line; null when unknown or, for a table, when its seats differ in price.

amountnumber | null

Price of the whole line; null when any of its prices is unknown.

seatIdsreadonly string[]

For the host's code (removing a line, holding): the seats this line covers. Not for display.

inventoryIdoptionalstring

For the host's code: the inventory ID of a GA, booth or whole-table line. Not for display.

Every member is readonly.

@openseat/booking/src/cart.ts

interface

Hold

interface Hold

Members

MemberTypeDescription
holdIdstring
eventIdstring
seatIdsreadonly string[]
areasoptionalreadonly AreaQuantity[]

Area quantities in the hold. Optional, since adapters written before areas existed leave it out.

expiresAtstring

Server-authoritative expiry (ISO 8601).

serverTimeoptionalstring

Server clock at response time (ISO 8601), used to correct for client clock skew.

Every member is readonly.

@openseat/booking/src/types.ts

interface

HoldController

interface HoldController

Members

MemberTypeDescription
getStatemethod() => HoldState
subscribemethod(listener: (state: HoldState) => void) => () => void
holdmethod(seatIds: readonly string[], areas?: readonly AreaQuantity[]) => Promise<HoldState>

Requests a hold of seats and, optionally, area quantities (GA places, booths, whole tables), all or nothing. Either may be empty, not both. Retrying after a retryable failure reuses the same idempotency key. Can be called again after confirmed, released or expired to start the next booking.

confirmmethod(paymentReference: string) => Promise<HoldState>

Confirms the current hold after the host's payment flow succeeded.

releasemethod() => Promise<HoldState>
resetmethod() => HoldState

Returns to idle after a booking finished (confirmed), a release, an expiry or a failed hold request, so the UI shows its start state for a new booking. Throws CONFLICT while a request is in flight or a hold is still active: release that first.

resumemethod(hold: Hold) => HoldState

Takes over a hold the server already has, for example after a page reload (from adapter.getHold or the host's own "my bookings" answer): the state becomes held with its countdown, or expired when its time is up. Throws CONFLICT while a request is in flight.

remainingMsmethod() => number

Milliseconds until the server releases the hold, by the server's clock. 0 when not held.

disposemethod() => void

Clears timers. Does not release the hold on the server; call release() for that.

Every member is readonly.

@openseat/booking/src/hold.ts

interface

HoldControllerOptions

interface HoldControllerOptions

Members

MemberTypeDescription
adapterBookingAdapter
eventIdstring
nowoptional() => number

Clock, for tests.

setTimeoutoptional(fn: () => void, ms: number) => unknown

Timer functions, for tests.

clearTimeoutoptional(handle: unknown) => void
createKeyoptional() => string

Idempotency key factory. Defaults to crypto.randomUUID.

maxTicketsoptionalnumber

Per-buyer ticket limit, checked before asking the adapter: a request over it fails with LIMIT_EXCEEDED ("You can buy up to 10 tickets.") and nothing is sent. Seats, GA places and booths count one each.

layoutoptionalLayoutDocument

The layout, so a whole table counts its seats towards maxTickets instead of 1.

Every member is readonly.

@openseat/booking/src/hold.ts

interface

HoldInput

interface HoldInput

Members

MemberTypeDescription
eventIdstring
seatIdsreadonly string[]
areasoptionalreadonly AreaQuantity[]

GA places, booths and whole tables to hold together with the seats, all or nothing. Omitted (or empty) for a seats-only hold, which is all that adapters written before areas existed understand.

idempotencyKeystring

Same key for retries of the same attempt. The server must return the original result for a repeated key instead of creating a second hold.

Every member is readonly.

@openseat/booking/src/types.ts

interface

InitialBooking

A confirmed booking to start with, for example restored from the host's own ledger.

interface InitialBooking

Members

MemberTypeDescription
bookingIdoptionalstring

Kept as given; generated when left out.

seatIdsoptionalreadonly string[]
areasoptionalreadonly AreaQuantity[]

Every member is readonly.

@openseat/booking/src/memory-adapter.ts

interface

InMemoryBookingOptions

interface InMemoryBookingOptions

Members

MemberTypeDescription
eventIdstring
statusesoptionalreadonly SeatStatusRecord[]

Starting statuses. Records for area keys (a GA area's or booth's inventoryId, or a whole table's ID) set the area instead: "blocked" makes it not for sale, so it can never be held; "held" or "booked" sell out what is left.

layoutoptionalLayoutDocument

The layout, so zones (GA areas and zone sections, capacity from the layout), booths (capacity 1) and bookAsWhole tables (one unit backed by their seats) can be held. The seats of zone sections cannot be held on their own: the zone is sold instead.

areaCapacitiesoptionalReadonly<Record<string, number>>

Capacity per area inventory ID, overriding the layout's or adding areas without a layout.

initialBookingsoptionalreadonly InitialBooking[]

Confirmed bookings to seed, so a host ledger can restore them after a reload or re-save.

maxTicketsPerHoldoptionalnumber

Per-buyer limit for one hold, counting seats, GA places and booths, and a whole table as its number of seats. Over it, holds fail with LIMIT_EXCEEDED. Default unlimited.

onBookingoptional(booking: Booking) => void

Called after every confirmed booking, for example to append it to a host ledger.

holdMsoptionalnumber

How long holds last. Default 10 minutes.

latencyMsoptionalnumber

Simulated latency per call. Default 0.

Default 0

nowoptional() => number

Every member is readonly.

@openseat/booking/src/memory-adapter.ts

interface

InventoryAdapter

interface InventoryAdapter

Members

MemberTypeDescription
getStatusesmethod(input: { readonly eventId: string; readonly seatIds?: readonly string[]; }, options?: RequestOptions) => Promise<SeatStatusRecord[]>
subscribeoptionalmethod(eventId: string, listener: (changes: SeatStatusRecord[]) => void, onError?: (error: BookingError) => void) => () => void

Optional real-time updates. Return an unsubscribe function. Updates are hints that make the map fresher; they do not make it authoritative.

getAreaStatusesoptionalmethod(input: { readonly eventId: string; readonly inventoryIds?: readonly string[]; }, options?: RequestOptions) => Promise<AreaStatusRecord[]>

Optional: availability of GA areas, booths and whole tables (remaining capacity and status). Adapters without it simply do not report areas.

subscribeAreasoptionalmethod(eventId: string, listener: (changes: AreaStatusRecord[]) => void, onError?: (error: BookingError) => void) => () => void

Optional real-time area updates (remaining capacity), like subscribe for seats.

Every member is readonly.

@openseat/booking/src/types.ts

interface

InventoryStore

interface InventoryStore

Members

MemberTypeDescription
getStatusesmethod() => ReadonlyMap<string, SeatStatusRecord>

Current records by seat ID. A new Map after every change, so it can be used as a React dependency.

applymethod(records: readonly SeatStatusRecord[], options?: { readonly replace?: boolean; }) => string[]

Applies records. With replace, the given records become the whole picture (seats not listed are available), as after a full refresh. Records whose version is older than the one already held are ignored. Returns the seat IDs that actually changed.

subscribemethod(listener: (statuses: ReadonlyMap<string, SeatStatusRecord>, changed: readonly string[]) => void) => () => void

Every member is readonly.

@openseat/booking/src/inventory.ts

interface

RequestOptions

interface RequestOptions

Members

MemberTypeDescription
signaloptionalreadonlyAbortSignal

Abort the request, for example when the component unmounts.

@openseat/booking/src/types.ts

interface

SeatDescription

interface SeatDescription

Members

MemberTypeDescription
namestring

Short label for lists: "Seat 5".

locationstring

Where the seat is: "Row A, Stalls".

textstring

Full sentence for screen readers: "Seat 5, row A, Stalls, VIP, £45, available, wheelchair space".

Every member is readonly.

@openseat/booking/src/describe.ts

interface

SeatIndex

interface SeatIndex

Members

MemberTypeDescription
layoutLayoutDocument
seatsReadonlyMap<string, SeatInfo>
orderedreadonly string[]

Seat IDs in reading order: by section, row, then seat order. Seats of zone sections are left out.

rowSegmentsReadonlyMap<string, readonly RowSegment[]>

Row ID → segments of seat IDs.

objectToSeatIdReadonlyMap<string, string>

Every member is readonly.

@openseat/booking/src/seat-index.ts

interface

SeatInfo

interface SeatInfo

Members

MemberTypeDescription
seatSeat
seatIdstring
objectIdstring
positionPoint
rowLabeloptionalstring
rowIdoptionalstring
sectionIdoptionalstring
sectionLabeloptionalstring
tableLabeloptionalstring
categoryIdoptionalstring

Category from the layout. Event statuses may override it.

categoryNameoptionalstring
hiddenboolean

True when the seat is hidden itself or through a hidden row, table or section, or is in a zone section (see zoneSectionId). Hidden seats are not drawn, focused, listed or selectable.

zoneSectionIdoptionalstring

The zone section (showSeats: false) the seat is in. Such a seat is not sold on its own (the section is sold by quantity), so it is also hidden.

Every member is readonly.

@openseat/booking/src/seat-index.ts

interface

SeatLabelOptions

Human names for seats and areas, for carts, confirmations and messages. They are built from labels only and never contain seat IDs, object IDs or inventory IDs.

interface SeatLabelOptions

Members

MemberTypeDescription
sectionoptionalreadonly"auto" | "always" | "never"

When to start with the section: "auto" (the default) when the layout has more than one section, so "Row A" alone would be ambiguous; "always" whenever the seat is in a section; "never".

@openseat/booking/src/labels.ts

interface

SeatStatusRecord

interface SeatStatusRecord

Members

MemberTypeDescription
seatIdstring

Public seat ID from the layout.

statusSeatStatus
categoryIdoptionalstring

Event-level category, overriding the layout's. Pricing tiers often vary by event.

priceLabeloptionalstring

Display-only price text, for example "£45". The SDK never computes prices.

expiresAtoptionalstring

For held seats: when the server will release the hold (ISO 8601).

versionoptionalnumber

Monotonic version from the server. When present, an update with a lower version than the one already applied is ignored, so out-of-order real-time messages cannot resurrect stale states.

Every member is readonly.

@openseat/booking/src/types.ts

interface

SelectionContext

interface SelectionContext

Members

MemberTypeDescription
layoutLayoutDocument
statusesStatusInput
areaStatusesoptionalReadonlyMap<string, AreaStatusRecord>

Optional area availability by inventory ID, to check GA and booth quantities against what is left.

Every member is readonly.

@openseat/booking/src/selection.ts

interface

SelectionRules

interface SelectionRules

Members

MemberTypeDescription
minSeatsoptionalnumber

Minimum seats for a valid selection (checked at checkout, not while clicking). Default 1.

Default 1

maxSeatsoptionalnumber

Maximum seats. Clicking beyond it is refused. Default unlimited. When area quantities are passed and maxTickets is not set, it limits the total number of tickets instead.

maxTicketsoptionalnumber

One per-buyer limit across seats, GA places, booths and whole tables (a table counts its seats). When absent, maxSeats applies to the total.

allowMixedCategoriesoptionalboolean

Allow seats from different categories in one selection. Default true.

Default true

allowedCategoriesoptionalreadonly string[]

Only these categories can be selected.

preventOrphansoptionalboolean

Refuse selections that leave a single available seat stranded between taken or selected seats (or a row end), which is usually unsellable. Checked at checkout, since an orphan is often fixed by the next click.

Every member is readonly.

@openseat/booking/src/selection.ts

interface

SelectionValidation

interface SelectionValidation

Members

MemberTypeDescription
validboolean
violationsreadonly SelectionViolation[]

Every member is readonly.

@openseat/booking/src/selection.ts

interface

SelectionViolation

interface SelectionViolation

Members

MemberTypeDescription
codeSelectionViolationCode
messagestring
seatIdsreadonly string[]

Seats the violation is about: the unavailable seats, or the stranded seat.

inventoryIdsoptionalreadonly string[]

Areas the violation is about, by inventory ID. Only on area violations.

Every member is readonly.

@openseat/booking/src/selection.ts

interface

ToggleResult

interface ToggleResult

Members

MemberTypeDescription
allowedboolean

True when the toggle was accepted.

selectionreadonly string[]

The selection after the toggle (unchanged when refused).

addedboolean
violationoptionalSelectionViolation

Every member is readonly.

@openseat/booking/src/selection.ts

Types8

type

BookingErrorCode

type BookingErrorCode =
  | "SEATS_UNAVAILABLE"
  | "AREA_SOLD_OUT"
  | "LIMIT_EXCEEDED"
  | "HOLD_EXPIRED"
  | "HOLD_NOT_FOUND"
  | "PAYMENT_REJECTED"
  | "CONFLICT"
  | "UNAUTHORIZED"
  | "FORBIDDEN"
  | "VALIDATION"
  | "RATE_LIMITED"
  | "NETWORK"
  | "TIMEOUT"
  | "ABORTED"
  | "SERVER"
  | "INVALID_RESPONSE"
  | "UNKNOWN";

@openseat/booking/src/types.ts

type

Direction

type Direction = "left" | "right" | "up" | "down";

@openseat/booking/src/navigation.ts

type

HoldState

type HoldState =
  | {
      readonly status: "idle";
    }
  | {
      readonly status: "holding";
      readonly seatIds: readonly string[];
      readonly areas?: readonly AreaQuantity[];
    }
  | {
      readonly status: "held";
      readonly hold: Hold;
      readonly expiresAt: number;
    }
  | {
      readonly status: "confirming";
      readonly hold: Hold;
      readonly expiresAt: number;
    }
  | {
      readonly status: "confirmed";
      readonly booking: Booking;
    }
  | {
      readonly status: "releasing";
      readonly hold: Hold;
    }
  | {
// … 13 more lines (type truncated)

This type is long; its declaration is shortened here.

Variants · 9

DiscriminantTypeDescription
status: "idle"{ readonly status: "idle"; }
status: "holding"{ readonly status: "holding"; readonly seatIds: readonly string[]; readonly areas?: readonly AreaQuantity[]; }
status: "held"{ readonly status: "held"; readonly hold: Hold; readonly expiresAt: number; }
status: "confirming"{ readonly status: "confirming"; readonly hold: Hold; readonly expiresAt: number; }
status: "confirmed"{ readonly status: "confirmed"; readonly booking: Booking; }
status: "releasing"{ readonly status: "releasing"; readonly hold: Hold; }
status: "released"{ readonly status: "released"; }
status: "expired"{ readonly status: "expired"; readonly hold: Hold; }
status: "failed"{ readonly status: "failed"; readonly error: BookingError; readonly seatIds: readonly string[]; readonly areas?: readonly AreaQuantity[]; readonly hold?: Hold; }

@openseat/booking/src/hold.ts

type

RowSegment

Seats of one row split at aisles. Seats in the same segment are adjacent for orphan-seat rules; an aisle separates segments.

type RowSegment = readonly string[];

@openseat/booking/src/seat-index.ts

type

SeatStatus

Inventory status reported by the host. "selected" is not a status: selection is local UI state.

type SeatStatus = "available" | "held" | "booked" | "blocked";

@openseat/booking/src/types.ts

type

SeatVisualState

Visual states understood by the renderer and theme.

type SeatVisualState =
  "available" | "selected" | "held" | "booked" | "blocked";

@openseat/booking/src/index.ts

type

SelectionViolationCode

type SelectionViolationCode =
  | "SEAT_UNKNOWN"
  | "SEAT_UNAVAILABLE"
  | "TOO_FEW"
  | "TOO_MANY"
  | "MIXED_CATEGORIES"
  | "CATEGORY_NOT_ALLOWED"
  | "ORPHAN_SEAT"
  | "AREA_UNAVAILABLE";

@openseat/booking/src/selection.ts

type

StatusInput

Statuses by seat ID, as records or bare statuses. Missing seats are available.

type StatusInput = ReadonlyMap<string, SeatStatusRecord | SeatStatus>;

@openseat/booking/src/selection.ts

Constants2

const

MAX_TIMER_MS

The longest delay a browser or Node timer takes without overflowing: 2^31 - 1 ms.

const MAX_TIMER_MS = 2147483647

@openseat/booking/src/hold.ts

const

SEAT_STATUSES

const SEAT_STATUSES: readonly SeatStatus[]

@openseat/booking/src/types.ts