Official PHP server SDK for the SeatLayer reserved-seating API.
Server-side only. This package authenticates with your secret key. Never expose it to a browser or anything a ticket buyer can reach — browser surfaces get short-lived, origin-bound tokens that you mint here.
composer require seatlayer/seatlayer-phpRequires PHP 8.1 or newer with ext-curl, ext-json and ext-hash. No Composer dependencies.
use SeatLayer\SeatLayer;
$seatlayer = new SeatLayer(getenv('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'], name: 'Spring Gala')['meta'];
// 3. Sell four seats over the phone.
$held = $seatlayer->inventory->holdBestAvailable($event['key'], qty: 4);
// … take payment against $held['items'], which carry authoritative prices …
$seatlayer->inventory->book($event['key'], holdId: $held['holdId'], bookingRef: '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 AuthException with
isModeMismatch().
$seatlayer = new SeatLayer(getenv('SEATLAYER_SECRET_KEY'));
if (getenv('APP_ENV') === 'production' && $seatlayer->mode !== 'live') {
throw new RuntimeException('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 — retrieveHold is authoritative.
$hold = $seatlayer->inventory->retrieveHold($eventKey, $holdId);
$total = array_sum(array_column($hold['items'], 'unitPrice'));
// … charge $total in $hold['currency'] …
$seatlayer->inventory->book($eventKey, holdId: $holdId, bookingRef: $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->bookBestAvailable($eventKey, qty: 2, bookingRef: 'phone-1183');
// Or name the seats yourself.
$seatlayer->inventory->boxOfficeBook($eventKey, ['A-1', 'A-2'], bookingRef: 'comp-14');list() returns one page plus a nextCursor. When you want everything, listAll() pages for you
and yields as it goes — a Generator rather than an array, 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['nextCursor'] ?? null; // absent once exhausted
// Or let the SDK walk it.
foreach ($seatlayer->events->listAll() as $event) {
sync($event);
}Listing events includes live availability counts by default, which costs the server one
round-trip per event. listAll() 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.
use SeatLayer\ConflictException;
try {
$seatlayer->inventory->extendHold($eventKey, $holdId, ttlMs: 10 * 60_000);
} catch (ConflictException) {
// 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->createManageSession(
$eventKey,
allowedOrigin: 'https://box-office.yourplatform.com',
capabilities: ['event:view', 'event:block'],
expiresInSeconds: 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('Riverside Theatre')['meta'];
$designer = $seatlayer->sessions->createDesignerSession(
workspaceId: $workspaceId,
chartId: $chart['id'],
allowedOrigin: 'https://app.yourplatform.com',
authority: 'edit',
);Verify every delivery against the raw body. Re-encoding the decoded array changes the bytes and verification will fail.
use SeatLayer\Webhook;
use SeatLayer\WebhookVerificationException;
// Laravel: $request->getContent() — never $request->all()
$payload = file_get_contents('php://input');
try {
$event = Webhook::verify(
$payload,
$_SERVER['HTTP_X_SEATLAYER_SIGNATURE'] ?? null,
getenv('SEATLAYER_WEBHOOK_SECRET'),
);
} catch (WebhookVerificationException) {
http_response_code(400);
return;
}
// 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 (alreadyProcessed($event['occurrenceId'])) {
http_response_code(200);
return;
}
handle($event);
http_response_code(200);use SeatLayer\AuthException;
use SeatLayer\ConflictException;
use SeatLayer\RateLimitException;
try {
$seatlayer->inventory->holdBestAvailable($eventKey, qty: 6);
} catch (ConflictException $error) {
if ($error->isSoldOut()) {
return showAlternativeDates(); // a business outcome, not a bug
}
throw $error;
} catch (RateLimitException $error) {
return retryAfter($error->retryAfterSeconds);
} catch (AuthException $error) {
if ($error->isModeMismatch()) {
throw new RuntimeException('Test key pointed at a live event, or the reverse.');
}
throw $error;
}Every exception carries status, errorCode, body, and requestId — quote the request id in
support requests.
Naming note. The error slug is
$e->errorCode, not$e->code, because PHP's baseExceptionalready owns$codeas an int. Other SeatLayer SDKs expose the same value ascode.
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($eventKey, holdId: $holdId, idempotencyKey: "order-{$orderId}");new SeatLayer(
getenv('SEATLAYER_SECRET_KEY'),
maxRetries: 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: [...]);Need your own HTTP stack? The constructor takes a $transport callable, which is also how the test
suite runs without a network.
| Resource | Methods |
|---|---|
charts |
list listAll create retrieve update delete copy archive unarchive publish |
events |
list listAll create retrieve update delete updateChart close reopen archive retrieveHoldTtl updateHoldTtl retrieveReport retrieveLog |
inventory |
hold holdBestAvailable bookBestAvailable extendHold retrieveHold release book boxOfficeBook unbook block unblock unblockAll retrieveAvailability updateAvailability |
sessions |
createManageSession revokeManageSession createDesignerSession revokeDesignerSession |
webhooks |
list create update delete listDeliveries |
workspaces |
list create retrieve update |
Full reference: docs.seatlayer.io/server-sdk
- 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 |
| Python (server) | seatlayer |
| Java (server) | io.seatlayer:seatlayer-java |
| Go (server) | github.com/seatlayer/seatlayer-go |
| Ruby (server) | seatlayer |
| .NET (server) | SeatLayer |
composer install
vendor/bin/phpstan analyse # level 8
vendor/bin/phpunitMIT