seatlayer 0.7.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: a65cc789351eb854341bf167fdedbd818035c29639e212df5312597f46e3f23c
4
- data.tar.gz: 0fc4163ccaedd380264fbc53853c71c476f4bbfdb4f99180663f66c434d94b66
3
+ metadata.gz: 2e7f487c6f37918712d5b9ad4967aefb979b5bbd85368cd1c669b3718837c19d
4
+ data.tar.gz: 1a350dcf149a427a410faf436a39ab8b4ada55a8a262956513316b7531a52b65
5
5
  SHA512:
6
- metadata.gz: dc59365e346c8a14cf52014ca804f70aa63eabee928214906913c060bf58f4c2edb0dbb3eaec90305096b396bf2257468f444b5d8ba3f154e78f57625b125126
7
- data.tar.gz: c74c63603bd068dd4566b18b1c940762de0258fbcf186b083608c263ce10ffa8a9934db69a135cdbb0d3c225a4334ecfb15a6f672e6e853c745a11d3cc9eab44
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,21 +5,31 @@
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 SeatLayer Ruby server SDK — the trusted side of a reserved-seating
9
- integration. Inspect what a hold really contains, price from server-owned seating-chart
10
- data, and book with a stable `booking_ref`, while managing charts, events, inventory,
11
- allocations, and webhooks through one typed ticketing API client.
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
+
10
+ SeatLayer's official Ruby server SDK is the trusted side of its reserved seating and seat booking
11
+ API. Inspect what a hold really contains, price from server-owned seating-chart data, and book
12
+ with a stable `booking_ref`, while managing charts, events, inventory, allocations, and webhooks
13
+ through one typed ticketing API client.
12
14
 
13
15
  [`seatlayer` gem on RubyGems](https://rubygems.org/gems/seatlayer) ·
14
- [SeatLayer server SDK documentation](https://docs.seatlayer.io/server-sdk/install/) ·
16
+ [Ruby server SDK guide](https://docs.seatlayer.io/server-sdk/ruby/) ·
15
17
  [SeatLayer developer platform](https://seatlayer.io/developers/) ·
16
18
  [SeatLayer JavaScript seat map SDK](https://www.npmjs.com/package/@seatlayer/js) ·
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
- ## Install
32
+ ## Install the Ruby seat booking SDK
23
33
 
24
34
  ```ruby
25
35
  gem "seatlayer"
@@ -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
@@ -40,11 +50,16 @@ require "seatlayer"
40
50
  client = SeatLayer::Client.new(ENV.fetch("SEATLAYER_SECRET_KEY"))
41
51
 
42
52
  # 1. Provision a venue for a new organiser from a public template.
43
- chart = client.templates.instantiate_template("arena-standard")["meta"]
53
+ # Replace this placeholder with a template id from your catalog.
54
+ chart = client.templates.instantiate_template("your-published-template")["meta"]
44
55
  client.charts.publish(chart["id"])
45
56
 
46
57
  # 2. Create an event on it.
47
- event = client.events.create(chart_id: chart["id"], name: "Spring Gala")["meta"]
58
+ event = client.events.create(
59
+ chart_id: chart["id"], name: "Spring Gala",
60
+ currency: "EUR", # omit to inherit the workspace currency
61
+ region: "western-europe" # India: "asia-pacific"
62
+ )["meta"]
48
63
 
49
64
  # 3. Sell four seats over the phone.
50
65
  held = client.inventory.hold_best_available(event["key"], qty: 4)
@@ -52,21 +67,47 @@ held = client.inventory.hold_best_available(event["key"], qty: 4)
52
67
  client.inventory.book(event["key"], hold_id: held["holdId"], booking_ref: "order-8842")
53
68
  ```
54
69
 
70
+ ## Event hosting region
71
+
72
+ Pass `region:` to `events.create` based on the **event venue**, not your API server or office. It
73
+ controls the initial placement of the Event's live inventory; an existing Event
74
+ cannot be moved later. Omit it to inherit the workspace default (`western-europe` for new accounts).
75
+ Set that default with `workspaces.create(default_region: ...)` or
76
+ `workspaces.update(workspace_id, default_region: ...)`; changing it affects only future Events.
77
+
78
+ - `western-europe`, `eastern-europe`, `north-america-east`, `north-america-west`, `south-america`
79
+ - `asia-pacific`, `northeast-asia`, `southeast-asia`, `oceania`, `africa`, `middle-east`
80
+
81
+ The hint is best effort, not a data-residency guarantee. See the
82
+ [full Event region guide](https://docs.seatlayer.io/server-api/event-regions/).
83
+
55
84
  Nullable event-create fields distinguish omission from an explicit reset: passing, for example,
56
85
  `venue: nil` sends JSON `null`; leaving `venue` out sends no field.
57
86
 
58
- ## Test vs live
59
-
60
87
  ## Fixed Renewable Seasons
61
88
 
62
- Version `0.7.0` exposes all 48 trusted organizer operations through
63
- `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.
64
104
 
65
105
  After the test hold/book/cancel journey and matching webhook deliveries,
66
106
  `validate_season_buyer_rehearsal(season_key)` sends no evidence body; SeatLayer
67
107
  discovers the retained chain automatically. Retrieved Season holds contain
68
- inventory identity, not an authoritative amount—your platform owns package
69
- 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.
70
111
 
71
112
  ```ruby
72
113
  checked = client.seasons.validate_season(
@@ -84,6 +125,93 @@ returned operation identity. Buyer-session minting and domain-exact booking,
84
125
  cancellation, and renewal actions remain single-attempt; only declared
85
126
  header-replay catalogue mutations retry automatically.
86
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
+
214
+ ## Test vs live
87
215
 
88
216
  Keys carry their own mode. `sk_test_…` keys can only touch test-mode events and `sk_live_…` only
89
217
  live ones; crossing them returns `403 mode_mismatch`, surfaced as `AuthError` with `mode_mismatch?`.
@@ -97,22 +225,28 @@ raise "Refusing to boot production against test-mode seating data." if
97
225
  A publishable `pk_` key is rejected at construction with a message naming the mistake, rather than
98
226
  failing as a `401` three round-trips later.
99
227
 
100
- ## The two selling flows
228
+ ## Book reserved seats from Ruby
101
229
 
102
230
  **Buyer picks seats in the browser.** Your frontend holds them; your backend confirms the price and
103
- 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.
104
232
 
105
233
  ```ruby
106
234
  hold = client.inventory.retrieve_hold(event_key, hold_id)
107
- total = hold["items"].sum { |item| item["unitPrice"] }
108
- # … charge `total` in hold["currency"] …
235
+ currencies = hold["items"].map { |item| item.fetch("currency") }.uniq
236
+ raise "A hold must use one currency" unless currencies.one?
237
+
238
+ currency = currencies.first
239
+ total = hold["items"].sum do |item|
240
+ item.fetch("unitPrice") * item.fetch("quantity", 1)
241
+ end
242
+ # … charge `total` in `currency` …
109
243
  client.inventory.book(event_key, hold_id: hold_id, booking_ref: charge.id)
110
244
  ```
111
245
 
112
246
  **Your backend picks the seats.** Phone orders, box office, comps.
113
247
 
114
248
  ```ruby
115
- # 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.
116
250
  client.inventory.book_best_available(event_key, qty: 2, booking_ref: "phone-1183")
117
251
 
118
252
  # Or name the seats yourself.
@@ -143,7 +277,7 @@ pause/archive controls, audit-safe session listing, and channel reports are on
143
277
  ## Listing and pagination
144
278
 
145
279
  `list` returns one page plus a `nextCursor`. `list_all` pages for you and returns a lazy
146
- `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
147
281
  set in memory, so `.lazy.first(n)` stops fetching once it has enough.
148
282
 
149
283
  ```ruby
@@ -157,28 +291,30 @@ client.events.list_all do |event|
157
291
  sync(event)
158
292
  end
159
293
 
160
- # Lazily — this fetches one page, not all of them.
294
+ # Lazily: this fetches one page, not all of them.
161
295
  client.charts.list_all.lazy.first(5)
162
296
  ```
163
297
 
164
298
  Listing events includes live availability `counts` by default, which costs the server one
165
- round-trip **per event**. `list_all` turns them off automatically — walking a whole catalogue is
166
- 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:
167
301
 
168
302
  ```ruby
169
303
  client.events.list(limit: 50, counts: false)
170
304
  ```
171
305
 
306
+ Pass `series_key: "ser_1"` to list only the dates of one event series.
307
+
172
308
  ## Keeping a hold alive
173
309
 
174
- 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
175
311
  release and re-hold. Releasing first hands the seats to whoever is racing for them in between.
176
312
 
177
313
  ```ruby
178
314
  begin
179
315
  client.inventory.extend_hold(event_key, hold_id, ttl_ms: 10 * 60_000)
180
316
  rescue SeatLayer::ConflictError
181
- # 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.
182
318
  end
183
319
  ```
184
320
 
@@ -205,19 +341,19 @@ The full set, all opt-in:
205
341
  |---|---|
206
342
  | `event:view` | Read the seat map and its live states |
207
343
  | `event:block` | Block and unblock seats |
208
- | `event:cancel` | Unbook paid seats and issue gateway refunds — destructive, moves money |
344
+ | `event:cancel` | Cancel a Platform/SDK booking by reference and return its inventory to sale; does not move gateway money |
209
345
  | `event:reports` | Read sales and availability reports |
210
346
  | `event:channels:view` | Read sales channels and their allocations |
211
347
  | `event:channels:manage` | Create, pause and archive channels; rotate access links |
212
348
  | `event:orders:read` | Read SeatLayer-managed orders |
213
- | `event:refund` | Refund a SeatLayer-managed order |
349
+ | `event:refund` | Refund an eligible Managed Ticketing order through its connected gateway |
214
350
  | `event:tickets:send` | Send SeatLayer-managed tickets |
215
351
  | `event:door:view` | Read the door list |
216
352
  | `event:door:checkin` | Check tickets in and out |
217
353
  | `event:boxoffice` | Use the managed box-office surface |
218
354
 
219
- The two `event:channels:*` capabilities are **not** in the default — a token minted before sales
220
- 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
221
357
  page manages channels.
222
358
 
223
359
  Designer minting returns the API envelope unchanged: read the token and effective safe-mode and
@@ -227,7 +363,7 @@ feature policy under `result["session"]`. Pass `safe_mode_options` only with `mo
227
363
 
228
364
  Subscription responses use the wire envelopes exactly: `list` returns `{"subs" => [...]}`,
229
365
  `create` returns `{"sub" => ..., "secret" => ...}` (the secret is shown once), and `update`
230
- 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;
231
367
  delivery history accepts `limit`, `status` (`"ok"` or `"failed"`), and `before`.
232
368
 
233
369
  Verify every delivery against the **raw** body. Re-encoding a parsed Hash changes the bytes and
@@ -246,7 +382,7 @@ class WebhooksController < ApplicationController
246
382
  )
247
383
 
248
384
  # The signed body carries `at`, but nothing enforces a freshness window, so a
249
- # captured delivery stays valid indefinitely. Deduplicate on occurrenceId —
385
+ # captured delivery stays valid indefinitely. Deduplicate on occurrenceId:
250
386
  # this is your replay protection, not an optimisation.
251
387
  return head :ok if already_processed?(event["occurrenceId"])
252
388
 
@@ -281,25 +417,41 @@ end
281
417
  | `ConflictError` | 409 | Inventory moved, or a guard rejected the change |
282
418
  | `ValidationError` | 422 | Understood and rejected |
283
419
  | `RateLimitError` | 429 | Over budget; carries `retry_after` |
284
- | `ConnectionError` | — | No answer: DNS, TLS, socket, timeout |
420
+ | `ConnectionError` | none | No answer: DNS, TLS, socket, timeout |
285
421
 
286
422
  All descend from `SeatLayer::Error`, so `rescue SeatLayer::Error` catches everything. Every API
287
- 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.
288
424
 
289
425
  ## Reliability
290
426
 
291
427
  **Retries.** Reads (`GET`/`HEAD`) retry 429, 408 and 5xx with exponential backoff and full jitter;
292
- `Retry-After` wins when the server sends it. Automatic mutation retries are limited to the five
293
- operations backed by exact response replay: `charts.create`, `charts.copy`,
294
- `templates.instantiate_template`, `events.create`, and `workspaces.create`. Other 4xx responses
295
- are never retried.
296
-
297
- **Idempotency.** Those five replay-backed operations carry an `Idempotency-Key`, generated when you
298
- do not supply one and reused across attempts. Other mutations are single-attempt and receive no
299
- automatic key. A caller-supplied key is forwarded but does not enable retries. This includes
300
- inventory holds and bookings, show-once credential or secret creation, unsupported operations, and
301
- raw `request` mutations. Keep `booking_ref` in the booking body for reconciliation, but handle an
302
- unknown network outcome explicitly instead of automatically repeating the sale.
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
450
+ do not supply one and reused across attempts. All remaining SDK mutations are single-attempt. Some
451
+ have a server-side domain idempotency contract, but the SDK does not retry them automatically. This
452
+ includes inventory holds and bookings, show-once credential or secret creation, unsupported
453
+ operations, and raw `request` mutations. Keep `booking_ref` in the booking body for reconciliation,
454
+ but handle an unknown network outcome explicitly instead of automatically repeating the sale.
303
455
 
304
456
  ```ruby
305
457
  client.events.create(chart_id: chart_id, idempotency_key: "provision-event-#{event_id}")
@@ -324,41 +476,47 @@ client.request("POST", "/v1/events/ev_1/some-new-route", body: { "qty" => 2 })
324
476
 
325
477
  ## API surface
326
478
 
479
+ The client exposes these resources. Performance Groups cover runs, sessions, holds, and bookings;
480
+ Seasons cover catalogue, plan, sales, buyer-session, booking, renewal, occurrence, reporting,
481
+ outbox, and support operations.
482
+
327
483
  | Resource | Methods |
328
484
  | --- | --- |
329
485
  | `charts` | `list` `list_all` `create` `retrieve` `update` `delete` `copy` `archive` `unarchive` `publish` |
330
486
  | `templates` | `instantiate_template` |
331
- | `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` |
332
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` |
333
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` |
334
490
  | `sessions` | `create_manage_session` `revoke_manage_session` `create_designer_session` `revoke_designer_session` |
335
491
  | `webhooks` | `list` `create` `update` `delete` `list_deliveries` |
336
492
  | `workspaces` | `list` `create` `retrieve` `update` |
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` |
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` |
337
498
 
338
- Full reference: [docs.seatlayer.io/server-sdk](https://docs.seatlayer.io/server-sdk/install/)
499
+ Full reference: [SeatLayer Ruby server SDK guide](https://docs.seatlayer.io/server-sdk/ruby/)
339
500
 
340
501
  ### Deliberately not in this SDK
341
502
 
342
503
  Some API surface is intentionally unwrapped, not merely pending:
343
504
 
344
- - **Hosted-checkout orders and refunds.** Reading or refunding a SeatLayer-hosted-checkout sale is
345
- not a server-SDK capability. Those records only exist for organisations using hosted checkout; if
346
- you run your own commerce store you refund in that store, through your own gateway.
347
- - **Connecting or assigning payment gateways.** Connecting one is a dashboard flow, so shipping only
348
- 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.
349
507
  - **Realtime seat updates.** Live seat state reaches the *browser* through the widget's own socket.
350
508
  There is no server-side subscribe; a secret-key caller gets authoritative state from
351
509
  `events.retrieve_report` and `inventory.retrieve_availability`.
352
510
 
353
- None of these are reachable through `request` as a supported path either — they are excluded from
354
- 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.
355
513
 
356
514
  ## Frequently asked questions
357
515
 
358
516
  ### How do I book seats from Ruby?
359
517
 
360
- Create a client with your secret key, obtain a hold id — either from the buyer's
361
- 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: ...)`.
362
520
  `booking_ref` is your own stable order id and is the join between SeatLayer
363
521
  inventory and your commercial order, so the same reference identifies the booking
364
522
  in Booking History and when you later cancel it. For phone orders, box office, and
@@ -370,7 +528,7 @@ The buyer SDK runs where the ticket buyer is: it renders the interactive seating
370
528
  chart, handles seat selection, and creates temporary holds. This server SDK is the
371
529
  trusted side. It authenticates with your secret key, inspects what a hold actually
372
530
  contains, prices from server-owned data, and books. Never bundle the secret key
373
- 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
374
532
  tokens that you mint here.
375
533
 
376
534
  ### How do temporary holds work server-side?
@@ -381,21 +539,21 @@ and at what price, so charge from its `items` rather than from anything the brow
381
539
  sent you. When an order runs longer than the checkout window, `inventory.extend_hold`
382
540
  renews the hold instead of releasing and re-holding, which would hand the seats to
383
541
  whoever is racing for them. Bookings carry the server's exact-selection plus
384
- `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
385
543
  outcome before trying again.
386
544
 
387
545
  ### Can I use my own payment provider?
388
546
 
389
- Yes. SeatLayer never processes payment. Inspect the hold, compute the charge from
390
- the returned `items` and their authoritative `unitPrice` and `currency`, take the
391
- money through whichever provider you already use — Stripe, Adyen, Razorpay, or your
392
- own — and then book the hold with your order id as `booking_ref`. SeatLayer owns
393
- seating state, holds, booking concurrency, and the inventory ledger; your platform
394
- owns payments, commercial orders, tickets, delivery, and refunds.
547
+ Yes. This server SDK does not process payment in a Platform/SDK integration. Inspect the hold,
548
+ compute the charge from each returned item's authoritative `unitPrice`, `quantity`, and `currency`,
549
+ take the money through whichever provider you already use, and then book the hold with your order
550
+ id as `booking_ref`. SeatLayer owns seating state, holds, booking concurrency, and the inventory
551
+ ledger in this integration; your platform owns payments, commercial orders, tickets, delivery,
552
+ and refunds. Managed Ticketing is a separate product path with organizer-connected payments.
395
553
 
396
554
  ## Continue your Ruby integration
397
555
 
398
- - [Follow the SeatLayer server SDK guide](https://docs.seatlayer.io/server-sdk/install/)
556
+ - [Follow the Ruby server SDK guide](https://docs.seatlayer.io/server-sdk/ruby/)
399
557
  for installation, authentication, and the full hold-to-booking flow.
400
558
  - [Handle errors, retries, and safe booking repeats](https://docs.seatlayer.io/server-sdk/reliability/)
401
559
  before connecting a production order flow.