A buyer access session is the credential that lets one browser see and select one event audience’s inventory. Use this server API when access depends on a login, invitation, presale, package purchase, partner handshake, or named sales channel. Your backend decides who deserves that scope and then asks SeatLayer to mint the session.
Read Sales channels and allocations for the access model and the organizer private-sales guide for the complete browser-to-booking flow. This page owns the exact session contract.
For ordinary Public sale on a Platform/SDK event, pass publicKey to the Buyer
SDK instead. SeatLayer validates the account, event mode, and exact registered
browser origin, then returns a Public-only in-memory bearer together with the
chart and compact inventory status. That direct bootstrap cannot expose private
inventory. If buyerAccessTokenProvider or buyerAccessToken is also present,
the explicit scoped credential always takes precedence over publicKey.
Your server authorizes; SeatLayer mints; the widget consumes. These endpoints
hand out authority to spend an allocation, so they accept a secret key and
nothing else. A bse_ token can never book: booking stays with your backend.
The same session also authorizes SeatLayer-hosted view-from-seat image bytes.
The current SDK fetches those Event-scoped assets with the bearer in the
Authorization header and renders an in-memory blob URL. Do not append the
token to an image URL or proxy it through query parameters.
Private audience flow
Use this flow only after you have chosen a private audience. It is not part of the ordinary Public-sale load path.
- The buyer opens your page and signs in, enters a presale code, follows an invitation, or otherwise proves eligibility to your application.
- Your backend decides which private channel(s), if any, that buyer may see; it also decides whether the same buyer may see Public sale.
- Your backend calls this secret-key endpoint with that exact scope, the
browser’s exact origin, a short expiry, and a retry-safe
clientRequestId. - Return only
{ token, expiresAt }to the browser. Keep it in memory and supply it throughbuyerAccessTokenProviderso the SDK can refresh it. - The browser creates holds with that scoped access. Your backend inspects the hold and books it; neither the token nor the browser can book directly.
For an ordinary Platform/SDK Public sale, stop before step 1: pass publicKey
to the SDK and let SeatLayer perform the public, exact-origin bootstrap.
Mint a session
/v1/events/:key/buyer-access-sessionsSecret keycurl -s -X POST "https://api.seatlayer.io/v1/events/ev_9f3a/buyer-access-sessions" \
-H "authorization: Bearer $SEATLAYER_SECRET_KEY" \
-H "content-type: application/json" \
-d '{
"channelIds": ["chn_9f1c"],
"includePublic": false,
"allowedOrigin": "https://booking.travel-agency.example",
"expiresInSeconds": 1800,
"maxQuantity": 4,
"buyerRef": "buyer_8372",
"partnerRef": "travel-agency-a",
"clientRequestId": "agency-login-01J8F2A7MRQ4"
}'| Field | Type | Required | Default | Notes |
|---|---|---|---|---|
allowedOrigin |
string |
Yes | — | One canonical HTTPS origin, checked on every request |
includePublic |
boolean |
Yes, explicitly | none | No default. See below |
channelIds |
string[] |
No | [] |
Up to 20. Each must belong to this event and be active |
expiresInSeconds |
integer |
No | 1800 |
60 to 43,200 (12 hours) |
maxQuantity |
integer | null |
No | null |
1 to 100. Guest-weighted, summed across all this buyer’s live holds |
buyerRef |
string |
No | null |
Your opaque, pseudonymous buyer reference. Max 120 characters |
partnerRef |
string |
No | null |
Your opaque partner reference. Its presence marks the sale partner rather than promoter |
clientRequestId |
string |
No | null |
Makes a retry safe. See below |
Mode comes from the event, not the body. A key whose mode differs from the
event’s gets 403 buyer_access_mode_mismatch.
includePublic has no default
You must send it. includePublic: true can expose public inventory to a partner;
includePublic: false can leave a VIP with an empty map. SeatLayer refuses to
guess. Omitting it returns 422 include_public_required.
An empty channelIds and includePublic: false is an empty scope and
returns 422 invalid_channel_scope. A session that can see nothing is a bug,
not a configuration.
Response
{
"sessionId": "bas_2f4c",
"token": "bse_ZXZ0X2FiYw_9d3f",
"expiresAt": 1764601800000,
"eventKey": "ev_9f3a",
"includePublic": false,
"maxQuantity": 4
}Keep sessionId for audit and revocation. Return only token and expiresAt
to the browser.
The response deliberately carries no channel names, no allocation labels, no partner details, and no internal notes.
Retries are safe, and never replay a token
Send a clientRequestId. On a repeat for the same issuer and id, SeatLayer
revokes any earlier session for that pair and issues a fresh one. The response
contains a new sessionId and bearer rather than a stored plaintext token. The
earlier token stops working immediately.
This is why buyer access sessions do not use the platform’s generic idempotency replay: that mechanism persists complete response JSON, and this response contains a bearer.
List sessions
/v1/events/:key/buyer-access-sessionsSecret keyNewest first. limit defaults to 50, capped at 200.
{
"sessions": [
{
"sessionId": "bas_2f4c",
"channelIds": ["chn_9f1c"],
"includePublic": false,
"allowedOrigin": "https://booking.travel-agency.example",
"mode": "live",
"expiresAt": 1764601800000,
"maxQuantity": 4,
"buyerRef": null,
"partnerRef": null,
"accessSource": "promoter",
"state": "active",
"createdAt": 1764600000000,
"revokedAt": null,
"accessLinkId": null
}
]
}state is active or revoked. Expiry is derived from expiresAt, never
stored as a state, so an expired session still lists as active with a past
timestamp. accessSource is promoter, partner, or hosted_link.
accessLinkId is set when the session came from a
hosted access link rather than a direct mint.
The token is never here, in any form.
Revoke a session
/v1/events/:key/buyer-access-sessions/:sessionIdSecret key{ "ok": true, "sessionId": "bas_2f4c", "grantVersion": 7 }Revoking twice is not an error.
Revocation is immediate and ordered
By the time this call returns, no further hold can use that session. The
event’s own inventory authority marks the grant revoked and bumps a monotonic
grant version before acknowledging, and any live map still on screen is
disconnected in the same instant with WebSocket close code 4401.
A revocation racing a hold therefore has one order, not a race: if the hold lands first it exists under the ordinary hold policy; once the revoke is acknowledged, nothing later can use that grant.
Revocation stops new availability sessions, holds, replacement holds, resumes, and extensions. It does not:
- change historical booking attribution;
- prevent the buyer releasing a hold they already have;
- stop your trusted backend booking an already-valid hold until its normal expiry.
What a session cannot do
| Situation | Result |
|---|---|
Valid publicKey bootstrap on a Platform/SDK event |
Public-only session plus chart and compact inventory status |
No Authorization or public bootstrap on a Platform/SDK event |
404 not_found; an event key is routing, not audience authority |
No Authorization on a Managed public/unlisted event |
Existing anonymous Public sale behavior is unchanged |
| Expired, revoked, wrong origin, or wrong event | A specific typed error; no fallback to public |
Browser claims a channelId it was not granted |
Ignored. The server derives scope from the credential |
| Buyer opens a second tab | maxQuantity is summed across all live holds, so the allowance does not double |
| Session expires while a hold is active | The buyer can still release it; your backend can still book it |
The API fails closed rather than degrading to public. This prevents a private buyer from receiving a wider scope.
Errors
| Status | Code | What to do |
|---|---|---|
| 401 | buyer_access_invalid |
Get a new session; do not retry the same bearer |
| 401 | buyer_access_expired |
Run your refresh flow |
| 403 | buyer_access_origin_mismatch |
Stop; check the configured origin |
| 403 | buyer_access_event_mismatch |
Stop; do not reuse a token across events |
| 403 | buyer_access_mode_mismatch |
Match test and live |
| 403 | channel_access_denied |
Return the buyer to inventory they may see. Do not reveal channel details |
| 404 | not_found |
Unknown or cross-tenant event, session, or channel |
| 409 | allocation_exhausted |
This private allocation has no inventory left. Do not say “sold out”; the event may still have public inventory |
| 422 | invalid_channel_scope |
Empty scope, more than 20 channels, or a paused/archived channel |
| 422 | include_public_required |
Send includePublic explicitly |
| 422 | invalid_allowed_origin |
One canonical HTTPS origin |
| 422 | invalid_expiry |
60 to 43,200 seconds |
| 422 | invalid_max_quantity |
1 to 100 |
| 422 | invalid_reference |
A reference exceeded 120 characters |
Server SDK
import SeatLayer from "@seatlayer/server";
const seatlayer = new SeatLayer({ secretKey: process.env.SEATLAYER_SECRET_KEY! });
export async function grantAgencyAccess(eventKey: string, buyerId: string) {
const session = await seatlayer.channels.createBuyerAccessSession(eventKey, {
channelIds: ["chn_9f1c"],
includePublic: false,
allowedOrigin: "https://booking.travel-agency.example",
expiresInSeconds: 1800,
maxQuantity: 4,
buyerRef: buyerId,
clientRequestId: `agency-login-${buyerId}`,
});
// Persist sessionId for audit and revocation. Return only these two.
return { token: session.token, expiresAt: session.expiresAt };
}session = seatlayer.create_buyer_access_session(
event_key,
channel_ids=["chn_9f1c"],
include_public=False,
allowed_origin="https://booking.travel-agency.example",
expires_in_seconds=1800,
max_quantity=4,
buyer_ref=buyer_id,
client_request_id=f"agency-login-{buyer_id}",
)Checklist
- Authenticate the buyer yourself before asking SeatLayer to mint. A session is a decision, not a lookup.
- For ordinary Platform Public sale, use the SDK’s direct
publicKeybootstrap instead of this server endpoint. - Mint here only when buyer identity or audience scope requires login, presale, partner, or channel access.
- Send
includePublicexplicitly, every time. - Keep the token in memory in the browser; never in storage, a URL, or a log.
- Send
clientRequestIdso a retry rotates instead of replaying. - Store
sessionIdso you can revoke without waiting for expiry. - Handle expired and revoked as distinct outcomes, not as a network failure.
- Keep test and live scopes separate.
Next: sales channels API and the private and partner sales tutorial.
Troubleshooting
| Symptom | Cause | Fix |
|---|---|---|
Public Platform picker shows “The seat map didn’t load”; bootstrap is 404 not_found |
Missing/wrong-mode publicKey, an unregistered exact origin, or a non-Public event lane. Refusals are deliberately indistinguishable |
Pass the event account’s matching publishable key and register the page’s exact Embed domain |
Scoped private picker shows “The seat map didn’t load”; GET /pub/events/:key/chart is 404 not_found |
No Authorization: Bearer bse_… on the request. A missing bearer is deliberately indistinguishable from an unknown event |
Pass buyerAccessTokenProvider; after authenticating the buyer, have your server ask SeatLayer for the scoped session |
403 buyer_access_origin_mismatch |
allowedOrigin differs from the page’s origin (scheme, host, or port) |
Mint with the exact origin the checkout page is served from; one session per origin |
401 buyer_access_expired after the map rendered |
Token lapsed and the provider did not renew | Return a provider function (not a one-shot buyerAccessToken) so the SDK can re-mint |
422 include_public_required |
includePublic omitted from an explicit private-audience mint |
Send it explicitly. Use true only when that scoped audience should also see Public inventory; ordinary Public sale uses direct publicKey bootstrap instead |
422 invalid_channel_scope |
channelIds: [] with includePublic: false |
A session must see inventory. Add channels or set includePublic: true |