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.

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" }

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. Exceeding the budget answers 429 with a retryAfterSeconds hint. Releases do not consume the budget. Returning inventory is never rate-limited.

Navigation

Type to search…

↑↓ navigate↵ selectEsc close