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.
BuyerSelects inventory
The buyer chooses seats, a table, booth, or general-admission quantity in your interface.
Buyer SDKCreates a temporary hold
SeatPicker or SeatingChart returns an opaque hold ID and expiry instead of a permanent booking.
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.
Your serverBuilds the trusted order
It inspects the active hold, calculates the charge from its returned items, and runs your payment workflow.
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
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:
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
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
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:
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:
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:
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.