Developer API — get your floor plan 3D model as data
Sketch2Space can hand you your project's 3D model as plain data: every room, wall, door, window, stair, column, and furniture piece, with real-meter positions and sizes. The data already includes all the clean-up our pipeline does (gap closing, wall merging, calibration), so what you rebuild matches what you see in the app.
Three ways to use it:
- REST API — fetch the model JSON with any language or tool. Available on every plan.
- Claude skill — a ready-made helper so Claude can answer questions about your projects. Available on every plan.
- MCP server — connect Claude, Cursor, and other AI tools directly, including an
asktool answered by Sketch2Space's own AI. Paid plans.
1. Create an API key#
Click the button below (or in the app, open the menu (☰) → API keys):
Open API keysGive the key a name → Create key. Copy the key immediately — it is shown only once. You can hold up to 5 keys.
Each key has an on/off switch: Disable stops it working straight away but keeps it listed, and Enable switches it back on — useful to cut off access without swapping the key out of your scripts. Delete is the permanent one, and it frees up a slot.
Keep keys secret. Anyone with the key can read your projects' model data.
2. REST API#
All endpoints live under https://sketch2space.com/api/v1/dev and authenticate with the key:
curl -H "Authorization: Bearer s2s_your_key" \
https://sketch2space.com/api/v1/dev/projectsGET /me#
Returns { "email": "…" } — the account the key belongs to. Handy when juggling several keys.
GET /projects#
Lists your projects: { items: [{ id, name, status, updatedAt, isExample }] }. Use the id in the next call. isExample: true marks the built-in demo project — it does not count toward your plan's project limit.
POST /projects#
Create a project: {"name": "My house", "description": "optional"} → 201 with the new project. Subject to your plan's project limit — at the cap you get 403 with code PROJECT_LIMIT_REACHED and a message saying to delete a project or upgrade.
POST /projects/{projectId}/sketch#
Upload the project's sketch as multipart form-data (-F "[email protected]"; JPG/PNG/WebP/TIFF/PDF). Uploading to a project that already has a sketch replaces it and wipes its previous analysis — that's also how you re-run on a new sketch. Rejected with 422 when the file is too low-resolution or isn't a usable floor plan.
POST /projects/{projectId}/analyze and GET /projects/{projectId}/analysis#
analyze starts the full pipeline on the uploaded sketch and returns 202 immediately — it typically runs 1–3 minutes. Poll analysis every ~10 seconds: { "status": "ANALYZING", "progress": { "percent": 42, "phase": "streaming" } } while working; READY, NEEDS_CLARIFICATION, or COMPLETE means the model is ready to fetch; ERROR carries an errorMessage. 409 on analyze means one is already running.
DELETE /projects/{projectId}#
Deletes the project and its files (204). Permanent.
GET /projects/{projectId}/route#
Walking distance between two points, computed on the real geometry — walls block, doors are open passages. Pass fromX, fromZ, toX, toZ in meters (same space as the model). Returns { "reachable": true, "distanceM": 14.2, "path": [{x, z}, …] }; reachable: false means the points are walled off from each other.
GET /projects/{projectId}/sketch#
Returns the project's original uploaded sketch (image or PDF, as uploaded). This is the ground truth the model data was extracted from — let your AI look at it for anything the data can't answer: where an open-plan room really is, fixtures drawn on the plan, printed labels and dimensions.
GET /projects/{projectId}/model#
Returns the full model:
{
"schemaVersion": "1",
"project": { "id": "…", "name": "My house" },
"units": "meters",
"coordinateSystem": { "up": "y", "groundPlane": "xz", "rotation": "radians" },
"building": { "totalFloors": 1, "northAngleDeg": 0, "size": { "width": 12.4, "depth": 9.8 } },
"rooms": [ { "id": "room_living_01", "label": "Living Room", "floorLevel": 0,
"position": { "x": 3.1, "y": 0, "z": 4.2 },
"size": { "width": 5.2, "depth": 4.0, "height": 3.0 },
"area": 20.4, "outline": "traced" } ],
"walls": [ { "id": "wall_01", "start": { "x": 0, "y": 0, "z": 0 }, "end": { "x": 5.7, "y": 0, "z": 0 },
"thickness": 0.2, "height": 3.0, "isExterior": true, "isRailing": false } ],
"doors": [ { "id": "door_01", "hostWallId": "wall_01", "positionAlongWall": 0.35,
"width": 0.9, "height": 2.1, "sillHeight": 0 } ],
"windows": [ … ],
"stairs": [ … ],
"columns": [ … ],
"furniture": [ { "id": "bed_1", "kind": "bed", "position": { "x": 2.0, "z": 6.1 },
"elevation": 0, "rotationY": 1.5708,
"size": { "width": 1.6, "depth": 2.0, "height": 0.5 } } ]
}Everything is in meters. x/z is the floor plane, y is up. rotationY is radians around the vertical axis (0 faces −Z, +π/2 faces +X).
Walls, doors, windows and furniture are measured from the sketch — you can trust them. Room shapes are not all equal: where the walls close a room off we trace its true outline; where they don't, the room is open to its neighbours and simply has no area of its own. Each room says which it is in outline, so check that before you draw a room, compare two rooms, or quote an area. An open-plan kitchen has no area — it has a share of a bigger space, so we give you sharedArea instead of inventing a per-room number. Doing exact geometry on a guessed box (overlaps, "this room crosses a wall") produces confident nonsense.
Field notes:
| Field | Meaning |
|---|---|
walls[].start/end | Wall centerline segment. Build a box of thickness × height along it. heightStart/heightEnd, when present, slant the top. |
walls[].isRailing | A railing / glass barrier, not a solid wall (railingHeight when set). |
doors[] / windows[] | Cut into the host wall. The opening's center sits at positionAlongWall (0–1 fraction from start to end). sillHeight is the bottom edge above the floor (0 for doors). |
building.outline | The building's outer outline as one {x, z} polygon, traced along the outside face of the outermost walls (present when it can be traced). Draw this for the building's exterior/perimeter — a wall can be partly inside and partly outside, and the outline takes only the truly-outside part. |
walls[].isExterior | True when a good part of the wall faces the outside (computed from the real geometry against building.outline). A flagged wall can still be partly interior — use it for things like "which door is the entrance", not for drawing the exterior. |
walls[].exteriorSpans | The stretch(es) of the wall that really face the outside, as 0–1 fractions along it (from/to, measured start → end). When marking a wall as exterior on a drawing, paint only these stretches — never the full segment. Present only when some part faces the outside. |
rooms[].outline | Read this first — how much of the room's shape we know. traced = we followed the room's own walls, so polygon/area/size/position are solid. merged = the room is open to its neighbours, so it has no area of its own — see sharedArea/sharedWith. estimated = no outline found, so position/size are a guessed box — a rough spot only. |
rooms[].area | Floor area in m², measured from the real outline (the value the app shows when you point at a floor). Present only for traced rooms. Absent means we genuinely don't know it — don't substitute width × depth. |
rooms[].sharedArea / sharedWith | merged rooms only: the area of the one open space the room is part of, and the other rooms sharing it. It belongs to the group, not to any one room — count it once, not per room. |
rooms[].size | Bounding size in meters — width × depth × height, centred on position. Exact when traced, otherwise a rough guess at the room's extent. |
rooms[].polygon | The exact floor outline as {x, z} points. Present only when outline is traced. |
sketchMapping | How to place model coordinates onto the original sketch image (present when the alignment is known). imageBox is the building's box in the image as 0–1 fractions (y down); metersSize is that box in meters. Draw point (x, z) at px = (imageBox.x + x / metersSize.width × imageBox.width) × imageWidth, py = (imageBox.y + (1 − z / metersSize.depth) × imageBox.height) × imageHeight. |
furniture[].elevation | Bottom of the piece above the floor — 0 for floor-standing, e.g. 1.0 for a wall TV. |
furniture[].hostWallId | Present on wall-hung pieces (wall TV, upper cabinet, range hood, wall art). |
furniture[].parentId | Present on parts of a bigger piece (drawer/door front on a cabinet, sink on a counter). Positions are already world coordinates — render them as-is. |
Errors: 401 bad or revoked key, 404 unknown project or no analyzed model yet, 429 rate limit (60 requests/minute per key).
3. Claude skill (all plans)#
Two commands — install the skill, then save your key next to it:
mkdir -p ~/.claude/skills/sketch2space-models
curl -o ~/.claude/skills/sketch2space-models/SKILL.md \
https://sketch2space.com/skills/sketch2space-models/SKILL.md
echo "s2s_your_key" >> ~/.claude/skills/sketch2space-models/key.txtThat's it — no environment variables or config to set. Now ask Claude things like "what's the total floor area of My house?" and it reads your key from that file, calls the two endpoints, and does the reasoning. Nothing extra runs on our side.
Several accounts? Put one key per line in key.txt, each followed by a name and the account's email:
s2s_aaa Work [email protected]
s2s_bbb Home [email protected]With a single key the skill just uses it; with several it asks which account you mean (or picks the one you name, e.g. "in my work account").
Updates: the skill checks its own version against the API on every use and tells you when a newer one is out — updating is re-running the curl line above. (The MCP server needs nothing; it updates on our side.)
4. MCP server (paid plans)#
Point any MCP-capable AI tool at our server and it gets three tools:
list_projects— your projects.create_project(name)/delete_project(projectId)— manage projects (plan limits apply).upload_sketch(projectId, filename, mimeType, dataBase64)— upload or replace the sketch (≤15MB).run_analysis(projectId)/analysis_status(projectId)— run the full pipeline and poll until done.get_model(projectId)— the same model JSON as the REST API.get_sketch(projectId)— the original sketch as an image, so your AI can look at the actual plan.route(projectId, fromX, fromZ, toX, toZ)— walking distance between two points (walls block, doors pass).ask(projectId, question)— ask in plain language; Sketch2Space's own AI answers using the model data and the sketch image, so your tool gets a short answer instead of the whole JSON. Capped per day by plan (paid: 25 questions/day, custom: 100/day).
Connect from Claude Code:
claude mcp add --transport http sketch2space \
https://sketch2space.com/api/v1/mcp \
--header "Authorization: Bearer s2s_your_key"Endpoint: https://sketch2space.com/api/v1/mcp (Streamable HTTP). Free-plan keys receive 403 MCP_NOT_AVAILABLE.
Several accounts? An MCP connection carries one key, so add the server once per account under different names (e.g. sketch2space-work, sketch2space-home) — your AI tool then picks between them.
What's not included#
The custom lighting you see in the app is a display effect, not model data. Built-in furniture shapes stay in the app — the data gives you each piece's type, position, rotation, and size, and you render your own look for it.