---
title: "Custom applications"
description: "Integrate SeatLayer into any frontend, backend, checkout, payment provider, or analytics stack without giving up product control."
---

SeatLayer is designed to sit inside an existing application rather than replace
it. Use the browser SDK for interactive inventory and the HTTP API from any
trusted server runtime.

<Aside type="tip" title="Custom to custom is the normal path">
  Your framework, database, cart, payment provider, analytics, and deployment
  platform do not need a SeatLayer-specific adapter. Keep a small integration
  boundary around the documented browser and HTTP contracts.
</Aside>

## Reference architecture

<FlowDiagram
  label="Custom-application integration boundary"
  steps={[
    {
      actor: "Your frontend",
      title: "Runs the buyer experience",
      detail: "SeatPicker or SeatingChart lives beside your cart, account, checkout UI, and analytics adapter.",
      tone: "buyer",
    },
    {
      actor: "Browser handoff",
      title: "Sends the hold ID",
      detail: "The browser passes an opaque hold ID to your backend; it never receives a SeatLayer secret key.",
      tone: "outcome",
    },
    {
      actor: "Your backend",
      title: "Owns product and commerce",
      detail: "Authentication, authorization, orders, payment, and the small secret-key SeatLayer adapter remain inside your application.",
      tone: "platform",
    },
    {
      actor: "SeatLayer",
      title: "Supplies charts and live inventory",
      detail: "Your adapter inspects and books inventory, while signed webhooks return inventory changes for reconciliation.",
      tone: "seatlayer",
    },
  ]}
/>

Only the browser SDK needs DOM access. The server integration is standard HTTPS,
so it works in Node, Bun, Deno, PHP, Ruby, Python, Go, Java, .NET, Workers, and
other environments that can send authenticated requests.

## Keep the integration boundary small

Create one server module responsible for:

- loading and validating the SeatLayer secret key;
- building the API base URL for the current environment;
- inspecting a hold;
- booking with a stable `bookingRef`;
- cancelling or changing server-managed inventory;
- normalizing errors into your application's domain errors; and
- attaching request ids to logs without recording secrets.

```ts title="server/seatlayer.ts"
const apiBase = "https://api.seatlayer.io";

async function seatLayerRequest(path: string, init: RequestInit = {}) {
  const response = await fetch(`${apiBase}${path}`, {
    ...init,
    headers: {
      authorization: `Bearer ${process.env.SEATLAYER_SECRET_KEY}`,
      "content-type": "application/json",
      ...init.headers,
    },
  });

  const body = await response.json().catch(() => null);
  return { ok: response.ok, status: response.status, body };
}

export function inspectHold(eventKey: string, holdId: string) {
  return seatLayerRequest(
    `/v1/events/${encodeURIComponent(eventKey)}/holds/${encodeURIComponent(holdId)}`,
  );
}

export function bookHold(
  eventKey: string,
  input: { holdId: string; bookingRef: string },
) {
  return seatLayerRequest(
    `/v1/events/${encodeURIComponent(eventKey)}/book`,
    { method: "POST", body: JSON.stringify(input) },
  );
}
```

Wrap this module in your existing service, dependency-injection, tracing, and
test conventions. Do not spread direct SeatLayer calls across route handlers.

## Define the browser-to-server contract

Your checkout route needs an opaque hold id and your own application context:

```ts title="shared/checkout-contract.ts"
type SeatCheckoutRequest = {
  eventKey: string;
  holdId: string;
  cartId: string;
};
```

`eventKey` may instead come from a trusted product record or route parameter.
Never accept buyer-supplied price, currency, category, or labels as the booking
authority.

## Connect any frontend

<Tabs>
  <TabItem label="Vanilla JavaScript">
    Mount `SeatPicker` from the hosted script or ESM build and call your
    checkout endpoint from `onCheckout`.
  </TabItem>
  <TabItem label="React">
    Use `@seatlayer/react` and keep the event key and callbacks in component
    state. Hold state remains server-authoritative.
  </TabItem>
  <TabItem label="Other frameworks">
    Mount the vanilla SDK in the framework's client-only lifecycle hook and call
    `destroy()` during cleanup. Avoid rendering it during server-side rendering.
  </TabItem>
  <TabItem label="Mobile webview">
    Load the buyer surface after the webview is ready, allow WebGL only if using
    3D, and bridge the checkout handoff into the native application.
  </TabItem>
</Tabs>

For an ordinary Platform Public sale, configure the event key plus `publicKey`.
SeatLayer validates the exact registered origin and returns a Public-only
in-memory session with the chart and compact inventory status together. Login,
presale, partner, and channel inventory use `buyerAccessTokenProvider` or
`buyerAccessToken` from your authenticated backend instead. The explicit scoped
credential always takes precedence.

## Send analytics anywhere

`SeatPicker.onAnalytics` is a callback, not a SeatLayer analytics destination.
Forward supported events to the system your application already uses:

```js title="browser/analytics.js"
const picker = new seatlayer.SeatPicker({
  container: "#picker",
  event: "ev_9f3a",
  publicKey: "pk_test_…",
  onAnalytics: (event, properties) => {
    // PostHog
    posthog.capture(`seatlayer_${event}`, properties);

    // Or Google Analytics:
    // gtag("event", `seatlayer_${event}`, properties);

    // Or your own endpoint:
    // navigator.sendBeacon("/analytics", JSON.stringify({ event, properties }));
  },
});
```

The currently emitted event names cover the 3D buyer journey and are marked
**Preview** in the [analytics reference](/buyer-sdk/analytics). A throwing sink
does not break the picker, but your adapter should still be defensive and avoid
sending personal data accidentally.

## Adapt errors into product behavior

| SeatLayer result | Domain behavior |
|---|---|
| `404 hold_not_found` | Cart is stale; return to selection |
| Active hold with expired timestamp | Return to selection and clear persisted hold |
| `403 mode_mismatch` | Configuration fault; alert operators, not buyers |
| Booking `409` | Recoverable inventory conflict |
| Timeout or `5xx` after booking request | Unknown state; retry with the same `bookingRef` |
| Webhook signature failure | Reject and log security event |

Keep raw provider errors in structured server logs and return stable,
buyer-appropriate application errors to the client.

## Test without real dependencies

Inject the HTTP transport or mock your server adapter at its boundary. Cover:

1. successful hold inspection and booking;
2. expired or missing hold;
3. payment rejection before booking;
4. atomic booking conflict;
5. booking timeout followed by same-reference retry;
6. webhook duplicate delivery;
7. missing or wrong-mode credentials; and
8. browser cleanup during navigation.

An end-to-end test in SeatLayer test mode should complement—not replace—these
fast contract tests.

## Production checklist

- [ ] SDK code runs client-side and is destroyed on unmount.
- [ ] A single server adapter owns API calls and secret loading.
- [ ] Public Platform mounts use a matching `publicKey` from an exact registered origin; scoped private mounts use a server-backed `buyerAccessTokenProvider`. Any resulting `bse_` stays out of logs, URLs, and storage.
- [ ] Browser payloads contain no trusted price or booking authority.
- [ ] HTTP errors are mapped to stable product states.
- [ ] Checkout retries reuse the existing order id.
- [ ] Analytics forwards only intended, non-sensitive properties.
- [ ] Unit, contract, and test-mode end-to-end paths pass.
- [ ] Framework upgrades cannot silently duplicate picker instances.

Continue with [install the Buyer SDK](/buyer-sdk/install),
[integrate a ticketing backend](/integrations/ticketing-backend), or
[build with agents](/agents/overview).