---
title: "How the Platform integration works"
description: "Understand the browser hold, inventory booking, and realtime boundary when your platform owns commerce and fulfilment."
---

Every SeatLayer Platform/SDK integration follows one invariant:

> **The browser holds. Your server books inventory.**

This page assumes the [chart and event model](/start/overview/): one reusable
venue document, and many independent realtime occurrences created from it.

The buyer can explore inventory and temporarily reserve a selection without
receiving an account secret. Only your trusted backend can commit that hold.
Your platform separately owns payment, its commercial Order, and ticket
fulfilment. Direct organizers using SeatLayer-managed checkout follow the
[Managed Ticketing path](/start/choose-an-integration#3-managed-ticketing).

<Aside type="danger" title="A browser never books production inventory">
  Do not place a SeatLayer secret key in client JavaScript, a mobile binary, page HTML, or a client-exposed environment variable.
</Aside>

## The complete journey

<Steps>
  <Step title="Render live inventory">
    Your browser or mobile surface loads a published event through the buyer
    SDK. For ordinary Platform Public sale, the event key and publishable key
    start a direct, exact-origin bootstrap. Login, presale, partner, and channel
    inventory use a scoped token from your authenticated backend. Neither path
    grants server booking authority.
  </Step>
  <Step title="Select and hold">
    The buyer chooses seats, a table, booth, or GA quantity. The SDK creates a short-lived hold and returns an opaque `holdId` with an expiry time.
  </Step>
  <Step title="Inspect on your server">
    Your backend receives the `holdId` and resolves it with `GET /v1/events/:eventKey/holds/:holdId`. The returned items—not browser-posted prices—become the order source.
  </Step>
  <Step title="Run your order and payment logic">
    Create or reuse your own order. Authorize or capture payment according to your payment provider and recovery model.
  </Step>
  <Step title="Book inventory atomically">
    Your backend calls `POST /v1/events/:eventKey/book` using a secret key and your immutable order id as `bookingRef`.
  </Step>
  <Step title="Confirm, fulfil, and reconcile">
    The synchronous response drives the buyer journey. Your platform creates
    and delivers its tickets. Realtime updates repaint open pickers, and signed
    inventory webhooks reconcile downstream systems.
  </Step>
</Steps>

<FlowDiagram
  label="Platform purchase flow"
  steps={[
    {
      actor: "Buyer",
      title: "Selects seats",
      detail: "The buyer uses your browser or mobile surface to choose currently available inventory.",
      tone: "buyer",
    },
    {
      actor: "Buyer SDK",
      title: "Creates a temporary hold",
      detail: "The browser receives an opaque hold ID and sends that ID, not a booking request, to your backend.",
      tone: "seatlayer",
    },
    {
      actor: "Your server",
      title: "Inspects trusted items",
      detail: "Your backend uses its secret key to read authoritative prices, quantities, labels, and the expiry.",
      tone: "platform",
    },
    {
      actor: "Your platform",
      title: "Runs checkout",
      detail: "Your product creates the order and applies its own payment, tax, and customer rules.",
      tone: "payment",
    },
    {
      actor: "Your server",
      title: "Books inventory once",
      detail: "It books with the stable booking reference. The result is atomic: success or an inventory conflict.",
      tone: "platform",
    },
    {
      actor: "Your platform",
      title: "Fulfils and reconciles",
      detail: "Your application confirms the order, delivers tickets, and uses signed webhooks for reconciliation.",
      tone: "outcome",
    },
  ]}
/>

## Why the split exists

<CardGrid>
  <Card title="Security" icon="ph:shield-check">
    A browser can reserve inventory but cannot create a permanent inventory booking. Booking authority remains in your trusted environment.
  </Card>
  <Card title="Correctness" icon="ph:arrows-clockwise">
    Each event serializes inventory transitions, so two buyers cannot successfully take the same seat.
  </Card>
  <Card title="Product control" icon="ph:sliders-horizontal">
    Your application owns identity, pricing rules, payment, tax, commercial
    Orders, tickets, email, refunds, scanning, support, and the final confirmation experience.
  </Card>
</CardGrid>

## What each system owns

| Responsibility | Browser / SDK | Your server | SeatLayer |
|---|:---:|:---:|:---:|
| Display chart and live availability | Yes | No | Supplies state |
| Let a buyer select | Yes | No | Enforces availability |
| Create and restore holds | Yes | May inspect | Stores authoritative hold |
| Calculate the trusted charge | No | Yes | Supplies trusted line items |
| Process payment and orders | No | Yes | No |
| Permanently book inventory | No | Initiates | Applies atomically |
| Create and deliver tickets | No | Yes | No |
| Refund and check in buyers | No | Yes | No |
| Realtime inventory synchronization | Receives | Optional listener | Publishes |
| Signed event notification | No | Receives | Sends |

## Seat lifecycle

| State | Meaning | Typical transition |
|---|---|---|
| `free` | Available to buyers | Initial, released, or expired |
| `held` | Temporarily reserved | Buyer SDK creates a hold |
| `booked` | Permanently committed inventory | Your server books |
| `not_for_sale` | Removed from buyer inventory | Operator or inventory rule |

Holds expire automatically. Booking is all-or-nothing: if any required item cannot be booked, the request returns a conflict without partially completing the sale.

## The browser handoff

```js title="browser/seat-picker.js"
const picker = new seatlayer.SeatPicker({
  container: "#picker",
  event: "ev_9f3a",
  publicKey: "pk_test_…",
  onCheckout: async (_, __, handoff) => {
    await fetch("/api/checkout/seats", {
      method: "POST",
      headers: { "content-type": "application/json" },
      body: JSON.stringify({ holdId: handoff.holdId }),
    });
  },
});

await picker.render();
```

For Public sale, the first SeatLayer response carries both the chart and compact
inventory status. The SDK keeps the returned bearer in memory and renews it when
needed. Supplying `buyerAccessTokenProvider` or `buyerAccessToken` for a private
audience takes precedence over `publicKey` and prevents an anonymous fallback.

The browser sends your backend the hold id. Your backend obtains trusted labels, quantities, tiers, currency, and prices from the hold inspection endpoint.

## Expected failure paths

- **Hold expired:** return the buyer to selection and clear stale cart state.
- **Booking returned `409`:** treat it as a recoverable inventory conflict; do not partially confirm the order.
- **Payment succeeded but booking is uncertain:** retry using the same `bookingRef`.
- **Realtime connection dropped:** reconnect and refresh authoritative inventory.
- **Mode mismatch:** use a test key with test events and a live key with live events.

## Verify your understanding

Before implementation, you should be able to answer:

- Which value crosses from the browser to your server?
- Where is the secret key stored?
- Which response supplies authoritative inventory and configured-price input,
  and where does the final amount charged remain authoritative?
- Which identifier makes a retry safe?
- What buyer experience follows a `409`?

Continue to [Authentication](/start/authentication), then [install the Buyer SDK](/buyer-sdk/install).