diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 2b95333..c09d09d 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -3,9 +3,9 @@ name: Release
# Uploads to Maven Central via the Central Portal.
#
# Unlike the other five registries in this fleet, Central has NO Trusted
-# Publishing — there is no OIDC path, so this is the one workflow that needs
-# real stored secrets. Doppler (project seatlayer-release, config prd) is the
-# source of truth; these are operational copies pushed into Actions secrets.
+# Publishing: there is no OIDC path, so this is the one workflow that needs
+# real stored secrets. They are kept in the organization's release secrets
+# store and copied into this repository's Actions secrets.
#
# This workflow PUBLISHES. pom.xml sets true, so a
# tag push goes all the way to Central with no human step.
@@ -78,6 +78,6 @@ jobs:
run: |
v="${GITHUB_REF_NAME#v}"
echo "PUBLISHED to Maven Central: io.seatlayer:seatlayer-java:$v"
- echo "This is permanent — Central versions cannot be replaced or deleted."
+ echo "This is permanent: Central versions cannot be replaced or deleted."
echo "https://repo1.maven.org/maven2/io/seatlayer/seatlayer-java/$v/"
echo "Search indexing lags publication by roughly 15-30 minutes."
diff --git a/README.md b/README.md
index f2885bb..4e84d3a 100644
--- a/README.md
+++ b/README.md
@@ -4,7 +4,7 @@
[](https://central.sonatype.com/artifact/io.seatlayer/seatlayer-java)
[](LICENSE)
-SeatLayer is interactive seating chart software built for stadium scale. Platforms embed the white-label seat picker with their own checkout; organizers sell seated events on their own website with their own payment gateway.
+The official Java client for the SeatLayer API. `io.seatlayer:seatlayer-java` lets a Java or Kotlin backend inspect seat holds, price orders from server data, book reserved seats and verify webhooks, with zero runtime dependencies. SeatLayer is seating chart and reserved-seat ticketing software built for venues up to stadium scale.
SeatLayer's official Java server SDK is the **trusted side** of its reserved seating and seat
booking API: inspect the holds a buyer created, price from server data, and book with a stable
@@ -44,7 +44,7 @@ implementation 'io.seatlayer:seatlayer-java:0.8.0'
```
Published on Maven Central as `io.seatlayer:seatlayer-java`; `0.8.0` is the current release, so no
-extra repository declaration is needed. Requires Java 17 or newer. **Zero runtime dependencies** — the SDK uses
+extra repository declaration is needed. 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.
@@ -99,7 +99,7 @@ Version `0.7.0` exposes all 48 trusted organizer operations through
After the test hold/book/cancel journey and matching webhook deliveries,
`validateSeasonBuyerRehearsal(seasonKey)` sends no evidence body; SeatLayer
discovers the retained chain automatically. Retrieved Season holds contain
-inventory identity, not an authoritative amount—your platform owns package
+inventory identity, not an authoritative amount. Your platform owns package
price, payment, order, tax, refunds, benefits, and ticket or pass delivery.
```java
@@ -132,7 +132,7 @@ if ("production".equals(System.getenv("ENV")) && !"live".equals(seatlayer.mode()
## Book reserved seats from Java
**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.
+books. Never price from what the browser sent you: `retrieveHold` is authoritative.
```java
import java.math.BigDecimal;
@@ -161,7 +161,7 @@ seatlayer.inventory().book(eventKey, holdId, charge.id());
**Your backend picks the seats.** Phone orders, box office, comps.
```java
-// Payment already taken — book outright, so nothing is stranded if a second call fails.
+// 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.
@@ -200,7 +200,7 @@ explicit privileged `ignoreChannelRestrictions` flag, and an audit `reason`.
## Listing and pagination
`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
+yields as you consume it. It is a lazy `Iterable` rather than a `List`, because the point of paginating
is to *not* hold an unbounded result set in memory.
```java
@@ -217,8 +217,8 @@ for (Map event : seatlayer.events().listAll()) {
```
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:
+round-trip **per event**. `listAll()` turns them off automatically, since walking a whole catalogue is
+exactly when you don't want that, and you can control it explicitly:
```java
seatlayer.events().list(EventListOptions.builder().limit(50).counts(false).build());
@@ -226,14 +226,14 @@ seatlayer.events().list(EventListOptions.builder().limit(50).counts(false).build
## Keeping a hold alive
-When an order takes longer than the checkout window — an invoice, a phone sale — extend rather than
+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.
```java
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.
+ // Gone, expired, or at its renewal cap: the buyer has to re-pick.
}
```
@@ -278,7 +278,7 @@ public ResponseEntity handle(
}
// The signed body carries `at`, but nothing enforces a freshness window, so a
- // captured delivery stays valid indefinitely. Deduplicate on occurrenceId —
+ // 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();
@@ -309,7 +309,7 @@ try {
}
```
-Every exception carries `status()`, `code()`, `body()`, and `requestId()` — quote the request id in
+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.
## Reliability
@@ -373,8 +373,8 @@ Full reference: [SeatLayer Java server SDK guide](https://docs.seatlayer.io/serv
Add the [`io.seatlayer:seatlayer-java` artifact](https://central.sonatype.com/artifact/io.seatlayer/seatlayer-java),
construct a `SeatLayer` instance with your secret key, and call `inventory().book(...)` with the
-hold id and a stable `bookingRef`. When your own backend picks the seats — phone orders, box
-office, comps — `inventory().bookBestAvailable(...)` and `inventory().boxOfficeBook(...)` book
+hold id and a stable `bookingRef`. When your own backend picks the seats (phone orders, box
+office, comps), `inventory().bookBestAvailable(...)` and `inventory().boxOfficeBook(...)` book
outright with no prior hold. A booking reference is required on every booking call, so each sale is
tied to an immutable order id you can reconcile against later.
@@ -393,7 +393,7 @@ retrieve it with `inventory().retrieveHold(...)`, whose item-level price, quanti
are authoritative, and confirm it with `inventory().book(...)`. Use
`inventory().extendHold(...)` for a long checkout instead of releasing and re-holding, which would
hand the seats to whoever is racing for them. Booking is a single automatic attempt: after an
-unknown network outcome you may reconcile and repeat the exact same event, hold, and `bookingRef` —
+unknown network outcome you may reconcile and repeat the exact same event, hold, and `bookingRef`;
seats already booked under that reference are not sold again.
### Can I use my own payment provider?
diff --git a/RELEASE.md b/RELEASE.md
index 112cde5..d3705f2 100644
--- a/RELEASE.md
+++ b/RELEASE.md
@@ -1,302 +1,68 @@
# Releasing to Maven Central
-Maven Central is the strictest registry SeatLayer publishes to, and the only one with a
-**manual human verification step that has to clear before the first publish can happen at
-all**. npm, PyPI, RubyGems and NuGet let you create an account and push within minutes.
-Central does not: you must prove you control the `io.seatlayer` namespace, and Sonatype
-reviews that proof. Budget for the wait — see step 2.
+`io.seatlayer:seatlayer-java` is published to Maven Central through the Central
+Portal by `.github/workflows/release.yml`. A tag push publishes all the way to
+Central: `pom.xml` sets `true`, so there is no manual
+approval step in the Portal.
-Everything below that is a repository concern is already done. What remains is
-account-, key- and DNS-shaped, and only the owner can do it.
+**A version on Maven Central can never be replaced, edited or deleted.** A bad
+release is fixed only by publishing a newer version. The workflow gate (tests, a
+tag-versus-`` check and GPG signing) runs before anything is uploaded,
+and any failure means nothing is sent. Do not weaken it.
----
+## One-time setup
-## What is already satisfied in this repo
+These are already in place and only need revisiting when a credential changes.
-Do not redo these; they are wired into `pom.xml` and verified by `mvn verify`.
+- **Namespace.** `io.seatlayer` is verified in the Central Portal by a DNS TXT
+ record on `seatlayer.io`. It covers every artifact under `io.seatlayer`.
+- **Signing key.** Central checks each `.asc` signature against the public key on
+ the public keyserver network, so the public key must be published (for example
+ to `keyserver.ubuntu.com` and `keys.openpgp.org`) and retrievable with
+ `gpg --recv-keys` before a release.
+- **Actions secrets** used by the workflow:
-| Central requirement | Where it is handled |
-| --- | --- |
-| `groupId` / `artifactId` / `version`, non-SNAPSHOT | `pom.xml` — `io.seatlayer:seatlayer-java:0.1.0` |
-| `name`, `description`, `url` | `pom.xml` |
-| `licenses` (name + url) | `pom.xml` — MIT |
-| `developers` | `pom.xml` |
-| `scm` — `connection`, `developerConnection`, `url` | `pom.xml` |
-| Main jar | `maven-jar-plugin`, default lifecycle |
-| `-sources.jar` | `maven-source-plugin`, `attach-sources` |
-| `-javadoc.jar` | `maven-javadoc-plugin`, `attach-javadocs` |
-| Javadoc actually builds under strict doclint | `all,-missing`, `failOnError=true` |
-| `.asc` signature on every artifact | `maven-gpg-plugin` in the `release` profile |
-| Upload transport | `central-publishing-maven-plugin`, `extensions=true` |
+ | Secret | Purpose |
+ | --- | --- |
+ | `MAVEN_CENTRAL_USERNAME` | Central Portal user token name |
+ | `MAVEN_CENTRAL_PASSWORD` | Central Portal user token password |
+ | `MAVEN_GPG_PRIVATE_KEY` | Armored private signing key |
+ | `MAVEN_GPG_PASSPHRASE` | Passphrase for the signing key |
----
+ Central has no Trusted Publishing (OIDC) option, so these are the only stored
+ release secrets across the SeatLayer server SDKs.
-## 0. Pre-flight, in the repo
+## Release steps
-```bash
-mvn -B clean verify
-```
-
-Must end `BUILD SUCCESS` with 48 tests passing and three jars in `target/`.
-
-Then flip the changelog heading in `CHANGELOG.md` from `## 0.1.0 — unreleased` to
-`## 0.1.0 — `, and commit. The version on Central is permanent, so the
-changelog entry describing it should not say "unreleased".
-
----
-
-## 1. Central Portal account
-
-1. Sign in at (GitHub or Google SSO is fine).
-2. Account → **Generate User Token**. This returns a `` / `` pair.
- These are *not* your login credentials — they are a scoped publishing token, and they
- are the only thing that belongs in `settings.xml`.
-
-Keep the token. Regenerating it invalidates the previous one.
-
----
-
-## 2. Namespace verification — THE HUMAN STEP, AND THE SLOW ONE
-
-Central will not accept an artifact in a namespace you have not proven you control.
-Nothing currently exists under `https://repo1.maven.org/maven2/io/seatlayer/` (verified
-404), so this is a first-time claim and there is no shortcut.
+1. Bump `` in `pom.xml` (never a `-SNAPSHOT`).
+2. Add a dated entry at the top of `CHANGELOG.md`.
+3. Run the gate locally:
-**We publish under `io.seatlayer`, which is verified by DNS.**
+ ```bash
+ mvn -B clean verify
+ ```
-In the Portal: **Namespaces → Add Namespace → `io.seatlayer`**. The Portal shows a
-verification key, something like `abc123xyz`.
+4. Merge the release commit to `main`, then tag it and push the tag:
-Add it as a TXT record on the apex of the domain:
+ ```bash
+ git tag v0.8.1 && git push origin v0.8.1
+ ```
-```
-seatlayer.io. TXT "abc123xyz"
-```
+The workflow runs `mvn -B verify`, refuses to publish if the tag and the pom
+version disagree, then runs `mvn -B -Prelease deploy`, which signs the jar,
+sources jar, javadoc jar and pom and uploads them to the Central Portal.
-The domain is on Cloudflare, so this is: DNS → Records → Add record → type TXT, name `@`,
-content the verification key. Then press **Verify Namespace** in the Portal.
+## Verify
-Confirm the record is actually visible before pressing verify, or the check fails and you
-wait again:
+The artifact appears on `repo1.maven.org` within minutes; search indexing can lag
+by up to a few hours.
```bash
-dig +short TXT seatlayer.io
+curl -sI https://repo1.maven.org/maven2/io/seatlayer/seatlayer-java/0.8.1/seatlayer-java-0.8.1.pom
```
-**Wall-clock cost.** Cloudflare publishes TXT records in well under a minute, so the DNS
-side is effectively instant. The Portal's own check is usually minutes; first-time
-namespace claims are sometimes queued for Sonatype staff review, which is where the
-multi-hour-to-a-business-day tail comes from. Start this step first — it is the only one
-that can block for a day, and every other step below takes minutes.
-
-Once verified, the namespace covers `io.seatlayer` and everything under it, so the other
-JVM artifacts SeatLayer may publish later need no further verification.
-
-### Why `io.seatlayer` and not `io.github.seatlayer`
-
-`io.github.` is verified by creating a temporary public GitHub repo whose name is
-the verification code — no DNS, no review queue, typically verified in under a minute.
-It is genuinely cheaper *in setup*.
-
-It is still the wrong choice here:
-
-- SeatLayer owns `seatlayer.io`, so the DNS proof is available at zero cost beyond one
- TXT record the owner can add in the Cloudflare dashboard in about a minute.
-- `io.seatlayer` is the coordinate every other SeatLayer surface already advertises. The
- README, the docs site and the published `pom.xml` all say `io.seatlayer`. Changing it
- to `io.github.seatlayer` means the install snippet no longer matches the brand.
-- **The groupId is permanent in practice.** A published artifact's coordinates can never
- be changed, only abandoned and re-published under a new groupId, orphaning everyone on
- the old one. Picking the cheap namespace now to save ten minutes buys a migration later.
-
-The cost difference is one DNS record versus one throwaway repo. Pay the DNS record.
-
----
-
-## 3. GPG key — generate it AND publish it to a keyserver
-
-**This is the step people miss.** Signing locally is not enough. Central fetches your
-*public* key from the public keyserver network to check the signature. A perfectly valid
-signature from a key nobody can find is rejected with a validation error that reads like a
-signing failure, and the usual reaction is to re-sign rather than to distribute the key.
-
-Generate the key:
-
-```bash
-gpg --full-generate-key
-# RSA and RSA, 4096 bits, no expiry (or a long one you will actually track),
-# real name + an @seatlayer.io address
-```
-
-Find its id:
+Then resolve it from a clean local repository in a scratch project:
```bash
-gpg --list-secret-keys --keyid-format=long
-# sec rsa4096/A1B2C3D4E5F6A7B8 2026-08-04 [SC]
-# ^^^^^^^^^^^^^^^^ the key id
+mvn -Dmaven.repo.local=/tmp/m2-clean dependency:get -Dartifact=io.seatlayer:seatlayer-java:0.8.1
```
-
-**Publish the public half** — do all of these, they do not fully sync with each other:
-
-```bash
-gpg --keyserver keyserver.ubuntu.com --send-keys A1B2C3D4E5F6A7B8
-gpg --keyserver keys.openpgp.org --send-keys A1B2C3D4E5F6A7B8
-gpg --keyserver pgp.mit.edu --send-keys A1B2C3D4E5F6A7B8
-```
-
-Then prove it is actually retrievable from somewhere other than your own machine, before
-you rely on it:
-
-```bash
-gpg --keyserver keyserver.ubuntu.com --recv-keys A1B2C3D4E5F6A7B8
-```
-
-Propagation is usually seconds to a few minutes. If you publish and immediately deploy,
-you can lose to that race — wait for a successful `--recv-keys` first.
-
-Back up the secret key somewhere durable. Losing it does not invalidate what is already
-published, but it means future releases are signed by a different key.
-
----
-
-## 4. Credentials — `~/.m2/settings.xml`, never this repo
-
-No secret goes in the repository. The `release` profile reads the passphrase from a
-property or the environment; the token lives in your user-level settings.
-
-`~/.m2/settings.xml`:
-
-```xml
-
-
-
-
- central
- TOKEN_USERNAME_FROM_STEP_1
- TOKEN_PASSWORD_FROM_STEP_1
-
-
-
-
-
- gpg
-
- A1B2C3D4E5F6A7B8
- YOUR_KEY_PASSPHRASE
-
-
-
-
- gpg
-
-
-```
-
-`chmod 600 ~/.m2/settings.xml`.
-
-Prefer not to keep the passphrase on disk at all? Drop the `gpg` profile above and pass it
-per-invocation instead — the plugin also honours `MAVEN_GPG_PASSPHRASE`:
-
-```bash
-export MAVEN_GPG_KEY=A1B2C3D4E5F6A7B8
-read -rs MAVEN_GPG_PASSPHRASE && export MAVEN_GPG_PASSPHRASE
-```
-
-Maven can also encrypt the token password (`mvn --encrypt-password`) if you would rather
-not have it in cleartext.
-
----
-
-## 5. Publish
-
-Confirm signing works and the `.asc` files are produced **before** uploading anything:
-
-```bash
-mvn -Prelease clean verify
-ls target/*.asc
-```
-
-You should see four signatures — jar, sources, javadoc, and the pom. If `target/*.asc` is
-empty, the `release` profile did not activate or GPG did not run; fix that now, because
-Central rejects an unsigned bundle.
-
-Then, the release:
-
-```bash
-mvn -Prelease clean deploy
-```
-
-That is the whole publish command. It builds, tests, signs, bundles and uploads, and
-because `waitUntil=validated` it blocks until Central has finished validating and reports
-any rejection to your terminal rather than silently exiting 0.
-
-**It does not go live yet.** `autoPublish` is `false` on purpose. The deployment lands in
-the Portal as **VALIDATED** and waits. Go to →
-**Deployments**, check the artifact list and the coordinates, and press **Publish**.
-
-Up to that button, a deployment can be dropped and redone freely. After it, see step 7.
-
-Sync to `repo1.maven.org` takes a few minutes; appearing in the search UI can take a few
-hours longer. Both are normal — the artifact is resolvable well before search finds it.
-
----
-
-## 6. Verify it resolves from a clean local repository
-
-Do not verify against your own `~/.m2`; it already has the artifact from the local build
-and will succeed regardless of whether the publish worked. Point Maven at an empty repo:
-
-```bash
-mvn dependency:get \
- -Dartifact=io.seatlayer:seatlayer-java:0.1.0 \
- -Dmaven.repo.local=/tmp/central-check \
- -DremoteRepositories=central::::https://repo1.maven.org/maven2
-```
-
-Then confirm the classified artifacts and signatures are all there too:
-
-```bash
-mvn dependency:get -Dartifact=io.seatlayer:seatlayer-java:0.1.0:jar:sources \
- -Dmaven.repo.local=/tmp/central-check
-mvn dependency:get -Dartifact=io.seatlayer:seatlayer-java:0.1.0:jar:javadoc \
- -Dmaven.repo.local=/tmp/central-check
-curl -sI https://repo1.maven.org/maven2/io/seatlayer/seatlayer-java/0.1.0/seatlayer-java-0.1.0.jar.asc \
- | head -1
-```
-
-Finally, the thing that actually matters — a consumer can compile against it. In an empty
-directory with the README's install snippet as the only dependency, `mvn compile` on a
-one-line file that does `new SeatLayer("sk_test_x")` should resolve and build.
-
----
-
-## 7. What is IRREVERSIBLE
-
-Read this before pressing **Publish**.
-
-- **A released version can never be replaced, edited or deleted.** Not by you, not by
- Sonatype support. `io.seatlayer:seatlayer-java:0.1.0` is that content, permanently. This
- is deliberate: the entire JVM ecosystem assumes Central coordinates are immutable, and
- build reproducibility depends on it.
-- **A bad release is fixed only by superseding it** — publish `0.1.1`. The broken `0.1.0`
- stays downloadable forever. You can mark it deprecated in documentation; you cannot
- remove it.
-- **The groupId is effectively permanent too.** Once consumers depend on
- `io.seatlayer:seatlayer-java`, changing the coordinate strands every one of them on an
- artifact that stops receiving updates.
-- **Before the Publish button, nothing is committed.** A VALIDATED deployment can be
- dropped in the Portal with no trace. That gap is the entire safety margin, which is why
- `autoPublish` is `false` in `pom.xml` — do not set it to `true` for convenience.
-
-So: check the coordinates, the version, and the contents of the three jars in the Portal's
-deployment view. Then publish.
-
----
-
-## Subsequent releases
-
-1. Bump `` in `pom.xml`, update `CHANGELOG.md`, commit, tag.
-2. `mvn -Prelease clean deploy`
-3. Approve in the Portal.
-
-Namespace verification and key distribution are one-time. Only step 5 onward repeats.
diff --git a/pom.xml b/pom.xml
index 91139bc..da7ecae 100644
--- a/pom.xml
+++ b/pom.xml
@@ -137,10 +137,10 @@
central