SeatBuilderSeatBuilder

SeatBuilder

An open seat-map platform for ticketing products.

SeatBuilder provides a server-backed seat selection layer you can embed in any ticketing checkout. Design venue layouts in the chart editor, publish them to events, and sell with a drop-in JavaScript SDK that handles holds, real-time availability, and concurrency. Self-host the API and own your data end to end.

Documentation

Two integration surfaces: a browser SDK for selling seats and signed webhooks for backend automation.

SDK integration

Install @seats/sdk (or load the IIFE bundle from a CDN) and call SeatsIO.render with your public key, event key, container, and the base URL of your self-hosted API.

import SeatsIO from '@seats/sdk';

const chart = SeatsIO.render({
  publicKey: 'pk_live_xxx',
  eventKey: 'evt_xxx',
  container: 'seats-container',
  apiUrl: 'https://api.your-host.example',
  maxSelectedObjects: 4,
  onObjectSelected: ({ objectLabel, holdToken }) => {
    console.log('Selected', objectLabel, 'with hold', holdToken);
  },
});

Webhook integration

Register your endpoint in the dashboard at /webhooks. Each delivery carries a Seats-Signature header in the Stripe-style format t=<unix_seconds>,v1=<hex>. Verify with HMAC-SHA256 over `${t}.${rawBody}` using your endpoint secret.

import express from 'express';
import { createHmac, timingSafeEqual } from 'crypto';

const app = express();

// Capture the raw body — HMAC must be computed over the exact bytes received.
app.post('/webhooks/seats', express.raw({ type: 'application/json' }), (req, res) => {
  const header = req.header('Seats-Signature') ?? '';
  const [tPart, v1Part] = header.split(',');
  const t = tPart?.startsWith('t=') ? tPart.slice(2) : '';
  const v1 = v1Part?.startsWith('v1=') ? v1Part.slice(3) : '';
  if (!t || !v1) return res.status(401).send('bad signature header');

  const expected = createHmac('sha256', process.env.SEATS_WEBHOOK_SECRET!)
    .update(`${t}.${req.body.toString('utf8')}`)
    .digest('hex');

  const a = Buffer.from(expected, 'hex');
  const b = Buffer.from(v1, 'hex');
  if (a.length !== b.length || !timingSafeEqual(a, b)) {
    return res.status(401).send('invalid signature');
  }
  // Optional: reject if |now - t| > 300s to defeat replay attacks.

  const event = JSON.parse(req.body.toString('utf8'));
  // ...handle event...
  res.status(200).send('ok');
});