Embedding the Stateless Picker
Learn how to embed SeatSquirrel seat picker on your website
Overview
The SeatSquirrel Stateless Picker allows you to embed a full-featured seat selection interface directly into your website using the Picker SDK. Unlike the database-driven picker, the stateless version gives you complete control over where and how layout data is stored.
Key Benefits:
- Full picker experience embedded in your app
- Complete control over data storage (your database, not ours)
- PostMessage-based communication (no server calls to SeatSquirrel)
- Domain-verified security
- No authentication required
- Real-time selection tracking and checkout handling
Quick Start
Step 1: Add Your Domain
- Navigate to Organization → Embed Settings
- Click "Add Domain"
- Enter your full domain with protocol and without a trailing slash (e.g.,
https://example.comorhttp://localhost:3000) - Save the domain
Important: Include the full URL with protocol (https:// or http://).
Self-Hosting
If you are self-hosting SeatSquirrel then you can add an approved domain to the APPROVED_DOMAINS environment
variable. See the Self-Hosting Guide for details.
Examples:
https://example.comhttps://www.example.comhttp://localhost:3000
Step 2: Include the SDK
Add the SeatSquirrel Picker SDK to your page:
<script src="https://seatsquirrel.com/embed/seatsquirrel-picker-sdk.js"></script>Or download and host it yourself for better control and performance.
Step 3: Create a Container
Add a container element where the picker will render:
<div id="picker-container" style="width: 100%; height: 600px;"></div>Container Height Required
The container must have an explicit height set. The picker will fill 100% of the container's dimensions.
Step 4: Initialize the Picker
<script>
const picker = new SeatSquirrel.StatelessPicker({
container: '#picker-container',
baseUrl: 'https://seatsquirrel.com',
onReady: function () {
console.log('Picker is ready');
// Load your layout data
picker.loadLayout(layoutJsonDataFromYourDatabase);
// Set your own availability data
picker.setAvailability(availabilityJsonDataFromYourDatabase);
// Set your own pricing data
picker.setPricing(pricingJsonDataFromYourDatabase);
},
onSelectionChange: function (data) {
console.log('Selected items:', data.selections);
console.log('Total:', data.totals.amount);
},
onComplete: function (data) {
// User clicked checkout - handle payment flow
console.log('Checkout requested:', data);
// Handle your own checkout flow here
myOwnCheckoutFlow(data.selections);
},
onError: function (error) {
console.error('Picker error:', error);
},
});
</script>SDK API Reference
For complete documentation on all constructor options, methods, and callbacks, see the Picker SDK API Reference.
Key Methods
| Method | Description |
|---|---|
loadLayout(data) | Required: Load a layout for seat selection |
setAvailability(data) | Set seat/area availability by ID |
setAvailabilityByLabels(data) | Set availability by row/seat labels |
setPricing(data) | Override pricing categories |
getSelections() | Request current selections (triggers onSelectionChange) |
clearSelections() | Clear all selections |
destroy() | Cleanup the instance |
Key Callbacks
| Callback | Description |
|---|---|
onReady() | Picker is ready to receive commands |
onSelectionChange(data) | User selected or deselected seats/areas |
onComplete(data) | Important: User clicked checkout button |
onError(error) | An error occurred |
Complete Example
<!DOCTYPE html>
<html>
<head>
<title>Event Ticket Booking</title>
<style>
#picker-container {
width: 100%;
height: 700px;
border: 1px solid #e5e7eb;
border-radius: 8px;
}
.controls {
display: flex;
gap: 10px;
margin-bottom: 20px;
}
button {
padding: 10px 20px;
border-radius: 6px;
border: none;
cursor: pointer;
}
.primary {
background: #3b82f6;
color: white;
}
.secondary {
background: #e5e7eb;
color: #374151;
}
#status {
margin-top: 10px;
font-weight: 600;
}
</style>
</head>
<body>
<h1>Select Your Seats</h1>
<div class="controls">
<button id="load-btn" class="primary">Load Venue</button>
<button id="set-availability-btn" class="secondary">Set Availability</button>
<button id="clear-btn" class="secondary">Clear Selections</button>
<span id="status"></span>
</div>
<div id="picker-container"></div>
<script src="https://seatsquirrel.com/embed/seatsquirrel-picker-sdk.js"></script>
<script>
const picker = new SeatSquirrel.StatelessPicker({
container: '#picker-container',
baseUrl: 'https://seatsquirrel.com',
onReady: function () {
document.getElementById('status').textContent = 'Ready - Load a venue to begin';
},
onLayoutLoaded: function (layoutData) {
document.getElementById('status').textContent =
`Loaded: ${layoutData.metadata.name} (${layoutData.totals.totalSeats} seats)`;
},
onSelectionChange: function (data) {
const count = data.totals.count;
const amount = (data.totals.amount / 100).toFixed(2);
document.getElementById('status').textContent = `Selected: ${count} items | Total: R${amount}`;
},
onComplete: async function (data) {
// Handle checkout
console.log('Processing checkout:', data);
const response = await fetch('/api/checkout', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
selections: data.selections,
total: data.totals.amount,
}),
});
if (response.ok) {
alert('Checkout successful!');
} else {
alert('Checkout failed. Please try again.');
}
},
onError: function (error) {
alert('Error: ' + error.message);
},
});
// Load venue button
document.getElementById('load-btn').addEventListener('click', async () => {
const layoutData = await fetch('/api/layouts/123').then(r => r.json());
picker.loadLayout(layoutData);
});
// Set availability button
document.getElementById('set-availability-btn').addEventListener('click', () => {
picker.setAvailabilityByLabels({
seats: [
{ rowLabel: 'A', seatLabel: '1', isAvailable: true },
{ rowLabel: 'A', seatLabel: '2', isAvailable: false },
{ rowLabel: 'B', seatLabel: '1', isAvailable: true },
],
areas: [{ areaName: 'Standing Room', availableCount: 50 }],
});
});
// Clear selections button
document.getElementById('clear-btn').addEventListener('click', () => {
picker.clearSelections();
});
</script>
</body>
</html>Framework Integration
React Component
import { useEffect, useRef, useState } from 'react';
export function SeatPicker({ layoutId, onCheckout }) {
const containerRef = useRef(null);
const pickerRef = useRef(null);
const [selections, setSelections] = useState({ count: 0, amount: 0 });
useEffect(() => {
// Load SDK dynamically
const script = document.createElement('script');
script.src = 'https://seatsquirrel.com/embed/seatsquirrel-picker-sdk.js';
script.async = true;
script.onload = async () => {
pickerRef.current = new window.SeatSquirrel.StatelessPicker({
container: containerRef.current,
baseUrl: 'https://seatsquirrel.com',
onReady: async () => {
// Load layout from your backend
const response = await fetch(`/api/layouts/${layoutId}`);
const layoutData = await response.json();
pickerRef.current.loadLayout(layoutData);
// Set availability
const availability = await fetch(`/api/layouts/${layoutId}/availability`).then(r => r.json());
pickerRef.current.setAvailability(availability);
},
onSelectionChange: data => {
setSelections(data.totals);
},
onComplete: data => {
onCheckout(data);
},
});
};
document.body.appendChild(script);
return () => {
if (pickerRef.current) {
pickerRef.current.destroy();
}
document.body.removeChild(script);
};
}, [layoutId, onCheckout]);
return (
<div>
<div className="stats">
<p>Selected: {selections.count} items</p>
<p>Total: R{(selections.amount / 100).toFixed(2)}</p>
</div>
<div ref={containerRef} style={{ width: '100%', height: '600px' }} />
</div>
);
}Vue Component
<template>
<div>
<div class="stats">
<p>Selected: {{ totals.count }} items</p>
<p>Total: R{{ (totals.amount / 100).toFixed(2) }}</p>
</div>
<div ref="container" style="width: 100%; height: 600px;"></div>
</div>
</template>
<script>
export default {
props: {
layoutId: String,
onCheckout: Function,
},
data() {
return {
picker: null,
totals: { count: 0, amount: 0 },
};
},
mounted() {
this.loadSDK();
},
beforeUnmount() {
if (this.picker) {
this.picker.destroy();
}
},
methods: {
async loadSDK() {
await new Promise(resolve => {
const script = document.createElement('script');
script.src = 'https://seatsquirrel.com/embed/seatsquirrel-picker-sdk.js';
script.onload = resolve;
document.body.appendChild(script);
});
this.picker = new window.SeatSquirrel.StatelessPicker({
container: this.$refs.container,
baseUrl: 'https://seatsquirrel.com',
onReady: async () => {
const response = await fetch(`/api/layouts/${this.layoutId}`);
const layoutData = await response.json();
this.picker.loadLayout(layoutData);
const availability = await fetch(`/api/layouts/${this.layoutId}/availability`).then(r => r.json());
this.picker.setAvailability(availability);
},
onSelectionChange: data => {
this.totals = data.totals;
},
onComplete: data => {
this.onCheckout(data);
},
});
},
},
};
</script>Best Practices
1. Container Sizing
The picker requires significant screen space for usability:
- Minimum height: 400px for basic use
- Recommended: 600px for comfortable viewing
- Full screen: Best user experience for large venues
2. Load Layout in onReady
Always wait for onReady before calling loadLayout():
const picker = new SeatSquirrel.StatelessPicker({
container: '#picker-container',
onReady: function () {
// Safe to load layout now
picker.loadLayout(layoutData);
},
});3. Handle Checkout Properly
The onComplete callback is critical - implement robust error handling:
onComplete: async function(data) {
try {
// Validate selections
if (!data.selections.length) {
alert('Please select at least one seat');
return;
}
// Process checkout
const response = await fetch('/api/checkout', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
});
if (!response.ok) {
throw new Error('Checkout failed');
}
window.location.href = '/checkout/success';
} catch (error) {
console.error('Checkout error:', error);
alert('Checkout failed. Please try again.');
}
}4. Dynamic Pricing
Update prices based on business rules (early bird, peak pricing, etc.):
// Update pricing when user selects a date
function updatePricingForDate(date) {
const isPeakTime = checkIfPeakTime(date);
picker.setPricing({
categories: [
{
id: 'standard',
name: 'Standard',
price: isPeakTime ? 7500 : 5000, // R75 vs R50 in cents
color: '#3b82f6',
},
],
});
}5. Error Handling
Always handle errors gracefully:
onError: function(error) {
console.error('Picker error:', error);
// Show user-friendly message
showErrorNotification('Unable to load venue. Please try again.');
// Log to error tracking service
logToErrorService(error);
}Domain Verification
The picker uses the same domain verification as the designer:
- Go to Organization → Embed Settings
- Add your domain with protocol (e.g.,
https://example.com) - The picker will verify the domain on load
- If verification fails, users see an "Access Denied" message
Troubleshooting
"Access Denied" Error
Problem: Picker shows "Access Denied"
Solutions:
- Verify domain is added with correct protocol (https:// or http://)
- Include port for localhost:
http://localhost:3000 - Check browser console for specific error details
Picker Not Loading
Problem: Container stays blank
Solutions:
- Ensure container has explicit height (not auto)
- Verify SDK file loaded successfully
- Check browser console for JavaScript errors
- Verify domain is on allowed list
Selections Not Working
Problem: Can't select seats/areas
Solutions:
- Ensure
loadLayout()was called inonReadycallback - Check that availability was set via
setAvailability()orsetAvailabilityByLabels() - Verify seats/areas have pricing categories assigned
- Check browser console for errors
Checkout Not Firing
Problem: onComplete callback never gets called
Solutions:
- Ensure user has selected at least one seat/area
- Check that user clicked the checkout button in the picker UI
- Verify
onCompletecallback is defined in constructor options - Check for JavaScript errors in console
Next Steps
- Picker SDK API Reference - Complete API documentation
- Designer Embedding - Embed the designer to create layouts
- Self-Hosting Guide - Deploy on your own infrastructure