# Norano > Norano is an agent-first API that turns floor plans into photorealistic > visualizations and walkable 360° tours. This file is an LLM-oriented index; > follow the links to load the full machine-readable contract. ## What Norano does (product context) - Turns uploaded floor plans (DXF/PDF) into photorealistic visualizations and walkable 360° tours, each produced by a long-running async `Job`. - NOTE: this is the product vision, NOT the list of endpoints available today. The OpenAPI document below is the authoritative, current API surface. ## What the v1 API exposes today - Discover the resource vocabulary: `GET /api/v1`. - Track any long operation through ONE unified async `Job`: `GET /api/v1/jobs` (list) and `GET /api/v1/jobs/{id}` (poll → fetch `result.assets[]`). - Job CREATE endpoints (projects, plans, visualizations, tours, assets) are rolling out incrementally — always treat the OpenAPI document as the source of truth for which endpoints currently exist. ## Auth - Bearer API key: `Authorization: Bearer norano_sk_live_…` (or `norano_sk_test_…`). - Keys are scoped (least privilege); `403 insufficient_scope` if a scope is missing. ## The async Job loop - Long operations return a `Job`. Poll `GET /api/v1/jobs/{id}` (optionally `Prefer: wait=30` to long-poll) until `status` is terminal, then read `result.assets[]`. ## Errors - Every error is `{ error: { type, code, message, remediation, retryable, docs_url, request_id } }`. - Branch on `type` + `code` (stable), never on `message`. ## Machine-readable resources - [OpenAPI 3.1 (JSON)](https://norano.ai/api/v1/openapi.json) — the single source of truth. - [OpenAPI 3.1 (Markdown)](https://norano.ai/api/v1/openapi.md) - [llms-full.txt](https://norano.ai/llms-full.txt) — the whole-corpus blob for one-shot loading. - [Capability manifest](https://norano.ai/.well-known/mcp.json) - [OAuth protected-resource metadata](https://norano.ai/.well-known/oauth-protected-resource) ## MCP - MCP server (Streamable HTTP): https://mcp.norano.ai/mcp - Discover it via the [capability manifest](https://norano.ai/.well-known/mcp.json). --- # Full contract ## Render-option enums - style: realistisch, reduziert, atmosphaerisch - lighting: tag, daemmerung, nacht, bewoelkt - furnishing: moebliert, leer - usage_type: wohnen, buero, retail, gastro, gewerbe, lager, bildung, gesundheit, parkieren ## Error catalog - invalid_request (type=invalid_request, http=400, retryable=false): Check the request shape against the API reference and resend. - missing_param (type=invalid_request, http=400, retryable=false): Add the missing parameter named in `param` and resend. - unsupported_media_type (type=invalid_request, http=415, retryable=false): Send the body with a supported Content-Type (e.g. application/json). - payload_too_large (type=invalid_request, http=413, retryable=false): Reduce the payload size, or use the signed-upload flow for large files. - method_not_allowed (type=invalid_request, http=405, retryable=false): Use one of the methods listed in the `Allow` response header. - missing_api_key (type=authentication, http=401, retryable=false): Send your key as `Authorization: Bearer norano_sk_live_…` (or a test key). - invalid_api_key (type=authentication, http=401, retryable=false): Check the key value, or issue a new key, then resend. - expired_api_key (type=authentication, http=401, retryable=false): Issue a new key and resend. - insufficient_scope (type=permission, http=403, retryable=false): Use a key that holds the required scope, or grant the scope to this key. - tenant_mismatch (type=permission, http=403, retryable=false): Use a key that belongs to the resource owner. - test_mode_resource_in_live (type=permission, http=403, retryable=false): Use a key whose mode (live/test) matches the resource. - resource_not_found (type=not_found, http=404, retryable=false): Check the id and that the resource belongs to your tenant. - idempotency_in_progress (type=conflict, http=409, retryable=true): Wait briefly and retry with the same Idempotency-Key. - plan_not_ready (type=conflict, http=409, retryable=true): Poll the plan's parse Job until it succeeds, then retry. - source_not_ready (type=conflict, http=409, retryable=true): Wait for the source to become ready, then retry. - validation_error (type=invalid_request, http=422, retryable=false): Fix the field named in `param` (see `message`) and resend. - idempotency_key_reuse (type=invalid_request, http=422, retryable=false): Use a fresh Idempotency-Key for a request with a different body. - rate_limited (type=rate_limit, http=429, retryable=true): Wait for the duration in the `Retry-After` header, then retry. - budget_exceeded (type=budget, http=402, retryable=false): Raise this key's cap or use a different key. - quota_exceeded (type=budget, http=402, retryable=false): Wait for the quota to reset, raise the quota, or use a different key. - provider_unavailable (type=provider, http=502, retryable=true): Retry after a short backoff; the provider may be temporarily down. - job_failed (type=job, http=409, retryable=false): Inspect the job's `error` object for the specific failure and remedy. - internal_error (type=server, http=500, retryable=true): Retry after a short backoff. If it persists, contact support. - service_unavailable (type=server, http=503, retryable=true): Retry after a short backoff. The request was rejected without being processed, so retrying is safe. ## OpenAPI 3.1 (full document) ```json { "openapi": "3.1.0", "info": { "title": "Norano API", "version": "v1", "summary": "Agent-first API for floor-plan visualization, tours, and plans.", "description": "The Norano public API turns floor plans into photorealistic visualizations and\nwalkable 360° tours — built for autonomous agents.\n\n## Auth\nSend your key as `Authorization: Bearer norano_sk_live_…` (or a `norano_sk_test_…`\ntest key). Keys are scoped (least privilege); a route returns `403 insufficient_scope`\nif the key lacks the scope it needs.\n\n## Response envelope\nA single object is returned bare with an `object` discriminator; lists are wrapped\nin a `{ object: \"list\", data, has_more, next_cursor, url }` envelope. Every response\nechoes a `Request-Id` header for correlation.\n\n## Errors\nEvery error is an envelope: error.{type, code, message, remediation, retryable,\nparam?, docs_url, request_id}. Branch on type + code (stable), never on message.\nPer-code docs: https://norano.app/docs/api/errors.\n\n## Idempotency\nSend `Idempotency-Key: ` on unsafe methods (POST/PUT/PATCH/DELETE) to make\na retry safe — the same key replays the original response. Keys are ≤255 chars.\n\n## Pagination\nCursor pagination: `limit` (1..100, default 20),\n`starting_after` / `ending_before` opaque cursors. Follow `next_cursor` until\n`has_more` is false.\n\n## Async jobs\nLong operations return a `Job`. Poll `GET /api/v1/jobs/{id}` (optionally with\n`Prefer: wait=` up to 30s to long-poll) until `status` is terminal\n(`succeeded` / `failed` / `canceled`); on success read `result.assets[]`.\n\n## Rate limits & spend caps\nEach key has a per-key sliding-window rate limit. Responses that pass through the limiter\n(any request that does real work) carry `RateLimit-Limit` / `RateLimit-Remaining` /\n`RateLimit-Reset`; an idempotency replay or an idempotency conflict (`409`/`422`) is\nresolved before the limiter and may omit them. A `429 rate_limited` (with `Retry-After`)\nmeans slow down. Each key also has a monthly spend cap: a paid\ncreate (`POST /visualizations` or `/tours`) reserves its cost estimate against the cap\nBEFORE enqueuing, and returns `402 budget_exceeded` if it would exceed — distinct from\nthe global provider guard. Check `GET /api/v1/usage` for the current rollup, and use\n`dry_run: true` to price a job without spending.\n\n## Discovery\nOpenAPI: https://norano.ai/api/v1/openapi.json (`.md` + `Accept: text/markdown` for Markdown).\nLLM index: https://norano.ai/llms.txt · whole-corpus blob: https://norano.ai/llms-full.txt.\nCapability manifest: https://norano.ai/.well-known/mcp.json · OAuth protected-resource metadata:\nhttps://norano.ai/.well-known/oauth-protected-resource.\nMCP server (Streamable HTTP): https://mcp.norano.ai/mcp.", "contact": { "name": "Norano", "url": "https://norano.ai/llms.txt" }, "license": { "name": "Proprietary" } }, "servers": [ { "url": "https://norano.ai", "description": "Production" } ], "security": [ { "apiKey": [] } ], "tags": [ { "name": "Discovery", "description": "Bootstrap the API from only the base URL." }, { "name": "Projects", "description": "The top-level container — create it first; plans/visualizations/tours hang off it." }, { "name": "Plans", "description": "A DWG/DXF/PDF floor plan — created via a two-step signed upload, parsed async." }, { "name": "Visualizations", "description": "Promptfree photorealistic renders of a plan — created async (a Job), with a `dry_run` cost pre-flight. No prompt; pick style/lighting/furnishing from fixed sets." }, { "name": "Tours", "description": "Walkable 360° tours (Marble worlds) of a project — created async (a Job), with a `dry_run` cost pre-flight. Once succeeded the Job carries the signed splat/pano/collider asset URLs + an `embed_url` in `metadata`." }, { "name": "Jobs", "description": "The unified async Job — submit→poll→fetch." }, { "name": "Webhooks", "description": "Signed outbound webhooks — register an https endpoint to receive job lifecycle events (job.queued/running/succeeded/failed/canceled) signed with a show-once whsec_ secret. Verify the Norano-Signature over the RAW body; the delivery log + manual replay are exposed too." }, { "name": "Assets", "description": "Project inputs/outputs + on-demand re-signing of expiring R2 URLs." }, { "name": "Usage", "description": "Per-key usage + reserved-spend rollup — what this key has consumed against its rate limit and spend cap. Distinct from the global per-provider guard." }, { "name": "Public", "description": "No-auth, rate-limited read-only surface — a pre-key on-ramp exposing only explicitly-published content. Never surfaces private projects." } ], "paths": { "/api/v1": { "get": { "tags": [ "Discovery" ], "operationId": "getApiRoot", "summary": "API root — the resource vocabulary.", "description": "Lists the resource-collection names the v1 API exposes so an agent can bootstrap.", "responses": { "200": { "description": "The API root object.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiRoot" } } } }, "401": { "description": "Error envelope (§3.6). Possible `code`: `missing_api_key`, `invalid_api_key`, `expired_api_key`.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "402": { "description": "Error envelope (§3.6). Possible `code`: `quota_exceeded`.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "405": { "description": "Error envelope (§3.6). Possible `code`: `method_not_allowed`.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "429": { "description": "Error envelope (§3.6). Possible `code`: `rate_limited`.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "500": { "description": "Error envelope (§3.6). Possible `code`: `internal_error`.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } } } } }, "/api/v1/projects": { "get": { "tags": [ "Projects" ], "operationId": "listProjects", "summary": "List your projects (newest first).", "security": [ { "apiKey": [] } ], "x-required-scopes": [ "projects:read" ], "parameters": [ { "$ref": "#/components/parameters/Limit" }, { "$ref": "#/components/parameters/StartingAfter" }, { "$ref": "#/components/parameters/EndingBefore" } ], "responses": { "200": { "description": "A page of projects.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ProjectList" } } } }, "400": { "description": "Error envelope (§3.6). Possible `code`: `invalid_request`.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "401": { "description": "Error envelope (§3.6). Possible `code`: `missing_api_key`, `invalid_api_key`, `expired_api_key`.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "402": { "description": "Error envelope (§3.6). Possible `code`: `quota_exceeded`.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "403": { "description": "Error envelope (§3.6). Possible `code`: `insufficient_scope`.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "405": { "description": "Error envelope (§3.6). Possible `code`: `method_not_allowed`.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "429": { "description": "Error envelope (§3.6). Possible `code`: `rate_limited`.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "500": { "description": "Error envelope (§3.6). Possible `code`: `internal_error`.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } } } }, "post": { "tags": [ "Projects" ], "operationId": "createProject", "summary": "Create a project.", "description": "Creates a project owned by your key’s tenant (org). Send `Idempotency-Key` to make a retry safe.", "security": [ { "apiKey": [] } ], "x-required-scopes": [ "projects:write" ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ProjectCreate" } } } }, "responses": { "201": { "description": "The created project.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Project" } } } }, "400": { "description": "Error envelope (§3.6). Possible `code`: `invalid_request`.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "401": { "description": "Error envelope (§3.6). Possible `code`: `missing_api_key`, `invalid_api_key`, `expired_api_key`.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "402": { "description": "Error envelope (§3.6). Possible `code`: `quota_exceeded`.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "403": { "description": "Error envelope (§3.6). Possible `code`: `insufficient_scope`.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "405": { "description": "Error envelope (§3.6). Possible `code`: `method_not_allowed`.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "422": { "description": "Error envelope (§3.6). Possible `code`: `idempotency_key_reuse`.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "429": { "description": "Error envelope (§3.6). Possible `code`: `rate_limited`.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "500": { "description": "Error envelope (§3.6). Possible `code`: `internal_error`.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } } } } }, "/api/v1/projects/{id}": { "get": { "tags": [ "Projects" ], "operationId": "getProject", "summary": "Fetch one project.", "security": [ { "apiKey": [] } ], "x-required-scopes": [ "projects:read" ], "parameters": [ { "name": "id", "in": "path", "required": true, "description": "The public project id (`proj_`).", "schema": { "type": "string" } } ], "responses": { "200": { "description": "The project.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Project" } } } }, "401": { "description": "Error envelope (§3.6). Possible `code`: `missing_api_key`, `invalid_api_key`, `expired_api_key`.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "402": { "description": "Error envelope (§3.6). Possible `code`: `quota_exceeded`.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "403": { "description": "Error envelope (§3.6). Possible `code`: `insufficient_scope`.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "404": { "description": "Error envelope (§3.6). Possible `code`: `resource_not_found`.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "405": { "description": "Error envelope (§3.6). Possible `code`: `method_not_allowed`.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "429": { "description": "Error envelope (§3.6). Possible `code`: `rate_limited`.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "500": { "description": "Error envelope (§3.6). Possible `code`: `internal_error`.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } } } } }, "/api/v1/projects/{id}/plans": { "post": { "tags": [ "Plans" ], "operationId": "createPlan", "summary": "Create a plan via a two-step signed upload.", "description": "Returns a single signed PUT `upload_url` (+`upload_headers`, `expires_at` ≤300s) you PUT the file bytes to, plus a `plans[]` (one entry for `mode:single`, two — grundriss + vermarktung — for `mode:both`), each with its parse Job id (`job_plan_`). You MUST PUT exactly `size` bytes with the returned `Content-Type`. The plan starts `queued`; its parse Job reflects `plan.status`. Poll the Job after your upload completes. Send `Idempotency-Key` to make a retry safe.", "security": [ { "apiKey": [] } ], "x-required-scopes": [ "plans:write" ], "parameters": [ { "name": "id", "in": "path", "required": true, "description": "The public project id (`proj_`).", "schema": { "type": "string" } } ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/PlanCreate" } } } }, "responses": { "202": { "description": "The signed upload + the created plan(s).", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/PlanUpload" } } } }, "400": { "description": "Error envelope (§3.6). Possible `code`: `invalid_request`.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "401": { "description": "Error envelope (§3.6). Possible `code`: `missing_api_key`, `invalid_api_key`, `expired_api_key`.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "402": { "description": "Error envelope (§3.6). Possible `code`: `quota_exceeded`.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "403": { "description": "Error envelope (§3.6). Possible `code`: `insufficient_scope`, `test_mode_resource_in_live`.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "404": { "description": "Error envelope (§3.6). Possible `code`: `resource_not_found`.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "405": { "description": "Error envelope (§3.6). Possible `code`: `method_not_allowed`.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "409": { "description": "Error envelope (§3.6). Possible `code`: `plan_not_ready`, `source_not_ready`.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "422": { "description": "Error envelope (§3.6). Possible `code`: `idempotency_key_reuse`.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "429": { "description": "Error envelope (§3.6). Possible `code`: `rate_limited`.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "500": { "description": "Error envelope (§3.6). Possible `code`: `internal_error`.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } } } } }, "/api/v1/plans/{id}": { "get": { "tags": [ "Plans" ], "operationId": "getPlan", "summary": "Fetch one plan (status reflects the unified Job).", "description": "The `status` is the unified job-style status (`queued|running|succeeded|failed`) mapped from the plan’s lifecycle — the same status the plan’s `job_id` reports.", "security": [ { "apiKey": [] } ], "x-required-scopes": [ "plans:read" ], "parameters": [ { "name": "id", "in": "path", "required": true, "description": "The public plan id (`plan_`).", "schema": { "type": "string" } } ], "responses": { "200": { "description": "The plan.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Plan" } } } }, "401": { "description": "Error envelope (§3.6). Possible `code`: `missing_api_key`, `invalid_api_key`, `expired_api_key`.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "402": { "description": "Error envelope (§3.6). Possible `code`: `quota_exceeded`.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "403": { "description": "Error envelope (§3.6). Possible `code`: `insufficient_scope`, `test_mode_resource_in_live`.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "404": { "description": "Error envelope (§3.6). Possible `code`: `resource_not_found`.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "405": { "description": "Error envelope (§3.6). Possible `code`: `method_not_allowed`.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "429": { "description": "Error envelope (§3.6). Possible `code`: `rate_limited`.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "500": { "description": "Error envelope (§3.6). Possible `code`: `internal_error`.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } } } } }, "/api/v1/plans/{id}/complete": { "post": { "tags": [ "Plans" ], "operationId": "completePlanUpload", "summary": "Confirm a plan upload completed and trigger parsing.", "description": "Call this AFTER you PUT the file bytes to the `upload_url` from `POST /api/v1/projects/{id}/plans`. It HEAD-verifies the object landed in storage, then triggers the parse for every plan still `queued` that is backed by that upload (one `mode:both` upload backs two plans — completing once triggers both). Idempotent: calling it again only re-triggers plans that are still `queued`, never re-parses an already-running/ready plan. If the object is not there yet you get `409 source_not_ready` (finish the PUT, then retry). Returns the plan; parsing is async, so poll the plan’s `job_id` (`GET /api/v1/jobs/{id}`) until it transitions `queued → running → succeeded|failed`. Send `Idempotency-Key` to make a retry safe.", "security": [ { "apiKey": [] } ], "x-required-scopes": [ "plans:write" ], "parameters": [ { "name": "id", "in": "path", "required": true, "description": "The public plan id (`plan_`).", "schema": { "type": "string" } } ], "responses": { "200": { "description": "The plan (its parse Job has been triggered if it was queued).", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Plan" } } } }, "401": { "description": "Error envelope (§3.6). Possible `code`: `missing_api_key`, `invalid_api_key`, `expired_api_key`.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "402": { "description": "Error envelope (§3.6). Possible `code`: `quota_exceeded`.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "403": { "description": "Error envelope (§3.6). Possible `code`: `insufficient_scope`, `test_mode_resource_in_live`.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "404": { "description": "Error envelope (§3.6). Possible `code`: `resource_not_found`.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "405": { "description": "Error envelope (§3.6). Possible `code`: `method_not_allowed`.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "409": { "description": "Error envelope (§3.6). Possible `code`: `source_not_ready`, `idempotency_in_progress`.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "422": { "description": "Error envelope (§3.6). Possible `code`: `idempotency_key_reuse`.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "429": { "description": "Error envelope (§3.6). Possible `code`: `rate_limited`.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "500": { "description": "Error envelope (§3.6). Possible `code`: `internal_error`.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } } } } }, "/api/v1/visualizations": { "post": { "tags": [ "Visualizations" ], "operationId": "createVisualization", "summary": "Create a visualization (async) or run a cost pre-flight.", "description": "Promptfree: the body has NO `prompt`. Pick `style`/`lighting`/`furnishing` from fixed value sets (defaults `realistisch`/`tag`/`moebliert`); `seed` is `0..2147483647`. `project_id` (a `proj_`) is REQUIRED. The render uses the project’s latest plan image, else its newest ready plan; with neither you get `source_not_ready` (409 — upload a plan/image, let it finish, then retry). With `dry_run:true` you get `200 { estimated_cost_usd_cents, would_create }` — NO job, NO spend (§8.1). Otherwise you get `202` + a `Job` (type `visualization`); poll it via `urls.self` — once `succeeded` its `result.assets[]` carry the signed JPG `url` + `expires_at`. Send `Idempotency-Key` to make a retry safe. A test-mode key creates a test job (fixture output, `cost_usd_cents:0`, no provider call).", "security": [ { "apiKey": [] } ], "x-required-scopes": [ "visualizations:write" ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/VisualizationCreate" } } } }, "responses": { "200": { "description": "The `dry_run` cost pre-flight (no job created, no spend).", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/VisualizationCostEstimate" } } } }, "202": { "description": "The created visualization Job (poll it via `urls.self`).", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Job" } } } }, "400": { "description": "Error envelope (§3.6). Possible `code`: `invalid_request`.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "401": { "description": "Error envelope (§3.6). Possible `code`: `missing_api_key`, `invalid_api_key`, `expired_api_key`.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "402": { "description": "Error envelope (§3.6). Possible `code`: `quota_exceeded`, `budget_exceeded`.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "403": { "description": "Error envelope (§3.6). Possible `code`: `insufficient_scope`.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "404": { "description": "Error envelope (§3.6). Possible `code`: `resource_not_found`.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "405": { "description": "Error envelope (§3.6). Possible `code`: `method_not_allowed`.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "409": { "description": "Error envelope (§3.6). Possible `code`: `source_not_ready`.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "422": { "description": "Error envelope (§3.6). Possible `code`: `idempotency_key_reuse`.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "429": { "description": "Error envelope (§3.6). Possible `code`: `rate_limited`.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "500": { "description": "Error envelope (§3.6). Possible `code`: `internal_error`.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } } } }, "get": { "tags": [ "Visualizations" ], "operationId": "listVisualizations", "summary": "List visualization Jobs (newest first).", "description": "Each item is a `Job` of type `visualization`. Optionally narrow to one project with `project_id` (a `proj_`). Cursor-paginated; test/live isolated.", "security": [ { "apiKey": [] } ], "x-required-scopes": [ "jobs:read" ], "parameters": [ { "name": "project_id", "in": "query", "required": false, "description": "Narrow to one project (`proj_`).", "schema": { "type": "string" } }, { "$ref": "#/components/parameters/Limit" }, { "$ref": "#/components/parameters/StartingAfter" }, { "$ref": "#/components/parameters/EndingBefore" } ], "responses": { "200": { "description": "A page of visualization Jobs.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/JobList" } } } }, "400": { "description": "Error envelope (§3.6). Possible `code`: `invalid_request`.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "401": { "description": "Error envelope (§3.6). Possible `code`: `missing_api_key`, `invalid_api_key`, `expired_api_key`.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "402": { "description": "Error envelope (§3.6). Possible `code`: `quota_exceeded`.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "403": { "description": "Error envelope (§3.6). Possible `code`: `insufficient_scope`.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "404": { "description": "Error envelope (§3.6). Possible `code`: `resource_not_found`.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "405": { "description": "Error envelope (§3.6). Possible `code`: `method_not_allowed`.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "429": { "description": "Error envelope (§3.6). Possible `code`: `rate_limited`.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "500": { "description": "Error envelope (§3.6). Possible `code`: `internal_error`.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } } } } }, "/api/v1/visualizations/{id}": { "get": { "tags": [ "Visualizations" ], "operationId": "getVisualization", "summary": "Fetch one visualization as a Job.", "description": "The `{id}` is the visualization’s Job id (`job_gj_`). Returns the unified `Job` (type `visualization`); once `succeeded`, `result.assets[]` carry the signed JPG `url` + `expires_at`.", "security": [ { "apiKey": [] } ], "x-required-scopes": [ "jobs:read" ], "parameters": [ { "name": "id", "in": "path", "required": true, "description": "The visualization’s Job id (`job_gj_`).", "schema": { "type": "string" } } ], "responses": { "200": { "description": "The visualization Job.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Job" } } } }, "401": { "description": "Error envelope (§3.6). Possible `code`: `missing_api_key`, `invalid_api_key`, `expired_api_key`.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "402": { "description": "Error envelope (§3.6). Possible `code`: `quota_exceeded`.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "403": { "description": "Error envelope (§3.6). Possible `code`: `insufficient_scope`, `test_mode_resource_in_live`.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "404": { "description": "Error envelope (§3.6). Possible `code`: `resource_not_found`.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "405": { "description": "Error envelope (§3.6). Possible `code`: `method_not_allowed`.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "429": { "description": "Error envelope (§3.6). Possible `code`: `rate_limited`.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "500": { "description": "Error envelope (§3.6). Possible `code`: `internal_error`.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } } } } }, "/api/v1/tours": { "post": { "tags": [ "Tours" ], "operationId": "createTour", "summary": "Create a tour (async) or run a cost pre-flight.", "description": "A tour is a walkable 360° Marble world generated from the project’s SOURCE VISUALIZATION. `project_id` (a `proj_`) is REQUIRED; `model` is the 2-value wire set `marble-1.1-plus`|`marble-1.1` (default `marble-1.1`; `marble-1.1-mini` is NOT accepted — the live Marble API rejects it). `display_name` is optional (1..64, default `Rundgang`); `seed` is a uint32 `0..4294967295`. A live key needs a completed visualization first, else `source_not_ready` (409 — create a visualization, let it finish, then retry). With `dry_run:true` you get `200 { estimated_cost_usd_cents, would_create }` — NO job, NO spend (§8.1). Otherwise you get `202` + a `Job` (type `tour`); poll it via `urls.self` — once `succeeded` its `result.assets[]` carry the signed splat (spz), collider (glb), and pano `url` + `expires_at`, and its `metadata` carries `embed_url` + `tour_id`. Send `Idempotency-Key` to make a retry safe. A test-mode key creates a test job (fixture output, `cost_usd_cents:0`, no provider call).", "security": [ { "apiKey": [] } ], "x-required-scopes": [ "tours:write" ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/TourCreate" } } } }, "responses": { "200": { "description": "The `dry_run` cost pre-flight (no job created, no spend).", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/TourCostEstimate" } } } }, "202": { "description": "The created tour Job (poll it via `urls.self`).", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Job" } } } }, "400": { "description": "Error envelope (§3.6). Possible `code`: `invalid_request`.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "401": { "description": "Error envelope (§3.6). Possible `code`: `missing_api_key`, `invalid_api_key`, `expired_api_key`.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "402": { "description": "Error envelope (§3.6). Possible `code`: `quota_exceeded`, `budget_exceeded`.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "403": { "description": "Error envelope (§3.6). Possible `code`: `insufficient_scope`.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "404": { "description": "Error envelope (§3.6). Possible `code`: `resource_not_found`.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "405": { "description": "Error envelope (§3.6). Possible `code`: `method_not_allowed`.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "409": { "description": "Error envelope (§3.6). Possible `code`: `source_not_ready`.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "422": { "description": "Error envelope (§3.6). Possible `code`: `idempotency_key_reuse`.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "429": { "description": "Error envelope (§3.6). Possible `code`: `rate_limited`.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "500": { "description": "Error envelope (§3.6). Possible `code`: `internal_error`.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } } } }, "get": { "tags": [ "Tours" ], "operationId": "listTours", "summary": "List tour Jobs (newest first).", "description": "Each item is a `Job` of type `tour`. Optionally narrow to one project with `project_id` (a `proj_`). Cursor-paginated; test/live isolated.", "security": [ { "apiKey": [] } ], "x-required-scopes": [ "jobs:read" ], "parameters": [ { "name": "project_id", "in": "query", "required": false, "description": "Narrow to one project (`proj_`).", "schema": { "type": "string" } }, { "$ref": "#/components/parameters/Limit" }, { "$ref": "#/components/parameters/StartingAfter" }, { "$ref": "#/components/parameters/EndingBefore" } ], "responses": { "200": { "description": "A page of tour Jobs.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/JobList" } } } }, "400": { "description": "Error envelope (§3.6). Possible `code`: `invalid_request`.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "401": { "description": "Error envelope (§3.6). Possible `code`: `missing_api_key`, `invalid_api_key`, `expired_api_key`.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "402": { "description": "Error envelope (§3.6). Possible `code`: `quota_exceeded`.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "403": { "description": "Error envelope (§3.6). Possible `code`: `insufficient_scope`.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "404": { "description": "Error envelope (§3.6). Possible `code`: `resource_not_found`.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "405": { "description": "Error envelope (§3.6). Possible `code`: `method_not_allowed`.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "429": { "description": "Error envelope (§3.6). Possible `code`: `rate_limited`.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "500": { "description": "Error envelope (§3.6). Possible `code`: `internal_error`.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } } } } }, "/api/v1/tours/{id}": { "get": { "tags": [ "Tours" ], "operationId": "getTour", "summary": "Fetch one tour as a Job.", "description": "The `{id}` is the tour’s Job id (`job_gj_`). Returns the unified `Job` (type `tour`); once `succeeded`, `result.assets[]` carry the signed splat (spz), collider (glb), and pano `url` + `expires_at`, and `metadata` carries `embed_url` (live tours) + `tour_id`.", "security": [ { "apiKey": [] } ], "x-required-scopes": [ "jobs:read" ], "parameters": [ { "name": "id", "in": "path", "required": true, "description": "The tour’s Job id (`job_gj_`).", "schema": { "type": "string" } } ], "responses": { "200": { "description": "The tour Job.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Job" } } } }, "401": { "description": "Error envelope (§3.6). Possible `code`: `missing_api_key`, `invalid_api_key`, `expired_api_key`.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "402": { "description": "Error envelope (§3.6). Possible `code`: `quota_exceeded`.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "403": { "description": "Error envelope (§3.6). Possible `code`: `insufficient_scope`, `test_mode_resource_in_live`.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "404": { "description": "Error envelope (§3.6). Possible `code`: `resource_not_found`.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "405": { "description": "Error envelope (§3.6). Possible `code`: `method_not_allowed`.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "429": { "description": "Error envelope (§3.6). Possible `code`: `rate_limited`.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "500": { "description": "Error envelope (§3.6). Possible `code`: `internal_error`.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } } } } }, "/api/v1/jobs": { "get": { "tags": [ "Jobs" ], "operationId": "listJobs", "summary": "List unified Jobs (newest first).", "security": [ { "apiKey": [] } ], "x-required-scopes": [ "jobs:read" ], "parameters": [ { "$ref": "#/components/parameters/Limit" }, { "$ref": "#/components/parameters/StartingAfter" }, { "$ref": "#/components/parameters/EndingBefore" } ], "responses": { "200": { "description": "A page of Jobs.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/JobList" } } } }, "400": { "description": "Error envelope (§3.6). Possible `code`: `invalid_request`.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "401": { "description": "Error envelope (§3.6). Possible `code`: `missing_api_key`, `invalid_api_key`, `expired_api_key`.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "402": { "description": "Error envelope (§3.6). Possible `code`: `quota_exceeded`.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "403": { "description": "Error envelope (§3.6). Possible `code`: `insufficient_scope`.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "405": { "description": "Error envelope (§3.6). Possible `code`: `method_not_allowed`.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "429": { "description": "Error envelope (§3.6). Possible `code`: `rate_limited`.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "500": { "description": "Error envelope (§3.6). Possible `code`: `internal_error`.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } } } } }, "/api/v1/jobs/{id}": { "get": { "tags": [ "Jobs" ], "operationId": "getJob", "summary": "Poll one unified Job.", "security": [ { "apiKey": [] } ], "x-required-scopes": [ "jobs:read" ], "parameters": [ { "name": "id", "in": "path", "required": true, "description": "The public Job id (e.g. `job_gj_…` or `job_plan_…`).", "schema": { "type": "string" } }, { "$ref": "#/components/parameters/PreferWait" } ], "responses": { "200": { "description": "The Job.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Job" } } } }, "401": { "description": "Error envelope (§3.6). Possible `code`: `missing_api_key`, `invalid_api_key`, `expired_api_key`.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "402": { "description": "Error envelope (§3.6). Possible `code`: `quota_exceeded`.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "403": { "description": "Error envelope (§3.6). Possible `code`: `insufficient_scope`.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "404": { "description": "Error envelope (§3.6). Possible `code`: `resource_not_found`.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "405": { "description": "Error envelope (§3.6). Possible `code`: `method_not_allowed`.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "429": { "description": "Error envelope (§3.6). Possible `code`: `rate_limited`.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "500": { "description": "Error envelope (§3.6). Possible `code`: `internal_error`.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } } } } }, "/api/v1/webhook_endpoints": { "get": { "tags": [ "Webhooks" ], "operationId": "listWebhookEndpoints", "summary": "List the tenant’s webhook endpoints.", "security": [ { "apiKey": [] } ], "x-required-scopes": [ "webhooks:read" ], "parameters": [ { "$ref": "#/components/parameters/Limit" }, { "$ref": "#/components/parameters/StartingAfter" } ], "responses": { "200": { "description": "A list of webhook endpoints.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/WebhookEndpointList" } } } }, "401": { "description": "Error envelope (§3.6). Possible `code`: `missing_api_key`, `invalid_api_key`, `expired_api_key`.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "402": { "description": "Error envelope (§3.6). Possible `code`: `quota_exceeded`.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "403": { "description": "Error envelope (§3.6). Possible `code`: `insufficient_scope`.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "405": { "description": "Error envelope (§3.6). Possible `code`: `method_not_allowed`.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "429": { "description": "Error envelope (§3.6). Possible `code`: `rate_limited`.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "500": { "description": "Error envelope (§3.6). Possible `code`: `internal_error`.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } } } }, "post": { "tags": [ "Webhooks" ], "operationId": "createWebhookEndpoint", "summary": "Register a webhook endpoint (returns the show-once secret).", "description": "Register an https callback URL to receive job lifecycle events. The URL is SSRF-validated (https-only; the host must resolve to a public address). The response carries the signing `secret` (`whsec_…`) EXACTLY ONCE — store it now; it is irrecoverable. Verify the `Norano-Signature` header over the RAW request body (see the schema description for the recipe).", "security": [ { "apiKey": [] } ], "x-required-scopes": [ "webhooks:write" ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/WebhookEndpointCreate" } } } }, "responses": { "201": { "description": "The registered endpoint + the show-once secret.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/WebhookEndpointWithSecret" } } } }, "400": { "description": "Error envelope (§3.6). Possible `code`: `invalid_request`.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "401": { "description": "Error envelope (§3.6). Possible `code`: `missing_api_key`, `invalid_api_key`, `expired_api_key`.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "402": { "description": "Error envelope (§3.6). Possible `code`: `quota_exceeded`.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "403": { "description": "Error envelope (§3.6). Possible `code`: `insufficient_scope`.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "405": { "description": "Error envelope (§3.6). Possible `code`: `method_not_allowed`.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "429": { "description": "Error envelope (§3.6). Possible `code`: `rate_limited`.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "500": { "description": "Error envelope (§3.6). Possible `code`: `internal_error`.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } } } } }, "/api/v1/webhook_endpoints/{id}": { "get": { "tags": [ "Webhooks" ], "operationId": "getWebhookEndpoint", "summary": "Get one webhook endpoint.", "security": [ { "apiKey": [] } ], "x-required-scopes": [ "webhooks:read" ], "parameters": [ { "name": "id", "in": "path", "required": true, "description": "The public endpoint id (`we_…`).", "schema": { "type": "string" } } ], "responses": { "200": { "description": "The endpoint.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/WebhookEndpoint" } } } }, "401": { "description": "Error envelope (§3.6). Possible `code`: `missing_api_key`, `invalid_api_key`, `expired_api_key`.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "402": { "description": "Error envelope (§3.6). Possible `code`: `quota_exceeded`.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "403": { "description": "Error envelope (§3.6). Possible `code`: `insufficient_scope`.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "404": { "description": "Error envelope (§3.6). Possible `code`: `resource_not_found`.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "405": { "description": "Error envelope (§3.6). Possible `code`: `method_not_allowed`.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "429": { "description": "Error envelope (§3.6). Possible `code`: `rate_limited`.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "500": { "description": "Error envelope (§3.6). Possible `code`: `internal_error`.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } } } }, "delete": { "tags": [ "Webhooks" ], "operationId": "deleteWebhookEndpoint", "summary": "Delete a webhook endpoint.", "security": [ { "apiKey": [] } ], "x-required-scopes": [ "webhooks:write" ], "parameters": [ { "name": "id", "in": "path", "required": true, "description": "The public endpoint id (`we_…`).", "schema": { "type": "string" } } ], "responses": { "200": { "description": "The endpoint was deleted.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/WebhookDeleted" } } } }, "401": { "description": "Error envelope (§3.6). Possible `code`: `missing_api_key`, `invalid_api_key`, `expired_api_key`.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "402": { "description": "Error envelope (§3.6). Possible `code`: `quota_exceeded`.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "403": { "description": "Error envelope (§3.6). Possible `code`: `insufficient_scope`.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "404": { "description": "Error envelope (§3.6). Possible `code`: `resource_not_found`.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "405": { "description": "Error envelope (§3.6). Possible `code`: `method_not_allowed`.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "429": { "description": "Error envelope (§3.6). Possible `code`: `rate_limited`.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "500": { "description": "Error envelope (§3.6). Possible `code`: `internal_error`.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } } } } }, "/api/v1/webhook_deliveries": { "get": { "tags": [ "Webhooks" ], "operationId": "listWebhookDeliveries", "summary": "List the tenant’s webhook delivery log (newest first).", "security": [ { "apiKey": [] } ], "x-required-scopes": [ "webhooks:read" ], "parameters": [ { "$ref": "#/components/parameters/Limit" }, { "$ref": "#/components/parameters/StartingAfter" } ], "responses": { "200": { "description": "A list of webhook deliveries.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/WebhookDeliveryList" } } } }, "401": { "description": "Error envelope (§3.6). Possible `code`: `missing_api_key`, `invalid_api_key`, `expired_api_key`.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "402": { "description": "Error envelope (§3.6). Possible `code`: `quota_exceeded`.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "403": { "description": "Error envelope (§3.6). Possible `code`: `insufficient_scope`.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "405": { "description": "Error envelope (§3.6). Possible `code`: `method_not_allowed`.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "429": { "description": "Error envelope (§3.6). Possible `code`: `rate_limited`.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "500": { "description": "Error envelope (§3.6). Possible `code`: `internal_error`.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } } } } }, "/api/v1/webhook_deliveries/{id}/retry": { "post": { "tags": [ "Webhooks" ], "operationId": "retryWebhookDelivery", "summary": "Manually replay a webhook delivery.", "description": "Creates a NEW delivery attempt re-sending the original event (same event id + signed body) and enqueues it. The original delivery’s history is preserved.", "security": [ { "apiKey": [] } ], "x-required-scopes": [ "webhooks:write" ], "parameters": [ { "name": "id", "in": "path", "required": true, "description": "The public delivery id (`wd_…`).", "schema": { "type": "string" } } ], "responses": { "202": { "description": "The replay delivery was created + enqueued.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/WebhookDelivery" } } } }, "401": { "description": "Error envelope (§3.6). Possible `code`: `missing_api_key`, `invalid_api_key`, `expired_api_key`.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "402": { "description": "Error envelope (§3.6). Possible `code`: `quota_exceeded`.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "403": { "description": "Error envelope (§3.6). Possible `code`: `insufficient_scope`.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "404": { "description": "Error envelope (§3.6). Possible `code`: `resource_not_found`.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "405": { "description": "Error envelope (§3.6). Possible `code`: `method_not_allowed`.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "429": { "description": "Error envelope (§3.6). Possible `code`: `rate_limited`.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "500": { "description": "Error envelope (§3.6). Possible `code`: `internal_error`.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } } } } }, "/api/v1/assets": { "get": { "tags": [ "Assets" ], "operationId": "listAssets", "summary": "List a project’s assets (inputs + outputs).", "description": "Lists the `input_asset` and `output_asset` rows for ONE project, newest first. Pass EXACTLY ONE of `project_id` or `plan_id` (a `plan_id` resolves to its project — assets are scoped per-project, not per-plan). A missing or cross-tenant anchor returns `resource_not_found`. The list carries metadata only; re-sign a downloadable URL via `GET /api/v1/assets/{id}/url`.", "security": [ { "apiKey": [] } ], "x-required-scopes": [ "assets:read" ], "parameters": [ { "$ref": "#/components/parameters/ProjectId" }, { "$ref": "#/components/parameters/PlanId" }, { "$ref": "#/components/parameters/Limit" }, { "$ref": "#/components/parameters/StartingAfter" }, { "$ref": "#/components/parameters/EndingBefore" } ], "responses": { "200": { "description": "A page of assets.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/AssetList" } } } }, "400": { "description": "Error envelope (§3.6). Possible `code`: `invalid_request`.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "401": { "description": "Error envelope (§3.6). Possible `code`: `missing_api_key`, `invalid_api_key`, `expired_api_key`.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "402": { "description": "Error envelope (§3.6). Possible `code`: `quota_exceeded`.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "403": { "description": "Error envelope (§3.6). Possible `code`: `insufficient_scope`.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "404": { "description": "Error envelope (§3.6). Possible `code`: `resource_not_found`.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "405": { "description": "Error envelope (§3.6). Possible `code`: `method_not_allowed`.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "429": { "description": "Error envelope (§3.6). Possible `code`: `rate_limited`.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "500": { "description": "Error envelope (§3.6). Possible `code`: `internal_error`.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } } } } }, "/api/v1/assets/{id}/url": { "get": { "tags": [ "Assets" ], "operationId": "getAssetUrl", "summary": "Re-sign an asset — fresh signed URL + expires_at.", "description": "Returns a fresh, short-lived signed download URL for the asset, with an `expires_at` (always present; the R2 TTL is capped at 300s). Call this again once a previously issued URL is past `expires_at`.", "security": [ { "apiKey": [] } ], "x-required-scopes": [ "assets:read" ], "parameters": [ { "name": "id", "in": "path", "required": true, "description": "The public asset id (`asset_` output or `asset_in_` input).", "schema": { "type": "string" } } ], "responses": { "200": { "description": "A freshly signed asset URL.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/AssetUrl" } } } }, "401": { "description": "Error envelope (§3.6). Possible `code`: `missing_api_key`, `invalid_api_key`, `expired_api_key`.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "402": { "description": "Error envelope (§3.6). Possible `code`: `quota_exceeded`.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "403": { "description": "Error envelope (§3.6). Possible `code`: `insufficient_scope`.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "404": { "description": "Error envelope (§3.6). Possible `code`: `resource_not_found`.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "405": { "description": "Error envelope (§3.6). Possible `code`: `method_not_allowed`.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "429": { "description": "Error envelope (§3.6). Possible `code`: `rate_limited`.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "500": { "description": "Error envelope (§3.6). Possible `code`: `internal_error`.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } } } } }, "/api/v1/usage": { "get": { "tags": [ "Usage" ], "operationId": "getUsage", "summary": "This key’s usage + spend rollup for the current period.", "description": "Returns the per-KEY usage rollup for the current month (DESIGN §8.2 + §10): `requests`, `cost_usd_cents` (RESERVED spend — the sum of each paid job’s dry-run cost estimate reserved at create; NOT metered realized provider cost), the effective `monthly_cap_usd_cents` (the key’s own cap or the account default), the optional `total_cap_usd_cents` (lifetime), the lifetime `total_cost_usd_cents`, and `remaining_usd_cents` under the monthly cap. A paid create that would push reserved spend over a cap is rejected with `402 budget_exceeded` BEFORE enqueue. This is per-key and DISTINCT from the global per-provider spend guard.", "security": [ { "apiKey": [] } ], "x-required-scopes": [ "usage:read" ], "responses": { "200": { "description": "The current-period usage + spend rollup for this key.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Usage" } } } }, "401": { "description": "Error envelope (§3.6). Possible `code`: `missing_api_key`, `invalid_api_key`, `expired_api_key`.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "402": { "description": "Error envelope (§3.6). Possible `code`: `quota_exceeded`.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "403": { "description": "Error envelope (§3.6). Possible `code`: `insufficient_scope`.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "405": { "description": "Error envelope (§3.6). Possible `code`: `method_not_allowed`.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "429": { "description": "Error envelope (§3.6). Possible `code`: `rate_limited`.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "500": { "description": "Error envelope (§3.6). Possible `code`: `internal_error`.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } } } } }, "/api/v1/public/showcase": { "get": { "tags": [ "Public" ], "operationId": "listShowcase", "summary": "Browse published showcase projects (no auth).", "description": "A NO-AUTH, rate-limited read-only feed exposing ONLY explicitly-published projects (the `project.published` opt-in flag) — a zero-friction on-ramp an agent can browse BEFORE it has a key. It is a dedicated public projection that surfaces nothing private by construction: never any private project, and no PII (no owner/org ids, address, floor, or orientation). Each item carries the title, usage type, and a short-lived signed preview image. Cursor pagination, newest first.", "security": [], "parameters": [ { "$ref": "#/components/parameters/Limit" }, { "$ref": "#/components/parameters/StartingAfter" }, { "$ref": "#/components/parameters/EndingBefore" } ], "responses": { "200": { "description": "A page of published showcase items.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ShowcaseList" } } } }, "400": { "description": "Error envelope (§3.6). Possible `code`: `invalid_request`.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "405": { "description": "Error envelope (§3.6). Possible `code`: `method_not_allowed`.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "429": { "description": "Error envelope (§3.6). Possible `code`: `rate_limited`.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "500": { "description": "Error envelope (§3.6). Possible `code`: `internal_error`.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } } } } } }, "components": { "securitySchemes": { "apiKey": { "type": "http", "scheme": "bearer", "bearerFormat": "norano_sk_live_… / norano_sk_test_…", "description": "A scoped, revocable API key. The mode (live/test) is encoded in the key prefix." } }, "parameters": { "Limit": { "name": "limit", "in": "query", "required": false, "description": "Page size (1..100).", "schema": { "type": "integer", "minimum": 1, "maximum": 100, "default": 20 } }, "StartingAfter": { "name": "starting_after", "in": "query", "required": false, "description": "Opaque cursor — return items after this one.", "schema": { "type": "string" } }, "EndingBefore": { "name": "ending_before", "in": "query", "required": false, "description": "Opaque cursor — return items before this one.", "schema": { "type": "string" } }, "PreferWait": { "name": "Prefer", "in": "header", "required": false, "description": "Long-poll up to 30s, e.g. `wait=30`, until the Job is terminal.", "schema": { "type": "string", "examples": [ "wait=30" ] } }, "ProjectId": { "name": "project_id", "in": "query", "required": false, "description": "Project id — a `proj_` (as returned by the projects endpoints) or a bare UUID. Mutually exclusive with `plan_id`; pass exactly one.", "schema": { "type": "string" } }, "PlanId": { "name": "plan_id", "in": "query", "required": false, "description": "Plan id — a `plan_` (as returned by the plans endpoints) or a bare UUID; resolved to its project (assets are project-scoped). Mutually exclusive with `project_id`; pass exactly one.", "schema": { "type": "string" } } }, "headers": { "Request-Id": { "description": "Correlation id echoed on every response.", "schema": { "type": "string" } }, "Retry-After": { "description": "Seconds to wait before retrying (rate limits / transient errors).", "schema": { "type": "integer" } }, "Allow": { "description": "Permitted methods, on a 405 response.", "schema": { "type": "string" } }, "RateLimit-Limit": { "description": "Per-key sliding-window request ceiling.", "schema": { "type": "integer" } }, "RateLimit-Remaining": { "description": "Requests left in the current window (0 on a 429).", "schema": { "type": "integer" } }, "RateLimit-Reset": { "description": "Seconds until the current rate-limit window resets.", "schema": { "type": "integer" } } }, "schemas": { "ApiRoot": { "type": "object", "required": [ "object", "version", "resources" ], "properties": { "object": { "type": "string", "const": "api_root" }, "version": { "type": "string", "const": "v1" }, "resources": { "type": "array", "items": { "type": "string" } } } }, "Usage": { "type": "object", "required": [ "object", "period", "requests", "cost_usd_cents", "monthly_cap_usd_cents", "total_cap_usd_cents", "total_cost_usd_cents", "remaining_usd_cents", "plan" ], "properties": { "object": { "type": "string", "const": "usage" }, "period": { "type": "string", "description": "The rollup period (`YYYY-MM`)." }, "requests": { "type": "integer", "description": "Requests this key made this period." }, "cost_usd_cents": { "type": "integer", "description": "RESERVED spend this period (sum of dry-run cost estimates), USD cents. NOT metered realized provider cost." }, "monthly_cap_usd_cents": { "type": "integer", "description": "EFFECTIVE monthly cap, USD cents — the tenant’s plan cap (`plan.monthly_cap_usd_cents`) narrowed by an explicit per-key cap if set (a key can be stricter than its plan, never looser)." }, "total_cap_usd_cents": { "type": [ "integer", "null" ], "description": "EFFECTIVE lifetime cap (plan ∧ key), else null (no lifetime cap)." }, "total_cost_usd_cents": { "type": "integer", "description": "Lifetime reserved spend, USD cents." }, "remaining_usd_cents": { "type": "integer", "description": "Cents left under the monthly cap (`max(0, cap - cost)`)." }, "plan": { "$ref": "#/components/schemas/PricingPlan" } } }, "PricingPlan": { "type": "object", "required": [ "id", "name", "price_cents", "monthly_cap_usd_cents", "total_cap_usd_cents", "rate_limit_requests", "rate_window_sec", "monthly_quota_requests" ], "properties": { "id": { "type": "string", "description": "The plan id/slug (e.g. `free`)." }, "name": { "type": "string", "description": "Human plan name (e.g. `Free`)." }, "price_cents": { "type": "integer", "description": "Headline price, USD cents (`0` = free)." }, "monthly_cap_usd_cents": { "type": "integer", "description": "The plan’s default/ceiling monthly spend cap, USD cents." }, "total_cap_usd_cents": { "type": [ "integer", "null" ], "description": "The plan’s lifetime spend cap, USD cents, or null (none)." }, "rate_limit_requests": { "type": "integer", "description": "The plan’s sliding-window request ceiling (the count)." }, "rate_window_sec": { "type": "integer", "description": "The plan’s sliding-window length, seconds." }, "monthly_quota_requests": { "type": [ "integer", "null" ], "description": "The plan’s monthly request quota, or null (unlimited)." } } }, "Project": { "type": "object", "required": [ "object", "id", "name", "usage_type", "address", "floor", "compass_orientation", "created_at" ], "properties": { "object": { "type": "string", "const": "project" }, "id": { "type": "string", "description": "The public id (`proj_`)." }, "name": { "type": "string" }, "usage_type": { "oneOf": [ { "$ref": "#/components/schemas/UsageType" }, { "type": "null" } ] }, "address": { "type": [ "string", "null" ] }, "floor": { "type": [ "string", "null" ] }, "compass_orientation": { "type": [ "string", "null" ] }, "created_at": { "type": "string", "format": "date-time" } } }, "ProjectList": { "type": "object", "required": [ "object", "data", "has_more", "next_cursor", "url" ], "properties": { "object": { "type": "string", "const": "list" }, "data": { "type": "array", "items": { "$ref": "#/components/schemas/Project" } }, "has_more": { "type": "boolean" }, "next_cursor": { "type": [ "string", "null" ] }, "url": { "type": "string" } } }, "ProjectCreate": { "type": "object", "required": [ "name" ], "additionalProperties": false, "properties": { "name": { "type": "string", "minLength": 1, "maxLength": 200 }, "usage_type": { "$ref": "#/components/schemas/UsageType" }, "address": { "type": "string", "maxLength": 300 }, "floor": { "type": "string", "maxLength": 50 }, "compass_orientation": { "type": "string", "maxLength": 50 } } }, "PlanFileKind": { "type": "string", "enum": [ "dwg", "dxf", "pdf" ] }, "PlanKind": { "type": "string", "enum": [ "grundriss", "vermarktung" ] }, "PlanBodyMode": { "type": "string", "enum": [ "single", "both" ] }, "Plan": { "type": "object", "required": [ "object", "id", "project_id", "kind", "status", "job_id", "created_at", "updated_at" ], "properties": { "object": { "type": "string", "const": "plan" }, "id": { "type": "string", "description": "The public id (`plan_`)." }, "project_id": { "type": "string", "description": "The owning project (`proj_`)." }, "kind": { "$ref": "#/components/schemas/PlanKind" }, "status": { "type": "string", "enum": [ "queued", "running", "succeeded", "failed", "canceled" ] }, "job_id": { "type": "string", "description": "The parse Job (`job_plan_`); poll it." }, "created_at": { "type": "string", "format": "date-time" }, "updated_at": { "type": "string", "format": "date-time" } } }, "PlanCreate": { "oneOf": [ { "type": "object", "required": [ "filename", "file_kind", "size" ], "additionalProperties": false, "properties": { "filename": { "type": "string", "minLength": 1, "maxLength": 512 }, "file_kind": { "$ref": "#/components/schemas/PlanFileKind" }, "mode": { "type": "string", "const": "single", "default": "single" }, "kind": { "$ref": "#/components/schemas/PlanKind" }, "size": { "type": "integer", "minimum": 1, "description": "Exact byte length you will PUT (bound into the signature)." } } }, { "type": "object", "required": [ "filename", "file_kind", "mode", "size" ], "additionalProperties": false, "properties": { "filename": { "type": "string", "minLength": 1, "maxLength": 512 }, "file_kind": { "$ref": "#/components/schemas/PlanFileKind" }, "mode": { "type": "string", "const": "both" }, "size": { "type": "integer", "minimum": 1, "description": "Exact byte length you will PUT (bound into the signature)." } } } ] }, "PlanUploadItem": { "type": "object", "required": [ "object", "id", "kind", "job_id", "status" ], "properties": { "object": { "type": "string", "const": "plan" }, "id": { "type": "string" }, "kind": { "$ref": "#/components/schemas/PlanKind" }, "job_id": { "type": "string" }, "status": { "type": "string", "enum": [ "queued", "running", "succeeded", "failed", "canceled" ] } } }, "PlanUpload": { "type": "object", "required": [ "object", "upload_url", "upload_headers", "expires_at", "plans" ], "properties": { "object": { "type": "string", "const": "plan_upload" }, "upload_url": { "type": "string", "format": "uri" }, "upload_headers": { "type": "object", "additionalProperties": { "type": "string" }, "description": "Headers to send on the PUT (the signed `Content-Type`)." }, "expires_at": { "type": "string", "format": "date-time" }, "plans": { "type": "array", "items": { "$ref": "#/components/schemas/PlanUploadItem" } } } }, "VisualizationCreate": { "type": "object", "required": [ "project_id" ], "additionalProperties": false, "properties": { "project_id": { "type": "string", "description": "The `proj_` to visualize." }, "usage_type": { "type": "string", "enum": [ "wohnen", "buero", "retail", "gastro", "gewerbe", "lager", "bildung", "gesundheit", "parkieren" ], "description": "Defaults to the project’s usage type when omitted." }, "style": { "type": "string", "enum": [ "realistisch", "reduziert", "atmosphaerisch" ], "default": "realistisch" }, "lighting": { "type": "string", "enum": [ "tag", "daemmerung", "nacht", "bewoelkt" ], "default": "tag" }, "furnishing": { "type": "string", "enum": [ "moebliert", "leer" ], "default": "moebliert" }, "seed": { "type": "integer", "minimum": 0, "maximum": 2147483647 }, "dry_run": { "type": "boolean", "default": false, "description": "When true, return the cost pre-flight only — no job, no spend." } } }, "VisualizationCostEstimate": { "type": "object", "required": [ "object", "estimated_cost_usd_cents", "would_create" ], "properties": { "object": { "type": "string", "const": "cost_estimate" }, "estimated_cost_usd_cents": { "type": "integer", "minimum": 0 }, "would_create": { "type": "object", "required": [ "type", "usage_type", "style", "lighting", "furnishing" ], "properties": { "type": { "type": "string", "const": "visualization" }, "usage_type": { "type": "string", "enum": [ "wohnen", "buero", "retail", "gastro", "gewerbe", "lager", "bildung", "gesundheit", "parkieren" ] }, "style": { "type": "string", "enum": [ "realistisch", "reduziert", "atmosphaerisch" ] }, "lighting": { "type": "string", "enum": [ "tag", "daemmerung", "nacht", "bewoelkt" ] }, "furnishing": { "type": "string", "enum": [ "moebliert", "leer" ] } } } } }, "TourCreate": { "type": "object", "required": [ "project_id" ], "additionalProperties": false, "properties": { "project_id": { "type": "string", "description": "The `proj_` to build a tour for." }, "model": { "type": "string", "enum": [ "marble-1.1-plus", "marble-1.1" ], "default": "marble-1.1", "description": "The Marble model tier. `marble-1.1-mini` is NOT accepted (SAM-381)." }, "display_name": { "type": "string", "minLength": 1, "maxLength": 64, "default": "Rundgang", "description": "User-facing world name (1..64 chars)." }, "seed": { "type": "integer", "minimum": 0, "maximum": 4294967295, "description": "Reproducibility seed (uint32)." }, "dry_run": { "type": "boolean", "default": false, "description": "When true, return the cost pre-flight only — no job, no spend." } } }, "TourCostEstimate": { "type": "object", "required": [ "object", "estimated_cost_usd_cents", "would_create" ], "properties": { "object": { "type": "string", "const": "cost_estimate" }, "estimated_cost_usd_cents": { "type": "integer", "minimum": 0 }, "would_create": { "type": "object", "required": [ "type", "model" ], "properties": { "type": { "type": "string", "const": "tour" }, "model": { "type": "string", "enum": [ "marble-1.1-plus", "marble-1.1" ] } } } } }, "Error": { "type": "object", "required": [ "error" ], "properties": { "error": { "type": "object", "required": [ "type", "code", "message", "remediation", "retryable", "docs_url", "request_id" ], "properties": { "type": { "type": "string", "enum": [ "invalid_request", "authentication", "permission", "not_found", "conflict", "rate_limit", "budget", "provider", "job", "server" ] }, "code": { "type": "string", "enum": [ "invalid_request", "missing_param", "unsupported_media_type", "payload_too_large", "method_not_allowed", "missing_api_key", "invalid_api_key", "expired_api_key", "insufficient_scope", "tenant_mismatch", "test_mode_resource_in_live", "resource_not_found", "idempotency_in_progress", "plan_not_ready", "source_not_ready", "validation_error", "idempotency_key_reuse", "rate_limited", "budget_exceeded", "quota_exceeded", "provider_unavailable", "job_failed", "internal_error", "service_unavailable" ] }, "message": { "type": "string", "description": "Human/LLM prose — NOT stable; do not branch on it." }, "remediation": { "type": "string" }, "retryable": { "type": "boolean" }, "param": { "type": "string", "description": "The offending field (validation errors)." }, "docs_url": { "type": "string", "format": "uri" }, "request_id": { "type": "string" } } } } }, "JobAsset": { "type": "object", "required": [ "id", "kind", "url", "expires_at" ], "properties": { "id": { "type": "string" }, "kind": { "type": "string" }, "url": { "type": "string", "format": "uri" }, "expires_at": { "type": "string", "format": "date-time" } } }, "Job": { "type": "object", "required": [ "object", "id", "type", "status", "progress", "result", "cost_usd_cents", "provider", "seed", "error", "metadata", "created_at", "updated_at", "urls" ], "properties": { "object": { "type": "string", "const": "job" }, "id": { "type": "string" }, "type": { "type": "string", "enum": [ "plan.parse", "plan.render", "visualization", "tour", "panorama", "object_mesh" ] }, "status": { "type": "string", "enum": [ "queued", "running", "succeeded", "failed", "canceled" ] }, "progress": { "type": [ "number", "null" ], "minimum": 0, "maximum": 1 }, "result": { "type": [ "object", "null" ], "required": [ "assets" ], "properties": { "assets": { "type": "array", "items": { "$ref": "#/components/schemas/JobAsset" } } } }, "cost_usd_cents": { "type": [ "integer", "null" ] }, "provider": { "type": [ "string", "null" ] }, "seed": { "type": [ "string", "null" ] }, "error": { "type": [ "object", "null" ], "required": [ "code", "message" ], "properties": { "code": { "type": "string" }, "message": { "type": "string" } } }, "metadata": { "type": [ "object", "null" ], "additionalProperties": true }, "created_at": { "type": "string", "format": "date-time" }, "updated_at": { "type": "string", "format": "date-time" }, "urls": { "type": "object", "required": [ "self", "cancel" ], "properties": { "self": { "type": "string" }, "cancel": { "type": [ "string", "null" ] } } } } }, "JobList": { "type": "object", "required": [ "object", "data", "has_more", "next_cursor", "url" ], "properties": { "object": { "type": "string", "const": "list" }, "data": { "type": "array", "items": { "$ref": "#/components/schemas/Job" } }, "has_more": { "type": "boolean" }, "next_cursor": { "type": [ "string", "null" ] }, "url": { "type": "string" } } }, "Asset": { "type": "object", "required": [ "object", "id", "type", "kind", "mime", "job_id", "created_at", "urls" ], "properties": { "object": { "type": "string", "const": "asset" }, "id": { "type": "string" }, "type": { "type": "string", "enum": [ "input_asset", "output_asset" ] }, "kind": { "type": "string" }, "mime": { "type": "string" }, "job_id": { "type": [ "string", "null" ] }, "created_at": { "type": "string", "format": "date-time" }, "urls": { "type": "object", "required": [ "url" ], "properties": { "url": { "type": "string" } } } } }, "AssetList": { "type": "object", "required": [ "object", "data", "has_more", "next_cursor", "url" ], "properties": { "object": { "type": "string", "const": "list" }, "data": { "type": "array", "items": { "$ref": "#/components/schemas/Asset" } }, "has_more": { "type": "boolean" }, "next_cursor": { "type": [ "string", "null" ] }, "url": { "type": "string" } } }, "AssetUrl": { "type": "object", "required": [ "object", "id", "url", "expires_at" ], "properties": { "object": { "type": "string", "const": "asset_url" }, "id": { "type": "string" }, "url": { "type": "string", "format": "uri" }, "expires_at": { "type": "string", "format": "date-time" } } }, "WebhookEventType": { "type": "string", "enum": [ "job.queued", "job.running", "job.succeeded", "job.failed", "job.canceled" ], "description": "A job lifecycle event type." }, "WebhookEndpoint": { "type": "object", "required": [ "object", "id", "url", "enabled_events", "enabled", "created_at" ], "properties": { "object": { "type": "string", "const": "webhook_endpoint" }, "id": { "type": "string", "description": "The public endpoint id (`we_…`)." }, "url": { "type": "string", "format": "uri" }, "enabled_events": { "type": "array", "items": { "$ref": "#/components/schemas/WebhookEventType" } }, "enabled": { "type": "boolean", "description": "Auto-disabled after sustained delivery failure." }, "created_at": { "type": "string", "format": "date-time" } } }, "WebhookEndpointWithSecret": { "allOf": [ { "$ref": "#/components/schemas/WebhookEndpoint" }, { "type": "object", "required": [ "secret" ], "properties": { "secret": { "type": "string", "description": "The signing secret (`whsec_…`), shown EXACTLY ONCE at registration and irrecoverable afterward. Verify each delivery: read `Norano-Signature: t=,v1=`, reject if `|now-t|>300`, then constant-time compare `v1` to `hex(hmac_sha256(secret, \".\" + raw_request_body))`." } } } ] }, "WebhookEndpointCreate": { "type": "object", "required": [ "url", "enabled_events" ], "properties": { "url": { "type": "string", "format": "uri", "description": "An https URL whose host resolves to a public address (SSRF-guarded)." }, "enabled_events": { "type": "array", "minItems": 1, "items": { "$ref": "#/components/schemas/WebhookEventType" } } } }, "WebhookEndpointList": { "type": "object", "required": [ "object", "data", "has_more", "next_cursor", "url" ], "properties": { "object": { "type": "string", "const": "list" }, "data": { "type": "array", "items": { "$ref": "#/components/schemas/WebhookEndpoint" } }, "has_more": { "type": "boolean" }, "next_cursor": { "type": [ "string", "null" ] }, "url": { "type": "string" } } }, "WebhookDeleted": { "type": "object", "required": [ "object", "id", "deleted" ], "properties": { "object": { "type": "string", "const": "webhook_endpoint" }, "id": { "type": "string" }, "deleted": { "type": "boolean", "const": true } } }, "WebhookDelivery": { "type": "object", "required": [ "object", "id", "endpoint_id", "event_id", "event_type", "job_id", "status", "attempts", "response_code", "created_at", "updated_at" ], "properties": { "object": { "type": "string", "const": "webhook_delivery" }, "id": { "type": "string", "description": "The public delivery id (`wd_…`)." }, "endpoint_id": { "type": "string" }, "event_id": { "type": "string", "description": "Stable event id (also sent as `Norano-Webhook-Id`) — dedupe on it." }, "event_type": { "$ref": "#/components/schemas/WebhookEventType" }, "job_id": { "type": "string" }, "status": { "type": "string", "enum": [ "pending", "delivered", "failed" ] }, "attempts": { "type": "integer" }, "response_code": { "type": [ "integer", "null" ] }, "created_at": { "type": "string", "format": "date-time" }, "updated_at": { "type": "string", "format": "date-time" } } }, "WebhookDeliveryList": { "type": "object", "required": [ "object", "data", "has_more", "next_cursor", "url" ], "properties": { "object": { "type": "string", "const": "list" }, "data": { "type": "array", "items": { "$ref": "#/components/schemas/WebhookDelivery" } }, "has_more": { "type": "boolean" }, "next_cursor": { "type": [ "string", "null" ] }, "url": { "type": "string" } } }, "Showcase": { "type": "object", "required": [ "object", "id", "title", "usage_type", "created_at", "preview_image" ], "properties": { "object": { "type": "string", "const": "showcase" }, "id": { "type": "string" }, "title": { "type": "string" }, "usage_type": { "oneOf": [ { "$ref": "#/components/schemas/UsageType" }, { "type": "null" } ] }, "created_at": { "type": "string", "format": "date-time" }, "preview_image": { "type": [ "object", "null" ], "required": [ "url", "expires_at" ], "properties": { "url": { "type": "string", "format": "uri" }, "expires_at": { "type": "string", "format": "date-time" } } } } }, "ShowcaseList": { "type": "object", "required": [ "object", "data", "has_more", "next_cursor", "url" ], "properties": { "object": { "type": "string", "const": "list" }, "data": { "type": "array", "items": { "$ref": "#/components/schemas/Showcase" } }, "has_more": { "type": "boolean" }, "next_cursor": { "type": [ "string", "null" ] }, "url": { "type": "string" } } }, "VizStyle": { "type": "string", "enum": [ "realistisch", "reduziert", "atmosphaerisch" ] }, "VizLighting": { "type": "string", "enum": [ "tag", "daemmerung", "nacht", "bewoelkt" ] }, "VizFurnishing": { "type": "string", "enum": [ "moebliert", "leer" ] }, "UsageType": { "type": "string", "enum": [ "wohnen", "buero", "retail", "gastro", "gewerbe", "lager", "bildung", "gesundheit", "parkieren" ] } } } } ```