Official Java server SDK for the SeatLayer reserved-seating API.
Server-side only. This library authenticates with your secret key. Never ship it in an Android app or anything a ticket buyer can reach — browser and mobile surfaces get short-lived, origin-bound tokens that you mint here.
<dependency>
<groupId>io.seatlayer</groupId>
<artifactId>seatlayer-java</artifactId>
<version>0.4.0</version>
</dependency>implementation 'io.seatlayer:seatlayer-java:0.4.0'Requires Java 17 or newer. Zero runtime dependencies — the SDK uses
java.net.http.HttpClient and javax.crypto.Mac from the JDK plus a small hand-written JSON
codec, so it never forces a Jackson or OkHttp version on an application that already has one.
import io.seatlayer.SeatLayer;
import java.util.Map;
SeatLayer seatlayer = new SeatLayer(System.getenv("SEATLAYER_SECRET_KEY"));
// 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.
Map<String, Object> event =
(Map<String, Object>) seatlayer.events().create((String) chart.get("id"), "Spring Gala").get("meta");
// 3. Sell four seats over the phone.
Map<String, Object> held = seatlayer.inventory().holdBestAvailable((String) event.get("key"), 4);
// … take payment against held.get("items"), which carry authoritative prices …
seatlayer.inventory().book((String) event.get("key"), (String) held.get("holdId"), "order-8842");Keys carry their own mode. sk_test_… keys can only touch test-mode events and sk_live_… only
live ones; crossing them returns 403 mode_mismatch, surfaced as SeatLayerAuthException with
isModeMismatch().
SeatLayer seatlayer = new SeatLayer(System.getenv("SEATLAYER_SECRET_KEY"));
if ("production".equals(System.getenv("ENV")) && !"live".equals(seatlayer.mode())) {
throw new IllegalStateException("Refusing to boot production against test-mode seating data.");
}Buyer picks seats in the browser. Your frontend holds them; your backend confirms the price and
books. Never price from what the browser sent you — retrieveHold is authoritative.
Map<String, Object> hold = seatlayer.inventory().retrieveHold(eventKey, holdId);
// … charge the total of hold.get("items") in hold.get("currency") …
seatlayer.inventory().book(eventKey, holdId, charge.id());Your backend picks the seats. Phone orders, box office, comps.
// Payment already taken — book outright, so nothing is stranded if a second call fails.
seatlayer.inventory().bookBestAvailable(eventKey, 2, "phone-1183");
// Or name the seats yourself.
seatlayer.inventory().boxOfficeBook(eventKey, List.of("A-1", "A-2"), "comp-14");Channels reserve inventory for a partner, member group, presale, or other private allocation. A buyer access session is short-lived and origin-bound, so the browser receives only the allocation it is allowed to sell; your secret key remains on your server.
seatlayer.channels().create(
eventKey, "Venue members", null, null, null, "private", null, null);
seatlayer.channels().updateAssignments(
eventKey, List.of("A-1", "A-2"), 1L, "ch_members", null, null);
Map<String, Object> access = seatlayer.channels().createBuyerAccessSession(
eventKey,
false,
"https://members.example",
List.of("ch_members"),
null,
2,
null,
null,
null,
null);Pass the returned token to the buyer SDK. Trusted backend sale overloads accept channelIds, an
explicit privileged ignoreChannelRestrictions flag, and an audit reason.
list() returns one Page plus a cursor. When you want everything, listAll() pages for you and
yields as you consume it — a lazy Iterable rather than a List, because the point of paginating
is to not hold an unbounded result set in memory.
// One page, your own paging.
Page<Map<String, Object>> page = seatlayer.events().list(
EventListOptions.builder().limit(50).build());
page.items();
page.nextCursor(); // Optional.empty() once exhausted
// Or let the SDK walk it.
for (Map<String, Object> event : seatlayer.events().listAll()) {
sync(event);
}Listing events includes live availability counts by default, which costs the server one
round-trip per event. listAll() turns them off automatically — walking a whole catalogue is
exactly when you don't want that — and you can control it explicitly:
seatlayer.events().list(EventListOptions.builder().limit(50).counts(false).build());When an order takes longer than the checkout window — an invoice, a phone sale — extend rather than release and re-hold. Releasing first hands the seats to whoever is racing for them in between.
try {
seatlayer.inventory().extendHold(eventKey, holdId, 10 * 60_000L);
} catch (SeatLayerConflictException e) {
// Gone, expired, or at its renewal cap — the buyer has to re-pick.
}Your secret key never reaches a browser. Mint a scoped token instead.
Map<String, Object> session = seatlayer.sessions().createManageSession(
eventKey,
"https://box-office.yourplatform.com",
List.of("event:view", "event:block"),
3600);capabilities is required by this SDK even though the API defaults it. That default grants all
four including event:cancel, which reverses paid bookings — not something that should arrive by
forgetting an argument. Grant the smallest set the page needs.
Verify every delivery against the raw body. Re-serialising it changes the bytes and verification will fail.
import io.seatlayer.Webhook;
import io.seatlayer.WebhookVerificationException;
// Spring: declare the parameter as byte[], never a parsed object
@PostMapping("/webhooks/seatlayer")
public ResponseEntity<Void> handle(
@RequestBody byte[] payload,
@RequestHeader("X-SeatLayer-Signature") String signature) {
Map<String, Object> event;
try {
event = Webhook.verify(payload, signature, System.getenv("SEATLAYER_WEBHOOK_SECRET"));
} catch (WebhookVerificationException e) {
return ResponseEntity.badRequest().build();
}
// The signed body carries `at`, but nothing enforces a freshness window, so a
// captured delivery stays valid indefinitely. Deduplicate on occurrenceId —
// this is your replay protection, not an optimisation.
if (alreadyProcessed((String) event.get("occurrenceId"))) {
return ResponseEntity.ok().build();
}
process(event);
return ResponseEntity.ok().build();
}try {
seatlayer.inventory().holdBestAvailable(eventKey, 6);
} catch (SeatLayerConflictException e) {
if (e.isSoldOut()) {
return showAlternativeDates(); // a business outcome, not a bug
}
throw e;
} catch (SeatLayerRateLimitException e) {
return retryAfter(e.retryAfterSeconds());
} catch (SeatLayerAuthException e) {
if (e.isModeMismatch()) {
throw new IllegalStateException("Test key pointed at a live event, or the reverse.");
}
throw e;
}Every exception carries status(), code(), body(), and requestId() — quote the request id in
support requests. All are unchecked, so they do not force throws clauses through your call stack.
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. 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
key on an existing typed-method overload is validated and forwarded once for compatibility, but it
does not enable retries or promise replay. Reconcile bookings with their required bookingRef;
never retry an unknown booking outcome as though the transport had made it safe.
SeatLayer.builder()
.secretKey(System.getenv("SEATLAYER_SECRET_KEY"))
.maxRetries(3) // total attempts
.timeout(Duration.ofSeconds(30)) // per attempt
.build();For surface this SDK does not wrap yet. Raw reads retain read retries; raw mutations use the same auth and error mapping but are sent once and never receive an automatically generated key:
seatlayer.request("POST", "/v1/events/ev_1/some-new-route", null, Map.of("qty", 2));| Resource | Methods |
|---|---|
charts() |
list listAll create retrieve update delete copy archive unarchive publish |
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 |
webhooks() |
list create update delete listDeliveries |
workspaces() |
list create retrieve update |
Full reference: docs.seatlayer.io/server-sdk
- Server SDK guide
- Errors, retries and idempotency
- Webhook verification
- Server API reference
- OpenAPI description
- Agent-readable documentation
- SeatLayer GitHub organization
| Surface | Package |
|---|---|
| Browser (vanilla) | @seatlayer/js |
| React | @seatlayer/react |
| React Native | @seatlayer/react-native |
| iOS | seatlayer-ios |
| Android | seatlayer-android |
| Flutter | seatlayer |
| Node.js (server) | @seatlayer/server |
| Python (server) | seatlayer |
| PHP (server) | seatlayer/seatlayer-php |
| Java (server) | io.seatlayer:seatlayer-java |
| Go (server) | github.com/seatlayer/seatlayer-go |
| Ruby (server) | seatlayer |
| .NET (server) | SeatLayer |
mvn verify # compile, test, and build the main/sources/javadoc jarsPublishing to Maven Central is documented in RELEASE.md. Signing lives in a
release profile, so an ordinary mvn verify needs no GPG key.
MIT