A buyer access session is the credential that lets one buyer see and select one audience’s inventory. Your backend decides who deserves it — a login, an invitation, a package purchase, a partner handshake — and then asks SeatLayer for a session.
Your server mints; the widget consumes. These endpoints hand out the
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.
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. Getting it wrong is expensive in both directions — either a
partner quietly drains your public inventory, or a VIP opens the link and sees
an empty map — so 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 — a new
sessionId and a new bearer — rather than replaying a stored plaintext token
back at you. 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 |
|---|---|
No Authorization header at all |
Public sale, exactly as before. Nothing changed for existing buyers |
| Expired, revoked, wrong origin, or wrong event | A specific typed error — never a silent downgrade 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 |
Failing closed rather than degrading to public is the point. Silently widening a private buyer’s access is the exact failure this feature exists to prevent.
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 not be |
| 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 minting. A session is a decision, not a lookup.
- 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.