DEVELOPER DOCUMENTATION

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.

Quickstart
JSON over HTTPS · language independent Your account · your assets & projects Existing credits · no separate API pricing
This environment https://planner5d.com/api/public
01

GET 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.

Authorization: Bearer YOUR_ACCESS_TOKEN

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 importsPublic API access, native import entitlement and limits Models and optional projects owned by the user
AI Studio jobsEligible account, enabled generation feature, available credits Uploaded assets and jobs belonging to the user
Business catalog managementExisting B2B owner or content-manager permissions The account’s configured catalog
Create a missing business catalogEnabled B2B account owner only One existing business account; no new account or role is created
Renders and exportsProject ownership and native license/format rights Owned project and output tasks
02

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.

03

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.

  1. 1GenerateSend your image and task together
  2. 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.

POST /generations
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}'
Retries reuse the same work. Repeat the same 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, mask and references.

Upload product reference images the same way. A 3D model hash is not an image asset ID.

POST /generation-assets
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.

Examples are not a price list. The displayed credit amounts are illustrative. Use the live quote to choose your ceiling.
POST /generations/quote
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.

POST /generations
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.

Python · complete generation workflow
"""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)

Download the Python example ↧

Choose a scenario, not a model

operationWhat it doesInputs
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
editEdit a marked image regionSource 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-planCreate a clean 2D plan drawingSource image
floor-plan-3d Create a furnished isometric illustration from a plan Source plan; optional references
videoGenerate a video with the native animation presetOptional source start frame; one result
upscaleEnhance a source image with a 2K output targetSource; 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
POST /generations
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
POST /generations
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
Illustration versus geometry: 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.
04

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

  1. Call /models/upload-from-url with a public HTTP(S) file URL and filename.
  2. Pass the returned assetModelToken to POST /models.
  3. Keep item.hash and poll /models/{modelHash}/status.
  4. 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.

POST /models/upload-from-url
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.

POST /models
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
05

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.

PRO alone does not create a B2B account. Catalog access must already be configured. A missing owner or a caller who is only a content manager receives 403 on this operation.

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.

POST /catalog
curl --request POST \
  'https://planner5d.com/api/public/catalog' \
  --header 'Authorization: Bearer YOUR_ACCESS_TOKEN'
  1. 1 Import models Use the B2B upload contract
  2. 2OrganizeCategories and hierarchy
  3. 3Add productsModels, SKUs and metadata
  4. 4BuildRun the native catalog builder
ResourceAvailable operations
/catalog/modelsList, create, read, update, delete, upload, retry, statistics, export, existing SKU lookup, existence checks and bulk updates
/catalog/itemsProduct records, metadata and bulk operations
/catalog/categories Create and manage categories and their default item settings
/catalog/category-relationsRead and update category hierarchy
/catalog/custom-iconsList, upload and delete category icons
/catalog/planner5d-productsRead Planner 5D products available to the business
PUT /catalog/buildBuild 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.

POST /catalog/items
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.

POST /catalog/models/{hash}/optimizations
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.

A failed optimization leaves the original available. Retry explicitly with 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.
POST /catalog/models/{hash}/optimizations/{jobId}/apply
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
06

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.

POST /recognition
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.

GET /projects/{projectHash}/document
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.

POST /projects/{projectHash}/export
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
07

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.

GET /projects/{projectHash}/embedded-links
curl --request GET \
  'https://planner5d.com/api/public/projects/{projectHash}/embedded-links' \
  --header 'Authorization: Bearer YOUR_ACCESS_TOKEN'
DisplayReady-made HTMLURL for your own iframe
Default project viewiframeproject_url
2D floor planiframe_2dproject_url_2d
Interactive 3D projectiframe_3dproject_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.

HTML · use the returned URL
<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 generatedHow to display it
Recognized plan or saved native sceneWait for recognition to finish and obtain project_hash. Use that project's 2D / 3D embed links.
Personal or B2B 3D modelFinish 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 illustrationUse results[].src in an <img>. Even floor-plan-3d returns an image, not editable geometry or an interactive 3D scene.
AI Studio videoUse the completed video result's src in <video controls>.
Native still renderUse image_url from a finished render in an <img>.
Native panorama or 360° walkthroughAfter 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 exportOffer 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_3d alone 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-src policy 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.
08

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

  1. Prepare a task from an owned project and camera.
  2. Read its id and call POST /renders/{id}/start.
  3. Poll GET /renders/{id}.
  4. On finished, read image_url and, 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.

POST /projects/{projectHash}/renders
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
renderOne explicit camera or one saved camera ID draft, hd, 4k; supported native dimensions
panoramaOne explicit camera or one saved camera ID Native 8192 × 4096; omit custom size and quality
walkthrough1–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
POST /renders/{id}/start
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
GET /renders/{id}
curl --request GET \
  'https://planner5d.com/api/public/renders/{id}' \
  --header 'Authorization: Bearer YOUR_ACCESS_TOKEN'
09

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.

Proposals do not automatically save a project. Save the selected native project JSON through PUT /projects, then read its document back.
GET /rooms/styles
curl --request GET \
  'https://planner5d.com/api/public/rooms/styles' \
  --header 'Authorization: Bearer YOUR_ACCESS_TOKEN'
POST /rooms/furnish
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.

10

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.

Example · a completed native render
{"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.

Idempotency-Key: use this header instead of 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 StudiojobId → /generations/{jobId} succeeded with results; inspect failed errors
3D importitem.hash → /models/{modelHash}/status Native model state, including finished; source/configuration use the model endpoint
Plan recognitionid → find it in /recognition Native task status and resulting project key
Native renderid → /renders/{id} finished with output links; error is unsuccessful
HTTP exportNative export hash → project’s /exports collection Native export status and artifact links

Polling checklist

  • Respect Retry-After and 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, retryable and 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, charged or refunded state.
  • 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.

Example · another page is available
{"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.

11

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.

Illustrative permission error · HTTP 403
{"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 / 422Invalid JSON, missing inputs, expired asset ID, unsupported operation, mismatched mask sizeCorrect the request before retrying.
401 / 403Missing/invalid authentication, license, server API access, ownership or catalog roleUse the right user token and verify account access.
402Insufficient credits, a quote above maximum_credits, or unavailable native rendersRead balance/quote and the operation’s existing entitlement.
404Missing 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 / 5xxService 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.

12

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.

Current API host: https://planner5d.com. The interactive reference uses this host; changing the code example base URL does not switch it.