Metadata-Version: 2.5
Name: seatlayer
Version: 0.9.0
Summary: Official Python server SDK for the SeatLayer reserved-seating API: seating charts, seat maps, events, holds, seat booking and webhooks.
Project-URL: Homepage, https://seatlayer.io
Project-URL: Documentation, https://docs.seatlayer.io/server-sdk/install/
Project-URL: Changelog, https://github.com/seatlayer/seatlayer-python/blob/main/CHANGELOG.md
Project-URL: Source, https://github.com/seatlayer/seatlayer-python
Project-URL: Issues, https://github.com/seatlayer/seatlayer-python/issues
Author-email: SeatLayer <hello@seatlayer.io>
Maintainer-email: SeatLayer <hello@seatlayer.io>
License-Expression: MIT
License-File: LICENSE
Keywords: box-office,event-ticketing,python,reserved-seating,seat-booking,seat-map,seating-chart,seatlayer,server-sdk,ticketing
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3 :: Only
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Office/Business
Classifier: Topic :: Software Development :: Libraries
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Classifier: Typing :: Typed
Requires-Python: >=3.10
Provides-Extra: dev
Requires-Dist: mypy>=1.11; extra == 'dev'
Requires-Dist: pytest>=8; extra == 'dev'
Requires-Dist: ruff>=0.6; extra == 'dev'
Requires-Dist: typing-extensions>=4.0; extra == 'dev'
Description-Content-Type: text/markdown

# SeatLayer Python Server SDK for Reserved Seating

[![CI](https://github.com/seatlayer/seatlayer-python/actions/workflows/ci.yml/badge.svg)](https://github.com/seatlayer/seatlayer-python/actions/workflows/ci.yml)
[![PyPI](https://img.shields.io/pypi/v/seatlayer.svg)](https://pypi.org/project/seatlayer/)
[![Python](https://img.shields.io/pypi/pyversions/seatlayer.svg)](https://pypi.org/project/seatlayer/)
[![License: MIT](https://img.shields.io/badge/license-MIT-111827.svg)](LICENSE)

The official Python client for the SeatLayer API. The `seatlayer` package lets a Python backend inspect seat holds, price orders from server data, book reserved seats and verify webhooks, with no runtime dependencies. SeatLayer is seating chart and reserved-seat ticketing software built for venues up to stadium scale.

SeatLayer's official Python server SDK is the trusted side of its reserved seating and seat
booking API. Inspect what a hold really contains, price from server-owned seating-chart data,
and book with a stable `booking_ref`, while managing charts, events, inventory, allocations,
and webhooks through one typed ticketing API client.

[`seatlayer` on PyPI](https://pypi.org/project/seatlayer/) ·
[Python server SDK guide](https://docs.seatlayer.io/server-sdk/python/) ·
[SeatLayer SDK and API overview](https://seatlayer.io/developers/) ·
[SeatLayer JavaScript seat map SDK](https://www.npmjs.com/package/@seatlayer/js) ·
[Server API reference](https://docs.seatlayer.io/server-api/events/)

> **Server-side only.** This package authenticates with your secret key. Never run it anywhere a
> ticket buyer can reach. Browser surfaces get short-lived, origin-bound tokens that you mint here.

Two-step shape: the buyer picks and holds seats in the client with your public key, then this SDK confirms the booking from your server with your secret key while your platform keeps checkout and its own payment provider.

**Start here:** [Quickstart](https://docs.seatlayer.io/start/quickstart/) · [Holds and checkout](https://docs.seatlayer.io/buyer-sdk/holds-and-checkout/) · [Python server SDK guide](https://docs.seatlayer.io/server-sdk/python/) · [SDK catalog](https://docs.seatlayer.io/sdk-catalog.json) · [Pricing](https://seatlayer.io/pricing/): $0 entry, 100 free confirmed-sold-seat credits per organization each month, then $0.10 down to $0.05 a credit, and credits never expire.

## Scale evidence

Benchmarked on public 100,000-, 150,000- and 200,000-seat venue fixtures on 15 September 2026: 200,000 seats chart-ready in 1.95 s, desktop, local production build. Fixtures, method and run logs: https://github.com/seatlayer/seatlayer-performance. Live 200,000-seat stadium demo: https://app.seatlayer.io/demo/play/century-stadium-200k. This is renderer evidence, not a concurrent-buyer claim.

## Install the Python seat booking SDK

```bash
pip install seatlayer
```

Requires Python 3.10 or newer. No runtime dependencies.

## Quick start

```python
import os
from seatlayer import SeatLayer

seatlayer = SeatLayer(os.environ["SEATLAYER_SECRET_KEY"])

# 1. Provision a venue from the public template catalog as a new draft chart.
# Replace this placeholder with a template id from your catalog.
chart = seatlayer.templates.instantiate_template("your-published-template")["meta"]
seatlayer.charts.publish(chart["id"])

# 2. Create an event on it.
event = seatlayer.events.create(
    chart_id=chart["id"], name="Spring Gala",
    currency="EUR",  # omit to inherit the workspace currency
    region="western-europe",  # India: "asia-pacific"
)["meta"]

# 3. Sell four seats over the phone.
held = seatlayer.inventory.hold_best_available(event["key"], qty=4)
# … take payment against held["items"], which carry authoritative prices …
seatlayer.inventory.book(event["key"], hold_id=held["holdId"], booking_ref="order-8842")
```

## Event hosting region

Pass `region` to `events.create` based on the **event venue**, not your API server or office.
It controls the initial placement of the Event's live inventory; an existing Event
cannot be moved later. Omit it to inherit the workspace default (`western-europe` for new accounts).
Set that default with `workspaces.create(..., default_region=...)` or
`workspaces.update(workspace_id, default_region=...)`; changing it affects only future Events.

- `western-europe`, `eastern-europe`, `north-america-east`, `north-america-west`, `south-america`
- `asia-pacific`, `northeast-asia`, `southeast-asia`, `oceania`, `africa`, `middle-east`

The hint is best effort, not a data-residency guarantee. See the
[full Event region guide](https://docs.seatlayer.io/server-api/event-regions/).

Nullable event-create fields distinguish omission from an explicit reset: passing, for example,
`venue=None` sends JSON `null`; leaving `venue` out sends no field.

## Test vs live

Keys carry their own mode. `sk_test_…` keys can only touch test-mode events and `sk_live_…` only
live ones; crossing them returns `403 mode_mismatch`, surfaced as `SeatLayerAuthError` with
`is_mode_mismatch`.

## Fixed Renewable Seasons

`seatlayer.seasons` exposes all 87 trusted organizer operations: catalogue and packages,
performances, package prices (per price type: `adult`, `child`, `concession`, `senior`,
`student`), sales, checkout tests, buyer access and booking, holder imports, holders, season
orders, renewals, hosted renewals and renewal checkouts, occurrences (postpone and cancel a date), bulk refunds of a
cancelled date, the release of lapsed renewal offers and their jobs, reports, CSV exports, audit, outbox, and support export.

After the test hold/book/cancel journey and matching webhook deliveries,
`validate_season_buyer_rehearsal(season_key)` sends no evidence body; SeatLayer
discovers the retained chain automatically. Retrieved Season holds contain
inventory identity, not an authoritative amount. When you run your own checkout,
your platform owns package price, payment, order, tax, refunds, benefits, and
ticket or pass delivery.

```python
checked = seatlayer.seasons.validate_season(
    source_performance_group_keys=["pg_subscription_run"],
)
draft = seatlayer.seasons.create_season(
    "2027 subscription",
    source_performance_group_keys=["pg_subscription_run"],
    idempotency_key="season-create-2027",
)["season"]
activation = seatlayer.seasons.activate_season(draft["key"], draft["revision"])
```

Treat `202` as accepted work: retain the returned `lifecycleOperation.operationId`
and poll `retrieve_season_lifecycle`. Buyer sessions are show-once and never
retried invisibly. Booking, cancellation, and renewal actions require caller-stable
identities. Only header-replay mutations retry automatically.

```python
seatlayer = SeatLayer(os.environ["SEATLAYER_SECRET_KEY"])
if os.environ.get("ENV") == "production" and seatlayer.mode != "live":
    raise RuntimeError("Refusing to boot production against test-mode seating data.")
```

When SeatLayer takes the season's payments, renew a holder with no hosted page: read the price
with `retrieve_season_renewal_quote`, then start a checkout and send the holder to
`payment["url"]`. For a renewal taken by phone or at the box office, record the holder's intent
with `create_season_renewal_intent` before you commit the offer. A decline can carry the holder's
reason.

```python
quote = seatlayer.seasons.retrieve_season_renewal_quote(season_key, offer_id)
if quote["problem"] is None:
    started = seatlayer.seasons.create_season_renewal_checkout(
        season_key,
        offer_id,
        "https://tickets.yourclub.example/renewed",
        idempotency_key=f"renew-{offer_id}",
    )
    redirect(started["payment"]["url"])

seatlayer.holders.decline_holder_renewal_offer("holder_123", offer_id, reason="Moving away")
```

### Season seats

Season seats are the seats a season sets aside on every date before its sales open. A season
buyer's hold then doesn't have to ask each date, and single-ticket buyers see those seats as
unavailable. They are off for every season until you pass `inventory_mode="block"` to
`create_season` or `update_season`. The season needs one package, and its sales must not have
opened.

```python
seatlayer.seasons.update_season(season_key, expected_revision=revision, inventory_mode="block")

job = seatlayer.seasons.set_aside_season_block(
    season_key,
    expected_revision=revision,
    keep_for_single_tickets={"sectionIds": ["sec_family"]},
)["job"]
while job["state"] in ("planning", "running"):
    time.sleep(2)
    job = seatlayer.seasons.retrieve_season_bulk_job(season_key, job["jobId"])["job"]
block = seatlayer.seasons.retrieve_season_block(season_key)["block"]

# Release unsold season seats to single sale. Leave out `at` to release now.
seatlayer.seasons.release_season_block(
    season_key,
    expected_revision=block["revision"],
    scope={"all": True},
    at="2026-12-01T09:00:00Z",
)
```

- On an active season, block mode turns on by itself once every date has the seats, and
  `season.block.ready` is sent once. On a draft, `publish_season` turns it on.
  `season.block.released` is sent once a release has run.
- In block mode, a date cancel (`create_season_amendment`, kind `"cancel"`) or
  `mark_season_performance_past` first takes the date off every season seat. Until that finishes
  the answer is a `202`: check it with `seatlayer.is_season_date_leave_pending(answer)`, then send
  the same request again after `Retry-After`.
- Refusals come through `SeatLayerError` with `code`, and nothing changes. For example
  `409 season_block_packages` (more than one package), `409 season_block_on_sale` (sales have
  opened), or `409 block_not_ready` from `open_season_sales` before block mode is on.
- A partner's seat on a date (one a sales channel sells there) stays out of the block. It is
  listed in `block["notFreeOnEveryDate"]` with reason `channel`.

The [Season seats docs](https://docs.seatlayer.io/server-api/seasons/#season-seats) list every refusal.

## Holders, orders, sales settings and event series

Three more resources sit next to `seasons`: `holders` (season ticket holders, their tickets and
transfers, and `list_customers`), `orders` (orders SeatLayer took through its own checkout, across
events and seasons, and one event's orders), and `event_series` (runs of dated events made from one
schedule). An event's sales settings (checkout choice, payment gateway, sales readiness and
transitions, booking fee, discount codes) are on `events`. Method names are the snake_case
operation ids, as on `seasons`.

```python
holders = seatlayer.seasons.list_season_holders("sea_2027", status="active", limit=50)
tickets = seatlayer.holders.list_holder_tickets("holder_123", season_key="sea_2027")

orders = seatlayer.orders.list_orders(season_key="sea_2027", status="confirmed")
seatlayer.orders.refund_event_order("ev_1", "ord_1", idempotency_key="refund-ord_1")
csv_text = seatlayer.orders.export_event_orders_csv("ev_1")   # the CSV as a str

series = seatlayer.event_series.create_event_series(
    "Hamlet",
    "chart_1",
    {"rule": "weekly", "from": "2026-11-03", "count": 6, "times": ["19:30"]},
    timezone="Europe/London",
    dry_run=True,   # answer the dates, create nothing
)
```

The three CSV exports (`seasons.export_season_holders_csv`, `seasons.export_season_orders_csv`,
`orders.export_event_orders_csv`) return the CSV text. `seasons.delete_season_plan` and
`events.delete_event_discount_code` return `None`. If you run your own checkout, your store stays
the order record; these order operations cover sales SeatLayer took.

## Book reserved seats from Python

**Buyer picks seats in the browser.** Your frontend holds them; your backend confirms the price and
books. Never price from what the browser sent you: `retrieve_hold` is authoritative.

```python
hold = seatlayer.inventory.retrieve_hold(event_key, hold_id)
currencies = {item["currency"] for item in hold["items"]}
if len(currencies) != 1:
    raise ValueError("A hold must use one currency")
currency = currencies.pop()
total = sum(item["unitPrice"] * item.get("quantity", 1) for item in hold["items"])
# … charge `total` in `currency` …
seatlayer.inventory.book(event_key, hold_id=hold_id, booking_ref=charge.id)
```

**Your backend picks the seats.** Phone orders, box office, comps.

```python
# Payment already taken: book outright, so nothing is stranded if a second call fails.
seatlayer.inventory.book_best_available(event_key, qty=2, booking_ref="phone-1183")

# Or name the seats yourself.
seatlayer.inventory.box_office_book(event_key, labels=["A-1", "A-2"], booking_ref="comp-14")
```

## Private and partner sales

Channels split event inventory into explicit allocations. A channel id is
reporting/routing metadata, not browser authority. Authenticate the buyer in
your backend and mint a short-lived token restricted to the event, origin, and
allowed allocations:

```python
access = seatlayer.channels.create_buyer_access_session(
    event_key,
    channel_ids=["chn_partner_a"],
    include_public=False,
    allowed_origin="https://tickets.example",
)
# Return access["token"] to the in-memory buyerAccessTokenProvider only.
```

Never log or persist the returned `bse_…` bearer. Allocation setup, previews,
pause/archive controls, audit-safe session listing, and channel reports are on
`seatlayer.channels`.

## Listing and pagination

`list()` returns one page plus a `nextCursor`. When you want everything, `list_all()` pages for you
and yields as it goes. It is a generator rather than a list, because the point of paginating is to *not*
hold an unbounded result set in memory.

```python
# One page, your own paging.
page = seatlayer.events.list(limit=50)
page["events"]
page.get("nextCursor")   # absent once exhausted

# Or let the SDK walk it.
for event in seatlayer.events.list_all():
    sync(event)
```

Listing events includes live availability `counts` by default, which costs the server one
round-trip **per event**. `list_all()` turns them off automatically, since walking a whole catalogue is
exactly when you don't want that, and you can control it explicitly:

```python
seatlayer.events.list(limit=50, counts=False)
```

To list only the dates of one event series, pass `series_key="ser_1"` to `list()` or `list_all()`.

## Keeping a hold alive

When an order takes longer than the checkout window (an invoice, a phone sale), extend rather than
release and re-hold. Releasing first hands the seats to whoever is racing for them in between.

```python
from seatlayer import SeatLayerConflictError

try:
    seatlayer.inventory.extend_hold(event_key, hold_id, ttl_ms=10 * 60_000)
except SeatLayerConflictError:
    # Gone, expired, or at its renewal cap: the buyer has to re-pick.
    ...
```

## Embedding the control room

Your secret key never reaches a browser. Mint a scoped token instead.

```python
session = seatlayer.sessions.create_manage_session(
    event_key,
    allowed_origin="https://box-office.yourplatform.com",
    capabilities=["event:view", "event:block"],
    expires_in_seconds=3600,
)
```

`capabilities` is **required** by this SDK even though the raw API safely defaults an omitted list
to view-only (`event:view`). Keeping the argument required makes browser authority visible at every
call site. Grant the smallest set the page needs.

The full set, all opt-in:

| Capability | Grants |
|---|---|
| `event:view` | Read the seat map and its live states |
| `event:block` | Block and unblock seats |
| `event:cancel` | Cancel a Platform/SDK booking by reference and return its inventory to sale; does not move gateway money |
| `event:reports` | Read sales and availability reports |
| `event:channels:view` | Read sales channels and their allocations |
| `event:channels:manage` | Create, pause and archive channels; rotate access links |
| `event:orders:read` | Read SeatLayer-managed orders |
| `event:refund` | Refund an eligible Managed Ticketing order through its connected gateway |
| `event:tickets:send` | Send SeatLayer-managed tickets |
| `event:door:view` | Read the door list |
| `event:door:checkin` | Check tickets in and out |
| `event:boxoffice` | Use the managed box-office surface |

The two `event:channels:*` capabilities are **not** in the default (a token minted before sales
channels existed must not silently acquire channel authority), so ask for them explicitly if the
page manages channels.

The same pattern embeds the Designer in your own UI:

```python
chart = seatlayer.charts.create(name="Riverside Theatre")["meta"]
designer = seatlayer.sessions.create_designer_session(
    workspace_id=workspace_id,
    chart_id=chart["id"],
    allowed_origin="https://app.yourplatform.com",
    authority="edit",
)
# The API response is an envelope; hand designer["session"]["token"] to the embed.
```

For safe-mode embeds, pass `mode="safe"` with `safe_mode_options`; feature policy is passed with
`features` and the returned settings live under `designer["session"]`.

## Webhooks

Subscription responses use the API envelopes exactly: `list()` returns `{"subs": [...]}`,
`create()` returns `{"sub": ..., "secret": ...}` (the secret is shown once), and `update()`
returns `{"sub": ...}`. Supported event names are `seat.booked`, `seat.released`, `seat.blocked`,
`hold.expired`, `hold.created`, `hold.extended`, `event.created`, `event.soldout`,
`series.created`, `series.dates_added`, `order.paid`, `order.refunded`, `order.cancelled`,
`customer.created`, `customer.email_changed`, `season.ticket.transfer_sent`,
`season.ticket.transferred`, and `season.ticket.transfer_cancelled`.

Verify every delivery against the **raw** body. Re-serialising it changes the bytes and
verification will fail.

```python
from flask import request
from seatlayer import verify_webhook, WebhookVerificationError

@app.post("/webhooks/seatlayer")
def seatlayer_webhook():
    try:
        event = verify_webhook(
            request.get_data(),                                 # raw bytes, not request.json
            request.headers.get("X-SeatLayer-Signature"),
            os.environ["SEATLAYER_WEBHOOK_SECRET"],
        )
    except WebhookVerificationError:
        return "", 400

    # The signed body carries `at`, but nothing enforces a freshness window, so
    # a captured delivery stays valid indefinitely. Deduplicate on occurrenceId:
    # this is your replay protection, not an optimisation.
    if already_processed(event["occurrenceId"]):
        return "", 200

    handle(event)
    return "", 200
```

## Errors

```python
from seatlayer import SeatLayerAuthError, SeatLayerConflictError, SeatLayerRateLimitError

try:
    seatlayer.inventory.hold_best_available(event_key, qty=6)
except SeatLayerConflictError as error:
    if error.is_sold_out:
        return show_alternative_dates()      # a business outcome, not a bug
    raise
except SeatLayerRateLimitError as error:
    return retry_after(error.retry_after_seconds)
except SeatLayerAuthError as error:
    if error.is_mode_mismatch:
        raise RuntimeError("Test key pointed at a live event (or the reverse.)") from error
    raise
```

Every error carries `status`, `code`, `body`, and `request_id`. Quote the request id in support
requests.

## Reliability

**Retries and idempotency.** Reads retry 408, 429 and 5xx responses with backoff. Forty-one
mutations opt into exact header replay: `charts.create`, `charts.copy`,
`templates.instantiate_template`, `events.create`, `workspaces.create`,
`performance_groups.create`; on `seasons`, `create_season`, `update_season`, `delete_season`,
`create_season_plan`, `update_season_plan`, `delete_season_plan`, `publish_season`,
`replace_season_package_prices`, `attach_season_plan_performance`,
`delete_season_plan_performance`, `create_season_performances`, `attach_season_performances`,
`delete_season_performance`, `mark_season_performance_past`, `create_season_checkout_test`,
`duplicate_season_to_live`, `create_season_holder_import`,
`create_season_holder_import_from_season`, `create_season_renewal_offers`,
`create_season_amendment`, `refund_season_order`, `cancel_season_order`,
`cancel_season_order_date`, `refund_season_performance`, `release_season_lapsed_offers`, and
`create_season_renewal_checkout`;
`holders.update_holder`, `holders.create_holder_transfer`, `holders.create_holder_return`, and
`holders.create_holder_renewal_checkout`; `orders.refund_event_order` and `orders.cancel_event_order`; and
`event_series.create_event_series`, `event_series.add_event_series_dates`, and
`event_series.update_event_series`. The SDK generates one `Idempotency-Key` (or uses yours) and
reuses it for every attempt. All remaining SDK mutations are sent once; some have a server-side domain idempotency
contract, but the SDK does not retry them automatically.

**Booking safety.** Direct and box-office bookings have the server's exact-selection plus
`booking_ref` safeguard, but the SDK still sends them once. Holds, best-available operations,
show-once secret creation and raw mutations are also single-attempt; reconcile an unknown outcome
before trying again.

```python
SeatLayer(
    os.environ["SEATLAYER_SECRET_KEY"],
    max_retries=3,   # attempts for reads and the 41 exact-header-replay mutations
    timeout=30.0,    # seconds, per attempt
)
```

## Escape hatch

For surface this SDK does not wrap yet. Raw reads retain retries; raw mutations are single-attempt
because the SDK cannot prove that an unknown operation supports exact replay:

```python
seatlayer.request("POST", "/v1/events/ev_1/some-new-route", body={...})
```

## API surface

The client exposes these resources. Performance Groups cover runs, sessions, holds, and bookings;
Seasons cover catalogue, plan, performance, price, sales, checkout-test, buyer-session, booking,
holder, order, renewal, occurrence, reporting, export, outbox, and support operations.

| Resource | Methods |
| --- | --- |
| `charts` | `list` `list_all` `create` `retrieve` `update` `delete` `copy` `archive` `unarchive` `publish` |
| `templates` | `instantiate_template` |
| `events` | `list` `list_all` `create` `retrieve` `retrieve_configuration_binding` `update_configuration_binding` `update` `delete` `update_poster` `delete_poster` `update_chart` `close` `reopen` `archive` `list_ticket_releases` `update_ticket_releases` `close_ticket_release` `retrieve_hold_ttl` `update_hold_ttl` `retrieve_report` `retrieve_log` `retrieve_event_buyer_checkout` `replace_event_buyer_checkout` `retrieve_event_payment_gateway` `replace_event_payment_gateway` `retrieve_event_sales_readiness` `preflight_event_sales` `transition_event_sales` `retrieve_event_booking_fee` `replace_event_booking_fee` `list_event_discount_codes` `create_event_discount_code` `update_event_discount_code` `end_event_discount_code` `delete_event_discount_code` |
| `inventory` | `hold` `hold_best_available` `book_best_available` `extend_hold` `retrieve_hold` `release` `book` `box_office_book` `unbook` `list_bookings` `retrieve_booking` `block` `unblock` `unblock_all` `retrieve_availability` `update_availability` |
| `channels` | `list_channels` `create_channel` `update_channel` `update_assignments` `list_allocation` `retrieve_access_preview` `retrieve_report` `pause` `unpause` `archive` `create_buyer_access_session` `list_buyer_access_sessions` `revoke_buyer_access_session` `create_access_link` `list_access_links` `rotate_access_link` `revoke_access_link` |
| `sessions` | `create_manage_session` `revoke_manage_session` `create_designer_session` `revoke_designer_session` |
| `webhooks` | `list` `create` `update` `delete` `list_deliveries` |
| `workspaces` | `list` `create` `retrieve` `update` |
| `performance_groups` | `list` `create` `retrieve` `delete` `activate` `close` `retrieve_lifecycle` `create_buyer_access_session` `list_buyer_access_sessions` `revoke_buyer_access_session` `retrieve_hold` `book_hold` `retrieve_booking` |
| `seasons` | 87 operations for catalogue and Plan lifecycle, performances, package prices, sales windows, checkout tests, buyer access and booking, holder imports, holders, season orders, renewals, hosted renewals and renewal checkouts, occurrence amendments, returns and reclaims, bulk refunds, lapsed offer release and jobs, reports, CSV exports, audit, outbox, and support export |
| `holders` | `list_customers` `retrieve_holder` `update_holder` `revoke_holder_sessions` `list_holder_seasons` `list_holder_tickets` `retrieve_holder_ticket` `create_holder_transfer` `list_holder_transfers` `cancel_holder_transfer` `accept_holder_transfer` `create_holder_return` `create_holder_renewal_checkout` `decline_holder_renewal_offer` |
| `orders` | `list_orders` `retrieve_order` `list_order_tickets` `list_event_orders` `retrieve_event_order` `export_event_orders_csv` `refund_event_order` `cancel_event_order` `resend_event_order_tickets` |
| `event_series` | `create_event_series` `list_event_series` `retrieve_event_series` `add_event_series_dates` `update_event_series` |

Full reference: [SeatLayer server API events](https://docs.seatlayer.io/server-api/events/)

### Deliberately not in this SDK

Some API surface is intentionally unwrapped, not merely pending:

- **Connecting payment gateways.** Connecting one is a dashboard flow. Once a gateway is connected,
  `events.replace_event_payment_gateway` assigns it to an event.
- **Realtime seat updates.** Live seat state reaches the *browser* through the widget's own socket.
  There is no server-side subscribe; a secret-key caller gets authoritative state from
  `events.retrieve_report()` and `inventory.retrieve_availability()`.

None of these are reachable through `request()` as a supported path either; they are excluded from
the public manifest, not just from the wrapper.

## Frequently asked questions

### How do I book seats from Python?

Create a client with your secret key, obtain a hold id (either from the buyer's
browser session or by holding server-side) and call `inventory.book(event_key, hold_id=..., booking_ref=...)`.
`booking_ref` is your own stable order id and is the join between SeatLayer
inventory and your commercial order, so the same reference identifies the booking
in Booking History and when you later cancel it. For phone orders, box office, and
comps, `inventory.book_best_available` books outright with no browser involved.

### What does the server SDK do compared with the buyer SDK?

The buyer SDK runs where the ticket buyer is: it renders the interactive seating
chart, handles seat selection, and creates temporary holds. This server SDK is the
trusted side. It authenticates with your secret key, inspects what a hold actually
contains, prices from server-owned data, and books. Never bundle the secret key
into a browser or a mobile app. Browser surfaces get short-lived, origin-bound
tokens that you mint here.

### How do temporary holds work server-side?

A hold reserves seats against concurrent buyers for a limited window.
`inventory.retrieve_hold(event_key, hold_id)` is the authoritative answer for what is held
and at what price, so charge from its `items` rather than from anything the browser
sent you. When an order runs longer than the checkout window, `inventory.extend_hold`
renews the hold instead of releasing and re-holding, which would hand the seats to
whoever is racing for them. Bookings carry the server's exact-selection plus
`booking_ref` safeguard, but the SDK sends each booking once; reconcile an unknown
outcome before trying again.

### Can I use my own payment provider?

Yes. This server SDK does not process payment in a Platform/SDK integration. Inspect the hold,
compute the charge from each returned item's authoritative `unitPrice`, `quantity`, and `currency`,
take the money through whichever provider you already use, and then book the hold with your order
id as `booking_ref`. SeatLayer owns seating state, holds, booking concurrency, and the inventory
ledger in this integration; your platform owns payments, commercial orders, tickets, delivery,
and refunds. Managed Ticketing is a separate product path with organizer-connected payments.

## Continue your Python integration

- [Follow the Python server SDK guide](https://docs.seatlayer.io/server-sdk/python/)
  for installation, authentication, and the full hold-to-booking flow.
- [Handle errors, retries, and safe booking repeats](https://docs.seatlayer.io/server-sdk/reliability/)
  before connecting a production order flow.
- [Verify SeatLayer webhooks](https://docs.seatlayer.io/server-sdk/webhooks/)
  to react to holds, expiry, and bookings on your server.
- [Browse the SeatLayer server API reference](https://docs.seatlayer.io/server-api/events/)
  for every endpoint behind this SDK.
- [Generate clients from the SeatLayer OpenAPI description](https://docs.seatlayer.io/openapi.json)
  or explore the raw API surface.
- [Point AI coding agents at the SeatLayer docs index](https://docs.seatlayer.io/llms.txt)
  (`llms.txt`) for an agent-readable map of the documentation.
- [Explore every SeatLayer SDK on GitHub](https://github.com/seatlayer)
  across web, mobile, and server.

### Other SeatLayer SDKs

| Surface | Package or source |
| --- | --- |
| JavaScript | [`@seatlayer/js`](https://www.npmjs.com/package/@seatlayer/js) |
| React | [`@seatlayer/react`](https://www.npmjs.com/package/@seatlayer/react) |
| React Native | [`@seatlayer/react-native`](https://www.npmjs.com/package/@seatlayer/react-native) |
| iOS | [`seatlayer-ios`](https://github.com/seatlayer/seatlayer-ios) |
| Flutter | [`seatlayer`](https://pub.dev/packages/seatlayer) |
| Android | [`seatlayer-android`](https://github.com/seatlayer/seatlayer-android) |
| Node.js (server) | [`@seatlayer/server`](https://www.npmjs.com/package/@seatlayer/server) |
| Python (server) | [`seatlayer`](https://pypi.org/project/seatlayer/) (this package) |
| PHP (server) | [`seatlayer/seatlayer-php`](https://packagist.org/packages/seatlayer/seatlayer-php) |
| Ruby (server) | [`seatlayer`](https://rubygems.org/gems/seatlayer) |
| .NET (server) | [`SeatLayer`](https://www.nuget.org/packages/SeatLayer) |
| Java (server) | [`io.seatlayer:seatlayer-java`](https://central.sonatype.com/artifact/io.seatlayer/seatlayer-java) |
| Go (server) | [`github.com/seatlayer/seatlayer-go`](https://pkg.go.dev/github.com/seatlayer/seatlayer-go) |

## Development

```bash
pip install -e ".[dev]"
ruff check src tests && mypy && pytest -q
```

## License

MIT
