---
title: "Seasons API"
description: "Create a Fixed Renewable Season catalogue, publish an immutable Plan, run buyer rehearsal, and import incumbent Seat Rights for exact-Plan renewal."
---

> Documentation Index
> Fetch the complete documentation index at: https://docs.seatlayer.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Seasons API

The Seasons API is the organizer catalogue for a **Platform/SDK Fixed Renewable
Season**. Your platform still owns identity, pricing, tax, checkout, payment,
orders, tickets or passes, and refunds. SeatLayer owns seating and inventory:
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](/server-api/how-seat-holds-work/). 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.

> **Public SDK support**
>
> The deployed REST API and all seven maintained server SDKs expose the Seasons
> resource at version `0.7.0`. The JavaScript, React, Vue, and Angular
> `SeasonPicker` packages are released at `0.72.1`. The organizer dashboard
> remains disabled in the production CMS during the controlled dark launch;
> that CMS gate does not disable authenticated API or SDK integrations. React
> Native, Flutter, iOS, and Android do not yet expose a native `SeasonPicker`.

| Server surface | Released package or coordinate |
|---|---|
| REST | `https://api.seatlayer.io/v1/seasons` |
| Node.js | `npm install @seatlayer/server@0.7.0` |
| Python | `pip install seatlayer==0.7.0` |
| PHP | `composer require seatlayer/seatlayer-php:0.7.0` |
| Java | `io.seatlayer:seatlayer-java:0.7.0` |
| Go | `go get github.com/seatlayer/seatlayer-go@v0.7.0` |
| Ruby | `gem install seatlayer -v 0.7.0` |
| .NET | `dotnet add package SeatLayer --version 0.7.0` |

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:

```http title="compatibility preflight"
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

```http title="HTTP request"
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.

`currency` is a child-Event compatibility constraint, not the Season package
currency. SeatLayer never derives the package price from Event category or tier
amounts.

`201` returns `{ season }` with `structureState: "draft"`, `salesState: "closed"`,
`audience: "embed_only"`, and at least one draft Plan. Retry create with the
same [idempotency key](/server-api/idempotency-and-conflicts/) 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:

```bash
npm install @seatlayer/server@0.7.0
```

```ts title="test-mode catalogue and lifecycle"
import { 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

```http title="activate"
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:

```http title="publish"
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:

```http title="open 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:

```http title="close"
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

```http title="mint"
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, React, Vue, or Angular Season picker](/buyer-sdk/seasons)
with that token against `/pub/seasons/:key`. After the picker
returns an opaque handoff, inspect then book from your server:

All four web `SeasonPicker` packages and all seven server SDK families expose
the released Season surface. 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.

`GET /v1/seasons/:key/holds/:operationId` returns the trusted Event, label,
object, category, optional tier, and quantity allocation. It deliberately
contains no `unitPrice`, `currency`, subtotal, or total. The response fixes
`pricingAuthority` to `"host"` and `authoritativeAmountIncluded` to `false` so
the allocation cannot be mistaken for a commercial quote. Calculate the
package amount in your own trusted system, take payment there, then book the
same operation. The booking body accepts only `bookActionId` and `bookingRef`;
commercial fields are rejected instead of silently ignored.

```http title="book"
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:

```http title="cancel"
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.

After one complete rehearsal journey, ask SeatLayer to discover the evidence:

```http title="verify retained rehearsal"
POST /v1/seasons/sea_…/buyer-rehearsals/validate
Authorization: Bearer sk_test_••••••••
```

The request has no body. SeatLayer joins the latest coherent hold, booking, and
cancellation occurrences by their retained operation identities, then requires
one active in-scope webhook subscription to have returned `2xx` for all three.
Operators never paste `sop_…`, `sba_…`, `sca_…`, or `wh_…` values into the
dashboard. A successful result records an immutable audit checkpoint; later
checks still succeed after delivery-attempt retention pruning. `409
rehearsal_incomplete` means the SDK journey or its matching webhook delivery is
not complete yet.

## Incumbent import and renewal offers

**POST /v1/seasons/:key/imports** — Authentication: Secret key
**GET /v1/seasons/:key/imports/:importId** — Authentication: Secret key
**POST /v1/seasons/:key/renewal-offers** — Authentication: Secret key
**GET /v1/seasons/:key/renewal-offers** — Authentication: Secret key
**GET /v1/seasons/:key/renewal-offers/:offerId** — Authentication: Secret key
**POST /v1/seasons/:key/renewal-offers/:offerId/extend** — Authentication: Secret key
**GET /v1/seasons/:key/renewal-offers/:offerId/inspect** — Authentication: Secret key
**POST /v1/seasons/:key/renewal-offers/:offerId/commit** — Authentication: Secret key
**POST /v1/seasons/:key/renewal-offers/:offerId/decline** — Authentication: Secret key
**POST /v1/seasons/:key/renewal-offers/:offerId/release** — Authentication: Secret key

Dry-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.

```http title="import"
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.

```http title="generate offers"
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

**GET /v1/seasons/:key/occurrences** — Authentication: Secret key
**POST /v1/seasons/:key/amendments** — Authentication: Secret key
**GET /v1/seasons/:key/amendments** — Authentication: Secret key
**GET /v1/seasons/:key/amendments/:amendmentId** — Authentication: Secret key
**GET /v1/seasons/:key/reports** — Authentication: Secret key
**GET /v1/seasons/:key/operations** — Authentication: Secret key
**GET /v1/seasons/:key/support-lookups** — Authentication: Secret key
**GET /v1/seasons/:key/outbox** — Authentication: Secret key
**POST /v1/seasons/:key/outbox/:occurrenceId/replay** — Authentication: Secret key

A `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.

```http title="reschedule"
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:

```http title="duplicate"
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"]
}
```

Source: https://docs.seatlayer.io/server-api/seasons/index.mdx
