Holds deliberately expire so abandoned carts do not lock inventory indefinitely. Expiry is a normal checkout state, not an exceptional system failure.
TTL precedence
SeatLayer resolves a new hold’s lifetime in this order:
ttlMssupplied byhold()orbestAvailable();- the event’s configured checkout window; or
- the 15-minute default.
Per-hold values are clamped between one second and 60 minutes. Event-level configuration is clamped between one and 60 minutes. Changes affect future holds, not holds already active.
Configure an event window
const response = await fetch(
"https://api.seatlayer.io/v1/events/ev_9f3a/hold-ttl",
{
method: "POST",
headers: {
authorization: `Bearer ${process.env.SEATLAYER_SECRET_KEY}`,
"content-type": "application/json",
},
body: JSON.stringify({
holdTtlMs: 20 * 60 * 1000,
}),
},
);
const { holdTtlMs } = await response.json();Send null to clear the event override and return to the default.
Choose a window long enough for the real payment journey and short enough to return abandoned inventory during peak demand.
Show a countdown
SeatPicker displays its own countdown. For a headless SeatingChart, render from the absolute expiry:
const hold = await chart.hold();
if (!hold) return handleSelectionConflict();
const timer = window.setInterval(() => {
const remainingMs = hold.expiresAt - Date.now();
if (remainingMs <= 0) {
window.clearInterval(timer);
returnBuyerToSelection();
return;
}
renderRemainingTime(remainingMs);
}, 1000);Also handle the authoritative SDK callback:
const chart = new seatlayer.SeatingChart({
container: "#chart",
event: "ev_9f3a",
onHoldExpired: () => {
clearCheckoutCart();
returnBuyerToSelection();
},
});The visible countdown improves expectation-setting; server state remains authoritative.
Offer more time
const refreshed = await chart.extendHold();
if (refreshed) {
renderRemainingTime(refreshed.expiresAt - Date.now());
} else {
returnBuyerToSelection();
}A hold can be extended at most three times. Do not automatically renew indefinitely—ask for active buyer intent.
Recover at each boundary
hold() returns null. Nothing was reserved. Refresh inventory and let the buyer select again.
Your server cannot resolve an active hold. Do not create a new charge; return a recoverable checkout response.
Booking returns 409. Void or refund based on payment state and let the buyer reselect.
onHoldExpired fires. Clear local cart state and explain that the seats were released.
Buyer-facing copy
Prefer:
Your seat reservation expired and the seats were released. Please choose again.
Avoid implying that the buyer was charged or that a permanent booking was cancelled when only a hold expired.
Expiry checklist
- Countdown uses
expiresAt. - A warning appears before zero.
- Extension has an intentional buyer action.
- Extension refusal enters the same recovery path as expiry.
- Server booking handles
409. - Payment has a tested void/refund path.
- Restored carts revalidate the hold.
- Async order processing can mark an expired hold stale.
Continue to Holds and checkout handoff or Idempotency and conflicts.