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

> Documentation Index
> Fetch the complete documentation index at: https://docs.seatlayer.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Book inventory

**POST /v1/events/:eventKey/book**. Authentication: 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.

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

## Request

```http title="HTTP request"
POST /v1/events/<YOUR_EVENT_KEY>/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. |
| `blockRef` | `string` | No | Books seats you set aside with [`best-available-block`](/server-api/holds/#set-seats-aside-for-a-longer-offer). Send it with `labels` and without `holdId`; up to 160 characters. |

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

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

## Responses

### 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.
### 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"
    }
  ]
}
```
### 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"
}
```

## 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/<YOUR_EVENT_KEY>/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.

Narrow the list with `channel` (a sales channel id, or `public` for seats sold
with no channel) and a booked-at window, `from` and `to` in epoch milliseconds.
Filters apply before paging, so every page holds only matching bookings.

```http title="Exact booking and audit"
GET /v1/events/<YOUR_EVENT_KEY>/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. Every amount is in the event's
`currency`, on the booking and on each object; each object also names its
category (`categoryLabel`, beside the stable `categoryKey`), and a seat that
sits outside every section has a null `sectionLabel`. 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.

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

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

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

Source: https://docs.seatlayer.io/server-api/booking/index.mdx
