Skip to content

Repository files navigation

SeatLayer Go SDK

CI Go Reference License: MIT

Official Go server SDK for the SeatLayer reserved-seating API.

Server-side only. This package authenticates with your secret key. Never embed it in anything a ticket buyer can reach — browser surfaces get short-lived, origin-bound tokens that you mint here.

Install

go get github.com/seatlayer/seatlayer-go

Requires Go 1.23 or newer (for range-over-func iterators). No dependencies — standard library only.

Quick start

import (
    "context"
    "os"

    "github.com/seatlayer/seatlayer-go"
)

client, err := seatlayer.New(os.Getenv("SEATLAYER_SECRET_KEY"))
if err != nil {
    return err
}
ctx := context.Background()

// 1. Materialize a published catalog template as the organiser's draft chart.
chart, err := client.Templates.InstantiateTemplate(ctx, "arena")
if err != nil {
    return err
}
chartID := chart["meta"].(map[string]any)["id"].(string)
if _, err := client.Charts.Publish(ctx, chartID); err != nil {
    return err
}

// 2. Create an event on it.
event, err := client.Events.Create(ctx, seatlayer.EventCreateParams{
    ChartID: chartID,
    Name:    "Spring Gala",
})
if err != nil {
    return err
}
eventKey := event["meta"].(map[string]any)["key"].(string)

// 3. Sell four seats over the phone.
held, err := client.Inventory.HoldBestAvailable(ctx, eventKey, seatlayer.BestAvailableParams{Qty: 4})
if err != nil {
    return err
}
// … take payment against held["items"], which carry authoritative prices …
_, err = client.Inventory.Book(ctx, eventKey, seatlayer.BookParams{
    HoldID:     held["holdId"].(string),
    BookingRef: "order-8842",
})

For nullable event-create fields, ordinary scalar fields cover the common value-or-omit case. Use the Nullable overlay when the wire call must contain an explicit JSON null, for example Nullable: seatlayer.EventCreateNullableFields{Venue: seatlayer.FieldNull[string]()}.

Every method takes a context.Context. Cancelling it stops retries immediately rather than being treated as a transient fault to back off through.

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.

client, err := seatlayer.New(os.Getenv("SEATLAYER_SECRET_KEY"))
if err != nil {
    return err
}
if os.Getenv("ENV") == "production" && client.Mode() != "live" {
    return errors.New("refusing to boot production against test-mode seating data")
}

A publishable pk_ key is rejected by New 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 — RetrieveHold is authoritative.

hold, err := client.Inventory.RetrieveHold(ctx, eventKey, holdID)
// … charge from hold.Items, whose UnitPrice and Currency are authoritative …
_, err = client.Inventory.Book(ctx, eventKey, seatlayer.BookParams{
    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.
_, err := client.Inventory.BookBestAvailable(ctx, eventKey, seatlayer.BestAvailableParams{
    Qty: 2, BookingRef: "phone-1183",
})

// Or name the seats yourself.
_, err = client.Inventory.BoxOfficeBook(ctx, eventKey, []string{"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.

_, err := client.Channels.CreateChannel(ctx, eventKey, seatlayer.ChannelCreateParams{
	Name:         "Venue members",
	AccessIntent: "private",
})

_, err = client.Channels.UpdateAssignments(ctx, eventKey, seatlayer.ChannelAssignmentParams{
	Labels:            []string{"A-1", "A-2"},
	AssignmentVersion: 1,
	TargetChannelID:   "ch_members",
})

access, err := client.Channels.CreateBuyerAccessSession(ctx, eventKey,
	seatlayer.BuyerAccessSessionParams{
		ChannelIDs:    []string{"ch_members"},
		IncludePublic: false,
		AllowedOrigin: "https://members.example",
		MaxQuantity:   2,
	})

Pass the returned token to the buyer SDK. Trusted backend sale params accept ChannelIDs, an explicit privileged IgnoreChannelRestrictions flag, and an audit Reason.

Listing and pagination

List returns one Page plus a cursor. All is a range-over-func iterator that pages as you consume it — deliberately not a slice, because the point of paginating is to not hold an unbounded result set in memory.

// One page, your own paging.
page, err := client.Events.List(ctx, &seatlayer.EventListParams{Limit: 50})
page.Items
page.NextCursor   // "" once exhausted

// Or let the SDK walk it.
for event, err := range client.Events.All(ctx, nil) {
    if err != nil {
        return err
    }
    sync(event)
}

The error rides alongside each item so a failed page reaches you — an iterator that silently ended on error would look identical to a list that finished.

Listing events includes live availability counts by default, which costs the server one round-trip per event. All drops them automatically — walking a whole catalogue is exactly when you don't want that — and you can control it explicitly:

client.Events.List(ctx, &seatlayer.EventListParams{Limit: 50, NoCounts: true})

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.

_, err := client.Inventory.ExtendHold(ctx, eventKey, holdID, 10*60*1000)

var conflict *seatlayer.ConflictError
if errors.As(err, &conflict) {
    // 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.

session, err := client.Sessions.CreateManageSession(ctx, eventKey, seatlayer.ManageSessionParams{
    AllowedOrigin:    "https://box-office.yourplatform.com",
    Capabilities:     []seatlayer.ManageCapability{
        seatlayer.CapabilityView,
        seatlayer.CapabilityBlock,
    },
    ExpiresInSeconds: 3600,
})

Capabilities is required by this SDK even though the raw API safely defaults an omitted list to view-only (event:view). Keeping the field required makes browser authority visible at every call site. Grant the smallest set the page needs. The constants also cover channel management and SeatLayer-managed orders, refunds, ticket delivery, door, and box-office capabilities.

Designer minting returns a DesignerSessionEnvelope; the token and the effective safe-mode and feature policy live under result.Session. Pass SafeModeOptions only with Mode: "safe".

Webhooks

Webhook methods expose the wire envelopes directly: List returns WebhookList.Subs, Create returns WebhookCreateEnvelope with the show-once Secret, and Update returns WebhookEnvelope.Sub. Use the WebhookEvent… constants for the eight accepted event names and WebhookDeliveryListParams for limit, status, and before filters.

Verify every delivery against the raw body. Decoding and re-encoding changes the bytes — in Go specifically, encoding/json marshals map keys in sorted order while a real delivery arrives in the order we serialised it, so a round trip reorders it and verification fails.

func handleWebhook(w http.ResponseWriter, r *http.Request) {
    payload, err := io.ReadAll(r.Body)   // raw bytes, before any decoding
    if err != nil {
        w.WriteHeader(http.StatusBadRequest)
        return
    }

    event, err := seatlayer.VerifyWebhook(
        payload,
        r.Header.Get("X-SeatLayer-Signature"),
        os.Getenv("SEATLAYER_WEBHOOK_SECRET"),
    )
    if errors.Is(err, seatlayer.ErrWebhookVerification) {
        w.WriteHeader(http.StatusBadRequest)
        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"].(string)) {
        w.WriteHeader(http.StatusOK)
        return
    }

    process(event)
    w.WriteHeader(http.StatusOK)
}

Errors

Errors are values here, not exceptions — reach for errors.As:

_, err := client.Inventory.HoldBestAvailable(ctx, eventKey, seatlayer.BestAvailableParams{Qty: 6})

var conflict *seatlayer.ConflictError
var rateLimit *seatlayer.RateLimitError
var auth *seatlayer.AuthError

switch {
case errors.As(err, &conflict) && conflict.SoldOut():
    return offerAlternativeDates()          // a business outcome, not a bug
case errors.As(err, &rateLimit):
    return retryAfter(rateLimit.RetryAfter)
case errors.As(err, &auth) && auth.ModeMismatch():
    return errors.New("test key pointed at a live event, or the reverse")
case err != nil:
    return err
}
Type Status Means
AuthError 401, 403 Bad, revoked, or wrong-mode key
NotFoundError 404 No such resource for this organisation
ConflictError 409 Inventory moved, or a guard rejected the change
ValidationError 422 Understood and rejected
RateLimitError 429 Over budget; carries RetryAfter
ConnectionError No answer: DNS, TLS, socket, context deadline (unwraps)

Every API error 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 five operations backed by exact response replay: Charts.Create, Charts.Copy, Templates.InstantiateTemplate, Events.Create, and Workspaces.Create. Other mutations, including ticket-release changes, stay single-attempt. Other 4xx responses are never retried.

Idempotency. Those five 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 Do mutations. Keep BookingRef in the booking body for reconciliation, but handle an unknown network outcome explicitly instead of automatically repeating the sale.

client.Events.Create(ctx, seatlayer.EventCreateParams{
    ChartID: chartID, IdempotencyKey: "provision-event-" + eventID,
})
client, err := seatlayer.New(
    os.Getenv("SEATLAYER_SECRET_KEY"),
    seatlayer.WithMaxRetries(3),
    seatlayer.WithHTTPClient(&http.Client{Timeout: 30 * time.Second}),
)

Client is safe for concurrent use.

Escape hatch

For surface this SDK does not wrap yet, Do keeps auth and error mapping. Raw reads retain the read retry policy; raw mutations are always single-attempt because their replay contract is unknown:

client.Do(ctx, http.MethodPost, "/v1/events/ev_1/some-new-route", nil, map[string]any{"qty": 2}, "")

API surface

Service Methods
Charts List All Create Retrieve Update Delete Copy Archive Unarchive Publish
Events List All Create Retrieve Update Delete UpdatePoster DeletePoster UpdateChart Close Reopen Archive RetrieveHoldTTL UpdateHoldTTL RetrieveReport RetrieveLog
Channels ListChannels CreateChannel UpdateChannel UpdateAssignments ListAllocation RetrieveAccessPreview RetrieveReport Pause Unpause Archive CreateBuyerAccessSession ListBuyerAccessSessions RevokeBuyerAccessSession CreateAccessLink ListAccessLinks RotateAccessLink RevokeAccessLink
Inventory Hold HoldBestAvailable BookBestAvailable ExtendHold RetrieveHold Release Book BoxOfficeBook Unbook Block Unblock UnblockAll RetrieveAvailability UpdateAvailability ListBookings RetrieveBooking
Sessions CreateManageSession RevokeManageSession CreateDesignerSession RevokeDesignerSession
Webhooks List Create Update Delete ListDeliveries
Workspaces List Create Retrieve Update

Full reference: docs.seatlayer.io/server-sdk

Related resources

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

gofmt -l .          # must be empty
go vet ./...
go test -race ./...

License

MIT

About

Official Go server SDK for SeatLayer reserved seating — charts, events, holds, booking, and webhook verification with idempotency and retries built in.

Topics

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages