Skip to content

Integrate a ticketing backend

Connect SeatLayer holds and bookings to your existing order, payment, and reconciliation flow.

Updated View as Markdown

Use this pattern when you already own the event catalogue, buyer, checkout, payment, commercial Orders, receipts, tickets, delivery, scanning, and refunds. SeatLayer owns live inventory. The buyer/server handoff is holdId; your stable bookingRef links the resulting inventory booking to your commercial order.

The transaction boundary

  1. BuyerSelects in the picker

    The buyer chooses seats through the SeatLayer buyer SDK in your application.

  2. Buyer SDKCreates the temporary hold

    The browser sends only the opaque hold ID to your checkout route.

  3. Your serverReads authoritative hold items

    Your backend validates the active hold and calculates the trusted order amount from its returned items.

  4. Your platformCreates the order and runs payment

    Your own order ID becomes the stable booking reference for this purchase.

  5. SeatLayerBooks atomically

    A successful booking confirms the order. A 409 is an inventory conflict to recover, not a partial sale.

  6. Your serverReconciles signed events

    Process webhooks idempotently to repair or confirm downstream records after the synchronous result.

The browser must not post trusted prices, seat labels, or a booking request. Those values either come from hold inspection or are owned by your server.

Implement the flow

Configure the buyer's browser-access lane

For an ordinary Platform Public sale, register the checkout page’s exact Embed domain and pass the matching account publicKey; SeatLayer returns a Public-only in-memory session with the chart and compact inventory snapshot. For login, presale, partner, or channel inventory, your authenticated backend instead calls POST /v1/events/:key/buyer-access-sessions and supplies the result through buyerAccessTokenProvider. Managed public/unlisted events need neither browser credential.

Create the hold in the browser

Let SeatPicker hold during its checkout action, or explicitly call SeatingChart.hold(). Persist the resulting holdId with the in-progress order.

Inspect the hold on your server

Resolve the hold using a secret key. Reject an expired hold and calculate the order from the returned items rather than browser input.

Run payment and order logic

Create a durable order id. Prefer a payment authorization that can be captured after booking when your payment provider supports it; otherwise define the void/refund compensation path before launch.

Book atomically

Call the book endpoint using the order id as bookingRef. Reuse that exact value for every retry of the same purchase.

Confirm, fulfil, and reconcile

Drive the buyer response from the synchronous booking result, then create and deliver tickets from your platform. Process signed inventory webhooks idempotently to repair or confirm downstream state.

1. Create and hand off the hold

With the headless chart, call hold() after selection:

browser/seats.jsjs
const chart = new seatlayer.SeatingChart({
  container: "#chart",
  event: "ev_9f3a",
  publicKey: "pk_test_…", // ordinary Public Platform sale
  onHold: async ({ holdId, expiresAt }) => {
    await fetch("/api/checkout/start", {
      method: "POST",
      headers: { "content-type": "application/json" },
      body: JSON.stringify({ holdId, expiresAt }),
    });
  },
});

await chart.render();
await chart.hold();

For a scoped private audience, replace publicKey with a server-backed buyerAccessTokenProvider. The hold and booking boundary does not change.

SeatPicker performs the hold as part of its CTA and exposes the same boundary through onCheckout(_, _, handoff). Prefer its third handoff argument for new integrations.

2. Inspect authoritative items

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

if (!holdResponse.ok) {
  return reply.status(409).send({ code: "hold_unavailable" });
}

const hold = await holdResponse.json();
if (hold.status !== "active" || hold.expiresAt <= Date.now()) {
  return reply.status(409).send({ code: "hold_unavailable" });
}

const total = hold.items.reduce(
  (sum, item) => sum + item.unitPrice * (item.quantity ?? 1),
  0,
);

Use the returned currency, tier, quantity, and price fields supported by your order model. Treat the browser handoff as display context, never the charging authority.

3. Book after the payment decision

POST/v1/events/:eventKey/bookSecret key
server/book-order.jsjs
const bookingResponse = 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: order.id,
    }),
  },
);

if (bookingResponse.status === 409) {
  await voidOrRefundPayment(order);
  return reply.status(409).send({ code: "inventory_conflict" });
}

if (!bookingResponse.ok) {
  // State may be uncertain after a timeout or transport failure.
  // Retry with the same order.id—never mint a new bookingRef.
  throw new Error(`SeatLayer booking failed: ${bookingResponse.status}`);
}

await markOrderConfirmed(order.id);
await issueAndDeliverYourTickets(order.id);

bookingRef is your idempotency boundary. If a request times out, retry the same purchase with the same value. A confirmed replay is safe, but the current replay response contains booked: []; keep the durable receipt details in your order record and webhook handler.

Choose a payment sequence

Sequence Best when Required recovery
Authorize → book → capture Your provider supports separate authorization and capture Void authorization if booking conflicts
Charge → book Immediate capture is required Refund promptly on conflict and monitor compensation failures
Book → charge Only for trusted/no-payment flows where unpaid inventory is acceptable Cancel booking if payment fails

Do not hide this tradeoff inside a generic “checkout failed” branch. Record each transition so support staff can distinguish payment state from inventory state.

Reconcile signed webhooks

server/webhook.jsjs
app.post("/webhooks/seatlayer", rawBodyMiddleware, async (req, res) => {
  verifySeatLayerSignature(req.rawBody, req.headers);

  const { event, payload } = JSON.parse(req.rawBody);
  if (event === "seat.booked") {
    await confirmOrderIdempotently(payload);
  }

  res.sendStatus(200);
});

Webhooks are the reconciliation path, not a reason to keep the buyer waiting. Verify the signature against the raw request body, return a successful status quickly, and process every delivery idempotently.

Expected failure behavior

Failure Application response
Hold is expired before payment Clear the stale cart and return to selection
Booking returns 409 Void/refund as needed; do not partially confirm
Booking response times out Retry with the same bookingRef
Payment succeeds but booking remains uncertain Keep an explicit recovery state and reconcile/retry
Webhook arrives twice Deduplicate by delivery/event identity and order state
Webhook arrives before local confirmation commits Upsert idempotently rather than rejecting it

To reverse a completed sale, run your ticket cancellation, monetary refund, and SeatLayer inventory cancellation as observable operations in your system. SeatLayer releases inventory; it does not move money or cancel/deliver your tickets.

Production checklist

  • The browser sends only holdId and non-authoritative UI context.
  • Hold inspection happens with a server-only secret key.
  • Price and currency are derived from the inspected hold.
  • One durable order id is reused as bookingRef.
  • Our platform creates, delivers, recovers, and scans its own tickets.
  • Payment compensation for 409 is tested.
  • Timeout retries use the same request identity.
  • The webhook handler verifies the raw body and is idempotent.
  • Support can see payment state and inventory state separately.
  • Test mode has completed a full purchase and duplicate retry.

Continue with the booking endpoint, idempotency and conflict behavior, or webhook signatures.

Navigation

Type to search…

↑↓ navigate↵ selectEsc close