Official Node.js server SDK for the SeatLayer reserved-seating API.
Server-side only. This package authenticates with your secret key. Never bundle it into a browser, a mobile app, or anything a ticket buyer can open. Browser surfaces get short-lived, origin-bound tokens that you mint here — see Embedding the control room.
This SDK is the Platform inventory product. SeatLayer owns seating state, configured prices, holds, booking concurrency, the inventory ledger, allocation reporting, and inventory webhooks. Your platform owns its event catalogue, buyer accounts, payments, commercial Orders, tickets, email/PDF delivery, refunds, scanning, and customer support. No booking method in this package accepts buyer, payment, ticket, email, or refund data.
npm install @seatlayer/serverRequires Node 20.19.4 or newer. No runtime dependencies.
import { SeatLayer } from '@seatlayer/server';
const seatlayer = new SeatLayer(process.env.SEATLAYER_SECRET_KEY!);
// 1. Provision a venue for a new organiser from one of your templates.
const { meta: chart } = await seatlayer.charts.copy('c_template_arena');
await seatlayer.charts.publish(chart.id);
// 2. Create an event on it.
const { meta: event } = await seatlayer.events.create({
chartId: chart.id,
name: 'Spring Gala',
startsAt: Date.parse('2026-09-12T19:30:00Z'),
});
// 3. Reserve four seats for an order in your own commerce system.
const held = await seatlayer.inventory.holdBestAvailable(event.key, { qty: 4 });
// … your system takes payment against held.items' authoritative configured prices …
await seatlayer.inventory.book(event.key, { holdId: held.holdId, bookingRef: 'order-8842' });Keys carry their own mode. sk_test_… keys can only touch test-mode events, and sk_live_… keys
only live ones; crossing them returns 403 mode_mismatch, surfaced as
SeatLayerAuthError with isModeMismatch === true.
const seatlayer = new SeatLayer(process.env.SEATLAYER_SECRET_KEY!);
if (process.env.NODE_ENV === 'production' && seatlayer.mode !== 'live') {
throw new Error('Refusing to boot production against test-mode seating data.');
}Buyer picks seats in the browser. Your frontend holds them; your backend confirms the price and
books. Never price from what the browser sent you — retrieveHold is the authoritative answer.
const hold = await seatlayer.inventory.retrieveHold(eventKey, holdId);
const total = hold.items.reduce((sum, item) => sum + item.unitPrice, 0);
// … charge `total` in hold.currency …
await seatlayer.inventory.book(eventKey, { holdId, bookingRef: charge.id });Your backend picks the seats. Marketplace orders, phone orders, or comps. No browser involved.
// Payment already taken — book outright, so nothing is stranded if a second call fails.
await seatlayer.inventory.bookBestAvailable(eventKey, { qty: 2, bookingRef: 'phone-1183' });
// Or name the seats yourself.
await seatlayer.inventory.book(eventKey, { labels: ['A-1', 'A-2'], bookingRef: 'comp-14' });bookingRef is your stable join between SeatLayer inventory and your own commercial order. The SDK
trims it, refuses an empty value before making a request, and echoes the normalized reference in
booking and cancellation results. It is not a SeatLayer Order id.
Booking History is the inventory ledger, not a commerce or fulfilment record. It contains labels, category/section/channel attribution, quantities, configured-value snapshots, and lifecycle events. It never contains buyer, payment, ticket, email, refund, or Door fields.
const page = await seatlayer.inventory.listBookings(eventKey, {
q: 'A-12',
state: 'booked',
limit: 50,
});
const detail = await seatlayer.inventory.retrieveBooking(
eventKey,
page.bookings[0].bookingRef,
);To release booked inventory, first update the commercial/refund state in your own system as your workflow requires, then cancel with the same reference that booked it:
await seatlayer.inventory.unbook(eventKey, {
labels: ['A-12'],
bookingRef: 'order-8842',
});SeatLayer releases inventory and records the lifecycle entry; it does not move or refund money, void a platform-owned ticket, send an email/PDF, or update a platform-owned scanner.
seatlayer.channels manages event allocations and mints short-lived buyer access for a browser.
A channel id is routing/reporting metadata, never authority. Authenticate the buyer in your own
backend, then mint an event- and origin-bound bearer:
const access = await seatlayer.channels.createBuyerAccessSession(eventKey, {
channelIds: ['chn_partner_a'],
includePublic: false,
allowedOrigin: 'https://tickets.marketplace.example',
buyerRef: 'buyer_318',
});
// Return only access.token + access.expiresAt to the browser. Never the secret key.Channel reports use bookedValue and includesBookedValue for configured-price snapshots. The
older revenue and includesRevenue response fields remain deprecated aliases for one
compatibility window. Managed SeatLayer hosted-link fulfilment is intentionally outside this
Platform SDK resource.
list() returns one page plus a nextCursor. When you want everything, listAll() pages for you
and yields as it goes — an async iterator rather than an array, because the point of paginating is
to not hold an unbounded list in memory.
// One page, your own paging.
const page = await seatlayer.events.list({ limit: 50 });
page.events; // EventMeta[]
page.nextCursor; // undefined once exhausted
// Or let the SDK walk it.
for await (const event of seatlayer.events.listAll()) {
await sync(event);
}Listing events includes live availability counts by default, which costs the server one
round-trip per event. listAll() turns them off automatically — walking a whole catalogue is
exactly when you don't want that — and you can control it explicitly:
await seatlayer.events.list({ limit: 50, counts: false });When an order takes longer than the checkout window — an invoice, a phone sale — extend rather than release and re-hold. Releasing first hands the seats to whoever is racing for them in between.
try {
await seatlayer.inventory.extendHold(eventKey, { holdId, ttlMs: 10 * 60_000 });
} catch (error) {
if (error instanceof SeatLayerConflictError) {
// Gone, expired, or at its renewal cap — the buyer has to re-pick.
}
}Your secret key never reaches a browser. Mint a scoped token instead and hand that to the widget.
const session = await seatlayer.sessions.createManageSession(eventKey, {
allowedOrigin: 'https://box-office.yourplatform.com',
capabilities: ['event:view', 'event:block'],
expiresInSeconds: 3600,
});capabilities is required by this SDK even though the API defaults it. That default grants all
four original inventory capabilities including event:cancel, which releases booked inventory —
not something that should arrive by forgetting an argument. Grant the smallest set the page needs.
The same pattern embeds the Designer in your own UI:
const { meta: chart } = await seatlayer.charts.create({ name: 'Riverside Theatre' });
const designer = await seatlayer.sessions.createDesignerSession({
workspaceId,
chartId: chart.id,
allowedOrigin: 'https://app.yourplatform.com',
authority: 'edit',
});Verify every delivery against the raw body. Re-serialising it (JSON.stringify(req.body))
changes the bytes and verification will fail.
import express from 'express';
import { verifyWebhook, WebhookVerificationError } from '@seatlayer/server';
app.post('/webhooks/seatlayer', express.raw({ type: 'application/json' }), (req, res) => {
try {
const event = verifyWebhook({
payload: req.body, // Buffer, not parsed JSON
signature: req.header('X-SeatLayer-Signature'),
secret: process.env.SEATLAYER_WEBHOOK_SECRET!,
});
// The signed body carries `at`, but nothing enforces a freshness window,
// so a captured delivery stays valid indefinitely. Deduplicate on
// occurrenceId — this is your replay protection, not an optimisation.
if (await alreadyProcessed(event.occurrenceId)) return res.sendStatus(200);
await handle(event);
res.sendStatus(200);
} catch (error) {
if (error instanceof WebhookVerificationError) return res.sendStatus(400);
throw error;
}
});import {
SeatLayerAuthError,
SeatLayerConflictError,
SeatLayerRateLimitError,
} from '@seatlayer/server';
try {
await seatlayer.inventory.holdBestAvailable(eventKey, { qty: 6 });
} catch (error) {
if (error instanceof SeatLayerConflictError && error.isSoldOut) {
return showAlternativeDates(); // a business outcome, not a bug
}
if (error instanceof SeatLayerRateLimitError) {
return retryAfter(error.retryAfterSeconds);
}
if (error instanceof SeatLayerAuthError && error.isModeMismatch) {
throw new Error('Test key pointed at a live event (or the reverse).');
}
throw error;
}Every error carries status, code, body, and requestId — quote the request id in support
requests.
Retries. 429, 408 and 5xx are retried with exponential backoff and full jitter; Retry-After
wins when the server sends it. 4xx responses are never retried — they will not start succeeding.
Idempotency. Every mutating request carries an Idempotency-Key, generated if you do not supply
one, and reused across retries so a retried booking cannot become two bookings. Pass your own
order id when you want end-to-end deduplication:
await seatlayer.inventory.book(
eventKey,
{ holdId, bookingRef: orderId },
{ idempotencyKey: `order-${orderId}` },
);new SeatLayer({
secretKey: process.env.SEATLAYER_SECRET_KEY!,
maxRetries: 3, // total attempts
timeoutMs: 30_000, // per attempt
});For surface this SDK does not wrap yet — same auth, retries, idempotency and error mapping:
await seatlayer.request('POST', '/v1/events/ev_1/some-new-route', { body: { … } });| Resource | Methods |
|---|---|
charts |
list listAll create retrieve update delete copy archive unarchive publish |
events |
list listAll create retrieve update delete updateChart close reopen archive retrieveHoldTtl updateHoldTtl retrieveReport retrieveLog |
inventory |
hold holdBestAvailable bookBestAvailable extendHold retrieveHold release book unbook listBookings retrieveBooking block unblock unblockAll retrieveAvailability updateAvailability |
channels |
listChannels createChannel updateChannel updateChannelAssignments listChannelAllocation retrieveChannelAccessPreview retrieveChannelReport pauseChannel unpauseChannel archiveChannel createBuyerAccessSession listBuyerAccessSessions revokeBuyerAccessSession |
sessions |
createManageSession revokeManageSession createDesignerSession revokeDesignerSession |
webhooks |
list create update delete listDeliveries |
workspaces |
list create retrieve update |
Full reference: docs.seatlayer.io/server-api
- Server SDK guide
- Errors, retries and idempotency
- Webhook verification
- Server API reference
- OpenAPI description
- Agent-readable documentation
- SeatLayer GitHub organization
| Surface | Package |
|---|---|
| Browser (vanilla) | @seatlayer/js |
| React | @seatlayer/react |
| React Native | @seatlayer/react-native |
| iOS | seatlayer-ios |
| Android | seatlayer-android |
| Flutter | seatlayer_flutter |
| Python (server) | seatlayer |
| PHP (server) | seatlayer/seatlayer-php |
| Java (server) | io.seatlayer:seatlayer-java |
| Go (server) | github.com/seatlayer/seatlayer-go |
| Ruby (server) | seatlayer |
| .NET (server) | SeatLayer |
pnpm install
pnpm validate # typecheck, tests, build, publint + attwMIT