Planner 5D API
Build with AI generation, your own product catalog, floor plans and native renders. Explore the guides, copy a request and connect your first workflow.
Generate with AI
Images, edits, floor-plan illustrations, video and upscale.
AI StudioBring your products
Personal 3D imports and a business catalog with your SKUs.
Models & catalogsWork with real projects
Recognize plans, read scene JSON and produce native renders.
Plans & rendershttps://planner5d.com/api/publicGET CONNECTED
Authentication & account access
Read these guides and the OpenAPI specification without signing in. To execute requests, supply
a Planner 5D user access token in the Authorization: Bearer header.
Get your access token
Copy your access token from Profile → Account settings → API key. Reset access token invalidates previous access tokens. Update your integrations afterwards. Every call requires commercial/PRO access. Existing account limits still apply.
Use Bearer for new integrations. The legacy as header is also accepted without a prefix. Browser sign-in and the
staging Basic Auth password are separate from this API credential.
This page does not issue keys or change account permissions. Keep tokens on your server or in environment variables, outside source control and client-side application code.
B2B partner API access
Contact your sales representative to request partner API access and authentication details.
| Capability | Access checked | Data boundary |
|---|---|---|
| Personal model imports | Public API access, native import entitlement and limits | Models and optional projects owned by the user |
| AI Studio jobs | Eligible account, enabled generation feature, available credits | Uploaded assets and jobs belonging to the user |
| Business catalog management | Existing B2B owner or content-manager permissions | The account’s configured catalog |
| Create a missing business catalog | Enabled B2B account owner only | One existing business account; no new account or role is created |
| Renders and exports | Project ownership and native license/format rights | Owned project and output tasks |
QUICKSTART
Your first request, in a few clicks
Start with the credit balance: it reads your account and does not spend credits. Pick another example, replace its input values, and copy code or send the request explicitly.
cURL examples use POSIX shell syntax. JavaScript uses Node.js 20+ ES modules.
Python examples use the requests package. These are HTTP examples, not a separate SDK.
IMAGES & VIDEO
Generate with AI Studio
Describe the task, supply your images and set a credit ceiling. Planner 5D chooses the preset and default model for that scenario. You do not need a sheet, a canvas or a provider-specific integration.
- 1GenerateSend your image and task together
- 2Read the resultPoll the returned job when processing is asynchronous
One request with your image
Send multipart POST /generations with source containing the image file and
request containing the JSON task. The server uploads the file, selects the preset, checks
the live price against maximum_credits, and submits the generation.
There is no separate upload or quote call. Save request_id before sending and the returned
jobId when received. A 202 response means work was accepted; poll the returned
Location or GET /generations/{jobId} for the result.
For edits, add a mask file. For reference images, repeat references[] file parts.
Use at most eight reference images in total, including existing IDs. Each image is PNG/JPEG/WebP, at most
20 MiB and 100 megapixels; all files together must fit within 40 MiB.
Keep parameters in the JSON request form field so numbers and booleans retain their types.
Supply either a file or an existing asset ID for each source/mask, never both.
curl --request POST \
'https://planner5d.com/api/public/generations' \
--header 'Authorization: Bearer YOUR_ACCESS_TOKEN' \
--form 'source=@room.jpg' \
--form-string 'request={"operation":"render-image","request_id":"listing-123-render-v1","instruction":"Natural daylight and neutral materials","maximum_credits":20}'
request_id,
image bytes and task parameters. Uploaded inputs are reused for up to 24 hours; increasing only the credit
ceiling is allowed. Changed inputs require a new ID. If the upload receipt expired but a generation already
exists, the API returns 409 with a status Location; read that job instead of starting another.Optional: upload once and reuse an image
Reuse an owned asset ID
Send multipart file to /generation-assets. Save the returned asset_id.
This reference belongs to your account and lasts 24 hours.
- PNG, JPEG or WebP.
- Up to 20 MiB and 100 megapixels.
- Use asset IDs in
source,maskandreferences.
Upload product reference images the same way. A 3D model hash is not an image asset ID.
curl --request POST \
'https://planner5d.com/api/public/generation-assets' \
--header 'Authorization: Bearer YOUR_ACCESS_TOKEN' \
--form 'file=@room.jpg'
Optional: preview the price
You can submit directly with maximum_credits; no separate quote call is required.
A quote reports requestCredits, available balance and a jobId without starting
a job. Keep the ID so you can recover status if the submission response is lost.
Send the same task to POST /generations with maximum_credits. The server
calculates the current price again and stops before submission if it exceeds your limit.
curl --request POST \
'https://planner5d.com/api/public/generations/quote' \
--header 'Authorization: Bearer YOUR_ACCESS_TOKEN' \
--header 'Content-Type: application/json' \
--data-binary @- <<'JSON'
{
"operation": "floor-plan-3d",
"request_id": "listing-123-image-v1",
"source": "YOUR_ASSET_ID",
"instruction": "Neutral oak finishes and a bright daytime view",
"aspect": "16:9",
"count": 1
}
JSON
Submit, then retrieve the result
Use a stable request_id for retries of the same task. Change the ID for a new task or changed
inputs. Image generation accepts count 1, 2, 3, 4 or 8, subject to the selected native model.
Poll GET /generations/{jobId}. On succeeded, read results[].src.
On failed, inspect the error before deciding whether to retry.
Read /generations/{jobId}/billing for the actual reserved, charged
or refunded state and credits. A queued job is not a completed result.
curl --request POST \
'https://planner5d.com/api/public/generations' \
--header 'Authorization: Bearer YOUR_ACCESS_TOKEN' \
--header 'Content-Type: application/json' \
--data-binary @- <<'JSON'
{
"operation": "floor-plan-3d",
"request_id": "listing-123-image-v1",
"source": "YOUR_ASSET_ID",
"instruction": "Neutral oak finishes and a bright daytime view",
"aspect": "16:9",
"count": 1,
"maximum_credits": 20
}
JSON
Complete Python workflow: send image + task → read result
This script reads credentials from environment variables. Set P5D_API_BASE_URL to
https://planner5d.com/api/public, P5D_API_TOKEN to your user token, and
P5D_REQUEST_ID to a new task identifier. Install requests, then run
python planner5d-generation.py room.jpg.
For a protected staging host, also set P5D_STAGE_USERNAME and
P5D_STAGE_PASSWORD for its Basic Auth.
The default credit ceiling is an example: adjust P5D_MAXIMUM_CREDITS after checking your quote.
Set P5D_OPERATION=floor-plan-3d for a plan illustration. The script prints the job ID before
submission; use P5D_JOB_ID to resume polling after a lost response, without uploading again.
"""Submit an image and task in one request, then retrieve its result.
Requires: pip install requests
Set P5D_API_BASE_URL, P5D_API_TOKEN and P5D_REQUEST_ID before running.
Use P5D_JOB_ID to resume polling an already submitted job without creating another.
"""
import os
import json
import sys
import time
from urllib.parse import quote
import requests
api_base = os.environ["P5D_API_BASE_URL"].rstrip("/")
api_token = os.environ["P5D_API_TOKEN"]
maximum_credits = int(os.environ.get("P5D_MAXIMUM_CREDITS", "20"))
stage_auth = None
if os.environ.get("P5D_STAGE_USERNAME") and os.environ.get("P5D_STAGE_PASSWORD"):
stage_auth = (os.environ["P5D_STAGE_USERNAME"], os.environ["P5D_STAGE_PASSWORD"])
headers = {"Accept": "application/json, application/problem+json"}
if stage_auth:
headers["as"] = api_token # Keep staging Basic Auth separate from the API credential.
else:
headers["Authorization"] = "Bearer " + api_token
def api(method, path, **kwargs):
response = requests.request(
method, api_base + path, headers=headers,
timeout=120, allow_redirects=False, auth=stage_auth, **kwargs
)
response.raise_for_status()
if 300 <= response.status_code < 400:
raise RuntimeError("Unexpected API redirect; check the API base URL")
data = response.json()
if isinstance(data.get("error"), int) and data["error"] != 0:
raise RuntimeError(data)
return response
job_id = os.environ.get("P5D_JOB_ID")
if not job_id:
request_id = os.environ["P5D_REQUEST_ID"]
source_filename = sys.argv[1] if len(sys.argv) > 1 else "room.jpg"
task = {
"operation": os.environ.get("P5D_OPERATION", "render-image"),
"request_id": request_id,
"instruction": "Natural daylight and neutral materials",
"count": 1,
"maximum_credits": maximum_credits,
}
print("Keep this request ID before submission:", request_id, flush=True)
try:
with open(source_filename, "rb") as source:
response = api("POST", "/generations", files={"source": source},
data={"request": json.dumps(task)})
job_id = response.json()["jobId"]
except (requests.Timeout, requests.ConnectionError) as error:
raise RuntimeError(
"Submission outcome is unknown. Retry the same request ID, image bytes and task; "
"do not assign a new request ID. Uploaded inputs and accepted work are reused."
) from error
except requests.HTTPError as error:
# A 409 with Location can identify work accepted before an upload receipt expired.
location = error.response.headers.get("Location", "")
if error.response.status_code != 409 or not location.startswith("/api/public/generations/"):
raise
job_id = location.rsplit("/", 1)[-1]
print("Keep this job ID:", job_id, flush=True)
print("Polling:", job_id, flush=True)
deadline = time.monotonic() + 600
while time.monotonic() < deadline:
response = api("GET", "/generations/" + quote(job_id, safe=""))
job = response.json()
if job["status"] == "succeeded":
for result in job.get("results", []):
print("Output:", result.get("src"))
print("Billing:", api("GET", "/generations/" + quote(job_id, safe="") + "/billing").json())
break
if job["status"] == "failed":
raise RuntimeError(job.get("error", "Generation failed"))
retry_after = response.headers.get("Retry-After", "2")
delay = max(1, int(retry_after)) if retry_after.isdigit() else 2
time.sleep(min(delay, max(0, deadline - time.monotonic())))
else:
raise TimeoutError("Still running. Resume with P5D_JOB_ID=" + job_id)
Choose a scenario, not a model
| operation | What it does | Inputs |
|---|---|---|
generate-image
|
Generate an image from an instruction | Instruction; optional images |
render-image
|
Turn an input image into a visual using the rendering preset | Source image |
edit | Edit a marked image region | Source and instruction; optional same-size mask |
swap-material
|
Change materials with the native preset | Source and same-size mask; optional references and instruction |
populate
|
Add people to a scene | Source image; this is not furniture planning |
floor-plan | Create a clean 2D plan drawing | Source image |
floor-plan-3d
|
Create a furnished isometric illustration from a plan | Source plan; optional references |
video | Generate a video with the native animation preset | Optional source start frame; one result |
upscale | Enhance a source image with a 2K output target | Source; one result |
Upscale is generative enhancement, not an exact 2× pixel resize. Read the returned width and height; an image already larger than the target can become smaller. Populate uses the native default density of two or three people.
references accepts up to eight uploaded image IDs. Aspect ratios: 16:9, 1:1,
4:3, 3:4, 9:16, 3:2, 2:3, 21:9.
Omit aspect to choose the closest source ratio, or 16:9 without a source. This is a preference:
native source-based presets, including image-to-video, can preserve the input framing and use their own
output sizes. Check the returned media dimensions before placing the result in your layout.
Image editing and upscaling examples
curl --request POST \
'https://planner5d.com/api/public/generations' \
--header 'Authorization: Bearer YOUR_ACCESS_TOKEN' \
--header 'Content-Type: application/json' \
--data-binary @- <<'JSON'
{
"operation": "edit",
"request_id": "room-edit-v1",
"source": "YOUR_ASSET_ID",
"mask": "YOUR_MASK_ASSET_ID",
"instruction": "Replace the marked sofa with a light linen sofa",
"maximum_credits": 20
}
JSON
curl --request POST \
'https://planner5d.com/api/public/generations' \
--header 'Authorization: Bearer YOUR_ACCESS_TOKEN' \
--header 'Content-Type: application/json' \
--data-binary @- <<'JSON'
{
"operation": "upscale",
"request_id": "room-upscale-v1",
"source": "YOUR_ASSET_ID",
"count": 1,
"maximum_credits": 20
}
JSON
floor-plan-3d returns an image,
not an editable project or verified measurements. Use plan recognition and project JSON for structured geometry.
Product references guide the image; exact SKU identity is not guaranteed.YOUR 3D ASSETS
Import personal models
Bring a model file into the user’s own collection through the existing Planner 5D import pipeline. Personal imports and B2B catalog publication are separate operations.
File URL → signed upload → model
- Call
/models/upload-from-urlwith a public HTTP(S) file URL and filename. - Pass the returned
assetModelTokentoPOST /models. - Keep
item.hashand poll/models/{modelHash}/status. - When finished, use
GET /models/{modelHash}for metadata, configuration and 2D previews.
The download rejects private-network destinations and enforces native size limits. Supported model formats, conversion and account limits are checked by the import pipeline.
assetModelToken authorizes use of an uploaded file. It is different from the access token sent
in the Authorization header.
curl --request POST \
'https://planner5d.com/api/public/models/upload-from-url' \
--header 'Authorization: Bearer YOUR_ACCESS_TOKEN' \
--header 'Content-Type: application/json' \
--data-binary @- <<'JSON'
{
"url": "https:\/\/example.com\/assets\/chair.glb",
"name": "chair.glb"
}
JSON
Other supported sources
POST /models/upload-destination provides the existing signed direct-upload instructions.
Upload the binary as instructed, then create the model.
For native image or product-page imports, use image or link in POST /models.
Do not mix image/link and uploaded-file source modes in one request.
An optional projectHash must identify your own project. Listing, statistics and deletion are
available under /models.
Responses contain model metadata and 2D previews. 3D model files are not public API exports.
To copy a finished generated model into your business catalog, pass its source_model_hash
to POST /catalog/models; the server reuses the owned source.
curl --request POST \
'https://planner5d.com/api/public/models' \
--header 'Authorization: Bearer YOUR_ACCESS_TOKEN' \
--header 'Content-Type: application/json' \
--data-binary @- <<'JSON'
{
"assetModelToken": "SIGNED_UPLOAD_TOKEN",
"original_filename": "chair.glb"
}
JSON
BUSINESS INTEGRATIONS
Maintain your own catalog
Manage models, SKUs, categories, product metadata and catalog builds for an existing business account.
The account determines the catalog; callers cannot select another business with b2bClientId.
Start with the account’s catalog
POST /catalog returns the existing catalog_id or creates the missing catalog.
It requires the enabled B2B account’s owner. Repeating it does not replace the catalog.
Existing owners and assigned content managers can use the native catalog-management operations allowed by their roles. The API does not assign roles or change account ownership.
curl --request POST \
'https://planner5d.com/api/public/catalog' \
--header 'Authorization: Bearer YOUR_ACCESS_TOKEN'
- 1 Import models Use the B2B upload contract
- 2OrganizeCategories and hierarchy
- 3Add productsModels, SKUs and metadata
- 4BuildRun the native catalog builder
| Resource | Available operations |
|---|---|
/catalog/models | List, create, read, update, delete, upload, retry, statistics, export, existing SKU lookup, existence checks and bulk updates |
/catalog/items | Product records, metadata and bulk operations |
/catalog/categories
|
Create and manage categories and their default item settings |
/catalog/category-relations | Read and update category hierarchy |
/catalog/custom-icons | List, upload and delete category icons |
/catalog/planner5d-products | Read Planner 5D products available to the business |
PUT /catalog/build | Build the account’s catalog |
Keep ownership all the way through
Category, icon and model references are checked against the current catalog, including bulk updates and hierarchy changes. You can reference a permitted business model or your own personal model.
After creating a category, use its returned identifier when creating a product. Record model_id
in the native form user_model/{modelHash}. Build the catalog after editing its contents.
Attach categories to the visible catalog tree with POST /catalog/category-relations.
Use parent_id: 0 for a root category and visible: true to include it.
This operation replaces the complete relation list: read the existing tree first and preserve its entries.
An unattached category and its products do not appear in the built catalog.
curl --request POST \
'https://planner5d.com/api/public/catalog/items' \
--header 'Authorization: Bearer YOUR_ACCESS_TOKEN' \
--header 'Content-Type: application/json' \
--data-binary @- <<'JSON'
{
"category_id": 42,
"model_id": "user_model\/YOUR_MODEL_HASH",
"free": true,
"hidden": false,
"editable_materials": true,
"editable_position": true,
"editable_rotation": true,
"editable_size": false,
"built_in": false,
"weight": 10,
"sku": "CHAIR-001",
"title": "Linen chair"
}
JSON
Choose detail without losing the original
B2B models can keep their original high-poly geometry. Optimization is optional: request a variant, compare its measured vertices, triangles and file size, then explicitly apply it if suitable.
Read GET /catalog/optimization-profiles for the current RapidPipeline profiles. Each profile
describes its face target, material/UV handling, occlusion culling and texture limit.
Face targets are not vertex limits or guaranteed output counts. The separate personal scene admission
limit of 100,000 vertices is not a blanket B2B catalog limit.
Only the requested profile is computed. Repeating a request reuses its job; another profile reuses the retained original. Variants do not consume extra catalog model slots. A different running profile returns 409; poll it before starting the next one.
curl --request POST \
'https://planner5d.com/api/public/catalog/models/{hash}/optimizations' \
--header 'Authorization: Bearer YOUR_ACCESS_TOKEN' \
--header 'Content-Type: application/json' \
--data-binary @- <<'JSON'
{
"profile": "00-high_quality-800000"
}
JSON
Review, then apply
Poll the returned links.self until job.terminal is true. A succeeded job exposes
original and variant measurements for comparison. The public API does not return
GLB, native 3D JSON or other mesh download links. Preview models inside Planner.
If a retained variant expires before applying it, repeat the profile request to renew it without recomputing.
Keep the original by leaving the variant unapplied. To use the variant, call its links.apply,
then poll the returned status_url until native model import finishes. Apply uses the existing
B2B replacement flow, retaining metadata and catalog references and reusing the model slot.
Record the returned model hash; build the catalog after the replacement is ready.
retry: true. If a running job reports an unconfirmed dispatch or an expired worker wait,
use the same retry to resume its existing task. A 503 response means optimization is unavailable;
it does not prevent the normal B2B import flow.curl --request POST \
'https://planner5d.com/api/public/catalog/models/{hash}/optimizations/{jobId}/apply' \
--header 'Authorization: Bearer YOUR_ACCESS_TOKEN' \
--header 'Content-Type: application/json' \
--data-binary @- <<'JSON'
{}
JSON
STRUCTURED PLAN DATA
Recognize, read and update projects
Turn a plan into an editable project
POST /recognition starts the existing floor-plan recognition workflow. Send file bytes as
Base64 with a base64, prefix, then find the returned task ID in GET /recognition.
Common supported inputs include JPG, PNG, PDF, DWG and DXF. The reference lists all supported formats.
Optional dimensions_info supplies a reference segment and real-world length for scale.
Read the task’s status and resulting project key. Recognition output should be checked for the source plan’s dimensions and layout before using it as verified geometry.
curl --request POST \
'https://planner5d.com/api/public/recognition' \
--header 'Authorization: Bearer YOUR_ACCESS_TOKEN' \
--header 'Content-Type: application/json' \
--data-binary @- <<'JSON'
{
"data": "base64,REPLACE_WITH_FILE_BASE64"
}
JSON
Read the entire scene
/projects/{projectHash}/document returns hash, udate and the complete
native scene as data. Use it to process rooms, walls, openings and items in your application.
Write edited scene data through PUT /projects with key, name and
data. Preserve the complete native document and fields your integration does not modify.
Existing last-write behavior applies; udate is not an optimistic-concurrency precondition.
A guessed hash or shared-project link does not grant ownership.
curl --request GET \
'https://planner5d.com/api/public/projects/{projectHash}/document' \
--header 'Authorization: Bearer YOUR_ACCESS_TOKEN'
Complete Python example: read → transform → save → verify
Use a project owned by the API account. This example backs up the complete document, selects its first
floor, preserves the project name, saves the modified JSON object, and reads it back. Replace the
currentFloor change with your own logic while retaining IDs, geometry and unknown fields.
import copy
import json
import os
from pathlib import Path
from urllib.parse import quote
import requests
base = os.environ["P5D_API_BASE_URL"].rstrip("/") # Includes /api/public
project_key = os.environ["P5D_PROJECT_KEY"]
headers = {
"Authorization": "Bearer " + os.environ["P5D_API_TOKEN"],
"Accept": "application/json, application/problem+json",
}
def api(method, path, **kwargs):
response = requests.request(method, base + path, headers=headers,
timeout=120, allow_redirects=False, **kwargs)
response.raise_for_status()
if response.is_redirect:
raise RuntimeError("Unexpected redirect; check the API base URL")
result = response.json()
if isinstance(result.get("error"), int) and result["error"] != 0:
raise RuntimeError(result)
return result
path = "/projects/" + quote(project_key, safe="")
metadata = api("GET", path)
document = api("GET", path + "/document")
scene = copy.deepcopy(document["data"])
if not isinstance(scene.get("items"), list) or not scene["items"]:
raise ValueError("This example needs a project with at least one floor")
# Exclusive creation avoids overwriting an earlier backup.
with Path("project-before.json").open("x", encoding="utf-8") as backup:
json.dump(document, backup, ensure_ascii=False, indent=2)
scene["currentFloor"] = 0 # Your transformation goes here.
api("PUT", "/projects", json={
"key": project_key,
"name": metadata["name"],
"data": scene, # The JSON object, not a JSON-encoded string or metrics summary.
})
saved = api("GET", path + "/document")
assert saved["data"]["currentFloor"] == 0
print("Saved project:", saved["hash"], "updated:", saved["udate"])
Only the changed field is checked above; validate your own geometry changes after reopening the project
in Planner. udate is informational, not an optimistic-lock token: coordinate writers to avoid
overwriting another editor's changes. To work on a copy, first use PUT /projects with
from and a new name, then use the returned key.
For staging Basic Auth, use the legacy as API header and the HTTP client's separate
auth=(username, password) option as described in Authentication.
Export without confusing the two contracts
HTTP export lifecycle
Public export supports PDF, 2D DXF and 2D DWG only. Use pdf,
dxf-2d or dwg-2d. The public dwg alias also means 2D.
IFC and all 3D formats are unavailable through the public API.
/projects/{projectHash}/exports lists and creates export jobs.
Use POST /{hash}/retry and DELETE /{hash} under that path for existing tasks.
Read native status and output links. Format rights and PRO/B2B gates still apply.
CAD drawings, including vector PDF
Singular POST /projects/{projectHash}/export returns the legacy queue transport contract.
Choose dxf-2d, dwg-2d or pdf.
{"format":"pdf"} uses the native CAD engine to publish A3 vector drawing sheets with
automatic scale, dimensions, schedules and title blocks. It does not require editor page settings.
Consume the returned queue. For a result, Base64-decode data.file and save the
bytes as a PDF. CAD formats keep their ZIP archive. Treat a queue error as a failed export.
The plural HTTP PDF export keeps its existing custom pages and settings.
curl --request POST \
'https://planner5d.com/api/public/projects/{projectHash}/export' \
--header 'Authorization: Bearer YOUR_ACCESS_TOKEN' \
--header 'Content-Type: application/json' \
--data-binary @- <<'JSON'
{
"format": "pdf"
}
JSON
EMBED & DISPLAY
Show imported scenes on your website
A saved Planner 5D project can open in 2D or interactive 3D inside an iframe. Use the project key returned
by project creation or the project_hash produced by completed plan recognition.
Get the embed links on your server
Call GET /projects/{projectHash}/embedded-links with your API credential. The response contains
ready-made iframe HTML and URLs for the default, 2D and 3D views.
Use the returned URLs as-is. They point to the configured B2B editor when the account has a client slug, or to the standard editor otherwise. They may be shortened; use the returned view-specific URL rather than appending parameters to a short link.
The API credential stays on your server. An iframe cannot send your custom Bearer header, and you must not put an API key, user token or staging password in its URL.
curl --request GET \
'https://planner5d.com/api/public/projects/{projectHash}/embedded-links' \
--header 'Authorization: Bearer YOUR_ACCESS_TOKEN'
| Display | Ready-made HTML | URL for your own iframe |
|---|---|---|
| Default project view | iframe | project_url |
| 2D floor plan | iframe_2d | project_url_2d |
| Interactive 3D project | iframe_3d | project_url_3d |
The generated destinations use viewMode=2d or viewMode=3d. These select the initial
view; they do not lock navigation, grant access, or change editing permissions.
Add a responsive iframe
Paste project_url_3d into src, or use project_url_2d for a plan view.
The HTML returned by the API starts at 960 × 500 pixels; you can change the dimensions for your layout.
Keep a descriptive title and allowfullscreen. Give the frame an explicit height.
There is no model download or 3D export involved: Planner displays the saved scene.
<iframe
id="planner-scene"
title="Interactive property floor plan"
src="PASTE_PROJECT_URL_3D_HERE"
style="width:100%;height:600px;border:0;"
loading="lazy"
allowfullscreen>
</iframe>Offer 2D / 3D buttons without a client SDK
Set each URL from your server's embed-links response. Changing src reloads the frame and
opens the saved project in that view; it does not preserve unsaved changes inside an editing session.
<button type="button" data-scene-url="PASTE_PROJECT_URL_2D_HERE">2D plan</button>
<button type="button" data-scene-url="PASTE_PROJECT_URL_3D_HERE">3D scene</button>
<script type="module">
const frame = document.getElementById('planner-scene');
document.querySelectorAll('[data-scene-url]').forEach(button => {
button.addEventListener('click', () => {
frame.src = button.dataset.sceneUrl;
});
});
</script>
When generating this HTML on your server, escape the URL as an HTML attribute using your template
engine. Pass the returned URL into src; do not execute arbitrary response HTML with
innerHTML.
B2B: switch views in the same loaded scene
The existing B2B browser API supports getViewMode(), setViewMode('2d'),
setViewMode('3d') and setOnViewModeChangedListener(). This is the customer-specific
JavaScript integration, separate from the server REST API. Your B2B account needs iframe embedding and
the client API enabled. Contact your sales representative for the correct client module and access setup.
After loading your configured client module and an accessible B2B project in the iframe, attach once to that frame. Wait for project readiness before switching. This example updates the live view without reloading.
<button id="show-scene-3d" type="button" disabled>Show 3D</button>
<script type="module">
await import('YOUR_CONFIGURED_CLIENT_API_MODULE_URL');
const frame = document.getElementById('planner-scene');
const button = document.getElementById('show-scene-3d');
const api = await window.planner5d.getB2BPublicApi(frame);
await api.setOnProjectReadyListener(() => { button.disabled = false; });
button.addEventListener('click', async () => {
try {
await api.setViewMode('3d'); // Use '2d' for the plan view.
} catch (error) {
console.error('Could not switch the project view', error);
}
});
</script>
Use this alternative instead of changing the frame URL. Do not invent postMessage commands
or read the cross-origin iframe DOM. This REST API does not expose floor selection, camera positioning,
or an arbitrary “dollhouse” mode as iframe parameters.
Choose the display for the actual output
| What you imported or generated | How to display it |
|---|---|
| Recognized plan or saved native scene | Wait for recognition to finish and obtain
project_hash. Use that project's 2D / 3D embed links. |
| Personal or B2B 3D model | Finish model import, place the model in a project in Planner and save it. Embed the project key. A model hash or catalog ID is not a project key. |
| AI Studio floor-plan image, render-image or other illustration | Use
results[].src in an <img>. Even floor-plan-3d returns an image,
not editable geometry or an interactive 3D scene. |
| AI Studio video | Use the completed video result's src in
<video controls>. |
| Native still render | Use image_url from a finished render in an
<img>. |
| Native panorama or 360° walkthrough | After rendering finishes, open the returned
view_url or use it as the iframe src. Wait until that field is populated. |
| PDF / 2D DWG / 2D DXF export | Offer the completed export's download link. A CAD file is not a Planner scene URL; public 3D mesh export is unavailable. |
Access and embedding checks
- Embed links do not authenticate the visitor or publish a private project. Existing project-sharing, account and B2B permissions still apply. Test the destination as the intended viewer.
- For a view-only audience, configure viewing access in Planner's sharing controls and use the resulting
Share → Embed link. Selecting
iframe_3dalone does not make a project read-only. - B2B can disable iframe embedding independently of server API access. An API key does not override that setting.
- If the frame is blocked, check your site's
frame-srcpolicy for the returned URL and its final Planner origin, the B2B embedding setting, and the visitor's access. Staging may also require browser Basic Auth; keep those credentials out of HTML.
NATIVE RENDERING
Render a project, panorama or walkthrough
Native renders use stored project geometry and the existing renderer. AI Studio’s render-image
uses an image as input; these are different operations with different output and entitlement contracts.
Prepare first, start explicitly
- Prepare a task from an owned project and camera.
- Read its
idand callPOST /renders/{id}/start. - Poll
GET /renders/{id}. - On
finished, readimage_urland, for panoramas,view_url.
Preparation stores the scene but does not start rendering. Repeating a start reads the current task
instead of starting it again. List tasks with GET /projects/{projectHash}/renders?page=1.
curl --request POST \
'https://planner5d.com/api/public/projects/{projectHash}/renders' \
--header 'Authorization: Bearer YOUR_ACCESS_TOKEN' \
--header 'Content-Type: application/json' \
--data-binary @- <<'JSON'
{
"request_id": "listing-123-render-v1",
"kind": "render",
"floor": 0,
"camera": {
"from": [
0,
200,
100
],
"to": [
0,
0,
0
],
"fov": 60
},
"night": false,
"light_ceiling": true
}
JSON
| kind | Camera input | Start settings |
|---|---|---|
render | One explicit camera or one saved camera ID | draft, hd, 4k; supported native dimensions |
panorama | One explicit camera or one saved camera ID | Native 8192 × 4096; omit custom size and quality |
walkthrough | 1–12 saved camera_ids from the project |
Native 8192 × 4096; creates parent and child frames |
Camera vectors use native project units. Choose a valid floor and optional night /
light_ceiling settings. The native renderer’s license and quotas remain authoritative.
Start a render and read its output
curl --request POST \
'https://planner5d.com/api/public/renders/{id}/start' \
--header 'Authorization: Bearer YOUR_ACCESS_TOKEN' \
--header 'Content-Type: application/json' \
--data-binary @- <<'JSON'
{
"quality": "hd"
}
JSON
curl --request GET \
'https://planner5d.com/api/public/renders/{id}' \
--header 'Authorization: Bearer YOUR_ACCESS_TOKEN'
ROOM-SCALE AUTOMATION
Generate furniture layouts for a room
POST /rooms/furnish generates alternative furniture layouts inside the room geometry you supply.
It uses the existing roomwizard service. Existing furniture may be replaced with a new selection.
Read the native project document and pass one Room object as room: [Room].
Supply room_type_id (0 is valid) and room_style_id.
Use GET /rooms/styles to select a style with used_in_ai_smart_wizard=true.
Both IDs are required by this Public API endpoint.
Consume proposals from the returned queue. An accepted request does not guarantee a suitable layout;
the queue can return an error. This workflow does not use the AI Studio jobId polling endpoint.
PUT /projects, then read its document back.curl --request GET \
'https://planner5d.com/api/public/rooms/styles' \
--header 'Authorization: Bearer YOUR_ACCESS_TOKEN'
curl --request POST \
'https://planner5d.com/api/public/rooms/furnish' \
--header 'Authorization: Bearer YOUR_ACCESS_TOKEN' \
--header 'Content-Type: application/json' \
--data-binary @- <<'JSON'
{
"id": "room-furnishing-example",
"room": [
{
"items": [
{
"items": [
{
"x": 0,
"y": 0,
"className": "Point",
"puid": "p100"
},
{
"x": 400,
"y": 0,
"className": "Point",
"puid": "p101"
}
],
"w": 9,
"hidden": false,
"materials": {
"outdoor": {
"name": "outdoor",
"texture": "wallp_1000",
"color": "#FFFFFF",
"tcolor": "#FFFFFF",
"scale": 1,
"rotate": 0,
"changedbyuser": true
},
"indoor": {
"name": "indoor",
"texture": "metal_1018",
"color": "#FFFFFF",
"tcolor": "#FFFFFF",
"scale": 1,
"rotate": 0,
"changedbyuser": true
},
"color_1": {
"name": "color_1",
"color": "#744214",
"tcolor": "#FFFFFF",
"scale": 1,
"rotate": 0,
"changedbyuser": true
}
},
"className": "Wall",
"puid": "p102",
"sX": 100,
"x": 3941.3804824352264,
"y": 3759.268952337291,
"sY": 100,
"z": 0,
"sZ": 100
},
{
"items": [
{
"x": 400,
"y": 0,
"className": "Point",
"puid": "p103"
},
{
"x": 400,
"y": 500,
"className": "Point",
"puid": "p104"
}
],
"w": 9,
"hidden": false,
"materials": {
"outdoor": {
"name": "outdoor",
"texture": "wallp_1000",
"color": "#FFFFFF",
"tcolor": "#FFFFFF",
"scale": 1,
"rotate": 0,
"changedbyuser": true
},
"indoor": {
"name": "indoor",
"texture": "metal_1018",
"color": "#FFFFFF",
"tcolor": "#FFFFFF",
"scale": 1,
"rotate": 0,
"changedbyuser": true
},
"color_1": {
"name": "color_1",
"color": "#744214",
"tcolor": "#FFFFFF",
"scale": 1,
"rotate": 0,
"changedbyuser": true
}
},
"className": "Wall",
"puid": "p105",
"sX": 100,
"x": 3941.3804824352264,
"y": 3759.268952337291,
"sY": 100,
"z": 0,
"sZ": 100
},
{
"items": [
{
"x": 400,
"y": 500,
"className": "Point",
"puid": "p106"
},
{
"x": 0,
"y": 500,
"className": "Point",
"puid": "p107"
}
],
"w": 9,
"hidden": false,
"materials": {
"outdoor": {
"name": "outdoor",
"texture": "wallp_1000",
"color": "#FFFFFF",
"tcolor": "#FFFFFF",
"scale": 1,
"rotate": 0,
"changedbyuser": true
},
"indoor": {
"name": "indoor",
"texture": "metal_1018",
"color": "#FFFFFF",
"tcolor": "#FFFFFF",
"scale": 1,
"rotate": 0,
"changedbyuser": true
},
"color_1": {
"name": "color_1",
"color": "#744214",
"tcolor": "#FFFFFF",
"scale": 1,
"rotate": 0,
"changedbyuser": true
}
},
"className": "Wall",
"puid": "p108",
"sX": 100,
"x": 3941.3804824352264,
"y": 3759.268952337291,
"sY": 100,
"z": 0,
"sZ": 100
},
{
"items": [
{
"x": 0,
"y": 500,
"className": "Point",
"puid": "p109"
},
{
"x": 0,
"y": 0,
"className": "Point",
"puid": "p110"
}
],
"w": 9,
"hidden": false,
"materials": {
"outdoor": {
"name": "outdoor",
"texture": "wallp_1000",
"color": "#FFFFFF",
"tcolor": "#FFFFFF",
"scale": 1,
"rotate": 0,
"changedbyuser": true
},
"indoor": {
"name": "indoor",
"texture": "metal_1018",
"color": "#FFFFFF",
"tcolor": "#FFFFFF",
"scale": 1,
"rotate": 0,
"changedbyuser": true
},
"color_1": {
"name": "color_1",
"color": "#744214",
"tcolor": "#FFFFFF",
"scale": 1,
"rotate": 0,
"changedbyuser": true
}
},
"className": "Wall",
"puid": "p111",
"sX": 100,
"x": 3941.3804824352264,
"y": 3759.268952337291,
"sY": 100,
"z": 0,
"sZ": 100
}
],
"x": 0,
"y": 0,
"z": 0,
"sX": 100,
"sY": 100,
"sZ": 100,
"h": 270,
"thickness": 10,
"fhidden": false,
"rhidden": false,
"w": 1,
"rtype": 0,
"materials": {
"outdoor": {
"name": "outdoor",
"texture": "wallp_1000",
"color": "#FFFFFF",
"tcolor": "#FFFFFF",
"scale": 1,
"rotate": 0,
"changedbyuser": true
},
"ceil": {
"name": "ceil",
"texture": "metal_1019",
"color": "#FFFFFF",
"tcolor": "#FFFFFF",
"scale": 0.6,
"rotate": 90,
"changedbyuser": true
},
"indoor": {
"name": "indoor",
"texture": "metal_1018",
"color": "#FFFFFF",
"tcolor": "#FFFFFF",
"scale": 1,
"rotate": 0,
"changedbyuser": true
},
"floor": {
"name": "floor",
"texture": "metal_1020",
"color": "#FFFFFF",
"tcolor": "#FFFFFF",
"scale": 1,
"rotate": 0,
"changedbyuser": true
}
},
"className": "Room",
"puid": "p112"
}
],
"room_type_id": 0,
"room_style_id": 3
}
JSON
What about rearranging the same furniture?
POST /rooms/relayout is intended to move existing furniture instead of replacing it.
It currently returns HTTP 503 and does not queue a task. The existing relayout engine still
needs a Public API adapter. Use /rooms/furnish when you want new furnishing proposals.
This workflow does not add a whole-house planner constrained to an uploaded product catalog, or lighting-fixture recognition.
BUILD A RELIABLE INTEGRATION
Jobs, retries & credits
Keep the identifier from submission
Task responses add a common job object. Keep job.id and poll the
matching endpoint; the native identifiers and status fields are also preserved. HTTP 200/202 alone does not mean that generation or rendering has finished.
Recover before resubmitting
If a submission times out, first check its job or task. Retry AI tasks with the same
request_id and unchanged inputs. Do not generate a new ID for every network retry.
One lifecycle to read
Use job.status and job.terminal across generation, model import, recognition,
rendering and export status responses. Collection entries carry their own job.
The original status remains available as job.native_status.
Keep the account, job.kind and job.id together; IDs are scoped to their workflow.
{"id":42,"status":"finished","job":{"id":"42","kind":"render","status":"succeeded",
"native_status":"finished","terminal":true}}prepared means a render still needs its explicit start call. queued and
running are in progress; requires_action needs native review.
succeeded, failed and cancelled are terminal.
An unrecognized native state is unknown; it is never treated as success.
A request that successfully reads a failed job still returns HTTP 200. Its job.status is
failed; that is different from a request rejected with an HTTP error.
request_id for
generation quotes/submissions and render preparation. It accepts 1–64 letters, digits, underscores or hyphens.
If both are present, they must match. Reuse a key only with identical task inputs.
Other operations reject this header instead of silently implying that retries are deduplicated.| Workflow | Identifier and status endpoint | Completion |
|---|---|---|
| AI Studio | jobId → /generations/{jobId} |
succeeded with results; inspect failed errors |
| 3D import | item.hash → /models/{modelHash}/status |
Native model state, including finished; source/configuration use the model endpoint |
| Plan recognition | id → find it in /recognition |
Native task status and resulting project key |
| Native render | id → /renders/{id} |
finished with output links; error is unsuccessful |
| HTTP export | Native export hash → project’s /exports collection |
Native export status and artifact links |
Polling checklist
- Respect
Retry-Afterand returned retry hints when present. - Otherwise use a short delay with backoff and a bounded deadline in your application.
- Stop on a terminal state. Inspect
error,retryableand billing before retrying. - Keep account and request IDs together. Another account cannot read the job.
There is no webhook registration or general job-cancellation endpoint in this version. Canceling a browser request does not cancel work already accepted by the server.
Usage and retention
- AI Studio uses the existing credit balance. Quote first; submit with a credit ceiling.
- Read billing for
reserved,chargedorrefundedstate. - Input generation asset IDs expire after 24 hours.
- Render preparation retry receipts last 24 hours.
- Native output retention applies. Download completed media for durable storage.
- Native imports, recognition, exports and renderer limits are preserved.
Consistent pagination metadata
Paged lists expose pagination alongside their existing fields. Project lists also expose
items as an alias for projects. Keep using the endpoint’s documented page parameters
and page size; this does not change native collection limits.
{"pagination":{"page":2,"page_size":30,"total_items":null,"total_pages":null,
"has_more":true,"next_page":3}}Unknown totals are null. Follow next_page while has_more is true;
collections without exact totals can end with an empty page. When present, sequence_end preserves
the native pageSequenceEnd marker. Unpaged reference lists do not pretend to have pagination.
No single global rate-limit number is promised here. Handle HTTP 429 and retry headers if returned by the service, and apply your own concurrency limits instead of unbounded polling or bulk submission.
WHEN A REQUEST NEEDS ATTENTION
Errors & troubleshooting
Bearer-authenticated integrations use standard errors by default. Legacy clients can opt in with
Accept: application/json, application/problem+json. Failed requests use HTTP 4xx/5xx and RFC 9457 Problem Details. The console and copied examples request this format.
{"type":"about:blank","title":"Forbidden","status":403,"detail":"Not enough permissions",
"instance":"/api/public/catalog","request_id":"EXAMPLE_REQUEST_ID","code":"forbidden"}type, title, status, detail, instance and
request_id are consistent. A native machine-readable code is included when available.
X-Request-Id is returned in the response headers. Internal error details and stack traces are hidden.
Compatibility with existing clients
Legacy authentication without the Problem Details Accept type keeps existing error status codes and bodies unchanged.
Legacy endpoints can use error: 0 for success or a nonzero numeric error with
errorMessage for failure, even on HTTP 200. Check the body when using that contract.
| HTTP status | What to check | Next action |
|---|---|---|
| 400 / 422 | Invalid JSON, missing inputs, expired asset ID, unsupported operation, mismatched mask size | Correct the request before retrying. |
| 401 / 403 | Missing/invalid authentication, license, server API access, ownership or catalog role | Use the right user token and verify account access. |
| 402 | Insufficient credits, a quote above maximum_credits,
or unavailable native renders | Read balance/quote and the operation’s existing entitlement. |
| 404 | Missing resource or resource outside this account | Check the identifier and which user created it. |
| 409 | Operation already in progress or request ID reused for changed input | Read status; retry the same task or assign a new ID to a genuinely new task. |
| 429 / 5xx | Service throttling or a temporary upstream problem | Use bounded backoff; check whether work was accepted before resubmitting. |
I can read the docs. Why does my API request return 403?
Documentation is public. Executing requests still needs a Bearer user token or existing B2B authentication, plus the endpoint’s license and ownership checks. Browser cookies alone do not admit a public request.
Why can a PRO user not create a business catalog?
POST /catalog requires an existing, enabled B2B account with that user assigned as its owner.
It does not provision a business account, grant roles or replace a missing owner assignment.
Why does an uploaded source image stop working?
Generation asset IDs are account-bound and last 24 hours. Reupload the image in the same account and use the new ID. A public image URL or a personal 3D model hash cannot replace an asset ID.
Can I pass a model name, provider or custom price?
No. Choose an operation such as floor-plan-3d or upscale. The server selects
its preset and default model. Existing license and credit rules determine access and cost.
Why does a staging request return HTML instead of JSON?
The staging host may require Basic Auth before the API is reached. That host password is separate from the API token. Use the console’s legacy-header option when both are required. The browser can reuse its host authentication; server-side clients must supply it through their normal HTTP authentication configuration. Do not embed passwords in URLs.
Can I safely retry every POST?
No global idempotency contract applies to all endpoints. AI generations use request_id;
render preparation has a 24-hour retry receipt, and repeated render start reads current status.
Catalog and other native writes retain their existing behavior. Inspect state before retrying a lost response.
Does a furnished illustration preserve exact dimensions or product identity?
An AI image is a visual output. Use recognized/stored project geometry for structured measurements and native rendering for a saved scene. Reference images do not guarantee an exact catalog SKU in generated pixels.
EVERY ENDPOINT
Complete API reference
Expand an operation for its input fields, response schema and parameters. Use the token above or Swagger’s Authorize control. Executing a write performs a real action in this environment.
https://planner5d.com.
The interactive reference uses this host; changing the code example base URL does not switch it.