---
title: "Events"
description: "Create events from published charts, read live counts, update metadata and chart snapshots, and manage the event lifecycle."
---

An event is an event-owned seating and pricing snapshot plus live inventory.
Create it from a published chart, address that inventory through its event key,
and update the map deliberately when the source chart changes. Dashboard users
can edit an event's normal prices without changing sibling events on the same
chart; existing hold and booking prices remain locked.

For a Platform/API integration, this is an infrastructure Event: it does not
imply a SeatLayer catalogue entry, Hosted Event Page, Organizer Website,
managed checkout, commerce Order, or ticket. Keep those in your platform unless
the organizer deliberately uses the separate Managed Ticketing product.

Every Event is created from a published reserved-seat Chart. GA areas may exist
inside that Chart, but a pure-GA event without a seating chart is not a supported
target. Platform/SDK versus Managed commerce ownership is also fixed when the
Event is created; reuse the Chart and create a new Event to change products.

<Aside type="note" title="Chart and event are different resources">
  Map refresh protects booked and held labels. Chart price changes are separate:
  linked events retain their prices until an organizer explicitly applies the
  new defaults in the dashboard.
</Aside>

## Create an event

<ApiEndpoint method="POST" path="/v1/events" auth="Secret key or dashboard session" />

```bash title="Create an event"
curl -sX POST "https://api.seatlayer.io/v1/events" \
  -H "authorization: Bearer $SEATLAYER_SECRET_KEY" \
  -H "content-type: application/json" \
  -H "idempotency-key: event_order_8271" \
  -d '{
    "chartId": "chart_abc",
    "name": "Opening Night",
    "slug": "opening-night",
    "startsAt": 1767204000000,
    "venue": "Main Hall",
    "region": "western-europe",
    "externalRef": "show_8271",
    "currency": "GBP"
  }'
```

The chart must be published and belong to an active
[workspace](/platform/workspaces/). A live event
requires a positive credit balance; test events do not consume credits.

| Field | Type | Required | Behavior |
|---|---|---|---|
| `chartId` | `string` | Yes | Published source chart |
| `name` | `string` | No | Defaults to chart name |
| `slug` | `string` | No | 3–64 lowercase letters, digits, `_`, or `-`; becomes the event key. Slugs are unique per workspace: when another customer already uses one, a numeric suffix is appended (`opening-night-2`), so always read the key from `meta.key` |
| `startsAt` | `number` | No | Epoch milliseconds |
| `venue` | `string` | No | Venue label |
| `mode` | `"live" \| "test"` | No | Session calls only; a secret key's mode wins |
| `externalRef` | `string` | No | Host identifier, at most 128 characters |
| `currency` | `string \| null` | No | Three-letter event override; otherwise org currency |
| `description` | `string \| null` | No | Event description used by share and hosted surfaces |
| `endsAt` | `number \| null` | No | End time in epoch milliseconds; cannot precede `startsAt` |
| `timezone` | `string \| null` | No | IANA time zone such as `Asia/Kolkata` |
| `region` | `string` | No | One of the 11 [Event regions](/server-api/event-regions/); otherwise the workspace default |
| `locale` | `string \| null` | No | BCP-47 language tag such as `hi-IN` |
| `posterAssetId` | `string \| null` | No | Previously staged dashboard poster asset |

A successful response is `201 {"meta": EventMeta}`. Store `meta.key` beside
your platform's own event ID; it is the inventory key used by the buyer SDK and
server booking calls. A secret-key-created event starts with Platform checkout
authority and live inventory immediately; it does not require event-page
details or SeatLayer ticket releases. Event creation supports
`Idempotency-Key`; retry the same logical create with the same key and body.

Choose `region` when creating the event because its live-inventory Durable
Object is placed on first use and an existing event cannot be moved by changing
later requests. Pick the broad region nearest the **event venue**, not the
developer, API server, or organizer creating it.

For Rotterdam use `western-europe`; for India use `asia-pacific`. Omit `region`
to inherit the chart workspace's default. See the [11-region reference and
resolution rules](/server-api/event-regions/). Placement is a best-effort
latency preference, not a legal data-residency guarantee.

Ticket releases are optional. A Platform integration can keep its own on-sale
windows and pricing policy while using SeatLayer inventory, or manage
event-scoped releases through the API described below.

<Aside type="caution" title="Choose Public or scoped browser access">
  Secret-key-created Platform events are embed-only. For ordinary Public sale,
  mount the Buyer SDK with the event key plus its account's publishable
  `pk_test_…` or `pk_live_…` key. SeatLayer validates the exact registered
  browser origin and returns one Public-only in-memory session together with the
  chart and compact inventory status. Login, presale, partner, and channel
  inventory still require an exact-origin
  [buyer access session](/server-api/buyer-access-sessions) from your backend.
  An explicit buyer token/provider takes precedence over `publicKey`; an event
  key by itself still receives `404`. Secret-key hold and booking calls are
  unchanged.
</Aside>

## List and retrieve

<ApiEndpoint method="GET" path="/v1/events" auth="Secret key or dashboard session" />

Use `workspaceId` to select an active workspace and `externalRef` to find the
event belonging to a host record:

```bash
curl -s \
  "https://api.seatlayer.io/v1/events?workspaceId=ws_main&externalRef=show_8271" \
  -H "authorization: Bearer $SEATLAYER_SECRET_KEY"
```

The response includes `events[]`, with cached metadata, `mode`, `sold`, and live
`counts` where available. A secret key receives only events matching its own
`live` or `test` mode. A dashboard session receives both modes for the selected
workspace. Environment tags do not filter event lists or event access.

Mode filtering happens before `externalRef`, paging, and counts are returned.
When no event in the key's mode matches the other filters, the list returns
`200 {"events":[]}`; list calls do not return `mode_mismatch`. Direct access to
a same-organization event in the opposite mode remains an explicit
`403 {"error":"mode_mismatch"}`. Unknown and other-organization event keys
remain non-disclosing `404` responses.

### Paging

The list is paginated. A response carries at most `limit` events (default 100,
maximum 200) plus a `nextCursor` when more remain; the absence of `nextCursor`
means you have reached the end.

```bash
curl -s "https://api.seatlayer.io/v1/events?limit=50" \
  -H "authorization: Bearer $SEATLAYER_SECRET_KEY"
# → { "events": [ … ], "nextCursor": "MTc1NDA2…" }

curl -s "https://api.seatlayer.io/v1/events?limit=50&cursor=MTc1NDA2…" \
  -H "authorization: Bearer $SEATLAYER_SECRET_KEY"
```

Cursors are opaque — pass back exactly what you were given and do not construct
one. A cursor we cannot read restarts the list from the beginning rather than
erroring, so a stale bookmark degrades instead of breaking. An oversized `limit`
clamps to the maximum rather than being rejected.

<Aside type="tip" title="Turn off counts when walking the whole catalogue">
  Live `counts` are computed per event, which costs one internal round-trip
  each. Pass `counts=0` when you are paging every event and only need metadata:

  ```bash
  curl -s "https://api.seatlayer.io/v1/events?limit=200&counts=0" \
    -H "authorization: Bearer $SEATLAYER_SECRET_KEY"
  ```

  The [server SDKs](/server-sdk/install/) do this for you — their `listAll`
  helpers page transparently and drop counts by default.
</Aside>

`GET /v1/charts` pages the same way, with `limit` and `cursor`.

<ApiEndpoint method="GET" path="/v1/events/:key" auth="Secret key or dashboard session" />

The detail response contains:

```json
{
  "meta": {
    "key": "opening-night",
    "name": "Opening Night",
    "chartId": "chart_abc",
    "status": "active",
    "seatTotal": 1200,
    "mode": "live",
    "workspaceId": "ws_main",
    "externalRef": "show_8271",
    "currency": "GBP",
    "region": "western-europe",
    "sold": 412,
    "inventoryModelVersion": 2
  },
  "counts": {
    "free": 760,
    "held": 8,
    "booked": 412,
    "blocked": 20
  },
  "holdTtlMs": null,
  "chartUpdate": {
    "behind": false,
    "canAutoUpdate": false
  }
}
```

`holdTtlMs: null` means the default 15-minute hold window applies.

## Update metadata and section state

<ApiEndpoint method="PATCH" path="/v1/events/:key" auth="Secret key or dashboard session" />

Send only the fields to change:

```json
{
  "name": "Opening Night — Rescheduled",
  "startsAt": 1767290400000,
  "venue": "Main Hall",
  "externalRef": "show_8271",
  "currency": "GBP",
  "sectionStates": {
    "sec-balcony": "hidden",
    "sec-stalls": "open"
  }
}
```

`startsAt`, `venue`, `externalRef`, and `currency` can be cleared with `null`.
`description`, `endsAt`, `timezone`, and `locale` can also be updated or cleared.
Section and zone states are `open`, `closed`, or `hidden`; unknown ids return
`422`. An empty patch returns `400 nothing_to_update`.

For timed and demand-triggered section release, use
[inventory and availability](/server-api/inventory).

## Ticket releases and on-sale waves

Ticket releases are ordered, event-scoped price windows. Each release can have
a start/end time, category scope, quota, and buyer action. Only `buy` releases
price SeatLayer holds and consume quota; `apply` and `invoice` link to the
organizer's HTTPS workflow and require `actionUrl`.

<ApiEndpoint method="GET" path="/v1/events/:key/releases" auth="Secret key or dashboard session" />

The response returns releases in `position` order. Quota-bearing releases also
include live `consumed` and `remaining` values. `remaining: null` means the
release has no quota.

<ApiEndpoint method="PUT" path="/v1/events/:key/releases" auth="Secret key or dashboard session" />

`PUT` replaces the entire ordered list; it is not a per-release patch. Send at
most 12 releases. Omit `id` for a new release and preserve returned IDs when
editing, because booked and held inventory records the release that priced it.
List order becomes the dense `position` order returned by the server.

```json title="Replace the release list"
{
  "releases": [
    {
      "name": "Early bird",
      "price": 18,
      "previousPrice": 22,
      "quota": 200,
      "startsAt": 1786530600000,
      "endsAt": 1787135400000,
      "action": "buy"
    },
    {
      "name": "Release 2",
      "price": 22,
      "action": "buy"
    }
  ]
}
```

Prices are integer major currency units, matching chart category prices. A
`categoryKey` must exist in the event chart and cannot name a tiered category;
SeatLayer never applies two independent price authorities to one selection.
`startsAt` and `endsAt` are epoch milliseconds. An unscoped release applies to
every non-tiered category.

Whole-list replacement is last-write-wins and does not support automatic
retry or optimistic concurrency. Serialize release editors per event, read the
current list before editing, and confirm the returned list before accepting a
write as complete.

<ApiEndpoint method="POST" path="/v1/events/:key/releases/:id/close" auth="Secret key or dashboard session" />

Closing sets the release's end time to now and returns the full list. It does
not delete the release or erase pricing provenance from existing holds and
bookings. Closing an already closed release is safe, but mutations have no
automatic-retry contract.

## Update an event to the latest chart

Publishing a changed chart reports whether existing events refreshed
automatically or need a manual update. Only pristine events without private
channel allocations can refresh automatically. Events with live state or
allocations require the explicit route:

<ApiEndpoint method="POST" path="/v1/events/:key/update-chart" auth="Secret key or dashboard session" />

```bash
curl -sX POST \
  "https://api.seatlayer.io/v1/events/opening-night/update-chart" \
  -H "authorization: Bearer $SEATLAYER_SECRET_KEY"
```

If the update would drop private channel assignments, inspect the returned
channel summary and repeat deliberately with an audit reason:

```json
{
  "acknowledgeDroppedAssignments": true,
  "reason": "Venue reconfiguration approved by operations"
}
```

If a booked or held label would disappear, the update fails atomically:

```json
{
  "error": "seats_would_vanish",
  "code": "seats_would_vanish",
  "missingLabels": ["A-12"],
  "message": "The update would remove booked or held seats."
}
```

Archived and deleted events are not mutable. Never work around a conflict by
recreating the event; reconcile the affected chart labels and retry.

<Aside type="caution" title="Season-owned Event integrity is a source candidate">
  The current Seasons candidate returns `409 season_event_locked` when a normal
  Event metadata update, archive, or delete would invalidate an active Season
  catalogue or published Plan. The protection is not part of the deployed REST
  baseline yet; the operation-support matrix therefore shows those REST
  contracts as release candidates while retaining the already-published SDK
  methods whose request shape is unchanged.
</Aside>

## Lifecycle

| Operation | Route | Buyer sales | Organizer reads/actions |
|---|---|---|---|
| Close | `POST /:key/close` | Stopped | Available |
| Reopen | `POST /:key/reopen` | Resumed | Available |
| Archive | `POST /:key/archive` | Stopped | Reports remain available |
| Restore | `POST /:key/unarchive` | Stays stopped | Available again |
| Soft delete | `DELETE /:key` | Stopped | Removed from normal event list |

Use close for a reversible pause. Archive after the event ends; SeatLayer writes
a durable inventory snapshot and keeps the live store for reports. Delete is a
soft delete rather than immediate physical erasure. Restoring an archive never
resumes buyer sales implicitly: it returns the event to the active organizer
list with sales paused. Call `reopen` afterwards to run the normal readiness
gate and resume sales deliberately.

Supported application transitions are:

```text
active -> closed -> active
active or closed -> archived
archived -> active (sales remain paused)
active, closed, or archived -> deleted
```

## Errors to handle

| Status | Code | Action |
|---|---|---|
| `402` | `insufficient_credits` | Top up before creating a new live event |
| `404` | `chart_not_found` / `not_found` | Check org, workspace, and resource id |
| `409` | `event_exists` | Your workspace already has an event with this slug: use a new slug or replay the original idempotent request |
| `409` | `seats_would_vanish` | Reconcile protected labels |
| `409` | `season_event_locked` (candidate) | Amend through the Season workflow or wait until the Season no longer owns the Event |
| `422` | `chart_not_published` | Publish the chart first |
| `422` | `invalid_event_inventory` | Fix the chart validation issue |
| `422` | `workspace_not_found_or_disabled` | Select or enable the workspace |

## Verification

- [ ] Event creation has a stable idempotency key.
- [ ] Host records store the returned event key and workspace id.
- [ ] Test and live resources use matching secret-key modes.
- [ ] The event's currency, name, start time, and external reference are checked.
- [ ] Chart updates are tested with booked and held inventory.
- [ ] Close, archive, and delete are explicit operator actions.

Continue to [inventory](/server-api/inventory),
[booking](/server-api/booking), and [reports](/server-api/reports).