Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 7 additions & 3 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,13 @@
## Unreleased

- **Security/reliability:** Mutations now default to a single attempt. Automatic header-replay
retries are limited to chart create/copy, event create, and workspace create, preventing
transient failures from duplicating holds or best-available results and from issuing extra
show-once credentials.
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
issuing extra show-once credentials.

- Added `templates().instantiateTemplate` for materializing published catalog templates as drafts,
with header-replay idempotency.
- Added ticket-release list, full-list replacement, and close methods to `events()`.

## 0.2.0 — 2026-08-12

Expand Down
22 changes: 12 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,12 @@ Official Java server SDK for the [SeatLayer](https://seatlayer.io) reserved-seat
<dependency>
<groupId>io.seatlayer</groupId>
<artifactId>seatlayer-java</artifactId>
<version>0.2.0</version>
<version>0.4.0</version>
</dependency>
```

```groovy
implementation 'io.seatlayer:seatlayer-java:0.2.0'
implementation 'io.seatlayer:seatlayer-java:0.4.0'
```

Requires Java 17 or newer. **Zero runtime dependencies** — the SDK uses
Expand All @@ -36,8 +36,9 @@ import java.util.Map;

SeatLayer seatlayer = new SeatLayer(System.getenv("SEATLAYER_SECRET_KEY"));

// 1. Provision a venue for a new organiser from one of your templates.
Map<String, Object> chart = (Map<String, Object>) seatlayer.charts().copy("c_template_arena").get("meta");
// 1. Materialize a published catalog template as a draft for this organiser.
Map<String, Object> chart = (Map<String, Object>) seatlayer.templates()
.instantiateTemplate("your-published-template").get("meta");
seatlayer.charts().publish((String) chart.get("id"));

// 2. Create an event on it.
Expand Down Expand Up @@ -229,11 +230,11 @@ support requests. All are unchecked, so they do not force `throws` clauses throu
## Reliability

**Retries and idempotency.** Reads (`GET`/`HEAD`) retry connection failures, 408, 429 and 5xx with
exponential backoff and full jitter; `Retry-After` wins when the server sends it. Four create
operations have the same retry behaviour with header replay: `charts().create`, `charts().copy`,
`events().create`, and `workspaces().create`. They generate an `Idempotency-Key` when absent and
reuse that key across every attempt. Overloads that accept a key let you provide a stable
provisioning key instead.
exponential backoff and full jitter; `Retry-After` wins when the server sends it. Five
provisioning operations have the same retry behaviour with header replay: `charts().create`,
`charts().copy`, `templates().instantiateTemplate`, `events().create`, and
`workspaces().create`. They generate an `Idempotency-Key` when absent and reuse that key across
every attempt. Overloads that accept a key let you provide a stable provisioning key instead.

All other mutations are single-attempt: holds, bookings, lifecycle changes, channel changes,
show-once secret creation, and raw requests. The SDK does not generate a key for them. A supplied
Expand Down Expand Up @@ -263,7 +264,8 @@ seatlayer.request("POST", "/v1/events/ev_1/some-new-route", null, Map.of("qty",
| Resource | Methods |
| --- | --- |
| `charts()` | `list` `listAll` `create` `retrieve` `update` `delete` `copy` `archive` `unarchive` `publish` |
| `events()` | `list` `listAll` `create` `retrieve` `update` `delete` `updateChart` `close` `reopen` `archive` `retrieveHoldTtl` `updateHoldTtl` `retrieveReport` `retrieveLog` |
| `templates()` | `instantiateTemplate` |
| `events()` | `list` `listAll` `create` `retrieve` `update` `delete` `updateChart` `close` `reopen` `archive` `retrieveHoldTtl` `updateHoldTtl` `listTicketReleases` `updateTicketReleases` `closeTicketRelease` `retrieveReport` `retrieveLog` |
| `channels()` | `list` `create` `update` `updateAssignments` `listAllocation` `retrieveAccessPreview` `retrieveReport` `pause` `unpause` `archive` `createBuyerAccessSession` `listBuyerAccessSessions` `revokeBuyerAccessSession` |
| `inventory()` | `hold` `holdBestAvailable` `bookBestAvailable` `extendHold` `retrieveHold` `release` `book` `bookLabels` `boxOfficeBook` `unbook` `block` `unblock` `unblockAll` `retrieveAvailability` `updateAvailability` `listBookings` `retrieveBooking` |
| `sessions()` | `createManageSession` `revokeManageSession` `createDesignerSession` `revokeDesignerSession` |
Expand Down
2 changes: 1 addition & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

<groupId>io.seatlayer</groupId>
<artifactId>seatlayer-java</artifactId>
<version>0.3.0</version>
<version>0.4.0</version>
<packaging>jar</packaging>

<name>SeatLayer Java SDK</name>
Expand Down
26 changes: 26 additions & 0 deletions src/main/java/io/seatlayer/Events.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import static io.seatlayer.SeatLayerHttpClient.encode;

import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

/** Event lifecycle, metadata, reports. */
Expand Down Expand Up @@ -126,6 +127,31 @@ public Map<String, Object> updateHoldTtl(String eventKey, Long holdTtlMs) {
return http.post("/v1/events/" + encode(eventKey) + "/hold-ttl", request);
}

/** Lists the event's ticket releases, including current quota consumption. */
public Map<String, Object> listTicketReleases(String eventKey) {
return http.get("/v1/events/" + encode(eventKey) + "/releases");
}

/**
* Replaces the event's complete, ordered ticket-release list.
*
* <p>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<String, Object> updateTicketReleases(
String eventKey, List<? extends Map<String, ?>> 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<String, Object> closeTicketRelease(String eventKey, String releaseId) {
return http.post(
"/v1/events/" + encode(eventKey) + "/releases/" + encode(releaseId) + "/close");
}

public Map<String, Object> retrieveReport(String eventKey) {
return http.get("/v1/events/" + encode(eventKey) + "/report");
}
Expand Down
7 changes: 7 additions & 0 deletions src/main/java/io/seatlayer/SeatLayer.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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);
}
Expand Down Expand Up @@ -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;
}
Expand Down
42 changes: 42 additions & 0 deletions src/main/java/io/seatlayer/Templates.java
Original file line number Diff line number Diff line change
@@ -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.
*
* <p>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<String, Object> instantiateTemplate(String templateId) {
return instantiateTemplate(templateId, Map.of());
}

/** Instantiates with optional {@code name}, {@code workspaceId}, or version-pinning overrides. */
public Map<String, Object> instantiateTemplate(String templateId, Map<String, Object> 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<String, Object> instantiateTemplate(
String templateId, Map<String, Object> params, String idempotencyKey) {
return http.postWithHeaderReplay(
"/v1/templates/" + encode(templateId) + "/instantiate",
params == null ? Map.of() : params,
idempotencyKey);
}
}
78 changes: 78 additions & 0 deletions src/test/java/io/seatlayer/ClientTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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<String, Object> 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() {
Expand Down Expand Up @@ -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<String, Object> 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() {
Expand Down
Loading