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
$npmi@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;
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.
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.
"B8" → "Booth B8"; a label that already says "Booth", "Stall" or "Stand" stays as it is.
function boothName(label: string): string;
Parameters
Parameter
Type
Description
label
string
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.
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?: readonlyAreaQuantity[],): number;
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.
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.
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.
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.
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.
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[];
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.
"D" → "Row D"; a label that already says "Row …" stays as it is.
function rowName(label: string): string;
Parameters
Parameter
Type
Description
label
string
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.
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;
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.
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.
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[];
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.
availability
string | null
Availability in words: "12 left", "sold out", "unavailable", or null when unknown.
text
string
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
Member
Type
Description
inventoryId
string
quantity
number
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".
Units that can still be held: 0 when sold out or not for sale.
capacityoptional
number
Total units for this event, when the host reports it.
versionoptional
number
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
Member
Type
Description
objectId
string
Layout object ID of the area, zone section, booth or table.
inventoryId
string
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.
seatedoptional
boolean
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.
label
string
capacity
number
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).
categoryIdoptional
string
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.
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.
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.
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.
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.
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.
Idempotency key factory. Defaults to crypto.randomUUID.
maxTicketsoptional
number
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.
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.
idempotencyKey
string
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.
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.
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.
areaCapacitiesoptional
Readonly<Record<string, number>>
Capacity per area inventory ID, overriding the layout's or adding areas without a layout.
Confirmed bookings to seed, so a host ledger can restore them after a reload or re-save.
maxTicketsPerHoldoptional
number
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.
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.
Category from the layout. Event statuses may override it.
categoryNameoptional
string
hidden
boolean
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.
zoneSectionIdoptional
string
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
Member
Type
Description
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".
Event-level category, overriding the layout's. Pricing tiers often vary by event.
priceLabeloptional
string
Display-only price text, for example "£45". The SDK never computes prices.
expiresAtoptional
string
For held seats: when the server will release the hold (ISO 8601).
versionoptional
number
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.
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
Member
Type
Description
minSeatsoptional
number
Minimum seats for a valid selection (checked at checkout, not while clicking). Default 1.
Default 1
maxSeatsoptional
number
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.
maxTicketsoptional
number
One per-buyer limit across seats, GA places, booths and whole tables (a table counts its seats). When absent, maxSeats applies to the total.
allowMixedCategoriesoptional
boolean
Allow seats from different categories in one selection. Default true.
Default true
allowedCategoriesoptional
readonly string[]
Only these categories can be selected.
preventOrphansoptional
boolean
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.