Skip to content

Seasons API

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

Updated View as Markdown

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:

compatibility preflighthttp
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 requesthttp
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/server
test-mode catalogue and lifecyclets
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

activatehttp
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:

publishhttp
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:

open saleshttp
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:

closehttp
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

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

bookhttp
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:

cancelhttp
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

POST/v1/seasons/:key/importsSecret key
GET/v1/seasons/:key/imports/:importIdSecret key
POST/v1/seasons/:key/renewal-offersSecret key
GET/v1/seasons/:key/renewal-offersSecret key
GET/v1/seasons/:key/renewal-offers/:offerIdSecret key
POST/v1/seasons/:key/renewal-offers/:offerId/extendSecret key
GET/v1/seasons/:key/renewal-offers/:offerId/inspectSecret key
POST/v1/seasons/:key/renewal-offers/:offerId/commitSecret key
POST/v1/seasons/:key/renewal-offers/:offerId/declineSecret key
POST/v1/seasons/:key/renewal-offers/:offerId/releaseSecret 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.

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

generate offershttp
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/occurrencesSecret key
POST/v1/seasons/:key/amendmentsSecret key
GET/v1/seasons/:key/amendmentsSecret key
GET/v1/seasons/:key/amendments/:amendmentIdSecret key
GET/v1/seasons/:key/reportsSecret key
GET/v1/seasons/:key/operationsSecret key
GET/v1/seasons/:key/support-lookupsSecret key
GET/v1/seasons/:key/outboxSecret key
POST/v1/seasons/:key/outbox/:occurrenceId/replaySecret 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.

reschedulehttp
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:

duplicatehttp
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"]
}
Navigation

Type to search…

↑↓ navigate↵ selectEsc close