seatlayer 0.8.0 → 0.9.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 841def7560b63781babf931924a29204c724ba5b4513eff1de283a370cb22699
4
- data.tar.gz: 7027c38a8aeeee9aa5c339d90d04b33c4d708e47362687951d30dc280402bef4
3
+ metadata.gz: 2e7f487c6f37918712d5b9ad4967aefb979b5bbd85368cd1c669b3718837c19d
4
+ data.tar.gz: 1a350dcf149a427a410faf436a39ab8b4ada55a8a262956513316b7531a52b65
5
5
  SHA512:
6
- metadata.gz: 2ef313b0db74c36aef5d0f512264586448d99330e1257c8a28b835da54a7b539fd3f9ab7183a91ae912cf1e55ec2c5dcede861260d80b77a4ed0eaaa6d1faaba
7
- data.tar.gz: e85e01d772d6639a36d7fa7154a062d60564fbbe750258c1080fa4f7cc17eba25277ebc0e7e31ad0f04589b9d716402c486c6c325b3d202e7c7fd2ed9e96c903
6
+ metadata.gz: a1c8d7522480ed8f26c1858ad590fed53146a2d672fa698d4ac07009978f6cacb73ce63a9d8912ecbbae50439344f7b8117e25d72e307cf323f22d33fcaeafab
7
+ data.tar.gz: c909fa65b7a98c2e6d03ba1b69a79ee4074a33d4041647fbced2d15d923b40b7385ccabe8ff270fb5c4e90b8664ee0f89bf9adcb8ded0388489f9f1ef81c00ed
data/CHANGELOG.md CHANGED
@@ -1,5 +1,114 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.9.0 (2026-11-11)
4
+
5
+ - Season seats: `set_aside_season_block` (`POST /v1/seasons/:key/block`), `retrieve_season_block`
6
+ and `release_season_block` (`POST /v1/seasons/:key/block/release`). Each write answers "job"
7
+ and "block"; follow the job with `retrieve_season_bulk_job` (kinds `season_block_set_aside` and
8
+ `season_block_release`, read by id, not listed). Both writes are retried safely with one
9
+ Idempotency-Key, generated when none is given.
10
+ - `inventory_mode:` on season create and update (opt a season into season seats), read back on
11
+ every season (`"inventoryMode"`).
12
+ - Webhook names `season.block.ready` and `season.block.released` (`Webhooks::EVENT_NAMES`, now 58).
13
+ - A season-seats season's date cancel (`create_season_amendment`) or past date
14
+ (`mark_season_performance_past`) can answer `202` while the date leaves every season seat:
15
+ `Seasons.date_leave_pending?` recognises it.
16
+ - New refusals come through `SeatLayer::APIError` and its subclasses with `code`:
17
+ `season_block_not_enabled`, `block_exists`, `block_not_ready`, `season_not_draft_or_active`,
18
+ `block_selection_empty`, `seat_not_in_block`, `season_block_set_aside`, `season_inventory_locked`,
19
+ `date_leave_in_progress`.
20
+ `inventory_mode:` can also answer `season_block_packages` (more than one package),
21
+ `season_block_on_sale`, `season_block_exists` and `invalid_inventory_mode`. The set-aside can
22
+ also answer `season_block_packages`, `season_block_one_package`, `plan_not_published`,
23
+ `season_block_undoing`, `chart_unavailable` and `dates_missing`, and the release
24
+ `release_mismatch`. On `503 season_busy` or `season_not_configured`, send the same request again.
25
+
26
+ - A date its season cancelled never opens again: `transition_event_sales` to `testable` or
27
+ `live` and `reopen` answer `409 season_date_cancelled`, and `update_event_series` with
28
+ `sales: "on"` leaves it closed as a failed item of the job (`season_date_cancelled`; not in
29
+ `failedDates`).
30
+ - `create_season_amendment` refuses to move (`reschedule`), postpone or replace a cancelled date,
31
+ and `mark_season_performance_past` refuses to mark one as taken place:
32
+ `409 performance_cancelled`.
33
+ - `update_event_series` sends only the prices that change: each date keeps its own price for
34
+ every category (or tier) left out, so weekend and weekday prices keep their difference.
35
+ - A season's `transferCutoffHours` / `returnCutoffHours` of `nil` means the club default
36
+ (Settings › Ticket site), or until the date starts when the club has none.
37
+ - `create_holder_transfer` and `create_holder_return` are not stopped by the club's customer
38
+ switches in Settings › Ticket site; cut-offs still apply.
39
+ - Cancelling a date (`create_season_amendment` with `kind: "cancel"`) voids its tickets as a
40
+ `date_cancel_void` job, named by `SeatLayer::Seasons.date_cancel_void_job_id(amendment_id)`
41
+ (`sbj_void_…`); follow it with `retrieve_season_bulk_job`. `season.ticket.voided` says
42
+ `date_cancelled` for each Season ticket it voids.
43
+ - A Season package holds 2 to 50 dates. `SeatLayer::Seasons.holder_import_rows_per_call(dates)`
44
+ gives the most rows one `create_season_holder_import` call takes (100 up to 20 dates, 43 at
45
+ 50); a call with more raises `ValidationError` `invalid_rows` with `"maxRows"` in its `body`.
46
+ - `update_season`'s `singleTicketDates` may carry `jobRefused`, why a `season_singles` job could
47
+ not start; the change stands, and sending `single_tickets: true` again opens any date still
48
+ not open. An `update_season` retried with its first `idempotency_key:` and
49
+ `expected_revision:` is not applied twice.
50
+ - An `event_series.create_event_series` given up for its key raises `ConflictError`
51
+ `create_abandoned` with a message to send it again with a new idempotency key.
52
+ - Added the 39 remaining Fixed Renewable Season operations to `client.seasons` (87 in all):
53
+ performances, package edits and prices, publishing, checkout tests, occurrence returns and
54
+ reclaims, holders, Season order refunds and cancellations, hosted renewals, renewal quotes,
55
+ intents and checkouts, the sales,
56
+ seats and seat-count reports, and bulk refunds and lapsed-offer releases with their jobs.
57
+ - Added three resources: `client.holders` (holders, customers, holder tickets and ticket
58
+ transfers), `client.orders` (account and event orders, refunds, cancellations and ticket
59
+ resends) and `client.event_series` (plan, read, extend and update an event series).
60
+ - Added `holders.create_holder_return`: give back a holder's seat on one date, one per ticket.
61
+ - Added the event sales settings to `client.events`: buyer checkout, payment gateway, sales
62
+ readiness, preflight and transition, booking fee, and discount codes.
63
+ - `export_season_holders_csv`, `export_season_orders_csv` and `orders.export_event_orders_csv`
64
+ return the raw CSV text. `delete_season_plan` and `events.delete_event_discount_code` return
65
+ an empty Hash (the API answers 204).
66
+ - 27 of the new operations use exact header replay (41 in the SDK now) and retry with one
67
+ `Idempotency-Key`; every other new mutation is single-attempt.
68
+ - `Webhooks::EVENT_NAMES` adds `series.created`, `series.dates_added`, `order.paid`,
69
+ `order.refunded`, `order.cancelled`, `customer.created`, `customer.email_changed`,
70
+ `season.ticket.transfer_sent`, `season.ticket.transferred` and
71
+ `season.ticket.transfer_cancelled`.
72
+ - Added `seasons.refund_season_performance` (refund every order of a cancelled date as a job,
73
+ with header replay), `seasons.list_season_bulk_jobs`, and `seasons.retrieve_season_bulk_job`.
74
+ - `create_season_amendment` takes `refund:` and `reason:`, for the new `postpone` and `cancel`
75
+ kinds.
76
+ - `update_season` takes `checkout_mode:`, `transfer_cutoff_hours:`, and `return_cutoff_hours:`
77
+ (`nil` sends an explicit null).
78
+ - `replace_season_package_prices` documents `"priceTypeKey"` on each price, and
79
+ `create_season_holder_import` documents `"holderEmail"` and `"holderName"` on each row.
80
+ - `Webhooks::EVENT_NAMES` lists all 56 public event names, adding `object.resale.*`,
81
+ `season.occurrence.*`, `season.performance.*` (including `postponed`, `cancelled`, and
82
+ `rescheduled`), `season.bulk_action.completed`, `season.order.*`, `season.ticket.issued`,
83
+ `season.ticket.voided`, and the Season lifecycle, hold, booking, renewal, operation and
84
+ amendment events.
85
+ - `events.list` and `events.list_all` take `series_key:` to list one event series' dates.
86
+ - Added `seasons.retrieve_season_renewal_quote`: what renewing an offer costs, and whether SeatLayer checkout can take it now.
87
+ - Added `seasons.create_season_renewal_intent`: record a holder's intent for a renewal taken by phone or at the box office, so your own checkout can commit.
88
+ - Added `seasons.create_season_renewal_checkout`: a SeatLayer checkout that renews an offer with no hosted page, with header replay.
89
+ - Added `holders.create_holder_renewal_checkout`: the same checkout for one of a holder's offers, with header replay.
90
+ - Added `holders.decline_holder_renewal_offer`: decline one of a holder's offers, with an optional `reason:`.
91
+ - `seasons.decline_season_renewal_offer` takes an optional `reason:`, kept on the offer as `declineReason`.
92
+ - `update_season` takes `audience:` (`"embed_only"` or `"public"`).
93
+ - `open_season_sales` answers `warnings` (`checkout_test_missing` until a checkout test or rehearsal has passed) and can refuse with `409 plan_not_published` or `409 checkout_mode_not_chosen`.
94
+ - A cancelled date refunds per date only: an amendment's `refund:` is `"none"` or `"per_date"`, and `refund_season_performance` takes `amount: "per_date"` (`full` answers `422 date_refund_per_date_only`; refund a whole order with the order refund).
95
+ - `add_event_series_dates` takes `retry_failed: true` to make the failed dates again (`409 series_busy`, `422 nothing_to_retry`).
96
+ - `list_orders` takes `holder_ref:`; orders carry `holderRef` and `customerId`, and the buyer's email can be nil (a box office sale with no email).
97
+ - Season holds, give-backs and date cancels can refuse with `performance_cancelled`, `refund_not_started` (503), `missing_manage_capability` (403) or `invalid_refund`; a holder's status can be `reserved`; hosted renewal readiness carries `missingPrices`.
98
+ - `create_event_series` takes `from_event_key:`: an event you already set up becomes the first date under its own key, and every other date is a copy of it. `name:` and `chart_key:` are then left out. It refuses with `404 event_not_found`, or `422 event_has_no_date`, `event_in_season`, `event_in_performance_group`, `event_in_series` or `invalid_request`.
99
+ - Every date of an event series carries the series' plain name (`Hamlet`, not `Hamlet · 3 Nov`); tell dates apart by `startsAt`. `update_event_series` renames each date that carries the series name, and a series name cannot hold `{n}`, `{date}` or `{time}`.
100
+ - Added `seasons.release_season_lapsed_offers`: release the seats of every lapsed renewal offer as a job (`202 {"job"}`), with header replay. An offer extended before its turn is skipped and keeps its seat. It can refuse with `503 job_busy` or `season_not_configured`; send it again.
101
+ - `retrieve_season_hosted_renewals` and `list_season_renewal_offers` take `cursor:` and `limit:` (1 to 500 and 1 to 100) and answer one page, newest first, with `nextCursor` (nil on the last page); a bad cursor or limit is refused with `400 invalid_cursor` or `invalid_limit`.
102
+ - `create_season_renewal_offers` answers `201 {"offers"}` for 100 contracts or fewer, and `202 {"job", "batchId"}` above that: a `renewal_run` job makes the offers. Follow it with `retrieve_season_bulk_job`, then page the offers with `list_season_renewal_offers`.
103
+ - `create_season_holder_import_from_season` answers `202 {"job"}` when a commit brings across more than 100 holders: a `holder_import` job, whose `summary` has the same counts once it completes. It and `create_season_renewal_offers` can refuse with `409 job_mismatch` or `503 season_not_configured`, and the bring-across also with `503 job_busy`.
104
+ - `create_season_renewal_notices` takes up to 20,000 `offer_ids:`; more is refused with `422 too_many_offer_ids`.
105
+ - `create_season_renewal_notices` takes `batch_id:` instead of `offer_ids:`: email every open offer one `create_season_renewal_offers` batch made, such as the batch a `renewal_run` job made.
106
+ - Single tickets on a season: `create_season` and `update_season` take `single_tickets:` (default `false`, Season only; anything else is refused with `422 invalid_single_tickets`). Every season read carries `singleTickets`, and a season on sale may carry `singleTicketsNotOpen`. `update_season`, `open_season_sales` and `resume_season_sales` answer `singleTicketDates` (`direction`, `opened`, `closed`, `notOpened`, `jobId`) when they open or stop the dates' own sales; a `season_singles` job does it for more than 12 upcoming dates.
107
+
108
+ ## 0.8.0 (2026-09-10)
109
+
110
+ - Added Event hosting regions: `region:` on event create, `default_region:` on workspace create and update, and `EVENT_HOSTING_REGIONS` to check a value before it is sent. Events created without a region run in Western Europe.
111
+
3
112
  ## 0.7.0 — 2026-08-30
4
113
 
5
114
  - Added coverage for all 48 Fixed Renewable Season server
data/README.md CHANGED
@@ -5,6 +5,8 @@
5
5
  [![Ruby](https://img.shields.io/badge/Ruby-%E2%89%A53.0-CC342D.svg)](https://www.ruby-lang.org/)
6
6
  [![License: MIT](https://img.shields.io/badge/license-MIT-111827.svg)](LICENSE)
7
7
 
8
+ The official Ruby client for the SeatLayer API. The `seatlayer` gem lets a Ruby or Rails backend inspect seat holds, price orders from server data, book reserved seats and verify webhooks, with no runtime dependencies. SeatLayer is seating chart and reserved-seat ticketing software built for venues up to stadium scale.
9
+
8
10
  SeatLayer's official Ruby server SDK is the trusted side of its reserved seating and seat booking
9
11
  API. Inspect what a hold really contains, price from server-owned seating-chart data, and book
10
12
  with a stable `booking_ref`, while managing charts, events, inventory, allocations, and webhooks
@@ -17,7 +19,15 @@ through one typed ticketing API client.
17
19
  [Server API reference](https://docs.seatlayer.io/server-api/events/)
18
20
 
19
21
  > **Server-side only.** This gem authenticates with your secret key. Never load it anywhere a
20
- > ticket buyer can reach — browser surfaces get short-lived, origin-bound tokens that you mint here.
22
+ > ticket buyer can reach. Browser surfaces get short-lived, origin-bound tokens that you mint here.
23
+
24
+ Two-step shape: the buyer picks and holds seats in the client with your public key, then this SDK confirms the booking from your server with your secret key while your platform keeps checkout and its own payment provider.
25
+
26
+ **Start here:** [Quickstart](https://docs.seatlayer.io/start/quickstart/) · [Holds and checkout](https://docs.seatlayer.io/buyer-sdk/holds-and-checkout/) · [Ruby server SDK guide](https://docs.seatlayer.io/server-sdk/ruby/) · [SDK catalog](https://docs.seatlayer.io/sdk-catalog.json) · [Pricing](https://seatlayer.io/pricing/): $0 entry, 100 free confirmed-sold-seat credits per organization each month, then $0.10 down to $0.05 a credit, and credits never expire.
27
+
28
+ ## Scale evidence
29
+
30
+ Benchmarked on public 100,000-, 150,000- and 200,000-seat venue fixtures on 15 September 2026: 200,000 seats chart-ready in 1.95 s, desktop, local production build. Fixtures, method and run logs: https://github.com/seatlayer/seatlayer-performance. Live 200,000-seat stadium demo: https://app.seatlayer.io/demo/play/century-stadium-200k. This is renderer evidence, not a concurrent-buyer claim.
21
31
 
22
32
  ## Install the Ruby seat booking SDK
23
33
 
@@ -29,7 +39,7 @@ gem "seatlayer"
29
39
  gem install seatlayer
30
40
  ```
31
41
 
32
- Requires Ruby 3.0 or newer. **No runtime dependencies** — `net/http`, `json` and `openssl` from the
42
+ Requires Ruby 3.0 or newer. **No runtime dependencies**: `net/http`, `json` and `openssl` from the
33
43
  standard library.
34
44
 
35
45
  ## Quick start
@@ -76,14 +86,28 @@ Nullable event-create fields distinguish omission from an explicit reset: passin
76
86
 
77
87
  ## Fixed Renewable Seasons
78
88
 
79
- Version `0.7.0` exposes all 48 trusted organizer operations through
80
- `client.seasons`.
89
+ `client.seasons` exposes all 87 trusted organizer operations: catalogue and
90
+ package lifecycle, performances, prices (each price hash may name a
91
+ `"priceTypeKey"`: `adult`, `child`, `concession`, `senior` or `student`),
92
+ checkout tests, sales windows, buyer access and booking, holder imports,
93
+ renewals with their quotes, intents and checkouts, occurrences, Season orders, reports and CSV exports, and the bulk
94
+ refund of a cancelled date (`refund_season_performance`) and the release of
95
+ lapsed renewal offers (`release_season_lapsed_offers`) with their jobs
96
+ (`list_season_bulk_jobs`, `retrieve_season_bulk_job`).
97
+
98
+ Above 100 contracts, `create_season_renewal_offers` answers `202` with
99
+ `{"job", "batchId"}` instead of the offers: follow the `renewal_run` job with
100
+ `retrieve_season_bulk_job`, then page the offers with `list_season_renewal_offers`
101
+ (`cursor:` and `limit:`, until `nextCursor` is nil). A bring-across of more than 100
102
+ holders (`create_season_holder_import_from_season`) answers `202` with a
103
+ `holder_import` job the same way.
81
104
 
82
105
  After the test hold/book/cancel journey and matching webhook deliveries,
83
106
  `validate_season_buyer_rehearsal(season_key)` sends no evidence body; SeatLayer
84
107
  discovers the retained chain automatically. Retrieved Season holds contain
85
- inventory identity, not an authoritative amount—your platform owns package
86
- price, payment, order, tax, refunds, benefits, and ticket or pass delivery.
108
+ inventory identity, not an authoritative amount. When you sell a Season through
109
+ your own checkout, your platform owns package price, payment, order, tax, refunds,
110
+ benefits, and ticket or pass delivery.
87
111
 
88
112
  ```ruby
89
113
  checked = client.seasons.validate_season(
@@ -101,6 +125,92 @@ returned operation identity. Buyer-session minting and domain-exact booking,
101
125
  cancellation, and renewal actions remain single-attempt; only declared
102
126
  header-replay catalogue mutations retry automatically.
103
127
 
128
+ When SeatLayer takes the Season's payments, renew a holder with no hosted page:
129
+ read the price, start a checkout, and send the holder to `payment.url`. For a
130
+ renewal taken by phone or at the box office, `create_season_renewal_intent`
131
+ records the holder's intent before you commit the offer. A decline can carry the
132
+ holder's reason.
133
+
134
+ ```ruby
135
+ quote = client.seasons.retrieve_season_renewal_quote(season_key, offer_id)
136
+ if quote["problem"].nil?
137
+ started = client.seasons.create_season_renewal_checkout(
138
+ season_key, offer_id,
139
+ return_url: "https://tickets.yourclub.example/renewed",
140
+ idempotency_key: "renew-#{offer_id}"
141
+ )
142
+ redirect_to started.dig("payment", "url")
143
+ end
144
+
145
+ client.holders.decline_holder_renewal_offer("holder_123", offer_id, reason: "Moving away")
146
+ ```
147
+
148
+ ### Season seats
149
+
150
+ Season seats are the seats a season sets aside on every date before its sales open. A season
151
+ buyer's hold then doesn't have to ask each date, and single-ticket buyers see those seats as
152
+ unavailable. They are off for every season until you pass `inventory_mode: "block"` to
153
+ `create_season` or `update_season`. The season needs one package, and its sales must not have
154
+ opened.
155
+
156
+ ```ruby
157
+ client.seasons.update_season(season_key, expected_revision: revision, inventory_mode: "block")
158
+
159
+ job = client.seasons.set_aside_season_block(
160
+ season_key,
161
+ expected_revision: revision,
162
+ keep_for_single_tickets: { "sectionIds" => ["sec_family"] }
163
+ ).fetch("job")
164
+ while %w[planning running].include?(job["state"])
165
+ sleep 2
166
+ job = client.seasons.retrieve_season_bulk_job(season_key, job["jobId"]).fetch("job")
167
+ end
168
+ block = client.seasons.retrieve_season_block(season_key).fetch("block")
169
+
170
+ # Release unsold season seats to single sale. Leave out at: to release now.
171
+ client.seasons.release_season_block(
172
+ season_key,
173
+ expected_revision: block["revision"],
174
+ scope: { "all" => true },
175
+ at: "2026-12-01T09:00:00Z"
176
+ )
177
+ ```
178
+
179
+ - On an active season, block mode turns on by itself once every date has the seats, and
180
+ `season.block.ready` is sent once. On a draft, `publish_season` turns it on.
181
+ `season.block.released` is sent once a release has run.
182
+ - In block mode, a date cancel (`create_season_amendment`, kind `"cancel"`) or
183
+ `mark_season_performance_past` first takes the date off every season seat. Until that finishes
184
+ the answer is a `202`: check it with `SeatLayer::Seasons.date_leave_pending?(answer)`, then send
185
+ the same request again after `Retry-After`.
186
+ - Refusals come through `SeatLayer::APIError` with `code`, and nothing changes. For example
187
+ `409 season_block_packages` (more than one package), `409 season_block_on_sale` (sales have
188
+ opened), or `409 block_not_ready` from `open_season_sales` before block mode is on.
189
+ - A partner's seat on a date (one a sales channel sells there) stays out of the block. It is
190
+ listed in `block["notFreeOnEveryDate"]` with reason `channel`.
191
+
192
+ The [Season seats docs](https://docs.seatlayer.io/server-api/seasons/#season-seats) list every refusal.
193
+
194
+ ## Orders, holders, event sales and event series
195
+
196
+ `client.orders` reads, exports, refunds and cancels the orders SeatLayer took through its own
197
+ checkout; `client.holders` covers Season holders, customers, their tickets and ticket transfers;
198
+ the event sales settings (who takes payment, gateway, booking fee, sales state, discount codes)
199
+ are on `client.events`; and `client.event_series` plans many dates of one show from a schedule.
200
+ The CSV exports return the raw CSV text, not a Hash.
201
+
202
+ ```ruby
203
+ page = client.orders.list_event_orders(event_key, status: "confirmed", limit: 50)
204
+ csv = client.orders.export_event_orders_csv(event_key, status: "confirmed") # a String
205
+ client.orders.refund_event_order(event_key, order_id, amount_minor: 1500)
206
+
207
+ client.events.transition_event_sales(event_key, to: "live", expected_revision: 4)
208
+ client.event_series.create_event_series(
209
+ name: "Hamlet", chart_key: chart_key, dry_run: true,
210
+ schedule: { "rule" => "weekly", "from" => "2027-01-04", "count" => 6, "times" => ["19:30"] }
211
+ )
212
+ ```
213
+
104
214
  ## Test vs live
105
215
 
106
216
  Keys carry their own mode. `sk_test_…` keys can only touch test-mode events and `sk_live_…` only
@@ -118,7 +228,7 @@ failing as a `401` three round-trips later.
118
228
  ## Book reserved seats from Ruby
119
229
 
120
230
  **Buyer picks seats in the browser.** Your frontend holds them; your backend confirms the price and
121
- books. Never price from what the browser sent you — `retrieve_hold` is authoritative.
231
+ books. Never price from what the browser sent you: `retrieve_hold` is authoritative.
122
232
 
123
233
  ```ruby
124
234
  hold = client.inventory.retrieve_hold(event_key, hold_id)
@@ -136,7 +246,7 @@ client.inventory.book(event_key, hold_id: hold_id, booking_ref: charge.id)
136
246
  **Your backend picks the seats.** Phone orders, box office, comps.
137
247
 
138
248
  ```ruby
139
- # Payment already taken — book outright, so nothing is stranded if a second call fails.
249
+ # Payment already taken: book outright, so nothing is stranded if a second call fails.
140
250
  client.inventory.book_best_available(event_key, qty: 2, booking_ref: "phone-1183")
141
251
 
142
252
  # Or name the seats yourself.
@@ -167,7 +277,7 @@ pause/archive controls, audit-safe session listing, and channel reports are on
167
277
  ## Listing and pagination
168
278
 
169
279
  `list` returns one page plus a `nextCursor`. `list_all` pages for you and returns a lazy
170
- `Enumerator` when no block is given — the point of paginating is to *not* hold an unbounded result
280
+ `Enumerator` when no block is given, because the point of paginating is to *not* hold an unbounded result
171
281
  set in memory, so `.lazy.first(n)` stops fetching once it has enough.
172
282
 
173
283
  ```ruby
@@ -181,28 +291,30 @@ client.events.list_all do |event|
181
291
  sync(event)
182
292
  end
183
293
 
184
- # Lazily — this fetches one page, not all of them.
294
+ # Lazily: this fetches one page, not all of them.
185
295
  client.charts.list_all.lazy.first(5)
186
296
  ```
187
297
 
188
298
  Listing events includes live availability `counts` by default, which costs the server one
189
- round-trip **per event**. `list_all` turns them off automatically — walking a whole catalogue is
190
- exactly when you don't want that — and you can control it explicitly:
299
+ round-trip **per event**. `list_all` turns them off automatically, since walking a whole catalogue is
300
+ exactly when you don't want that, and you can control it explicitly:
191
301
 
192
302
  ```ruby
193
303
  client.events.list(limit: 50, counts: false)
194
304
  ```
195
305
 
306
+ Pass `series_key: "ser_1"` to list only the dates of one event series.
307
+
196
308
  ## Keeping a hold alive
197
309
 
198
- When an order takes longer than the checkout window — an invoice, a phone sale — extend rather than
310
+ When an order takes longer than the checkout window (an invoice, a phone sale), extend rather than
199
311
  release and re-hold. Releasing first hands the seats to whoever is racing for them in between.
200
312
 
201
313
  ```ruby
202
314
  begin
203
315
  client.inventory.extend_hold(event_key, hold_id, ttl_ms: 10 * 60_000)
204
316
  rescue SeatLayer::ConflictError
205
- # Gone, expired, or at its renewal cap — the buyer has to re-pick.
317
+ # Gone, expired, or at its renewal cap: the buyer has to re-pick.
206
318
  end
207
319
  ```
208
320
 
@@ -240,8 +352,8 @@ The full set, all opt-in:
240
352
  | `event:door:checkin` | Check tickets in and out |
241
353
  | `event:boxoffice` | Use the managed box-office surface |
242
354
 
243
- The two `event:channels:*` capabilities are **not** in the default — a token minted before sales
244
- channels existed must not silently acquire channel authority — so ask for them explicitly if the
355
+ The two `event:channels:*` capabilities are **not** in the default (a token minted before sales
356
+ channels existed must not silently acquire channel authority), so ask for them explicitly if the
245
357
  page manages channels.
246
358
 
247
359
  Designer minting returns the API envelope unchanged: read the token and effective safe-mode and
@@ -251,7 +363,7 @@ feature policy under `result["session"]`. Pass `safe_mode_options` only with `mo
251
363
 
252
364
  Subscription responses use the wire envelopes exactly: `list` returns `{"subs" => [...]}`,
253
365
  `create` returns `{"sub" => ..., "secret" => ...}` (the secret is shown once), and `update`
254
- returns `{"sub" => ...}`. `SeatLayer::Webhooks::EVENT_NAMES` is the exact eight-name event set;
366
+ returns `{"sub" => ...}`. `SeatLayer::Webhooks::EVENT_NAMES` is the exact 56-name public event set;
255
367
  delivery history accepts `limit`, `status` (`"ok"` or `"failed"`), and `before`.
256
368
 
257
369
  Verify every delivery against the **raw** body. Re-encoding a parsed Hash changes the bytes and
@@ -270,7 +382,7 @@ class WebhooksController < ApplicationController
270
382
  )
271
383
 
272
384
  # The signed body carries `at`, but nothing enforces a freshness window, so a
273
- # captured delivery stays valid indefinitely. Deduplicate on occurrenceId —
385
+ # captured delivery stays valid indefinitely. Deduplicate on occurrenceId:
274
386
  # this is your replay protection, not an optimisation.
275
387
  return head :ok if already_processed?(event["occurrenceId"])
276
388
 
@@ -305,23 +417,36 @@ end
305
417
  | `ConflictError` | 409 | Inventory moved, or a guard rejected the change |
306
418
  | `ValidationError` | 422 | Understood and rejected |
307
419
  | `RateLimitError` | 429 | Over budget; carries `retry_after` |
308
- | `ConnectionError` | — | No answer: DNS, TLS, socket, timeout |
420
+ | `ConnectionError` | none | No answer: DNS, TLS, socket, timeout |
309
421
 
310
422
  All descend from `SeatLayer::Error`, so `rescue SeatLayer::Error` catches everything. Every API
311
- error carries `status`, `code`, `body` and `request_id` — quote the request id in support requests.
423
+ error carries `status`, `code`, `body` and `request_id`. Quote the request id in support requests.
312
424
 
313
425
  ## Reliability
314
426
 
315
427
  **Retries.** Reads (`GET`/`HEAD`) retry 429, 408 and 5xx with exponential backoff and full jitter;
316
- `Retry-After` wins when the server sends it. Fourteen mutations use exact header replay:
317
- `charts.create`, `charts.copy`, `templates.instantiate_template`, `events.create`,
318
- `workspaces.create`, `performance_groups.create`, `seasons.create_season`,
319
- `seasons.update_season`, `seasons.delete_season`, `seasons.create_season_plan`,
320
- `seasons.duplicate_season_to_live`, `seasons.create_season_holder_import`,
321
- `seasons.create_season_renewal_offers`, and `seasons.create_season_amendment`. Other 4xx responses
322
- are never retried.
323
-
324
- **Idempotency.** Those 14 replay-backed operations carry an `Idempotency-Key`, generated when you
428
+ `Retry-After` wins when the server sends it. Forty-one mutations use exact header replay:
429
+
430
+ - `charts.create`, `charts.copy`, `templates.instantiate_template`, `events.create`,
431
+ `workspaces.create`, and `performance_groups.create`;
432
+ - 26 on `seasons`: `create_season`, `update_season`, `delete_season`, `create_season_plan`,
433
+ `update_season_plan`, `delete_season_plan`, `publish_season`, `duplicate_season_to_live`,
434
+ `create_season_holder_import`, `create_season_holder_import_from_season`,
435
+ `create_season_renewal_offers`, `create_season_amendment`, `create_season_performances`,
436
+ `attach_season_performances`, `delete_season_performance`, `create_season_checkout_test`,
437
+ `replace_season_package_prices`, `attach_season_plan_performance`,
438
+ `delete_season_plan_performance`, `mark_season_performance_past`, `refund_season_order`,
439
+ `cancel_season_order`, `cancel_season_order_date`, `refund_season_performance`,
440
+ `release_season_lapsed_offers`, and `create_season_renewal_checkout`;
441
+ - `holders.update_holder`, `holders.create_holder_transfer`, `holders.create_holder_return`, and
442
+ `holders.create_holder_renewal_checkout`;
443
+ - `orders.refund_event_order` and `orders.cancel_event_order`;
444
+ - `event_series.create_event_series`, `event_series.add_event_series_dates`, and
445
+ `event_series.update_event_series`.
446
+
447
+ Other 4xx responses are never retried.
448
+
449
+ **Idempotency.** Those 41 replay-backed operations carry an `Idempotency-Key`, generated when you
325
450
  do not supply one and reused across attempts. All remaining SDK mutations are single-attempt. Some
326
451
  have a server-side domain idempotency contract, but the SDK does not retry them automatically. This
327
452
  includes inventory holds and bookings, show-once credential or secret creation, unsupported
@@ -359,14 +484,17 @@ outbox, and support operations.
359
484
  | --- | --- |
360
485
  | `charts` | `list` `list_all` `create` `retrieve` `update` `delete` `copy` `archive` `unarchive` `publish` |
361
486
  | `templates` | `instantiate_template` |
362
- | `events` | `list` `list_all` `create` `retrieve` `retrieve_configuration_binding` `update_configuration_binding` `update` `delete` `update_poster` `delete_poster` `update_chart` `close` `reopen` `archive` `retrieve_hold_ttl` `update_hold_ttl` `list_ticket_releases` `update_ticket_releases` `close_ticket_release` `retrieve_report` `retrieve_log` |
487
+ | `events` | `list` `list_all` `create` `retrieve` `retrieve_configuration_binding` `update_configuration_binding` `update` `delete` `update_poster` `delete_poster` `update_chart` `close` `reopen` `archive` `retrieve_hold_ttl` `update_hold_ttl` `list_ticket_releases` `update_ticket_releases` `close_ticket_release` `retrieve_report` `retrieve_log` `retrieve_event_buyer_checkout` `replace_event_buyer_checkout` `retrieve_event_payment_gateway` `replace_event_payment_gateway` `retrieve_event_sales_readiness` `preflight_event_sales` `transition_event_sales` `retrieve_event_booking_fee` `replace_event_booking_fee` `list_event_discount_codes` `create_event_discount_code` `update_event_discount_code` `end_event_discount_code` `delete_event_discount_code` |
363
488
  | `inventory` | `hold` `hold_best_available` `book_best_available` `extend_hold` `retrieve_hold` `release` `book` `box_office_book` `unbook` `list_bookings` `retrieve_booking` `block` `unblock` `unblock_all` `retrieve_availability` `update_availability` |
364
489
  | `channels` | `list_channels` `create_channel` `update_channel` `update_assignments` `list_allocation` `retrieve_access_preview` `retrieve_report` `pause` `unpause` `archive` `create_buyer_access_session` `list_buyer_access_sessions` `revoke_buyer_access_session` `create_access_link` `list_access_links` `rotate_access_link` `revoke_access_link` |
365
490
  | `sessions` | `create_manage_session` `revoke_manage_session` `create_designer_session` `revoke_designer_session` |
366
491
  | `webhooks` | `list` `create` `update` `delete` `list_deliveries` |
367
492
  | `workspaces` | `list` `create` `retrieve` `update` |
368
493
  | `performance_groups` | `list` `create` `retrieve` `delete` `activate` `close` `retrieve_lifecycle` `create_buyer_access_session` `list_buyer_access_sessions` `revoke_buyer_access_session` `retrieve_hold` `book_hold` `retrieve_booking` |
369
- | `seasons` | 48 operations for catalogue and Plan lifecycle, sales windows, buyer access and booking, holder imports, renewals, occurrence amendments, reports, audit, outbox, and support export |
494
+ | `seasons` | 87 operations for catalogue and Plan lifecycle, performances, package prices, checkout tests, sales windows, buyer access and booking, holder imports, holders, renewals, renewal quotes and checkouts, occurrence amendments, returns and reclaims, Season orders, bulk refunds, lapsed-offer releases and jobs, reports and CSV exports, audit, outbox, and support export |
495
+ | `holders` | `list_customers` `retrieve_holder` `update_holder` `revoke_holder_sessions` `list_holder_seasons` `list_holder_tickets` `retrieve_holder_ticket` `create_holder_transfer` `list_holder_transfers` `cancel_holder_transfer` `accept_holder_transfer` `create_holder_return` `create_holder_renewal_checkout` `decline_holder_renewal_offer` |
496
+ | `orders` | `list_orders` `retrieve_order` `list_order_tickets` `list_event_orders` `retrieve_event_order` `export_event_orders_csv` `refund_event_order` `cancel_event_order` `resend_event_order_tickets` |
497
+ | `event_series` | `create_event_series` `list_event_series` `retrieve_event_series` `add_event_series_dates` `update_event_series` |
370
498
 
371
499
  Full reference: [SeatLayer Ruby server SDK guide](https://docs.seatlayer.io/server-sdk/ruby/)
372
500
 
@@ -374,24 +502,21 @@ Full reference: [SeatLayer Ruby server SDK guide](https://docs.seatlayer.io/serv
374
502
 
375
503
  Some API surface is intentionally unwrapped, not merely pending:
376
504
 
377
- - **Hosted-checkout orders and refunds.** Reading or refunding a SeatLayer-hosted-checkout sale is
378
- not a server-SDK capability. Those records only exist for organisations using hosted checkout; if
379
- you run your own commerce store you refund in that store, through your own gateway.
380
- - **Connecting or assigning payment gateways.** Connecting one is a dashboard flow, so shipping only
381
- the assignment half across seven SDKs would hand you a method that cannot yet succeed.
505
+ - **Connecting a payment gateway.** Connecting one is a dashboard flow. Once it is connected,
506
+ `events.replace_event_payment_gateway` assigns it to an event.
382
507
  - **Realtime seat updates.** Live seat state reaches the *browser* through the widget's own socket.
383
508
  There is no server-side subscribe; a secret-key caller gets authoritative state from
384
509
  `events.retrieve_report` and `inventory.retrieve_availability`.
385
510
 
386
- None of these are reachable through `request` as a supported path either — they are excluded from
387
- the public manifest, not just from the wrapper.
511
+ Neither is reachable through `request` as a supported path either; both are excluded from the
512
+ public manifest, not just from the wrapper.
388
513
 
389
514
  ## Frequently asked questions
390
515
 
391
516
  ### How do I book seats from Ruby?
392
517
 
393
- Create a client with your secret key, obtain a hold id — either from the buyer's
394
- browser session or by holding server-side — and call `inventory.book(event_key, hold_id: ..., booking_ref: ...)`.
518
+ Create a client with your secret key, obtain a hold id (either from the buyer's
519
+ browser session or by holding server-side) and call `inventory.book(event_key, hold_id: ..., booking_ref: ...)`.
395
520
  `booking_ref` is your own stable order id and is the join between SeatLayer
396
521
  inventory and your commercial order, so the same reference identifies the booking
397
522
  in Booking History and when you later cancel it. For phone orders, box office, and
@@ -403,7 +528,7 @@ The buyer SDK runs where the ticket buyer is: it renders the interactive seating
403
528
  chart, handles seat selection, and creates temporary holds. This server SDK is the
404
529
  trusted side. It authenticates with your secret key, inspects what a hold actually
405
530
  contains, prices from server-owned data, and books. Never bundle the secret key
406
- into a browser or a mobile app — browser surfaces get short-lived, origin-bound
531
+ into a browser or a mobile app. Browser surfaces get short-lived, origin-bound
407
532
  tokens that you mint here.
408
533
 
409
534
  ### How do temporary holds work server-side?
@@ -414,7 +539,7 @@ and at what price, so charge from its `items` rather than from anything the brow
414
539
  sent you. When an order runs longer than the checkout window, `inventory.extend_hold`
415
540
  renews the hold instead of releasing and re-holding, which would hand the seats to
416
541
  whoever is racing for them. Bookings carry the server's exact-selection plus
417
- `booking_ref` safeguard, but the SDK sends each booking once — reconcile an unknown
542
+ `booking_ref` safeguard, but the SDK sends each booking once; reconcile an unknown
418
543
  outcome before trying again.
419
544
 
420
545
  ### Can I use my own payment provider?
@@ -0,0 +1,78 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SeatLayer
4
+ # Event series: many dates of one show, planned from a schedule and run as
5
+ # one. A series is addressed by the key of its first event.
6
+ class EventSeries < Resource
7
+ # Plan an event series from a +schedule+ Hash, or create it as a job.
8
+ # Pass <tt>dry_run: true</tt> to get the dates back without creating any.
9
+ # Every date carries +name+ as its name.
10
+ #
11
+ # Give +name+ and +chart_key+, or +from_event_key+: an event you already set
12
+ # up becomes the first date under its own key, and every other date is a copy
13
+ # of it (floor plan, prices, checkout, poster and its own seat blocks, none of
14
+ # its orders, holds or sales). The series takes the event's name, zone and
15
+ # floor plan.
16
+ #
17
+ # A series from an event that never started and was given up answers 409
18
+ # create_abandoned for that key every time: send it again with a new
19
+ # +idempotency_key+.
20
+ def create_event_series( # rubocop:disable Metrics/ParameterLists
21
+ schedule:, name: nil, chart_key: nil, timezone: nil, venue: nil,
22
+ duration_minutes: nil, dry_run: nil, idempotency_key: nil, from_event_key: nil
23
+ )
24
+ if from_event_key.nil? && (name.nil? || chart_key.nil?)
25
+ raise ArgumentError, "give name: and chart_key:, or from_event_key:"
26
+ end
27
+
28
+ body = compact({ "fromEventKey" => from_event_key, "name" => name, "chartKey" => chart_key,
29
+ "schedule" => schedule, "timezone" => timezone, "venue" => venue,
30
+ "durationMinutes" => duration_minutes, "dryRun" => dry_run })
31
+ @client.post("/v1/event-series", body, idempotency_key: idempotency_key,
32
+ retry_policy: :header_replay)
33
+ end
34
+
35
+ # One page of event series.
36
+ def list_event_series(limit: nil, cursor: nil)
37
+ @client.get("/v1/event-series", compact({ "limit" => limit, "cursor" => cursor }))
38
+ end
39
+
40
+ # Read one event series.
41
+ def retrieve_event_series(event_key)
42
+ @client.get(path(event_key))
43
+ end
44
+
45
+ # Add dates to a series, copied from its first date. Pass one of a +schedule+
46
+ # Hash, a +dates+ list of "YYYY-MM-DDTHH:MM" strings, or +retry_failed: true+ to
47
+ # make again every date that failed to be made (422 nothing_to_retry when none
48
+ # did, 409 series_busy while another change runs).
49
+ def add_event_series_dates(event_key, schedule: nil, dates: nil, retry_failed: false,
50
+ idempotency_key: nil)
51
+ @client.post(path(event_key, "/dates"),
52
+ compact({ "schedule" => schedule, "dates" => dates,
53
+ "retryFailed" => (retry_failed ? true : nil) }),
54
+ idempotency_key: idempotency_key, retry_policy: :header_replay)
55
+ end
56
+
57
+ # Rename a series, change its prices, or turn sales "on" or "off" on every
58
+ # upcoming date. +prices+ ({ "categories" => [...] }) takes only the
59
+ # categories whose price changes (for a tiered category, only the tiers
60
+ # sent); every price left out keeps each date's own value. Sales "on" never
61
+ # opens a date its season cancelled: that date is a failed item of the job
62
+ # (season_date_cancelled), counted in the job's "failed" but not in
63
+ # "failedDates", which are the dates that could not be made.
64
+ def update_event_series(event_key, name: nil, prices: nil, sales: nil, idempotency_key: nil)
65
+ @client.mutation_with_header_replay(
66
+ "PATCH", path(event_key),
67
+ compact({ "name" => name, "prices" => prices, "sales" => sales }),
68
+ idempotency_key: idempotency_key
69
+ )
70
+ end
71
+
72
+ private
73
+
74
+ def path(event_key, suffix = "")
75
+ "/v1/event-series/#{encode(event_key)}#{suffix}"
76
+ end
77
+ end
78
+ end