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 checkout, payment, orders, receipts, and refunds. SeatLayer owns live inventory. The systems meet at one opaque value: holdId.

The transaction boundary

ticketing-transaction.txttext
Buyer
  └─ selects in SeatLayer buyer SDK
       └─ SDK creates a temporary hold
            └─ browser sends holdId to your server
                 ├─ your server inspects authoritative hold items
                 ├─ your server creates an order and runs payment
                 └─ your server books with secret key + bookingRef
                      ├─ 200: confirm the order
                      └─ 409: recover as an inventory conflict

Signed webhooks reconcile your downstream records after the synchronous flow.

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

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 and reconcile

Drive the buyer response from the synchronous booking result. Process signed 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",
  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();

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);

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 monetary refund and SeatLayer cancellation as two observable operations. SeatLayer releases inventory; it never moves the buyer’s money.

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.
  • 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