{"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: <unique>` 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=<seconds>` 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_<uuid>`).","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_<uuid>`). 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_<uuid>`).","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_<uuid>`).","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_<uuid>`).","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_<uuid>`) 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_<uuid>`). 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_<uuid>`).","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_<uuid>`). 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_<uuid>`).","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_<uuid>`) 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_<uuid>`). 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_<uuid>`).","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_<uuid>`). 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_<uuid>`).","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_<uuid>` output or `asset_in_<uuid>` 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_<uuid>` (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_<uuid>` (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_<uuid>`)."},"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_<uuid>`)."},"project_id":{"type":"string","description":"The owning project (`proj_<uuid>`)."},"kind":{"$ref":"#/components/schemas/PlanKind"},"status":{"type":"string","enum":["queued","running","succeeded","failed","canceled"]},"job_id":{"type":"string","description":"The parse Job (`job_plan_<uuid>`); 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_<uuid>` 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_<uuid>` 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=<unix>,v1=<hex>`, reject if `|now-t|>300`, then constant-time compare `v1` to `hex(hmac_sha256(secret, \"<t>.\" + 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"]}}}}