This example shows the smallest production-shaped Platform/SDK loop. Adapt the routing and payment calls to your framework; keep the trust boundary and state transitions intact. Your application owns the commercial Order and creates, delivers, recovers, and scans its own tickets.
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-buyer.js"></script>
<script type="module">
const error = document.querySelector("#checkout-error");
const picker = new seatlayer.SeatPicker({
container: "#seat-picker",
event: window.SEATLAYER_EVENT_KEY,
publicKey: window.SEATLAYER_PUBLIC_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>Register this page’s exact origin as an Embed domain. The public key starts one
direct Public-sale bootstrap; SeatLayer returns the chart and compact inventory
status together, and the SDK keeps the resulting bearer in memory. For a
login, presale, partner, or channel audience, replace publicKey with a scoped
buyerAccessTokenProvider from your authenticated backend.
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 mintPrivateBuyerAccess(eventKey, allowedOrigin) {
const response = await seatlayer(
`/v1/events/${encodeURIComponent(eventKey)}/buyer-access-sessions`,
{
method: "POST",
body: JSON.stringify({
// A private agency allocation. Both fields are deliberate: SeatLayer
// has no default for includePublic and an empty scope is rejected.
channelIds: ["chn_agency_private"],
includePublic: false,
// One exact HTTPS origin — the origin your checkout page is served
// from. SeatLayer checks it on every request the token makes.
allowedOrigin,
}),
},
);
if (!response.ok) {
throw new Error(`SeatLayer buyer access mint failed: ${response.status}`);
}
const session = await response.json();
// Keep sessionId for audit/revocation; hand only these two to the browser.
return { token: session.token, expiresAt: session.expiresAt };
}
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 }),
},
);
}// Private-audience alternative to the publicKey bootstrap above. Gate this
// route according to who may see the login, presale, partner, or channel
// inventory. The token cannot book; booking stays with your server.
app.post("/api/events/:eventId/seat-access", async (req, res) => {
const event = await db.events.findById(req.params.eventId);
if (!event?.seatlayerEventKey) {
return res.status(404).json({ error: "event_not_found" });
}
// Bind the token to the page's origin. Never trust a body-supplied origin;
// derive it from the request (Origin header) or your configured site URL.
const allowedOrigin = req.get("origin") ?? process.env.SHOP_ORIGIN;
const session = await mintPrivateBuyerAccess(
event.seatlayerEventKey,
allowedOrigin,
);
res.set("cache-control", "no-store");
res.json(session);
});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());
await tickets.issueAndDeliver(order.id);
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.
- Expose only the matching publishable key to the browser, register the exact
Embed origin, and never expose
SEATLAYER_SECRET_KEY. - 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.
- Create and deliver tickets only after the inventory booking is confirmed.
- Keep ticket recovery, scanning, and attendance in your platform.
- 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.