A temporary seat hold is a short, revocable claim on specific inventory while a buyer finishes checkout. It is the mechanism that stops two people paying for the same seat, and it exists in every reserved-seating system whether or not it is named. This page explains the model generally, then points at the seat map API endpoints that implement each part.
Why a hold is needed at all
Selling assigned seats has a gap between choosing and paying. Payment takes seconds at best and minutes at worst — card authentication, a wallet redirect, a buyer hunting for their card. Without a hold, the system has two bad options:
- Sell optimistically, and discover at capture time that the seat went to someone else. The buyer is charged or challenged for a seat they cannot have, and you own the refund.
- Lock at selection with no expiry, and one abandoned cart removes a seat from sale permanently.
A hold resolves both: inventory is protected for a bounded window, and the window ends by itself if the sale does not complete. The hold is explicitly not a sale — nothing is owed, and no ticket exists yet.
The four properties that matter
1. A bounded window
A hold must carry an expiry, and the server must own it. If the browser decides when a hold ends, a stopped clock or a closed laptop leaks inventory. The buyer UI should count down from a server-supplied timestamp rather than a locally invented duration.
In SeatLayer, a hold returns an absolute expiresAt. The default window is 15
minutes; a per-hold ttlMs or an event-level checkout window can change it, and
both are clamped server-side — per-hold values between one second and 60 minutes,
event configuration between one and 60 minutes. Changes apply to future holds,
not to holds already active. See
hold expiry for the precedence rules.
2. Concurrency control: two buyers, one seat
This is the part that is genuinely hard, and the part naive implementations get
wrong. A “check if free, then mark held” sequence is a race: two requests can
both read free before either writes. Correctness requires that the decision and
the write happen as one atomic step, which in practice means either a database
transaction with the right isolation and row locks, a compare-and-set on seat
state, or serializing all transitions for a given event through a single writer.
The observable contract you want is simple: when two requests contend for one seat, exactly one succeeds and the other is told why it failed — not silently overwritten, and not left ambiguous.
SeatLayer processes hold, book, block and release transitions for each event
through one authoritative serialized inventory writer, so two competing requests
cannot both move the same seat out of free; one commits first and the other
observes the new state. The loser receives 409 with a conflicts array naming
each contested label and its current status, so your interface can remove exactly
those seats and keep the rest of the selection intact.
3. Expiry and release
Holds end three ways: the buyer completes the purchase, the application releases them deliberately (an abandoned cart, a removed seat, a changed selection), or the window elapses. All three must return the inventory to sale, and expiry should be treated as an ordinary checkout state rather than an error — abandoned carts are the common case, not the exceptional one.
Design the buyer experience for the boundary: clear the stale selection, explain what happened, and let the buyer choose again. In SeatLayer, releasing is a first-class operation, and releases are never rate-limited — handing inventory back should always be cheap.
4. Confirming: from hold to booking
Booking converts the temporary claim into permanent inventory. Two rules keep this safe.
The client must not be trusted with the price. Hand off only the hold identity and recompute the amount server-side from the hold’s own records. A browser that can name its own total is a browser that can pay less than the seats cost.
Retries must be idempotent. Networks time out, webhooks repeat, and workers retry — so a repeated booking call must not create a second sale. Use one immutable reference per business order, and on an uncertain response reconcile and retry with the same reference rather than generating a new one.
SeatLayer’s booking call takes a bookingRef for exactly this purpose. A booking
attempted under a different reference against already-booked seats returns
409, as do an expired or invalid hold and missing inventory. Idempotency
protects repeated or unknown delivery; it does not make unavailable inventory
available, so a genuine 409 should not be retried in a loop. Note that
bookingRef deduplicates the SeatLayer booking only — your own order creation and
payment operations still need their own idempotency.
Putting it together
The end-to-end shape of a correct implementation:
- The buyer selects seats; the system creates a hold and returns an opaque hold id with a server-owned expiry.
- Your interface counts down from that expiry and keeps the selection editable.
- At checkout, your server takes the hold id, recomputes the amount from authoritative records, and runs payment.
- On success it books the hold under a stable reference; on failure or abandonment it releases, or simply lets the window lapse.
- Every conflict path — contested seat, expired hold, repeated delivery — has a defined answer rather than an exception.
Where this is documented
- Hold seats from your server — the endpoint, request and response shapes, releasing, and keeping a hold alive.
- Holds and checkout handoff — the buyer-side flow, where the SDK creates the hold for you.
- Hold expiry — TTL precedence, countdowns, and recovery at each boundary.
- Idempotency and conflicts — safe
retries, the
409taxonomy, and the payment-safe state machine. - Booking — turning a hold into an inventory booking.