Good reserved-seating UX depends on more than rendering a chart. The integration must preserve a clear trust boundary, behave well while inventory changes, and give buyers a useful recovery path when a hold or payment fails.
Keep one trust boundary
| Browser may | Server must |
|---|---|
| Render public event inventory | Store SEATLAYER_SECRET_KEY |
| Let the buyer select and hold | Inspect the hold and its authoritative items |
Send your backend an opaque holdId |
Calculate the order total |
| Display availability and pricing | Take payment and confirm the booking |
Use an explicit checkout state machine
A practical checkout has these states:
selecting— no hold exists.held— inventory is protected untilexpiresAt.paying— payment is in flight; keep the same hold and order.booked— SeatLayer confirmed the sale.recoverable_error— hold expired, inventory conflicted, or payment failed.
Do not create a new order or bookingRef merely because a network request timed out. Retry the original operation with the original reference.
Make booking idempotent
Use your immutable order or payment identifier as bookingRef.
async function bookHeldSeats({ eventKey, holdId, orderId }) {
const response = await fetch(
`https://api.seatlayer.io/v1/events/${eventKey}/book`,
{
method: "POST",
headers: {
authorization: `Bearer ${process.env.SEATLAYER_SECRET_KEY}`,
"content-type": "application/json",
},
body: JSON.stringify({ holdId, bookingRef: orderId }),
},
);
if (response.ok) return response.json();
if (response.status === 409) return { conflict: true };
throw new Error(`SeatLayer booking failed: ${response.status}`);
}The same bookingRef is safe to replay. A different reference for the same sold seats is a real conflict.
Decide the payment sequence
Most integrations inspect the hold, authorize or capture payment, then book:
hold → inspect trusted items → authorize payment → book → capture/settleIf your payment provider captures before booking, define an automatic void/refund path for a booking conflict. If it supports authorization and capture, authorizing before booking reduces refund noise.
Treat 409 as a product path
Inventory conflicts are expected under load. When booking returns 409:
- do not partially confirm the order;
- void or refund payment according to your payment sequence;
- explain that the reservation expired or changed;
- reopen the picker with the previous intent where possible; and
- let the buyer choose again without restarting the whole event journey.
Preserve active holds across navigation
The buyer SDK restores its current hold from sessionStorage by default. If your application owns navigation or cart persistence, set restoreHold: false, store the holdId, and pass initialHoldId when the picker mounts again.
const picker = new SeatPicker({
container: "#picker",
event: eventKey,
restoreHold: false,
initialHoldId: cart.seatlayerHoldId,
onHoldChange: (hold) => {
saveCart({ seatlayerHoldId: hold?.holdId ?? null });
},
});Design for every buyer
- Give the picker container an explicit height and test it at its actual embed width.
- Keep keyboard focus visible and do not trap focus around the embed.
- Preserve accessible-seat metadata and companion-seat rules from the venue.
- Test high zoom, narrow screens, screen-reader labels, and colorblind-safe mode.
- Treat 3D and 360° seat views as progressive enhancement. The 2D map remains the reliable fallback.
- Localize surrounding checkout copy as well as the picker.
Add useful observability
Record your own correlation fields together:
- SeatLayer
eventKey; holdId;- your
orderId/bookingRef; - payment reference;
- HTTP status and SeatLayer error code; and
- webhook delivery/event identifier.
Never log secret keys or full authorization headers. Avoid logging buyer personal data in inventory diagnostics.
Use webhooks for reconciliation
The synchronous booking response drives the buyer journey. Signed webhooks provide asynchronous confirmation and repair.
- Verify HMAC against the raw request body before parsing or trusting it.
- Deduplicate deliveries.
- Return a fast
2xx, then run slow business work asynchronously. - Route platform traffic by your stored event mapping and verify
workspaceId. - Keep test and live traffic separate using
livemode.
Test the system, not only the component
Before live credentials:
- two browsers compete for the same seat;
- a hold expires during checkout;
- payment succeeds but booking returns
409; - booking succeeds but the HTTP response times out;
- the same booking request is delivered twice;
- the page refreshes with an active hold;
- realtime disconnects and reconnects;
- the event sells out while the picker is open;
- a narrow mobile container completes checkout; and
- missing or mode-mismatched credentials fail safely.
Production checklist
- Secret and public configuration are separated.
- CSP allows only the SeatLayer origins your embed needs.
- Server hold inspection is the source of price and quantity.
-
bookingRefcomes from an immutable order identifier. - Payment conflict compensation is automated.
- Webhook signatures and duplicate deliveries are tested.
- Mobile, keyboard, accessibility, and 2D fallback are verified.
- Logs connect event, hold, order, and payment without leaking secrets.
- Test and live credentials cannot be mixed.
Use the complete checkout as a reference implementation.