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 +4 -4
- data/CHANGELOG.md +109 -0
- data/README.md +167 -42
- data/lib/seatlayer/event_series.rb +78 -0
- data/lib/seatlayer/holders.rb +109 -0
- data/lib/seatlayer/http_client.rb +29 -14
- data/lib/seatlayer/inventory.rb +19 -2
- data/lib/seatlayer/orders.rb +75 -0
- data/lib/seatlayer/resources.rb +103 -2
- data/lib/seatlayer/seasons.rb +432 -11
- data/lib/seatlayer/version.rb +1 -1
- data/lib/seatlayer.rb +7 -1
- metadata +6 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 2e7f487c6f37918712d5b9ad4967aefb979b5bbd85368cd1c669b3718837c19d
|
|
4
|
+
data.tar.gz: 1a350dcf149a427a410faf436a39ab8b4ada55a8a262956513316b7531a52b65
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
[](https://www.ruby-lang.org/)
|
|
6
6
|
[](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
|
|
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
|
|
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
|
-
|
|
80
|
-
|
|
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
|
|
86
|
-
price, payment, order, tax, refunds,
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
190
|
-
exactly when you don't want that
|
|
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
|
|
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
|
|
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
|
|
244
|
-
channels existed must not silently acquire channel authority
|
|
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
|
|
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` |
|
|
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
|
|
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.
|
|
317
|
-
|
|
318
|
-
`
|
|
319
|
-
`
|
|
320
|
-
`seasons
|
|
321
|
-
`
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
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` |
|
|
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
|
-
- **
|
|
378
|
-
|
|
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
|
-
|
|
387
|
-
|
|
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
|
|
394
|
-
browser session or by holding server-side
|
|
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
|
|
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
|
|
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
|