Skip to content

Holds and checkout handoff

Protect a buyer's selection in the browser, hand an opaque hold to your backend, and release or restore it safely.

Updated View as Markdown

A hold temporarily protects inventory while a buyer completes checkout. It is not a sale. The Buyer SDK creates the hold; your server inspects and books it. For box-office, invoice, or partner reservations that never touch a browser, your backend can create one directly with the server-side hold endpoint.

  1. BuyerSelects inventory

    The buyer chooses seats, a table, booth, or general-admission quantity in your interface.

  2. Buyer SDKCreates a temporary hold

    SeatPicker or SeatingChart returns an opaque hold ID and expiry instead of a permanent booking.

  3. Browser handoffSends only the hold ID

    Your checkout route receives the hold identity and your own cart context, never a secret key or trusted price.

  4. Your serverBuilds the trusted order

    It inspects the active hold, calculates the charge from its returned items, and runs your payment workflow.

  5. Your serverBooks once or recovers

    Use the stable order ID as bookingRef. Confirm success, or handle expiry and conflicts without a partial sale.

SeatPicker handoff

browser/seat-picker.jsjs
const picker = new seatlayer.SeatPicker({
  container: "#picker",
  event: "ev_9f3a",
  // Public Platform sale: publishable, mode-matched account key. SeatLayer
  // validates this page's exact registered origin and keeps the grant in memory.
  publicKey: "pk_test_…",
  onCheckout: async (hold, seats, handoff) => {
    await fetch("/api/checkout/seats", {
      method: "POST",
      headers: { "content-type": "application/json" },
      body: JSON.stringify({ holdId: handoff.holdId }),
    });
  },
});

await picker.render();

For private inventory, replace publicKey with a buyerAccessTokenProvider backed by your authenticated server route. The hold handoff below is identical in both lanes.

The third callback argument is the stable checkout handoff:

CheckoutHandoff.tsts
interface CheckoutHandoff {
  holdId: string;
  expiresAt: number;
  currency: string;
  lineItems: CheckoutLineItem[];
  total: number;
}

Browser totals are useful for presentation, but your server must inspect the hold before creating the trusted order amount.

Headless SeatingChart flow

browser/headless-checkout.jsjs
const chart = new seatlayer.SeatingChart({
  container: "#chart",
  event: "ev_9f3a",
  onSelectionChange: (seats) => updateYourCart(seats),
  onHold: ({ holdId, expiresAt }) => {
    saveCheckoutHandoff({ holdId, expiresAt });
  },
  onError: (error) => reportPickerError(error),
});

await chart.render();

document.querySelector("#continue").addEventListener("click", async () => {
  const hold = await chart.hold();

  if (!hold) {
    showMessage("One of those seats is no longer available. Please choose again.");
    return;
  }

  await beginCheckout({ holdId: hold.holdId });
});

hold() resolves to:

Field Meaning
holdId Opaque capability identifying this hold
expiresAt Absolute expiry timestamp in epoch milliseconds
seats Held selection for buyer-side UI
items Resolved hold line items

The public method returns null when the hold cannot be created and sends structured unexpected errors to onError.

Inspect from your server

server/inspect-hold.jsjs
const response = await fetch(
  `https://api.seatlayer.io/v1/events/${eventKey}/holds/${holdId}`,
  {
    headers: {
      authorization: `Bearer ${process.env.SEATLAYER_SECRET_KEY}`,
    },
  },
);

if (!response.ok) {
  return handleInactiveOrInvalidHold(response.status);
}

const hold = await response.json();
const order = await createOrderFromTrustedItems(hold.items);

Do not accept buyer-posted labels, tiers, quantities, prices, or currency as authoritative checkout input. For a channel-priced item, the inspected hold also returns channelId and the frozen channelPricingVersion; a later organizer price edit does not rewrite that hold.

Restore a hold

SeatPicker restores its active hold from sessionStorage by default. When your application owns cart persistence:

browser/restore-hold.jsjs
const picker = new seatlayer.SeatPicker({
  container: "#picker",
  event: eventKey,
  restoreHold: false,
  initialHoldId: cart.seatlayerHoldId,
  onHoldChange: (hold) => {
    persistCart({
      seatlayerHoldId: hold?.holdId ?? null,
      expiresAt: hold?.expiresAt ?? null,
    });
  },
  onHoldRestored: (hold) => {
    renderCountdown(hold.expiresAt);
  },
});

For a headless chart, call resumeHold(holdId). Restoring verifies the hold and does not extend its expiry.

Extend a hold

The SDK can request more time for the current hold:

browser/extend-hold.jsjs
const extended = await chart.extendHold();

if (!extended) {
  returnBuyerToSelection();
} else {
  renderCountdown(extended.expiresAt);
}

An active hold may be extended at most three times. A refusal is a normal outcome when the hold is missing, inactive, expired, or has reached the renewal limit.

Release a hold

Release inventory explicitly when the buyer abandons checkout or chooses to start over:

browser/release-hold.jsjs
await chart.release();

If you do nothing, the server releases the hold at expiresAt.

Conflict behavior

Moment Signal Product response
Hold creation hold() returns null Refresh selection and explain that availability changed
Hold inspection 404 or inactive hold Do not create a new charge; return to selection
Booking 409 conflict Void/refund according to payment state and let the buyer reselect
Restore null / callback not fired Clear stale cart hold state
Extend null Continue expiry recovery; do not promise more time

Verification checklist

  • A selection produces one hold and one stored holdId.
  • A competing browser cannot hold the same seat.
  • Refresh restores the active hold only when configured.
  • Release returns inventory to other buyers.
  • Expiry clears the buyer cart and selection state.
  • The backend calculates the charge from inspected hold items.
  • No secret key appears in client code.

Continue to Booking held seats and Handling hold expiry.

Navigation

Type to search…

↑↓ navigate↵ selectEsc close