Skip to content

Seasons API

Create a Fixed Renewable Season catalogue, publish immutable Plans, 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, 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.

Start with the end-to-end Season integration for the complete lifecycle and responsibility boundary. Use the Season picker for buyer UI; this page owns exact trusted operations and recovery behavior.

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 released secret-key route on this page uses a matching sk_test_… or sk_live_… key. Keep that key on your server. Dashboard availability is a separate release boundary and does not change the public SDK contract.

Released baseline surface Package or coordinate
REST https://api.seatlayer.io/v1/seasons
Node.js npm install @seatlayer/server@0.8.0
Python pip install seatlayer==0.8.0
PHP composer require seatlayer/seatlayer-php:0.8.0
Java io.seatlayer:seatlayer-java:0.8.0
Go go get github.com/seatlayer/seatlayer-go@v0.8.0
Ruby gem install seatlayer -v 0.8.0
.NET dotnet add package SeatLayer --version 0.8.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.

Released 0.8.0 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, or set checkout mode and audience 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.

REST operations beyond the 48

These 32 operations are in REST. No 0.8.0 server SDK includes them; call them over HTTP with your secret key. The last 15 rows are the holder API.

Task Route
Bring holders from a previous season POST /v1/seasons/:key/imports/from-season
List performances (works on a draft) GET /v1/seasons/:key/performances
Create or attach performances in a draft POST /performances · POST /performances/attach
Remove a performance from a draft DELETE /performances/:eventKey
Rename, re-date, or delete a draft Plan PATCH /plans/:planKey · DELETE /plans/:planKey
Publish in one call POST /v1/seasons/:key/publish
Run and list checkout tests POST /checkout-tests · GET /checkout-tests
Set and list package prices PUT /plans/:planKey/prices · GET /prices
Add a performance to a published Plan POST /plans/:planKey/performances
Remove a performance from a published Plan DELETE /plans/:planKey/performances/:eventKey
Mark a performance as taken place POST /performances/:eventKey/past
Return one booked occurrence to ordinary Event inventory POST /occurrences/:eventKey/returns
Reclaim a still-free occurrence return POST /occurrences/:eventKey/reclaims
List holders GET /v1/seasons/:key/holders
Refund, cancel, or cancel one date of a holder’s order POST /orders/:orderId/refund · POST /orders/:orderId/cancel · POST /orders/:orderId/dates/:eventKey/cancel
Email an order’s tickets again POST /orders/:orderId/resend
Renewal links, payments and emails per offer GET /renewal-hosted
New renewal link, holder contact, email again POST /renewal-offers/:offerId/link · POST /contact · POST /resend
Email renewal invitations and reminders POST /renewal-notices
Sales and free-seat reports GET /reports/sales · GET /reports/seats
Holder and order downloads GET /reports/holders.csv · GET /reports/orders.csv
Season seats sold per package and date GET /seat-counts

Seven released operation IDs also have newer shapes in source. Keep these boundaries explicit when writing code against a 0.8.0 package:

Existing operation Copy-safe 0.8.0 boundary Source-preview delta
Create buyer session allowedOrigin, includePublic, optional expiresInSeconds, maxQuantity, buyerRef; the sole published Plan is inferred Request may add planActivationId and purpose
List buyer sessions Optional limit; returns the released session metadata Rows may add purpose, multi-Plan, and adoption metadata
Validate buyer rehearsal No request body; returns one retained rehearsal result Response may add planActivationIds and per-activation evidence
Create renewal offers successorPlanActivationId?, deadlineAt, contractIds? Adds multi-Plan selection semantics and newer response detail
List occurrences No query in the SDK method May accept planActivationId
Create amendment eventKey, kind, optional startsAt and name May accept planActivationId and multi-Plan guards
List amendments No arguments; returns amendments and truncated Rows may include newer multi-Plan outcomes

The version-pinned language references are authoritative for the exact method spelling and request shape. The current OpenAPI document is not a backport into 0.8.0.

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

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 to replay the original body.

DELETE /v1/seasons/:key succeeds only for a draft.

Create new performances in a draft Season

When the Season should create ordinary Events rather than attach existing ones, pass the venue region with the performance request:

create performances near the venuehttp
POST /v1/seasons/sea_123/performances HTTP/1.1
Authorization: Bearer sk_test_••••••••
Content-Type: application/json

{
  "titlePattern": "Autumn night {n}",
  "firstStartsAt": 1792346400000,
  "timezone": "Europe/Amsterdam",
  "repeat": "weekly",
  "count": 3,
  "region": "western-europe"
}

region follows the same 11-region table and first-placement rules as ordinary Event creation. Omit it to use the workspace default; pass asia-pacific for venues in India.

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, a performance joins or leaves a published Plan only through the amendment routes (POST /plans/:planKey/performances and DELETE /plans/:planKey/performances/:eventKey), and each change is recorded as an amendment.

Node.js quickstart

Install the current public server SDK:

npm install @seatlayer/server@0.8.0
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 },
);

This quickstart exercises the released baseline. The multi-Plan and occurrence inventory examples below are a preview, not yet released, and do not identify a published package version.

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. In the multi-Plan candidate, publishing any additional Plan also resets rehearsal readiness. 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 buyer rehearsal readiness is recorded. The released baseline requires one coherent journey. The candidate requires one coherent hold → book → cancel webhook-delivery journey for each currently published Plan activation. 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. The outcomes are durable: list and retrieve always return them.

GET /reports combines live Season state with allocation totals and receiver-attempt health. An occurrence is counted as delivered only after at least one subscribed receiver returns 2xx. This is an inventory/operations report, not revenue, payment, refund, attendance, or utilization reporting.

POST /outbox/:occurrenceId/replay resends the retained payload with the same business occurrenceId. It increments replay metadata once the resend is accepted 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
}

This is the released 0.8.0 request. 201 reveals the bss_… token once and returns the server-inferred planActivationId for the single published Plan, plus Origin, mode, environment, quantity limit, and buyer reference metadata. The token is not a refresh token. v1 requires includePublic: true; a false value does not create a private scope.

Use the JavaScript, React, Vue, or Angular Season picker 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 expose the released browser baseline. All seven server SDKs at 0.8.0 expose the same frozen 48 server operations. Seven of those operation IDs have newer request or response shapes in the current REST/Node source preview; that does not mean the released Python, PHP, Java, Go, Ruby, or .NET methods are absent. Multi-Plan targeting and safe session adoption are a preview, not yet released. Occurrence return and reclaim are deployed in REST; only the unreleased Node.js preview wraps them. A backend integration remains necessary for a Platform checkout: the secret-key server must inspect the opaque hold, apply the host’s trusted price and payment decision, and book it.

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.

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"
}

Season Best Available

When the buyer wants “the best 2 seats for the whole season”, let SeatLayer pick. It chooses seats that are free on every date in the Plan and holds the same labels on each date in one operation.

best availablehttp
POST /pub/seasons/sea_…/best-available
Authorization: Bearer bss_••••••••
Content-Type: application/json

{ "operationId": "sop_7d21", "qty": 2, "categoryKey": "stalls", "requireContiguous": true }

This is a buyer route: it takes the bss_… token from the session you minted above, not a secret key, and counts against the per-IP buyer hold budget in rate limits. It is not a server SDK operation.

Field Required Meaning
operationId Yes Your stable id for this hold, starting with sop_. Resending the same id and body returns the first result
qty Yes 1 to 10 seats. More answers 422 best_available_quantity_exceeded
categoryKey No Pick from one category
zoneId No Pick from one zone
accessibility No Accessibility needs the seats must meet
requireContiguous No true to fail instead of splitting the group

The response is {operation, labels, items, holdId, expiresAt}. 201 means the hold is in place on every date; 202 means it is still being applied, so poll the Location header after Retry-After. Then inspect and book the operation from your server exactly as for a buyer-chosen hold. Performances already marked past are skipped. 409 conflict with reason means no set of seats is free on every date; 409 operation_mismatch means the same operationId was sent with a different body.

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 the released-baseline journey, or one journey per published Plan in the candidate, ask SeatLayer to discover the retained evidence:

verify retained rehearsalhttp
POST /v1/seasons/sea_…/buyer-rehearsals/validate
Authorization: Bearer sk_test_••••••••

The released request has no body. SeatLayer joins 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. The 0.8.0 response contains one rehearsal result. In the multi-Plan source preview, proof is required independently for every published activation and the response adds planActivationIds plus activationEvidence; those fields are not part of the frozen baseline. A later preview Plan publication invalidates aggregate readiness until the new Plan has its own journey. 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/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.

Bring holders from last season

POST/v1/seasons/:key/imports/from-seasonSecret key

Instead of building an import file, point at a previous season of the same account and mode. SeatLayer takes every holder who still has a seat on at least one date of that season and runs them through the same checks as POST /imports. Test bookings, refunded or cancelled orders, holders with more seats than one right can hold, and holders without a usable reference are left out and counted in excluded.

dryRun is true unless you send false. Look at the dry run first, then send the same body with "dryRun": false and a new Idempotency-Key.

dry runhttp
POST /v1/seasons/sea_2027…/imports/from-season
Authorization: Bearer sk_live_••••••••
Idempotency-Key: bring-2026-holders-check
Content-Type: application/json

{ "sourceSeasonId": "sea_2026…", "successorPlanActivationId": "spa_…", "dryRun": true }
200 dry runjson
{
  "import": {
    "sourceSeasonId": "sea_2026…",
    "successorPlanActivationId": "spa_…",
    "dryRun": true,
    "holders": 3,
    "acceptedCount": 2,
    "alreadyImported": 0,
    "rejectedCount": 1,
    "withEmail": 1,
    "hosted": 1,
    "integrator": 2,
    "excluded": { "refunded": 1, "rehearsal": 1, "tooManySeats": 0, "unusableRef": 0 },
    "importIds": ["sim_…"],
    "rejected": [{ "holderRef": "booking_pia_2026", "labels": ["B-4"], "errorCode": "unknown_seat" }],
    "preview": [
      { "holderRef": "ord_…", "holderName": "Hanna Holder", "hasEmail": true, "labels": ["A-2", "A-3"] },
      { "holderRef": "booking_ann_2026", "holderName": null, "hasEmail": false, "labels": ["A-1"] }
    ]
  }
}

The commit answers 201 with the same shape. A holder is refused with unknown_seat when a seat is not on the new plan and seat_conflict when someone else already holds it. Running it again is safe: a holder already brought across counts under alreadyImported, never twice. holderRef is the order id for a SeatLayer checkout sale and the bookingRef your system booked with otherwise. Holders who bought through SeatLayer checkout come with their name and email, so SeatLayer can email their renewal link; holders your own system booked come without an email. Read each import with GET /v1/seasons/:key/imports/:importId, using the ids in importIds.

Error Meaning
404 source_season_not_found No season with that key in this account and mode
409 source_too_large Too many holders to bring across in one go
409 season_not_active · 409 plan_not_published Activate this season and publish the package first
422 invalid_source_season · 422 invalid_successor_plan A key is missing, or the source is this season

Return or reclaim one occurrence

Return the exact labels from one active booked Season allocation to ordinary Event inventory without cancelling the holder’s complete Season contract:

return one occurrencehttp
POST /v1/seasons/sea_…/occurrences/ev_night_2/returns
Authorization: Bearer sk_test_••••••••
Content-Type: application/json

{
  "actionId": "sor_order_1_night_2",
  "bookingRef": "order_1",
  "planActivationId": "spa_…",
  "labels": ["A-1"],
  "source": { "bookingActionId": "sba_1" }
}

For a renewed right, use { "renewalOfferId": "sro_…" } as source instead. SeatLayer resolves the allocation for you; callers never send a child hold, internal allocation, or Seat Right ID. actionId owns exact replay. Reusing it with any changed Event, Plan, source, booking reference, or label set returns 409 occurrence_action_mismatch.

A successful return emits season.occurrence.returned and makes only that Event’s labels available for ordinary sale. Reclaim succeeds only while the same return remains active and every label is still free, unheld, unbooked, and unclaimed by another Season allocation:

unreleased Node.js previewts
const returned = await seatlayer.seasons.returnSeasonOccurrenceInventory(
  "sea_…",
  "ev_night_2",
  {
    actionId: "sor_order_1_night_2",
    bookingRef: "order_1",
    planActivationId: "spa_…",
    labels: ["A-1"],
    source: { bookingActionId: "sba_1" },
  },
);

await seatlayer.seasons.reclaimSeasonOccurrenceInventory(
  "sea_…",
  "ev_night_2",
  {
    actionId: "src_order_1_night_2",
    returnActionId: returned.occurrenceInventoryAction.returnActionId,
    bookingRef: "order_1",
    planActivationId: "spa_…",
    labels: ["A-1"],
    source: { bookingActionId: "sba_1" },
  },
);

Reclaim emits season.occurrence.reclaimed. A preserve cancellation is refused while that booking has an active occurrence return; a full release cancellation does not seize independently held or resold Event inventory. Neither operation prices a resale, moves money, refunds a buyer, creates a listing, or issues a ticket. Those commerce decisions remain entirely in the host platform.

Holders, orders, renewal emails, and reports

These are the dashboard’s Holders and Reports tabs for your own server. Each call does the same work as the matching dashboard button. The order calls act on orders SeatLayer took: SeatLayer checkout and the box office. A season sold through your own checkout has no SeatLayer orders; your system refunds those.

  • REST today; server SDK methods arrive in the next SDK release. No released server SDK has these calls yet, so call the routes over HTTP. The two CSV downloads stay HTTP-only.
  • Use a secret key (sk_live_… or sk_test_…). It reaches the seasons of its own account and mode. Another account’s season, or a live season from a test key, is 404 not_found.
  • A publishable key gets 403 publishable_key_not_allowed. A dashboard login cookie gets 401.
  • These calls share the 600-a-minute budget of every Season route.
  • Each answer carries only the fields shown here. Internal ids, the payment provider’s refund id, and staff notes are never in them. A package is named by its plan key (spl_…), the plans[].key of GET /v1/seasons/:key.

List holders

GET/v1/seasons/:key/holdersSecret key

Everyone who holds this season: SeatLayer checkout and box office buyers (hosted), bookings your system made (integrator), and holders brought over from last season (imported).

Query Meaning
q Search name, email, reference, order number, or seat
status active, cancelled, or renewal
limit 1–100, default 50
cursor nextCursor from the page before
200json
{
  "holders": [
    {
      "id": "hld_6c0edf602c0a948627fec31675ee011e",
      "kind": "hosted",
      "bookingRef": "ord_ea11…",
      "orderId": "ord_ea11…",
      "orderNumber": "#1098 5206",
      "holderName": "Hanna Holder",
      "holderEmail": "hanna@example.com",
      "holderRef": null,
      "labels": ["A-2", "A-3"],
      "packageName": "Full season",
      "packageId": "spl_97d1…",
      "dates": [
        { "eventKey": "ev_3709…", "state": "active" },
        { "eventKey": "ev_1eff…", "state": "active" },
        { "eventKey": "ev_80b7…", "state": "active" }
      ],
      "datesTotal": 3,
      "datesActive": 3,
      "datesCancelled": 0,
      "paid": { "amountMinor": 18000, "currency": "GBP", "refundedMinor": 0 },
      "soldAtCounter": false,
      "orderStatus": "confirmed",
      "status": "active",
      "renewal": null,
      "boughtAt": 1790220682247
    }
  ],
  "nextCursor": "eyJwIjoiYiIs…",
  "totals": { "holders": 3, "active": 2, "cancelled": 0, "imported": 1 }
}

totals comes with the first page only. A page can hold fewer rows than limit and still have a nextCursor; keep reading until it is null. id is stable but opaque: use orderId, bookingRef, or holderRef to match your records. status is one of active, partly_cancelled, cancelled, needs_review, renewal_open, renewed, declined, and lapsed.

Refund an order

POST/v1/seasons/:key/orders/:orderId/refundSecret key

Pays money back through the payment provider that took it. Leave out amountMinor to refund what is left; a full refund also cancels every date, so the seats go back on sale and the tickets stop working. A partial refund keeps the seats and tickets.

refund part of an orderhttp
POST /v1/seasons/sea_a245…/orders/ord_2596…/refund
Authorization: Bearer sk_live_••••••••
Idempotency-Key: refund-ord_2596-1
Content-Type: application/json

{ "amountMinor": 5000 }
200json
{
  "orderId": "ord_2596…",
  "refundId": "rfo_1979…",
  "amountMinor": 5000,
  "currency": "GBP",
  "state": "succeeded"
}

Idempotency-Key is required: without it the call is 400 idempotency_key_required and nothing moves. refundId is SeatLayer’s refund (rfo_…), built from your account and the key. Send the same key again and you get the same answer; the dashboard sending that key reaches the same refund. A 202 with "state": "pending" means the provider has not confirmed yet: retry with the same key to read it.

Error Meaning
409 not_refundable A box office sale (code: box_office_sale) or an order with no payment to reverse
409 already_refunded · 409 refund_failed · 409 gateway_not_connected Nothing left, the provider said no (detail says why), or payments are disconnected
422 invalid_amount Not a whole number above zero, or more than outstanding
502 refund_recovery_required The provider may have taken it; retry with the same key

Cancel an order without a refund

POST/v1/seasons/:key/orders/:orderId/cancelSecret key

For money handled outside SeatLayer: a lost payment dispute, or a refund made another way. Every ticket stops working and every date goes back on sale. Idempotency-Key is required.

cancelhttp
POST /v1/seasons/sea_a245…/orders/ord_ea11…/cancel
Authorization: Bearer sk_live_••••••••
Idempotency-Key: cancel-ord_ea11
Content-Type: application/json

{ "reason": "refunded_elsewhere", "refundConfirmed": true, "notifyBuyer": false }
200json
{
  "receipt": {
    "orderId": "ord_ea11…",
    "displayRef": "#1098 5206",
    "status": "refunded",
    "currency": "GBP",
    "settledOutsideMinor": 18000,
    "released": ["A-2", "A-3"],
    "seatsReleased": true,
    "emailQueued": false,
    "cancelledAt": 1790220682345
  }
}

reason is lost_dispute, refunded_elsewhere, or other; note is kept for your team and never returned. Send usedTicketsAcknowledged: true when a ticket was already scanned. A box office order is 409 box_office_order_use_dashboard: staff cancel it at the counter after giving the money back.

Cancel one date

POST/v1/seasons/:key/orders/:orderId/dates/:eventKey/cancelSecret key

Takes back one date of an order. That date’s tickets stop working first, then its seats go back on sale; every other date stays. No money moves: refund the date separately if you need to. Idempotency-Key is required; the body is optional.

cancel one datehttp
POST /v1/seasons/sea_a245…/orders/ord_ea11…/dates/ev_3709…/cancel
Authorization: Bearer sk_live_••••••••
Idempotency-Key: cancel-ord_ea11-night-1
200json
{
  "dateCancel": {
    "eventKey": "ev_3709…",
    "ticketsVoided": 2,
    "labels": ["A-2", "A-3"],
    "state": "returned"
  }
}

A 202 with "state": "pending" means the tickets are void and the seats are on their way back; retry with the same key. 409 date_already_used means someone was admitted with a ticket for that date. 409 date_not_held or 409 conflict means the date was already taken back.

Email the tickets again

POST/v1/seasons/:key/orders/:orderId/resendSecret key

Sends the season confirmation with every live ticket. Pass email to send it to a new address, which also replaces the one on the order.

200json
{ "sentTo": "hanna@example.com" }

One order can be resent 3 times an hour (429 rate_limited). 409 no_tickets means the order has no live tickets, and 409 no_buyer_email means a walk-up buyer gave no address: send one in email.

GET/v1/seasons/:key/renewal-hostedSecret key
POST/v1/seasons/:key/renewal-offers/:offerId/linkSecret key
POST/v1/seasons/:key/renewal-offers/:offerId/contactSecret key
POST/v1/seasons/:key/renewal-noticesSecret key
POST/v1/seasons/:key/renewal-offers/:offerId/resendSecret key

With SeatLayer checkout, holders renew and pay on a SeatLayer page, from a link SeatLayer emails them. GET /renewal-hosted shows where each offer stands and whether payments can be taken:

200json
{
  "checkoutMode": "seatlayer",
  "readiness": { "ready": true, "problem": null },
  "holdersWithoutEmail": 1,
  "offers": [
    {
      "offerId": "sro_be78…",
      "holderName": "Ana Silva",
      "holderEmail": "ana@example.com",
      "linkActiveSince": null,
      "paymentActive": false,
      "notices": {
        "invitedAt": null,
        "lastReminderAt": null,
        "nextNoticeAt": null,
        "nextNoticeKind": null,
        "problem": null
      }
    }
  ]
}

readiness.problem is own_checkout, prices_missing, or a payments problem when holders cannot pay yet.

  • POST /renewal-offers/:offerId/link answers 201 with { "url": "…/r/…" }. The link is shown once, and making a new one turns the previous link off, so do not retry this call blindly: each call makes another link.
  • POST /renewal-offers/:offerId/contact with { "email", "name" } answers { "contact": { "name": "Ana Silva", "email": "ana@example.com" } }. A new email turns the current link off.
  • POST /renewal-notices emails the invitation now, then reminders 7 days and 1 day before the deadline. Send { "offerIds": […] } to pick holders, or no body for every open offer. It answers { "scheduled": 2, "withoutEmail": 1 }.
  • POST /renewal-offers/:offerId/resend sends one holder a new invitation and answers 202 with { "ok": true }. The old link stops working. A holder with no email is 422 holder_email_missing.

Emails need SeatLayer checkout: on your own checkout, notices and resend are 409 checkout_mode_not_seatlayer, and your system tells holders.

Reports and downloads

GET/v1/seasons/:key/reports/salesSecret key
GET/v1/seasons/:key/reports/seatsSecret key
GET/v1/seasons/:key/reports/holders.csvSecret key
GET/v1/seasons/:key/reports/orders.csvSecret key
GET/v1/seasons/:key/seat-countsSecret key

GET /reports/sales is the Reports tab in figures: money per package, channel (online or box_office) and currency, seats per package and per date, holders, and renewals. It holds totals only, never a buyer.

200 (trimmed)json
{
  "report": {
    "season": { "key": "sea_a245…", "name": "Holders", "currency": "GBP", "mode": "live", "checkoutMode": "seatlayer", "timezone": null },
    "generatedAt": 1790220682369,
    "ownCheckout": false,
    "money": {
      "rows": [
        {
          "packageId": "spl_97d1…", "packageName": "Full season", "channel": "online", "currency": "GBP",
          "orders": 1, "refundedOrders": 0, "seats": 2,
          "grossMinor": 18000, "discountMinor": 0, "refundedMinor": 0, "netMinor": 18000
        }
      ]
    },
    "seats": { "sold": 3, "byPackage": [{ "packageId": "spl_97d1…", "packageName": "Full season", "seats": 3, "holders": 2 }], "byDate": [] },
    "holders": {
      "total": 3, "active": 2, "cancelled": 0, "imported": 1, "importedRenewed": 0,
      "soldThroughSeatLayer": 1, "soldThroughYourCheckout": 1,
      "byStatus": { "all": 3, "active": 2, "cancelled": 0, "renewal": 1 }
    },
    "renewals": { "offered": 0, "renewed": 0, "declined": 0, "lapsed": 0, "open": 0, "needsReview": 0, "rate": null },
    "downloadLimit": 50000,
    "unavailable": []
  }
}

A section that could not be read right now is null and named in unavailable; it is never a wrong zero. GET /seat-counts returns the same seats block on its own, and null when it cannot be read.

GET /reports/seats counts free seats, per package on every one of its dates still to come, and per date:

200json
{
  "seats": {
    "packages": [
      {
        "packageId": "spl_97d1…", "packageName": "Full season", "datesLeft": 3, "state": "ok",
        "freeEveryDate": 7, "notEveryDate": 0, "renewalHeld": 1
      }
    ],
    "dates": [{ "eventKey": "ev_3709…", "free": 8 }]
  }
}

The two downloads answer text/csv, the same files as the dashboard. They are plain HTTP calls, not SDK methods.

  • holders.csv has 17 columns: Name, Email, Reference, Your reference, Seats, Package, Dates active, Paid, Refunded, Currency, Method, Sold through, Order status, Holder status, Renewal state, Renewal deadline, Bought. It takes the same q and status as the holder list. Past 50,000 holders it is 413 download_too_large: filter by status.
  • orders.csv has every order SeatLayer took for the season. A season on your own checkout has none: 409 season_sold_elsewhere.

Webhooks for these calls

Call Webhook
A SeatLayer checkout or box office sale is paid season.order.paid
Refund (full or partial) season.order.refunded; a full refund also sends season.booking.cancelled
Cancel an order without a refund season.booking.cancelled
Cancel one date season.occurrence.returned

There is no webhook yet for a ticket resend, a renewal email being sent, or a contact change. See Webhook events for the payloads.

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. In the multi-Plan source preview, list occurrences with the exact planActivationId query and include that activation in amendment requests when more than one Plan is published; omission is 409 plan_selection_required. Because Event schedule metadata is global, an Event shared by multiple published Plans fails closed with 409 occurrence_shared_across_plans until every affected activation can be retained in one amendment audit record. Do not infer this multi-Plan request shape from a released 0.8.0 package.

Occurrence rows include nullable latestAmendment identity, kind, and classification. Amendment list rows retain the full activation, occurrence set, before/after state, contract outcomes, and allocation outcomes; they are not reduced summaries.

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
}

That is the 0.8.0 request. Only the multi-Plan source preview adds planActivationId to amendment creation.

GET /reports totals live inventory and catalogue counts. 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 resends the same payload.

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