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.1.0</version>
</dependency>implementation 'io.seatlayer:seatlayer-java:0.1.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. 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");
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");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. 429, 408 and 5xx are retried with exponential backoff and full jitter; Retry-After
wins when the server sends it. 4xx is never retried — it will not start succeeding.
Idempotency. Every mutating request carries an Idempotency-Key, generated if you do not supply
one, and reused across retries so a retried booking cannot become two bookings.
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 — same auth, retries, idempotency and error mapping:
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 |
events() |
list listAll create retrieve update delete updateChart close reopen archive retrieveHoldTtl updateHoldTtl retrieveReport retrieveLog |
inventory() |
hold holdBestAvailable bookBestAvailable extendHold retrieveHold release book bookLabels boxOfficeBook unbook block unblock unblockAll retrieveAvailability updateAvailability |
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_flutter |
| Node.js (server) | @seatlayer/server |
| Python (server) | seatlayer |
| PHP (server) | seatlayer/seatlayer-php |
| Go (server) | github.com/seatlayer/seatlayer-go |
| Ruby (server) | seatlayer |
| .NET (server) | SeatLayer |
mvn verify # compile, test, and build the main/sources/javadoc jarsMIT