Document / EN

Integrating as a ticketing platform

This guide is for ticketing platforms that sell through SeatsChart: your platform owns the event, the prices, the buyer account and the payment; SeatsChart owns the map, the seat inventory and the atomic holds. It covers the three pricing sources and the Brazilian sales rules — meia-entrada quota (Lei 12.933), lot switching (virada de lote) and per-buyer limits — which SeatsChart enforces in the same Redis step as the seat hold, so no quota, lot or limit can be exceeded under any concurrency.

Rules for every call on this page

  • All routes are under /api. Examples use API=https://api.seatschart.com and the placeholder key sk_live_xxx (header x-api-key).
  • Mutations need a fresh Idempotency-Key (one UUID per operation).
  • Errors are JSON: { "error": "<code>", "message": "…", …details }.
  • Every event route accepts your own event key in place of the SeatsChart id (POST /api/events with "key": "show-2026-12-01").

1. Choose the pricing source

SourceWho sets pricesRenderer showsConfirm
partner (default for new workspaces)Your platformYour host pricing configDeclared ticket types and amounts — no quote
synced_catalogYour platform, pushed to SeatsChartThe pushed price tableValidated against the table (409 price_mismatch)
seatschart_catalogOrganizer, in the designer / Event ManagerCatalog pricesQuote required before payment (409 quote_required)
  • Workspace default: Dashboard → Settings → Integration mode, or PATCH /api/workspace/integration (dashboard admins) with { "pricingSource": "partner", "ownCatalog": false }. Workspaces created before pricing modes keep seatschart_catalog.
  • Per event: "pricingSource" in the event sales rules (section 4) overrides the workspace.
  • ownCatalog: true keeps the direct-selling tools (category prices, box office) visible in the dashboard for partner-priced workspaces.

2.1 Show your prices in the renderer

Pass seats.io-style pricing in the embed config — by category, with ticket types, fees and channels:

js
const chart = new SeatsChart({
  divId: "seat-map",
  event: "show-2026-12-01",
  token: viewerToken,              // POST /api/viewer-tokens (bind the buyer, see 4.3)
  onHoldCreated: (hold) => checkout(hold.holdToken, hold.seatIds),
  pricing: {
    allFeesIncluded: false,
    prices: [
      { category: "Pista", ticketTypes: [
        { ticketType: "inteira", price: 240, label: "Inteira" },
        { ticketType: "meia", price: 120, label: "Meia-entrada", description: "Estudantes, idosos e PcD" },
      ] },
      { category: "Camarote", price: 480 },
    ],
  },
}).render();

When the buyer clicks a seat of a category with several ticket types, the renderer opens the ticket card with your types and prices; the choice travels with the hold (seatTicketTypes). Change prices at any time with chart.setPricing(pricing) — for example when you receive lots.switched (section 4.2).

2.2 Hold, charge, confirm — without a quote

  1. The renderer holds the seats (POST /api/seats/reserve, per-seat ticket types included) and hands you the holdToken.
  2. Your platform charges the buyer its own price.
  3. Confirm server-to-server, declaring what was sold:
bash
curl -X POST "$API/api/seats/confirm" -H "x-api-key: sk_live_xxx" -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" -d '{
  "eventId": "show-2026-12-01",
  "userId": "buyer-123",
  "holdToken": "…",
  "seatIds": ["…seat A…", "…seat B…"],
  "seatTicketTypes": { "…seat A…": "inteira", "…seat B…": "meia" },
  "seatPrices": { "…seat A…": 24000, "…seat B…": 12000 },
  "externalOrderId": "order-98765",
  "externalPaymentId": "pay-55555",
  "paidCurrency": "BRL"
}'
  • seatPrices (cents per seat) is optional; with only paidAmountCents the total is split evenly across the seats. Amounts are stored on the order items for reports and never overwrite the seat catalog price.
  • The final ticket types are re-checked against the meia-entrada quota inside the confirmation (section 4.1).
  • A quote stays available as optional price validation (POST /api/quotes then confirm with quoteId, as for the catalog modes).

3. Synced catalog

Push the price table of an event — categories × ticket types, with optional fees and per-lot prices:

bash
curl -X PUT "$API/api/events/show-2026-12-01/pricing" -H "x-api-key: sk_live_xxx" \
  -H "Content-Type: application/json" -d '{
  "currency": "BRL",
  "allFeesIncluded": true,
  "categories": [
    { "category": "Pista", "ticketTypes": [
      { "id": "inteira", "name": "Inteira", "priceCents": 24000, "lotPrices": { "lote-2": 28000 } },
      { "id": "meia", "name": "Meia-entrada", "priceCents": 12000, "lotPrices": { "lote-2": 14000 } }
    ] }
  ]
}'
  • The table is cached per event by content hash: reserve and confirm never read it from the database.
  • It sets the event currency when the event has none; a different currency returns 409 currency_mismatch.
  • The renderer shows the table (with the active lot price). Confirm with seatTicketTypes and seatPrices (or paidAmountCents): a different amount returns 409 price_mismatch with expectedCents (or expectedTotalCents). A missing table returns 409 price_table_missing; a seat of a category with several types and no type returns 409 ticket_type_required.
  • GET /api/events/{eventId}/pricing returns the stored table and its hash.

4. Brazilian sales rules

Configure them in Dashboard → Event Manager → Sales rules, or with the API (partner keys need the events_write scope):

bash
curl -X PUT "$API/api/events/show-2026-12-01/sales-rules" -H "x-api-key: sk_live_xxx" \
  -H "Content-Type: application/json" -d '{
  "pricingSource": "partner",
  "quotas": [
    { "id": "meia", "label": "Meia-entrada", "ticketTypes": ["meia"], "maxShare": 0.4 }
  ],
  "lotPlans": [
    { "id": "pista", "name": "Pista", "categories": ["Pista"], "lots": [
      { "id": "lote-1", "name": "1º lote", "quantity": 500 },
      { "id": "lote-2", "name": "2º lote", "quantity": 800, "untilDate": "2026-11-20T23:59:00-03:00" },
      { "id": "lote-3", "name": "3º lote" }
    ] }
  ],
  "buyerLimit": { "maxTickets": 4, "requireBuyerId": true }
}'

GET /api/events/{eventId}/sales-rules returns the rules with each quota limit (quotaLimits) and the event capacity. Saving rebuilds the counters from the event orders on next use.

4.1 Meia-entrada quota (Lei 12.933)

  • maxShare is a share of the sellable capacity (for-sale seats + general-admission places), rounded down: 0.4 of 1,001 tickets = 400. maxCount is an absolute number; with both, the lower limit applies.
  • Every ticket type listed in ticketTypes counts against the same quota (e.g. meia, meia-estudante, meia-idoso). Other types are never limited by it.
  • Counted at hold (when the hold declares types — the renderer always does), at quote (the quote declares the final types) and at confirm (final types re-checked), always atomically with the seat lock. Released on hold expiry, release, refused confirmation, failed order and cancellation.
  • Refusal: 409 quota_exceeded { "quotaId": "meia", "remaining": 2, "limit": 400 }. The hold is all-or-nothing: no seat of a refused hold is locked.
  • The renderer shows the remaining meia-entrada tickets in the ticket card and in the legend, and disables the type when the quota is exhausted — from the live state, without any extra request.

4.2 Lot switching (virada de lote)

  • A lot plan sells the categories it lists (or, without categories, every category not listed by another plan). Lots are used in order: the next lot opens when the current one has sold its quantity (holds included) or reached its untilDate — whichever comes first. A hold larger than what is left in a lot is split across lots.
  • Catalog modes: prices (cents per ticket type) replace the catalog price while the lot is active; a synced table can use lotPrices. The lot of every ticket is stored on the order item.
  • Partner mode: you own the price. Subscribe to the lots.switched webhook and switch the price your renderer config shows:
json
{ "type": "lots.switched", "items": [ { "tenant_id": "…", "event_id": "…", "lot_plan_id": "pista",
  "from_lot_id": "lote-1", "from_lot_name": "1º lote", "to_lot_id": "lote-2", "to_lot_name": "2º lote",
  "pricing_source": "partner" } ] }

to_lot_id: null means every lot is sold out. Lots also reopen when held tickets are released (the webhook fires again). Date switches are reported at the next hold.

  • Refusal when every lot of the plan is sold out or closed: 409 lot_sold_out. The renderer shows the active lot next to the category in the legend.

4.3 Per-buyer limit (CPF)

  • maxTickets per ticket buyer id per event, holds included. Use the buyer's CPF or your own buyer id: register it (POST /api/ticket-buyers) and mint the viewer token for it (POST /api/viewer-tokens with ticketBuyerId) so every hold carries it. Ids are hashed before they reach the counters.
  • requireBuyerId: true refuses holds without a buyer id (403 ticket_buyer_required), before any seat is locked.
  • Refusal: 409 buyer_limit_exceeded { "remaining": 1, "limit": 4 }.

4.4 General admission (pista)

General-admission places count exactly like seats against quotas, lots and buyer limits. POST /api/ga/reserve accepts an optional ticketTypeId for every place of the hold; POST /api/ga/confirm re-checks the final ticketTypeId atomically, and in partner mode accepts the declared paidAmountCents (and externalPaymentId) without a quote. The receipt stores the unit amount, its source and the places per lot. Releasing a GA hold gives its places back.


5. Live counters and operations

  • GET /api/events/{eventId}/sales-counters (scope reports): per quota limit, sold, held, remaining; per lot plan the activeLotId and each lot's sold / held. Expired holds are pruned before the read.
  • POST /api/events/{eventId}/sales-counters/actions/rebuild recounts sold tickets from the event orders (after manual repairs). Live holds are kept.
  • The live summary (remaining quota, active lot) reaches every open renderer through the event stream; events without rules carry nothing extra.

6. Error reference

StatuserrorMeaning
409quota_exceededThe ticket types of this hold or confirmation exceed a quota (quotaId, remaining, limit).
409lot_sold_outEvery lot of the sector's plan is sold out or closed.
409buyer_limit_exceededThe buyer would exceed maxTickets for the event.
403ticket_buyer_requiredThe event requires a buyer id and the hold has none.
409price_mismatchSynced catalog: declared amount differs from the price table (expectedCents).
409price_table_missingSynced catalog without a pushed table.
409ticket_type_requiredSynced catalog: the seat's category has several types and none was declared.
409quote_requiredSeatsChart catalog: confirm without a quote.
503service_unavailableCounters are being rebuilt; retry with the same Idempotency-Key.

7. Checklist

  1. Set the workspace integration mode (partner, synced or catalog).
  2. Create events with your event key; publish the map.
  3. Configure sales rules (meia-entrada 40%, lots, per-buyer limit).
  4. Mint viewer tokens bound to the buyer (CPF) and render with your pricing.
  5. Subscribe to lots.switched (partner mode) and reservations.confirmed.
  6. Confirm server-to-server with the declared ticket types and amounts; handle the 409 codes above.

8. Mobile apps (React Native, Flutter)

Buyer apps render the same seat map as the web embed inside a WebView. The viewer runs as the top-level page with ?bridge=react-native or ?bridge=flutter and talks to the app through the WebView's native channel instead of postMessage. Events, commands, prompts and holds are the same as in the web loader; the parent_origin allowlist still applies.

8.1 Buyer token (never ship a secret)

The app never holds your API key. Your backend mints a short-lived buyer token and the app fetches it from your backend (bind it to the buyer with ticketBuyerId when buyer validation is on):

bash
curl -X POST https://api.seatschart.com/api/viewer-tokens \
  -H "x-api-key: $SEATSCHART_API_KEY" -H "Content-Type: application/json" \
  -d '{"ttlSeconds":900}'

The public workspace key (pub_…) can live in the app. After payment, your backend confirms the hold token server-to-server, exactly as for the web.

8.2 React Native and Expo

npm install @seatschart/react-native react-native-webview (Expo: npx expo install react-native-webview).

tsx
import { SeatsChartView } from "@seatschart/react-native";

<SeatsChartView
  ref={chart}
  webBase="https://seatschart.com"
  event={eventId}
  token={buyerToken}
  pricing={pricing}
  onHoldSucceeded={(e) => setHoldToken(e.holdToken)}
/>;

ref.current exposes every renderer command as a promise (selectObjects, selectBestAvailable, goToFloor, getHoldToken, changeConfig…) plus holdToken and selectedObjects. Navigation stays on the viewer origin; other links open in the system browser.

8.3 Flutter

Add seatschart_flutter to pubspec.yaml (it uses webview_flutter).

dart
SeatsChartView(
  controller: chart,
  config: SeatsChartConfig(webBase: 'https://seatschart.com', event: eventId, token: buyerToken),
  callbacks: SeatsChartCallbacks(onHoldSucceeded: (e) => holdToken = e.holdToken),
)

SeatsChartController has the same commands as Futures. Callbacks receive SeatsChartEvent objects (typed getters plus the JSON payload).

8.4 From seats.io

seats.ioSeatsChart
@seatsio/seatsio-react-native SeatsioSeatingChart@seatschart/react-native SeatsChartView (alias SeatingChart)
Flutter seatsio SeatsioWebView + SeatsioWebViewControllerseatschart_flutter SeatsChartView + SeatsChartController
regionwebBase
workspaceKey, event, session, holdToken, pricing, languagesame names
onChartRendered(chart)onChartRendered(event, chart) (React Native) or the controller (Flutter)
JavaScript function strings (objectColor="(object) => …")declarative objectColor, objectLabel, tooltip (no app code runs in the page)
prompts not available in FlutteronPlacesPrompt, onTicketTypePrompt, onPlacesWithTicketTypesPrompt on both