SeatMapOS

Developers

Sell your seats on your own site.

Drop the live seat map into your ticket page with one script tag, take payment with the provider you already use, and let your organizers design venues inside your own admin. Seat locking, inventory, and double-booking protection stay ours.

Quick start

  1. Sign in, then open Developers. Create a publishable and a secret key in test mode, and add your site as an allowed origin (for example http://localhost:5173 while you develop).
  2. Create a test event on one of your published venues and put it on sale:
curl -X POST https://os.ofpixel.com/api/v1/events \
  -H "authorization: Bearer sk_test_…" -H "content-type: application/json" \
  -d '{"venueId":"ven_your-venue","name":"Season Opener"}'

curl -X PATCH https://os.ofpixel.com/api/v1/events/evt_… \
  -H "authorization: Bearer sk_test_…" -H "content-type: application/json" \
  -d '{"status":"onsale"}'
  1. Mount the map in your page:
<div id="seats"></div>
<script src="https://os.ofpixel.com/sdk/v1/seatmapos.js"></script>
<script>
  const booking = SeatMapOS("pk_test_…").booking.mount("#seats", {
    eventId: "evt_…",
    checkout: "host",
  });
  booking.on("checkout", (hold) => {
    // hold = { eventId, holdId, buyerRef, seats, estimate, expiresAt }
    // → send it to YOUR server (next section)
  });
</script>

A complete working integration — page, server, webhook receiver — lives in the repository at examples/host-site.

Keys & modes

KeyWhere it livesWhat it can do
pk_test_… pk_live_…Your web pageRead on-sale events, seat maps, availability; mint buyer sessions. From a browser it only works on your allowed origins.
sk_test_… sk_live_…Your server onlyEverything for your organization: events, holds, confirming sales, orders, refunds, designer sessions. Sent from a browser (with an Origin header) it is refused.

Test keys see and create only test events; live keys only live ones — a full sandbox in the same deployment. Secret keys are shown once at creation; revoke one and it stops working immediately.

Booking embed

booking.mount(target, options) renders the full SeatMapOS booking map — listings, filters, 3D, view-from-seat, live availability — in an iframe sized to your container.

OptionMeaning
eventIdThe event to sell (on sale, yours, same mode as the key).
checkout"host" (default): you take payment. "hosted": our demo checkout — test mode only, payment is simulated.
theme"auto" | "light" | "dark"
quantityInitial party size, 1–10.
heightCSS height of the frame (default 640px).

Events

  • ready — loaded; carries { eventId, mode, checkout }.
  • hold — seats are held for this buyer (5 minutes by default).
  • checkout — the buyer pressed Checkout: open your payment flow.
  • holdExpired, released — the hold is gone; close your payment flow.
  • order — a sale completed; error — something went wrong.

Commands

Each returns a Promise: setQuantity(n), setTheme(t), release() (the buyer cancelled payment), complete(order) (show the receipt), and destroy().

Taking payment

You charge the buyer with your own payment provider; SeatMapOS never touches money. The embed hands your page { eventId, holdId, buyerRef }; your server does the rest with the zero-dependency server helpers:

import { createClient } from "@seatmapos/js/server";
const smos = createClient({ secretKey: process.env.SEATMAPOS_SECRET_KEY });

// 1. what to charge — from OUR records, never a total the browser reported
const { hold } = await smos.getHold(eventId, holdId, buyerRef);

// 2. charge hold.totalCents with your provider …
//    on failure: await smos.releaseHold(eventId, holdId, buyerRef)

// 3. the sale (idempotent: a replay answers 409 already_completed with the order)
const { order } = await smos.confirm(eventId, holdId, {
  owner: buyerRef,
  buyer: { name, email },
  externalRef: yourOrderId,
});

// 4. back in the page
await booking.complete(order);
Live events created through the API are sold only through your checkout. Their checkout is external: no SeatMapOS page, demo checkout or AI agent can complete a sale for them — only your secret-key confirm. If payment succeeds after the hold expired, confirm answers 410 hold_expired: refund the buyer.

Prefer plain HTTP? The same flow is GET /events/{id}/holds/{holdId}?owner=… then POST /events/{id}/holds/{holdId}/confirm — see the reference below. Building your own seat picker? Hold with { best: N } instead of seatIds and we pick and hold the best N together in one step — during an on-sale, buyers get different seats instead of racing for the same pair (the embed's “Best available” works this way).

Designer embed

Give your organizers the full venue designer inside your admin. Your server mints a short-lived designer session for one of your users, after your own login check:

const { token } = await smos.createDesignerSession({
  externalUserId: "admin-42",                  // your user id, for attribution
  permissions: ["edit", "publish", "events"],  // + "ai" (venue generation, daily quota)
  venueSlug: "riverside-stadium",              // optional: confine to one venue
  ttlSec: 3600,                                // 5 min – 8 h
});
const designer = SeatMapOS("pk_…").designer.mount("#designer", { token });
designer.on("saved", ({ draftSlug }) => …);
designer.on("published", ({ venueId, eventId, seatCount }) => …);
designer.on("error", ({ code, message }) => …);
await designer.save();     // rejects with the reason on failure
await designer.publish();  // needs the "publish" permission

A session only ever sees your organization's drafts and venues. We recommend a venueSlug per session: venue slugs are global, and a name another organization owns is refused.

Tickets & QR

Every sale issues one ticket per seat, each with a QR signed by SeatMapOS (Ed25519). They come back from the confirm call (tickets), in the order.confirmed webhook, and from GET /orders/{id}/tickets. A refund revokes them.

// SML1.<b64url body>.<b64url signature>   body = { t: ticketId, e: eventId, n: serial, k: keyId }
const { keys } = await fetch("https://os.ofpixel.com/api/tickets/keys").then((r) => r.json());
// verify offline with the public key whose kid = body.k, then check the
// ticket id against your own list: a valid signature is not a valid ticket
// (it may have been refunded or already scanned)

The QR is a bearer credential — whoever holds it walks in — so send it to the buyer, never put it in a URL or a log. The public keys can only verify, never mint, so they are safe to ship inside a scanner app.

Webhooks

Add an endpoint under Developers (https; http://localhost for test endpoints) and copy its signing secret. We POST:

  • order.confirmed — every sale, with the buyer's name and email, and data.tickets: one signed QR per seat to send them.
  • order.refunded — the seats are back on sale.
  • venue.published — a venue (and its first event) went live.
  • hold.expired / hold.released — a hold your buyer placed lapsed, or was released. data.hold carries id, buyerRef and seatIds: stop any payment still open for it — confirming it now returns 410.
  • webhook.test — from the “Send test” button.
import { verifyWebhook } from "@seatmapos/js/server";

// Use the RAW body — not re-serialized JSON.
const event = await verifyWebhook(rawBody, req.headers["seatmapos-signature"], process.env.SEATMAPOS_WEBHOOK_SECRET);
// event = { id, type, createdAt, mode, data }

The signature is t=<unix>,v1=<hex HMAC-SHA256(secret, "t.body")>; older than 5 minutes is rejected. Answer 2xx; anything else is retried with backoff (1 minute up to 24 hours). Use event.id to de-duplicate.

Security model

  • Who may frame an embed is your allowed-origins list, enforced by the browser (frame-ancestors). Any other site sees a refused frame.
  • Buyers are anonymous. Each page view mints a 2-hour buyer session bound to one event; it can hold and release seats, never complete a live sale.
  • Holds are all-or-nothing and owned: only the owner can confirm or release one, and other buyers' identities never appear in conflicts or the live feed.
  • Your secret key is the authority for money: prices come from getHold, sales from your confirm.

Limits

Hold duration1–15 minutes (default 5)
Seats per buyer session10 held at once
Buyer sessions30 per minute per IP, per organization
Hold attempts40 per 10 minutes per buyer session
AI venue generation50 per day per organization
Designer session5 minutes – 8 hours

Errors

Every error is { "error": { "code": "…", "message": "…" } }. The codes you will see most:

401 invalid_api_keyUnknown, revoked, or the organization is suspended.
403 origin_not_allowedAdd the page's origin under Developers.
403 secret_key_in_browserMove that call to your server.
404 event_not_foundNot yours, not on sale (publishable keys), or the other mode.
409 seat_conflictSomeone else holds or bought a seat; see conflicts.
409 blockedSeats withheld by the organizer.
409 not_on_saleThe event is a draft or closed.
404 none_availableWith best: N: no N seats are free together.
409 seats_movingWith best: N: seats sold faster than we could hand them out; retry.
422 orphan_seatWould strand a single seat; resend with allowOrphans to accept.
429 hold_limit / rate_limitedSee Limits.
409 already_completedThat hold was already sold — the order is attached.
410 hold_expiredThe hold lapsed, was released, or is someone else's.

API reference

Base URL https://os.ofpixel.com/api/v1, Authorization: Bearer <key>. The full contract with schemas: openapi.yaml. Version 1 is additive-only.

get/venuessecret keyList your published venues
get/eventspk or skList your events (in the key's mode)
post/eventssecret keyCreate an event on one of your venues
get/events/{eventId}pk or skEvent manifest
patch/events/{eventId}secret keyUpdate an event
get/events/{eventId}/seatmappk or skVenue geometry (protobuf)
get/events/{eventId}/viewspk or skSection decor (tags + view-from-seat images)
get/events/{eventId}/availabilitypk or skLive seat states
get/events/{eventId}/bestpk or skBest contiguous seats
post/events/{eventId}/holdssecret keyHold seats (all-or-nothing)
get/events/{eventId}/holds/{holdId}secret keyWhat a hold covers and costs
delete/events/{eventId}/holds/{holdId}secret keyRelease a hold
post/events/{eventId}/holds/{holdId}/confirmsecret keyConfirm the sale
post/buyer-sessionspk or skMint a buyer session (embeds)
post/session/holdsbuyer sessionHold seats with a buyer session
delete/session/holds/{holdId}buyer sessionRelease this session's hold
post/session/holds/{holdId}/checkoutbuyer sessionHosted checkout (TEST MODE ONLY)
post/session/promobuyer sessionPreview a promo code
post/designer-sessionssecret keyMint a designer session (embedded designer)
get/orderssecret keyList orders
get/orders/{orderId}secret keyGet an order
get/orders/{orderId}/ticketssecret keyAn order's tickets
post/events/{eventId}/tickets/backfillsecret keyIssue and sign missing tickets
post/orders/{orderId}/refundsecret keyRefund an order