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

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. Defaults to the event’s checkout window. Clamped server-side to a 60-minute ceiling.
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

Let a hold you no longer need go back on sale immediately rather than waiting for it to expire — an abandoned checkout, a declined card, 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,
    body: JSON.stringify({ holdId, 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 — a phone order, 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 — a normal outcome, not an error to alert on.

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 — an invoice awaiting approval, a caller hunting for 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". There is no recovering it — the buyer has to pick again. 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 — handing inventory back is never rate-limited.

Navigation

Type to search…

↑↓ navigate↵ selectEsc close