listTicketReleases(String eventKey) {
+ return http.get("/v1/events/" + encode(eventKey) + "/releases");
+ }
+
+ /**
+ * Replaces the event's complete, ordered ticket-release list.
+ *
+ * Each map is a release input. Do not send response-only fields such as
+ * {@code position}, {@code soldOutAt}, {@code consumed}, or {@code remaining}; the server
+ * derives those from the ordered replacement and live inventory.
+ */
+ public Map updateTicketReleases(
+ String eventKey, List extends Map> releases) {
+ return http.put(
+ "/v1/events/" + encode(eventKey) + "/releases",
+ body("releases", releases));
+ }
+
+ /** Ends one ticket release immediately. This mutation is intentionally single-attempt. */
+ public Map closeTicketRelease(String eventKey, String releaseId) {
+ return http.post(
+ "/v1/events/" + encode(eventKey) + "/releases/" + encode(releaseId) + "/close");
+ }
+
public Map retrieveReport(String eventKey) {
return http.get("/v1/events/" + encode(eventKey) + "/report");
}
diff --git a/src/main/java/io/seatlayer/SeatLayer.java b/src/main/java/io/seatlayer/SeatLayer.java
index 2a532d2..29ba74e 100644
--- a/src/main/java/io/seatlayer/SeatLayer.java
+++ b/src/main/java/io/seatlayer/SeatLayer.java
@@ -22,6 +22,7 @@ public final class SeatLayer {
private final Events events;
private final Inventory inventory;
private final Sessions sessions;
+ private final Templates templates;
private final Webhooks webhooks;
private final Workspaces workspaces;
@@ -37,6 +38,7 @@ private SeatLayer(Builder builder) {
this.events = new Events(http);
this.inventory = new Inventory(http);
this.sessions = new Sessions(http);
+ this.templates = new Templates(http);
this.webhooks = new Webhooks(http);
this.workspaces = new Workspaces(http);
}
@@ -66,6 +68,11 @@ public Sessions sessions() {
return sessions;
}
+ /** Published catalog templates that can be materialized as chart drafts. */
+ public Templates templates() {
+ return templates;
+ }
+
public Webhooks webhooks() {
return webhooks;
}
diff --git a/src/main/java/io/seatlayer/Templates.java b/src/main/java/io/seatlayer/Templates.java
new file mode 100644
index 0000000..41c2912
--- /dev/null
+++ b/src/main/java/io/seatlayer/Templates.java
@@ -0,0 +1,42 @@
+package io.seatlayer;
+
+import static io.seatlayer.SeatLayerHttpClient.encode;
+
+import java.util.Map;
+
+/** Published catalog templates that can be materialized as workspace chart drafts. */
+public final class Templates {
+
+ private final SeatLayerHttpClient http;
+
+ Templates(SeatLayerHttpClient http) {
+ this.http = http;
+ }
+
+ /**
+ * Instantiates a published template as a draft in the caller's workspace.
+ *
+ * The API requires a JSON object even when no overrides are needed, so this sends
+ * {@code {}}. It uses the server's header-replay contract and therefore retries with
+ * one stable idempotency key.
+ */
+ public Map instantiateTemplate(String templateId) {
+ return instantiateTemplate(templateId, Map.of());
+ }
+
+ /** Instantiates with optional {@code name}, {@code workspaceId}, or version-pinning overrides. */
+ public Map instantiateTemplate(String templateId, Map params) {
+ return http.postWithHeaderReplay(
+ "/v1/templates/" + encode(templateId) + "/instantiate",
+ params == null ? Map.of() : params);
+ }
+
+ /** Instantiates with a caller-owned idempotency key for exact server replay. */
+ public Map instantiateTemplate(
+ String templateId, Map params, String idempotencyKey) {
+ return http.postWithHeaderReplay(
+ "/v1/templates/" + encode(templateId) + "/instantiate",
+ params == null ? Map.of() : params,
+ idempotencyKey);
+ }
+}
diff --git a/src/test/java/io/seatlayer/ClientTest.java b/src/test/java/io/seatlayer/ClientTest.java
index ec77e74..da26cb7 100644
--- a/src/test/java/io/seatlayer/ClientTest.java
+++ b/src/test/java/io/seatlayer/ClientTest.java
@@ -9,6 +9,7 @@
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Deque;
+import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import org.junit.jupiter.api.DisplayName;
@@ -124,6 +125,46 @@ void idempotencyKeyOnlyOnHeaderReplayMutations() {
assertNull(call(2).headers().get("Idempotency-Key"));
}
+ @Test
+ @DisplayName("instantiates templates with an object body and escaped identifier")
+ void instantiateTemplateUsesObjectBodyAndEscapedPath() {
+ SeatLayer sdk = client(List.of(Stub.of(201, "{\"meta\":{}}")));
+
+ sdk.templates().instantiateTemplate("tpl / main");
+
+ assertEquals("POST", call(0).method());
+ assertEquals(
+ "https://api.seatlayer.io/v1/templates/tpl%20%2F%20main/instantiate",
+ call(0).url());
+ assertEquals("{}", call(0).body());
+ assertTrue(call(0).headers().get("Idempotency-Key").matches("[A-Za-z0-9._:-]{1,128}"));
+ }
+
+ @Test
+ @DisplayName("sends ticket-release routes and whole-list request body")
+ void ticketReleaseRoutesAndBody() {
+ SeatLayer sdk = client(List.of(
+ Stub.of(200, "{\"releases\":[]}"),
+ Stub.of(200, "{\"releases\":[]}"),
+ Stub.of(200, "{\"releases\":[]}")));
+ Map release = new LinkedHashMap<>();
+ release.put("name", "Early bird");
+ release.put("price", 18);
+
+ sdk.events().listTicketReleases("ev / main");
+ sdk.events().updateTicketReleases("ev / main", List.of(release));
+ sdk.events().closeTicketRelease("ev / main", "rel / one");
+
+ assertEquals(
+ "https://api.seatlayer.io/v1/events/ev%20%2F%20main/releases", call(0).url());
+ assertEquals("PUT", call(1).method());
+ assertEquals(
+ "{\"releases\":[{\"name\":\"Early bird\",\"price\":18}]}", call(1).body());
+ assertEquals(
+ "https://api.seatlayer.io/v1/events/ev%20%2F%20main/releases/rel%20%2F%20one/close",
+ call(2).url());
+ }
+
@Test
@DisplayName("rejects an idempotency key the API would reject")
void rejectsBadIdempotencyKey() {
@@ -238,6 +279,43 @@ void retriesAndReusesKey() {
call(0).headers().get("Idempotency-Key"), call(1).headers().get("Idempotency-Key"));
}
+ @Test
+ @DisplayName("template instantiation retries with one stable idempotency key")
+ void templateInstantiationRetriesAndReusesKey() {
+ SeatLayer sdk = client(List.of(
+ new Stub(429, "{\"error\":\"rate_limited\"}", Map.of("retry-after", "0")),
+ Stub.of(201, "{\"meta\":{}}")));
+
+ sdk.templates().instantiateTemplate("tpl_1");
+
+ assertEquals(2, calls.size());
+ assertEquals(
+ call(0).headers().get("Idempotency-Key"), call(1).headers().get("Idempotency-Key"));
+ }
+
+ @Test
+ @DisplayName("ticket-release mutations are single-attempt")
+ void ticketReleaseMutationsAreSingleAttempt() {
+ SeatLayer updater = client(List.of(
+ new Stub(429, "{\"error\":\"rate_limited\"}", Map.of("retry-after", "0"))));
+ Map release = new LinkedHashMap<>();
+ release.put("name", "Early bird");
+ release.put("price", 18);
+ assertThrows(
+ SeatLayerRateLimitException.class,
+ () -> updater.events().updateTicketReleases("ev_1", List.of(release)));
+ assertEquals(1, calls.size());
+ assertNull(call(0).headers().get("Idempotency-Key"));
+
+ SeatLayer closer = client(List.of(
+ new Stub(429, "{\"error\":\"rate_limited\"}", Map.of("retry-after", "0"))));
+ assertThrows(
+ SeatLayerRateLimitException.class,
+ () -> closer.events().closeTicketRelease("ev_1", "rel_123456789abc"));
+ assertEquals(1, calls.size());
+ assertNull(call(0).headers().get("Idempotency-Key"));
+ }
+
@Test
@DisplayName("read retries remain enabled without an idempotency key")
void retriesReads() {