diff --git a/CHANGELOG.md b/CHANGELOG.md
index bd5aeb6..00c9468 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,14 @@
## Unreleased
+## 0.5.0 — 2026-08-21
+
+- Added `performanceGroups()`, the trusted server resource for fixed two-to-eight
+ performance runs. It creates and activates groups, mints one-time browser
+ access, retrieves authoritative group holds, and confirms bookings with
+ stable action and order references. Browser-only group routes remain outside
+ this secret-key SDK.
+
- **Security/reliability:** Mutations now default to a single attempt. Automatic header-replay
retries are limited to chart create/copy, template instantiation, event create, and workspace
create, preventing transient failures from duplicating holds or best-available results and from
diff --git a/README.md b/README.md
index a431026..687a3f4 100644
--- a/README.md
+++ b/README.md
@@ -16,12 +16,12 @@ Official Java server SDK for the [SeatLayer](https://seatlayer.io) reserved-seat
io.seatlayer
seatlayer-java
- 0.4.0
+ 0.5.0
```
```groovy
-implementation 'io.seatlayer:seatlayer-java:0.4.0'
+implementation 'io.seatlayer:seatlayer-java:0.5.0'
```
Requires Java 17 or newer. **Zero runtime dependencies** — the SDK uses
diff --git a/pom.xml b/pom.xml
index d7f6387..bac198d 100644
--- a/pom.xml
+++ b/pom.xml
@@ -6,7 +6,7 @@
io.seatlayer
seatlayer-java
- 0.4.0
+ 0.5.0
jar
SeatLayer Java SDK
diff --git a/src/main/java/io/seatlayer/PerformanceGroups.java b/src/main/java/io/seatlayer/PerformanceGroups.java
new file mode 100644
index 0000000..5b2beb9
--- /dev/null
+++ b/src/main/java/io/seatlayer/PerformanceGroups.java
@@ -0,0 +1,136 @@
+package io.seatlayer;
+
+import static io.seatlayer.SeatLayerHttpClient.body;
+import static io.seatlayer.SeatLayerHttpClient.encode;
+
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Fixed multi-performance runs, trusted hold inspection, and host-authorized booking.
+ *
+ *
This is a secret-key resource. Pass only the one-time token returned from
+ * {@link #createBuyerAccessSession(String, String, boolean)} to PerformanceGroupPicker in a browser.
+ */
+public final class PerformanceGroups {
+
+ private final SeatLayerHttpClient http;
+
+ PerformanceGroups(SeatLayerHttpClient http) {
+ this.http = http;
+ }
+
+ private static String path(String performanceGroupKey, String suffix) {
+ return "/v1/performance-groups/" + encode(performanceGroupKey) + suffix;
+ }
+
+ public Map list() {
+ return http.get("/v1/performance-groups");
+ }
+
+ public Map list(
+ String workspaceId, String externalRef, String state, Integer limit, String cursor) {
+ return http.get(
+ "/v1/performance-groups",
+ body(
+ "workspaceId", workspaceId,
+ "externalRef", externalRef,
+ "state", state,
+ "limit", limit,
+ "cursor", cursor));
+ }
+
+ /** Create a draft run with exact idempotency replay. */
+ public Map create(String name, List eventKeys) {
+ return create(name, eventKeys, null, null);
+ }
+
+ public Map create(
+ String name, List eventKeys, String externalRef, String idempotencyKey) {
+ return http.postWithHeaderReplay(
+ "/v1/performance-groups",
+ body("name", name, "eventKeys", eventKeys, "externalRef", externalRef),
+ idempotencyKey);
+ }
+
+ public Map retrieve(String performanceGroupKey) {
+ return http.get(path(performanceGroupKey, ""));
+ }
+
+ /** Delete a draft only; activated runs remain available for audit. */
+ public void delete(String performanceGroupKey) {
+ http.delete(path(performanceGroupKey, ""));
+ }
+
+ /** Start activation. Poll retrieveLifecycle when the returned operation is not terminal. */
+ public Map activate(String performanceGroupKey, int expectedRevision) {
+ return http.post(path(performanceGroupKey, "/activate"), body("expectedRevision", expectedRevision));
+ }
+
+ /** Stop new group sales. Poll retrieveLifecycle until the close becomes terminal. */
+ public Map close(String performanceGroupKey, int expectedRevision) {
+ return http.post(path(performanceGroupKey, "/close"), body("expectedRevision", expectedRevision));
+ }
+
+ public Map retrieveLifecycle(String performanceGroupKey, String operationId) {
+ return http.get(path(performanceGroupKey, "/lifecycle/" + encode(operationId)));
+ }
+
+ /**
+ * Reveal one origin-bound browser bearer. This call is deliberately single-attempt.
+ */
+ public Map createBuyerAccessSession(
+ String performanceGroupKey, String allowedOrigin, boolean includePublic) {
+ return createBuyerAccessSession(
+ performanceGroupKey, allowedOrigin, includePublic, null, null, null, null, null);
+ }
+
+ public Map createBuyerAccessSession(
+ String performanceGroupKey,
+ String allowedOrigin,
+ boolean includePublic,
+ Map> channelIdsByEvent,
+ Integer expiresInSeconds,
+ Integer maxQuantity,
+ String buyerRef,
+ String partnerRef) {
+ return http.post(
+ path(performanceGroupKey, "/buyer-access-sessions"),
+ body(
+ "allowedOrigin", allowedOrigin,
+ "includePublic", includePublic,
+ "channelIdsByEvent", channelIdsByEvent,
+ "expiresInSeconds", expiresInSeconds,
+ "maxQuantity", maxQuantity,
+ "buyerRef", buyerRef,
+ "partnerRef", partnerRef));
+ }
+
+ public Map listBuyerAccessSessions(String performanceGroupKey) {
+ return listBuyerAccessSessions(performanceGroupKey, null);
+ }
+
+ public Map listBuyerAccessSessions(String performanceGroupKey, Integer limit) {
+ return http.get(path(performanceGroupKey, "/buyer-access-sessions"), body("limit", limit));
+ }
+
+ public Map revokeBuyerAccessSession(String performanceGroupKey, String sessionId) {
+ return http.delete(path(performanceGroupKey, "/buyer-access-sessions/" + encode(sessionId)));
+ }
+
+ public Map retrieveHold(String performanceGroupKey, String operationId) {
+ return http.get(path(performanceGroupKey, "/holds/" + encode(operationId)));
+ }
+
+ /** Reuse both stable IDs and poll retrieveBooking while the returned booking is pending. */
+ public Map bookHold(
+ String performanceGroupKey, String operationId, String bookActionId, String bookingRef) {
+ return http.post(
+ path(performanceGroupKey, "/holds/" + encode(operationId) + "/book"),
+ body("bookActionId", bookActionId, "bookingRef", bookingRef));
+ }
+
+ public Map retrieveBooking(String performanceGroupKey, String actionId) {
+ return http.get(path(performanceGroupKey, "/bookings/" + encode(actionId)));
+ }
+}
diff --git a/src/main/java/io/seatlayer/SeatLayer.java b/src/main/java/io/seatlayer/SeatLayer.java
index 29ba74e..7d733b0 100644
--- a/src/main/java/io/seatlayer/SeatLayer.java
+++ b/src/main/java/io/seatlayer/SeatLayer.java
@@ -21,6 +21,7 @@ public final class SeatLayer {
private final Channels channels;
private final Events events;
private final Inventory inventory;
+ private final PerformanceGroups performanceGroups;
private final Sessions sessions;
private final Templates templates;
private final Webhooks webhooks;
@@ -37,6 +38,7 @@ private SeatLayer(Builder builder) {
this.channels = new Channels(http);
this.events = new Events(http);
this.inventory = new Inventory(http);
+ this.performanceGroups = new PerformanceGroups(http);
this.sessions = new Sessions(http);
this.templates = new Templates(http);
this.webhooks = new Webhooks(http);
@@ -64,6 +66,11 @@ public Inventory inventory() {
return inventory;
}
+ /** Fixed multi-performance lifecycle and host-side booking coordination. */
+ public PerformanceGroups performanceGroups() {
+ return performanceGroups;
+ }
+
public Sessions sessions() {
return sessions;
}
diff --git a/src/test/java/io/seatlayer/ClientTest.java b/src/test/java/io/seatlayer/ClientTest.java
index da26cb7..90090fc 100644
--- a/src/test/java/io/seatlayer/ClientTest.java
+++ b/src/test/java/io/seatlayer/ClientTest.java
@@ -180,6 +180,57 @@ void dropsNullQueryParameters() {
sdk.charts().list(ChartListOptions.builder().workspaceId("ws_1").build());
assertEquals("https://api.seatlayer.io/v1/charts?workspaceId=ws_1", call(0).url());
}
+
+ @Test
+ @DisplayName("maps the full Performance Groups lifecycle")
+ void performanceGroupLifecycle() {
+ SeatLayer sdk = client(List.of(
+ Stub.of(200, "{\"performanceGroups\":[]}"),
+ Stub.of(201, "{\"performanceGroup\":{}}"),
+ Stub.of(200, "{\"performanceGroup\":{}}"),
+ Stub.of(204, ""),
+ Stub.of(202, "{\"lifecycleOperation\":{\"terminal\":false}}"),
+ Stub.of(200, "{\"lifecycleOperation\":{\"terminal\":true}}"),
+ Stub.of(200, "{\"lifecycleOperation\":{}}"),
+ Stub.of(201, "{\"token\":\"bsg_secret\"}"),
+ Stub.of(200, "{\"sessions\":[]}"),
+ Stub.of(200, "{\"ok\":true}"),
+ Stub.of(200, "{\"hold\":{}}"),
+ Stub.of(202, "{\"booking\":{\"state\":\"book_pending\"}}"),
+ Stub.of(200, "{\"booking\":{\"state\":\"booked\"}}")));
+ String groupKey = "pg_a/b";
+
+ sdk.performanceGroups().list("ws_1", null, "draft", null, null);
+ sdk.performanceGroups().create("Weekend run", List.of("ev_1", "ev_2"), null, "weekend-run-1");
+ sdk.performanceGroups().retrieve(groupKey);
+ sdk.performanceGroups().delete(groupKey);
+ sdk.performanceGroups().activate(groupKey, 1);
+ sdk.performanceGroups().close(groupKey, 2);
+ sdk.performanceGroups().retrieveLifecycle(groupKey, "pga_1");
+ sdk.performanceGroups().createBuyerAccessSession(groupKey, "https://tickets.example.test", true);
+ sdk.performanceGroups().listBuyerAccessSessions(groupKey, 25);
+ sdk.performanceGroups().revokeBuyerAccessSession(groupKey, "pgbs_1");
+ sdk.performanceGroups().retrieveHold(groupKey, "pgh_1");
+ sdk.performanceGroups().bookHold(groupKey, "pgh_1", "book_1", "order_1");
+ sdk.performanceGroups().retrieveBooking(groupKey, "book_1");
+
+ String base = "https://api.seatlayer.io/v1/performance-groups/pg_a%2Fb";
+ assertEquals("https://api.seatlayer.io/v1/performance-groups?workspaceId=ws_1&state=draft", call(0).url());
+ assertEquals("weekend-run-1", call(1).headers().get("Idempotency-Key"));
+ assertEquals(base, call(2).url());
+ assertEquals("DELETE", call(3).method());
+ assertEquals(base + "/activate", call(4).url());
+ assertEquals(base + "/close", call(5).url());
+ assertEquals(base + "/lifecycle/pga_1", call(6).url());
+ assertEquals(base + "/buyer-access-sessions", call(7).url());
+ assertNull(call(7).headers().get("Idempotency-Key"));
+ assertEquals(base + "/buyer-access-sessions?limit=25", call(8).url());
+ assertEquals(base + "/buyer-access-sessions/pgbs_1", call(9).url());
+ assertEquals(base + "/holds/pgh_1", call(10).url());
+ assertEquals(base + "/holds/pgh_1/book", call(11).url());
+ assertNull(call(11).headers().get("Idempotency-Key"));
+ assertEquals(base + "/bookings/book_1", call(12).url());
+ }
}
@Nested