Skip to content

Hold seats from your server

Reserve seats by label from your backend with a secret key, then book or release them before the hold expires.

Updated View as Markdown
POST/v1/events/:eventKey/holdSecret key

Most holds come from a buyer picking seats in the browser, through the Buyer SDK hold and checkout handoff. This endpoint is for the ones that do not: a phone or box-office order, an invoice reserved while payment authorizes, an allocation held for a partner. Your backend reserves the seats itself, then books or releases them.

A hold is not a sale, and holding costs nothing: only a confirmed sold seat consumes a credit under per-sold-seat pricing.

New to the model? How seat holds work covers the hold window, two-buyers-one-seat concurrency, expiry, and idempotent confirmation before you reach for the endpoint below.

Request

HTTP requesthttp
POST /v1/events/summer-gala-2026/hold HTTP/1.1
Host: api.seatlayer.io
Authorization: Bearer sk_test_••••••••
Content-Type: application/json

{
  "labels": ["STALLS-A-12", "STALLS-A-13"],
  "ttlMs": 900000
}
Field Type Required Description
labels string[] Yes* Seat labels to reserve.
selections object[] Yes* Instead of labels, when you need a tier or a quantity: { label, tierId?, quantity? }. Required for a variable-occupancy object such as a shared table or a GA area.
ttlMs number No Requested hold window. For this trusted server endpoint it overrides the event duration; when omitted, the event duration and then the 15-minute default apply. Clamped to 60 minutes.
replaceHoldId string No Atomically replace an existing hold with this new selection.

Responses

Every requested seat is now held for you until expiresAt.

201 responsejson
{
  "ok": true,
  "holdId": "hold_01J8F2A7MRQ4",
  "expiresAt": 1785312452577,
  "items": [
    {
      "label": "STALLS-A-12",
      "categoryKey": "stalls",
      "tierId": null,
      "unitPrice": 7500,
      "currency": "GBP",
      "quantity": 1
    }
  ]
}

Holds are all-or-nothing. If any seat is unavailable, nothing is held.

409 responsejson
{
  "error": "conflict",
  "conflicts": [{ "label": "STALLS-A-13", "status": "held" }]
}

A closed event answers 409 with "error": "event_closed".

A label that does not exist, is hidden, or carries a quantity outside the object’s occupancy rules. Nothing is held.

422 responsejson
{
  "error": "invalid_selection"
}

Releasing

POST/v1/events/:eventKey/releaseSecret key

Release a hold you no longer need immediately rather than waiting for it to expire. Common cases include an abandoned checkout, a declined card, or a cancelled phone order.

HTTP requesthttp
POST /v1/events/summer-gala-2026/release HTTP/1.1
Authorization: Bearer sk_test_••••••••
Content-Type: application/json

{
  "labels": ["STALLS-A-12", "STALLS-A-13"],
  "holdId": "hold_01J8F2A7MRQ4"
}
200 responsejson
{
  "ok": true,
  "released": ["STALLS-A-12", "STALLS-A-13"]
}

Full flow

server/reserve.jsjs
const base = "https://api.seatlayer.io/v1/events/summer-gala-2026";
const headers = {
  authorization: `Bearer ${process.env.SEATLAYER_SECRET_KEY}`,
  "content-type": "application/json",
};

// 1. Reserve the seats.
const held = await fetch(`${base}/hold`, {
  method: "POST",
  headers,
  body: JSON.stringify({ labels: ["STALLS-A-12", "STALLS-A-13"] }),
});

if (held.status === 409) return seatsNoLongerAvailable();
const { holdId, items } = await held.json();

// 2. Charge from OUR prices, never the caller's.
const total = items.reduce((sum, item) => sum + item.unitPrice * item.quantity, 0);
const payment = await charge(total);

// 3. Book it, or hand the seats straight back.
if (payment.ok) {
  await fetch(`${base}/book`, {
    method: "POST",
    headers,
    // Pin the seats you charged for: `409 hold_changed` means the buyer
    // changed seats after this hold was priced. Refund; do not retry.
    body: JSON.stringify({ holdId, labels: items.map((item) => item.label), bookingRef: payment.orderId }),
  });
} else {
  await fetch(`${base}/release`, {
    method: "POST",
    headers,
    body: JSON.stringify({ labels: ["STALLS-A-12", "STALLS-A-13"], holdId }),
  });
}

Let us pick the seats

POST/v1/events/:eventKey/best-availableSecret key

When the caller does not care which seats, only how many, such as for a phone order or a “best 4 together” request, ask for best available instead of naming labels. The picker is the same one the buyer widget uses, so a phone order and a web order get the same answer for the same inventory.

HTTP requesthttp
POST /v1/events/summer-gala-2026/best-available HTTP/1.1
Authorization: Bearer sk_test_••••••••
Content-Type: application/json

{ "qty": 4, "categoryKey": "stalls" }
Field Type Required Description
qty number Yes How many to pick. Clamped to the server maximum rather than rejected.
categoryKey string No Restrict to one price category.
zoneId string No Restrict to one zone. An unknown zone answers 422, never silently ignored.
ttlMs number No Requested hold window, same contract as /hold.
nearLabels string[] No 1 to 10 labels to pick close to, such as a buyer’s original seats. The pick stays in one contiguous group and prefers the same rows.
preferAisle boolean No Prefer a group with at least one aisle seat: a row end, or a seat beside a gap. Seats behind the stage still rank last. Off by default.

The response is a hold: holdId, expiresAt, the chosen labels, and priced items. A 409 with reason: "sold_out" or "not_enough_together" means the request could not be satisfied. Treat that as a normal outcome, not an alert.

Book without holding first

POST/v1/events/:eventKey/best-available-bookSecret key

For box-office and phone sales where payment is already taken, pick and book in one call. bookingRef is required so the sale can be reconciled against your own order.

HTTP requesthttp
POST /v1/events/summer-gala-2026/best-available-book HTTP/1.1
Authorization: Bearer sk_test_••••••••
Content-Type: application/json

{ "qty": 2, "bookingRef": "phone-1183" }

Set seats aside for a longer offer

POST/v1/events/:eventKey/best-available-blockSecret key

A buyer hold lasts at most an hour. When you need to offer replacement seats to a customer who may take a day to answer, for example after a paid order had to move, pick the seats and block them instead. The block ends by itself at releaseAt.

HTTP requesthttp
POST /v1/events/summer-gala-2026/best-available-block HTTP/1.1
Authorization: Bearer sk_test_••••••••
Content-Type: application/json

{
  "qty": 2,
  "nearLabels": ["C-14", "C-15"],
  "releaseAt": 1767290400000,
  "blockRef": "reseat-order-8812"
}
200 OKjson
{
  "ok": true,
  "labels": ["C-16", "C-17"],
  "items": [
    { "label": "C-16", "objectId": "seat-c-16", "objectType": "seat", "categoryKey": "stalls", "tierId": null, "unitPrice": 60, "currency": "EUR", "quantity": 1 },
    { "label": "C-17", "objectId": "seat-c-17", "objectType": "seat", "categoryKey": "stalls", "tierId": null, "unitPrice": 60, "currency": "EUR", "quantity": 1 }
  ],
  "placements": [
    { "label": "C-16", "sectionLabel": null, "rowLabel": "C", "seatLabel": "16" },
    { "label": "C-17", "sectionLabel": null, "rowLabel": "C", "seatLabel": "17" }
  ],
  "releaseAt": 1767290400000
}

It takes the same qty, categoryKey, zoneId, and nearLabels fields as best available, plus two required fields:

Field Type Description
releaseAt number Epoch ms when the block ends. At least 1 minute and at most 72 hours from now; otherwise 400 releaseAt_required
blockRef string Your reference for the offer, up to 160 characters, recorded in the event log; otherwise 400 blockRef_required

To confirm, call POST /v1/events/:eventKey/book with the labels, your bookingRef, and the same blockRef (no holdId). To withdraw the offer early, unblock them. It shares the server hold budget.

Keep a hold alive

POST/v1/events/:eventKey/extendSecret key

When an order takes longer than the checkout window, for example while an invoice awaits approval or a caller finds their card, extend the hold rather than releasing and re-holding. Releasing first hands the seats to whoever is racing for them in between.

HTTP requesthttp
POST /v1/events/summer-gala-2026/extend HTTP/1.1
Authorization: Bearer sk_test_••••••••
Content-Type: application/json

{ "holdId": "h_9f2c…", "ttlMs": 600000 }

A hold that is already gone, expired, or at its renewal cap answers 409 with error: "cannot_extend". The hold cannot be recovered; the buyer must pick again. On this trusted server endpoint, an explicit ttlMs takes priority over the event duration. Extensions share the hold budget and the same server-side TTL ceiling, so a hold cannot be renewed indefinitely.

Rate limits

Server holds are budgeted per secret key rather than per IP, because one backend legitimately speaks for every buyer on your platform: 600 requests per minute, shared by hold, extend, and the three Best Available routes. Exceeding the budget answers 429 with Retry-After and a retryAfterSeconds hint. Releases do not consume the budget. Returning inventory is never rate-limited. See rate limits for every budget and the response headers, and concurrency and performance for how simultaneous holds on the same seats are resolved.

Navigation

Type to search…

↑↓ navigate↵ selectEsc close