SeatLayer 0.3.0
dotnet add package SeatLayer --version 0.3.0
NuGet\Install-Package SeatLayer -Version 0.3.0
<PackageReference Include="SeatLayer" Version="0.3.0" />
<PackageVersion Include="SeatLayer" Version="0.3.0" />
<PackageReference Include="SeatLayer" />
paket add SeatLayer --version 0.3.0
#r "nuget: SeatLayer, 0.3.0"
#:package SeatLayer@0.3.0
#addin nuget:?package=SeatLayer&version=0.3.0
#tool nuget:?package=SeatLayer&version=0.3.0
SeatLayer .NET SDK
Official .NET server SDK for the SeatLayer reserved-seating API.
Server-side only. This library authenticates with your secret key. Never ship it in a client application — browser surfaces get short-lived, origin-bound tokens that you mint here.
Install
dotnet add package SeatLayer
Requires .NET 8 or newer. No package dependencies — HttpClient, System.Text.Json and
HMACSHA256 all ship with the framework, so the SDK forces no version on your application.
Quick start
using SeatLayer;
var client = new SeatLayerClient(Environment.GetEnvironmentVariable("SEATLAYER_SECRET_KEY")!);
// 1. Provision a venue for a new organiser from one of your templates.
var chart = (IReadOnlyDictionary<string, object?>)(await client.Charts.CopyAsync("c_template_arena"))["meta"]!;
await client.Charts.PublishAsync((string)chart["id"]!);
// 2. Create an event on it.
var created = await client.Events.CreateAsync((string)chart["id"]!, name: "Spring Gala");
var meta = (IReadOnlyDictionary<string, object?>)created["meta"]!;
var eventKey = (string)meta["key"]!;
// 3. Sell four seats over the phone.
var held = await client.Inventory.HoldBestAvailableAsync(eventKey, new BestAvailableRequest { Qty = 4 });
// … take payment against held["items"], which carry authoritative prices …
await client.Inventory.BookAsync(eventKey, (string)held["holdId"]!, bookingRef: "order-8842");
Register the client as a singleton. It is thread-safe, and its HttpClient is meant to be
long-lived — constructing one per request exhausts sockets.
builder.Services.AddSingleton(_ =>
new SeatLayerClient(builder.Configuration["SeatLayer:SecretKey"]!));
Using IHttpClientFactory? Pass the client in and the SDK will not dispose it, because it does not
own its lifetime:
new SeatLayerClient(secretKey, new SeatLayerClientOptions { HttpClient = factory.CreateClient() });
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 SeatLayerAuthException with
IsModeMismatch.
if (env.IsProduction() && client.Mode != "live")
{
throw new InvalidOperationException("Refusing to boot production against test-mode seating data.");
}
A publishable pk_ key is rejected at construction with a message naming the mistake, rather than
failing as a 401 three round-trips later.
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 — RetrieveHoldAsync is authoritative.
var hold = await client.Inventory.RetrieveHoldAsync(eventKey, holdId);
// … charge the total of hold["items"] in hold["currency"] …
await client.Inventory.BookAsync(eventKey, 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.
await client.Inventory.BookBestAvailableAsync(eventKey,
new BestAvailableRequest { Qty = 2, BookingRef = "phone-1183" });
// Or name the seats yourself.
await client.Inventory.BoxOfficeBookAsync(eventKey, new[] { "A-1", "A-2" }, "comp-14");
Private and partner sales
Channels reserve inventory for a partner, member group, presale, or other private allocation. A buyer access session is short-lived and origin-bound, so the browser receives only the allocation it is allowed to sell; your secret key remains on your server.
var channel = await client.Channels.CreateAsync(eventKey, new CreateChannelRequest
{
Name = "Venue members",
AccessIntent = "private",
});
await client.Channels.UpdateAssignmentsAsync(
eventKey,
new[] { "A-1", "A-2" },
assignmentVersion: 1,
targetChannelId: "ch_members");
var access = await client.Channels.CreateBuyerAccessSessionAsync(
eventKey,
new BuyerAccessSessionRequest
{
IncludePublic = false,
AllowedOrigin = "https://members.example",
ChannelIds = new[] { "ch_members" },
MaxQuantity = 2,
});
Pass the returned token to the buyer SDK. For trusted backend sales, provide ChannelIds on a
BestAvailableRequest, or use the named channelIds argument on HoldAsync, BookAsync, or
BookLabelsAsync. IgnoreChannelRestrictions = true is an explicit privileged override and
should be accompanied by an audit Reason.
Listing and pagination
ListAsync returns one Page plus a cursor. ListAllAsync is an async stream that pages as you
consume it — deliberately not a List, because the point of paginating is to not hold an
unbounded result set in memory.
// One page, your own paging.
var page = await client.Events.ListAsync(new EventListRequest { Limit = 50 });
page.Items;
page.NextCursor; // null once exhausted
// Or let the SDK walk it.
await foreach (var seatEvent in client.Events.ListAllAsync())
{
await SyncAsync(seatEvent);
}
Listing events includes live availability counts by default, which costs the server one round-trip
per event. ListAllAsync drops them automatically — walking a whole catalogue is exactly when
you don't want that — and you can control it explicitly:
await client.Events.ListAsync(new EventListRequest { 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.
try
{
await client.Inventory.ExtendHoldAsync(eventKey, holdId, ttlMs: 10 * 60_000);
}
catch (SeatLayerConflictException)
{
// 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.
var session = await client.Sessions.CreateManageSessionAsync(
eventKey,
"https://box-office.yourplatform.com",
new[] { SessionsService.CapabilityView, SessionsService.CapabilityBlock },
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.
Webhooks
Verify every delivery against the raw body. Model binding and re-serialising changes the bytes, so verification will fail.
app.MapPost("/webhooks/seatlayer", async (HttpRequest request) =>
{
using var buffer = new MemoryStream();
await request.Body.CopyToAsync(buffer); // raw bytes, never a bound model
IReadOnlyDictionary<string, object?> seatEvent;
try
{
seatEvent = Webhook.Verify(
buffer.ToArray(),
request.Headers["X-SeatLayer-Signature"],
Environment.GetEnvironmentVariable("SEATLAYER_WEBHOOK_SECRET")!);
}
catch (SeatLayerWebhookVerificationException)
{
return Results.BadRequest();
}
// 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 (await AlreadyProcessedAsync((string)seatEvent["occurrenceId"]!))
{
return Results.Ok();
}
await ProcessAsync(seatEvent);
return Results.Ok();
});
Errors
try
{
await client.Inventory.HoldBestAvailableAsync(eventKey, new BestAvailableRequest { Qty = 6 });
}
catch (SeatLayerConflictException e) when (e.IsSoldOut)
{
return OfferAlternativeDates(); // a business outcome, not a bug
}
catch (SeatLayerRateLimitException e)
{
return RetryAfter(e.RetryAfterSeconds);
}
catch (SeatLayerAuthException e) when (e.IsModeMismatch)
{
throw new InvalidOperationException("Test key pointed at a live event, or the reverse.");
}
when filters read especially well here — a sold-out result and a genuine conflict are the same
exception type but different outcomes.
| Type | Status | Means |
|---|---|---|
SeatLayerAuthException |
401, 403 | Bad, revoked, or wrong-mode key |
SeatLayerNotFoundException |
404 | No such resource for this organisation |
SeatLayerConflictException |
409 | Inventory moved, or a guard rejected the change |
SeatLayerValidationException |
422 | Understood and rejected |
SeatLayerRateLimitException |
429 | Over budget; carries RetryAfterSeconds |
SeatLayerConnectionException |
— | No answer: DNS, TLS, socket, timeout |
Every API exception carries Status, Code, Body and RequestId — quote the request id in
support requests.
Reliability
Retries. Reads (GET/HEAD) retry 429, 408 and 5xx with exponential backoff and full jitter;
Retry-After wins when the server sends it. Automatic mutation retries are limited to the four
operations backed by exact response replay: Charts.CreateAsync, Charts.CopyAsync,
Events.CreateAsync, and Workspaces.CreateAsync. Other 4xx responses are never retried. A cancelled
CancellationToken stops the loop immediately rather than being treated as a transient fault.
Idempotency. Those four replay-backed operations carry an Idempotency-Key, generated when you
do not supply one and reused across attempts. Other mutations are single-attempt and receive no
automatic key. A caller-supplied key is forwarded but does not enable retries. This includes
inventory holds and bookings, show-once credential or secret creation, unsupported operations, and
raw SendAsync mutations. Keep the booking reference in the booking body for reconciliation, but
handle an unknown network outcome explicitly instead of automatically repeating the sale.
new SeatLayerClient(secretKey, new SeatLayerClientOptions
{
MaxRetries = 3, // total attempts
Timeout = TimeSpan.FromSeconds(30), // per attempt
});
Escape hatch
For surface this SDK does not wrap yet, SendAsync keeps auth and error mapping. Raw reads retain
the read retry policy; raw mutations are always single-attempt because their replay contract is unknown:
await client.SendAsync(HttpMethod.Post, "/v1/events/ev_1/some-new-route",
body: new Dictionary<string, object?> { ["qty"] = 2 });
API surface
| Service | Methods |
|---|---|
Charts |
ListAsync ListAllAsync CreateAsync RetrieveAsync UpdateAsync DeleteAsync CopyAsync ArchiveAsync UnarchiveAsync PublishAsync |
Events |
ListAsync ListAllAsync CreateAsync RetrieveAsync UpdateAsync DeleteAsync UpdateChartAsync CloseAsync ReopenAsync ArchiveAsync RetrieveHoldTtlAsync UpdateHoldTtlAsync RetrieveReportAsync RetrieveLogAsync |
Channels |
ListAsync CreateAsync UpdateAsync UpdateAssignmentsAsync ListAllocationAsync RetrieveAccessPreviewAsync RetrieveReportAsync PauseAsync UnpauseAsync ArchiveAsync CreateBuyerAccessSessionAsync ListBuyerAccessSessionsAsync RevokeBuyerAccessSessionAsync |
Inventory |
HoldAsync HoldBestAvailableAsync BookBestAvailableAsync ExtendHoldAsync RetrieveHoldAsync ReleaseAsync BookAsync BookLabelsAsync BoxOfficeBookAsync UnbookAsync BlockAsync UnblockAsync UnblockAllAsync RetrieveAvailabilityAsync UpdateAvailabilityAsync ListBookingsAsync RetrieveBookingAsync |
Sessions |
CreateManageSessionAsync RevokeManageSessionAsync CreateDesignerSessionAsync RevokeDesignerSessionAsync |
Webhooks |
ListAsync CreateAsync UpdateAsync DeleteAsync ListDeliveriesAsync |
Workspaces |
ListAsync CreateAsync RetrieveAsync UpdateAsync |
Full reference: docs.seatlayer.io/server-sdk
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 |
| Python (server) | seatlayer |
| 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
dotnet build # warnings are errors
dotnet test
dotnet pack -c Release
License
MIT
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net8.0 is compatible. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. net9.0 was computed. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. net10.0 was computed. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
-
net8.0
- No dependencies.
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.