SeatLayer Python SDK
Official Python server SDK for the SeatLayer reserved-seating API.
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.
Install
pip install seatlayer
Requires Python 3.10 or newer. No runtime dependencies.
Quick start
import os
from seatlayer import SeatLayer
seatlayer = SeatLayer(os.environ["SEATLAYER_SECRET_KEY"])
# 1. Provision a venue for a new organiser from one of your templates.
chart = seatlayer.charts.copy("c_template_arena")["meta"]
seatlayer.charts.publish(chart["id"])
# 2. Create an event on it.
event = seatlayer.events.create(chart_id=chart["id"], name="Spring Gala")["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")
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.
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.")
The two selling flows
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.
hold = seatlayer.inventory.retrieve_hold(event_key, hold_id)
total = sum(item["unitPrice"] for item in hold["items"])
# … charge `total` in hold["currency"] …
seatlayer.inventory.book(event_key, hold_id=hold_id, booking_ref=charge.id)
Your backend picks the seats. Phone orders, box office, comps.
# 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:
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 — a generator rather than a list, because the point of paginating is to not
hold an unbounded result set in memory.
# 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 — walking a whole catalogue is
exactly when you don't want that — and you can control it explicitly:
seatlayer.events.list(limit=50, counts=False)
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.
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.
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 API defaults it. Omit it at the API level
and you get event:view, event:block, event:cancel and event:reports — including
event:cancel, which unbooks paid seats and authorises refunds against the organiser's connected
payment gateway. That is real money, moved by a token you handed to a browser; it should not
arrive by forgetting an argument. 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 |
Unbook paid seats and issue gateway refunds — destructive, moves 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 |
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:
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",
)
Webhooks
Verify every delivery against the raw body. Re-serialising it changes the bytes and verification will fail.
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
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. 429, 408 and 5xx are retried with exponential backoff and full jitter; Retry-After
wins when the server sends it. 4xx is never retried — it will not start succeeding.
Idempotency. Every mutating request carries an Idempotency-Key, generated if you do not supply
one, and reused across retries so a retried booking cannot become two bookings. Pass your own
order id for end-to-end deduplication:
seatlayer.inventory.book(event_key, hold_id=hold_id, idempotency_key=f"order-{order_id}")
SeatLayer(
os.environ["SEATLAYER_SECRET_KEY"],
max_retries=3, # total attempts
timeout=30.0, # seconds, per attempt
)
Escape hatch
For surface this SDK does not wrap yet — same auth, retries, idempotency and error mapping:
seatlayer.request("POST", "/v1/events/ev_1/some-new-route", body={...})
API surface
| Resource | Methods |
|---|---|
charts |
list list_all create retrieve update delete copy archive unarchive publish |
events |
list list_all create retrieve update delete update_chart close reopen archive retrieve_hold_ttl update_hold_ttl retrieve_report retrieve_log |
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 |
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 |
Full reference: docs.seatlayer.io/server-api
Deliberately not in this SDK
Some API surface is intentionally unwrapped, not merely pending:
- Hosted-checkout orders and refunds. Reading or refunding a SeatLayer-hosted-checkout sale is not a server-SDK capability. Those records only exist for organisations using hosted checkout; if you run your own commerce store you refund in that store, through your own gateway.
- Connecting or assigning payment gateways. Connecting one is a dashboard flow, so shipping only the assignment half across seven SDKs would hand you a method that cannot yet succeed.
- 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()andinventory.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.
Related resources
- Server SDK guide
- Errors, retries and idempotency
- Webhook verification
- Server API reference
- OpenAPI description
- Agent-readable documentation
- SeatLayer GitHub organization
Other SeatLayer SDKs
| Surface | Package |
|---|---|
| Browser (vanilla) | @seatlayer/js |
| React | @seatlayer/react |
| React Native | @seatlayer/react-native |
| iOS | seatlayer-ios |
| Android | seatlayer-android |
| Flutter | seatlayer |
| Node.js (server) | @seatlayer/server |
| PHP (server) | seatlayer/seatlayer-php |
| Java (server) | io.seatlayer:seatlayer-java |
| Go (server) | github.com/seatlayer/seatlayer-go |
| Ruby (server) | seatlayer |
| .NET (server) | SeatLayer |
Development
pip install -e ".[dev]"
ruff check src tests && mypy && pytest -q
License
MIT
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file seatlayer-0.2.0.tar.gz.
File metadata
- Download URL: seatlayer-0.2.0.tar.gz
- Upload date:
- Size: 25.5 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
730dfb62d17bf620aac5398df17c8cb2b19a4ca23cb9ede171f8582af6c07b9d
|
|
| MD5 |
1571ce2ecb7cd57902f7d529f869b6ed
|
|
| BLAKE2b-256 |
65209909714546895889671c06352f40fe4bc6b9b0e86381c95091b1fa76278d
|
Provenance
The following attestation bundles were made for seatlayer-0.2.0.tar.gz:
Publisher:
release.yml on seatlayer/seatlayer-python
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
seatlayer-0.2.0.tar.gz -
Subject digest:
730dfb62d17bf620aac5398df17c8cb2b19a4ca23cb9ede171f8582af6c07b9d - Sigstore transparency entry: 2432353450
- Sigstore integration time:
-
Permalink:
seatlayer/seatlayer-python@a840c70721a3d4b21d766ed7e63e9abb77f5e46a -
Branch / Tag:
refs/tags/v0.2.0 - Owner: https://github.com/seatlayer
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@a840c70721a3d4b21d766ed7e63e9abb77f5e46a -
Trigger Event:
push
-
Statement type:
File details
Details for the file seatlayer-0.2.0-py3-none-any.whl.
File metadata
- Download URL: seatlayer-0.2.0-py3-none-any.whl
- Upload date:
- Size: 20.0 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
0b0ae039012064a849ea9cca8671b3fb673bb2d9939d75bac213e2f5d000779d
|
|
| MD5 |
76dc310a392ec525898453de67181e09
|
|
| BLAKE2b-256 |
c4de6a7166ebd268861f7c87293c92a39c028c18e7047eaafdbbd56b3b18f035
|
Provenance
The following attestation bundles were made for seatlayer-0.2.0-py3-none-any.whl:
Publisher:
release.yml on seatlayer/seatlayer-python
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
seatlayer-0.2.0-py3-none-any.whl -
Subject digest:
0b0ae039012064a849ea9cca8671b3fb673bb2d9939d75bac213e2f5d000779d - Sigstore transparency entry: 2432353514
- Sigstore integration time:
-
Permalink:
seatlayer/seatlayer-python@a840c70721a3d4b21d766ed7e63e9abb77f5e46a -
Branch / Tag:
refs/tags/v0.2.0 - Owner: https://github.com/seatlayer
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@a840c70721a3d4b21d766ed7e63e9abb77f5e46a -
Trigger Event:
push
-
Statement type: