SeatSquirrel
EmbeddingPicker

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

  1. Navigate to Organization → Embed Settings
  2. Click "Add Domain"
  3. Enter your full domain with protocol and without a trailing slash (e.g., https://example.com or http://localhost:3000)
  4. 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.com
  • https://www.example.com
  • http://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

MethodDescription
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

CallbackDescription
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:

  1. Go to Organization → Embed Settings
  2. Add your domain with protocol (e.g., https://example.com)
  3. The picker will verify the domain on load
  4. If verification fails, users see an "Access Denied" message

Troubleshooting

"Access Denied" Error

Problem: Picker shows "Access Denied"

Solutions:

  1. Verify domain is added with correct protocol (https:// or http://)
  2. Include port for localhost: http://localhost:3000
  3. Check browser console for specific error details

Picker Not Loading

Problem: Container stays blank

Solutions:

  1. Ensure container has explicit height (not auto)
  2. Verify SDK file loaded successfully
  3. Check browser console for JavaScript errors
  4. Verify domain is on allowed list

Selections Not Working

Problem: Can't select seats/areas

Solutions:

  1. Ensure loadLayout() was called in onReady callback
  2. Check that availability was set via setAvailability() or setAvailabilityByLabels()
  3. Verify seats/areas have pricing categories assigned
  4. Check browser console for errors

Checkout Not Firing

Problem: onComplete callback never gets called

Solutions:

  1. Ensure user has selected at least one seat/area
  2. Check that user clicked the checkout button in the picker UI
  3. Verify onComplete callback is defined in constructor options
  4. Check for JavaScript errors in console

Next Steps