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.
dotnet add package SeatLayerRequires .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.
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() });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.
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");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 });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.
}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.
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();
});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.
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. A cancelled
CancellationToken stops the loop immediately rather than being treated as a transient fault.
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.
new SeatLayerClient(secretKey, new SeatLayerClientOptions
{
MaxRetries = 3, // total attempts
Timeout = TimeSpan.FromSeconds(30), // per attempt
});For surface this SDK does not wrap yet — same auth, retries, idempotency and error mapping:
await client.SendAsync(HttpMethod.Post, "/v1/events/ev_1/some-new-route",
body: new Dictionary<string, object?> { ["qty"] = 2 });| 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 |
Inventory |
HoldAsync HoldBestAvailableAsync BookBestAvailableAsync ExtendHoldAsync RetrieveHoldAsync ReleaseAsync BookAsync BookLabelsAsync BoxOfficeBookAsync UnbookAsync BlockAsync UnblockAsync UnblockAllAsync RetrieveAvailabilityAsync UpdateAvailabilityAsync |
Sessions |
CreateManageSessionAsync RevokeManageSessionAsync CreateDesignerSessionAsync RevokeDesignerSessionAsync |
Webhooks |
ListAsync CreateAsync UpdateAsync DeleteAsync ListDeliveriesAsync |
Workspaces |
ListAsync CreateAsync RetrieveAsync UpdateAsync |
Full reference: docs.seatlayer.io/server-sdk
- Server SDK guide
- Errors, retries and idempotency
- Webhook verification
- Server API reference
- OpenAPI description
- 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 |
| PHP (server) | seatlayer/seatlayer-php |
| Java (server) | io.seatlayer:seatlayer-java |
| Go (server) | github.com/seatlayer/seatlayer-go |
| Ruby (server) | seatlayer |
dotnet build # warnings are errors
dotnet test
dotnet pack -c ReleaseMIT