Skip to content

Handle hold expiry

Configure the checkout window, display trusted remaining time, extend safely, and recover when inventory is released.

Updated View as Markdown

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:

  1. ttlMs supplied by hold() or bestAvailable();
  2. the event’s configured checkout window; or
  3. 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

server/set-checkout-window.jsjs
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:

browser/hold-countdown.jsjs
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:

browser/expiry-callback.jsjs
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

browser/need-more-time.jsjs
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.

Navigation

Type to search…

↑↓ navigate↵ selectEsc close