@seatlayer/server 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +277 -0
- package/dist/index.cjs +693 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +637 -0
- package/dist/index.d.ts +637 -0
- package/dist/index.js +657 -0
- package/dist/index.js.map +1 -0
- package/package.json +66 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 SeatLayer
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
# SeatLayer Node SDK
|
|
2
|
+
|
|
3
|
+
Official Node.js server SDK for the [SeatLayer](https://seatlayer.io) reserved-seating API.
|
|
4
|
+
|
|
5
|
+
> **Server-side only.** This package authenticates with your secret key. Never bundle it into a
|
|
6
|
+
> browser, a mobile app, or anything a ticket buyer can open. Browser surfaces get short-lived,
|
|
7
|
+
> origin-bound tokens that you mint here — see [Embedding the control room](#embedding-the-control-room).
|
|
8
|
+
|
|
9
|
+
## Install
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
npm install @seatlayer/server
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Requires Node 20.19.4 or newer. No runtime dependencies.
|
|
16
|
+
|
|
17
|
+
## Quick start
|
|
18
|
+
|
|
19
|
+
```ts
|
|
20
|
+
import { SeatLayer } from '@seatlayer/server';
|
|
21
|
+
|
|
22
|
+
const seatlayer = new SeatLayer(process.env.SEATLAYER_SECRET_KEY!);
|
|
23
|
+
|
|
24
|
+
// 1. Provision a venue for a new organiser from one of your templates.
|
|
25
|
+
const { meta: chart } = await seatlayer.charts.copy('c_template_arena');
|
|
26
|
+
await seatlayer.charts.publish(chart.id);
|
|
27
|
+
|
|
28
|
+
// 2. Create an event on it.
|
|
29
|
+
const { meta: event } = await seatlayer.events.create({
|
|
30
|
+
chartId: chart.id,
|
|
31
|
+
name: 'Spring Gala',
|
|
32
|
+
startsAt: Date.parse('2026-09-12T19:30:00Z'),
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
// 3. Sell four seats over the phone.
|
|
36
|
+
const held = await seatlayer.inventory.holdBestAvailable(event.key, { qty: 4 });
|
|
37
|
+
// … take payment against held.items, which carry authoritative prices …
|
|
38
|
+
await seatlayer.inventory.book(event.key, { holdId: held.holdId, bookingRef: 'order-8842' });
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Test vs live
|
|
42
|
+
|
|
43
|
+
Keys carry their own mode. `sk_test_…` keys can only touch test-mode events, and `sk_live_…` keys
|
|
44
|
+
only live ones; crossing them returns `403 mode_mismatch`, surfaced as
|
|
45
|
+
`SeatLayerAuthError` with `isModeMismatch === true`.
|
|
46
|
+
|
|
47
|
+
```ts
|
|
48
|
+
const seatlayer = new SeatLayer(process.env.SEATLAYER_SECRET_KEY!);
|
|
49
|
+
if (process.env.NODE_ENV === 'production' && seatlayer.mode !== 'live') {
|
|
50
|
+
throw new Error('Refusing to boot production against test-mode seating data.');
|
|
51
|
+
}
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## The two selling flows
|
|
55
|
+
|
|
56
|
+
**Buyer picks seats in the browser.** Your frontend holds them; your backend confirms the price and
|
|
57
|
+
books. Never price from what the browser sent you — `retrieveHold` is the authoritative answer.
|
|
58
|
+
|
|
59
|
+
```ts
|
|
60
|
+
const hold = await seatlayer.inventory.retrieveHold(eventKey, holdId);
|
|
61
|
+
const total = hold.items.reduce((sum, item) => sum + item.unitPrice, 0);
|
|
62
|
+
// … charge `total` in hold.currency …
|
|
63
|
+
await seatlayer.inventory.book(eventKey, { holdId, bookingRef: charge.id });
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
**Your backend picks the seats.** Phone orders, box office, comps. No browser involved.
|
|
67
|
+
|
|
68
|
+
```ts
|
|
69
|
+
// Payment already taken — book outright, so nothing is stranded if a second call fails.
|
|
70
|
+
await seatlayer.inventory.bookBestAvailable(eventKey, { qty: 2, bookingRef: 'phone-1183' });
|
|
71
|
+
|
|
72
|
+
// Or name the seats yourself.
|
|
73
|
+
await seatlayer.inventory.boxOfficeBook(eventKey, { labels: ['A-1', 'A-2'], bookingRef: 'comp-14' });
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
## Listing and pagination
|
|
77
|
+
|
|
78
|
+
`list()` returns one page plus a `nextCursor`. When you want everything, `listAll()` pages for you
|
|
79
|
+
and yields as it goes — an async iterator rather than an array, because the point of paginating is
|
|
80
|
+
to *not* hold an unbounded list in memory.
|
|
81
|
+
|
|
82
|
+
```ts
|
|
83
|
+
// One page, your own paging.
|
|
84
|
+
const page = await seatlayer.events.list({ limit: 50 });
|
|
85
|
+
page.events; // EventMeta[]
|
|
86
|
+
page.nextCursor; // undefined once exhausted
|
|
87
|
+
|
|
88
|
+
// Or let the SDK walk it.
|
|
89
|
+
for await (const event of seatlayer.events.listAll()) {
|
|
90
|
+
await sync(event);
|
|
91
|
+
}
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
Listing events includes live availability `counts` by default, which costs the server one
|
|
95
|
+
round-trip **per event**. `listAll()` turns them off automatically — walking a whole catalogue is
|
|
96
|
+
exactly when you don't want that — and you can control it explicitly:
|
|
97
|
+
|
|
98
|
+
```ts
|
|
99
|
+
await seatlayer.events.list({ limit: 50, counts: false });
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
## Keeping a hold alive
|
|
103
|
+
|
|
104
|
+
When an order takes longer than the checkout window — an invoice, a phone sale — extend rather than
|
|
105
|
+
release and re-hold. Releasing first hands the seats to whoever is racing for them in between.
|
|
106
|
+
|
|
107
|
+
```ts
|
|
108
|
+
try {
|
|
109
|
+
await seatlayer.inventory.extendHold(eventKey, { holdId, ttlMs: 10 * 60_000 });
|
|
110
|
+
} catch (error) {
|
|
111
|
+
if (error instanceof SeatLayerConflictError) {
|
|
112
|
+
// Gone, expired, or at its renewal cap — the buyer has to re-pick.
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
## Embedding the control room
|
|
118
|
+
|
|
119
|
+
Your secret key never reaches a browser. Mint a scoped token instead and hand that to the widget.
|
|
120
|
+
|
|
121
|
+
```ts
|
|
122
|
+
const session = await seatlayer.sessions.createManageSession(eventKey, {
|
|
123
|
+
allowedOrigin: 'https://box-office.yourplatform.com',
|
|
124
|
+
capabilities: ['event:view', 'event:block'],
|
|
125
|
+
expiresInSeconds: 3600,
|
|
126
|
+
});
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
`capabilities` is **required** by this SDK even though the API defaults it. That default grants all
|
|
130
|
+
four capabilities including `event:cancel`, which reverses paid bookings — not something that should
|
|
131
|
+
arrive by forgetting an argument. Grant the smallest set the page needs.
|
|
132
|
+
|
|
133
|
+
The same pattern embeds the Designer in your own UI:
|
|
134
|
+
|
|
135
|
+
```ts
|
|
136
|
+
const { meta: chart } = await seatlayer.charts.create({ name: 'Riverside Theatre' });
|
|
137
|
+
const designer = await seatlayer.sessions.createDesignerSession({
|
|
138
|
+
workspaceId,
|
|
139
|
+
chartId: chart.id,
|
|
140
|
+
allowedOrigin: 'https://app.yourplatform.com',
|
|
141
|
+
authority: 'edit',
|
|
142
|
+
});
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
## Webhooks
|
|
146
|
+
|
|
147
|
+
Verify every delivery against the **raw** body. Re-serialising it (`JSON.stringify(req.body)`)
|
|
148
|
+
changes the bytes and verification will fail.
|
|
149
|
+
|
|
150
|
+
```ts
|
|
151
|
+
import express from 'express';
|
|
152
|
+
import { verifyWebhook, WebhookVerificationError } from '@seatlayer/server';
|
|
153
|
+
|
|
154
|
+
app.post('/webhooks/seatlayer', express.raw({ type: 'application/json' }), (req, res) => {
|
|
155
|
+
try {
|
|
156
|
+
const event = verifyWebhook({
|
|
157
|
+
payload: req.body, // Buffer, not parsed JSON
|
|
158
|
+
signature: req.header('X-SeatLayer-Signature'),
|
|
159
|
+
secret: process.env.SEATLAYER_WEBHOOK_SECRET!,
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
// The signed body carries `at`, but nothing enforces a freshness window,
|
|
163
|
+
// so a captured delivery stays valid indefinitely. Deduplicate on
|
|
164
|
+
// occurrenceId — this is your replay protection, not an optimisation.
|
|
165
|
+
if (await alreadyProcessed(event.occurrenceId)) return res.sendStatus(200);
|
|
166
|
+
|
|
167
|
+
await handle(event);
|
|
168
|
+
res.sendStatus(200);
|
|
169
|
+
} catch (error) {
|
|
170
|
+
if (error instanceof WebhookVerificationError) return res.sendStatus(400);
|
|
171
|
+
throw error;
|
|
172
|
+
}
|
|
173
|
+
});
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
## Errors
|
|
177
|
+
|
|
178
|
+
```ts
|
|
179
|
+
import {
|
|
180
|
+
SeatLayerAuthError,
|
|
181
|
+
SeatLayerConflictError,
|
|
182
|
+
SeatLayerRateLimitError,
|
|
183
|
+
} from '@seatlayer/server';
|
|
184
|
+
|
|
185
|
+
try {
|
|
186
|
+
await seatlayer.inventory.holdBestAvailable(eventKey, { qty: 6 });
|
|
187
|
+
} catch (error) {
|
|
188
|
+
if (error instanceof SeatLayerConflictError && error.isSoldOut) {
|
|
189
|
+
return showAlternativeDates(); // a business outcome, not a bug
|
|
190
|
+
}
|
|
191
|
+
if (error instanceof SeatLayerRateLimitError) {
|
|
192
|
+
return retryAfter(error.retryAfterSeconds);
|
|
193
|
+
}
|
|
194
|
+
if (error instanceof SeatLayerAuthError && error.isModeMismatch) {
|
|
195
|
+
throw new Error('Test key pointed at a live event (or the reverse).');
|
|
196
|
+
}
|
|
197
|
+
throw error;
|
|
198
|
+
}
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
Every error carries `status`, `code`, `body`, and `requestId` — quote the request id in support
|
|
202
|
+
requests.
|
|
203
|
+
|
|
204
|
+
## Reliability
|
|
205
|
+
|
|
206
|
+
**Retries.** 429, 408 and 5xx are retried with exponential backoff and full jitter; `Retry-After`
|
|
207
|
+
wins when the server sends it. 4xx responses are never retried — they will not start succeeding.
|
|
208
|
+
|
|
209
|
+
**Idempotency.** Every mutating request carries an `Idempotency-Key`, generated if you do not supply
|
|
210
|
+
one, and **reused across retries** so a retried booking cannot become two bookings. Pass your own
|
|
211
|
+
order id when you want end-to-end deduplication:
|
|
212
|
+
|
|
213
|
+
```ts
|
|
214
|
+
await seatlayer.inventory.book(eventKey, { holdId }, { idempotencyKey: `order-${orderId}` });
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
```ts
|
|
218
|
+
new SeatLayer({
|
|
219
|
+
secretKey: process.env.SEATLAYER_SECRET_KEY!,
|
|
220
|
+
maxRetries: 3, // total attempts
|
|
221
|
+
timeoutMs: 30_000, // per attempt
|
|
222
|
+
});
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
## Escape hatch
|
|
226
|
+
|
|
227
|
+
For surface this SDK does not wrap yet — same auth, retries, idempotency and error mapping:
|
|
228
|
+
|
|
229
|
+
```ts
|
|
230
|
+
await seatlayer.request('POST', '/v1/events/ev_1/some-new-route', { body: { … } });
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
## API surface
|
|
234
|
+
|
|
235
|
+
| Resource | Methods |
|
|
236
|
+
| --- | --- |
|
|
237
|
+
| `charts` | `list` `listAll` `create` `retrieve` `update` `delete` `copy` `archive` `unarchive` `publish` |
|
|
238
|
+
| `events` | `list` `listAll` `create` `retrieve` `update` `delete` `updateChart` `close` `reopen` `archive` `retrieveHoldTtl` `updateHoldTtl` `retrieveReport` `retrieveLog` |
|
|
239
|
+
| `inventory` | `hold` `holdBestAvailable` `bookBestAvailable` `extendHold` `retrieveHold` `release` `book` `boxOfficeBook` `unbook` `block` `unblock` `unblockAll` `retrieveAvailability` `updateAvailability` |
|
|
240
|
+
| `sessions` | `createManageSession` `revokeManageSession` `createDesignerSession` `revokeDesignerSession` |
|
|
241
|
+
| `webhooks` | `list` `create` `update` `delete` `listDeliveries` |
|
|
242
|
+
| `workspaces` | `list` `create` `retrieve` `update` |
|
|
243
|
+
|
|
244
|
+
Full reference: [docs.seatlayer.io/server-api](https://docs.seatlayer.io/server-api/)
|
|
245
|
+
|
|
246
|
+
## Related resources
|
|
247
|
+
|
|
248
|
+
- [Server SDK guide](https://docs.seatlayer.io/server-sdk/install/)
|
|
249
|
+
- [Errors, retries and idempotency](https://docs.seatlayer.io/server-sdk/reliability/)
|
|
250
|
+
- [Webhook verification](https://docs.seatlayer.io/server-sdk/webhooks/)
|
|
251
|
+
- [Server API reference](https://docs.seatlayer.io/server-api/events/)
|
|
252
|
+
- [OpenAPI description](https://docs.seatlayer.io/openapi.json)
|
|
253
|
+
- [Agent-readable documentation](https://docs.seatlayer.io/llms.txt)
|
|
254
|
+
- [SeatLayer GitHub organization](https://github.com/seatlayer)
|
|
255
|
+
|
|
256
|
+
### Other SeatLayer SDKs
|
|
257
|
+
|
|
258
|
+
| Surface | Package |
|
|
259
|
+
|---|---|
|
|
260
|
+
| Browser (vanilla) | [`@seatlayer/js`](https://github.com/seatlayer/seatlayer-sdk) |
|
|
261
|
+
| React | [`@seatlayer/react`](https://github.com/seatlayer/seatlayer-sdk) |
|
|
262
|
+
| React Native | [`@seatlayer/react-native`](https://github.com/seatlayer/seatlayer-react-native) |
|
|
263
|
+
| iOS | [`seatlayer-ios`](https://github.com/seatlayer/seatlayer-ios) |
|
|
264
|
+
| Android | [`seatlayer-android`](https://github.com/seatlayer/seatlayer-android) |
|
|
265
|
+
| Flutter | [`seatlayer_flutter`](https://github.com/seatlayer/seatlayer-flutter) |
|
|
266
|
+
| Python (server) | [`seatlayer`](https://github.com/seatlayer/seatlayer-python) |
|
|
267
|
+
|
|
268
|
+
## Development
|
|
269
|
+
|
|
270
|
+
```bash
|
|
271
|
+
pnpm install
|
|
272
|
+
pnpm validate # typecheck, tests, build, publint + attw
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
## License
|
|
276
|
+
|
|
277
|
+
MIT
|