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 useAPI=https://api.seatschart.comand the placeholder keysk_live_xxx(headerx-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/eventswith"key": "show-2026-12-01").
1. Choose the pricing source
| Source | Who sets prices | Renderer shows | Confirm |
|---|---|---|---|
partner (default for new workspaces) | Your platform | Your host pricing config | Declared ticket types and amounts — no quote |
synced_catalog | Your platform, pushed to SeatsChart | The pushed price table | Validated against the table (409 price_mismatch) |
seatschart_catalog | Organizer, in the designer / Event Manager | Catalog prices | Quote 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 keepseatschart_catalog. - Per event:
"pricingSource"in the event sales rules (section 4) overrides the workspace. ownCatalog: truekeeps the direct-selling tools (category prices, box office) visible in the dashboard for partner-priced workspaces.
2. Partner pricing (recommended for ticketing platforms)
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:
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
- The renderer holds the seats (
POST /api/seats/reserve, per-seat ticket types included) and hands you theholdToken. - Your platform charges the buyer its own price.
- Confirm server-to-server, declaring what was sold:
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 onlypaidAmountCentsthe 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/quotesthen confirm withquoteId, 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:
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
seatTicketTypesandseatPrices(orpaidAmountCents): a different amount returns409 price_mismatchwithexpectedCents(orexpectedTotalCents). A missing table returns409 price_table_missing; a seat of a category with several types and no type returns409 ticket_type_required. GET /api/events/{eventId}/pricingreturns 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):
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)
maxShareis a share of the sellable capacity (for-sale seats + general-admission places), rounded down:0.4of 1,001 tickets = 400.maxCountis an absolute number; with both, the lower limit applies.- Every ticket type listed in
ticketTypescounts 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 itsquantity(holds included) or reached itsuntilDate— 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 uselotPrices. The lot of every ticket is stored on the order item. - Partner mode: you own the price. Subscribe to the
lots.switchedwebhook and switch the price your renderer config shows:
{ "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)
maxTicketsper 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-tokenswithticketBuyerId) so every hold carries it. Ids are hashed before they reach the counters.requireBuyerId: truerefuses 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(scopereports): per quotalimit,sold,held,remaining; per lot plan theactiveLotIdand each lot'ssold/held. Expired holds are pruned before the read.POST /api/events/{eventId}/sales-counters/actions/rebuildrecounts 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
| Status | error | Meaning |
|---|---|---|
| 409 | quota_exceeded | The ticket types of this hold or confirmation exceed a quota (quotaId, remaining, limit). |
| 409 | lot_sold_out | Every lot of the sector's plan is sold out or closed. |
| 409 | buyer_limit_exceeded | The buyer would exceed maxTickets for the event. |
| 403 | ticket_buyer_required | The event requires a buyer id and the hold has none. |
| 409 | price_mismatch | Synced catalog: declared amount differs from the price table (expectedCents). |
| 409 | price_table_missing | Synced catalog without a pushed table. |
| 409 | ticket_type_required | Synced catalog: the seat's category has several types and none was declared. |
| 409 | quote_required | SeatsChart catalog: confirm without a quote. |
| 503 | service_unavailable | Counters are being rebuilt; retry with the same Idempotency-Key. |
7. Checklist
- Set the workspace integration mode (partner, synced or catalog).
- Create events with your event key; publish the map.
- Configure sales rules (meia-entrada 40%, lots, per-buyer limit).
- Mint viewer tokens bound to the buyer (CPF) and render with your
pricing. - Subscribe to
lots.switched(partner mode) andreservations.confirmed. - 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):
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).
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).
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.io | SeatsChart |
|---|---|
@seatsio/seatsio-react-native SeatsioSeatingChart | @seatschart/react-native SeatsChartView (alias SeatingChart) |
Flutter seatsio SeatsioWebView + SeatsioWebViewController | seatschart_flutter SeatsChartView + SeatsChartController |
region | webBase |
workspaceKey, event, session, holdToken, pricing, language | same 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 Flutter | onPlacesPrompt, onTicketTypePrompt, onPlacesWithTicketTypesPrompt on both |
