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');
});