@seathold/sdk
v0.1.17
Published
SeatHold embed SDK — renders an interactive seat map inside an iframe and communicates via postMessage
Maintainers
Readme
SeatHold SDK
Embed an interactive seat map in any web page.
The SDK now also includes helpers for the public SeatHold backend flow:
- create temporary
session_token - read builder and inventory with the required headers
- create and release hold by
inventory.label - recreate the session before expiration
Installation
npm install @seathold/sdkOr via CDN:
<script src="https://cdn.jsdelivr.net/npm/@seathold/sdk/dist/seathold.umd.js"></script>Basic Usage
<div id="seat-map"></div>
<script type="module">
import { SeatingChart } from '@seathold/sdk';
const chart = new SeatingChart({
divId: 'seat-map',
baseUrl: 'https://tickets.myapp.com',
workspaceKey: 'pub_xxxxxxxxxxxx',
event: '019f117c-5954-7312-8e35-f8d29bc57c8e',
mode: 'simplified',
onReady: (eventId, objectKeys) => {
console.log('Map ready for event:', eventId, objectKeys);
},
onSelectionChanged: (seatIds, ticketTypes, objectKeys, items, pricingSelection) => {
console.log('Selected seats:', seatIds);
console.log('Ticket types chosen:', ticketTypes);
console.log('Pricing selection:', pricingSelection);
console.log('Selected items:', items);
},
onSessionCreated: (sessionToken, expiresAt) => {
console.log('Embed session created:', sessionToken, expiresAt);
},
onSessionUpdated: (sessionToken, expiresAt) => {
console.log('Embed session token:', sessionToken, 'expires at:', expiresAt);
},
onHoldCreated: (holdId, holdToken, expiresAt, seatIds, ticketTypes) => {
console.log('Hold created:', holdId);
},
}).render();
</script>Multiprice
Pass pricing to define ticket types per section key. pricing[].category must match the key exposed by the embed payload, for example sections[].key. When the user clicks a seat in that section, a popover shows the available ticket types to choose from.
const chart = new SeatingChart({
divId: 'seat-map',
baseUrl: 'https://tickets.myapp.com',
workspaceKey: 'pub_xxxxxxxxxxxx',
event: '019f117c-5954-7312-8e35-f8d29bc57c8e',
pricing: [
{
category: '1b',
ticketTypes: [
{ id: 'pista-inteira', label: 'Inteira', price: 120, currency: 'BRL' },
{ id: 'pista-meia', label: 'Meia', price: 60, currency: 'BRL', color: '#22c55e' },
],
},
{
category: 'vip-a',
ticketTypes: [
{ id: 'vip-inteira', label: 'VIP Inteira', price: 350, currency: 'BRL' },
],
},
],
onSelectionChanged: (seatIds, ticketTypes, objectKeys, items, pricingSelection) => {
// ticketTypes: { "seat-42": "pista-inteira", "seat-43": "pista-meia" }
console.log(seatIds, ticketTypes, objectKeys, items, pricingSelection);
},
onSessionCreated: (sessionToken, expiresAt) => {
// Persist the first session token created inside the iframe
fetch('/api/embed-session-created', {
method: 'POST',
body: JSON.stringify({ sessionToken, expiresAt }),
});
},
onSessionUpdated: (sessionToken, expiresAt) => {
// Persist the new embed session token created inside the iframe
fetch('/api/embed-session', {
method: 'POST',
body: JSON.stringify({ sessionToken, expiresAt }),
});
},
onHoldCreated: (holdId, holdToken, expiresAt, seatIds, ticketTypes) => {
// Since the session-token refactor, holdId === holdToken === sessionToken
// Send the session token to your backend to finalize the order
fetch('/api/orders', {
method: 'POST',
body: JSON.stringify({ holdId, holdToken, ticketTypes }),
});
},
}).render();API
new SeatingChart(config)
| Option | Type | Required | Description |
|---|---|---|---|
| divId | string | yes | ID of the container element |
| baseUrl | string | yes | Base URL of your SeatHold server |
| workspaceKey | string | yes | Public workspace key |
| event | string | yes | Public event.id used in X-SeatHold-Event-Id |
| mode | 'manager' \| 'simplified' | | Embed mode applied on iframe mount |
| environment | 'production' \| 'sandbox' | | Optional environment header for POST /api/session-tokens |
| sessionToken | string | | Session token for authenticated holds |
| sessionExpiresAt | string | | Current session expiration timestamp |
| sessionRefreshBufferMs | number | | Recreate the session this many ms before expiration. Default: 30000 |
| pricing | PricingRule[] | | Multiprice rules per section key |
| height | number or string | | iframe height (default: 600px) |
| width | number or string | | iframe width (default: 100%) |
| onReady | (eventId, objectKeys?) => void | | Fired when the map finishes loading |
| onSelectionChanged | (seatIds, ticketTypes, objectKeys, items, pricingSelection) => void | | Fired on every selection change |
| onObjectClicked | (objectId, objectType, objectKey?, categoryKey?) => void | | Fired when any object is clicked |
| onCategoryChanged | (categoryId) => void | | Fired when active category changes |
| onViewChanged | (zoom, position) => void | | Fired on pan/zoom |
| onHoldCreated | (holdId, holdToken, expiresAt, seatIds, ticketTypes, objectKeys, items) => void | | Fired after a hold is created |
| onHoldReleased | () => void | | Fired after a hold is released |
| onState | (state) => void | | Fired when the iframe responds to requestState() |
| onSessionCreated | (sessionToken, expiresAt) => void | | Fired when the iframe creates a new session |
| onSessionUpdated | (sessionToken, expiresAt) => void | | Fired when the iframe creates or refreshes the embed session token |
| onError | (action, message) => void | | Fired on errors |
Instance methods
chart.render() // Inject the iframe and start listening
chart.destroy() // Remove the iframe and all listeners
chart.setSelectedSeats([id1, id2]) // Programmatically select seats
await chart.createSessionToken() // Create a public API session token
await chart.refreshSessionToken() // Force a new session token and sync the iframe
await chart.ensureValidSession() // Reuse or recreate the session if close to expiring
await chart.getBuilder() // GET /api/render-map/builder
await chart.getInventory() // GET /api/render-map/inventory
await chart.holdByLabel('A-10') // POST /api/inventory/{label}/hold
await chart.releaseByLabel('A-10') // POST /api/inventory/{label}/release
chart.holdCreated(sessionToken, expiresAt) // Notify the iframe about an external hold/session
chart.releaseHold() // Release the current hold
chart.updateSession(token, expiresAt) // Refresh the session token
chart.requestState() // Ask for current state snapshot
chart.setPricing(rules) // Update pricing rules at runtimePublic API Flow
The public backend uses a temporary session_token before any hold exists. The SDK methods above follow this contract:
createSessionToken()callsPOST /api/session-tokensgetBuilder()callsGET /api/render-map/buildergetInventory()callsGET /api/render-map/inventoryholdByLabel(label)callsPOST /api/inventory/{label}/holdreleaseByLabel(label)callsPOST /api/inventory/{label}/release
Protected routes automatically send:
X-SeatHold-Event-IdX-SeatHold-Public-KeyX-SeatHold-Session-Token
Important:
- the SDK sends
event.idinX-SeatHold-Event-Id - do not use
event.uid - hold/release is done by
inventory.label, not byseat_id
createSessionToken() optionally sends X-SeatHold-Environment: production|sandbox when config.environment is provided.
Embed mode
Set mode only when creating the chart. The SDK sends it as a query param in the iframe URL, for example ?mode=simplified or ?mode=manager.
If mode is omitted, the embed uses the default purchase flow. To change mode after load, destroy and render a new iframe with a different mode.
Session token capture
If the embed creates a new authenticated session from inside the iframe, listen to onSessionCreated and onSessionUpdated to capture the sessionToken in the host app:
const chart = new SeatingChart({
// ...
onSessionCreated: (sessionToken, expiresAt) => {
console.log('New sessionToken from iframe:', sessionToken, expiresAt);
},
onSessionUpdated: (sessionToken, expiresAt) => {
console.log('Updated sessionToken from iframe:', sessionToken, expiresAt);
},
onState: (state) => {
console.log('Current embed state:', state);
},
}).render();
chart.requestState();For this to work, the embed must post either:
seathold:session_createdwith{ sessionToken, expiresAt }seathold:session_updatedwith{ sessionToken, expiresAt }seathold:statewith{ sessionToken, sessionExpiresAt, ... }
When the SDK itself creates or refreshes the session through createSessionToken(), refreshSessionToken() or ensureValidSession(), it also:
- updates the internal token used by the public API helpers
- syncs the iframe with
seathold:update_session - recreates the session automatically before
expires_at
Hold token compatibility
Since the session-token refactor, holdId === holdToken === sessionToken.
If your booking API previously received holdId, update that integration to use the sessionToken semantics instead. The TypeScript types still allow the legacy names for backward compatibility.
When calling chart.holdCreated(sessionToken, expiresAt), the SDK sends:
{
type: 'seathold:hold_created',
holdId: sessionToken,
holdToken: sessionToken,
sessionToken,
expiresAt,
}Selection payload
The embed can now expose stable commercial keys directly in the selection payload.
type SelectedItem = {
objectKey: string;
objectId: string | number;
sectionKey?: string | null;
categoryKey?: string | null;
ticketType: string | null;
};items in onSelectionChanged and onHoldCreated may include sectionKey and categoryKey when the map or inventory payload provides them.
onSelectionChanged now also receives:
pricingSelection: Record<string, string | null>;This maps each selected object to the current pricing choice coming from the embed.
Public API Errors
The public API can return codes such as:
workspace_key_requiredinvalid_workspace_keysession_token_requiredinvalid_or_expired_session_tokensession_token_event_mismatchsession_token_workspace_mismatch
The SDK surfaces these through thrown errors and onError.
TicketType
type TicketType = {
id: string;
label: string;
color?: string | null;
price?: number | null;
currency?: string | null;
};