{"openapi":"3.1.0","info":{"title":"Glossi REST API","version":"1.0.0","description":"Programmatic access to the Glossi 3D rendering platform: upload models, create projects, kick off renders, run material-swap colorway batches, manage automated pipelines, and receive webhooks. All endpoints are authenticated with a workspace API key sent in the X-API-Key header and are rate limited per key.\n\nThis document covers only the public API-key surface. The connector OAuth routes (/api/v1/connectors, Auth0 user Bearer auth) and the internal server-to-server callbacks (/api/v1/internal/*) are intentionally excluded — they are not API-key endpoints.\n\nCreation requests accept an optional `Idempotency-Key` header so retries can be replayed safely (see the header parameter and the Idempotency section of the docs)."},"servers":[{"url":"https://api.glossi.io/api/v1"}],"security":[{"ApiKeyAuth":[]}],"tags":[{"name":"Me","description":"Identify the authenticated API key and its workspace."},{"name":"Models","description":"Upload 3D models and poll their processing status."},{"name":"Projects","description":"Create scenes from ready models."},{"name":"Renders","description":"Kick off and track bulk render jobs."},{"name":"Templates","description":"List and fetch reusable scene templates."},{"name":"Webhooks","description":"Configure workspace webhook delivery."},{"name":"Jobs","description":"Unified upload → project → render workflow."},{"name":"Pipelines","description":"Automated file-to-render pipelines (Enterprise)."},{"name":"Colorways","description":"Batch material-swap operations across models."},{"name":"Props","description":"Upload scene-dressing props into the workspace library."},{"name":"Decals","description":"Upload decal images into the workspace library."},{"name":"Materials","description":"Import PBR materials from texture files and search the library."}],"components":{"securitySchemes":{"ApiKeyAuth":{"type":"apiKey","in":"header","name":"X-API-Key","description":"Workspace API key, format `glsi_{prefix}_{secret}`. Sent on every request."}},"schemas":{"Error":{"type":"object","required":["error"],"properties":{"error":{"type":"object","required":["code","message"],"properties":{"code":{"type":"string","description":"Stable machine-readable error code.","example":"VALIDATION_ERROR"},"message":{"type":"string","description":"Human-readable explanation."}}}}},"ModelStatus":{"type":"string","description":"Model processing lifecycle status.","enum":["PENDING","UPLOADING","ANALYZING","READY","APPROVED","FAILED","MODEL_REJECTED","MODEL_REQUIRED"]},"RenderJobStatus":{"type":"string","description":"Render job lifecycle status. CANCELLED is a terminal, user-cancelled state (only reachable from QUEUED/RENDER_QUEUED, before the job is dispatched).","enum":["QUEUED","INGESTING","PROCESSING","RENDER_QUEUED","RENDERING","STYLING","EXPORTING","COMPLETED","FAILED","CANCELLED"]},"JobStatus":{"type":"string","description":"Unified job workflow status.","enum":["AWAITING_UPLOADS","PROCESSING","CREATING_PROJECTS","RENDERING","COMPLETE","FAILED"]},"PipelineStatus":{"type":"string","enum":["ACTIVE","PAUSED","ERROR"]},"WebhookEvent":{"type":"string","description":"Events a workspace webhook can subscribe to.","enum":["render.complete","render.failed","model.processed","model.failed","project.created","job.complete","job.failed"]},"RenderSettings":{"type":"object","description":"Optional render settings. Omitted fields fall back to server defaults (renderBookmarks/renderShots/renderVariants default to true; imageQuality 1 (1080p); imageFormat PNG; videoQuality 1 (Best); videoResolution 1 (1080p)).","additionalProperties":true,"properties":{"renderBookmarks":{"type":"boolean","description":"Render all camera bookmarks (still images)."},"renderShots":{"type":"boolean","description":"Render all video shots."},"renderVariants":{"type":"boolean","description":"Render all model variants."},"imageQuality":{"type":"integer","description":"0 = 720p, 1 = 1080p, 2 = 4K.","enum":[0,1,2]},"imageFormat":{"type":"string","description":"PNG (8-bit), TIFF (16-bit) or JPEG (8-bit, sRGB IEC61966-2.1).","enum":["PNG","TIFF","JPEG"]},"postProcess":{"type":"boolean","description":"Apply a post processing finishing pass to each image after it renders. Uses more usage per image. Omitted = the workspace default. PNG and JPEG only; TIFF and transparent renders are delivered as rendered."},"videoQuality":{"type":"integer","description":"0 = Fast (preview quality), 1 = Best (production quality).","enum":[0,1]},"videoResolution":{"type":"integer","description":"0 = 720p, 1 = 1080p, 2 = 4K.","enum":[0,1,2]},"imageCustomResolution":{"type":"boolean"},"imageCustomWidth":{"type":"integer"},"imageCustomHeight":{"type":"integer"},"videoCustomResolution":{"type":"boolean"},"videoCustomWidth":{"type":"integer"},"videoCustomHeight":{"type":"integer"},"snapToPadding":{"type":"boolean"},"snapPaddingPx":{"type":"integer"},"centerFraming":{"type":"boolean"},"zeroProductOffset":{"type":"boolean"},"bookmarksToRender":{"type":"array","items":{"type":"string"},"description":"Optional subset of bookmark ids to render. Empty/absent = render all."},"shotsToRender":{"type":"array","items":{"type":"string"},"description":"Optional subset of shot ids to render. Empty/absent = render all."},"variantsToRender":{"type":"array","items":{"type":"string"},"description":"Optional subset of variant ids to render. Empty/absent = render all."},"videoRenderMode":{"type":"string","description":"Video delivery mode: \"full-sequence\" or \"single-clips\"."}}},"MeResponse":{"type":"object","properties":{"workspace":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"}}},"apiKey":{"type":"object","properties":{"name":{"type":["string","null"]},"expiresAt":{"type":["string","null"],"format":"date-time"}}}}},"CreateModelsRequest":{"type":"object","required":["models"],"properties":{"models":{"type":"array","minItems":1,"maxItems":50,"description":"1–50 models to create.","items":{"type":"object","required":["fileType"],"properties":{"fileType":{"type":"string","description":"Bare lowercase file extension (no leading dot), 1–8 chars matching ^[a-z0-9]{1,8}$ — e.g. `glb`, `fbx`, `usdz`. Not a fixed allowlist; the converter supports 50+ formats.","example":"glb"},"name":{"type":"string","description":"Optional display name."},"fileName":{"type":"string","description":"Optional original file name; used to derive a name if `name` is omitted."},"category":{"type":"string"},"fileSize":{"type":"integer","description":"File size in bytes."}}}}}},"CreateModelsResponse":{"type":"object","properties":{"models":{"type":"array","items":{"type":"object","properties":{"modelId":{"type":"string"},"name":{"type":"string"},"uploadUrl":{"type":"string","description":"Signed S3 PUT URL (expires ~1 hour). Do NOT add the API key header to the PUT."},"uploadKey":{"type":"string","description":"S3 object key the file will live at."}}}},"errors":{"type":"array","items":{"type":"string"},"description":"Present only on partial success — per-model creation failures."}}},"ConfirmModelRequest":{"type":"object","required":["fileType"],"properties":{"fileType":{"type":"string","description":"Bare extension the file was uploaded as (^[a-z0-9]{1,8}$).","example":"glb"}}},"ConfirmModelResponse":{"type":"object","properties":{"modelId":{"type":"string"},"name":{"type":"string"},"status":{"$ref":"#/components/schemas/ModelStatus"},"filePath":{"type":"string"}}},"ModelStatusRequest":{"type":"object","required":["modelIds"],"properties":{"modelIds":{"type":"array","items":{"type":"string"},"minItems":1,"maxItems":100,"description":"1–100 model ids."}}},"ModelStatusResponse":{"type":"object","properties":{"models":{"type":"array","items":{"type":"object","properties":{"modelId":{"type":"string"},"name":{"type":"string"},"status":{"$ref":"#/components/schemas/ModelStatus"},"ready":{"type":"boolean","description":"True when status is READY or APPROVED."},"error":{"type":"string","description":"Present on FAILED models imported from a URL: why the import failed."}}}},"allReady":{"type":"boolean","description":"True when all requested models are ready and none are missing."},"notFound":{"type":"array","items":{"type":"string"},"description":"Requested ids that were not found in this workspace."}}},"Model":{"type":"object","properties":{"modelId":{"type":"string"},"name":{"type":"string"},"status":{"$ref":"#/components/schemas/ModelStatus"},"category":{"type":"string"},"fileType":{"type":"string"},"glbFilePath":{"type":"string"},"usdFilePath":{"type":"string"}}},"CreateProjectRequest":{"type":"object","required":["modelIds"],"properties":{"modelIds":{"type":"array","items":{"type":"string"},"minItems":1,"maxItems":50,"description":"1–50 model ids. Models must be READY or APPROVED."},"name":{"type":"string","description":"Project name (single-project mode only)."},"templateId":{"type":"string","description":"Optional template to apply."},"createPerModel":{"type":"boolean","description":"When true, creates one project per model (batch mode) instead of a single project containing all models. A BatchProject grouping may be returned."}}},"Project":{"type":"object","description":"Project summary. Note: no `status` field is returned by this API.","properties":{"projectId":{"type":"string"},"name":{"type":"string"},"modelIds":{"type":"array","items":{"type":"string"}},"templateId":{"type":"string"}}},"CreateProjectResponse":{"type":"object","properties":{"projects":{"type":"array","items":{"$ref":"#/components/schemas/Project"}},"batchProject":{"type":"object","description":"Present only in createPerModel mode when a batch grouping was created.","properties":{"id":{"type":"string"},"title":{"type":"string"}}},"errors":{"type":"array","items":{"type":"string"},"description":"Present only on partial success (createPerModel mode)."}}},"CreateRenderRequest":{"type":"object","required":["projectIds"],"properties":{"projectIds":{"type":"array","items":{"type":"string"},"minItems":1,"maxItems":50,"description":"1–50 project ids. Duplicates are removed before the 50 cap is checked."},"settings":{"$ref":"#/components/schemas/RenderSettings"}}},"RenderJobResult":{"type":"object","properties":{"jobId":{"type":"string"},"status":{"$ref":"#/components/schemas/RenderJobStatus"},"projectId":{"type":"string"}}},"CreateRenderResponse":{"type":"object","properties":{"batchId":{"type":"string"},"jobs":{"type":"array","items":{"$ref":"#/components/schemas/RenderJobResult"}},"totalProjects":{"type":"integer"}}},"RenderJobStatusResponse":{"type":"object","properties":{"jobId":{"type":"string"},"status":{"$ref":"#/components/schemas/RenderJobStatus"},"projectId":{"type":"string"},"errorMessage":{"type":"string"},"renders":{"type":"array","description":"Populated once the job is COMPLETED.","items":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"filePath":{"type":"string","description":"Playable CDN URL for the render."},"fileType":{"type":"string"},"isVideo":{"type":"boolean"}}}}}},"RetryRenderResponse":{"type":"object","properties":{"jobId":{"type":"string"},"status":{"$ref":"#/components/schemas/RenderJobStatus"},"projectId":{"type":"string"},"batchId":{"type":"string"}}},"RenderAsset":{"type":"object","description":"A finished render output (image or video).","properties":{"id":{"type":"string"},"name":{"type":"string"},"projectId":{"type":"string"},"fileType":{"type":"string"},"isVideo":{"type":"boolean"},"aspectRatio":{"type":"string"},"resolution":{"type":"string"},"duration":{"type":"number","description":"Video length in seconds (videos only)."},"fileSize":{"type":"integer","description":"Bytes."},"favorited":{"type":"boolean"},"isPreview":{"type":"boolean","description":"Watermarked preview; does not count toward usage."},"createdAt":{"type":"string","format":"date-time"},"url":{"type":"string","description":"Resolvable URL for the primary asset (video → mp4, else image)."},"thumbnailUrl":{"type":"string","description":"Image, or the video's thumbnail."}}},"RenderList":{"type":"object","properties":{"renders":{"type":"array","items":{"$ref":"#/components/schemas/RenderAsset"}},"total":{"type":"integer","description":"Total renders matching the filter (for pagination)."},"limit":{"type":"integer"},"offset":{"type":"integer"}}},"Template":{"type":"object","properties":{"templateId":{"type":"string"},"name":{"type":"string"},"description":{"type":"string"}}},"TemplatesListResponse":{"type":"object","properties":{"templates":{"type":"array","items":{"$ref":"#/components/schemas/Template"}}}},"WebhookConfigResponse":{"type":"object","description":"Either `{ configured: false }` when no webhook is set, or the current config.","properties":{"configured":{"type":"boolean"},"id":{"type":"string"},"url":{"type":"string"},"events":{"type":"array","items":{"$ref":"#/components/schemas/WebhookEvent"}},"enabled":{"type":"boolean"},"description":{"type":"string"},"lastTriggeredAt":{"type":"string","format":"date-time"},"failureCount":{"type":"integer"}}},"UpsertWebhookRequest":{"type":"object","required":["url"],"properties":{"url":{"type":"string","format":"uri","description":"Required. Must be a public http(s) URL — private/internal/metadata hosts are rejected (SSRF protection)."},"events":{"type":"array","items":{"$ref":"#/components/schemas/WebhookEvent"},"description":"Optional subscribed events. Invalid values are rejected."},"enabled":{"type":"boolean"},"description":{"type":"string"}}},"UpsertWebhookResponse":{"type":"object","properties":{"id":{"type":"string"},"url":{"type":"string"},"secret":{"type":"string","description":"HMAC signing secret."},"events":{"type":"array","items":{"$ref":"#/components/schemas/WebhookEvent"}},"enabled":{"type":"boolean"},"description":{"type":"string"}}},"WebhookSecretResponse":{"type":"object","properties":{"secret":{"type":"string"}}},"WebhookEventsResponse":{"type":"object","properties":{"events":{"type":"array","items":{"type":"object","properties":{"name":{"$ref":"#/components/schemas/WebhookEvent"},"description":{"type":"string"}}}}}},"SuccessResponse":{"type":"object","properties":{"success":{"type":"boolean"}}},"CreateJobRequest":{"type":"object","required":["models"],"properties":{"models":{"type":"array","minItems":1,"maxItems":50,"items":{"type":"object","required":["fileName","fileType"],"properties":{"fileName":{"type":"string","description":"Required original file name."},"fileType":{"type":"string","description":"Required bare extension (^[a-z0-9]{1,8}$).","example":"glb"},"name":{"type":"string"}}}},"createProjects":{"type":"boolean","description":"Create projects once models are ready. Defaults to true."},"templateId":{"type":"string","description":"Required when renderSettings.renderBookmarks or renderSettings.renderShots is true."},"renderSettings":{"$ref":"#/components/schemas/RenderSettings"},"webhookUrl":{"type":"string","format":"uri","description":"Optional per-job webhook. Validated as a public http(s) URL."}}},"CreateJobResponse":{"type":"object","properties":{"jobId":{"type":"string"},"status":{"$ref":"#/components/schemas/JobStatus"},"models":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"fileName":{"type":"string"},"uploadUrl":{"type":"string"},"uploadKey":{"type":"string"}}}},"errors":{"type":"array","items":{"type":"string"},"description":"Present only on partial model-creation failure."}}},"ConfirmUploadsResponse":{"type":"object","properties":{"jobId":{"type":"string"},"status":{"$ref":"#/components/schemas/JobStatus"},"modelsConfirmed":{"type":"integer"}}},"JobStatusResponse":{"type":"object","properties":{"jobId":{"type":"string"},"status":{"$ref":"#/components/schemas/JobStatus"},"progress":{"type":"object","properties":{"modelsUploaded":{"type":"integer"},"modelsReady":{"type":"integer"},"modelsTotal":{"type":"integer"},"projectsCreated":{"type":"integer"},"projectsTotal":{"type":"integer"},"rendersComplete":{"type":"integer"},"rendersTotal":{"type":"integer"}}},"batchProject":{"type":"object","properties":{"id":{"type":"string"},"title":{"type":"string"}}},"results":{"type":"array","description":"Present when status is COMPLETE.","items":{"type":"object","properties":{"model":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"}}},"project":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"}}},"renders":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"imageUrl":{"type":"string"},"fileType":{"type":"string"}}}}}}},"error":{"type":"string"}}},"PipelineNode":{"type":"object","required":["type","enabled"],"properties":{"id":{"type":"string","description":"Node id (update/read only)."},"type":{"type":"string","description":"PipelineNodeType value: connector_poll, model_ready, project_created, render_complete, render, template_match, variant, export.","enum":["connector_poll","model_ready","project_created","render_complete","render","template_match","variant","export"]},"enabled":{"type":"boolean"},"config":{"type":"object","additionalProperties":true},"parentIds":{"type":"array","items":{"type":"string"},"description":"DAG parents (update only)."}}},"CreatePipelineRequest":{"type":"object","required":["name","nodes"],"properties":{"name":{"type":"string"},"nodes":{"type":"array","minItems":1,"items":{"$ref":"#/components/schemas/PipelineNode"}},"batchSize":{"type":"integer"},"maxRetries":{"type":"integer"}}},"UpdatePipelineRequest":{"type":"object","properties":{"name":{"type":"string"},"status":{"$ref":"#/components/schemas/PipelineStatus"},"nodes":{"type":"array","items":{"$ref":"#/components/schemas/PipelineNode"}},"batchSize":{"type":"integer","minimum":1,"maximum":100,"description":"Integer 1–100."},"maxRetries":{"type":"integer","minimum":0,"maximum":10,"description":"Integer 0–10."}}},"CreatePipelineResponse":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"status":{"$ref":"#/components/schemas/PipelineStatus"},"nodes":{"type":"array","items":{"type":"object","properties":{"type":{"type":"string"},"enabled":{"type":"boolean"}}}},"batchSize":{"type":"integer"},"maxRetries":{"type":"integer"}}},"PipelineListResponse":{"type":"object","properties":{"pipelines":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"status":{"$ref":"#/components/schemas/PipelineStatus"},"nodes":{"type":"array","items":{"type":"object","properties":{"type":{"type":"string"},"enabled":{"type":"boolean"}}}},"consecutiveFailures":{"type":"integer"},"createdAt":{"type":"string","format":"date-time"}}}}}},"PipelineDetailResponse":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"status":{"$ref":"#/components/schemas/PipelineStatus"},"nodes":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"type":{"type":"string"},"enabled":{"type":"boolean"}}}},"batchSize":{"type":"integer"},"maxRetries":{"type":"integer"},"consecutiveFailures":{"type":"integer"},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"},"jobStats":{"type":"object","additionalProperties":{"type":"integer"},"description":"Count of pipeline render jobs keyed by RenderJobStatus."}}},"UpdatePipelineResponse":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"status":{"$ref":"#/components/schemas/PipelineStatus"},"batchSize":{"type":"integer"},"maxRetries":{"type":"integer"},"consecutiveFailures":{"type":"integer"}}},"PipelineConnectorsResponse":{"type":"object","properties":{"connectors":{"type":"array","items":{"type":"string"}}}},"PipelineJobsResponse":{"type":"object","properties":{"jobs":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"status":{"$ref":"#/components/schemas/RenderJobStatus"},"sourceFile":{"type":["object","null"],"properties":{"name":{"type":"string"},"path":{"type":"string"},"size":{"type":"integer"}}},"glossiModelId":{"type":"string"},"glossiProjectId":{"type":"string"},"renderOutputs":{"type":"integer","description":"Count of render outputs."},"retryCount":{"type":"integer"},"lastError":{"type":"string"},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"}}}},"total":{"type":"integer"},"limit":{"type":"integer"},"offset":{"type":"integer"}}},"CreateColorwayBatchRequest":{"type":"object","required":["swapRules","modelScope"],"properties":{"swapRules":{"type":"array","minItems":1,"items":{"type":"object","required":["matchText","matchMode","materialId"],"properties":{"matchText":{"type":"string","description":"Non-empty text to match slot names against."},"matchMode":{"type":"string","enum":["contains","exact"]},"materialId":{"type":"string","description":"Replacement material id."}}}},"modelScope":{"type":"object","required":["type"],"properties":{"type":{"type":"string","enum":["all","tag","manual"]},"tagIds":{"type":"array","items":{"type":"string"},"description":"Required when type is `tag`."},"modelIds":{"type":"array","items":{"type":"string"},"description":"Required when type is `manual`."}}}}},"CreateColorwayBatchResponse":{"type":"object","properties":{"batchId":{"type":"string"},"totalJobs":{"type":"integer"},"warnings":{"type":"array","items":{"type":"object","properties":{"modelId":{"type":"string"},"modelName":{"type":"string"},"message":{"type":"string"}}}}}},"ColorwayBatchStatusResponse":{"type":"object","description":"The full colorway batch document plus its jobs.","additionalProperties":true,"properties":{"id":{"type":"string"},"status":{"type":"string","enum":["in_progress","complete","partial_failure","failed"]},"totalJobs":{"type":"integer"},"completedJobs":{"type":"integer"},"failedJobs":{"type":"integer"},"swapRules":{"type":"array","items":{"type":"object","properties":{"matchText":{"type":"string"},"matchMode":{"type":"string","enum":["contains","exact"]},"materialId":{"type":"string"}}}},"modelScope":{"type":"object","additionalProperties":true,"properties":{"type":{"type":"string","enum":["all","tag","manual"]},"tagIds":{"type":"array","items":{"type":"string"}},"modelIds":{"type":"array","items":{"type":"string"}}}},"jobs":{"type":"array","items":{"type":"object","additionalProperties":true,"properties":{"id":{"type":"string"},"batchId":{"type":"string"},"colorwayDefinitionId":{"type":"string"},"baseModelId":{"type":"string"},"status":{"type":"string","enum":["queued","swapping","complete","failed"]},"retryCount":{"type":"integer"},"errorMessage":{"type":["string","null"]}}}}}},"RetryColorwayJobResponse":{"type":"object","properties":{"jobId":{"type":"string"},"batchId":{"type":"string"},"status":{"type":"string","enum":["queued","swapping","complete","failed"]},"retryCount":{"type":"integer"}}},"UsageResponse":{"type":"object","description":"The workspace's plan tier plus the limits this API enforces and current counts. A `null` limit means unlimited on the current plan. Render volume is governed by credit allowances (see billing), not a live counter — `renders.limitPerMonth`/`limitPerYear` are the plan's headline figures, not a used-vs-remaining count.","properties":{"tier":{"type":"string","description":"Effective plan tier.","enum":["free","creator","production","enterprise"]},"projects":{"type":"object","properties":{"used":{"type":"integer","description":"Current non-sample project count."},"limit":{"type":["integer","null"],"description":"Max projects, or null for unlimited."}}},"models":{"type":"object","properties":{"used":{"type":"integer","description":"Current non-sample model count."},"limit":{"type":["integer","null"],"description":"Max models, or null for unlimited."}}},"renders":{"type":"object","properties":{"limitPerMonth":{"type":["integer","null"],"description":"Monthly render allowance, or null for unlimited."},"limitPerYear":{"type":["integer","null"],"description":"Annual render allowance, or null for unlimited."}}}}},"ModelListItem":{"type":"object","properties":{"modelId":{"type":"string"},"name":{"type":"string"},"status":{"$ref":"#/components/schemas/ModelStatus"},"category":{"type":"string"},"fileType":{"type":"string"},"fileSize":{"type":"integer","description":"File size in bytes."},"createdAt":{"type":"string","format":"date-time"}}},"ModelList":{"type":"object","properties":{"models":{"type":"array","items":{"$ref":"#/components/schemas/ModelListItem"}},"total":{"type":"integer","description":"Total models matching the filter (for pagination)."},"limit":{"type":"integer"},"offset":{"type":"integer"}}},"DeleteModelResponse":{"type":"object","properties":{"deleted":{"type":"boolean"},"modelId":{"type":"string"}}},"ProjectListItem":{"type":"object","properties":{"projectId":{"type":"string"},"name":{"type":"string"},"status":{"type":"string","description":"Project status (e.g. PENDING, IN_PROGRESS, COMPLETED)."},"source":{"type":"string","description":"Origin of the project (e.g. STUDIO, QUICK_RENDER, API)."},"modelIds":{"type":"array","items":{"type":"string"}},"templateId":{"type":"string"},"createdAt":{"type":"string","format":"date-time"}}},"ProjectList":{"type":"object","properties":{"projects":{"type":"array","items":{"$ref":"#/components/schemas/ProjectListItem"}},"total":{"type":"integer","description":"Total projects matching the filter (for pagination)."},"limit":{"type":"integer"},"offset":{"type":"integer"}}},"DeleteProjectResponse":{"type":"object","properties":{"deleted":{"type":"boolean"},"projectId":{"type":"string"}}},"BatchStatusResponse":{"type":"object","description":"Aggregate status of every render job in a batch (the batchId returned by POST /renders).","properties":{"batchId":{"type":"string"},"total":{"type":"integer","description":"Number of render jobs in the batch."},"counts":{"type":"object","additionalProperties":{"type":"integer"},"description":"Job count keyed by RenderJobStatus."},"jobs":{"type":"array","items":{"type":"object","properties":{"jobId":{"type":"string"},"status":{"$ref":"#/components/schemas/RenderJobStatus"},"projectId":{"type":"string"},"errorMessage":{"type":"string"}}}}}},"CancelRenderResponse":{"type":"object","properties":{"jobId":{"type":"string"},"status":{"type":"string","enum":["CANCELLED"]}}},"ImportModelRequest":{"type":"object","required":["url"],"properties":{"url":{"type":"string","format":"uri","description":"Public http(s) download URL of the model file."},"name":{"type":"string","description":"Display name. Defaults to the file name in the URL."},"fileType":{"type":"string","description":"File extension, needed when the URL has none (e.g. a signed download link)."}}},"ImportModelResponse":{"type":"object","properties":{"modelId":{"type":"string"},"name":{"type":"string"},"fileType":{"type":"string"},"status":{"type":"string","enum":["UPLOADING"]}}},"LibraryStatus":{"type":"string","enum":["PENDING","UPLOADING","ANALYZING","READY","FAILED"]},"CreatePropsRequest":{"type":"object","required":["props"],"properties":{"props":{"type":"array","minItems":1,"maxItems":50,"items":{"type":"object","properties":{"fileName":{"type":"string","description":"Original file name; derives name and fileType when omitted."},"fileType":{"type":"string","description":"File extension (glb, fbx, obj, usdz, …)."},"name":{"type":"string"},"fileSize":{"type":"integer"},"collection":{"type":"string","description":"Collection name; created in the workspace on first use."}}}}}},"CreatePropsResponse":{"type":"object","properties":{"props":{"type":"array","items":{"type":"object","properties":{"propId":{"type":"string"},"name":{"type":"string"},"uploadUrl":{"type":"string","description":"Presigned S3 PUT URL, valid 1 hour."},"uploadKey":{"type":"string"}}}},"errors":{"type":"array","items":{"type":"string"}}}},"Prop":{"type":"object","properties":{"propId":{"type":"string"},"name":{"type":"string"},"status":{"$ref":"#/components/schemas/LibraryStatus"},"source":{"type":"string","enum":["catalog","uploaded"]},"fileType":{"type":"string","nullable":true},"fileSize":{"type":"integer","nullable":true},"collectionId":{"type":"string","nullable":true},"thumbnailUrl":{"type":"string","nullable":true},"createdAt":{"type":"string","format":"date-time"}}},"ListPropsResponse":{"type":"object","properties":{"props":{"type":"array","items":{"$ref":"#/components/schemas/Prop"}},"total":{"type":"integer"},"limit":{"type":"integer"},"offset":{"type":"integer"}}},"PropsStatusRequest":{"type":"object","required":["propIds"],"properties":{"propIds":{"type":"array","minItems":1,"maxItems":100,"items":{"type":"string"}}}},"PropsStatusResponse":{"type":"object","properties":{"props":{"type":"array","items":{"type":"object","properties":{"propId":{"type":"string"},"name":{"type":"string"},"status":{"$ref":"#/components/schemas/LibraryStatus"},"ready":{"type":"boolean"}}}},"allReady":{"type":"boolean"},"notFound":{"type":"array","items":{"type":"string"}}}},"ConfirmPropResponse":{"type":"object","properties":{"propId":{"type":"string"},"status":{"$ref":"#/components/schemas/LibraryStatus"}}},"CreateDecalsRequest":{"type":"object","required":["decals"],"properties":{"decals":{"type":"array","minItems":1,"maxItems":50,"items":{"type":"object","properties":{"fileName":{"type":"string"},"fileType":{"type":"string","description":"Image extension: png, jpg, webp, gif, svg, tiff."},"name":{"type":"string"},"fileSize":{"type":"integer"},"collection":{"type":"string","description":"Collection name; created on first use."}}}}}},"CreateDecalsResponse":{"type":"object","properties":{"decals":{"type":"array","items":{"type":"object","properties":{"decalId":{"type":"string"},"name":{"type":"string"},"uploadUrl":{"type":"string","description":"Presigned S3 PUT URL, valid 1 hour."},"uploadKey":{"type":"string"},"contentType":{"type":"string","description":"The URL is bound to this Content-Type; the PUT must send it."}}}},"errors":{"type":"array","items":{"type":"string"}}}},"Decal":{"type":"object","properties":{"decalId":{"type":"string"},"name":{"type":"string"},"status":{"$ref":"#/components/schemas/LibraryStatus"},"fileType":{"type":"string","nullable":true},"width":{"type":"integer","nullable":true},"height":{"type":"integer","nullable":true},"collectionId":{"type":"string","nullable":true},"createdAt":{"type":"string","format":"date-time"}}},"ListDecalsResponse":{"type":"object","properties":{"decals":{"type":"array","items":{"$ref":"#/components/schemas/Decal"}},"total":{"type":"integer"},"limit":{"type":"integer"},"offset":{"type":"integer"}}},"DecalsStatusRequest":{"type":"object","required":["decalIds"],"properties":{"decalIds":{"type":"array","minItems":1,"maxItems":100,"items":{"type":"string"}}}},"DecalsStatusResponse":{"type":"object","properties":{"decals":{"type":"array","items":{"type":"object","properties":{"decalId":{"type":"string"},"name":{"type":"string"},"status":{"$ref":"#/components/schemas/LibraryStatus"},"ready":{"type":"boolean"}}}},"allReady":{"type":"boolean"},"notFound":{"type":"array","items":{"type":"string"}}}},"ConfirmDecalResponse":{"type":"object","properties":{"decalId":{"type":"string"},"status":{"$ref":"#/components/schemas/LibraryStatus"}}},"TextureChannel":{"type":"string","enum":["diffuse","normal","roughness","metallic","depth","opacity"]},"PlanMaterialImportRequest":{"type":"object","required":["paths"],"properties":{"paths":{"type":"array","minItems":1,"maxItems":2000,"items":{"type":"string"},"description":"File paths or names from the folder being imported."},"unmatchedAsDiffuse":{"type":"boolean","default":false,"description":"Treat an image whose name has no channel suffix as a single-texture diffuse material."}}},"PlanMaterialImportResponse":{"type":"object","properties":{"materials":{"type":"array","items":{"type":"object","properties":{"name":{"type":"string"},"textures":{"type":"object","description":"Channel → input path.","additionalProperties":{"type":"string"}}}}},"ignored":{"type":"array","items":{"type":"string"},"description":"Recognised maps Glossi has no slot for (AO, specular, emissive)."},"packed":{"type":"array","items":{"type":"string"},"description":"Packed multi-channel maps (ORM etc.); export them as separate channels."},"unmatched":{"type":"array","items":{"type":"string"},"description":"Images whose channel could not be told from the name."},"skipped":{"type":"array","items":{"type":"string"},"description":"Non-image files."},"duplicates":{"type":"array","items":{"type":"object","properties":{"file":{"type":"string"},"material":{"type":"string"},"channel":{"$ref":"#/components/schemas/TextureChannel"}}}}}},"CreateMaterialRequest":{"type":"object","required":["name","textures"],"properties":{"name":{"type":"string"},"textures":{"type":"array","minItems":1,"items":{"type":"object","required":["channel","fileType"],"properties":{"channel":{"$ref":"#/components/schemas/TextureChannel"},"fileType":{"type":"string","description":"Image extension: png, jpg, jpeg, exr."}}}},"collection":{"type":"string","description":"Collection name; created on first use."}}},"CreateMaterialResponse":{"type":"object","properties":{"materialId":{"type":"string"},"name":{"type":"string"},"collectionId":{"type":"string"},"uploads":{"type":"array","items":{"type":"object","properties":{"channel":{"type":"string","description":"channel.ext, e.g. diffuse.png"},"uploadUrl":{"type":"string","description":"Presigned S3 PUT URL, valid 1 hour."}}}},"uploadedTypes":{"type":"array","items":{"type":"string"},"description":"Echo these back on confirm."}}},"ConfirmMaterialRequest":{"type":"object","required":["name","uploadedTypes"],"properties":{"name":{"type":"string"},"uploadedTypes":{"type":"array","minItems":1,"items":{"type":"string"}},"collectionId":{"type":"string"}}},"ConfirmMaterialResponse":{"type":"object","properties":{"materialId":{"type":"string"},"status":{"type":"string","enum":["processing"]}}},"Material":{"type":"object","properties":{"materialId":{"type":"string"},"name":{"type":"string"},"type":{"type":"string"},"status":{"type":"string"},"source":{"type":"string"},"collectionId":{"type":"string","nullable":true},"thumbnailUrl":{"type":"string","nullable":true},"createdAt":{"type":"string","format":"date-time"}}},"ListMaterialsResponse":{"type":"object","properties":{"materials":{"type":"array","items":{"$ref":"#/components/schemas/Material"}},"total":{"type":"integer"},"limit":{"type":"integer"},"offset":{"type":"integer"}}},"MaterialsStatusRequest":{"type":"object","required":["materialIds"],"properties":{"materialIds":{"type":"array","minItems":1,"maxItems":100,"items":{"type":"string"}}}},"MaterialsStatusResponse":{"type":"object","properties":{"materials":{"type":"array","items":{"type":"object","properties":{"materialId":{"type":"string"},"status":{"type":"string","enum":["processing","ready","failed"]},"error":{"type":"string"}}}},"allReady":{"type":"boolean"}}},"UpdateLibraryItemRequest":{"type":"object","properties":{"name":{"type":"string","description":"New library name."},"collection":{"type":"string","nullable":true,"description":"Collection name to file the item into (created if missing); null removes it from its collection."},"collectionId":{"type":"string","nullable":true,"description":"Collection id; null removes. Ignored when `collection` is given."}}},"UpdateModelRequest":{"type":"object","required":["name"],"properties":{"name":{"type":"string","description":"New library name. Models have no collections."}}},"DeletePropResponse":{"type":"object","properties":{"deleted":{"type":"boolean","enum":[true]},"propId":{"type":"string"}}},"DeleteDecalResponse":{"type":"object","properties":{"deleted":{"type":"boolean","enum":[true]},"decalId":{"type":"string"}}},"DeleteMaterialResponse":{"type":"object","properties":{"deleted":{"type":"boolean","enum":[true]},"materialId":{"type":"string"}}}},"responses":{"Unauthorized":{"description":"Authentication failed — missing, malformed, revoked, or unknown API key (codes: MISSING_API_KEY, INVALID_API_KEY, WORKSPACE_NOT_FOUND).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"RateLimited":{"description":"Rate limit exceeded for this API key (code: RATE_LIMITED).","headers":{"X-RateLimit-Limit":{"schema":{"type":"integer"},"description":"Requests allowed per fixed 60s window."},"X-RateLimit-Remaining":{"schema":{"type":"integer"},"description":"Requests remaining in the current window."},"X-RateLimit-Reset":{"schema":{"type":"integer"},"description":"Unix timestamp (seconds) when the window resets."},"Retry-After":{"schema":{"type":"integer"},"description":"Seconds to wait before retrying."}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"InternalError":{"description":"Unhandled server error (code: INTERNAL_ERROR).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"ValidationError":{"description":"Request validation failed (code: VALIDATION_ERROR).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"IdempotencyConflict":{"description":"A request with the same Idempotency-Key is still in flight (code: IDEMPOTENCY_CONFLICT).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"IdempotencyKeyReused":{"description":"The Idempotency-Key was already used with a different request body (code: IDEMPOTENCY_KEY_REUSED).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"parameters":{"RateLimitLimit":{"name":"X-RateLimit-Limit","in":"header","required":false,"schema":{"type":"integer"},"description":"Echoed in responses; not a request parameter."},"IdempotencyKey":{"name":"Idempotency-Key","in":"header","required":false,"schema":{"type":"string","maxLength":255},"description":"Optional idempotency token (Stripe-style). A retry with the same key replays the original response instead of creating a duplicate resource. Reusing a key with a different request body returns 422; an identical request still in flight returns 409."}}},"paths":{"/me":{"get":{"operationId":"getMe","summary":"Identify the authenticated API key","tags":["Me"],"responses":{"200":{"description":"Key and workspace identity.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MeResponse"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/me/usage":{"get":{"operationId":"getUsage","summary":"Get plan tier, limits, and current usage","description":"A cheap preflight for clients/plugins before creating resources. Returns the workspace's plan tier, the project/model limits this API enforces (with current counts), and the render allowances. Render volume is governed by credit allowances, not a live counter.","tags":["Me"],"responses":{"200":{"description":"Plan tier, limits, and current usage.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UsageResponse"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/models":{"get":{"operationId":"listModels","summary":"List models","description":"List the workspace's models (newest first), excluding sample models.","tags":["Models"],"parameters":[{"name":"limit","in":"query","required":false,"schema":{"type":"integer","minimum":1,"maximum":100,"default":50}},{"name":"offset","in":"query","required":false,"schema":{"type":"integer","minimum":0,"default":0}},{"name":"status","in":"query","required":false,"schema":{"type":"string"},"description":"Filter by model status."},{"name":"name","in":"query","required":false,"schema":{"type":"string"},"description":"Case-insensitive substring match on the model name."}],"responses":{"200":{"description":"A page of models.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ModelList"}}}},"400":{"$ref":"#/components/responses/ValidationError"},"401":{"$ref":"#/components/responses/Unauthorized"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}},"post":{"operationId":"createModels","summary":"Create models with signed upload URLs","tags":["Models"],"parameters":[{"$ref":"#/components/parameters/IdempotencyKey"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateModelsRequest"}}}},"responses":{"201":{"description":"Models created (may include a partial-failure `errors` array).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateModelsResponse"}}}},"400":{"$ref":"#/components/responses/ValidationError"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"Plan model limit reached (code: PLAN_LIMIT_REACHED).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"$ref":"#/components/responses/IdempotencyConflict"},"422":{"$ref":"#/components/responses/IdempotencyKeyReused"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/models/{modelId}/confirm":{"post":{"operationId":"confirmModelUpload","summary":"Confirm a model file was uploaded","tags":["Models"],"parameters":[{"name":"modelId","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ConfirmModelRequest"}}}},"responses":{"200":{"description":"Upload recorded.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ConfirmModelResponse"}}}},"400":{"description":"Validation failed, or no file was found at the upload location (codes: VALIDATION_ERROR, FILE_NOT_UPLOADED).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"description":"Model not found or access denied (code: MODEL_NOT_FOUND).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/models/import":{"post":{"operationId":"importModel","summary":"Import a model from a public URL","description":"Glossi downloads the file server-side (every redirect hop is checked against private networks; 1 GB cap) and confirms it, so no client-side upload is needed. Returns as soon as the model is registered; poll POST /models/status. A failed download leaves the model FAILED with the reason in `error`.","tags":["Models"],"parameters":[{"$ref":"#/components/parameters/IdempotencyKey"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ImportModelRequest"}}}},"responses":{"202":{"description":"Import started.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ImportModelResponse"}}}},"400":{"description":"Missing url, unsafe or unreachable URL, unknown file type, or file over the limit (VALIDATION_ERROR).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"409":{"$ref":"#/components/responses/IdempotencyConflict"},"422":{"$ref":"#/components/responses/IdempotencyKeyReused"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/models/status":{"post":{"operationId":"getModelsStatus","summary":"Batch model status check","tags":["Models"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ModelStatusRequest"}}}},"responses":{"200":{"description":"Per-model status.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ModelStatusResponse"}}}},"400":{"$ref":"#/components/responses/ValidationError"},"401":{"$ref":"#/components/responses/Unauthorized"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/models/{modelId}/download":{"get":{"operationId":"downloadModel","summary":"Download a model file","description":"302-redirects to a short-lived (1 hour) presigned URL for one of the model's stored files: the converted GLB (default), the USDZ derivative, or the original upload.","tags":["Models"],"parameters":[{"name":"modelId","in":"path","required":true,"schema":{"type":"string"}},{"name":"format","in":"query","required":false,"schema":{"type":"string","enum":["glb","usdz","source"],"default":"glb"}}],"responses":{"302":{"description":"Redirect to a presigned download URL (Location header)."},"400":{"description":"Unknown format (VALIDATION_ERROR).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"description":"Model not found (MODEL_NOT_FOUND) or it has no file of that format (NO_FILE).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/models/{modelId}":{"patch":{"operationId":"updateModel","summary":"Rename a model","tags":["Models"],"parameters":[{"name":"modelId","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateModelRequest"}}}},"responses":{"200":{"description":"The updated model.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Model"}}}},"400":{"$ref":"#/components/responses/ValidationError"},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"description":"Model not found or access denied (code: MODEL_NOT_FOUND).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}},"get":{"operationId":"getModel","summary":"Get a single model","tags":["Models"],"parameters":[{"name":"modelId","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Model details.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Model"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"description":"Model not found or access denied (code: MODEL_NOT_FOUND).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}},"delete":{"operationId":"deleteModel","summary":"Delete a model","description":"Delete a model and clean up its references and S3 assets.","tags":["Models"],"parameters":[{"name":"modelId","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Model deleted.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DeleteModelResponse"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"description":"Model not found or access denied (code: MODEL_NOT_FOUND).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/projects":{"get":{"operationId":"listProjects","summary":"List projects","description":"List the workspace's projects (newest first).","tags":["Projects"],"parameters":[{"name":"limit","in":"query","required":false,"schema":{"type":"integer","minimum":1,"maximum":100,"default":50}},{"name":"offset","in":"query","required":false,"schema":{"type":"integer","minimum":0,"default":0}},{"name":"status","in":"query","required":false,"schema":{"type":"string"},"description":"Filter by project status."}],"responses":{"200":{"description":"A page of projects.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProjectList"}}}},"400":{"$ref":"#/components/responses/ValidationError"},"401":{"$ref":"#/components/responses/Unauthorized"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}},"post":{"operationId":"createProject","summary":"Create one project (or one per model)","tags":["Projects"],"parameters":[{"$ref":"#/components/parameters/IdempotencyKey"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateProjectRequest"}}}},"responses":{"201":{"description":"Project(s) created.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateProjectResponse"}}}},"400":{"$ref":"#/components/responses/ValidationError"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"Plan project limit reached (code: PLAN_LIMIT_REACHED).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Referenced model or template not found (code: NOT_FOUND).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"$ref":"#/components/responses/IdempotencyConflict"},"422":{"$ref":"#/components/responses/IdempotencyKeyReused"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/projects/{projectId}":{"get":{"operationId":"getProject","summary":"Get a single project","tags":["Projects"],"parameters":[{"name":"projectId","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Project details.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Project"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"description":"Project not found or access denied (code: PROJECT_NOT_FOUND).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}},"delete":{"operationId":"deleteProject","summary":"Delete a project","description":"Delete a project and clean up its renders, references, and S3 assets.","tags":["Projects"],"parameters":[{"name":"projectId","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Project deleted.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DeleteProjectResponse"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"description":"Project not found or access denied (code: PROJECT_NOT_FOUND).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/renders":{"get":{"operationId":"listRenders","summary":"List render assets","description":"List finished render outputs (images/videos) for the workspace, newest first.","tags":["Renders"],"parameters":[{"name":"limit","in":"query","required":false,"schema":{"type":"integer","minimum":1,"maximum":100,"default":50}},{"name":"offset","in":"query","required":false,"schema":{"type":"integer","minimum":0,"default":0}},{"name":"projectId","in":"query","required":false,"schema":{"type":"string"}},{"name":"isVideo","in":"query","required":false,"schema":{"type":"boolean"}},{"name":"favorited","in":"query","required":false,"schema":{"type":"boolean"}}],"responses":{"200":{"description":"A page of render assets.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RenderList"}}}},"400":{"$ref":"#/components/responses/ValidationError"},"401":{"$ref":"#/components/responses/Unauthorized"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}},"post":{"operationId":"createRender","summary":"Kick off a bulk render","tags":["Renders"],"parameters":[{"$ref":"#/components/parameters/IdempotencyKey"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateRenderRequest"}}}},"responses":{"202":{"description":"Render batch accepted.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateRenderResponse"}}}},"400":{"$ref":"#/components/responses/ValidationError"},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"description":"One or more projects not found (code: PROJECT_NOT_FOUND).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"$ref":"#/components/responses/IdempotencyConflict"},"422":{"$ref":"#/components/responses/IdempotencyKeyReused"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/renders/batch/{batchId}":{"get":{"operationId":"getRenderBatch","summary":"Get aggregate status of a render batch","description":"Aggregate status of every render job in a batch (the batchId returned by POST /renders).","tags":["Renders"],"parameters":[{"name":"batchId","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Batch status with per-job breakdown.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BatchStatusResponse"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"description":"Batch not found or access denied (code: NOT_FOUND).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/renders/{id}":{"get":{"operationId":"getRenderById","summary":"Get a render job or render asset by id","description":"Resolves a render JOB status or a render ASSET by id (the id spaces are disjoint). A job response carries `status` (and `renders` when COMPLETED); an asset response carries `url`/`fileType`/`isVideo`.","tags":["Renders"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"A render job status or a render asset.","content":{"application/json":{"schema":{"oneOf":[{"$ref":"#/components/schemas/RenderJobStatusResponse"},{"$ref":"#/components/schemas/RenderAsset"}]}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"description":"No render job or render found for that id (code: RENDER_JOB_NOT_FOUND).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/renders/{renderId}/download":{"get":{"operationId":"downloadRender","summary":"Download a render asset","description":"302-redirects to a short-lived (1 hour) presigned URL for the render's primary file (video → mp4, else image).","tags":["Renders"],"parameters":[{"name":"renderId","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"302":{"description":"Redirect to a presigned download URL (Location header)."},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"description":"Render not found (NOT_FOUND) or it has no downloadable file (NO_FILE).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/renders/{jobId}/retry":{"post":{"operationId":"retryRenderJob","summary":"Retry a failed render job","tags":["Renders"],"parameters":[{"name":"jobId","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"202":{"description":"Retry accepted.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RetryRenderResponse"}}}},"400":{"description":"Job cannot be retried in its current state (codes: INVALID_STATUS, VALIDATION_ERROR).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"description":"Render job not found or access denied (code: RENDER_JOB_NOT_FOUND).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/renders/{jobId}/cancel":{"post":{"operationId":"cancelRenderJob","summary":"Cancel an un-dispatched render job","description":"Cancel a render job that has not been dispatched yet. Only QUEUED / RENDER_QUEUED jobs can be cancelled; once a job has started rendering it cannot be cancelled.","tags":["Renders"],"parameters":[{"name":"jobId","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Job cancelled.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CancelRenderResponse"}}}},"400":{"description":"Job has already started and cannot be cancelled (code: INVALID_STATUS).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"description":"Render job not found or access denied (code: RENDER_JOB_NOT_FOUND).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/templates":{"get":{"operationId":"listTemplates","summary":"List templates","tags":["Templates"],"parameters":[{"name":"public","in":"query","required":false,"schema":{"type":"boolean","default":true},"description":"Include public templates. Defaults to true."},{"name":"internal","in":"query","required":false,"schema":{"type":"boolean","default":true},"description":"Include this workspace's templates. Defaults to true. At least one of `public`/`internal` must be true."}],"responses":{"200":{"description":"Matching templates.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TemplatesListResponse"}}}},"400":{"$ref":"#/components/responses/ValidationError"},"401":{"$ref":"#/components/responses/Unauthorized"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/templates/{templateId}":{"get":{"operationId":"getTemplate","summary":"Get a single template","tags":["Templates"],"parameters":[{"name":"templateId","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Template details.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Template"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"description":"Template not found or not accessible (code: TEMPLATE_NOT_FOUND).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/webhooks":{"get":{"operationId":"getWebhookConfig","summary":"Get webhook configuration","tags":["Webhooks"],"responses":{"200":{"description":"Webhook config, or `{ configured: false }` when none exists.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookConfigResponse"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}},"put":{"operationId":"upsertWebhookConfig","summary":"Create or update webhook configuration","tags":["Webhooks"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpsertWebhookRequest"}}}},"responses":{"200":{"description":"Config saved.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpsertWebhookResponse"}}}},"400":{"$ref":"#/components/responses/ValidationError"},"401":{"$ref":"#/components/responses/Unauthorized"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}},"delete":{"operationId":"deleteWebhookConfig","summary":"Delete webhook configuration","tags":["Webhooks"],"responses":{"200":{"description":"Deleted.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SuccessResponse"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"description":"No webhook configured (code: NOT_FOUND).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/webhooks/secret":{"get":{"operationId":"getWebhookSecret","summary":"Get the webhook signing secret","tags":["Webhooks"],"responses":{"200":{"description":"The signing secret.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookSecretResponse"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"description":"No webhook configured (code: NOT_FOUND).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/webhooks/regenerate-secret":{"post":{"operationId":"regenerateWebhookSecret","summary":"Regenerate the webhook signing secret","tags":["Webhooks"],"responses":{"200":{"description":"The new signing secret.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookSecretResponse"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"description":"No webhook configured (code: NOT_FOUND).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/webhooks/reset-failures":{"post":{"operationId":"resetWebhookFailures","summary":"Reset the webhook failure counter","tags":["Webhooks"],"responses":{"200":{"description":"Failure counter reset.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SuccessResponse"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"description":"No webhook configured (code: NOT_FOUND).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/webhooks/events":{"get":{"operationId":"listWebhookEvents","summary":"List available webhook events","tags":["Webhooks"],"responses":{"200":{"description":"Available events with descriptions.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookEventsResponse"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/jobs":{"post":{"operationId":"createJob","summary":"Create a full-workflow job","tags":["Jobs"],"parameters":[{"$ref":"#/components/parameters/IdempotencyKey"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateJobRequest"}}}},"responses":{"201":{"description":"Job created with per-model upload URLs (may include `errors`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateJobResponse"}}}},"400":{"$ref":"#/components/responses/ValidationError"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"Plan limit reached (code: PLAN_LIMIT_REACHED).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"$ref":"#/components/responses/IdempotencyConflict"},"422":{"$ref":"#/components/responses/IdempotencyKeyReused"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/jobs/{jobId}/confirm-uploads":{"post":{"operationId":"confirmJobUploads","summary":"Confirm all job files uploaded","tags":["Jobs"],"parameters":[{"name":"jobId","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Uploads confirmed; job advanced to PROCESSING.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ConfirmUploadsResponse"}}}},"400":{"description":"Some files missing in S3, or the job is not awaiting uploads (codes: VALIDATION_ERROR, INVALID_STATUS).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"description":"Job not found or access denied (code: JOB_NOT_FOUND).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/jobs/{jobId}":{"get":{"operationId":"getJob","summary":"Get job status and progress","tags":["Jobs"],"parameters":[{"name":"jobId","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Job status, progress, and results (when COMPLETE).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/JobStatusResponse"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"description":"Job not found or access denied (code: JOB_NOT_FOUND).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/pipelines":{"post":{"operationId":"createPipeline","summary":"Create a pipeline","description":"Requires an Enterprise plan or an ADMIN user.","tags":["Pipelines"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreatePipelineRequest"}}}},"responses":{"201":{"description":"Pipeline created.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreatePipelineResponse"}}}},"400":{"$ref":"#/components/responses/ValidationError"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"Pipeline access requires an Enterprise plan (code: PLAN_LIMIT_REACHED).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}},"get":{"operationId":"listPipelines","summary":"List pipelines","description":"Requires an Enterprise plan or an ADMIN user.","tags":["Pipelines"],"responses":{"200":{"description":"Workspace pipelines.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PipelineListResponse"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"Pipeline access requires an Enterprise plan (code: PLAN_LIMIT_REACHED).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/pipelines/connectors":{"get":{"operationId":"listPipelineConnectors","summary":"List available connector types","description":"Requires an Enterprise plan or an ADMIN user.","tags":["Pipelines"],"responses":{"200":{"description":"Registered connector type keys.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PipelineConnectorsResponse"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"Pipeline access requires an Enterprise plan (code: PLAN_LIMIT_REACHED).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/pipelines/{id}":{"get":{"operationId":"getPipeline","summary":"Get a pipeline with job stats","description":"Requires an Enterprise plan or an ADMIN user.","tags":["Pipelines"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Pipeline details.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PipelineDetailResponse"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"Pipeline access requires an Enterprise plan (code: PLAN_LIMIT_REACHED).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Pipeline not found (code: NOT_FOUND).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}},"patch":{"operationId":"updatePipeline","summary":"Update pipeline settings or pause/resume","description":"Requires an Enterprise plan or an ADMIN user.","tags":["Pipelines"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdatePipelineRequest"}}}},"responses":{"200":{"description":"Pipeline updated.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdatePipelineResponse"}}}},"400":{"$ref":"#/components/responses/ValidationError"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"Pipeline access requires an Enterprise plan (code: PLAN_LIMIT_REACHED).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Pipeline not found (code: NOT_FOUND).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}},"delete":{"operationId":"deletePipeline","summary":"Delete a pipeline and its jobs","description":"Requires an Enterprise plan or an ADMIN user.","tags":["Pipelines"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}},{"name":"cleanupResources","in":"query","required":false,"schema":{"type":"boolean","default":true},"description":"Also clean up associated resources. Defaults to true."}],"responses":{"200":{"description":"Deleted.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SuccessResponse"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"Pipeline access requires an Enterprise plan (code: PLAN_LIMIT_REACHED).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Pipeline not found (code: NOT_FOUND).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/pipelines/{id}/jobs":{"get":{"operationId":"listPipelineJobs","summary":"List a pipeline's jobs","description":"Requires an Enterprise plan or an ADMIN user.","tags":["Pipelines"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}},{"name":"status","in":"query","required":false,"schema":{"$ref":"#/components/schemas/RenderJobStatus"},"description":"Optional status filter."},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","default":50,"maximum":100},"description":"Page size (default 50, capped at 100)."},{"name":"offset","in":"query","required":false,"schema":{"type":"integer","default":0},"description":"Pagination offset (default 0)."}],"responses":{"200":{"description":"Paginated pipeline jobs.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PipelineJobsResponse"}}}},"400":{"$ref":"#/components/responses/ValidationError"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"Pipeline access requires an Enterprise plan (code: PLAN_LIMIT_REACHED).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Pipeline not found (code: NOT_FOUND).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/colorway-batches":{"post":{"operationId":"createColorwayBatch","summary":"Create a colorway (material-swap) batch","tags":["Colorways"],"parameters":[{"$ref":"#/components/parameters/IdempotencyKey"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateColorwayBatchRequest"}}}},"responses":{"202":{"description":"Batch accepted.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateColorwayBatchResponse"}}}},"400":{"$ref":"#/components/responses/ValidationError"},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"description":"No matching/ready models or missing referenced entity (code: NOT_FOUND).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"$ref":"#/components/responses/IdempotencyConflict"},"422":{"$ref":"#/components/responses/IdempotencyKeyReused"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/colorway-batches/{id}":{"get":{"operationId":"getColorwayBatch","summary":"Get a colorway batch with its jobs","tags":["Colorways"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Batch status with per-job breakdown.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ColorwayBatchStatusResponse"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"description":"Batch not found (code: NOT_FOUND).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/colorway-jobs/{id}/retry":{"post":{"operationId":"retryColorwayJob","summary":"Retry a failed colorway job","tags":["Colorways"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Job re-queued.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RetryColorwayJobResponse"}}}},"400":{"description":"Only failed jobs can be retried (code: INVALID_STATUS).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"description":"Colorway job not found (code: NOT_FOUND).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/props":{"post":{"operationId":"createProps","summary":"Create props with upload URLs","description":"Registers up to 50 props and returns a presigned S3 PUT URL for each (valid 1 hour). Upload the file, then POST /props/{propId}/confirm; the converter normalises it and the prop becomes READY. `collection` is a name: the collection is created in the workspace on first use.","tags":["Props"],"parameters":[{"$ref":"#/components/parameters/IdempotencyKey"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreatePropsRequest"}}}},"responses":{"201":{"description":"Props created. `errors` lists items that failed, if any.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreatePropsResponse"}}}},"400":{"$ref":"#/components/responses/ValidationError"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"Workspace storage limit reached (STORAGE_LIMIT_REACHED).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"$ref":"#/components/responses/IdempotencyConflict"},"422":{"$ref":"#/components/responses/IdempotencyKeyReused"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}},"get":{"operationId":"listProps","summary":"List props","tags":["Props"],"parameters":[{"name":"limit","in":"query","schema":{"type":"integer","minimum":1,"maximum":100,"default":50}},{"name":"offset","in":"query","schema":{"type":"integer","minimum":0,"default":0}},{"name":"status","in":"query","schema":{"$ref":"#/components/schemas/LibraryStatus"}},{"name":"name","in":"query","schema":{"type":"string"},"description":"Case-insensitive substring match."},{"name":"collectionId","in":"query","schema":{"type":"string"}}],"responses":{"200":{"description":"Props, newest first.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ListPropsResponse"}}}},"400":{"$ref":"#/components/responses/ValidationError"},"401":{"$ref":"#/components/responses/Unauthorized"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/props/status":{"post":{"operationId":"getPropsStatus","summary":"Batch prop status check","tags":["Props"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PropsStatusRequest"}}}},"responses":{"200":{"description":"Per-prop status.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PropsStatusResponse"}}}},"400":{"$ref":"#/components/responses/ValidationError"},"401":{"$ref":"#/components/responses/Unauthorized"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/props/{propId}/confirm":{"post":{"operationId":"confirmPropUpload","summary":"Confirm a prop upload","description":"Marks the file as uploaded. Conversion starts and the prop moves through ANALYZING to READY.","tags":["Props"],"parameters":[{"name":"propId","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Upload confirmed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ConfirmPropResponse"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"description":"Prop not found or access denied (NOT_FOUND).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/props/{propId}":{"patch":{"operationId":"updateProp","summary":"Rename or re-file a prop","tags":["Props"],"parameters":[{"name":"propId","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateLibraryItemRequest"}}}},"responses":{"200":{"description":"The updated prop.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Prop"}}}},"400":{"$ref":"#/components/responses/ValidationError"},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"description":"Prop not found or access denied (NOT_FOUND).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}},"delete":{"operationId":"deleteProp","summary":"Delete a prop","description":"Permanent. Removes the prop and its files.","tags":["Props"],"parameters":[{"name":"propId","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Deleted.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DeletePropResponse"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"description":"Prop not found or access denied (NOT_FOUND).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}},"get":{"operationId":"getProp","summary":"Get a single prop","tags":["Props"],"parameters":[{"name":"propId","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Prop details.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Prop"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"description":"Prop not found or access denied (NOT_FOUND).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/decals":{"post":{"operationId":"createDecals","summary":"Create decals with upload URLs","description":"Registers up to 50 decal images and returns a presigned S3 PUT URL for each (valid 1 hour). The URL is bound to the returned `contentType`; send it as the PUT's Content-Type header. Then POST /decals/{decalId}/confirm — decals need no conversion and become READY on confirm.","tags":["Decals"],"parameters":[{"$ref":"#/components/parameters/IdempotencyKey"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateDecalsRequest"}}}},"responses":{"201":{"description":"Decals created. `errors` lists items that failed, if any.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateDecalsResponse"}}}},"400":{"$ref":"#/components/responses/ValidationError"},"401":{"$ref":"#/components/responses/Unauthorized"},"409":{"$ref":"#/components/responses/IdempotencyConflict"},"422":{"$ref":"#/components/responses/IdempotencyKeyReused"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}},"get":{"operationId":"listDecals","summary":"List decals","tags":["Decals"],"parameters":[{"name":"limit","in":"query","schema":{"type":"integer","minimum":1,"maximum":100,"default":50}},{"name":"offset","in":"query","schema":{"type":"integer","minimum":0,"default":0}},{"name":"status","in":"query","schema":{"$ref":"#/components/schemas/LibraryStatus"}},{"name":"name","in":"query","schema":{"type":"string"},"description":"Case-insensitive substring match."},{"name":"collectionId","in":"query","schema":{"type":"string"}}],"responses":{"200":{"description":"Decals, newest first.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ListDecalsResponse"}}}},"400":{"$ref":"#/components/responses/ValidationError"},"401":{"$ref":"#/components/responses/Unauthorized"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/decals/status":{"post":{"operationId":"getDecalsStatus","summary":"Batch decal status check","tags":["Decals"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/DecalsStatusRequest"}}}},"responses":{"200":{"description":"Per-decal status.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DecalsStatusResponse"}}}},"400":{"$ref":"#/components/responses/ValidationError"},"401":{"$ref":"#/components/responses/Unauthorized"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/decals/{decalId}/confirm":{"post":{"operationId":"confirmDecalUpload","summary":"Confirm a decal upload","description":"Reads the uploaded image's dimensions and marks the decal READY.","tags":["Decals"],"parameters":[{"name":"decalId","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Upload confirmed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ConfirmDecalResponse"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"description":"Decal not found or access denied (NOT_FOUND).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"description":"The image is missing or not decodable; the decal is marked FAILED (INTERNAL_ERROR).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/decals/{decalId}":{"patch":{"operationId":"updateDecal","summary":"Rename or re-file a decal","tags":["Decals"],"parameters":[{"name":"decalId","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateLibraryItemRequest"}}}},"responses":{"200":{"description":"The updated decal.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Decal"}}}},"400":{"$ref":"#/components/responses/ValidationError"},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"description":"Decal not found or access denied (NOT_FOUND).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}},"delete":{"operationId":"deleteDecal","summary":"Delete a decal","description":"Permanent. Removes the decal and its image.","tags":["Decals"],"parameters":[{"name":"decalId","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Deleted.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DeleteDecalResponse"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"description":"Decal not found or access denied (NOT_FOUND).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}},"get":{"operationId":"getDecal","summary":"Get a single decal","tags":["Decals"],"parameters":[{"name":"decalId","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Decal details.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Decal"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"description":"Decal not found or access denied (NOT_FOUND).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/materials/plan-import":{"post":{"operationId":"planMaterialImport","summary":"Group texture files into materials","description":"Pure helper for folder imports: takes file paths and groups them into materials by filename (Wood_Oak_BaseColor.png + Wood_Oak_Normal.png → one material with two channels). Nothing is uploaded.","tags":["Materials"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PlanMaterialImportRequest"}}}},"responses":{"200":{"description":"The proposed materials and every file that was not slotted, with why.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PlanMaterialImportResponse"}}}},"400":{"$ref":"#/components/responses/ValidationError"},"401":{"$ref":"#/components/responses/Unauthorized"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/materials":{"post":{"operationId":"createMaterial","summary":"Create a material with per-channel upload URLs","description":"Registers one PBR material and returns a presigned S3 PUT URL per texture channel (valid 1 hour). Upload each texture, then POST /materials/{materialId}/confirm with the returned `name` and `uploadedTypes`.","tags":["Materials"],"parameters":[{"$ref":"#/components/parameters/IdempotencyKey"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateMaterialRequest"}}}},"responses":{"201":{"description":"Material registered; upload the textures next.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateMaterialResponse"}}}},"400":{"$ref":"#/components/responses/ValidationError"},"401":{"$ref":"#/components/responses/Unauthorized"},"409":{"$ref":"#/components/responses/IdempotencyConflict"},"422":{"$ref":"#/components/responses/IdempotencyKeyReused"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}},"get":{"operationId":"listMaterials","summary":"List or search materials","description":"Without `search`, the workspace's materials newest first. With `search`, a fuzzy match on name, keywords and colour (the same index the Studio assistant uses); `offset` is ignored.","tags":["Materials"],"parameters":[{"name":"limit","in":"query","schema":{"type":"integer","minimum":1,"maximum":100,"default":50}},{"name":"offset","in":"query","schema":{"type":"integer","minimum":0,"default":0}},{"name":"search","in":"query","schema":{"type":"string"}},{"name":"collectionId","in":"query","schema":{"type":"string"}}],"responses":{"200":{"description":"Materials.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ListMaterialsResponse"}}}},"400":{"$ref":"#/components/responses/ValidationError"},"401":{"$ref":"#/components/responses/Unauthorized"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/materials/status":{"post":{"operationId":"getMaterialsStatus","summary":"Batch material import status","tags":["Materials"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/MaterialsStatusRequest"}}}},"responses":{"200":{"description":"Per-material import state.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MaterialsStatusResponse"}}}},"400":{"$ref":"#/components/responses/ValidationError"},"401":{"$ref":"#/components/responses/Unauthorized"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/materials/{materialId}/confirm":{"post":{"operationId":"confirmMaterialUpload","summary":"Confirm material texture uploads","description":"Starts processing in the background (missing PBR channels are generated). Poll POST /materials/status until `ready`.","tags":["Materials"],"parameters":[{"name":"materialId","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ConfirmMaterialRequest"}}}},"responses":{"202":{"description":"Processing started.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ConfirmMaterialResponse"}}}},"400":{"$ref":"#/components/responses/ValidationError"},"401":{"$ref":"#/components/responses/Unauthorized"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/materials/{materialId}":{"patch":{"operationId":"updateMaterial","summary":"Rename or re-file a material","tags":["Materials"],"parameters":[{"name":"materialId","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateLibraryItemRequest"}}}},"responses":{"200":{"description":"The updated material.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Material"}}}},"400":{"$ref":"#/components/responses/ValidationError"},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"description":"Material not found or access denied (NOT_FOUND).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}},"delete":{"operationId":"deleteMaterial","summary":"Delete a material","description":"Permanent. Removes the material, its versions and textures, and drops references to it from surfaces, colorways, and showroom pricing.","tags":["Materials"],"parameters":[{"name":"materialId","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Deleted.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DeleteMaterialResponse"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"description":"Material not found or access denied (NOT_FOUND).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}},"get":{"operationId":"getMaterial","summary":"Get a single material","tags":["Materials"],"parameters":[{"name":"materialId","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Material details.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Material"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"description":"Material not found or access denied (NOT_FOUND).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}}}}