Layout format
sunny-layout is a versioned JSON document describing one room: its shell (size, shape, doors and
windows, finishes) and the furniture placed in it. Use it to move a layout between Sunny and a
scan-to-plan tool, a CAD pipeline, or your own system.
In the API a room is a space, and it’s addressed by its space id (spc_…). “Room” and “space”
mean the same thing on this page.
Example
Section titled “Example”{ "format": "sunny-layout", "version": 1, "room": { "w": 6, "d": 4, "openings": [ { "id": "op_V1StGXR8_Z5jdHi6B-myT", "kind": "window", "side": "back", "center": 0.5, "width": 1.2, "sill": 0.72, "height": 0.76 }, { "id": "op_3ZtqT7bWq0cY8mNn1kLpA", "kind": "door", "side": "front", "center": 0.15, "width": 0.9, "sill": 0, "height": 2.1, "hinge": "start" } ], "floorType": "oak", "wallColor": "#c7d0c3" }, "placements": [ { "id": 1, "catalogId": "conf-table", "x": 1.5, "y": 1.4, "item": { "name": "Conference Table", "w": 3, "d": 1.2 } }, { "id": 2, "catalogId": "dining-chair", "x": 2.75, "y": 2.7, "item": { "name": "Chair", "w": 0.5, "d": 0.55 }, "config": { "rotation": 180 } } ], "meta": { "name": "Boardroom", "exportedAt": "2026-10-05T21:00:00.000Z", "generator": "sunny-api" }}Coordinates and units
Section titled “Coordinates and units”- Every length is in meters.
- The origin is the room’s top-left interior corner: x runs right, y runs down.
room.wandroom.dare the interior width and depth.- A placement’s
x/yis the top-left corner of its footprint.
Document
Section titled “Document”| Field | Type | Description |
|---|---|---|
format |
"sunny-layout" |
Required. |
version |
number |
Required. 1 is the only version today. |
room |
object | Required. The shell — see Room. |
placements |
array | Required. The furniture — see Placements. May be empty. |
meta |
object | Optional: name, rationale, exportedAt (ISO 8601), generator. |
meta.generator names what wrote the document — sunny-api from the REST API, sunny-mcp from
the MCP server. It’s informational; don’t branch on it. On import, meta.name becomes the new
version’s name.
| Field | Type | Description |
|---|---|---|
architecture | object | Imported building geometry bound to this room. Round-trips a model already uploaded to Sunny; not something to author by hand. |
h | number | Ceiling height, meters. |
floorMaterial | object | Custom floor texture. Round-trips textures already uploaded to Sunny; not something to author by hand. |
wrequired | number | Interior width, meters |
drequired | number | Interior depth, meters |
openings | object[] | Parametric openings only. Absent/empty = a plain box. |
footprint | object | Non-rectangular room shape. Absent = the plain w×d rectangle. |
floorType | "tile" | "concrete" | "oak" | "birch" | "walnut" | "espresso" | "plush" | "oatmeal" | "sage" | "clay" | "charcoal" | Present only when the finish isn't the studio's tile default |
floorColor | string | #rrggbb tint overriding the finish's own color |
wallColor | string | Present only when the paint isn't the studio's default plaster |
Fields the room doesn’t need are left out on export: floorType is omitted for the default tile,
wallColor for the default plaster, footprint for a plain rectangle, openings for a plain box.
footprint describes a non-rectangular room as a set of grid cells: cellSize in meters (one of
0.1, 0.25, 0.5, 1, 2) and cells as [column, row] pairs from a 0,0 origin.
Openings
Section titled “Openings”Doors, windows, open sections, and railings are always listed explicitly in room.openings.
| Field | Type | Description |
|---|---|---|
idrequired | string | Opening id, unique within the room (op_…). |
kindrequired | "window" | "door" | "open" | "railing" | "window", "door", "open" (a stretch of wall that isn't there — a cased opening or a room open to the next), or "railing" (a half-height balustrade where a wall would be). |
siderequired | "back" | "front" | "left" | "right" | Which wall the opening is on. |
edge | number | Traced-outline edge index for shaped rooms; absent = the 4-side address |
centerrequired | number | Center as a 0–1 fraction along the wall |
widthrequired | number | Opening width, meters |
sillrequired | number | Bottom edge above the floor, meters (0 for doors) |
heightrequired | number | Opening height, meters |
panes | number | Window panes: omitted/0 = auto, 1 = single sheet, N = N panes. Doors: 0 = no leaf (cased opening) |
hinge | "start" | "end" | Door hinge jamb; windows ignore it |
swing | "in" | "out" | Door swing; only "out" is stored |
leaf | "swing" | "sliding" | "bifold" | "pocket" | Door leaf type; omitted = swing, which is never stored |
leafStyle | "flush" | "lite" | "glass" | "panel" | "twoPanel" | Door leaf face; omitted = flush, which is never stored |
leafColor | string | Door leaf paint as #rrggbb |
frameColor | string | Casing/frame paint as #rrggbb; omitted = inherit |
open | boolean | Door leaf open; only false is stored |
An explicit empty openings array means “no openings”.
Placements
Section titled “Placements”Each placement has:
| Field | Type | Description |
|---|---|---|
id |
integer | Required. Positive, unique within the document. Other placements refer to it (parentId). |
catalogId |
string | Required. The catalog item. Catalog ids are opaque; use the ids the catalog API returns. |
x, y |
number | Required. Top-left of the footprint, meters. |
item |
object | Required. { name, w, d } — a snapshot so the document reads without the catalog. Informational only. |
config |
object | Optional. The fields below, each present only when set on the piece. |
item is never trusted on import: the footprint is recomputed from catalogId, size, scale,
and rotation.
Placement config
Section titled “Placement config”| Field | Type | Description |
|---|---|---|
rotation | number | Facing in degrees: 0 = front toward +y (into the room), 90 = left, 180 = up (−y), 270 = right. Any angle is valid. |
finish | string | Name of one of the catalog item's finishes. Unknown names fall back to the default. |
size | string | Name of one of the catalog item's size presets. Unknown names fall back to the default. |
mountH | number | Wall-mount height above the floor, meters, for wall-snapping items such as a TV. |
elevation | number | Free lift above the floor, meters, for a piece with nothing else to rest on. |
on | boolean | Powered on, for screens and lights. Only true is written. |
power | number | Dimmer for a light, 0–1. Absent means full. |
open | boolean | Lid state for lidded items (a laptop). Only false is written. |
src | string | Displayed content for a screen, artwork, or board: an http(s) URL or a small data:image URL. |
propParams | object | Parameters for a parametric prop (width, height, diagonal, ratio, and so on). Rejected on an item that has none. |
pileH | number | Pile height for a rug, meters. |
fringed | boolean | Fringed ends on a rug. Only true is written. |
hidden | boolean | Hidden from the stage but still part of the room and its bill of materials. |
locked | boolean | Locks the piece in place. true and false are both meaningful; absent uses the item's default. |
selectable | boolean | Whether viewers can pick the piece on share and embed surfaces. true and false are both meaningful; absent uses the item's default. |
scale | number | Uniform scale. Absent means 1. |
ordered | boolean | false leaves a priced piece out of the order and quote. |
suggested | boolean | Shows the piece as a suggestion rather than a committed choice. |
raised | boolean | Sit-stand desk raised to standing. Superseded by raiseT. |
raiseT | number | Sit-stand height, 0 (sitting) to 1 (standing). |
doorsOpen | string[] | Ids of the item's door leaves that are swung open. |
artW | number | Custom artwork width, meters. Wins over size. |
artH | number | Custom artwork height, meters. Wins over size. |
slotFinishes | object | Per-part finish overrides keyed by slot (frame, base, textile, …), for manufacturer models that support them. |
parentId | number | Id of another placement in this document that this piece is grouped under or attached to. |
attachAt | string | The parent's attach point this piece is seated in. Only meaningful with parentId. |
attachT | number | Position along a rail attach point, 0–1. |
attachments | object[] | Every seat of a piece that spans two or more parents (a worksurface across two pedestals). |
system | object | Identifies a manufacturer system assembly. Only meaningful on a group. |
label | string | A name given to a group. |
Anything not listed here is not part of the format: unknown fields anywhere in the document are ignored on import rather than rejected.
Versioning
Section titled “Versioning”- Within a version, only optional fields are added. A v1 reader keeps accepting every v1 document.
- A breaking change bumps
version. During a migration, Sunny may accept more than one version. - A document with an unsupported
formatorversionfails with a message naming the versions that are supported.
Export and import over the REST API
Section titled “Export and import over the REST API”| Method and path | Scope | Purpose |
|---|---|---|
GET /spaces/{spaceId}/layout |
projects:read |
Export the room’s current layout. |
POST /spaces/{spaceId}/layout |
projects:write |
Import a document as a new version of the room. |
POST /plan.svg |
projects:read |
Render a document as a 2D plan SVG. Saves nothing. |
Call them on https://api.sunnysdk.com with an OAuth access token or API key carrying the scope —
see OAuth & scopes and the REST API.
curl https://api.sunnysdk.com/spaces/spc_8QnZr2kVtL4x/layout \ -H "Authorization: Bearer $TOKEN"curl -X POST https://api.sunnysdk.com/spaces/spc_8QnZr2kVtL4x/layout \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d @layout.jsonAn import never edits an existing version: it saves the document as a new version of the room,
and the room takes on the document’s width and depth (unless the document
carries a footprint, which defines the shape instead). Each placement is resolved against the catalog; one
whose catalogId doesn’t resolve is dropped and reported, and the rest still import.
| Status | Meaning |
|---|---|
201 |
Imported; every placement resolved. |
207 |
Imported, but some placements were dropped. itemErrors lists them. |
400 |
Not imported: the body isn’t JSON, or the document failed validation. |
404 |
The space doesn’t exist or isn’t visible to your workspace. |
{ "id": "gen_h4Kd9Qe2LmXa", "itemErrors": [ { "index": 3, "catalogId": "itm_retired01", "error": "Unknown catalog item: itm_retired01" } ]}id is the new version. itemErrors[].index is the position in your placements array.
{ "error": "Invalid layout document", "details": ["placements.0.x: Invalid input: expected number, received string"]}details lists one message per invalid field, prefixed with its path. A body that isn’t JSON at
all returns { "error": "Invalid JSON body" } with no details.
Over MCP
Section titled “Over MCP”The MCP server exposes the same operations as export_layout (needs projects:read)
and import_layout (needs projects:write). They read and write the same format with the same
validation and catalog resolution; import_layout reports dropped placements per item the same
way.
In an embed
Section titled “In an embed”The full studio embed accepts a document over postMessage with load_layout, and every
embeddable room posts its current layout as a sunny-layout document. Nothing is saved to a
project that way. See Embeds.
Limits
Section titled “Limits”- One room per document; there are no floor-level documents.
- No import/export UI in the app yet — use the API, MCP, or an embed.
- No IFC, GeoJSON, or other CAD formats.