Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,14 @@

## Unreleased

## 0.5.0 — 2026-08-21

- Added `performanceGroups`, the trusted server resource for fixed two-to-eight
performance runs. It creates and activates groups, mints one-time browser
access, retrieves authoritative group holds, and confirms bookings with
stable action and order references. Browser-only group routes remain outside
this secret-key SDK.

- Added `templates->instantiateTemplate()` and the ticket-release lifecycle on
`events` (`listTicketReleases`, `updateTicketReleases`, and
`closeTicketRelease`). Template instantiation sends `{}` when no overrides
Expand Down
201 changes: 201 additions & 0 deletions src/Resources/PerformanceGroups.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,201 @@
<?php

declare(strict_types=1);

namespace SeatLayer\Resources;

use SeatLayer\HttpClient;

/**
* Fixed multi-performance runs, trusted hold inspection, and host-authorized booking.
*
* This resource uses a secret key. Give only the one-time token returned by
* createBuyerAccessSession() to PerformanceGroupPicker in the browser.
*/
final class PerformanceGroups
{
public function __construct(private readonly HttpClient $http)
{
}

private static function path(string $performanceGroupKey, string $suffix = ''): string
{
return '/v1/performance-groups/' . HttpClient::encode($performanceGroupKey) . $suffix;
}

/** @return array<string, mixed> */
public function list(
?string $workspaceId = null,
?string $externalRef = null,
?string $state = null,
?int $limit = null,
?string $cursor = null,
): array {
/** @var array<string, mixed> */
return (array) $this->http->get('/v1/performance-groups', array_filter([
'workspaceId' => $workspaceId,
'externalRef' => $externalRef,
'state' => $state,
'limit' => $limit,
'cursor' => $cursor,
], static fn (mixed $value): bool => $value !== null));
}

/**
* Create a draft run. The same idempotency key safely replays the original response.
*
* @param list<string> $eventKeys
* @return array<string, mixed>
*/
public function create(
string $name,
array $eventKeys,
?string $externalRef = null,
?string $idempotencyKey = null,
): array {
$body = ['name' => $name, 'eventKeys' => $eventKeys];
if ($externalRef !== null) {
$body['externalRef'] = $externalRef;
}

/** @var array<string, mixed> */
return (array) $this->http->postWithHeaderReplay('/v1/performance-groups', $body, $idempotencyKey);
}

/** @return array<string, mixed> */
public function retrieve(string $performanceGroupKey): array
{
/** @var array<string, mixed> */
return (array) $this->http->get(self::path($performanceGroupKey));
}

/** Delete a draft only; activated runs remain available for audit. */
public function delete(string $performanceGroupKey): void
{
$this->http->delete(self::path($performanceGroupKey));
}

/**
* Start activation. If lifecycleOperation.terminal is false, poll retrieveLifecycle().
*
* @return array<string, mixed>
*/
public function activate(string $performanceGroupKey, int $expectedRevision): array
{
/** @var array<string, mixed> */
return (array) $this->http->post(self::path($performanceGroupKey, '/activate'), [
'expectedRevision' => $expectedRevision,
]);
}

/**
* Stop new group sales. Poll retrieveLifecycle() until the close becomes terminal.
*
* @return array<string, mixed>
*/
public function close(string $performanceGroupKey, int $expectedRevision): array
{
/** @var array<string, mixed> */
return (array) $this->http->post(self::path($performanceGroupKey, '/close'), [
'expectedRevision' => $expectedRevision,
]);
}

/** @return array<string, mixed> */
public function retrieveLifecycle(string $performanceGroupKey, string $operationId): array
{
/** @var array<string, mixed> */
return (array) $this->http->get(self::path(
$performanceGroupKey,
'/lifecycle/' . HttpClient::encode($operationId),
));
}

/**
* Reveal one origin-bound browser bearer. This call remains single-attempt.
*
* @param array<string, list<string>>|null $channelIdsByEvent
* @return array<string, mixed>
*/
public function createBuyerAccessSession(
string $performanceGroupKey,
string $allowedOrigin,
bool $includePublic,
?array $channelIdsByEvent = null,
?int $expiresInSeconds = null,
?int $maxQuantity = null,
?string $buyerRef = null,
?string $partnerRef = null,
): array {
$body = array_filter([
'allowedOrigin' => $allowedOrigin,
'includePublic' => $includePublic,
'channelIdsByEvent' => $channelIdsByEvent,
'expiresInSeconds' => $expiresInSeconds,
'maxQuantity' => $maxQuantity,
'buyerRef' => $buyerRef,
'partnerRef' => $partnerRef,
], static fn (mixed $value): bool => $value !== null);

/** @var array<string, mixed> */
return (array) $this->http->post(self::path($performanceGroupKey, '/buyer-access-sessions'), $body);
}

/** @return array<string, mixed> */
public function listBuyerAccessSessions(string $performanceGroupKey, ?int $limit = null): array
{
/** @var array<string, mixed> */
return (array) $this->http->get(
self::path($performanceGroupKey, '/buyer-access-sessions'),
$limit === null ? null : ['limit' => $limit],
);
}

/** @return array<string, mixed> */
public function revokeBuyerAccessSession(string $performanceGroupKey, string $sessionId): array
{
/** @var array<string, mixed> */
return (array) $this->http->delete(self::path(
$performanceGroupKey,
'/buyer-access-sessions/' . HttpClient::encode($sessionId),
));
}

/** @return array<string, mixed> */
public function retrieveHold(string $performanceGroupKey, string $operationId): array
{
/** @var array<string, mixed> */
return (array) $this->http->get(self::path(
$performanceGroupKey,
'/holds/' . HttpClient::encode($operationId),
));
}

/**
* Confirm an already-authorized payment. Reuse both stable IDs and poll retrieveBooking() while pending.
*
* @return array<string, mixed>
*/
public function bookHold(
string $performanceGroupKey,
string $operationId,
string $bookActionId,
string $bookingRef,
): array {
/** @var array<string, mixed> */
return (array) $this->http->post(self::path(
$performanceGroupKey,
'/holds/' . HttpClient::encode($operationId) . '/book',
), ['bookActionId' => $bookActionId, 'bookingRef' => $bookingRef]);
}

/** @return array<string, mixed> */
public function retrieveBooking(string $performanceGroupKey, string $actionId): array
{
/** @var array<string, mixed> */
return (array) $this->http->get(self::path(
$performanceGroupKey,
'/bookings/' . HttpClient::encode($actionId),
));
}
}
3 changes: 3 additions & 0 deletions src/SeatLayer.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
use SeatLayer\Resources\Channels;
use SeatLayer\Resources\Events;
use SeatLayer\Resources\Inventory;
use SeatLayer\Resources\PerformanceGroups;
use SeatLayer\Resources\Sessions;
use SeatLayer\Resources\Templates;
use SeatLayer\Resources\Webhooks;
Expand All @@ -25,6 +26,7 @@ final class SeatLayer
public readonly Channels $channels;
public readonly Events $events;
public readonly Inventory $inventory;
public readonly PerformanceGroups $performanceGroups;
public readonly Sessions $sessions;
public readonly Templates $templates;
public readonly Webhooks $webhooks;
Expand Down Expand Up @@ -52,6 +54,7 @@ public function __construct(
$this->channels = new Channels($this->http);
$this->events = new Events($this->http);
$this->inventory = new Inventory($this->http);
$this->performanceGroups = new PerformanceGroups($this->http);
$this->sessions = new Sessions($this->http);
$this->templates = new Templates($this->http);
$this->webhooks = new Webhooks($this->http);
Expand Down
51 changes: 51 additions & 0 deletions tests/ClientTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,57 @@ public function testDropsNullQueryParameters(): void
self::assertSame('https://api.seatlayer.io/v1/charts?workspaceId=ws_1', $this->call(0)['url']);
}

public function testMapsTheFullPerformanceGroupLifecycle(): void
{
$sdk = $this->client([
['status' => 200, 'body' => ['performanceGroups' => [], 'nextCursor' => null]],
['status' => 201, 'body' => ['performanceGroup' => []]],
['status' => 200, 'body' => ['performanceGroup' => []]],
['status' => 204, 'body' => []],
['status' => 202, 'body' => ['lifecycleOperation' => ['terminal' => false]]],
['status' => 200, 'body' => ['lifecycleOperation' => ['terminal' => true]]],
['status' => 200, 'body' => ['lifecycleOperation' => []]],
['status' => 201, 'body' => ['token' => 'bsg_secret']],
['status' => 200, 'body' => ['sessions' => []]],
['status' => 200, 'body' => ['ok' => true]],
['status' => 200, 'body' => ['hold' => []]],
['status' => 202, 'body' => ['booking' => ['state' => 'book_pending']]],
['status' => 200, 'body' => ['booking' => ['state' => 'booked']]],
]);
$groupKey = 'pg_a/b';

$sdk->performanceGroups->list(workspaceId: 'ws_1', state: 'draft');
$sdk->performanceGroups->create('Weekend run', ['ev_1', 'ev_2'], idempotencyKey: 'weekend-run-1');
$sdk->performanceGroups->retrieve($groupKey);
$sdk->performanceGroups->delete($groupKey);
$sdk->performanceGroups->activate($groupKey, 1);
$sdk->performanceGroups->close($groupKey, 2);
$sdk->performanceGroups->retrieveLifecycle($groupKey, 'pga_1');
$sdk->performanceGroups->createBuyerAccessSession($groupKey, 'https://tickets.example.test', true);
$sdk->performanceGroups->listBuyerAccessSessions($groupKey, 25);
$sdk->performanceGroups->revokeBuyerAccessSession($groupKey, 'pgbs_1');
$sdk->performanceGroups->retrieveHold($groupKey, 'pgh_1');
$sdk->performanceGroups->bookHold($groupKey, 'pgh_1', 'book_1', 'order_1');
$sdk->performanceGroups->retrieveBooking($groupKey, 'book_1');

$base = 'https://api.seatlayer.io/v1/performance-groups/pg_a%2Fb';
self::assertSame('https://api.seatlayer.io/v1/performance-groups?workspaceId=ws_1&state=draft', $this->call(0)['url']);
self::assertSame('weekend-run-1', $this->call(1)['headers']['Idempotency-Key']);
self::assertSame($base, $this->call(2)['url']);
self::assertSame('DELETE', $this->call(3)['method']);
self::assertSame($base . '/activate', $this->call(4)['url']);
self::assertSame($base . '/close', $this->call(5)['url']);
self::assertSame($base . '/lifecycle/pga_1', $this->call(6)['url']);
self::assertSame($base . '/buyer-access-sessions', $this->call(7)['url']);
self::assertArrayNotHasKey('Idempotency-Key', $this->call(7)['headers']);
self::assertSame($base . '/buyer-access-sessions?limit=25', $this->call(8)['url']);
self::assertSame($base . '/buyer-access-sessions/pgbs_1', $this->call(9)['url']);
self::assertSame($base . '/holds/pgh_1', $this->call(10)['url']);
self::assertSame($base . '/holds/pgh_1/book', $this->call(11)['url']);
self::assertArrayNotHasKey('Idempotency-Key', $this->call(11)['headers']);
self::assertSame($base . '/bookings/book_1', $this->call(12)['url']);
}

// ---------- errors ----------

public function testModeMismatchIsSelfExplaining(): void
Expand Down