---
title: "Book inventory"
description: "Commit held inventory from your trusted server with an idempotent booking reference and safe conflict handling."
---

<ApiEndpoint method="POST" path="/v1/events/:eventKey/book" auth="Secret key" />

Booking turns a temporary hold into a permanent **inventory booking**. It does
not create your commercial order or fulfil the purchase. This endpoint is
server-only and accepts `sk_test_…` or `sk_live_…` credentials whose mode
matches the event. The event must explicitly use **Platform checkout**
(`buyerCheckout: "integrator"`); Managed and unresolved events return 404 so a
server key cannot bypass SeatLayer's managed checkout.

<Aside type="danger" title="Server-side only">
  Never call this endpoint from browser code. The buyer SDK holds inventory and
  hands your backend an opaque `holdId` through the
  [checkout handoff](/buyer-sdk/holds-and-checkout/); only your backend has
  booking authority.
</Aside>

## Request

```http title="HTTP request"
POST /v1/events/ev_9f3a/book HTTP/1.1
Host: api.seatlayer.io
Authorization: Bearer sk_test_••••••••
Content-Type: application/json

{
  "holdId": "hold_01J8F2A7MRQ4",
  "labels": ["STALLS-A-12", "STALLS-A-13"],
  "bookingRef": "order_1842"
}
```

| Field | Type | Required | Description |
|---|---|---:|---|
| `holdId` | `string` | Yes for a buyer flow | Opaque hold identifier returned by the buyer SDK. |
| `labels` | `string[]` | Recommended with `holdId` | The seats you priced at checkout, copied from the trusted server-side hold inspection. A hold keeps its id when the buyer changes seats, so pinning the labels makes booking fail with `409 hold_changed` instead of booking seats you never charged for. Required for a direct booking without a hold. |
| `bookingRef` | `string` | Yes | Your stable order or reservation reference. It makes retries idempotent and links SeatLayer inventory to your own system. |

Do not send buyer identity, payment details, ticket delivery data, or an amount
to this endpoint. Those belong to your platform.

<Aside type="caution" title="Private inventory needs a named channel">
  This affects direct label bookings after you introduce
  [sales channels](/platform/sales-channels). Booking a buyer hold is unaffected
  because the hold already captured its inventory authorization.

  For a direct label booking against private inventory, pass the authorized
  `channelIds`. Use `ignoreChannelRestrictions: true` with a short `reason` only
  for a genuine back-office override; the action is audited. Public inventory
  continues to work without a channel.
</Aside>

## Responses

<Tabs>
  <TabItem label="200 Booked">
    Every requested object is now booked in SeatLayer inventory.

    ```json title="200 response"
    {
      "ok": true,
      "booked": ["STALLS-A-12", "STALLS-A-13"]
    }
    ```

    On an idempotent replay, `booked` may be empty because that same
    `bookingRef` already committed the inventory:

    ```json title="200 replay response"
    {
      "ok": true,
      "booked": []
    }
    ```

    Keep the commercial receipt, buyer, tickets, and delivery state in your own
    order record. Only `ok` and `booked` belong to the Platform booking
    response.
  </TabItem>
  <TabItem label="409 Conflict">
    Nothing is booked when any requested object is unavailable.

    ```json title="409 response" {3-7}
    {
      "error": "conflict",
      "conflicts": [
        {
          "label": "STALLS-A-13",
          "status": "booked"
        }
      ]
    }
    ```
  </TabItem>
  <TabItem label="403 Mode mismatch">
    A test key cannot book live inventory, and a live key cannot book a sandbox event.

    ```json title="403 response"
    {
      "error": "mode_mismatch"
    }
    ```
  </TabItem>
</Tabs>

## Safe retry behavior

`bookingRef` is your [idempotency key](/server-api/idempotency-and-conflicts/).
Repeating the same request after a timeout
succeeds without creating a second inventory booking or spending credits twice.
Always reuse the same value for the same platform purchase.

```js title="server/book.js"
const trustedHold = await retrieveSeatLayerHold(eventKey, holdId);
const labels = trustedHold.items.map((item) => item.label);
const order = await priceAndAuthorizePayment(trustedHold); // Your commerce code.

const response = await fetch(
  `https://api.seatlayer.io/v1/events/${encodeURIComponent(eventKey)}/book`,
  {
    method: "POST",
    headers: {
      authorization: `Bearer ${process.env.SEATLAYER_SECRET_KEY}`,
      "content-type": "application/json",
    },
    body: JSON.stringify({ holdId, labels, bookingRef: order.id }),
  },
);

if (response.status === 409) {
  return recoverInventoryConflict();
}

if (!response.ok) {
  // Reconcile order.id through exact Booking History before another action.
  throw new Error(`SeatLayer booking failed: ${response.status}`);
}
```

If the connection fails before a response arrives, look up
`GET /v1/events/:eventKey/bookings/:bookingRef`. Only then decide whether your
workflow should repeat the **same** event, hold, labels, and `bookingRef`. Never
change the reference merely because the first outcome is unknown.

## Read inventory Booking History

Platform events expose the durable inventory ledger through the official
server API:

```http title="List and search"
GET /v1/events/ev_9f3a/bookings?q=order_1842&state=booked&limit=50
Authorization: Bearer sk_test_••••••••
```

Pass the opaque `nextCursor` back as `cursor` to read the next page. Search is
limited to inventory identifiers such as booking reference, label, object,
category, section, and sales-channel reference.

```http title="Exact booking and audit"
GET /v1/events/ev_9f3a/bookings/order_1842
Authorization: Bearer sk_test_••••••••
```

The detail response includes immutable configured-price snapshots, current
object cancellation state, and a chronological `book` / `replay` /
`partial_cancel` / `cancel` audit. Configured value is operational inventory
value—not the amount charged or settled. These routes never return buyer,
payment, commercial Order, ticket, email, refund, or door data.

The synchronous booking response or this exact Booking History record is the
command outcome. A later [`seat.booked` webhook](/webhooks/events/#event-catalog)
is a reconciliation signal; deduplicate it and never use it as permission to
charge the buyer again.

For a resumable background sync, use the
[`/booking-changes` reconciliation feed](/server-api/reports#incremental-booking-reconciliation).
Its append-only activity checkpoint surfaces cancellations to older booking
references without paging the full Booking History again.

<Aside type="tip" title="Inspect the hold on your server">
  Resolve the hold before your payment decision. SeatLayer's hold response is
  authoritative for the inventory, quantities, categories, and configured
  price snapshots. Your platform remains authoritative for discounts, fees,
  taxes, the final amount charged, and payment state; never trust those values
  when they come from the browser.
</Aside>

## Fulfilment and refunds stay in your platform

After a successful inventory booking, your platform confirms its commercial
order and creates and delivers its own tickets, QR/barcodes, PDFs, and emails.
It also owns ticket recovery, scanning/check-in, refunds, and customer support.

If your platform later cancels or refunds the purchase, call
[`/unbook`](/server-api/cancellations-and-box-office) with the matching
`bookingRef` to return inventory to sale. SeatLayer changes inventory on this
route; it does not move or refund your money.

<Aside type="note" title="Behavior corrected on 2026-08-10">
  Older deployments briefly created internal `external` Orders and SeatLayer
  ticket artifacts after `/book`. That behavior was removed. Existing
  historical records may remain available during migration, but new Platform
  bookings create inventory records only.
</Aside>