This example shows the smallest production-shaped SeatLayer loop. Adapt the routing and payment calls to your framework; keep the trust boundary and state transitions intact.
Browser
The browser renders inventory, creates a hold, and sends only the hold id to your backend.
<div id="seat-picker" style="height: min(720px, 75vh)"></div>
<p id="checkout-error" role="alert"></p>
<script src="https://cdn.seatlayer.io/seatlayer-js@0/seatlayer.js"></script>
<script type="module">
const error = document.querySelector("#checkout-error");
const picker = new seatlayer.SeatPicker({
container: "#seat-picker",
event: window.SEATLAYER_EVENT_KEY,
onCheckout: async (_, __, handoff) => {
error.textContent = "";
const response = await fetch(
`/api/events/${window.BUSINESS_EVENT_ID}/checkout/seats`,
{
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({ holdId: handoff.holdId }),
},
);
if (response.status === 409) {
error.textContent =
"Your reservation changed. Please choose the seats again.";
return;
}
if (!response.ok) {
error.textContent = "Checkout could not continue. Please try again.";
return;
}
const { redirectUrl } = await response.json();
window.location.assign(redirectUrl);
},
});
await picker.render();
</script>The buyer never posts a price, category, or trusted seat allocation. Those values are resolved from the hold by the server.
Server
const SEATLAYER_API = "https://api.seatlayer.io";
async function seatlayer(path, init = {}) {
const response = await fetch(`${SEATLAYER_API}${path}`, {
...init,
headers: {
authorization: `Bearer ${process.env.SEATLAYER_SECRET_KEY}`,
"content-type": "application/json",
...init.headers,
},
});
return response;
}
export async function inspectHold(eventKey, holdId) {
const response = await seatlayer(
`/v1/events/${encodeURIComponent(eventKey)}/holds/${encodeURIComponent(holdId)}`,
);
if (response.status === 404 || response.status === 409) return null;
if (!response.ok) {
throw new Error(`SeatLayer hold inspection failed: ${response.status}`);
}
return response.json();
}
export async function bookHold(eventKey, holdId, bookingRef) {
return seatlayer(
`/v1/events/${encodeURIComponent(eventKey)}/book`,
{
method: "POST",
body: JSON.stringify({ holdId, bookingRef }),
},
);
}app.post("/api/events/:eventId/checkout/seats", async (req, res) => {
const holdId =
typeof req.body?.holdId === "string" ? req.body.holdId : "";
if (!holdId) return res.status(400).json({ error: "holdId_required" });
const event = await loadBusinessEvent(req.params.eventId);
const hold = await inspectHold(event.seatlayerKey, holdId);
if (!hold || hold.status !== "active") {
return res.status(409).json({ error: "hold_not_active" });
}
// Create from trusted server-returned items.
const order = await orders.create({
customerId: req.user.id,
eventId: event.id,
items: hold.items,
currency: hold.items[0]?.currency,
amount: hold.items.reduce(
(total, item) => total + item.unitPrice * item.quantity,
0,
),
});
// Prefer authorization before booking when your provider supports it.
const payment = await payments.authorize({
orderId: order.id,
amount: order.amount,
currency: order.currency,
});
const booking = await bookHold(
event.seatlayerKey,
holdId,
order.id, // stable idempotency reference
);
if (booking.status === 409) {
await payments.void(payment.id);
await orders.markInventoryConflict(order.id);
return res.status(409).json({ error: "inventory_conflict" });
}
if (!booking.ok) {
// The booking result may be unknown after a network/server failure.
// Queue a retry with the SAME order.id; do not issue a new bookingRef.
await retries.enqueueSeatLayerBooking({
eventKey: event.seatlayerKey,
holdId,
bookingRef: order.id,
paymentId: payment.id,
});
return res.status(202).json({
redirectUrl: `/orders/${order.id}/processing`,
});
}
await payments.capture(payment.id);
await orders.markBooked(order.id, await booking.json());
return res.json({
redirectUrl: `/orders/${order.id}/confirmation`,
});
});Retry worker
A timeout is an unknown result, not a failed result. Retry with the same order id:
async function retrySeatLayerBooking(job) {
const response = await bookHold(
job.eventKey,
job.holdId,
job.bookingRef,
);
if (response.ok) {
await payments.capture(job.paymentId);
await orders.markBooked(job.bookingRef, await response.json());
return;
}
if (response.status === 409) {
await payments.void(job.paymentId);
await orders.markInventoryConflict(job.bookingRef);
return;
}
throw new Error(`Retryable SeatLayer error: ${response.status}`);
}What to adapt
- Resolve the SeatLayer event key from your own trusted event record, not from arbitrary browser input.
- Use your actual authentication and authorization middleware.
- Use minor currency units if your payment provider requires them.
- Keep order creation idempotent if the browser repeats the checkout POST.
- Follow your provider’s authorization, capture, void, and refund semantics.
- Add a signed webhook receiver for reconciliation.
Tests worth keeping
| Test | Expected result |
|---|---|
| Valid active hold | One order, one booking, payment captured |
| Repeated checkout POST | Existing order/reference reused |
| Hold expired before inspection | 409, no payment attempted |
| Booking conflict after authorization | Payment voided, order recoverable |
| Booking response times out | Processing state, same reference retried |
| Retry after successful first booking | Idempotent success |
| Browser posts fake price | Ignored; hold items determine amount |
| Missing secret key | Safe server error, no browser secret |
Read integration best practices before adapting this to production.