The Seasons API is the organizer catalogue for a Platform/SDK Fixed Renewable Season. Your platform still owns identity, pricing, checkout, orders, tickets, and refunds. SeatLayer owns occurrence membership, Plan publication, sales control, same-seat holds, incumbent import, and renewal offers.
Buyer access sessions, trusted hold inspection, booking, cancellation, holder
import, and renewal offers are on this page. Browser /pub/seasons/* routes
stay picker-only and are not server-SDK operations. A Seat Right is a durable
fence, not a checkout hold. Opening sales
still requires a recorded buyer rehearsal.
Every route on this page uses a matching sk_test_… or sk_live_… secret key.
Keep that key on your server. The organizer dashboard uses the same domain
rules through a cookie session.
Season secret-key routes have a durable per-key budget of 600 requests per
minute and return 429 rate_limited, Retry-After, and standard rate-limit
headers when exhausted. Browser and dashboard routes use separate budgets.
API map
| Task | Method and route |
|---|---|
| List Seasons | GET /v1/seasons |
| Validate Event / Performance Group compatibility | POST /v1/seasons/validate |
| Create a draft | POST /v1/seasons |
| Read one Season | GET /v1/seasons/:key |
| Rename a draft | PATCH /v1/seasons/:key |
| Discard a draft | DELETE /v1/seasons/:key |
| Activate structure | POST /v1/seasons/:key/activate |
| Close structure | POST /v1/seasons/:key/close |
| Archive a closed Season | POST /v1/seasons/:key/archive |
| Recover lifecycle | GET /v1/seasons/:key/lifecycle/:operationId |
| Add a draft Plan | POST /v1/seasons/:key/plans |
| Read a Plan | GET /v1/seasons/:key/plans/:planKey |
| Publish a Plan | POST /v1/seasons/:key/plans/:planKey/publish |
| Supersede a Plan | POST /v1/seasons/:key/plans/:planKey/supersede |
| Open / pause / resume / end sales | /v1/seasons/:key/sales/open · pause · resume · end |
| Duplicate test configuration to live | POST /v1/seasons/:key/duplicate-to-live |
| Mint / list / revoke browser sessions | /buyer-access-sessions |
| Inspect the opaque Season hold | GET /:key/holds/:operationId |
| Book the hold | POST /:key/holds/:operationId/book |
| Retrieve / cancel a booking | GET /:key/bookings/:actionId · POST /:key/bookings/:actionId/cancel |
| Import incumbent holders | POST /v1/seasons/:key/imports · GET /imports/:importId |
| Generate / list renewal offers | POST /v1/seasons/:key/renewal-offers · GET /renewal-offers |
| Read / inspect / extend an offer | GET /renewal-offers/:offerId · GET /inspect · POST /extend |
| Commit / decline / release | POST /commit · POST /decline · POST /release |
| List occurrences | GET /v1/seasons/:key/occurrences |
| Amend / list amendments | POST /amendments · GET /amendments |
| Reports / operations / support | GET /reports · GET /operations · GET /support-lookups |
| Outbox / replay | GET /outbox · POST /outbox/:occurrenceId/replay |
| Redacted audit / support export | GET /audit · GET /export |
Activation does not publish a Plan. Publication does not open sales. Audience
stays embed_only.
Validate before creation
Preflight is read-only and returns exact incompatible Event identity, field, expected/actual values, and remediation. It accepts direct Event keys, active Performance Group keys, or both:
POST /v1/seasons/validate HTTP/1.1
Authorization: Bearer sk_test_••••••••
Content-Type: application/json
{
"sourcePerformanceGroupKeys": ["pg_spring_series"],
"eventKeys": ["ev_bonus_night"]
}Performance Group sources must be active. Creation freezes the group’s activation identity, member-set hash, activation-spec hash, and exact Event membership on the Plan. Publication refuses a source that has since changed or closed; it never silently adopts new membership.
Create a draft Season
POST /v1/seasons HTTP/1.1
Authorization: Bearer sk_test_••••••••
Idempotency-Key: 2026-series-draft
Content-Type: application/json
{
"name": "2026 series",
"edition": "2026–27",
"eventKeys": ["ev_night_1", "ev_night_2"]
}name is required (1–120 characters). edition is optional. Selection may use
eventKeys, sourcePerformanceGroupKeys, or both. Each fixed Plan must contain
2–20 distinct Events that share organization, workspace, mode, chart, currency,
venue, and timezone. Channel-restricted inventory is excluded from v1. The
seasons module must be enabled.
201 returns { season } with structureState: "draft", salesState: "closed",
audience: "embed_only", and at least one draft Plan. Retry create with the
same idempotency key to replay the
original body.
DELETE /v1/seasons/:key succeeds only for a draft.
Additional immutable fixed Plans may be added only while the Season is still a draft. Every successful addition atomically advances the Season revision and its distinct-Event union identity. Activation fences catalogue membership; after activation, adding/removing occurrences is refused.
Node.js quickstart
Install the current public server SDK:
npm install @seatlayer/serverimport { SeatLayer } from '@seatlayer/server';
const seatlayer = new SeatLayer({
secretKey: process.env.SEATLAYER_TEST_SECRET_KEY!,
});
const compatibility = await seatlayer.seasons.validateSeason({
sourcePerformanceGroupKeys: ['pg_spring_series'],
});
if (!compatibility.valid) throw new Error(compatibility.issues[0]?.remediation);
const { season: draft } = await seatlayer.seasons.createSeason({
name: '2026 series',
edition: '2026–27',
sourcePerformanceGroupKeys: ['pg_spring_series'],
}, { idempotencyKey: '2026-series-draft' });
const active = await seatlayer.seasons.waitForSeasonLifecycle(
await seatlayer.seasons.activateSeason(draft.key, draft.revision),
{ timeoutMs: 30_000 },
);
const firstPlan = active.season.plans[0]!;
await seatlayer.seasons.waitForSeasonLifecycle(
await seatlayer.seasons.publishSeasonPlan(
active.season.key,
firstPlan.key,
active.season.revision,
),
{ timeoutMs: 30_000 },
);The SDK polls only the retained Location / lifecycle operation identity
returned by 202 Accepted, honors Retry-After, and stops at the caller’s
timeout. Use sk_test_… credentials and test Events while integrating.
Activate, publish, and sales
POST /v1/seasons/sea_…/activate
Authorization: Bearer sk_test_••••••••
Content-Type: application/json
{ "expectedRevision": 1 }Activation is structural. The published Plan is a separate machine:
POST /v1/seasons/sea_…/plans/spl_…/publish
Authorization: Bearer sk_test_••••••••
Content-Type: application/json
{ "expectedRevision": 2 }After publication, salesState is still closed. Opening sales:
POST /v1/seasons/sea_…/sales/open
Authorization: Bearer sk_test_••••••••
Content-Type: application/json
{ "expectedRevision": 3 }409 coupled_lifecycle is the expected answer until S04 records buyer
rehearsal readiness. Pause, resume, and end use the same expectedRevision
body on /sales/pause, /sales/resume, and /sales/end.
Close structure only when sales are not open:
POST /v1/seasons/sea_…/close
Authorization: Bearer sk_test_••••••••
Content-Type: application/json
{ "expectedRevision": 4 }Close starts a durable close/drain decision: already accepted operations remain
retrievable and recoverable, while fresh sale work is refused. The returned
operation becomes terminal when the structure reaches closed.
POST /v1/seasons/:key/archive accepts only a closed Season and preserves
sales as closed. GET /v1/seasons/:key/lifecycle/:operationId returns the
activation, close, archive, or Plan-publication operation recorded on the
Season.
Activation, close, archive, Plan publication, and Plan supersession retain one audit checkpoint and one semantic webhook occurrence per immutable action.
Occurrence amendments and operational support
POST /v1/seasons/:key/amendments records either an identity-preserving
reschedule or an explicit replacement/cancellation exception. It never changes
activated Plan membership. Each successful mutation gets an immutable
revision and retains deterministic contractOutcomes and
allocationOutcomes; list and retrieve return those same outcomes after a
coordinator restart.
GET /reports combines coordinator booking/renewal/operation state with D1
allocation projections and receiver-attempt health. delivered means at least
one subscribed receiver returned 2xx; queue enqueue alone is never counted as
delivery. This is an inventory/operations report, not revenue, payment, refund,
attendance, or utilization reporting.
POST /outbox/:occurrenceId/replay enqueues the retained payload with the same
business occurrenceId. It increments replay metadata only after enqueue and
does not mark the occurrence delivered. Receiver deduplication remains required.
GET /support-lookups accepts either bookingRef or holderRef, returns the
correlated inventory records plus the request ID, and makes no refund claim.
GET /audit returns bounded redacted activity. GET /export returns
season-support-export.v1, a bounded JSON snapshot with catalogue identity,
Plans, amendments, allocations, report, and audit; truncated flags disclose
whether either bounded collection has more rows.
Top-level Season listing is cursor-paginated. Occurrences are complete within
the 20-occurrence public contract. Buyer-session history is latest 100.
Renewal-offer and amendment lists return latest 100 plus truncated;
operations return latest 50 plus truncated; outbox returns latest 100 plus
truncated. Report and outbox totals use the full retained aggregate, and an
older omitted outbox occurrence remains replayable by its exact ID.
Buyer session, inspect, book, cancel
POST /v1/seasons/sea_…/buyer-access-sessions
Authorization: Bearer sk_test_••••••••
Content-Type: application/json
{ "allowedOrigin": "https://tickets.example.com", "includePublic": true }201 reveals the bss_… token once. The token is bound to the published Plan
activation, exact Origin, and mode. It is not refreshable. v1 rejects channel
authority on the session body.
Use the JavaScript or React Season picker with that token
against /pub/seasons/:key. After the picker
returns an opaque handoff, inspect then book from your server:
SeasonPicker and the matching Node.js Season resource are the supported SDK
surfaces. A backend integration remains necessary for a Platform checkout even
when organizers configure the Season in the dashboard: the secret-key server
must inspect, price, charge, and book the opaque hold. Other server SDK families
can call the same REST contract but do not yet expose a first-class resource.
POST /v1/seasons/sea_…/holds/sop_…/book
Authorization: Bearer sk_test_••••••••
Content-Type: application/json
{ "bookActionId": "sba_1", "bookingRef": "order_1" }Booking may return 202 Accepted with Location and Retry-After. Poll that
booking URL until state is terminal; replaying the same action is exact.
Cancel the booking with a new caller-stable action while repeating the original booking and published Plan identities:
POST /v1/seasons/sea_…/bookings/sba_1/cancel
Authorization: Bearer sk_test_••••••••
Content-Type: application/json
{
"cancelActionId": "sca_1",
"bookingRef": "order_1",
"planActivationId": "spa_…",
"rightDisposition": "preserve"
}Cancellation requires an explicit preserve or release future-right
disposition. There is no implicit default and no automatic refund. It may also
return 202 with the booking Location and Retry-After; the retrieved
booking includes nested per-occurrence cancellation outcomes. A mixed
nonretryable result is terminal partial_terminal and requires reconciliation,
not a new action or blind rollback.
Incumbent import and renewal offers
/v1/seasons/:key/importsSecret key/v1/seasons/:key/imports/:importIdSecret key/v1/seasons/:key/renewal-offersSecret key/v1/seasons/:key/renewal-offersSecret key/v1/seasons/:key/renewal-offers/:offerIdSecret key/v1/seasons/:key/renewal-offers/:offerId/extendSecret key/v1/seasons/:key/renewal-offers/:offerId/inspectSecret key/v1/seasons/:key/renewal-offers/:offerId/commitSecret key/v1/seasons/:key/renewal-offers/:offerId/declineSecret key/v1/seasons/:key/renewal-offers/:offerId/releaseSecret keyDry-run an incumbent file, then commit the same rows. Invalid or conflicting rows stay explicit and do not create rights. Each accepted row becomes one Contract and one reserved Seat Right, fenced on every included Event.
POST /v1/seasons/sea_…/imports
Authorization: Bearer sk_test_••••••••
Idempotency-Key: 2026-incumbents
Content-Type: application/json
{
"dryRun": false,
"successorPlanActivationId": "spa_…",
"rows": [
{
"rowId": "row_1",
"holderRef": "holder_ada",
"priorPlanActivationId": "spa_prior",
"priorContractRef": "legacy_ada",
"labels": ["A-1"],
"existingBookingRef": "legacy_booking_ada"
}
]
}Generate one time-bounded offer per prior Contract. The offer carries that Contract’s complete seat set and one immutable successor Plan activation.
POST /v1/seasons/sea_…/renewal-offers
Authorization: Bearer sk_test_••••••••
Idempotency-Key: 2026-offers
Content-Type: application/json
{ "successorPlanActivationId": "spa_…", "deadlineAt": 1790000000000 }Browser POST /pub/seasons/:key/renewal-intents records holder intent only. It
requires a buyer-access session bound to the offer’s opaque holder reference;
an unbound or different holder is refused. It is not proof of purchase. After
you inspect the offer, commit with a stable
action id, order/booking references, and the exact Plan activation. A wrong Plan
is 409 plan_activation_mismatch. Commit may return 202 with Location and
Retry-After; poll the offer until terminal. commitOutcomes retains each
Event as pending, committed, or failed. A mixed nonretryable result is
terminal partial_terminal, emits season.operation.partial_terminal, and
requires reconciliation rather than a new commit action or blind rollback.
Decline releases the right set immediately. Release is only valid after
terminal lapse. Extension is explicit, pre-lapse, and must move the deadline
forward; this release does not invent a default extension. Account-specific
organizer bounds and permission policy still apply.
Amendments, reports, and support
/v1/seasons/:key/occurrencesSecret key/v1/seasons/:key/amendmentsSecret key/v1/seasons/:key/amendmentsSecret key/v1/seasons/:key/amendments/:amendmentIdSecret key/v1/seasons/:key/reportsSecret key/v1/seasons/:key/operationsSecret key/v1/seasons/:key/support-lookupsSecret key/v1/seasons/:key/outboxSecret key/v1/seasons/:key/outbox/:occurrenceId/replaySecret keyA reschedule may change Event date-time or name. It does not change Event
keys, Plan activation, or occurrence_set_sha256. Passing eventKeys that
would add or remove occurrences is 409 occurrence_membership_immutable.
replace and cancel_exception record operator-visible exceptions. They do
not silently rewrite the published Plan and they do not refund money.
POST /v1/seasons/sea_…/amendments
Authorization: Bearer sk_test_••••••••
Idempotency-Key: night-1-rain
Content-Type: application/json
{ "eventKey": "ev_night_1", "kind": "reschedule", "startsAt": 1790000000000 }GET /reports totals live inventory from the Season coordinator and catalogue
counts from D1. GET /support-lookups?bookingRef= returns bookings, offers,
and contracts. It never includes a refund. GET /outbox is the missed-event
feed; POST /outbox/:occurrenceId/replay keeps the same payload hash.
Duplicate to live
A test Season can be copied onto new live identities. Sales stay closed. Pass the live Event keys that should replace the test occurrences:
POST /v1/seasons/sea_…/duplicate-to-live
Authorization: Bearer sk_test_••••••••
Idempotency-Key: 2026-series-live
Content-Type: application/json
{
"name": "2026 series",
"eventKeys": ["ev_live_1", "ev_live_2"]
}