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.
pip install seatlayerRequires Python 3.10 or newer. No runtime dependencies.
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")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.")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")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)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.
...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. That default grants all
four including event:cancel, which reverses paid bookings — not something that should arrive by
forgetting an argument. Grant the smallest set the page needs.
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",
)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 "", 200from 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
raiseEvery error carries status, code, body, and request_id — quote the request id in support
requests.
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
)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={...})| 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 block unblock unblock_all retrieve_availability update_availability |
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
- Server SDK guide
- Errors, retries and idempotency
- Webhook verification
- Server API reference
- OpenAPI description
- Agent-readable documentation
- SeatLayer GitHub organization
| Surface | Package |
|---|---|
| Browser (vanilla) | @seatlayer/js |
| React | @seatlayer/react |
| React Native | @seatlayer/react-native |
| iOS | seatlayer-ios |
| Android | seatlayer-android |
| Flutter | seatlayer_flutter |
| 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 |
pip install -e ".[dev]"
ruff check src tests && mypy && pytest -qMIT