---
title: "Handle hold expiry"
description: "Configure the checkout window, display trusted remaining time, extend safely, and recover when inventory is released."
---

> Documentation Index
> Fetch the complete documentation index at: https://docs.seatlayer.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Handle hold expiry

Holds deliberately expire so abandoned carts do not lock inventory indefinitely. Expiry is a normal checkout state, not an exceptional system failure.

> **Read the actual expiry**
>
> The default hold window is 15 minutes, but event and per-hold overrides may change it. Drive buyer UI from the returned `expiresAt`; never hardcode a countdown duration.

## 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

```js title="server/set-checkout-window.js"
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:

```js title="browser/hold-countdown.js"
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:

```js title="browser/expiry-callback.js"
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

```js title="browser/need-more-time.js"
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 failed

`hold()` returns `null`. Nothing was reserved. Refresh inventory and let the buyer select again.
### Inspection failed

Your server cannot resolve an active hold. Do not create a new charge; return a recoverable checkout response.
### Booking conflicted

Booking returns `409`. Void or refund based on payment state and let the buyer reselect.
### Client expired

`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](/buyer-sdk/holds-and-checkout) or [Idempotency and conflicts](/server-api/idempotency-and-conflicts).

Source: https://docs.seatlayer.io/buyer-sdk/hold-expiry/index.mdx
