Document / EN
Map designer (partner dashboard)
Visual floor-plan editor at /dashboard/events/<event_uuid>/designer. Charts are created via /dashboard/charts/new.
Quick start
- Charts → New chart: pick a layout preset (Simple, Sections and floors, Zones). The chart is created right away as Untitled chart and the designer opens — there is no naming step.
- Rename inline: click the name in the designer top bar, type, then press Enter (or click away) to save; Esc cancels. The breadcrumb and chart library follow.
- Start panel: an empty chart shows three choices — Start from a floor-plan image (uploads a tracing image and opens Scan floor plan when available), Draw from scratch, or Take the 1-minute tour. The choice is remembered per chart in this browser. Above them, Venue type (Simple, Sections and floors, Zones) switches the layout preset of the empty chart and its tools; it is saved with the first change.
- Tour: seven stops (top toolbar, tools, inspector, hint bar, power tools — layers, section edit, row and area joins, GA tables, calibration, icons — surroundings and live preview, save and publish) with Next / Back / Skip; Esc closes it. Replay it from the ? help menu.
- Hint bar: the bottom bar always shows the active tool, its key, its mouse gestures and how many objects are selected. The focal-point tool explains itself: Best available picks seats closest to this point.
- Quick-start tutorial: the first time you open the designer, a three-step card in the canvas corner shows how to draw, edit and save. Next / Back (or the arrow keys) move between steps, Esc or Skip closes it, and the last step offers the full tour. It is shown once per browser; replay it from ? → Quick-start tutorial. Tool shortcuts keep working while it is open: press R and draw a row right away.
Keyboard shortcuts
Press ? (or Help → Keyboard shortcuts) for the searchable sheet. Shortcuts are ignored while you type in a field or a dialog is open. On macOS use ⌘ instead of Ctrl. Tool keys follow seats.io's designer.
| Tool | Key |
|---|---|
| Select | V |
| Brush select | C |
| Select seats (box; Simple preset) | X |
| Select same type | Z |
| Paint category | P |
| Node | A |
| Focal point | F |
| Section tools | S |
| Row tools | R |
| Table tools | E |
| GA table tool (in the Table tools group) | N |
| Booth | B |
| Icon | O |
| Area (general admission) tools | G |
| Shape tools (Sections and Zones presets) | H |
| Line | L |
| Billboard (3D) | K |
| Text | T |
| Image | I |
| Calibrate the reference image | M |
Grouped tools (S, R, E, G, H) resume the last tool used in that group. Keys of tools a preset doesn't have are ignored.
| Action | Keys |
|---|---|
| Undo / Redo | Ctrl+Z / Ctrl+Y, Ctrl+Shift+Z |
| Copy / Cut / Paste (also between charts and tabs) | Ctrl+C / Ctrl+X / Ctrl+V |
| Duplicate | Ctrl+J |
| Select all (current floor) | Ctrl+A |
| Deselect | Ctrl+D |
| Delete selection | Del, Backspace |
| Bring forward / Send backward (Shift: to front / to back) | Ctrl+] / Ctrl+[ (+ Shift) |
| Nudge | ↑ ↓ ← → (+ Shift ×10) |
| Cancel the drawing in progress | Esc |
| Join areas (two GA areas / zones, or pick the second area or section) | Shift+U |
| Layers panel (hide / lock kinds of objects) | Shift+L |
| Edit the surroundings (streets, parking, landmarks) | Shift+S |
| Live preview window (the draft as buyers see it) | Shift+P |
| Edit the selected section's seats (section edit mode) | Enter, or double-click the section |
| Leave section edit mode | Esc |
| Join rows / Merge rows (interleave seats) — two rows selected | Shift+J / Shift+I |
| Split the row at the selected seat | Shift+K |
| Renumber the selected seats of a row | Shift+N |
| Open a segment gap before the selected seat | Shift+G |
| Join the segments of the selected row | Shift+M |
| Select the whole row | Shift+R |
| Navigation | Keys |
|---|---|
| Pan with the hand (any tool) | Space + drag |
| Zoom | Ctrl + scroll |
| Scroll the canvas | scroll · Shift + scroll |
| Switch to floor 1–9 | Ctrl+Alt+1…9 |
| Show / hide the reference image and the reference chart (not saved) | Ctrl+E |
| Keyboard shortcuts sheet | ? |
Ctrl+D deselects (as in seats.io); duplicate is Ctrl+J.
Copy and paste between charts
Copy (Ctrl+C, the edit bar or the right-click menu) takes the selected seats and objects — or, with a section selected, the section with its seats and the tables inside it. Paste (Ctrl+V) works in the same chart and in any other chart open in this browser, in the same tab or another one: the Paste button and menu item light up as soon as another designer tab copies something.
- Pasted into its own chart, the copy is offset so it doesn't cover the original; in another chart it keeps its position.
- Every pasted object gets a new id. Seat labels are kept unless they clash in their section (then a fresh label); a pasted section gets a free name (
Stalls (2)), tables get the next table name, and categories the target chart doesn't have are cleared. - The clipboard lives in the browser's local storage (
seatschart-designer-clipboard, versioned, up to about 1.5 MB). Every paste validates it first — types, coordinates, text lengths, image sources (inline PNG / JPEG / WebP / GIF or http(s) URLs only) and counts — so data that doesn't come from the designer is ignored. A copy too large to share stays in its tab. - Embedded designers share the clipboard when they are served from the same SeatsChart origin and the browser does not partition storage between the host sites.
Templates
| Template | Slug | Use case |
|---|---|---|
| Simple | simple | Full toolbar: rows, tables, areas, shapes, categories |
| Sections and floors | sections | Multi-floor venues; section guides per floor; row/table tools |
| Zones | zones | Open areas (lawns, GA); shape tools only; wider canvas |
The template is stored in layoutMeta.mapTemplate inside event_layouts.shapes on first Save draft.
Appearance
- The dashboard designer uses a dark theme by default (
dashboard-dark.css). - Toggle canvas theme (sun/moon) in the toolbar to switch the Pixi background between dark and light while editing.
- Number fields scrub: drag a number field's label left or right to change its value (the pointer shows ↔; Shift moves 10 steps at a time, Esc cancels). The value follows the field's step, minimum and maximum, previews live and is one undo step per drag. Typing and the arrow keys work as before.
Categories and ticket types
- Click + Add category to open a floating popover (name + color only).
- Click a category pill to open the ticket types popover: rename category, change color, add/remove types (Full, Half, PCD, etc.) each with its own price in currency units (stored as integer cents).
- Painting or bulk-adding seats applies the active category name and tint. Seat
priceon save is the minimum ticket-type price for that category.
Category catalog lives in layoutMeta.categories[]:
{
"id": "uuid",
"name": "VIP",
"tint": 13938431,
"ticketTypes": [
{ "id": "tt-1", "name": "Full", "priceCents": 4990 },
{ "id": "tt-2", "name": "Half", "priceCents": 2495 }
]
}Legacy maps with a single priceCents on the category are migrated to one “Standard” ticket type on load.
Floors (sections template only)
- Floors are controlled by a floating panel at the bottom-right of the canvas (not a top strip).
- Collapsed: shows the active floor name; expanded: list opens upward.
- Add, rename, remove floors; drag ⋮⋮ to reorder.
- Seats and section guides belong to the active floor.
- Abbreviation: renaming a floor (✎ or double-click) also edits its short name, up to 6 characters (
G,L2,Mezz). The buyer's floor picker shows it instead of the name where space is tight (phones, the 3D floor list); screen readers and the tooltip keep the full name. - Persisted in
layoutMeta.floors[]({ "id", "name", "order", "abbreviation"? }) andlayoutMeta.seatFloors(seat id → floor id).abbreviationreaches the published geometry only on floors that set one.
Seat properties
Select one or more seats; the Seat panel in the inspector edits them together (one undo step per change):
| Field | Meaning |
|---|---|
| Label | Unique seat ID used by the API, orders and tickets (seats.label). Single selection only. |
| Printed label | Short text drawn on the seat and shown to buyers (e.g. 12). Defaults to the label. |
| Row / Section | Structural membership. Labels must be unique within a section. |
| Category | Sets color and price (prices follow the category). |
| Seat type / Entrance | Chair, stool, bench or couch; an optional entrance shown to buyers. |
| Accessibility | Grouped checkboxes (Accessibility, Hearing impaired, Comfort and view) plus an optional view note — see Accessibility and amenities. |
| Row group | Label position on the buyer map and an entrance for every row of the selection. |
| Displayed type | What buyers read as the kind of the object (seats.io displayObjectType), e.g. Wheelchair space, Sofa, Box. Blank keeps the default (Seat). Tables, GA areas, booths and sections have the same field. |
With several seats selected, fields that differ show Mixed; toggling a flag applies it to all of them (e.g. mark 4 seats as wheelchair spaces in one click). Wheelchair and companion seats get a small badge on the canvas and a legend entry in the bottom-right corner.
The displayed type is saved once per chart in layoutMeta.displayTypes (object id → type, at most 64 characters) and published in the geometry only when a chart sets one. Buyers see it as the caption over the name: in the seat tooltip (Sofa instead of Seat), on the section and general admission cards (hover and phone) and in the step of an area sold as a whole (Booth, Camarote), and host events report it as displayObjectType; an event's published extraData.displayObjectType still wins over it.
Label rows
Label rows… (Seat panel or right-click menu) numbers the selection with a live preview on the canvas:
- Rows: detected from seat positions (angled rows work); use One row (curved) for arcs. Letters (A–Z then AA…), numbers, roman numerals or lowercase, start and optional end value, top→bottom or bottom→top. Keep current rows reuses existing row labels.
- Seats: consecutive, odd, even, up-down ascending / descending, letters, lowercase or roman; start and optional end value; left→right or right→left. See Labelling algorithms.
- Prefix, separator and optional section.
Result per seat: label = prefix + row + separator + seat (e.g. 101-A-12), printed label = seat value (12), row = A. Apply records one undo step (48-step history); Esc or Cancel restores the previous labels.
Row geometry (curve, skew, spacing)
Select two or more seats (Shift- or Ctrl/⌘-click to add or remove seats, Shift + drag a box, or the box-select tool). The inspector shows Row geometry for the rows it detects (angled rows work; a single bent row is recognised as an arc):
| Field | Meaning |
|---|---|
| Curve | Degrees subtended by the first row (−180…180). Rows become concentric arcs; seats keep their spacing along the arc. Positive values bring the row ends towards the stage side (first row). |
| Skew | Shears each following row along the row direction (−45…45°). |
| Seat spacing | Centre-to-centre distance in a row (never below the overlap distance). |
| Row spacing | Distance between consecutive rows. |
The same values are on the canvas: ● (above the first row) bends the rows and follows the pointer; ■ after the first row changes seat spacing; ■ below the last row changes row spacing; ◆ before the last row skews. Sliders and handles preview live; releasing records one undo step. Straighten rows resets curve and skew.
Seats stay the source of truth (positions are saved as usual). To re-edit the same values later, the designer stores the row parameters in layoutMeta.rowBlocks (id, seat ids per row, seatSpacing, rowSpacing, curve, skew, angle, origin, rowOffsets). Moving or rotating the block keeps the stored values; editing seats by hand falls back to values inferred from their positions. Row geometry is offered for selections up to 12,000 seats.
Arrange: rotate, nudge, grid, align, layers
- Rotate: drag the round handle off the top-right corner of the selection (Shift snaps to 15°), or use the Arrange panel (±15° buttons or an exact angle). Seats rotate around the selection centre (discs stay upright); rectangles, ellipses, images, text, icons and tables store a
rotation(degrees); polygons, lines and sections rotate their points. Rotating a rectangular/elliptic GA area turns it into a polygon area. - Nudge: arrow keys move the selection by 1 px (one grid step when snapping), Shift ×10. Holding a key records a single undo step.
- Grid snapping: the magnet button in the edit bar turns snapping on; the number next to it is the grid size (2–400 px, saved per browser). Snapping applies to seat / shape / section drags and to every drawing tool. The visible grid follows the same step.
- Align / distribute: works for any mix of seats and shapes (left, centre, right, top, middle, bottom; distribute needs three items).
- Layer order: Bring to front / forward / backward / Send to back for shapes, images, text and icons (also
Ctrl+],Ctrl+[, with Shift for front/back). The order is the order ofshapes[]and is used by viewers.
Row tools: join, split, interleave
Rows are edited from a seat selection — the Rows box under Label rows… in the Seat panel, the right-click menu or the keys below. Each tool is one undo step and is saved with the draft like any seat edit.
| Tool | Selection | Key | Result |
|---|---|---|---|
| Join | Two rows | Shift+J | One row, end to end: the row that starts first along the row direction comes first. |
| Interleave | Two rows | Shift+I | One row with the seats alternating (1st of the first row, 1st of the second, 2nd of the first…) — for staggered rows numbered as one. |
| Split here | One seat | Shift+K | The selected seat and the seats after it become a second row; that part stays selected so you can move it. |
- Rows are recognised from the designer's own structure first (row parameters stored by Row geometry,
layoutMeta.rowBlocks), then from shared row labels (section + row), then from seat positions — angled rows work. - Join and interleave relabel both rows with the labelling scheme of the first row (numbers, odd / even, letters with the skipped letters, roman, up-down; see Labelling algorithms) and keep its label format (
prefix + row + separator + seat, e.g.101-A-6). Ascending numbering continues from the first row's first seat; a descending row keeps its lowest number at the far end. The second row takes the first row's row label and section. Unrecognised numbering falls back to 1…n. - Split keeps every label (tickets, orders and holds are unaffected); the two parts become separate rows for Row geometry (curve, skew, spacing). Splitting needs a seat inside a labelled row (or a row with stored geometry); the first seat of a row can't start a new one.
- Selections above 4,000 seats are not analysed (the panel explains why).
- The hint bar lists the row tools whenever the selection allows them (
Shift+J Join rows · Shift+I Merge rows).
Renumber seats (part of a row)
Select seats next to each other in one row — part of it or all of it — and press Shift+N (or Renumber in the Rows box, or the right-click menu). Every other seat keeps its label.
- Numbers: 1, 2, 3 with a Step, Odd or Even (steps of 2; the start moves to the right parity). Start at defaults to the run's current first number.
- Direction: along the row or reversed. Prefix and Suffix wrap the number (
B12,12a). - Result per seat: printed label =
prefix + number + suffix; the label keeps its row part (101-A-+B41=101-A-B41). - The canvas previews the new labels. A label already used by another seat of the section is a conflict — the same
duplicate_labelrule as the chart check and publish: the panel lists it with Show and Apply stays disabled. - Apply is one undo step; Esc or Cancel restores the labels. Seats with a hole between them, or from two rows, are refused with a hint.
Segmented rows
A segmented row is one row with gaps — around a column, a camera platform or an aisle. It keeps one row label and one numbering, Select row (Shift+R) selects every segment, and the buyers' orphan rules treat it as one row.
- Gap here (
Shift+G, Rows box → Segments): select the first seat after the column; that seat and the ones after it move along the row by one seat position. With one seat selected per row, the same gap opens in several rows at once. - The Rows box shows the segments and the Gap size (px) of the selected row; Join segments (
Shift+M) removes the gaps and puts every seat back. - Curve, skew and spacing (Row geometry) keep the gaps: the segments of a curved row stay on its arc.
- Stored with the row parameters (
layoutMeta.rowBlocks[].params.gaps:{ row, after, size }) and, for labelled rows, aslayoutMeta.rowSettings[section][row].segmented: true— published in the geometry, so the buyer viewer keeps one row for the orphan rules. Theorphan_gapcheck ignores gaps inside a segmented row. Rows without a row label get the gap but not the flag.
Intertwined rows
The Intertwined rows tool (in the Row tools group, R) draws two rows on one line whose seats alternate — first seat of the first row, first seat of the second row, second seat of the first row… — as in tribunes where two rows are staggered. Click the start, then the end, like a single row.
- The second row sits slightly behind the line, so consecutive seats zig-zag and each row keeps the normal seat spacing.
- Both rows are labelled right away with the next two free row letters (
CandDafter rowsAandB, skippingIandO):C-1,D-1,C-2,D-2…, the seat number printed on the seat. Each row is numbered from 1 along the drawing direction. - They are ordinary rows afterwards: row settings, renumbering and validation see two rows. Relabel them with Renumber (one row at a time) rather than Label rows…, which groups seats by position.
Selection layers
Layers in the edit bar (or Shift+L) opens a small panel with one row per kind of object, each with an eye (hide) and a lock:
| Layer | Objects |
|---|---|
| Background | Background image and decorative images |
| Reference image | The tracing image (its lock is the Lock of the Images panel) |
| Surroundings | Streets, parking and landmarks around the venue (edited apart, see Surroundings) |
| Zones | Zone outlines and names |
| Sections | Section outlines |
| Seats, tables & GA | Seats, tables, GA areas and booths |
| Shapes & icons | Rectangles, ellipses, polygons, lines, icons, the focal point |
| Labels & text | Text objects, seat numbers, section / zone / GA names |
| Foreground decorations | Shapes, text, icons and images shown above the seats (see Icons, appearance and text) |
- Hidden or locked objects can't be clicked, box-selected, brushed or selected with Ctrl+A; whatever was selected on that layer is deselected. Drawing tools keep working.
- Show all and Unlock all reset the panel; the layers button shows how many layers are hidden or locked.
- The settings are saved per chart in the draft (
layoutMeta.layers, only the layers that differ from the default, e.g.{ "seats": { "locked": true } }). They are designer-only: the published geometry never carries them and buyers always see every layer. Undo / redo don't change them.
Surroundings
Streets, parking lots, outside entrances and landmarks — the context around the venue — live in their own Surroundings layer, drawn under zones, sections and seats.
- Edit surroundings: the trees button in the edit bar,
Shift+S, the Surroundings row of the Layers panel or Chart properties → Surroundings. While you edit them the venue is dimmed and locked, the rail offers selection, shapes (rectangle, ellipse, polygon), lines, text, images and icons on every preset, and everything you draw joins the layer. Esc or Done goes back to the venue. - While you edit the venue, the surroundings are shown dimmed (as buyers see them) and can't be clicked, box-selected or selected with Ctrl+A. The Layers panel hides or locks them like any layer.
- They never count: the chart check, seat counts and capacities ignore them.
- Show to buyers (Chart properties → Surroundings, on by default): buyers see the surroundings dimmed under the map, never selectable. Hosts override it with the renderer option
showSurroundings: true | false. - Saved as ordinary shapes with
layer: "surroundings"; the chart setting is written only when off (layoutMeta.showSurroundings: false). Both reach the published geometry only when present, so maps without surroundings keep their geometry hash.
{ "type": "rectangle", "id": "street-1", "x": -400, "y": 600, "w": 2400, "h": 80, "fill": "#cbd5e1", "layer": "surroundings" }Live preview window
Live preview (the window button in the top bar, or Shift+P) opens a second window with the buyer viewer rendering the current draft — not the published map — and follows every edit about a third of a second later.
- Nothing is saved or published to update it: the designer sends the draft (the seats and shapes the next save would send) over a same-origin
BroadcastChannel. There is no server round trip per edit, and the preview never calls the API. - Light / Dark and Desktop / Phone (390 px) switch in the preview header. The camera stays where you put it while the draft changes.
- Every seat is shown available, with the draft's categories, prices, ticket types, booking limits, surroundings and row labels.
- When the browser blocks the window, the same preview opens in a panel over the canvas. Closing or leaving the designer marks the preview as closed.
Sections
Use Polygon section (Sections group) and click the outline; click the first point or double-click to close. The section gets a name (Section 1, …) and owns the seats inside: their sectionId becomes the section name, which scopes label uniqueness, validation and the buyer drill-down (the viewer lists sections by name). Rectangular section guides behave the same way.
Select a section to edit Name (renaming updates its seats), Category, Colour, run Assign seats inside after reshaping, Select seats, or Apply category to its seats. Moving, nudging or rotating a section moves its seats with it. Deleting a section keeps the seats and clears their sectionId.
Persisted as sectionGuide shapes: points (outline), x/y/w/h (bounding box, kept for older consumers), name, color, category, floorId, bookable. bookable and category are published with the chart on the sections that set them, so buyers can book a bookable section whole (Book section) and hosts read its sectionCategory; charts published before 2026-10-29 get them on their next publish.
Section edit mode
Double-click a section (select tool), press Enter with a section selected, or use Edit seats in this section in its panel to work inside it:
- Only the section's seats (the ones whose
sectionIdis its name) can be selected, moved, labelled or deleted; box select, the brush, Ctrl+A and select-same-type stay inside it. - Everything else — other sections, their seats, shapes, GA areas, zones — is dimmed and locked.
- The rail keeps the selection and seat tools (rows, tables); other tools explain that they need you to leave first.
- Seats you draw inside the outline join the section automatically (one undo step with the drawing).
- A breadcrumb at the top of the canvas (Map › Section · Editing · N seats) shows where you are. Leave with Esc, Done, the Map link, a double-click outside the section, preview mode, another floor or Locate in the chart check.
The mode is a view: nothing about it is saved.
Join areas
Merge two GA areas, two zones or two sections that overlap or touch into one outline (Shift+U, the right-click menu, or Join with another area / section in the area or section panel):
- Two GA areas or two zones selected (click, then Shift+click): they are joined at once.
- One area or section selected: the next click on the canvas picks the second one (the cursor turns into a crosshair; Esc cancels).
- The first one keeps its id, name, category, colour and settings and becomes a polygon; the second is removed. GA capacities add up (max 100,000) and GA tables standing in the second area move to the first. Seats of the second section join the first (their
sectionIdfollows); explicit members of the second zone move to the first. - Outlines may overlap, share an edge or a part of one, or be a few screen pixels apart (they snap together). Areas that don't overlap or touch, or lie on different floors, are refused with a message. Ellipses become 32-sided polygons; a hole left between the two outlines is filled.
- One undo step. The union is a small dependency-free helper.
Zones
Zones group the map above sections: Lower bowl, Club level, Floor. Use the Zones tool group (rectangle or polygon) and draw the outline; the zone is named Zone 1, Zone 2, … and gets its own colour. Zones are drawn beneath everything else with a dashed outline; seats, shapes and sections inside stay clickable — click an empty spot inside the zone to select it, press again and drag to move it, drag the corners (rectangle) or vertices (polygon) to reshape it.
Membership — resolved the same way by the designer, the viewer, best available (zone) and reports:
- An explicit assignment wins: a section's Zone, a GA area's Zone, or selected seats assigned to a zone.
- Otherwise a seat follows its section's zone.
- Otherwise anything whose centre lies inside the zone outline belongs to it.
The Zones panel in the inspector lists every zone with its seats / sections / GA counts. Select a zone to rename it (unique, up to 64 characters), change its colour, Select seats or delete it. With seats, a section or GA areas selected, use Assign selection on a zone row, or the Zone field in the selection's inspector; Automatic (from outline) clears the explicit assignment. The field shows whether the current zone is explicit or inherited. Deleting a zone removes its explicit assignments; seats keep their sections.
Persisted with the other layout shapes and published in the geometry: { "type": "zone", "id", "name", "x", "y", "w", "h", "points"?, "color"?, "floorId"? }, zoneId on sectionGuide / gaArea shapes, and layoutMeta.seatZones (seatId → zoneId). Maps without zones keep their geometry hash.
Focal point — the map focal point (focal-point tool on the toolbar) is where best available looks first: blocks rank by the sum of their seats' distances to it. A zone can have its own focal point — in the Zones panel, Focal point: type X/Y, Pick on map, Use map focal point or Clear — and seats of that zone then rank by distance to it (for example a side stand facing its own end of the pitch). Without any focal point the ranking starts from the middle of the available seats. Stored as focal: { x, y } on the zone shape; the geometry carries it only when set.
Tables and booths
Round / rectangular table tools draw a table body with a label (T1, T2, …) and seat the guests around it (T1-1, T1-2, …). Select the table body to edit Label (seat labels follow), Seats (re-flows: existing seats keep their ids, new ones are added or the last ones removed), Shape, Diameter / Width × Depth and Seat guests at both ends. Moving or rotating the table moves its seats; deleting the table deletes its seats. Persisted as a table shape (geom, x/y/w/h, rotation, name, seatIds, endSeats) plus layoutMeta.tableBooking.seatTables.
Layout (rectangular tables, as seats.io): All sides (guests on both long sides and one at each end, endSeats: true), Both sides (the default) or One side (every guest on the bottom long side, oneSide: true; the table grows to fit them). Open spaces (0–20, round and rectangular tables) leaves that many chair positions empty while the spacing stays as if they were taken: a round table with 6 seats and 2 open spaces places its seats where 6 of 8 chairs would sit. The open spaces come just before seat 1; rotate the table to face them where you need them (towards the stage, a pillar). Both values re-flow the seats in one undo step, and oneSide / openSpaces reach the published geometry only on tables that use them. Buyers and the 3D view see the seats where they are; nothing is drawn in an open space.
Booth draws a single bookable rectangle: it is published as a GA area with capacity 1 (booth: true, named Booth 1, …), so it reserves and sells as one object through the existing GA flow (/ga/reserve, orders, holds) and uses the active category's price. A booth has no individual seats by design — use a table when guests are sold per seat.
GA areas (rectangle, ellipse or polygon area tools): a newly drawn area becomes the whole selection and the inspector opens on General Admission first — name, capacity (1–100 000, applied on Enter or when you leave the field; Escape restores it), category (its price and colour come from layoutMeta.categories) and, for polygon areas, corner smoothing.
GA tables
The GA table tool (Table tools group, or N; also on the Zones preset rail) places a round table with 6 places where you click — for cabaret floors, VIP lounges or festival tables sold without numbered seats.
- Inside a GA area the table stands in it (
parentId): its places are part of the area's capacity. The area's panel shows N GA tables · P places · S standing places left; the table's panel shows which area it stands in. Moving a table re-checks the area under its centre; moving or nudging the area carries its tables; deleting the area keeps its tables as standalone ones. Outside any area a table is standalone (its places add to the map). - Panel (select the table): name, Places (1–500), category, Shape (round / rectangle, resize with the corner handles) and Booking: Per place or Whole table. Booking follows the table booking modes (Chart properties → Table booking): all tables by seat = per place, all tables by table = whole, custom = per table; changing one table switches to custom and keeps the others on their previous mode. A whole table counts as one object against the order limit (Booking limits).
- Validation:
ga_table_capacity(error by default) fails when the places at an area's tables exceed its capacity (Locate selects the area and its tables). - Saved and published as GA areas of their own:
{ "type": "gaArea", "gaTable": true, "geom": "ellipse" | "rect", "capacity": 6, "parentId": "<area id>" }. On publish the area sellscapacity − places at its tables(never below 0) and each table is a GA area with its own capacity, so holds, orders, reports and webhooks use the GA flow unchanged. The published GA shape carriesgaTable,parentIdandbookAs: "whole"(whole tables only) — maps without GA tables keep their geometry hash. - Whole tables are held in one piece:
POST /api/ga/reserveanswers 409{ "error": "ga_table_whole", "capacity": 8, "requested": 3 }for any other quantity, and a partly held table can't be booked whole. The check reads a flag of the GA row in the same query (no extra round trip). - Buyers see tables drawn as round / rectangular tables with name · places left above their area; clicking a whole table opens the GA card with the quantity fixed to its places (Places (whole table)); per-place tables keep the quantity field.
Areas sold as a whole (camarotes and bistro areas)
Select a GA area (not a GA table or a booth) and choose Sold as in its panel — the seats.io area types:
- Individual places (
generalAdmission, the default): buyers pick a quantity; several buyers share the area. - Whole area (fixed) (
fixedOccupancy): one buyer books the whole area for exactly its capacity — a camarote or box. - Whole area (choose people) (
variableOccupancy): one buyer books the whole area and picks how many people come, from Minimum people to the capacity — a bistro area. Once booked the area is closed to everyone else, even when only the minimum was taken. - Price: Per person (the category's ticket price × people, the default) or Whole area (the ticket price once, whatever the number of people).
- Rules: a whole area holds 1–50 people (one order) and its minimum fits the capacity (
ga_invalid_capacity); an area with GA tables is sold place by place; safe mode keeps how a published area is sold. - Saved and published: the layout
gaAreacarriesoccupancy: "fixed" | "variable",minOccupancyandpriceBy: "area"; the published GA shape addsbookAs: "whole". Maps without whole areas keep their geometry hash. - Booking:
POST /api/ga/reserve,POST /api/ga/confirmand the status changes (book,hold,release) takequantity= the number of people (fixed: the capacity). Every place of the area is held or sold atomically, so a second buyer can never get part of it; any other quantity answers 400ga_area_whole(areaType,capacity,minOccupancy,maxOccupancy,requested). The receipt recordspeoplenext to the places (sales report columnpeople). - Rules of sale: a whole area counts as one object against the booking limits; its people count against quotas (meia-entrada), lots and per-buyer (CPF) limits, with the ticket type of the hold for every person. Best available never picks GA areas. For-sale
areaPlaceson a whole area answers 400ga_area_whole: put it on or off sale whole. - Republishing: a change of how an area with sold or held places is sold (kind, capacity, minimum, pricing) is refused with 409
ga_area_occupancy_locked(objects); areas without sales take the change in every event of the chart. - Buyers: a fixed area opens a card with its people and price and is held in one tap; a variable area asks How many people? (a stepper within its range). Hold callbacks report
areaType,peopleandplaces; arrow keys reach GA areas and Enter opens them. Event Manager → Statuses: Whole areas lists them with Book area (people) and Release.
Icons, appearance and text
- Icon tool (
O): open the icon menu in the edit bar and pick one of 25 venue icons in three groups — Wayfinding (entrance, exit, stairs, elevator, accessible, information, meeting point, parking), Venue (stage, sound desk, VIP, press, ticket office, security) and Services (toilets, bar, food, water, merchandise, cloakroom, ATM, first aid, baby care, smoking area, Wi-Fi) — then click to place it. Select an icon to change kind (same grouped grid in the inspector), Size, Colour and an optional Name (for example Entrance A: buyers find the icon by this name in the 3D view's Places list); every change is one undo step. - Published icons stay compact:
{ "type": "icon", "icon": "wheelchair", "x", "y", "w", "h", "color" }(plusrotation/floorId/namewhen used) — the icon id and its transform, never SVG paths. Each kind is drawn once into a shared white glyph texture on a tinted badge (no per-icon graphics); the buyer viewer downloads the glyph table as a separate small chunk, only for maps that place icons. - Appearance: rectangles, ellipses and polygons have Fill and Stroke colours; lines have Stroke (
fill/stroke, falling back tocolor). - Text: content, colour, Text size (
fontSize, 8–120 px) and Bold / Italic (bold: true,italic: true, written only when on). The designer, the buyer viewer and the 3D view draw the style. - Foreground: a selected shape, line, text, icon or image (one or several) can be shown above the seats (seats.io Foreground decorations: canopies, pillars, overlays). Buyers see it on top of the seats, but it never takes a click, tap or hover: the seats underneath stay pickable. Saved as
layer: "foreground"on the shape (published only when set); the Layers panel has a Foreground decorations row to hide or lock them while editing. The buyer viewer loads the foreground layer from a separate small chunk, only for charts that use it.
Viewers render rotation, fill/stroke, text size, icons and table bodies. The published geometry only gains these keys on shapes that use them, so existing maps keep the same geometry hash.
Reference image
In Images, once a reference image is loaded: Opacity, Scale (% of the image's pixel size), X / Y position, Lock (disables the controls) and Reset placement. Stored in layoutMeta.referenceImage (frame, opacity, locked); designer-only — viewers never draw the reference image.
Calibrate the reference image
Trace at the right scale: with the Calibrate reference tool (M, next to Image on the rail, or Calibrate scale… in the Images panel) click both ends of a distance you know on the tracing image — a stage width, a dimension line of the architect's plan — and type its real length (metres or feet). The reference image is scaled around the middle of the line so that this distance measures the typed length at the chart scale: 1 m = 64 px (seats are placed 32 px = 50 cm apart; 1 ft ≈ 20 px).
- While you draw, a dimension line shows the length in pixels; Esc cancels. The dialog also shows what the line measures today.
- Also set the grid to 1 m / 1 ft switches the snapping grid to that unit, so the grid matches the calibrated drawing.
- Calibration needs a reference image that is not locked; the result is a new reference placement (
layoutMeta.referenceImage.frame) and one undo step. Factors above 50× or below 2% are refused (check the unit). - Like every reference setting it is designer-only: buyers never see the reference image.
Reference chart
Trace or compare against another chart: Chart properties → Reference chart draws another published chart — or the published version of this chart — faded under your drawing.
- Choose another chart… searches the workspace's published charts (dashboard). An embedded designer token can only read its own chart, so embeds offer the published version of the chart being edited.
- Show under the drawing or
Ctrl+Eshows and hides it (together with the reference image); Opacity sets how faded it is; Change and Remove pick another one or drop it. - It is read-only and session-only: it never becomes part of the map, is never saved or published, and only you see it. Seats are drawn as dots and sections, areas, tables and shapes as outlines, in one layer under everything you draw.
Import seats from CSV
Import seats from CSV (edit bar) accepts a file or pasted text (comma, semicolon or tab; header optional):
section,row,seat,x,y,category,accessibility
101,A,1,120,200,Orchestra,
101,A,2,152,200,Orchestra,wheelchair
101,B,1,120,236,Balcony,companionx/yare seat centres in map pixels (±10,000).- Accessibility: one
accessibilitycolumn withwheelchair|companion|restricted_view|obstructed_view, or separatewheelchair,companion,restricted_view,obstructed_viewcolumns withyes/1. - Result per seat:
label = section-row-seat(e.g.101-A-1), printed label = seat,rowLabel= row,sectionId= section.
The dialog previews the seats (count, sections, rows, a dot plot) and lists problems with line numbers: missing columns, non-numeric or out-of-range coordinates, empty seat numbers, duplicates within a section (errors block the import); unknown flags, new categories (created on import) and overlapping seats are warnings. When the positions overlap seats already on the map, the block is placed to the right of the current content. Imported seats are added (never replacing anything), selected, and recorded as one undo step.
Labelling, accessibility, entrances and palettes
Labelling algorithms
Label rows… offers the same algorithms as the API (shared module @seatschart/sdk):
| Rows | Seats |
|---|---|
| Letters A, B … Z, AA · Numbers 1, 2, 3 · Roman I, II, III · Lowercase a, b, c · Keep current rows | Consecutive 1, 2, 3 · Odd 1, 3, 5 · Even 2, 4, 6 · Up-down ascending 1, 3, 5 … 6, 4, 2 · Up-down descending 5, 3, 1, 2, 4, 6 · Letters · Lowercase · Roman |
- Start at / End at: the end value is optional; below the start the sequence counts down (
10 → 1). The panel reports a range that is too short for the selection. - Skip letters (default
IO): never used, also after the Z → AA roll-over (Y, Z, AA, AB). - Right to left inverts the seat direction; Bottom to top inverts the rows. Multi-row selections are labelled row by row.
- Row labels on the map: Hidden, Left end, Right end or Both ends for the labelled rows (stored per row in
layoutMeta.rowSettings). The buyer map prints them next to the row ends once seats are large enough on screen. - The preview lists the first rows and always the last one, so the end of the sequence is visible before applying.
Accessibility and amenities
The Seat panel groups the flags as checkboxes (mixed state for multi-select):
| Group | Flags |
|---|---|
| Accessibility | Wheelchair space (space type: fixed removable seat or no seat in place), Semi-ambulatory, Plus-size, Companion seat |
| Hearing impaired | CART captioning, Sign language interpretation |
| Comfort and view | Aisle seat, Restricted view, Obstructed view, Not for vertigo sufferers, Limited legroom, Lift-up armrests |
Seat type (Chair, Stool, Bench, Couch) is shown to buyers. Storage stays compact and backward compatible: the four original flags remain booleans, every newer flag is one bit of accessibility.amenities (16 no seat in place, 32 semi-ambulatory, 64 plus-size, 128 CART, 256 sign language, 512 vertigo, 1024 limited legroom, 2048 lift-up armrests, 4096 aisle seat) and accessibility.seatType is written only when it is not chair. Maps that do not use them keep their geometry hash.
Entrances
An Entrance (e.g. "Gate 3") can be set on a seat, on the rows of the selection (Seat panel → Row), on a table, a section or a GA area. Buyers see the most specific one (seat → row → table → section) in the tooltip; the object info API and the event / chart reports return it as entrance.
Category palettes
Palette in the categories bar applies one of ten curated palettes (Rainbow, Polar sunset, Frosty autumn, Vibrant nature, Scenery, Whiteboard, Blackboard, Platinum, Ivory, Structures) to every category in order, with a live preview. Badges mark colour-blind safe palettes (checked by simulating deuteranopia and protanopia) and the backgrounds each palette reads well on (Light / Dark). One undo step.
3D view
Buyers can switch any published map to 3D (viewer toolbar, or defaultRenderer=3d). Without authored values every chart gets automatic tiering: rows rise away from the stage. Chart properties → 3D view, the Section panel and the Zones panel author the 3D instead; charts that author nothing render exactly as before.
3D seat picking
Allow 3D seat picking (top of the 3D view panel, seats.io's go-live switch) decides whether buyers get the 3D seat picker by default. A checklist comes first:
| Check | Level | When |
|---|---|---|
| Sections without a foundation | blocks the switch | A section has neither its own 3D (Custom 3D) nor a whole-chart 3D |
| Multiple floors | warning | The chart has two or more floors: test it in 3D before going live |
| Booths | warning | Booths show as flat areas in 3D |
| Images | warning | Background images and image objects are not drawn in 3D |
- While a blocking check fails the switch can't be turned on ("3D seat picking cannot be enabled until all checks above pass"); turning it off is always possible. Turning it on asks Enable 3D seat picking? — all events of this chart will use the 3D seat picker instead of 2D.
- Remove 3D (with confirmation) deletes every 3D setting of the chart — foundations (section, zone and whole-chart 3D), floor heights, 3D tables, light, spectator target, billboards — and turns 3D seat picking off. It is one undo step until you save.
- Stored as
layoutMeta.settings3d.seatPicking(true/false). When it was never set, it is on for charts that author 3D (areas or floor heights) and off for the others, so charts that already opened in 3D keep doing so. - Embeds:
defaultRenderer: "auto"(seats.iodefaultRenderingEngine: "auto", the seats.io default) opens 3D only when 3D seat picking is on for the chart and the device can show it;"2d"always starts in 2D. An explicitdefaultRenderer: "3d"still opens 3D when the switch is off, as seats.io's renderer does for a forced 3D engine: hosts can test a chart in 3D before going live. Buyers can switch to 3D by hand from the toolbar either way.
Sections, zones and the whole chart
Turn on Custom 3D to set, in metres at the chart scale (seats 50 cm apart, 1 m = 64 px):
- Elevation: height of the front row above its floor (−20 to 100 m). A negative value sinks the area into a pit (an orchestra pit or a sunken floor): the ground opens above it and its stand starts at the front row;
- Rake: Per row (rise between consecutive rows, 0–3 m, default 0.4 m) or Front to back (total rise from the front row to the back row, 0–100 m). Rows are ranked by distance from the focal point, as in automatic tiering;
- Stand under the seats (default on): raised seats stand on a solid stand down to the floor; off, the tier floats like a balcony or a suspended box, Tier depth deep (0.1–20 m, default one step);
- Back row height (front-to-back rake, seats.io's back height): the height of the last row. To stack two rings without a gap, give the upper ring the back row height of the ring below as its elevation.
A section's values win over its zone's; Whole chart (Chart properties) applies to every seat outside a section or zone with custom 3D. Reset to automatic removes the values. Fields accept a decimal point or comma, show their range and refuse values outside it (Esc restores the last value).
Assisted foundation
Assisted foundation (Chart properties → 3D view) suggests the 3D of your sections from their rows, like seats.io's assisted foundation. Choose Selected section (with a section selected on the map) or All sections, then Suggest foundation. For each section it works out which row is the front (the row nearest the spectator target or focal point, whatever the row labels say, so rows numbered from the back work too) and how deep the rows are on the map, then suggests:
- a raked stand: the front row on the ground (1 m up when the venue has a flat floor in front of it), each row about 45 % of its depth higher than the row in front (≈ 24°);
- an upper tier (a section entirely behind another one in the same direction): it starts just above the back row of the tier in front, rises more steeply (≈ 60 % of the row depth) and floats as a stepped tier with a matching depth;
- flat: a single row, rows that do not step away from the stage (side-on), or a floor block in the middle of the venue with stands around it.
The preview lists every section with its rows, row depth and the suggested front and back heights. Sections whose 3D you set by hand are flagged with your current values and stay unticked: they are only replaced when you tick them. Apply writes the ticked sections as one edit (one undo reverts it); Cancel changes nothing. The results are ordinary section values you can refine afterwards. Charts without sections get one suggestion for the whole chart.
Light
Light (Chart properties → 3D view) sets the sky and the lights of the 3D view: Daytime (default, the look of earlier versions), Sunset, Night or Indoor, the seats.io presets exteriorDaytime, exteriorSunset, exteriorNight and interior. The open preview follows the choice. A host page can override it with the renderer config settings3d: { lightPreset }.
Spectator target
Seats face the focal point by default. Turn on Custom target to set where spectators look instead: map X / Y in px and a height in metres (default 1 m). Seats face it, rows rise away from it and the view from a seat looks at it. Place on map takes the next click on the canvas (Esc cancels); Use focal point copies the focal point. A target alone keeps the automatic tiering.
Floors
On multi-floor charts (sections template) each floor gets a height (0–200 m); Stack floors 6 m apart fills them in order. In 3D the floors are stacked at those heights and the camera looks at the active floor: the floors below it stay visible, dimmed and not selectable, and the floors above it fade to a translucent outline that takes no clicks, so the active floor shows through (frisas, boxes and galleries of a theatre).
Buyers move between floors with the floor elevator on the left of the 3D view (floor names from the top floor down; Tab, arrow keys, Home / End): the camera glides to frame the chosen floor and it becomes the active floor, as in 2D. The 3D view opens on the floor of the buyer's selection, else on the lowest floor. Floors without heights are not stacked: the elevator shows one floor at a time.
Floors push the floor above (off by default): each floor then starts at least 2.5 m above the highest seat of the floor below, so a raked lower floor never cuts through the one above it.
3D tables
With custom 3D, tables are drawn as a top on a pedestal and the seats around a table sit level at the area's elevation and face it. 3D tables (on by default) turns this off.
GA tables (sold by place or as a whole) are drawn as 3D tables too, with a chair at each place (one instanced mesh, at most 60 chairs per table). The chairs show availability like the seats: free places in the category colour, the buyer's own places in the selection colour, held, sold and not-for-sale places in the same muted tones as 2D (a table sold whole is one colour). They follow the live counts and the buyer's selection without rebuilding the scene.
Markers
The chart's icons (entrance, exit, bar, toilets, first aid, stairs, elevator, information…) appear in 3D as round badges that face the camera, in the icon's colour and drawn from the designer's icon set. They float above the seats around them, keep a readable size on screen at any zoom, and step aside up close when they would cover seats. Places in the 3D view lists the active floor's markers by name (the icon's Name, else its kind, numbered when a kind repeats); choosing one moves the camera to it, so keyboard and screen reader users find Entrance A without the canvas.
Billboards
Billboards are LED and advertising boards in the 3D view, like the boards around a pitch (seats.io 3D billboards). Draw them with the Billboard tool (K, or Draw a billboard under Chart properties → 3D view): click to add points, Enter or a double-click finishes, Esc cancels and Backspace removes the last point. On the canvas a board is a line with a thin band; the 2D buyer map does not show it.
Click a board (Select or Billboard tool) to select it: drag a point to move it, double-click the line to add a point, Alt+click a point to remove it, drag the line to move the whole board, use the arrow keys to nudge it (Shift: 10 px) and Delete to remove it. Its panel sets the Height (0.2–20 m, default 1 m), the Bottom edge above the ground (−20 to 100 m, default 0) and how its images move: Scroll, Scroll back or Still. A chart has up to 64 boards of up to 64 points each.
Sponsor images (up to 12 per chart) are shared by every board and scroll along all of them in their order; reorder them with the arrows and remove them with ×. The designer re-encodes each image to WebP in the browser (a strip tile at most 256 px high); on save the API stores it on the map-asset CDN (/assets/map/:tenantId/:id/billboard_<hash>.webp, content-addressed) and keeps the URL, or keeps it inline when no map-asset storage is configured. Images added before any board is drawn stay in the draft and are published with the first board.
In 3D every board is part of one merged mesh textured by one strip that packs the images side by side. The strip loads after the scene is ready (boards show a plain panel until then) and scrolls by moving the texture, with no redraw per frame, at most 30 frames per second and only while a board is in view. With reduced motion the images stay still. Boards cannot be clicked.
{
"settings3d": {
"billboards": [
{ "id": "led-north", "points": [{ "x": 0, "y": -60 }, { "x": 960, "y": -60 }], "height": 1, "elevation": 0.2 },
{ "id": "led-east", "points": [{ "x": 1000, "y": 0 }, { "x": 1000, "y": 640 }], "scroll": "reverse" }
],
"billboardImages": ["https://cdn.example.com/assets/map/acme/7d0c…/billboard_3f2a….webp"]
}
}Billboards alone keep the automatic tiering. They are validated on every save like the rest of settings3d (400 validation_error for more than 12 images, a URL outside MAP_ASSET_PUBLIC_BASE_URL, a non-raster image, an inline image over 300,000 characters, fewer than two points…) and reach the content-hashed geometry only when the chart has at least one board.
View from a seat
Buyers see the venue from a seat with View from seat (the seat card, the cart, or the renderer command chart.showViewFromSeat(object)): the camera sits at a seated spectator's eye height and looks at the spectator target, or the stage. Drag or use the arrow keys to look around, the wheel or +/− to zoom, Back to overview or Esc to return. In the 3D preview, click a seat to check its sightline; public preview links have a 3D button with the same view. Devices without 3D show the seat's photo / 360° view instead, or a short notice.
Preview and storage
Preview in 3D opens the buyer viewer's 3D scene over the canvas, fed straight from the draft; it follows every edit after a short pause (Esc or the close button hides it). The values are saved with the map in layoutMeta.settings3d (pruned to sections, zones and floors that still exist), validated on every save (400 validation_error for a non-finite value, a value out of range or an unknown key), published in the content-hashed geometry only when set, and kept by chart publish, duplicate, version copy, copy to another workspace and the sandbox copy from production. The light preset, spectator target, tier depth, negative heights and floors pushing the floor above are validated and kept the same way.
{
"type": "layoutMeta",
"settings3d": {
"chart": { "rise": 0.3 },
"sections": {
"stalls-guide-id": { "elevation": 0.5, "rise": 0.45 },
"pit-guide-id": { "elevation": -2.5, "rise": 0.3 },
"balcony-guide-id": { "elevation": 7, "rise": 6, "riseMode": "total", "foundation": false, "thickness": 1.2 }
},
"zones": { "club-zone-id": { "elevation": 3 } },
"floors": { "ground": 0, "upper": 6 },
"pushFloors": true,
"tables": false,
"lightPreset": "exteriorSunset",
"spectatorTarget": { "x": 640, "y": 120, "height": 1.5 }
}
}Validation
Chart check (live). With nothing selected, the inspector shows the Chart check: totals (seats, GA places, tables, floors) and one row per rule that applies to the map — ✓ passed, ✕ error, ⚠ warning — refreshed about 250 ms after each edit. Locate selects the offenders (seats, GA areas, sections, zones) and zooms to them; press it again to cycle through their groups (one per section or floor, e.g. "56 unlabeled objects · Orchestra"). The focal point row offers Place focal point. While something else is selected, the Validation panel shows the same results with Show buttons.
The browser and the API run the same rules (@seatschart/sdk, about 25 ms for 50,000 seats) at the workspace severity — Settings → Chart validation sets each rule to Off, Warning or Error (admins only):
| Code | Default | Rule |
|---|---|---|
no_objects | error | The chart has no seats and no GA areas |
empty_label | error | Seats or GA areas without a label |
duplicate_label | error | Two seats share a label in the same section |
uncategorized | error | Seats or GA areas without a category while the chart has categories |
overlap | error | Seats overlap on the same floor |
table_too_small | error | A table has fewer than 2 seats |
ga_invalid_capacity | error | GA area capacity missing or outside 1–100,000; areas sold as a whole: 1–50 people and a minimum within the capacity |
ga_table_capacity | error | GA tables in an area hold more places than its capacity |
empty_floor | error | A floor of a multi-floor chart has no seats or GA areas |
empty_section | warning | A section has no seats or GA areas |
outside_section | warning | A seat lies outside its section outline |
orphan_gap | warning | A single empty position splits a row |
images_size | warning | The chart's images (background and image objects) download more than 2,048 KB in total |
multiple_object_types | off | One category is used by row seats, table seats and GA areas |
missing_focal_point | off | The chart has no focal point |
zone_missing_focal_point | off | A zone has no focal point of its own |
Duplicates across sections (seats.io Duplicate objects across sections): a seat's section scope is its section's label, so two sections that share a label (for example "A" on two floors) share one label space. Seat ids never collide — holds and bookings use the id — but two seats labeled "1-1" in those sections read the same to buyers and in reports, so duplicate_label reports them as "2 sections labeled A contain an object labeled 1-1" (sections: 2 on the issue; unlabeled sections likewise). Rename one of the sections or the seats.
Image size (images_size, seats.io chart images tip): sizes are known for inline images and for images uploaded in the designer (it records the encoded size as bytes on image objects and layoutMeta.backgroundBytes; neither is published). Images stored before that have no known size and are not counted. The issue carries the total in bytes.
Errors block publishing and creating events from the chart; warnings don't. Before publishing a map with warnings the designer lists them (Publish anyway / Keep editing). Leaving the designer through the breadcrumb or any dashboard link while errors remain asks Close floor plan with errors?, with Keep editing focused.
On publish, the API validates the saved draft again and rejects error-level issues with 422 (warnings are listed too):
{
"error": "validation_failed",
"message": "Map validation failed with 1 blocking issue. Fix them and publish again.",
"issues": [
{ "code": "duplicate_label", "severity": "error", "objectIds": ["<seat uuid>", "<seat uuid>"],
"message": "2 seats share the label \"A-1\".", "count": 2, "label": "A-1", "sectionId": "", "scope": "" },
{ "code": "orphan_gap", "severity": "warning", "objectIds": ["<seat uuid>", "<seat uuid>"],
"message": "A single empty seat position splits a row.", "count": 1 }
]
}A successful publish returns non-blocking issues in warnings. scope names the section or floor of the offenders; floorId is set on multi-floor charts.
Stored counts. Every draft save and publish stores the per-rule counts (severity-free) on the chart and on the version. The charts library shows a "N validation errors ▾" pill from them (expand it for the list and Fix in designer), and event creation checks the published counts under the current severity — no rule runs again, and a severity change applies at once. Creating an event (single or bulk) from a chart whose published map has error-level issues returns 409:
{
"error": "chart_invalid",
"message": "The published chart has 1 validation error. Fix them in the designer and publish again.",
"chartId": "<chart uuid>",
"issues": [{ "code": "ga_invalid_capacity", "severity": "error", "count": 1, "message": "1 GA area has no valid capacity." }]
}The New event form shows This chart has validation errors — Fix in designer instead of creating the event. Charts published before this check are validated once on first use.
Draft vs publish
| Step | API | Who sees it |
|---|---|---|
| Save draft | PUT /api/events/:eventId/map/draft | Designer only |
| Publish | POST /api/events/:eventId/map/publish | B2C viewer + reservations |
GET /api/events/:eventId/seats(viewer) always returns the last published map.GET /api/events/:eventId/map/draftloads draft or bootstraps from published (dashboard session orx-api-keyonly).- Publish is blocked if the draft removes a sold seat (
409 conflict) or fails validation (422 validation_failed). - Publish is also blocked when the draft removes seats of active listings (
409 listing_objects_removed, withlistings: key andremovedObjectsof each). The designer names the listings and offers Withdraw listings and publish, which publishes withwithdrawAffectedListings: true: the listings are withdrawn in the publish transaction (refused with409 listing_has_holdswhile any of their objects is held) and returned inwithdrawnListings. Changing an event's chart (PATCH /api/events/:eventIdwithchartId) follows the same rule, in the Event Manager Switch chart dialog. - The same flow exists for reusable charts:
PUT /api/charts/:chartId/map/draftandPOST /api/charts/:chartId/map/publish.
Legacy PUT /api/events/:eventId/map now saves draft only (same as /map/draft).
Autosave
The designer saves the draft by itself 2 seconds after the last edit (never while a save or a publish is running). The chip next to the map status shows Saving… → Saved (hover for the time); a failed autosave shows Not saved · Retry and backs off after 3 attempts. Save draft still saves at once.
Autosave is conflict-safe: every designer save sends baseDraftUpdatedAt (the draftUpdatedAt it loaded or last saved; null for a never-saved draft). When the stored draft changed meanwhile (another tab, a teammate, a version restored to draft), the API answers 409 { "error": "draft_conflict", "message", "draftUpdatedAt" } and nothing is written. The designer then pauses autosave and shows This draft was changed elsewhere with Load latest (discard local edits and reload) or Keep mine (save once without a base, replacing the other version). Publishing moves the base to the new version. Saves without baseDraftUpdatedAt (API clients) keep last-writer-wins.
Auto-publish (charts)
Chart properties → Publishing → Auto-publish changes (off by default) publishes the chart right after every successful save — autosave or Save draft — while no event uses the chart, so a template that is still being built is always live for previews and new events without pressing Publish.
- It is stored in the draft (
layoutMeta.autoPublish), never in the geometry hash. - The designer reads the chart's
eventCount: with events the switch can't be turned on ("Used by N events: publish changes manually"), so live events never change by surprise. The API enforces it too:POST /api/charts/:chartId/map/publishwith{ "autoPublish": true }returns 409{ "code": "ERR_AUTO_PUBLISH_CHART_HAS_EVENTS", "params": { "eventCount" } }when an event uses the chart, and the designer shows Paused. - Validation errors stop it ("Not published: fix N errors first"); warnings don't. Each auto-publish is a normal version (note Auto-published, audit metadata
autoPublish: true), with its thumbnail.
Draft save body
{
"seats": [
{ "id": "uuid", "label": "A1", "pos_x": 120, "pos_y": 80, "category": "VIP", "price": 2495 },
{ "id": "uuid", "label": "101-A-12", "pos_x": 160, "pos_y": 80, "category": "VIP",
"rowLabel": "A", "sectionId": "101", "displayedLabel": "12",
"accessibility": { "wheelchair": true, "viewNote": "Step-free access from gate 3" } }
],
"shapes": [ "... layoutMeta, sectionGuide, rectangles, ..." ],
"reference": "data:image/webp;base64,... or https://cdn.../reference.webp",
"background": "data:image/webp;base64,... or https://cdn.../background.webp"
}Omitted seats in the draft payload are removed only on publish, not on draft save.
rowLabel, sectionId, displayedLabel (max 64 chars) and accessibility (wheelchair, companion, restrictedView, obstructedView booleans + viewNote up to 280 chars) are optional and structural (so are accessibility.amenities, accessibility.seatType, entrance and layoutMeta.rowSettings): they are published in the immutable geometry (GET /api/events/:eventId/geometry/:hash), not in GET /seats or /state. Only true flags are stored. Maps without these fields keep the same geometry hash.
Embedding the designer
Partners can put this designer in their own admin with /embed/designer and drive it from their page with SeatsChartDesigner (script /embed/v1/seatschart-designer.js, or <Designer> in @seatschart/react): full reference and the seats.io migration table in Integration guide.
| URL parameter | Meaning |
|---|---|
token | Designer embed token (POST /api/embed-tokens, purpose: "designer"). A token for one chartId opens only that chart. |
chartId | Chart id or partner key. Without it, a workspace-wide token creates a new chart (its id stays in the iframe URL, so a reload never creates a second one). |
mode | normal, safe or readOnly (below). |
features | Base64 JSON { enabled | disabled, readOnly } with the feature names below. |
language, colorScheme, canvasColorScheme | UI language (from the first render), chrome scheme and canvas scheme (light / dark fixes the canvas and hides its switch). |
parent_origin, config | Set by SeatsChartDesigner: the host origin (turns events and commands on) and safeModeOptions, the version to open and the Exit button. |
- Safe mode (
mode=safe) refuses every change that could affect existing bookings, on objects of the published version: deleting them (unlesssafeModeOptions.allowDeletingObjects), changing their labels or row labels, the booking type of tables (by seat, by table, variable occupancy) and the type of areas (area, booth, GA table), and the capacity of areas (unlessallowEditingAreaCapacity). Deletes are refused before anything changes; any other edit is undone right away, with a message saying why. Objects added since the last publication stay free. The chrome shows a Safe mode chip. - Read-only (
mode=readOnly): the user can look around and try changes, but nothing is saved — no autosave, Save draft, Publish, version history actions or auto-publish. The chrome shows a Read-only chip. - Enforced by the API: mint the designer token with the mode (
POST /api/embed-tokenswith"mode": "safe"and optionalsafeModeOptions, or"mode": "readOnly") and the API applies the same rules to every save, publish and version restore made with that token. It answers 403forbiddenwithcodeERR_DESIGNER_SAFE_MODE(plusreasonandobjectIds) orERR_DESIGNER_READ_ONLY. The designer runs the stricter of the token's mode and themodeparameter, and shows the reason if the API refuses anyway. A safe-mode token cannot publish a draft that deletes or relabels published objects, whoever saved it. Tokens minted without a mode arenormal. - Published version (
openLatestDrawing: false): the designer opens the published map. While newer draft changes exist it opens as a view (nothing is saved, so the draft is never overwritten) with Open the latest draft. - Exit appears only when the host registered
onExitRequested; it saves pending edits, then tells the host. - Saving status: the host hears
saving,savedanderror(failed save or draft conflict) throughonStatusChanged, once per change.
Feature names
A feature left out of enabled (or listed in disabled) hides its tools and panels; one listed in readOnly stays visible with its controls disabled. A sub-feature (tables.generalAdmission) also follows its parent (tables).
| Feature | What it gates |
|---|---|
rows, tables, booths, texts, sections, areas, shapes, images, icons, focalPoint | The drawing tools of that kind (rail and keys). |
nodes | The Node tool (A). |
tables.generalAdmission | The GA table tool. |
tables.bookAsAWhole | Booking a table as a whole: the Table booking mode and the GA table Booking choice (read-only hides them). |
tables.variableOccupancy | The variable occupancy fields of a table (read-only hides them). |
3D | The 3D panel (heights, foundation, light, spectator target, billboards), the 3D preview, the 3D fields of sections and zones, and the Billboard tool. |
venueType.simple, venueType.withSectionsAndFloors, venueType.withZones | The venue types an empty chart may take (the start panel's Venue type); a new chart opens with the first allowed one. |
contextActions | The right-click menu of actions on the selection. |
labeling | Seat label, row and section fields, Label rows… and Renumber. |
displayedLabels | The printed label (displayed label) field. |
multipleFloors | Adding, renaming, reordering and removing floors (switching between existing floors stays). |
objectProperties | The inspector panels of the selection (seat, area, text, section, table, zone…). |
publishedSectionLabel | Changing the label of a section that is in the published version. |
referenceImage, referenceChart | The reference image tools and the reference chart. |
firstTimeTutorial | The quick-start tutorial. |
seatViews (seats.io viewFromYourSeat) | Seat view photos and panoramas, and the view-from-seats toggle. |
categoryList, publishing, scan, tableBooking, bookingRules, chartName | The categories strip, Publish and auto-publish, floor-plan scanning, the table booking mode, the booking limits, renaming the chart. |
backgroundImage, categoryKeys | Accepted for seats.io compatibility. |
Unknown names are ignored (SeatsChartDesigner warns about them in the console).
Map images (CDN / object storage)
When configured on the API:
| Env | Purpose |
|---|---|
MAP_ASSET_STORAGE_BACKEND | local or r2 (auto-detect from vars) |
MAP_ASSET_STORAGE_DIR | Local directory (when backend is local) |
MAP_ASSET_PUBLIC_BASE_URL | Public base URL (API path or R2 custom domain) |
MAP_ASSET_R2_* | Cloudflare R2 bucket + API token credentials |
On save, data URLs are uploaded and replaced with public URLs. Served via GET /assets/map/... (local or API proxy) or directly from an R2 CDN domain.
GET /assets/map/... answers with Access-Control-Allow-Origin: * (public, credential-free images), so the 3D view can read billboard sponsor images into a WebGL texture. When images are served straight from an R2 custom domain, allow GET from any origin in the bucket's CORS rules, or the boards keep their plain panel.
Scan floor plan
- Upload a reference or background image (PNG, JPEG, WebP or SVG, or a PDF, up to 8 MiB and 16 megapixels). Keep the original file loaded: the scan reads its full-resolution pixels. A PDF or SVG plan is the best source: see PDF and SVG plans.
- Click Scan floor plan in the Images inspector (or pick Start from a floor-plan image on an empty chart). With no image loaded yet, the button first asks you to pick one, then opens the scan.
- Choose how seats are found — Mark the seating area, click one seat (recommended, the default) or Scan the whole plan automatically — and click Find seats.
- A card above the canvas shows progress (with Cancel scan). When the scan is done, the review opens by itself on the seats to check (see Doubtful seats); the result card behind it tells how many seats were found, in how many rows, and how many items are left to review.
- Detected seats are added as a new, selected group — existing seats are never removed. Detections that land on a seat already on the map are skipped, so re-scanning the same image adds nothing twice. Rows are detected and labelled automatically (
A-1,A-2, … with a prefix when those labels already exist). One Undo removes the whole scan.
Automatic and example modes
- Mark the seating area, click one seat is the recommended path and the most accurate. Step 1: drag a rectangle around the seats on the plan preview (leave out the legend, the title and the stage text). Step 2: click the centre of one typical seat (one more for each different seat style, such as accessible places). The clicked seat tells the scanner the size and look of a seat: when seats are tiny (under about 8 px apart, a small or low-resolution plan, or several small level plans on one page), the area is read again enlarged two or three times, so dots and cells a few pixels wide are found one by one. A click never makes the result worse: a seat the click argues against is kept and flagged for review unless the evidence against it is strong (the scanner already doubted it and it looks nothing like the clicked seat). A click on a seat the scan missed also adds that seat and the seats that look like it, and drops any marks found inside them (their digits). Every clicked seat is kept. With the keyboard, move the crosshair with the arrow keys (Shift for fine steps), press Enter at the two corners of the area and then on the seat; Backspace removes the last mark. Zoom in for small seats; Undo and Clear edit the marks.
- Scan the whole plan automatically needs no marking. Two readers run side by side on the scanner's own CPU (the image goes nowhere else) and check each other. A small neural network trained for seating plans reads the plan at several zoom levels and marks the centre of every seat: circles, squares, chair icons and numbered cells, filled, outlined, shaded or pale, on light or dark backgrounds, in straight, curved or rotated rows. A rule-based reader finds the shapes that repeat across the plan and matches them everywhere. A shape the rule-based reader repeats counts as a seat style only where the network also sees seats, and then fills in seats the network was unsure of (outline cells, rotated blocks); marks of the network that the rule-based reader does not confirm are dropped when they look unlike the confirmed seats (notes, dotted guide lines, the digits inside cells). Row letters, seat numbers, legend swatches, price tables, the stage, titles and faint background patterns are left out. Seats from about 5 px to 160 px apart are found, several seat styles in one plan included; a plan the scanner cannot read seat by seat is read again enlarged; on plans with 6,000 seats or more the network reads alone, so arenas stay fast.
- Limit to an area is on in the recommended path and can be switched on for the automatic scan too: only the rectangle drawn on the plan preview is scanned. Drag a rectangle (drag again to redraw it; Clear area removes it). With the keyboard, move the crosshair, press Enter at one corner and again at the opposite corner; Backspace clears the area and Escape drops a half-drawn one. Seats keep their place on the whole plan.
- After a scan, the result card offers Rescan with examples and Mark wrong seats. Both undo that scan (if it is still your latest change). Rescan with examples opens example mode. Mark wrong seats shows the plan with the seats just found as green dots: click the dots that are not seats (a row letter, a number, a legend swatch — one mark per kind of mistake is usually enough), then Scan again. The detection under each mark is left out, together with the detections that look like it and are as uncertain as it (every row letter, say); a mark on something drawn exactly like a seat (a legend swatch) removes only the small group around it, and confident seats of another style stay. The same mode, example seats and area are kept. When nothing is found, the card offers Mark example seats directly.
- Advanced → Use the legacy edge detection keeps the older contour mode (seat diameter and edge closing) for plans where both other modes fail.
- Results depend on the drawing. Clean plans with distinct seat glyphs scan best; dense historic engravings, photos of paper plans and seats closer than about 5 px may need examples or manual work. Always review the seats before publishing.
Doubtful seats
Every seat the scan finds carries a confidence. The scanner leaves out marks it is almost sure are not seats, flags the uncertain ones instead of keeping them silently, and points out the places where it probably missed a seat (a gap in a row, the end of a row, a mark that looks like the clicked seat but scored just below the line):
- Doubtful seats are outlined in amber on the map; suggested (probably missed) seats are marked in blue with a +. They are not seats yet.
- The review panel opens after the scan and moves the map to each item in turn. For a doubtful seat: Keep (K or Enter) or Remove (Delete or R). For a suggested seat: Add seat (K or Enter) or Skip (Delete or R). Previous / Next (arrow keys) skip around and Esc puts the panel aside (the card offers Review K to come back; the remaining items stay marked). Keep all / Remove all (or Accept all / Reject all when there are suggestions) decide every remaining item at once. Add a missed seat (A) places a seat by hand: click the map where the seat is. Every change is a normal edit: Undo reverts it.
- When every item is decided the panel shows Review done with what was kept, removed and added; Done closes it. Done on the result card keeps every seat that is still undecided and adds no suggested seat.
- When the plan cannot be read seat by seat, the card says so instead of showing a confident count: Too low-resolution to read seat by seat (seats drawn a few pixels apart, a blurry scan: upload a larger image or the original PDF) or No individual seats on this plan (sections or blocks only: upload a plan that draws each seat, or draw the seats). Whatever was found is added for review only.
PDF and SVG plans
A plan exported from CAD, a drawing program or a spreadsheet keeps each seat as drawn geometry. Upload the PDF (or the SVG) itself rather than a screenshot:
- A PDF page is rendered in your browser (200 DPI, reduced to stay within 16 megapixels) and becomes the plan image. For a multi-page PDF, choose the page with the seating plan (arrow keys or Page Up / Page Down move between pages).
- When you scan, the PDF or SVG travels with the image. The scanner reads its closed shapes, rectangles, circles, repeated symbols and text, groups the shapes that repeat like seats, and uses them to confirm seats, add seats the image scan missed and flag marks that stand where nothing seat-like is drawn (text, dimension lines). The result card then says checked against the PDF drawing. Seat coordinates stay exactly on the rendered image.
- A scanned PDF (a photo or bitmap of a plan inside a PDF) has no drawn seats: the image is scanned as usual.
Read row and seat labels
With Read row and seat labels on (the default in the scan settings), the scan also reads the printed row letters and seat numbers, so the scanned seats arrive already labelled.
- Reading runs in your browser with a self-hosted OCR engine (Tesseract, about 4 MB downloaded on first use, then cached). The image goes nowhere else and there is no server cost. It adds a few seconds (about 2–5 s for 500–1,000 seats); a progress bar offers Stop and review and Cancel scan.
- Only small regions are read: the inside of each detected seat, both row ends and the aisles. Seats are grouped into blocks and rows; a label read at either end of a row (or in an aisle) names the row, and a missing or misread label is taken from the block's sequence (A, B, ?, D → C).
- Seat numbering is inferred per row: consecutive, odd, even, or odd/even from the centre or from the ends, in either direction, keeping the printed zero padding (
01). Numbers that follow no pattern are kept as printed, with gaps filled from their neighbours. - Review lists every row with its label chip, its numbering and whether it was read; rows to check are amber (filter To check) and the preview highlights the row under the pointer. Edit a label or switch off Read numbers for a row, then Apply labels. Use automatic labels keeps the classic
A-1labelling; Discard scan adds nothing. - Rows that could not be read get the next free letters and numbers 1…n. A row whose labels would clash with seats already on the map gets an
S2-prefix (its printed numbers and row label stay). One Undo still removes the whole scan. - Tiny or blurred text (numbers below about 8 px tall in the image) cannot be read: upload the original, full-resolution plan. When nothing readable is found, the seats are added with automatic labels and the status line says so.
Viewer behavior
Public /viewer/:eventId and embed iframes:
- Read published maps only.
- Draw the surroundings dimmed under the map, never selectable, unless the chart hides them (
layoutMeta.showSurroundings: false) or the host setsshowSurroundings. - Use category tints from
layoutMeta.categories. - Ticket type dropdown at checkout when categories define
ticketTypes. - Load
background/referencefrom URL or data URL. - Seat tooltip shows every accessibility and amenity flag with its icon, the seat type, the view note and the entrance. The renderer API
findObjectreturnsrowLabel,sectionId,displayedLabel,accessibilityandentrancewhen set. - Row labels are printed at the row ends chosen in the designer once seats are large enough on screen.
- Companion seats selected without an accessible seat beside them show a notice (
selectionValidators.companionSeats). - Optional view-from-seat still (
imageUrl) plus 360 panorama (panoUrl,kind: equirect) viaGET /api/events/:eventId/seat-views. Never joined intoGET /seats. Buyer opens a drag-to-look WebGL lightbox whenpanoUrlis present; photo-only seats keep the flat image lightbox. - In 3D, charts with
layoutMeta.settings3duse their authored heights, rakes, stands, stacked floors and 3D tables; others keep the automatic tiering. - In 3D, the light follows the chart's preset unless the host sets
settings3d.lightPreset; View from seat (orchart.showViewFromSeat) shows the venue from a seat, and devices without 3D fall back to the seat's photo / 360° view. - In 3D, GA table chairs show availability, icons become markers listed under Places, and multi-floor charts get a floor elevator that fades the floors above the active one.
Booking limits
The Booking limits panel of the designer inspector (nothing selected) sets Max seats per order (1–1000) and Max GA tickets per order (1–64); empty = sales-channel defaults: web 8, box office 20, partner 64 seats / 50 GA places. They are saved in the published layout (layoutMeta.bookingRules) and enforced by the API once the map is published:
- Hold: one hold token (one order) never holds more seats than allowed. The first
POST /api/seats/reserveand every append with the sameholdTokencount together; the check runs in the same atomic Redis call as the seat locks, so parallel appends to one token cannot exceed it. - General admission:
POST /api/ga/reservecaps the places per hold at Max GA tickets per order; when that field is empty, Max seats per order applies to GA too. - Best available:
POST /api/seats/best-availablenever picks more than allowed (seats already onholdTokenincluded). - Confirm:
POST /api/seats/confirmandPOST /api/ga/confirmcheck the limit again (defence in depth). - Precedence: the strictest of the sales-channel default, the booking limit, the per-buyer limit (event sales rules) and the workspace
maxSeatsPerHoldTokenwins. Events created from a chart use the chart's published limits (the layout buyers see); there is no separate per-event limit. - Large group holds: a workspace whose
maxSeatsPerHoldTokenis above 64 (Settings → Workspace settings) lifts the sales-channel defaults for seats: a buyer can hold up to that many seats in one atomic step (all or none), and a Max seats per order above 64 then applies. Without it, a limit above the channel default has no effect. - Live sessions: holds placed before a lower limit was published keep their seats and stay confirmable; they only cannot grow.
- Refusal: HTTP 409
{ "error": "booking_limit_exceeded", "limit": 4, "current": 3, "requested": 2, "kind": "seats" }(kind: "ga"for GA). The TS SDK exposesisBookingLimitExceeded(error). - Viewer: buyers cannot select past the limit (click, box/lasso, best-available quantity); a short message says how many seats one order may have.
To change it: open the chart (or event) in the designer → inspector with nothing selected → Booking limits → enter the number → Publish. The new limit applies to the next hold.
Chart library: archive, tags, versions, thumbnails
Archive instead of delete
| Action | API |
|---|---|
| Archive | POST /api/charts/:chartId/actions/archive |
| Restore | POST /api/charts/:chartId/actions/unarchive |
| Delete permanently | DELETE /api/charts/:chartId — only archived charts that no event uses |
- Archived charts are hidden from
GET /api/charts(and the legacy{ charts }list) unless you sendarchived=true(only archived) orarchived=all. - Events created from an archived chart keep working: seat maps, holds, sales and the viewer are untouched. The chart can still be opened, edited and published by id.
DELETEanswers409 ERR_CHART_NOT_ARCHIVEDfor an active chart and409 ERR_CHART_HAS_EVENTS(params.eventCount) while events reference it. Both actions are idempotent.
Tags
PUT /api/charts/:chartId/tagswith{ "tags": ["VIP", "Arena"] }replaces the full set ([]clears it).- Tags are trimmed, whitespace-collapsed and case-insensitive (
VIP=vip); 1–40 characters, no commas, at most 20 per chart (422 ERR_CHART_TAG_INVALID/ERR_CHART_TAG_LIMIT). GET /api/charts/tagslists every tag with its chart count (archived charts excluded unlessarchived=all|true).- Filter lists with
GET /api/charts?tag=vip,arena— a chart must carry all listed tags (max 5). Duplicating a chart copies its tags.
Listing
GET /api/charts?limit=24&sort=recent&q=arena&tag=vip&archived=false returns { items, nextCursor }. sort=recent orders by last draft save / publish (then creation); sort=name is A→Z. Items carry tags, archivedAt, eventCount, latestVersion, hasThumbnail and thumbnailUrl. Pass nextCursor back as cursor with the same filters.
Discard draft
POST /api/charts/:chartId/map/draft/actions/discard (and POST /api/events/:eventId/map/draft/actions/discard) throws away unpublished draft changes; the next GET .../map/draft returns the published map. Nothing live changes. 409 ERR_DRAFT_NOTHING_TO_DISCARD when there are no unpublished changes; for charts, 409 ERR_MAP_NEVER_PUBLISHED when there is no published version to go back to.
Version history and rollback
- Every successful
POST /api/charts/:chartId/map/publishappends an immutable version:version,geometryHash,publishedAt,publishedBy,seatCount, optionalnote(send{ "note": "..." }in the publish body, ≤ 280 chars). The publish response includesversion,geometryHashandsameAsVersion(an earlier version with identical content). GET /api/charts/:chartId/versions— newest first, cursor-paginated.POST /api/charts/:chartId/versions/:version/actions/restore-to-draftcopies that version into the draft. It never publishes: review it in the designer, then publish as usual (validation runs and a new version is recorded).- Snapshots are content-addressed per chart: republishing or restoring identical content reuses the stored geometry instead of duplicating it.
- Charts published before history existed get a
baselineversion 1 (the map that was live) on their next publish. - Copy as new chart: in Version history every chart version has Copy as new chart (name prefilled as “Chart (vN copy)”), which calls
POST /api/charts/:chartId/versions/:version/actions/copywith{ "name" }. The new chart holds that version as its draft, with the same tags; the source chart and its events don't change. The dialog links straight to the new chart's designer. - Event maps keep their own history (migration
0055): everyPOST /api/events/:eventId/map/publishappends an immutable version (the response carriesversionandsameAsVersion). The map the event had before its first recorded publish (cloned from the chart, or published earlier) becomes thebaselineversion 1.GET /api/events/:eventId/map/versionslists them (newest first, cursor-paginated),GET /api/events/:eventId/map/versions/:version/thumbnailis an immutable PNG preview, andPOST /api/events/:eventId/map/versions/:version/actions/restore-to-draftcopies a version into the event draft (charts_writescope) — buyers keep the live map until you publish, and publishing still checks sold and held seats. - In the designer, Version history (clock icon) works for charts and event maps: click a preview to enlarge it, Restore to draft to bring it back.
Thumbnails
- At publish time the designer renders the stage offscreen (~640 px, WebP) and attaches it with
PUT /api/charts/:chartId/versions/:version/thumbnail({ "image": "data:image/webp;base64,..." }, ≤ 512 KB, set once per version). It is stored like other map assets (MAP_ASSET_STORAGE_DIR/ R2, inline data URL otherwise). GET /api/charts/:chartId/thumbnail?version=Nserves it withCache-Control: private, max-age=31536000, immutable. Versions without a designer image get a server-rendered PNG placeholder (outlines and seat dots, see below;X-Thumbnail-Source: placeholder, cached 5 minutes). Withoutversionthe latest published state is returned with anETag(If-None-Match→304).- The render shows what buyers see — zones, sections, GA areas, seats, shapes / text / icons and the background image — never the designer-only reference image, selection, handles or guides. The placeholder draws zone, section and GA outlines and decorative shapes under the seat dots (first floor of multi-floor maps), cached per geometry hash. A version still showing the placeholder (published through the API, or a baseline) is replaced by the designer render as soon as the designer shows that published map (no draft, no local edits); for a chart published before history, attaching to version 1 records its baseline first.
- Thumbnail requests are authenticated (Bearer or
x-api-key); fetch them server-side or withfetch+ object URLs, not a bare<img src>.
See Integration guide for iframe embed and reservation lifecycle.
