{
  "openapi": "3.1.0",
  "info": {
    "title": "Pinwing API",
    "version": "1.0.0",
    "description": "Render 3D models (GLB, or zipped OBJ scans) into turntable videos and multi-view image datasets on GPU nodes. Credit-based: video is billed per second by quality tier; datasets are flat-rate. The base URL https://api.pinwing.ai/v1 is canonical; https://api.dx.gl/v1 remains fully supported (same API, same keys). Human-readable reference: https://pinwing.ai/docs/api/",
    "contact": {
      "name": "Pinwing support",
      "email": "support@pinwing.ai",
      "url": "https://pinwing.ai/contact/"
    },
    "license": {
      "name": "Proprietary — see Terms of Service",
      "url": "https://pinwing.ai/terms/"
    }
  },
  "servers": [
    {
      "url": "https://api.pinwing.ai/v1",
      "description": "Canonical"
    },
    {
      "url": "https://api.dx.gl/v1",
      "description": "Legacy host (fully supported)"
    }
  ],
  "security": [
    {
      "bearerAuth": []
    }
  ],
  "tags": [
    {
      "name": "Models",
      "description": "Upload, ingest, list, update, archive, and download 3D models."
    },
    {
      "name": "Renders",
      "description": "Create and manage renders; download videos, posters, bundles, datasets."
    },
    {
      "name": "Assets",
      "description": "Generated texture and HDRI-environment assets."
    },
    {
      "name": "Overlays",
      "description": "Background/overlay images for renders."
    },
    {
      "name": "Account",
      "description": "Account, quoting, and service health."
    }
  ],
  "paths": {
    "/models": {
      "post": {
        "operationId": "uploadModel",
        "summary": "Upload a model file (multipart); always creates a render",
        "description": "Registers a `.glb` (or `.zip` OBJ scan, auto-converted to GLB) and always creates a render: billed at the given `renderSettings`, or a free 540p system preview when omitted. Files are SHA-256 deduplicated per workspace. Requires the `render` scope.",
        "tags": [
          "Models"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "multipart/form-data": {
              "schema": {
                "type": "object",
                "required": [
                  "file"
                ],
                "properties": {
                  "file": {
                    "type": "string",
                    "format": "binary",
                    "description": ".glb (glTF binary container) or .zip (one OBJ + MTL + JPG/PNG textures, converted to GLB server-side; ZIP support is server-gated). Raw .gltf JSON is rejected — pack it into a .glb first. Max 1 GB."
                  },
                  "renderSettings": {
                    "$ref": "#/components/schemas/RenderSettings",
                    "description": "JSON-encoded render configuration. Omitting it makes the upload's render a free system preview; providing it creates a billed render at the settings given."
                  }
                }
              },
              "encoding": {
                "renderSettings": {
                  "contentType": "application/json"
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Duplicate — the file's SHA-256 matched an existing ACTIVE model in the workspace (archived/deleted models are not matched); it is reused and the response carries `duplicate: true`. With renderSettings a new billed render is created for the existing model; without, its existing system preview render is returned (a new free one is created if the model has none).",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data"
                  ],
                  "properties": {
                    "data": {
                      "$ref": "#/components/schemas/ModelUploadResult"
                    }
                  }
                }
              }
            }
          },
          "201": {
            "description": "Model registered and render queued (billed if renderSettings were given, else free system preview).",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data"
                  ],
                  "properties": {
                    "data": {
                      "$ref": "#/components/schemas/ModelUploadResult"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "no_file · file_too_large (>1 GB) · invalid_format (.glb/.zip only; ZIP disabled unless OBJ ingest is enabled) · invalid_settings (renderSettings not valid JSON or failed validation) · upload_failed (malformed multipart, or ZIP contents exceed 500 MB when unpacked).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "401": {
            "description": "unauthorized / token_expired — missing, malformed, invalid, revoked, or expired bearer token.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "402": {
            "description": "no_credits — renderSettings given but the workspace owner's credit balance cannot cover the render.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "403": {
            "description": "forbidden (account suspended, or bound workspace deleted / membership revoked) · insufficient_scope (token lacks the render scope).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "422": {
            "description": "upload_failed — file failed GLB structural validation, or ZIP→GLB conversion failed (corrupt ZIP, missing/multiple OBJ, etc.).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "429": {
            "description": "upload_limit — free-upload quota exhausted (purchase render credits to continue) · rate_limited — over 300 requests/min per token.",
            "headers": {
              "Retry-After": {
                "description": "Seconds until a retry is allowed (set for rate_limited).",
                "schema": {
                  "type": "integer"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "500": {
            "description": "upload_failed — unclassified failure while processing the upload.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        }
      },
      "get": {
        "operationId": "listModels",
        "summary": "List models (offset or keyset-cursor pagination, delta polling via updatedSince)",
        "description": "Two pagination modes: offset (default; meta carries total/offset/limit) and keyset cursor (pass `cursor=`, empty for page 1 or the previous `meta.nextCursor`; walk until null — O(page) at any depth, no COUNT). `updatedSince` composes with both modes for delta polling. Deleted models are never listed. Requires the `read` scope.",
        "tags": [
          "Models"
        ],
        "parameters": [
          {
            "name": "limit",
            "in": "query",
            "description": "Page size (max 100).",
            "schema": {
              "type": "integer",
              "default": 50,
              "minimum": 1,
              "maximum": 100
            }
          },
          {
            "name": "offset",
            "in": "query",
            "description": "Pagination offset (offset mode; ignored in cursor mode).",
            "schema": {
              "type": "integer",
              "default": 0,
              "minimum": 0
            }
          },
          {
            "name": "cursor",
            "in": "query",
            "description": "Keyset pagination: pass empty string for the first page, then the previous response's `meta.nextCursor` until it is null. Opaque — do not parse. Invalid cursor → 400 bad_request. When present, response meta becomes `{limit, nextCursor}`.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "updatedSince",
            "in": "query",
            "description": "ISO 8601 — only models modified at/after this instant (cheap delta polling). Invalid value → 400 bad_request.",
            "schema": {
              "type": "string",
              "format": "date-time"
            }
          },
          {
            "name": "tags",
            "in": "query",
            "description": "Comma-separated tag list; OR (array-overlap) filter.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "status",
            "in": "query",
            "description": "`any` = active + archived (each row carries its status). Deleted models are never listed.",
            "schema": {
              "type": "string",
              "enum": [
                "active",
                "archived",
                "any"
              ],
              "default": "active"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Model list. Offset mode meta: {total, offset, limit}; cursor mode meta: {limit, nextCursor|null}.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data",
                    "meta"
                  ],
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/ModelListItem"
                      }
                    },
                    "meta": {
                      "description": "ListMeta in offset mode; ListMetaCursor when the cursor query parameter was passed.",
                      "oneOf": [
                        {
                          "$ref": "#/components/schemas/ListMeta"
                        },
                        {
                          "$ref": "#/components/schemas/ListMetaCursor"
                        }
                      ]
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "bad_request — invalid cursor, invalid updatedSince (use ISO 8601), or invalid status (active|archived|any).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "401": {
            "description": "unauthorized / token_expired — missing, malformed, invalid, revoked, or expired bearer token.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "403": {
            "description": "forbidden (account suspended, or bound workspace deleted / membership revoked) · insufficient_scope (token lacks the read scope).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "429": {
            "description": "rate_limited — over 300 requests/min per token.",
            "headers": {
              "Retry-After": {
                "description": "Seconds until a retry is allowed.",
                "schema": {
                  "type": "integer"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "500": {
            "description": "internal.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/models/ingest": {
      "post": {
        "operationId": "ingestModel",
        "summary": "Ingest a GLB from a URL (including presigned S3/R2 GET URLs)",
        "description": "Downloads the file server-side (GLB only, max 1 GB; 45 s stall abort, 10 min absolute ceiling) and registers it like an upload. Presigned URLs skip the availability HEAD probe and are streamed directly. SHA-256 deduplication applies. Additionally rate-limited to 20 requests / 10 min per token. Requires the `render` scope.",
        "tags": [
          "Models"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ModelIngestRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Duplicate — SHA-256 matched an existing ACTIVE model in the workspace (archived/deleted models are not matched); reused with `duplicate: true` (new billed render with renderSettings, else the existing system preview — a new free one is created if the model has none).",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data"
                  ],
                  "properties": {
                    "data": {
                      "$ref": "#/components/schemas/ModelUploadResult"
                    }
                  }
                }
              }
            }
          },
          "201": {
            "description": "Model registered and render queued (billed if renderSettings were given, else free system preview).",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data"
                  ],
                  "properties": {
                    "data": {
                      "$ref": "#/components/schemas/ModelUploadResult"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "url_required · invalid_url · invalid_url_scheme (http(s) only) · blocked_host · dns_error · url_error (bad status / too many redirects) · download_failed · download_timeout (45 s without progress / 10 min ceiling) · invalid_content_type (URL does not point to a GLB) · file_too_large (>1 GB) · invalid_settings.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "401": {
            "description": "unauthorized / token_expired — missing, malformed, invalid, revoked, or expired bearer token.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "402": {
            "description": "no_credits — renderSettings given but the workspace owner's credit balance cannot cover the render.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "403": {
            "description": "forbidden (account suspended, or bound workspace deleted / membership revoked) · insufficient_scope (token lacks the render scope).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "408": {
            "description": "timeout — HEAD probe or download timed out.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "422": {
            "description": "ingest_failed — downloaded file failed GLB structural validation.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "429": {
            "description": "rate_limited — over 20 ingest requests / 10 min per token (or over the global 300/min) · upload_limit — free-upload quota exhausted.",
            "headers": {
              "Retry-After": {
                "description": "Seconds until a retry is allowed (set for rate_limited).",
                "schema": {
                  "type": "integer"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "500": {
            "description": "ingest_failed — unclassified failure while downloading or registering the file.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/models/{modelId}": {
      "get": {
        "operationId": "getModel",
        "summary": "Get model detail with animation/mesh/material inventory and all its renders",
        "description": "Returns the model (visible for status active or archived) including `animations` ([{name, duration, channels}], seconds), `meshNames` and `materialNames` (null when the model predates inventory extraction), and all of its renders. Requires the `read` scope.",
        "tags": [
          "Models"
        ],
        "parameters": [
          {
            "name": "modelId",
            "in": "path",
            "required": true,
            "description": "Model ID (12-char opaque string).",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Model detail with renders.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data"
                  ],
                  "properties": {
                    "data": {
                      "$ref": "#/components/schemas/Model"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "unauthorized / token_expired — missing, malformed, invalid, revoked, or expired bearer token.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "403": {
            "description": "forbidden (account suspended, or bound workspace deleted / membership revoked) · insufficient_scope (token lacks the read scope).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "404": {
            "description": "model_not_found — no active or archived model with this ID in the workspace.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "429": {
            "description": "rate_limited — over 300 requests/min per token.",
            "headers": {
              "Retry-After": {
                "description": "Seconds until a retry is allowed.",
                "schema": {
                  "type": "integer"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "500": {
            "description": "internal.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        }
      },
      "patch": {
        "operationId": "updateModel",
        "summary": "Partial metadata update: only fields present in the body change",
        "description": "Updates title, sku, and/or tags. An omitted field is never touched; passing null or \"\" explicitly clears title/sku. A body with none of the three fields returns 400 bad_request. Requires the `render` scope.",
        "tags": [
          "Models"
        ],
        "parameters": [
          {
            "name": "modelId",
            "in": "path",
            "required": true,
            "description": "Model ID (12-char opaque string).",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ModelUpdateRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Updated metadata.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data"
                  ],
                  "properties": {
                    "data": {
                      "type": "object",
                      "required": [
                        "id",
                        "title",
                        "sku",
                        "tags"
                      ],
                      "properties": {
                        "id": {
                          "type": "string"
                        },
                        "title": {
                          "type": [
                            "string",
                            "null"
                          ]
                        },
                        "sku": {
                          "type": [
                            "string",
                            "null"
                          ]
                        },
                        "tags": {
                          "type": "array",
                          "items": {
                            "type": "string"
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "bad_request — no updatable fields provided (title, sku, tags), or tags is not an array.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "401": {
            "description": "unauthorized / token_expired — missing, malformed, invalid, revoked, or expired bearer token.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "403": {
            "description": "forbidden (account suspended, or bound workspace deleted / membership revoked) · insufficient_scope (token lacks the render scope).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "404": {
            "description": "model_not_found — no active or archived model with this ID in the workspace.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "429": {
            "description": "rate_limited — over 300 requests/min per token.",
            "headers": {
              "Retry-After": {
                "description": "Seconds until a retry is allowed.",
                "schema": {
                  "type": "integer"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "500": {
            "description": "internal.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        }
      },
      "delete": {
        "operationId": "deleteModel",
        "summary": "Soft-delete model; existing renders stay accessible",
        "description": "Sets status to `deleted` (works on active or archived models). Existing renders remain accessible until independently deleted. No refunds. Requires the `render` scope.",
        "tags": [
          "Models"
        ],
        "parameters": [
          {
            "name": "modelId",
            "in": "path",
            "required": true,
            "description": "Model ID (12-char opaque string).",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Model soft-deleted.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data"
                  ],
                  "properties": {
                    "data": {
                      "type": "object",
                      "required": [
                        "id",
                        "deleted"
                      ],
                      "properties": {
                        "id": {
                          "type": "string"
                        },
                        "deleted": {
                          "type": "boolean",
                          "const": true
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "unauthorized / token_expired — missing, malformed, invalid, revoked, or expired bearer token.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "403": {
            "description": "forbidden (account suspended, or bound workspace deleted / membership revoked) · insufficient_scope (token lacks the render scope).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "404": {
            "description": "model_not_found — no active or archived model with this ID in the workspace.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "429": {
            "description": "rate_limited — over 300 requests/min per token.",
            "headers": {
              "Retry-After": {
                "description": "Seconds until a retry is allowed.",
                "schema": {
                  "type": "integer"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "500": {
            "description": "internal.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/models/{modelId}/archive": {
      "patch": {
        "operationId": "archiveModel",
        "summary": "Archive model (active → archived; hidden from the default list)",
        "description": "Archived models are hidden from the default list; enumerate with `GET /models?status=archived` (or `status=any`). Detail, metadata edits, file download, and delete keep working on archived models. Only models in status `active` can be archived. Requires the `render` scope.",
        "tags": [
          "Models"
        ],
        "parameters": [
          {
            "name": "modelId",
            "in": "path",
            "required": true,
            "description": "Model ID (12-char opaque string).",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Model archived.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data"
                  ],
                  "properties": {
                    "data": {
                      "type": "object",
                      "required": [
                        "id",
                        "archived"
                      ],
                      "properties": {
                        "id": {
                          "type": "string"
                        },
                        "archived": {
                          "type": "boolean",
                          "const": true
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "unauthorized / token_expired — missing, malformed, invalid, revoked, or expired bearer token.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "403": {
            "description": "forbidden (account suspended, or bound workspace deleted / membership revoked) · insufficient_scope (token lacks the render scope).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "404": {
            "description": "model_not_found — no model with this ID in the workspace, or the model is not in status active (already archived / deleted).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "429": {
            "description": "rate_limited — over 300 requests/min per token.",
            "headers": {
              "Retry-After": {
                "description": "Seconds until a retry is allowed.",
                "schema": {
                  "type": "integer"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "500": {
            "description": "internal.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/models/{modelId}/unarchive": {
      "patch": {
        "operationId": "unarchiveModel",
        "summary": "Restore an archived model to active",
        "description": "Only models in status `archived` can be unarchived. Requires the `render` scope.",
        "tags": [
          "Models"
        ],
        "parameters": [
          {
            "name": "modelId",
            "in": "path",
            "required": true,
            "description": "Model ID (12-char opaque string).",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Model restored to active.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data"
                  ],
                  "properties": {
                    "data": {
                      "type": "object",
                      "required": [
                        "id",
                        "archived"
                      ],
                      "properties": {
                        "id": {
                          "type": "string"
                        },
                        "archived": {
                          "type": "boolean",
                          "const": false
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "unauthorized / token_expired — missing, malformed, invalid, revoked, or expired bearer token.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "403": {
            "description": "forbidden (account suspended, or bound workspace deleted / membership revoked) · insufficient_scope (token lacks the render scope).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "404": {
            "description": "model_not_found — no model with this ID in the workspace, or the model is not in status archived.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "429": {
            "description": "rate_limited — over 300 requests/min per token.",
            "headers": {
              "Retry-After": {
                "description": "Seconds until a retry is allowed.",
                "schema": {
                  "type": "integer"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "500": {
            "description": "internal.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/models/{modelId}/file-url": {
      "get": {
        "operationId": "getModelFileUrl",
        "summary": "Presigned direct-from-bucket GET URL for the original GLB",
        "description": "Returns a short-lived URL that downloads the GLB directly from object storage, taking the API out of the data path. The URL is an unauthenticated capability link until it expires — treat it as a secret and keep `expires` short. Download filename is the model's originalName. Requires the `read` scope.",
        "tags": [
          "Models"
        ],
        "parameters": [
          {
            "name": "modelId",
            "in": "path",
            "required": true,
            "description": "Model ID (12-char opaque string).",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "expires",
            "in": "query",
            "description": "Link lifetime in seconds — default 3600, clamped to 60–604800 (7 days).",
            "schema": {
              "type": "integer",
              "default": 3600,
              "minimum": 60,
              "maximum": 604800
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Presigned download URL.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data"
                  ],
                  "properties": {
                    "data": {
                      "type": "object",
                      "required": [
                        "url",
                        "expiresIn"
                      ],
                      "properties": {
                        "url": {
                          "type": "string",
                          "format": "uri",
                          "description": "Unauthenticated capability GET URL — treat as a secret."
                        },
                        "expiresIn": {
                          "type": "integer",
                          "description": "Effective link lifetime in seconds (after clamping)."
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "unauthorized / token_expired — missing, malformed, invalid, revoked, or expired bearer token.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "403": {
            "description": "forbidden (account suspended, or bound workspace deleted / membership revoked) · insufficient_scope (token lacks the read scope).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "404": {
            "description": "model_not_found — no active or archived model with this ID in the workspace.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "429": {
            "description": "rate_limited — over 300 requests/min per token.",
            "headers": {
              "Retry-After": {
                "description": "Seconds until a retry is allowed.",
                "schema": {
                  "type": "integer"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "500": {
            "description": "internal.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/models/{modelId}/file": {
      "get": {
        "operationId": "downloadModelFile",
        "summary": "Stream/download the original GLB",
        "description": "Streams the stored GLB (raw bytes, no envelope) with `Content-Disposition: attachment` (filename = originalName). Requires the `read` scope.",
        "tags": [
          "Models"
        ],
        "parameters": [
          {
            "name": "modelId",
            "in": "path",
            "required": true,
            "description": "Model ID (12-char opaque string).",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "GLB file bytes.",
            "headers": {
              "Content-Disposition": {
                "description": "attachment; filename=\"<originalName>\"",
                "schema": {
                  "type": "string"
                }
              },
              "Content-Length": {
                "description": "File size in bytes, when known.",
                "schema": {
                  "type": "integer"
                }
              }
            },
            "content": {
              "model/gltf-binary": {
                "schema": {
                  "type": "string",
                  "format": "binary"
                }
              }
            }
          },
          "401": {
            "description": "unauthorized / token_expired — missing, malformed, invalid, revoked, or expired bearer token.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "403": {
            "description": "forbidden (account suspended, or bound workspace deleted / membership revoked) · insufficient_scope (token lacks the read scope).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "404": {
            "description": "model_not_found — no active or archived model with this ID in the workspace · file_not_found — file missing in storage.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "429": {
            "description": "rate_limited — over 300 requests/min per token.",
            "headers": {
              "Retry-After": {
                "description": "Seconds until a retry is allowed.",
                "schema": {
                  "type": "integer"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "500": {
            "description": "internal.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/models/create-upload-url": {
      "post": {
        "operationId": "createModelUploadUrl",
        "summary": "Mint a presigned staging PUT URL + ticket for large/resumable GLB uploads",
        "description": "Returns a presigned PUT URL to a staging area plus a ticket. PUT the raw GLB bytes to the URL, then register the model with POST /models/finalize. The free-upload quota is gated up front. Abandoned staging objects are garbage-collected after ~48 h — do not rely on staging as storage. Requires the `render` scope.",
        "tags": [
          "Models"
        ],
        "parameters": [
          {
            "name": "expires",
            "in": "query",
            "description": "PUT URL lifetime in seconds (also accepted in the request body) — default 3600, clamped to 60–604800 (7 days).",
            "schema": {
              "type": "integer",
              "default": 3600,
              "minimum": 60,
              "maximum": 604800
            }
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "expires": {
                    "type": "integer",
                    "default": 3600,
                    "minimum": 60,
                    "maximum": 604800,
                    "description": "PUT URL lifetime in seconds (alternative to the query parameter)."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Presigned staging PUT URL and ticket.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data"
                  ],
                  "properties": {
                    "data": {
                      "$ref": "#/components/schemas/ModelStagedUploadTicket"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "unauthorized / token_expired — missing, malformed, invalid, revoked, or expired bearer token.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "403": {
            "description": "forbidden (account suspended, or bound workspace deleted / membership revoked) · insufficient_scope (token lacks the render scope).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "429": {
            "description": "upload_limit — free-upload quota exhausted (purchase render credits to continue) · rate_limited — over 300 requests/min per token.",
            "headers": {
              "Retry-After": {
                "description": "Seconds until a retry is allowed (set for rate_limited).",
                "schema": {
                  "type": "integer"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "500": {
            "description": "internal.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/models/finalize": {
      "post": {
        "operationId": "finalizeModelUpload",
        "summary": "Register a staged presigned-PUT upload; same pipeline and response as POST /models",
        "description": "Pairs with POST /models/create-upload-url. Runs the exact same pipeline as the multipart upload (GLB validation, SHA-256 dedup with duplicate:true reuse, free system preview or billed render per renderSettings) and responds identically. Staged uploads are GLB-only; oversized staged objects are deleted at finalize. Requires the `render` scope.",
        "tags": [
          "Models"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ModelFinalizeRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Duplicate — SHA-256 matched an existing ACTIVE model in the workspace (archived/deleted models are not matched); reused with `duplicate: true` (new billed render with renderSettings, else the existing system preview — a new free one is created if the model has none).",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data"
                  ],
                  "properties": {
                    "data": {
                      "$ref": "#/components/schemas/ModelUploadResult"
                    }
                  }
                }
              }
            }
          },
          "201": {
            "description": "Model registered and render queued (billed if renderSettings were given, else free system preview).",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data"
                  ],
                  "properties": {
                    "data": {
                      "$ref": "#/components/schemas/ModelUploadResult"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "bad_request (missing or malformed ticket) · invalid_format (filename must end .glb — staged uploads are GLB-only) · invalid_settings · file_too_large (staged object >1 GB — deleted) · upload_failed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "401": {
            "description": "unauthorized / token_expired — missing, malformed, invalid, revoked, or expired bearer token.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "402": {
            "description": "no_credits — renderSettings given but the workspace owner's credit balance cannot cover the render.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "403": {
            "description": "forbidden (account suspended, or bound workspace deleted / membership revoked) · insufficient_scope (token lacks the render scope).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "404": {
            "description": "not_found — no staged object for this ticket (the PUT never happened, or the presigned URL expired unused).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "422": {
            "description": "upload_failed — staged file failed GLB structural validation.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "429": {
            "description": "upload_limit — free-upload quota exhausted · rate_limited — over 300 requests/min per token.",
            "headers": {
              "Retry-After": {
                "description": "Seconds until a retry is allowed (set for rate_limited).",
                "schema": {
                  "type": "integer"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "500": {
            "description": "upload_failed — unclassified failure while finalizing the staged upload.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/models/check-hash": {
      "post": {
        "operationId": "checkModelHash",
        "summary": "Dedup pre-flight: does this SHA-256 already exist in the workspace?",
        "description": "Compute the SHA-256 of your file locally and call this before uploading to avoid duplicate transfers. Only matches an existing ACTIVE model in the workspace (archived/deleted models are not matched — their hashes return exists: false). Requires the `read` scope.",
        "tags": [
          "Models"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "sha256"
                ],
                "properties": {
                  "sha256": {
                    "type": "string",
                    "description": "Hex-encoded SHA-256 of the file."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Dedup check result.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data"
                  ],
                  "properties": {
                    "data": {
                      "type": "object",
                      "required": [
                        "exists",
                        "modelId"
                      ],
                      "properties": {
                        "exists": {
                          "type": "boolean"
                        },
                        "modelId": {
                          "type": [
                            "string",
                            "null"
                          ],
                          "description": "ID of the existing model, or null when no match."
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "sha256_required — sha256 missing from the body.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "401": {
            "description": "unauthorized / token_expired — missing, malformed, invalid, revoked, or expired bearer token.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "403": {
            "description": "forbidden (account suspended, or bound workspace deleted / membership revoked) · insufficient_scope (token lacks the read scope).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "429": {
            "description": "rate_limited — over 300 requests/min per token.",
            "headers": {
              "Retry-After": {
                "description": "Seconds until a retry is allowed.",
                "schema": {
                  "type": "integer"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "500": {
            "description": "internal.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/renders": {
      "post": {
        "operationId": "createRender",
        "summary": "Create a render for an existing model (queues job, charges credits).",
        "description": "Requires `render` scope. The render starts as `pending`; poll `GET /renders/{renderId}` (or `GET /renders?updatedSince=`) until `done` or `error`. Video cost = tier rate × length in seconds (`quality` defaults to `standard`, 4 credits/sec — a default 6 s render with no `quality` set costs 24 credits; pass `share` for the cheapest tier). If the paid balance is insufficient but the legacy free pool can cover it, the render is funded from the free pool and flagged `isPreview: true` (watermarked) unless `strictCredits: true` forces `402 no_credits`. Independently of the Idempotency-Key header, an identical resubmit of the same model + settings within a few seconds is deduplicated automatically.",
        "tags": [
          "Renders"
        ],
        "parameters": [
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": false,
            "schema": {
              "type": "string",
              "maxLength": 80
            },
            "description": "Stable client key per submit intent (any string; values longer than 80 chars are truncated, not rejected). If a render with the same key and byte-identical `renderSettings` was created within the last 30 minutes and hasn't errored, the request replays: HTTP 200 with the original render and `deduped: true`, nothing charged. The same key with different settings creates (and charges) a new render."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/RenderCreateRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Replay — the original render is returned with `deduped: true` (Idempotency-Key replay, or the automatic dedupe of an identical model + settings resubmit within a few seconds); nothing is charged.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data"
                  ],
                  "properties": {
                    "data": {
                      "allOf": [
                        {
                          "$ref": "#/components/schemas/RenderCreateResult"
                        },
                        {
                          "type": "object",
                          "required": [
                            "deduped"
                          ]
                        }
                      ]
                    }
                  }
                }
              }
            }
          },
          "201": {
            "description": "Render created and queued (`status: pending`).",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data"
                  ],
                  "properties": {
                    "data": {
                      "$ref": "#/components/schemas/RenderCreateResult"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "`model_id_required` (missing `modelId`) or `invalid_settings` (renderSettings failed validation; message has detail).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "401": {
            "description": "`unauthorized` (missing/invalid/revoked token) or `token_expired`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "402": {
            "description": "`no_credits` — insufficient credits (always when `strictCredits: true`; otherwise only when the free pool can't cover it either).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "403": {
            "description": "`insufficient_scope` (token lacks `render` scope) or `forbidden` (account suspended; workspace membership revoked).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "404": {
            "description": "`model_not_found`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "422": {
            "description": "`invalid_length` — video length outside the allowed range for the tier (>0 up to 120 s; `pro` up to 60 s). Note: an out-of-range `length` key in `renderSettings` is normally rejected earlier as 400 `invalid_settings`; 422 fires when the duration arrives via the legacy `videoLength` alias.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "429": {
            "description": "`rate_limited` — 300 requests/min per token account.",
            "headers": {
              "Retry-After": {
                "description": "Seconds to wait before retrying.",
                "schema": {
                  "type": "integer"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "500": {
            "description": "`internal`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        }
      },
      "get": {
        "operationId": "listRenders",
        "summary": "List renders (offset or cursor pagination; updatedSince delta polling).",
        "description": "Requires `read` scope. Two pagination modes: offset (default; `meta` = `{total, offset, limit}`) or keyset cursor (pass `cursor=` — empty for page 1, then `meta.nextCursor` — `meta` becomes `{limit, nextCursor}`; walk until `nextCursor` is null; O(page) at any depth, no COUNT). `updatedSince` filters on the row's modified timestamp — render status flips bump it, making it ideal for completion polling across a big batch. Rows carry the same field set as `GET /renders/{renderId}`.",
        "tags": [
          "Renders"
        ],
        "parameters": [
          {
            "name": "model",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "Filter by model ID."
          },
          {
            "name": "status",
            "in": "query",
            "required": false,
            "schema": {
              "$ref": "#/components/schemas/RenderStatus"
            },
            "description": "Filter by exact render status."
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "default": 50,
              "minimum": 1,
              "maximum": 100
            },
            "description": "Page size. Values above 100 are clamped to 100."
          },
          {
            "name": "offset",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "default": 0,
              "minimum": 0
            },
            "description": "Pagination offset (offset mode; ignored in cursor mode)."
          },
          {
            "name": "cursor",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "Keyset pagination: empty string for page 1, then the previous response's `meta.nextCursor`. Opaque — don't parse. Invalid cursor → 400 `bad_request`."
          },
          {
            "name": "updatedSince",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "format": "date-time"
            },
            "description": "ISO 8601 — only renders modified at/after this instant (status changes update it). Composes with either pagination mode."
          }
        ],
        "responses": {
          "200": {
            "description": "Render list. `meta` is offset-shaped by default, cursor-shaped when `cursor` was passed.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data",
                    "meta"
                  ],
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Render"
                      }
                    },
                    "meta": {
                      "oneOf": [
                        {
                          "$ref": "#/components/schemas/ListMeta"
                        },
                        {
                          "$ref": "#/components/schemas/ListMetaCursor"
                        }
                      ]
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "`bad_request` — invalid `cursor` or invalid `updatedSince` (use ISO 8601).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "401": {
            "description": "`unauthorized` or `token_expired`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "403": {
            "description": "`insufficient_scope` (token lacks `read` scope) or `forbidden`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "429": {
            "description": "`rate_limited` — 300 requests/min per token account.",
            "headers": {
              "Retry-After": {
                "description": "Seconds to wait before retrying.",
                "schema": {
                  "type": "integer"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "500": {
            "description": "`internal`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/renders/batch": {
      "post": {
        "operationId": "createRenderBatch",
        "summary": "Create up to 100 renders atomically (all credits deducted or the whole batch fails).",
        "description": "Requires `render` scope. Each item takes a `modelId` and optional `renderSettings` (same validator as `POST /renders`). Credits are deducted atomically — insufficient credits fail the entire batch with `402 no_credits` (a single `strictCredits: true` item that would fall to the free pool also fails the whole batch). Renders for the same model are grouped so the worker renders shared frames once. `Idempotency-Key`: one key covers the whole batch; on a retried call each item that matches a prior row (byte-identical settings, within 30 min, not errored) comes back with `deduped: true` instead of being re-charged. Independently of the Idempotency-Key header, an identical resubmit of the same model + settings within a few seconds is deduplicated automatically — applied per item, so a re-POSTed identical batch returns items with `deduped: true` even with no key sent. The batch response is always 201, replayed or not.",
        "tags": [
          "Renders"
        ],
        "parameters": [
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": false,
            "schema": {
              "type": "string",
              "maxLength": 80
            },
            "description": "Stable client key (values longer than 80 chars are truncated). One key covers the whole batch; each item replays against its own prior row."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/BatchRenderRequest"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Renders created and queued. Items that replayed an existing render (via Idempotency-Key or the automatic dedupe) carry `deduped: true`.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data"
                  ],
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/BatchRenderResult"
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "`renders_required` (missing/empty array), `too_many` (over 100 items), `model_id_required` (item missing `modelId`), or `invalid_settings`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "401": {
            "description": "`unauthorized` or `token_expired`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "402": {
            "description": "`no_credits` — the whole batch fails atomically.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "403": {
            "description": "`insufficient_scope` (token lacks `render` scope) or `forbidden`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "404": {
            "description": "`model_not_found`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "422": {
            "description": "`invalid_length` — video length outside the allowed range for the tier (>0 up to 120 s; `pro` up to 60 s). Note: an out-of-range `length` key in `renderSettings` is normally rejected earlier as 400 `invalid_settings`; 422 fires when the duration arrives via the legacy `videoLength` alias.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "429": {
            "description": "`rate_limited` — 300 requests/min per token account.",
            "headers": {
              "Retry-After": {
                "description": "Seconds to wait before retrying.",
                "schema": {
                  "type": "integer"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "500": {
            "description": "`internal`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/renders/{renderId}": {
      "get": {
        "operationId": "getRender",
        "summary": "Get render status and detail.",
        "description": "Requires `read` scope. Same field set as List Renders rows. Statuses progress pending → poster-processing → video-processing → done (poster-done is a recovery state); any status can transition to `error`; `failed` is a dismissed error. Branch on `failureCode`, not on message text.",
        "tags": [
          "Renders"
        ],
        "parameters": [
          {
            "name": "renderId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Render ID."
          }
        ],
        "responses": {
          "200": {
            "description": "Render detail.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data"
                  ],
                  "properties": {
                    "data": {
                      "$ref": "#/components/schemas/Render"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "`unauthorized` or `token_expired`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "403": {
            "description": "`insufficient_scope` (token lacks `read` scope) or `forbidden`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "404": {
            "description": "`render_not_found`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "429": {
            "description": "`rate_limited` — 300 requests/min per token account.",
            "headers": {
              "Retry-After": {
                "description": "Seconds to wait before retrying.",
                "schema": {
                  "type": "integer"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "500": {
            "description": "`internal`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        }
      },
      "delete": {
        "operationId": "deleteRender",
        "summary": "Permanently delete a terminal render (done/error/failed) and its files.",
        "description": "Requires `render` scope. Deletes the DB row and both storage prefixes (video/poster/thumb and, for dataset renders, the dataset ZIP). Only terminal statuses (`done`, `error`, `failed`) can be deleted; in-flight statuses (`pending`, `poster-processing`, `poster-done`, `video-processing`) return `409 render_in_flight` — cancel a queued render instead (which refunds). Deleting never refunds.",
        "tags": [
          "Renders"
        ],
        "parameters": [
          {
            "name": "renderId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Render ID."
          }
        ],
        "responses": {
          "200": {
            "description": "Render and files deleted.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data"
                  ],
                  "properties": {
                    "data": {
                      "type": "object",
                      "required": [
                        "id",
                        "deleted"
                      ],
                      "properties": {
                        "id": {
                          "type": "string",
                          "description": "Deleted render ID."
                        },
                        "deleted": {
                          "type": "boolean",
                          "const": true
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "`unauthorized` or `token_expired`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "403": {
            "description": "`insufficient_scope` (token lacks `render` scope) or `forbidden`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "404": {
            "description": "`render_not_found`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "409": {
            "description": "`render_in_flight` — render is queued or in progress; cancel it instead (`POST /renders/{renderId}/cancel`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "429": {
            "description": "`rate_limited` — 300 requests/min per token account.",
            "headers": {
              "Retry-After": {
                "description": "Seconds to wait before retrying.",
                "schema": {
                  "type": "integer"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "500": {
            "description": "`internal`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/renders/{renderId}/dismiss": {
      "patch": {
        "operationId": "dismissRender",
        "summary": "Acknowledge an errored render (status error → failed).",
        "description": "Requires `render` scope. Only rows currently in status `error` can be dismissed; anything else (including an unknown ID) returns `404 render_not_found`.",
        "tags": [
          "Renders"
        ],
        "parameters": [
          {
            "name": "renderId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Render ID."
          }
        ],
        "responses": {
          "200": {
            "description": "Render dismissed.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data"
                  ],
                  "properties": {
                    "data": {
                      "type": "object",
                      "required": [
                        "id",
                        "dismissed"
                      ],
                      "properties": {
                        "id": {
                          "type": "string",
                          "description": "Dismissed render ID."
                        },
                        "dismissed": {
                          "type": "boolean",
                          "const": true
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "`unauthorized` or `token_expired`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "403": {
            "description": "`insufficient_scope` (token lacks `render` scope) or `forbidden`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "404": {
            "description": "`render_not_found` — render not found or not in `error` state.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "429": {
            "description": "`rate_limited` — 300 requests/min per token account.",
            "headers": {
              "Retry-After": {
                "description": "Seconds to wait before retrying.",
                "schema": {
                  "type": "integer"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "500": {
            "description": "`internal`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/renders/{renderId}/cancel": {
      "post": {
        "operationId": "cancelRender",
        "summary": "Cancel a still-pending render and refund its credits.",
        "description": "Requires `render` scope. Only renders still queued (`pending`) can be cancelled; the charged credits are refunded to the pool they came from and stored files are cleaned up. Once a worker has claimed the job (any status past `pending`), cancellation returns `409 render_started` — the render will complete or error normally.",
        "tags": [
          "Renders"
        ],
        "parameters": [
          {
            "name": "renderId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Render ID."
          }
        ],
        "responses": {
          "200": {
            "description": "Render cancelled and refunded.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data"
                  ],
                  "properties": {
                    "data": {
                      "type": "object",
                      "required": [
                        "id",
                        "cancelled",
                        "refundedCredits"
                      ],
                      "properties": {
                        "id": {
                          "type": "string",
                          "description": "Cancelled render ID."
                        },
                        "cancelled": {
                          "type": "boolean",
                          "const": true
                        },
                        "refundedCredits": {
                          "type": "integer",
                          "description": "Credits refunded to the pool they were charged from."
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "`unauthorized` or `token_expired`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "403": {
            "description": "`insufficient_scope` (token lacks `render` scope) or `forbidden`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "404": {
            "description": "`render_not_found`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "409": {
            "description": "`render_started` — the worker already claimed the job; cancellation is no longer possible.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "429": {
            "description": "`rate_limited` — 300 requests/min per token account.",
            "headers": {
              "Retry-After": {
                "description": "Seconds to wait before retrying.",
                "schema": {
                  "type": "integer"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "500": {
            "description": "`internal`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/renders/{renderId}/video": {
      "get": {
        "operationId": "getRenderVideo",
        "summary": "Stream the rendered video (Range supported).",
        "description": "Requires `read` scope. Raw bytes, no envelope. `video/mp4` for share/standard/4k, `video/quicktime` (.mov, ProRes 4444 with alpha) for `pro`. Full 200 responses are cached immutable for 1 year; 206 partial responses are `Cache-Control: no-store`.",
        "tags": [
          "Assets"
        ],
        "parameters": [
          {
            "name": "renderId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Render ID."
          },
          {
            "name": "quality",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "enum": [
                "web"
              ]
            },
            "description": "`web` — the lighter web-optimized MP4 when available (see `hasWebVariant`); falls back to the main video. Omit for the main video."
          },
          {
            "name": "Range",
            "in": "header",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "Byte-range request (e.g. `bytes=0-1023`) → 206 partial content.",
            "example": "bytes=0-1023"
          }
        ],
        "responses": {
          "200": {
            "description": "Full video file.",
            "headers": {
              "Accept-Ranges": {
                "description": "`bytes` (MP4 responses).",
                "schema": {
                  "type": "string"
                }
              },
              "Cache-Control": {
                "description": "`public, max-age=31536000, immutable`.",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "video/mp4": {},
              "video/quicktime": {}
            }
          },
          "206": {
            "description": "Partial content (Range request). `Cache-Control: no-store`.",
            "headers": {
              "Content-Range": {
                "description": "Byte range served.",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "video/mp4": {},
              "video/quicktime": {}
            }
          },
          "401": {
            "description": "`unauthorized` or `token_expired`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "403": {
            "description": "`insufficient_scope` (token lacks `read` scope) or `forbidden`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "404": {
            "description": "`video_not_found` — render unknown or video not yet available.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "429": {
            "description": "`rate_limited` — 300 requests/min per token account.",
            "headers": {
              "Retry-After": {
                "description": "Seconds to wait before retrying.",
                "schema": {
                  "type": "integer"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "500": {
            "description": "`internal`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/renders/{renderId}/poster": {
      "get": {
        "operationId": "getRenderPoster",
        "summary": "Poster image (first frame); thumbnail-size by default.",
        "description": "Requires `read` scope. Raw PNG bytes, no envelope, cached immutable. Pass `?quality=full` for the full-resolution PNG at the video's native dimensions (falls back to the thumbnail poster if the full-res variant is absent).",
        "tags": [
          "Assets"
        ],
        "parameters": [
          {
            "name": "renderId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Render ID."
          },
          {
            "name": "quality",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "enum": [
                "full"
              ]
            },
            "description": "`full` — full-resolution PNG (falls back to the thumbnail poster if unavailable). Omit for the small thumbnail poster."
          }
        ],
        "responses": {
          "200": {
            "description": "Poster PNG.",
            "headers": {
              "Cache-Control": {
                "description": "`public, max-age=31536000, immutable`.",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "image/png": {}
            }
          },
          "401": {
            "description": "`unauthorized` or `token_expired`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "403": {
            "description": "`insufficient_scope` (token lacks `read` scope) or `forbidden`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "404": {
            "description": "`poster_not_found` — render unknown or poster not yet available.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "429": {
            "description": "`rate_limited` — 300 requests/min per token account.",
            "headers": {
              "Retry-After": {
                "description": "Seconds to wait before retrying.",
                "schema": {
                  "type": "integer"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "500": {
            "description": "`internal`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/renders/{renderId}/thumb": {
      "get": {
        "operationId": "getRenderThumb",
        "summary": "Quarter-resolution MP4 thumbnail video (Range supported).",
        "description": "Requires `read` scope. Raw bytes, no envelope. Full 200 responses are cached immutable; 206 partial responses are `Cache-Control: no-store`.",
        "tags": [
          "Assets"
        ],
        "parameters": [
          {
            "name": "renderId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Render ID."
          },
          {
            "name": "Range",
            "in": "header",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "Byte-range request → 206 partial content.",
            "example": "bytes=0-1023"
          }
        ],
        "responses": {
          "200": {
            "description": "Full thumbnail video.",
            "headers": {
              "Accept-Ranges": {
                "description": "`bytes`.",
                "schema": {
                  "type": "string"
                }
              },
              "Cache-Control": {
                "description": "`public, max-age=31536000, immutable`.",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "video/mp4": {}
            }
          },
          "206": {
            "description": "Partial content (Range request). `Cache-Control: no-store`.",
            "headers": {
              "Content-Range": {
                "description": "Byte range served.",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "video/mp4": {}
            }
          },
          "401": {
            "description": "`unauthorized` or `token_expired`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "403": {
            "description": "`insufficient_scope` (token lacks `read` scope) or `forbidden`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "404": {
            "description": "`thumb_not_found` — render unknown or thumbnail not yet available.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "429": {
            "description": "`rate_limited` — 300 requests/min per token account.",
            "headers": {
              "Retry-After": {
                "description": "Seconds to wait before retrying.",
                "schema": {
                  "type": "integer"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "500": {
            "description": "`internal`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/renders/{renderId}/download-url": {
      "get": {
        "operationId": "getRenderDownloadUrl",
        "summary": "Presigned direct-from-bucket download URL for one render asset.",
        "description": "Requires `read` scope. Returns a short-lived URL that downloads the asset directly from object storage, taking the API out of the data path — the efficient way to pull large files (multi-GB ProRes) and to parallelize or resume downloads with your own tooling. The URL is an unauthenticated capability link until it expires — treat it as a secret and keep `expires` short. `asset=dataset` requires the render to be `done`. HLS manifests and the on-the-fly `/bundle` ZIP cannot be presigned — use their streaming endpoints. The download filename is derived from the model's title/original name.",
        "tags": [
          "Assets"
        ],
        "parameters": [
          {
            "name": "renderId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Render ID."
          },
          {
            "name": "asset",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "enum": [
                "video",
                "web",
                "poster",
                "full_poster",
                "thumb",
                "dataset"
              ],
              "default": "video"
            },
            "description": "Which asset to presign. Unknown values → 400 `bad_request`."
          },
          {
            "name": "expires",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "default": 3600,
              "minimum": 60,
              "maximum": 604800
            },
            "description": "Link lifetime in seconds — default 3600; values below 60 or above 604800 (7 days) are clamped, not rejected. `0` or a non-numeric value falls back to the 3600 default (the response's `expiresIn` may misreport these edges)."
          }
        ],
        "responses": {
          "200": {
            "description": "Presigned URL minted.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data"
                  ],
                  "properties": {
                    "data": {
                      "$ref": "#/components/schemas/RenderDownloadUrl"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "`bad_request` — unknown `asset` value.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "401": {
            "description": "`unauthorized` or `token_expired`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "403": {
            "description": "`insufficient_scope` (token lacks `read` scope) or `forbidden`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "404": {
            "description": "`render_not_found`; `dataset_not_found` (`asset=dataset` on a render that isn't `done`); or `asset_not_found` (the requested variant was not produced for this render).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "429": {
            "description": "`rate_limited` — 300 requests/min per token account.",
            "headers": {
              "Retry-After": {
                "description": "Seconds to wait before retrying.",
                "schema": {
                  "type": "integer"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "500": {
            "description": "`internal`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/renders/{renderId}/dataset": {
      "get": {
        "operationId": "getRenderDataset",
        "summary": "Stream the vision-training dataset ZIP (dataset renders, status done).",
        "description": "Requires `read` scope. Raw bytes, no envelope; `Content-Disposition: attachment; filename=\"{name}_dataset.zip\"`. Only for completed dataset renders (`output: \"dataset\"`, status `done`). ZIP contents: images/ (RGB PNGs), depth/ (8-bit), depth_16bit/, normals/, masks/, transforms.json (instant-ngp/nerfstudio format), overview.webp contact sheet.",
        "tags": [
          "Assets"
        ],
        "parameters": [
          {
            "name": "renderId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Render ID."
          }
        ],
        "responses": {
          "200": {
            "description": "Dataset ZIP.",
            "headers": {
              "Content-Disposition": {
                "description": "`attachment; filename=\"{name}_dataset.zip\"`.",
                "schema": {
                  "type": "string"
                }
              },
              "Cache-Control": {
                "description": "`public, max-age=31536000, immutable`.",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/zip": {}
            }
          },
          "401": {
            "description": "`unauthorized` or `token_expired`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "403": {
            "description": "`insufficient_scope` (token lacks `read` scope) or `forbidden`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "404": {
            "description": "`dataset_not_found` — render unknown, not `done`, not a dataset render, or the ZIP is missing from storage.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "429": {
            "description": "`rate_limited` — 300 requests/min per token account.",
            "headers": {
              "Retry-After": {
                "description": "Seconds to wait before retrying.",
                "schema": {
                  "type": "integer"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "500": {
            "description": "`internal` (only if streaming had not started).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/renders/{renderId}/hls": {
      "get": {
        "operationId": "getRenderHlsInfo",
        "summary": "HLS packaging state and playback entry-point URLs.",
        "description": "Requires `read` scope. Branch on `status` before hitting the manifest endpoint: `masterUrl` and `thumbnailsVttUrl` are null until `status` is `ready` (they point at `/v1/renders/{renderId}/hls/master.m3u8` and `.../thumbs.vtt`).",
        "tags": [
          "Assets"
        ],
        "parameters": [
          {
            "name": "renderId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Render ID."
          }
        ],
        "responses": {
          "200": {
            "description": "HLS state.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data"
                  ],
                  "properties": {
                    "data": {
                      "$ref": "#/components/schemas/RenderHlsInfo"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "`unauthorized` or `token_expired`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "403": {
            "description": "`insufficient_scope` (token lacks `read` scope) or `forbidden`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "404": {
            "description": "`not_found` — render not found.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "429": {
            "description": "`rate_limited` — 300 requests/min per token account.",
            "headers": {
              "Retry-After": {
                "description": "Seconds to wait before retrying.",
                "schema": {
                  "type": "integer"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "500": {
            "description": "`internal`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/renders/{renderId}/hls/{assetPath}": {
      "get": {
        "operationId": "getRenderHlsAsset",
        "summary": "Stream one HLS artifact (manifest, fMP4 segment, VTT track, or sprite JPEG).",
        "description": "Requires `read` scope. Bearer-authed like every /v1 route — for server-side consumption or proxying, not direct browser playback (no browser CORS). Requires the render's HLS status to be `ready`. Extension whitelist: `.m3u8` (manifest, `Cache-Control: max-age=60`, no Range), `.m4s` (fMP4 segment), `.mp4`, `.vtt` (thumbnail track), `.jpg` (sprite); anything else, `..`, or a leading `/` → 404 `not_found`. Range is honored for `.m4s`/`.mp4` only. Segments and static artifacts are cached immutable for 1 year; 206 partial responses are `Cache-Control: no-store`.",
        "tags": [
          "Assets"
        ],
        "parameters": [
          {
            "name": "renderId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Render ID."
          },
          {
            "name": "assetPath",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Path relative to the HLS package root (e.g. `master.m3u8`, `thumbs.vtt`, a variant playlist, or a segment). May contain `/` separators — the server route is a wildcard."
          },
          {
            "name": "Range",
            "in": "header",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "Byte-range request; honored for `.m4s`/`.mp4` artifacts only.",
            "example": "bytes=0-1023"
          }
        ],
        "responses": {
          "200": {
            "description": "Artifact bytes; Content-Type per extension (`application/vnd.apple.mpegurl`, `video/iso.segment`, `video/mp4`, `text/vtt`, `image/jpeg`).",
            "headers": {
              "Cache-Control": {
                "description": "Manifests `public, max-age=60`; segments/VTT/JPEG `public, max-age=31536000, immutable`.",
                "schema": {
                  "type": "string"
                }
              },
              "Accept-Ranges": {
                "description": "`bytes` (on `.m4s`/`.mp4`).",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/vnd.apple.mpegurl": {},
              "video/iso.segment": {},
              "video/mp4": {},
              "text/vtt": {},
              "image/jpeg": {}
            }
          },
          "206": {
            "description": "Partial content (Range request on `.m4s`/`.mp4`). `Cache-Control: no-store`.",
            "headers": {
              "Content-Range": {
                "description": "Byte range served.",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "video/iso.segment": {},
              "video/mp4": {}
            }
          },
          "401": {
            "description": "`unauthorized` or `token_expired`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "403": {
            "description": "`insufficient_scope` (token lacks `read` scope) or `forbidden`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "404": {
            "description": "`not_found` — invalid path/extension, render unknown, HLS not `ready`, or artifact missing from storage.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "429": {
            "description": "`rate_limited` — 300 requests/min per token account.",
            "headers": {
              "Retry-After": {
                "description": "Seconds to wait before retrying.",
                "schema": {
                  "type": "integer"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "500": {
            "description": "`internal` (only if streaming had not started).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/renders/{renderId}/bundle": {
      "get": {
        "operationId": "getRenderBundle",
        "summary": "All render assets as one streaming ZIP.",
        "description": "Requires `read` scope; render must be `done`. The ZIP is streamed directly with no server-side buffering (store mode, no compression) — suitable for multi-GB ProRes files. Contains: `video.mp4` or `video.mov` (pro), `web.mp4`, `poster.png` (full-resolution preferred), `thumb.mp4`; assets missing from storage are skipped. Errors after streaming starts truncate the stream (best-effort).",
        "tags": [
          "Assets"
        ],
        "parameters": [
          {
            "name": "renderId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Render ID."
          }
        ],
        "responses": {
          "200": {
            "description": "Streaming ZIP of all render assets.",
            "headers": {
              "Content-Disposition": {
                "description": "`attachment; filename=\"{name}_{renderId}.zip\"`.",
                "schema": {
                  "type": "string"
                }
              },
              "Cache-Control": {
                "description": "`public, max-age=31536000, immutable` — renders never change after completion.",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/zip": {}
            }
          },
          "401": {
            "description": "`unauthorized` or `token_expired`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "403": {
            "description": "`insufficient_scope` (token lacks `read` scope) or `forbidden`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "404": {
            "description": "`render_not_found` — render unknown or not `done`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "429": {
            "description": "`rate_limited` — 300 requests/min per token account.",
            "headers": {
              "Retry-After": {
                "description": "Seconds to wait before retrying.",
                "schema": {
                  "type": "integer"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "500": {
            "description": "`internal` (only if streaming had not started).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/assets/textures": {
      "post": {
        "operationId": "createTexture",
        "summary": "Upload a texture asset (base-color map) for materialEdits.<name>.mapAssetId",
        "description": "Mints a workspace texture asset. Pass the returned id as `materialEdits.<materialName>.mapAssetId` in render settings to replace that material's base-color map. Content-sha dedup per uploader within the workspace (scoped to user + workspace): re-POSTing identical bytes *from the same token user* returns HTTP 200 with `deduped: true` and the existing id; the same bytes uploaded by a different workspace member create a new asset. Quota: 100 textures / 500 MB per account. Failure mode: a wrong or missing `mapAssetId` silently degrades — the render keeps the model's original map. Requires `render` scope.",
        "tags": [
          "Assets"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "multipart/form-data": {
              "schema": {
                "type": "object",
                "required": [
                  "file"
                ],
                "properties": {
                  "file": {
                    "type": "string",
                    "format": "binary",
                    "description": "PNG, JPEG, or WebP image, max 25 MB."
                  },
                  "name": {
                    "type": "string",
                    "maxLength": 100,
                    "default": "Texture",
                    "description": "Display name (server truncates to 100 chars)."
                  }
                }
              },
              "encoding": {
                "file": {
                  "contentType": "image/png, image/jpeg, image/webp"
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Dedup replay — identical bytes were already uploaded by this token's user in this workspace; returns the existing asset with `deduped: true`. Nothing new is stored. (Identical bytes uploaded by a different workspace member create a new asset instead — 201.)",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data"
                  ],
                  "properties": {
                    "data": {
                      "$ref": "#/components/schemas/TextureCreateResult"
                    }
                  }
                }
              }
            }
          },
          "201": {
            "description": "Texture created.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data"
                  ],
                  "properties": {
                    "data": {
                      "$ref": "#/components/schemas/TextureCreateResult"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "`no_file` (no file part), `bad_request` (invalid file type — must be image/png, image/jpeg, image/webp — or unprocessable upload), or `file_too_large` (image exceeds the 25 MB limit).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "401": {
            "description": "`unauthorized` (missing/invalid token) or `token_expired`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "403": {
            "description": "`insufficient_scope` (token lacks `render` scope) or `forbidden` (suspended account / revoked workspace membership).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "429": {
            "description": "`limit_reached` (texture quota: 100 textures or 500 MB per account) or `rate_limited` (300 req/min per token account; Retry-After header set).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "500": {
            "description": "`internal`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        }
      },
      "get": {
        "operationId": "listTextures",
        "summary": "List the workspace's texture assets",
        "description": "Returns all texture assets in the token's workspace, newest first. Unpaginated. Requires `read` scope.",
        "tags": [
          "Assets"
        ],
        "responses": {
          "200": {
            "description": "Texture asset list.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data"
                  ],
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/TextureAsset"
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "`unauthorized` or `token_expired`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "403": {
            "description": "`insufficient_scope` (token lacks `read` scope) or `forbidden`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "429": {
            "description": "`rate_limited` (300 req/min per token account; Retry-After header set).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "500": {
            "description": "`internal`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/assets/textures/{assetId}": {
      "delete": {
        "operationId": "deleteTexture",
        "summary": "Delete an owned texture asset",
        "description": "Deletes the texture asset row and its stored object (best-effort). Owner-only — deleting another user's texture returns 403. Requires `render` scope.",
        "tags": [
          "Assets"
        ],
        "parameters": [
          {
            "name": "assetId",
            "in": "path",
            "required": true,
            "description": "Texture asset id (12-char opaque string).",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Texture deleted.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data"
                  ],
                  "properties": {
                    "data": {
                      "$ref": "#/components/schemas/AssetDeleteResult"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "`unauthorized` or `token_expired`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "403": {
            "description": "`forbidden` (cannot delete others' textures) or `insufficient_scope` (token lacks `render` scope).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "404": {
            "description": "`not_found` — no texture asset with this id.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "429": {
            "description": "`rate_limited` (300 req/min per token account; Retry-After header set).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "500": {
            "description": "`internal`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/assets/envs": {
      "post": {
        "operationId": "createEnvironment",
        "summary": "Upload a custom HDRI environment (pre-encoded gainmap triple) for envId",
        "description": "Mints a custom HDRI environment referenced by `envId` in render settings. Upload the pre-encoded gainmap triple exactly as gainmap-js `encodeAndCompress` produces it: `sdr` (WebP) + `gainmap` (WebP) + `metadata` (JSON), plus an optional small `thumb` WebP. There is no server-side HDR-to-gainmap encoder — encode with gainmap-js or export from Studio. Parts are validated on upload (WebP magic bytes, JSON metadata). Caps: 48 MB per part; quota 20 environments / 200 MB per account. Failure mode (asymmetric with textures): a bad `envId` hard-fails the render — it errors with `failureCode: \"env_download_failed\"` and the credits are refunded. Requires `render` scope.",
        "tags": [
          "Assets"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "multipart/form-data": {
              "schema": {
                "type": "object",
                "required": [
                  "sdr",
                  "gainmap",
                  "metadata"
                ],
                "properties": {
                  "sdr": {
                    "type": "string",
                    "format": "binary",
                    "description": "Base SDR image, WebP (magic-checked). Max 48 MB."
                  },
                  "gainmap": {
                    "type": "string",
                    "format": "binary",
                    "description": "Gain map image, WebP (magic-checked). Max 48 MB."
                  },
                  "metadata": {
                    "type": "string",
                    "format": "binary",
                    "description": "gainmap-js encodeAndCompress metadata, valid JSON. Max 48 MB."
                  },
                  "thumb": {
                    "type": "string",
                    "format": "binary",
                    "description": "Optional small thumbnail, WebP. Max 48 MB."
                  },
                  "name": {
                    "type": "string",
                    "maxLength": 100,
                    "default": "Environment",
                    "description": "Display name (server truncates to 100 chars)."
                  },
                  "width": {
                    "type": "integer",
                    "description": "Source environment width in pixels (optional)."
                  },
                  "height": {
                    "type": "integer",
                    "description": "Source environment height in pixels (optional)."
                  }
                }
              },
              "encoding": {
                "sdr": {
                  "contentType": "image/webp"
                },
                "gainmap": {
                  "contentType": "image/webp"
                },
                "metadata": {
                  "contentType": "application/json"
                },
                "thumb": {
                  "contentType": "image/webp"
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Environment created. Pass `id` as `envId` in render settings (with `envMode`/`envRotation`/`envExposure` etc.).",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data"
                  ],
                  "properties": {
                    "data": {
                      "$ref": "#/components/schemas/EnvironmentCreateResult"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "`bad_request` (missing sdr/gainmap/metadata part, sdr or gainmap not WebP, metadata not valid JSON, or unprocessable upload) or `file_too_large` (a part exceeds the 48 MB per-part limit).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "401": {
            "description": "`unauthorized` or `token_expired`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "403": {
            "description": "`insufficient_scope` (token lacks `render` scope) or `forbidden`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "429": {
            "description": "`limit_reached` (environment quota: 20 environments or 200 MB per account) or `rate_limited` (300 req/min per token account; Retry-After header set).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "500": {
            "description": "`internal`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        }
      },
      "get": {
        "operationId": "listEnvironments",
        "summary": "List the workspace's environments",
        "description": "Returns all environment assets in the token's workspace, newest first. Unpaginated. Requires `read` scope.",
        "tags": [
          "Assets"
        ],
        "responses": {
          "200": {
            "description": "Environment asset list.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data"
                  ],
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/EnvironmentAsset"
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "`unauthorized` or `token_expired`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "403": {
            "description": "`insufficient_scope` (token lacks `read` scope) or `forbidden`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "429": {
            "description": "`rate_limited` (300 req/min per token account; Retry-After header set).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "500": {
            "description": "`internal`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/assets/envs/{assetId}": {
      "delete": {
        "operationId": "deleteEnvironment",
        "summary": "Delete an owned environment",
        "description": "Deletes the environment asset row and its whole storage prefix — the gainmap triple plus the derived thumb (best-effort). Owner-only — deleting another user's environment returns 403. Requires `render` scope.",
        "tags": [
          "Assets"
        ],
        "parameters": [
          {
            "name": "assetId",
            "in": "path",
            "required": true,
            "description": "Environment asset id (12-char opaque string).",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Environment deleted.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data"
                  ],
                  "properties": {
                    "data": {
                      "$ref": "#/components/schemas/AssetDeleteResult"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "`unauthorized` or `token_expired`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "403": {
            "description": "`forbidden` (cannot delete others' environments) or `insufficient_scope` (token lacks `render` scope).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "404": {
            "description": "`not_found` — no environment asset with this id.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "429": {
            "description": "`rate_limited` (300 req/min per token account; Retry-After header set).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "500": {
            "description": "`internal`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/overlays": {
      "post": {
        "operationId": "uploadOverlay",
        "summary": "Upload a reusable overlay image (background, foreground, decal, overlay frame, or texture) referenced by id from render settings.",
        "description": "Multipart upload of a PNG, JPEG, or WebP image (max 25 MB). The returned `id` is referenced from `renderSettings` (`bgImageId` for backgrounds, `decals[].imageAssetId` for decals, `materialEdits.<name>.mapAssetId` for textures). Thumbnail and pixel dimensions are extracted best-effort. Assets are workspace-stamped so they resolve in team renders. Requires the `render` scope. Each account can hold up to 100 overlay assets. Returns 200 (not 201).",
        "tags": [
          "Overlays"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "multipart/form-data": {
              "schema": {
                "type": "object",
                "required": [
                  "file"
                ],
                "properties": {
                  "file": {
                    "type": "string",
                    "format": "binary",
                    "description": "Image file (PNG, JPEG, WebP; max 25 MB)."
                  },
                  "layer": {
                    "$ref": "#/components/schemas/OverlayLayer",
                    "default": "background",
                    "description": "Asset layer. `background`, `foreground`, `decal` (referenced from `decals[].imageAssetId`), `overlay` (full-frame composite on top of the finished render), or `texture` (referenced from `materialEdits.<name>.mapAssetId`). Any other value is rejected with 400 `bad_request`."
                  },
                  "name": {
                    "type": "string",
                    "maxLength": 100,
                    "description": "Display name (max 100 chars; longer values are truncated)."
                  },
                  "description": {
                    "type": "string",
                    "maxLength": 500,
                    "description": "Short description (max 500 chars; longer values are truncated)."
                  },
                  "alignment": {
                    "$ref": "#/components/schemas/OverlayAlignment",
                    "default": "left",
                    "description": "Horizontal anchor when the image is used as a background (see renderSettings.bgAlign). Unrecognized values silently fall back to `left`."
                  },
                  "opacity": {
                    "type": "number",
                    "maximum": 1,
                    "default": 1,
                    "description": "Overlay opacity, 0–1 (background/foreground compositing). Boundary quirk: a value of exactly 0 is treated as unset and becomes 1 (send a negative value to clamp to 0); non-numeric values fall back to 1; other out-of-range values are clamped to 0–1."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Overlay asset created.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data"
                  ],
                  "properties": {
                    "data": {
                      "$ref": "#/components/schemas/OverlayUploadResult"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "`bad_request` (no file uploaded, invalid file type — must be image/png, image/jpeg, image/webp — invalid layer, or unprocessable upload) or `file_too_large` (image exceeds the 25 MB limit).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "401": {
            "description": "`unauthorized` (missing/invalid/revoked token) or `token_expired`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "403": {
            "description": "`insufficient_scope` (token lacks the `render` scope) or `forbidden` (account suspended, or bound workspace deleted/membership revoked).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "429": {
            "description": "`limit_reached` (100 overlay assets per account — delete unused assets to free slots) or `rate_limited` (300 requests/min per token account; includes Retry-After).",
            "headers": {
              "Retry-After": {
                "description": "Seconds until the rate-limit window resets (present for `rate_limited`).",
                "schema": {
                  "type": "integer"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "500": {
            "description": "`internal` — unexpected server error.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        }
      },
      "get": {
        "operationId": "listOverlays",
        "summary": "List the workspace's overlay assets plus all published system overlays.",
        "description": "Returns your workspace's overlay assets and all published system overlays (system overlays first). Unpaginated. Requires the `read` scope.",
        "tags": [
          "Overlays"
        ],
        "parameters": [
          {
            "name": "layer",
            "in": "query",
            "required": false,
            "description": "Filter by layer. Invalid values return 400 `bad_request`. Omit to list all layers.",
            "schema": {
              "$ref": "#/components/schemas/OverlayLayer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Overlay assets, system-published first, then own assets newest first.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data"
                  ],
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/OverlayAsset"
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "`bad_request` — invalid `layer` filter.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "401": {
            "description": "`unauthorized` (missing/invalid/revoked token) or `token_expired`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "403": {
            "description": "`insufficient_scope` (token lacks the `read` scope) or `forbidden` (account suspended, or bound workspace deleted/membership revoked).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "429": {
            "description": "`rate_limited` — 300 requests/min per token account; includes Retry-After.",
            "headers": {
              "Retry-After": {
                "description": "Seconds until the rate-limit window resets.",
                "schema": {
                  "type": "integer"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "500": {
            "description": "`internal` — unexpected server error.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/overlays/resolve": {
      "post": {
        "operationId": "resolveOverlay",
        "summary": "SHA-256 pre-flight: check whether this exact source content already exists among the assets uploaded by this token's user in the workspace.",
        "description": "Pre-flight for bulk pipelines: returns the existing asset id if this exact source content already exists as an asset of that layer among the assets uploaded by this token's user in the workspace — skip the upload and reuse the id instead of re-uploading into the 100-asset cap. A teammate's identical asset in the same workspace does not match. Note on sha coverage: Studio uploads and /v1/assets/textures record a content sha; assets uploaded via POST /v1/overlays currently do not, and will never match resolve. Requires the `read` scope.",
        "tags": [
          "Overlays"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/OverlayResolveRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Resolution result. `exists: true` with the asset `id` if a matching asset was found; otherwise `exists: false` and `id: null`.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data"
                  ],
                  "properties": {
                    "data": {
                      "$ref": "#/components/schemas/OverlayResolveResult"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "`bad_request` — `sha256` is not a 64-char hex SHA-256, or invalid `layer`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "401": {
            "description": "`unauthorized` (missing/invalid/revoked token) or `token_expired`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "403": {
            "description": "`insufficient_scope` (token lacks the `read` scope) or `forbidden` (account suspended, or bound workspace deleted/membership revoked).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "429": {
            "description": "`rate_limited` — 300 requests/min per token account; includes Retry-After.",
            "headers": {
              "Retry-After": {
                "description": "Seconds until the rate-limit window resets.",
                "schema": {
                  "type": "integer"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "500": {
            "description": "`internal` — unexpected server error.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/overlays/{overlayId}": {
      "delete": {
        "operationId": "deleteOverlay",
        "summary": "Delete an overlay you own; removes the image, thumbnail, and preview from storage.",
        "description": "Owner-only (checked against the uploading user, not just the workspace). System overlays and other users' assets cannot be deleted. Requires the `render` scope.",
        "tags": [
          "Overlays"
        ],
        "parameters": [
          {
            "name": "overlayId",
            "in": "path",
            "required": true,
            "description": "Overlay asset id (12-char opaque string).",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Overlay deleted.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data"
                  ],
                  "properties": {
                    "data": {
                      "type": "object",
                      "required": [
                        "ok"
                      ],
                      "properties": {
                        "ok": {
                          "type": "boolean",
                          "const": true
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "`unauthorized` (missing/invalid/revoked token) or `token_expired`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "403": {
            "description": "`forbidden` (not the owner — includes system overlays; or account suspended / bound workspace revoked) or `insufficient_scope` (token lacks the `render` scope).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "404": {
            "description": "`not_found` — overlay does not exist.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "429": {
            "description": "`rate_limited` — 300 requests/min per token account; includes Retry-After.",
            "headers": {
              "Retry-After": {
                "description": "Seconds until the rate-limit window resets.",
                "schema": {
                  "type": "integer"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "500": {
            "description": "`internal` — unexpected server error.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/health": {
      "get": {
        "operationId": "getHealth",
        "summary": "Health check: database connectivity probe (SELECT now()).",
        "description": "No authentication required. Returns database connectivity status.",
        "tags": [
          "Account"
        ],
        "security": [],
        "responses": {
          "200": {
            "description": "Service healthy; database reachable.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data"
                  ],
                  "properties": {
                    "data": {
                      "$ref": "#/components/schemas/HealthStatus"
                    }
                  }
                }
              }
            }
          },
          "500": {
            "description": "`unhealthy` — database probe failed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/account": {
      "get": {
        "operationId": "getAccount",
        "summary": "Authenticated account info: credit balance breakdown, token scopes, bound workspace.",
        "description": "Returns the account info and credit balances of the pool that funds this token's renders. Workspace-bound tokens report the workspace owner's pool (owner-pays); `workspace` names the binding (null = personal key). No scope required beyond a valid token.",
        "tags": [
          "Account"
        ],
        "responses": {
          "200": {
            "description": "Account info and credit balances.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data"
                  ],
                  "properties": {
                    "data": {
                      "$ref": "#/components/schemas/Account"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "`unauthorized` — missing, invalid, or revoked token; `token_expired` — token has expired.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "403": {
            "description": "`forbidden` — account suspended, or the token's bound workspace was deleted or membership revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "404": {
            "description": "`not_found` — account not found.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "429": {
            "description": "`rate_limited` — 300 requests/minute per token account exceeded. Honor the Retry-After header.",
            "headers": {
              "Retry-After": {
                "description": "Seconds to wait before retrying.",
                "schema": {
                  "type": "integer"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "500": {
            "description": "`internal` — internal error.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/quote": {
      "post": {
        "operationId": "createQuote",
        "summary": "Estimate credit cost for up to 100 renders before committing.",
        "description": "Each item accepts the cost-relevant render fields. Video cost = tier rate × ceil(seconds), minimum 1 s; datasets are flat per set. `creditsAvailable` is the paid + free balance of the pool that would fund the renders (workspace owner's pool for bound tokens). No scope required beyond a valid token.",
        "tags": [
          "Account"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/QuoteRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Credit estimate with per-item breakdown.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data"
                  ],
                  "properties": {
                    "data": {
                      "$ref": "#/components/schemas/QuoteResponse"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "`renders_required` — missing or empty renders array; `too_many` — more than 100 items per quote.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "401": {
            "description": "`unauthorized` — missing, invalid, or revoked token; `token_expired` — token has expired.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "403": {
            "description": "`forbidden` — account suspended, or the token's bound workspace was deleted or membership revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "429": {
            "description": "`rate_limited` — 300 requests/minute per token account exceeded. Honor the Retry-After header.",
            "headers": {
              "Retry-After": {
                "description": "Seconds to wait before retrying.",
                "schema": {
                  "type": "integer"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "500": {
            "description": "`internal` — internal error.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/products": {
      "get": {
        "operationId": "listProducts",
        "summary": "List active purchasable credit packs.",
        "description": "Purchasing itself happens in the web app (Stripe checkout), not over the API. Ordered by sort order. No scope required beyond a valid token.",
        "tags": [
          "Account"
        ],
        "responses": {
          "200": {
            "description": "Active credit packs.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data"
                  ],
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Product"
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "`unauthorized` — missing, invalid, or revoked token; `token_expired` — token has expired.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "403": {
            "description": "`forbidden` — account suspended, or the token's bound workspace was deleted or membership revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "429": {
            "description": "`rate_limited` — 300 requests/minute per token account exceeded. Honor the Retry-After header.",
            "headers": {
              "Retry-After": {
                "description": "Seconds to wait before retrying.",
                "schema": {
                  "type": "integer"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "500": {
            "description": "`internal` — internal error.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/purchases": {
      "get": {
        "operationId": "listPurchases",
        "summary": "Credit ledger (purchases, unlocks, renders, refunds, grants), newest first.",
        "description": "Offset pagination only (no cursor mode). Legacy tokens see the caller's full user ledger; workspace-bound tokens see only rows attributable to the bound workspace (render debits/refunds). No scope required beyond a valid token.",
        "tags": [
          "Account"
        ],
        "parameters": [
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Page size. Max 100, default 50.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 50
            }
          },
          {
            "name": "offset",
            "in": "query",
            "required": false,
            "description": "Pagination offset.",
            "schema": {
              "type": "integer",
              "minimum": 0,
              "default": 0
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Ledger page, newest first.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data",
                    "meta"
                  ],
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Purchase"
                      }
                    },
                    "meta": {
                      "$ref": "#/components/schemas/ListMeta"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "`unauthorized` — missing, invalid, or revoked token; `token_expired` — token has expired.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "403": {
            "description": "`forbidden` — account suspended, or the token's bound workspace was deleted or membership revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "429": {
            "description": "`rate_limited` — 300 requests/minute per token account exceeded. Honor the Retry-After header.",
            "headers": {
              "Retry-After": {
                "description": "Seconds to wait before retrying.",
                "schema": {
                  "type": "integer"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "500": {
            "description": "`internal` — internal error.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "bearerAuth": {
        "type": "http",
        "scheme": "bearer",
        "description": "API key with prefix dxgl_sk_ — create in the studio app (API panel). Keys carry scopes (read/render/...) and may be personal or workspace-bound."
      }
    },
    "schemas": {
      "RenderSettings": {
        "type": "object",
        "description": "Render configuration accepted by POST /v1/renders, POST /v1/renders/batch, POST /v1/models (as a JSON string form field), /v1/models/ingest, and /v1/models/finalize. All fields are optional; omitted fields use defaults. Invalid values are rejected with 400 invalid_settings (out-of-range length: 422 invalid_length). `preview` and `thumbOnly` are server-set and rejected if sent.",
        "properties": {
          "quality": {
            "type": "string",
            "enum": [
              "share",
              "standard",
              "4k",
              "pro"
            ],
            "default": "standard",
            "description": "Quality tier: share = Standard 960x540 H.264 (1 credit/sec), standard = HD 1920x1080 H.264 (4 credits/sec, the default when omitted), 4k = 3840x2160 H.264 (16 credits/sec), pro = ProRes 4444 4K with alpha (64 credits/sec). JSON enum values are frozen and differ from display labels."
          },
          "effect": {
            "type": "string",
            "enum": [
              "turntable",
              "hero-spin",
              "showcase",
              "zoom-orbit",
              "reveal",
              "keyframes",
              "dataset"
            ],
            "default": "turntable",
            "description": "Camera path: turntable (360 deg), hero-spin (fast spin to front), showcase (look-around), zoom-orbit (360 deg + zoom), reveal (scale-up entrance), keyframes (custom fly-through, see keyframes/camera), dataset (vision training; legacy alias for output: \"dataset\")."
          },
          "sweepAngle": {
            "type": "number",
            "minimum": 10,
            "maximum": 1080,
            "description": "Override sweep angle in degrees (hero-spin default 540, reveal default 180)."
          },
          "easeOutRatio": {
            "type": "number",
            "minimum": 0,
            "maximum": 1,
            "description": "Hero-spin deceleration fraction (default 0.6)."
          },
          "amplitude": {
            "type": "number",
            "minimum": 5,
            "maximum": 180,
            "description": "Showcase swing angle in degrees (default 60)."
          },
          "zoomStart": {
            "type": "number",
            "minimum": 0.1,
            "maximum": 3,
            "description": "Zoom-orbit starting radius multiplier (default 1.0)."
          },
          "zoomEnd": {
            "type": "number",
            "minimum": 0.1,
            "maximum": 3,
            "description": "Zoom-orbit ending radius multiplier (default 0.7)."
          },
          "scaleFrom": {
            "type": "number",
            "minimum": 0,
            "maximum": 1,
            "description": "Reveal starting model scale (default 0)."
          },
          "vignette": {
            "type": "number",
            "minimum": 0,
            "maximum": 1,
            "description": "Vignette intensity (0 = none, 1 = full)."
          },
          "aspect": {
            "type": "string",
            "enum": [
              "16:9",
              "1:1",
              "9:16"
            ],
            "default": "16:9",
            "description": "Video aspect ratio."
          },
          "bgColor": {
            "type": "string",
            "pattern": "^#[0-9a-fA-F]{6}$",
            "default": "#ffffff",
            "description": "Background color as a 6-digit hex including the # (e.g. \"#ff8800\"); ignored when bgImageId is set."
          },
          "bgImageId": {
            "type": "string",
            "minLength": 1,
            "description": "Overlay asset ID for a background image (overrides bgColor); upload via POST /v1/overlays."
          },
          "bgAlign": {
            "type": "string",
            "enum": [
              "left",
              "center",
              "right"
            ],
            "default": "left",
            "description": "Horizontal anchor for the background image: which width slice of the 16:9-canonical cover the output keeps (vertical is never cropped). Legacy 9-point values (top-left … bottom-right) are still accepted and collapse to their horizontal token."
          },
          "length": {
            "type": "number",
            "exclusiveMinimum": 0,
            "maximum": 120,
            "default": 6,
            "description": "Video duration in seconds (decimals OK, rounded to 0.1 s server-side); max 120 (ProRes 4444: 60; admin accounts uncapped). Credits = tier rate x seconds, 1-credit minimum."
          },
          "videoLength": {
            "type": "number",
            "exclusiveMinimum": 0,
            "maximum": 120,
            "description": "Legacy alias for length (honored when length is absent; same per-tier caps)."
          },
          "animation": {
            "type": "string",
            "minLength": 1,
            "maxLength": 120,
            "description": "Animation clip name to play (\"*\" = first clip); discover clips via GET /v1/models/:id -> animations."
          },
          "animationSpeed": {
            "type": "number",
            "minimum": -10,
            "maximum": 10,
            "not": {
              "const": 0
            },
            "default": 1,
            "description": "Animation playback speed multiplier, -10 to 10 and non-zero (negative plays the clip in reverse)."
          },
          "shadows": {
            "type": "boolean",
            "default": true,
            "description": "Enable ground shadows."
          },
          "reflector": {
            "type": "boolean",
            "default": false,
            "description": "Enable reflective ground plane."
          },
          "easing": {
            "type": "boolean",
            "default": true,
            "description": "Ease in/out on turntable rotation."
          },
          "strictCredits": {
            "type": "boolean",
            "default": false,
            "description": "Refuse the free-credit fallback: fail with 402 no_credits instead of funding from the free pool and flagging isPreview (recommended for unattended pipelines; one strict item fails a whole atomic batch)."
          },
          "rotateY": {
            "type": "number",
            "minimum": -180,
            "maximum": 180,
            "default": 0,
            "description": "Turntable camera starting angle in degrees; offsets where the orbit begins and does not rotate the model."
          },
          "tiltX": {
            "type": "number",
            "minimum": -180,
            "maximum": 180,
            "default": 0,
            "description": "Model pitch in degrees, applied in the yawed frame (after panX, before rollZ); bypasses shadow caching."
          },
          "panX": {
            "type": "number",
            "minimum": -180,
            "maximum": 180,
            "default": 0,
            "description": "Model yaw in degrees around the vertical axis; applied first, before tiltX and rollZ."
          },
          "rollZ": {
            "type": "number",
            "minimum": -180,
            "maximum": 180,
            "default": 0,
            "description": "Model roll around the resulting Z axis in degrees; applied last."
          },
          "scaleX": {
            "type": "number",
            "minimum": -20,
            "maximum": 20,
            "default": 1,
            "description": "Per-axis model scale X: magnitude must be 0.01-20 (near-zero rejected); negative mirrors the axis (chirality fix); a uniform scale on all three axes is a visual no-op (the frame refits)."
          },
          "scaleY": {
            "type": "number",
            "minimum": -20,
            "maximum": 20,
            "default": 1,
            "description": "Per-axis model scale Y (up/down); same semantics and bounds as scaleX."
          },
          "scaleZ": {
            "type": "number",
            "minimum": -20,
            "maximum": 20,
            "default": 1,
            "description": "Per-axis model scale Z (front/back); same semantics and bounds as scaleX."
          },
          "translateX": {
            "type": "number",
            "minimum": -20,
            "maximum": 20,
            "default": 0,
            "description": "Model position offset X (slide left/right) in normalized units (the model spans ~2)."
          },
          "translateY": {
            "type": "number",
            "minimum": -20,
            "maximum": 20,
            "default": 0,
            "description": "Model position offset Y; a positive value lifts the model off the ground so it floats above its shadow."
          },
          "translateZ": {
            "type": "number",
            "minimum": -20,
            "maximum": 20,
            "default": 0,
            "description": "Model position offset Z (slide front/back) in normalized units."
          },
          "zoom": {
            "type": "number",
            "minimum": 0.5,
            "maximum": 2,
            "default": 1,
            "description": "Camera zoom factor: above 1 moves the camera closer, below 1 farther (distance = base / zoom)."
          },
          "output": {
            "type": "string",
            "enum": [
              "video",
              "dataset"
            ],
            "default": "video",
            "description": "Output kind; dataset renders also take datasetQuality and coverage and cost a flat amount per set."
          },
          "datasetQuality": {
            "type": "string",
            "enum": [
              "100x800",
              "196x1024",
              "400x2048"
            ],
            "description": "Dataset tier: 100 views at 800x800 (40 credits), 196 at 1024x1024 (160), or 400 at 2048x2048 (640); flat cost per set."
          },
          "coverage": {
            "type": "string",
            "enum": [
              "hemisphere",
              "sphere"
            ],
            "default": "hemisphere",
            "description": "Dataset camera viewpoint coverage: upper hemisphere (default) or full sphere."
          },
          "filter": {
            "type": "string",
            "enum": [
              "none",
              "sepia",
              "bw",
              "vivid",
              "warm",
              "cool",
              "faded",
              "noir",
              "grain",
              "toon"
            ],
            "description": "Post-process color filter preset."
          },
          "bloomEnabled": {
            "type": "boolean",
            "description": "Enable bloom (cinematic glow on bright highlights)."
          },
          "bloomStrength": {
            "type": "number",
            "minimum": 0,
            "maximum": 2,
            "description": "Bloom strength."
          },
          "dofEnabled": {
            "type": "boolean",
            "description": "Enable depth of field."
          },
          "dofStrength": {
            "type": "number",
            "minimum": 0,
            "maximum": 1,
            "description": "Depth-of-field amount."
          },
          "n8aoEnabled": {
            "type": "boolean",
            "description": "Enable N8AO screen-space ambient occlusion (share-tier renders trade CAS-downscale AA for AO + MSAA 8x)."
          },
          "n8aoIntensity": {
            "type": "number",
            "minimum": 0.5,
            "maximum": 32,
            "description": "N8AO ambient-occlusion intensity."
          },
          "swayEnabled": {
            "type": "boolean",
            "description": "Enable cloth-sway (garment inertia) vertex displacement with an appended settle tail."
          },
          "swayIntensity": {
            "type": "number",
            "minimum": 0,
            "maximum": 2,
            "description": "Cloth-sway intensity."
          },
          "shadowMode": {
            "type": "string",
            "enum": [
              "shader",
              "flat",
              "accumulated"
            ],
            "description": "Ground-shadow rendering mode."
          },
          "shadowDarkness": {
            "type": "number",
            "minimum": 0,
            "maximum": 5,
            "description": "Shadow darkness multiplier."
          },
          "shadowSoftness": {
            "type": "number",
            "minimum": 0,
            "maximum": 1,
            "description": "Normalized shadow softness (shader mode: blur passes; flat mode: PCF radius)."
          },
          "reflectorMix": {
            "type": "number",
            "minimum": 0,
            "maximum": 1,
            "description": "Reflector mirror opacity (strength)."
          },
          "reflectorFade": {
            "type": "number",
            "minimum": 0.05,
            "maximum": 5,
            "description": "Reflector fade distance as a multiple of the model's largest dimension (falloff)."
          },
          "reflectorDepthBlur": {
            "type": "number",
            "minimum": 0,
            "maximum": 10,
            "description": "Reflector height-driven gloss (depth blur)."
          },
          "envId": {
            "type": "string",
            "pattern": "^[A-Za-z0-9_-]{1,32}$",
            "description": "Custom HDRI environment asset id (mint via POST /v1/assets/envs); a bad id hard-fails the render with failureCode env_download_failed and a refund."
          },
          "envRotation": {
            "type": "number",
            "minimum": -180,
            "maximum": 180,
            "description": "Environment rotation in degrees."
          },
          "envExposure": {
            "type": "number",
            "minimum": 0.015625,
            "maximum": 4,
            "description": "Environment brightness multiplier on backdrop + IBL together (-6 to +2 EV; 1 = unchanged)."
          },
          "envMode": {
            "type": "string",
            "enum": [
              "grounded",
              "lighting",
              "floating"
            ],
            "description": "Environment mode: grounded = dome + IBL replacing the background (default when absent), lighting = IBL only keeping the chosen background, floating = full 360-degree surround sphere with the model in mid-air."
          },
          "envOpacity": {
            "type": "number",
            "minimum": 0,
            "maximum": 1,
            "description": "Visible-environment backdrop opacity (1 = opaque, the default); values below 1 render transparent so the chosen background shows through."
          },
          "envBlur": {
            "type": "number",
            "minimum": 0,
            "maximum": 1,
            "description": "Visible-environment backdrop blur (0 = sharp, the default); the IBL stays sharp."
          },
          "envScale": {
            "type": "number",
            "minimum": 0.25,
            "maximum": 4,
            "description": "Environment dome/sphere radius multiplier (visible mesh only; 1 = unscaled)."
          },
          "envTint": {
            "type": "string",
            "pattern": "^#[0-9a-fA-F]{6}$",
            "description": "Luma-preserving RGB environment tint as #rrggbb hex."
          },
          "envTintStrength": {
            "type": "number",
            "minimum": 0,
            "maximum": 1,
            "description": "Environment tint strength."
          },
          "spotlightIntensity": {
            "type": "number",
            "minimum": 0,
            "maximum": 32,
            "description": "Directional light-rig multiplier (key + fill + rim + bottom); 0 = off, 1 = renderer baseline."
          },
          "lightsIntensity": {
            "type": "number",
            "minimum": 0,
            "maximum": 32,
            "description": "Hemisphere (sky/ground) light multiplier; 0 = off, 1 = baseline."
          },
          "ambientIntensity": {
            "type": "number",
            "minimum": 0,
            "maximum": 16,
            "description": "Diffuse IBL (environment irradiance/fill) multiplier."
          },
          "glossIntensity": {
            "type": "number",
            "minimum": 0,
            "maximum": 16,
            "description": "Specular IBL (environment reflections/shine) multiplier."
          },
          "keyLightColor": {
            "type": "string",
            "pattern": "^#[0-9a-fA-F]{6}$",
            "description": "Key light color as #rrggbb hex."
          },
          "fillLightColor": {
            "type": "string",
            "pattern": "^#[0-9a-fA-F]{6}$",
            "description": "Fill light color as #rrggbb hex."
          },
          "hiddenMeshNames": {
            "type": "array",
            "maxItems": 512,
            "items": {
              "type": "string",
              "minLength": 1,
              "maxLength": 256
            },
            "description": "Runtime mesh names to hide for this render (exact, case-sensitive match against GET /v1/models/:id -> meshNames); unknown names are a silent no-op; hidden meshes are also excluded from shadows and reflections."
          },
          "bgGradient": {
            "$ref": "#/components/schemas/BgGradient"
          },
          "materialEdits": {
            "type": "object",
            "maxProperties": 256,
            "propertyNames": {
              "minLength": 1,
              "maxLength": 256
            },
            "additionalProperties": {
              "$ref": "#/components/schemas/MaterialEdit"
            },
            "description": "Per-material PBR overrides keyed by material name (GET /v1/models/:id -> materialNames, exact match); unknown names/fields are silent no-ops; max 256 keys, 32 KB serialized."
          },
          "decals": {
            "type": "array",
            "maxItems": 64,
            "items": {
              "$ref": "#/components/schemas/Decal"
            },
            "description": "Decal (sticker) placements projected onto the model."
          },
          "keyframes": {
            "type": "array",
            "minItems": 1,
            "maxItems": 64,
            "items": {
              "$ref": "#/components/schemas/Keyframe"
            },
            "description": "Custom camera fly-through keyframes (used with effect: \"keyframes\"); position and lookAt are Catmull-Rom-splined between keyframes."
          },
          "camera": {
            "$ref": "#/components/schemas/CameraOverride"
          },
          "startAngle": {
            "type": "number",
            "minimum": -1080,
            "maximum": 1080,
            "description": "Turntable sweep start-angle override in degrees (pose-relative offset)."
          },
          "endAngle": {
            "type": "number",
            "minimum": -1080,
            "maximum": 1080,
            "description": "Turntable sweep end-angle override in degrees (pose-relative offset)."
          },
          "startTilt": {
            "type": "number",
            "minimum": -89,
            "maximum": 89,
            "description": "Camera tilt at the start of the sweep, in degrees."
          },
          "endTilt": {
            "type": "number",
            "minimum": -89,
            "maximum": 89,
            "description": "Camera tilt at the end of the sweep, in degrees."
          },
          "startZoom": {
            "type": "number",
            "minimum": 0.3,
            "maximum": 3,
            "description": "Camera zoom multiplier at the start of the sweep."
          },
          "endZoom": {
            "type": "number",
            "minimum": 0.3,
            "maximum": 3,
            "description": "Camera zoom multiplier at the end of the sweep."
          },
          "startFov": {
            "type": "number",
            "minimum": 0.3,
            "maximum": 3,
            "description": "Field-of-view multiplier at the start of the sweep."
          },
          "endFov": {
            "type": "number",
            "minimum": 0.3,
            "maximum": 3,
            "description": "Field-of-view multiplier at the end of the sweep."
          },
          "easeInSeconds": {
            "type": "number",
            "minimum": 0,
            "maximum": 60,
            "description": "Ease-in duration in seconds."
          },
          "easeOutSeconds": {
            "type": "number",
            "minimum": 0,
            "maximum": 60,
            "description": "Ease-out duration in seconds."
          },
          "kfSegmentDuration": {
            "type": "number",
            "minimum": 0.1,
            "maximum": 30,
            "description": "Duration per keyframe segment, in seconds."
          },
          "loop": {
            "type": "boolean",
            "description": "Close the keyframe sequence into a seamless loop (implicit closing segment back to the first keyframe); only meaningful alongside keyframes."
          },
          "kfEase": {
            "type": "string",
            "enum": [
              "ease-out",
              "ease-in-out",
              "continuous"
            ],
            "description": "Legacy global keyframe easing; accepted but unused — use keyframes[].easing instead.",
            "deprecated": true
          },
          "overlayId": {
            "type": "string",
            "minLength": 1,
            "description": "Baked full-frame overlay asset id (layer \"overlay\"), composited top-left onto the finished render (accepted by the validator; not in the public docs table). Hard-fails like envId: an id that no longer resolves or a frame that cannot be fetched errors the render with failureCode overlay_missing / overlay_download_failed and refunds the credits."
          },
          "overlayAlign": {
            "type": "string",
            "enum": [
              "left",
              "center",
              "right"
            ],
            "description": "Horizontal alignment for the baked overlay frame crop (accepted by the validator; not in the public docs table)."
          },
          "overlayOpacity": {
            "type": "number",
            "minimum": 0,
            "maximum": 1,
            "description": "Opacity multiplied onto the baked overlay frame's alpha (accepted by the validator; not in the public docs table)."
          },
          "preview": {
            "type": "boolean",
            "readOnly": true,
            "description": "Server-stamped: true when the render was funded from the free-credit pool; rejected if sent — strip it before re-submitting settings echoed by GET /v1/renders."
          },
          "thumbOnly": {
            "type": "boolean",
            "readOnly": true,
            "description": "Server-set; rejected if sent."
          }
        }
      },
      "MaterialEdit": {
        "type": "object",
        "description": "Per-material PBR override; only fields present are applied (missing names/fields are silent no-ops). Physical-only fields (transmission, sheen, clearcoat, etc.) are silently skipped on non-Physical materials.",
        "properties": {
          "color": {
            "type": "string",
            "pattern": "^#[0-9a-fA-F]{6}$",
            "description": "Base color as 6-digit hex."
          },
          "roughness": {
            "type": "number",
            "minimum": 0,
            "maximum": 1,
            "description": "Surface roughness."
          },
          "metalness": {
            "type": "number",
            "minimum": 0,
            "maximum": 1,
            "description": "Metalness."
          },
          "opacity": {
            "type": "number",
            "minimum": 0,
            "maximum": 1,
            "description": "Material opacity."
          },
          "alphaMode": {
            "type": "string",
            "enum": [
              "opaque",
              "mask",
              "blend"
            ],
            "description": "Alpha handling mode."
          },
          "aoMapIntensity": {
            "type": "number",
            "minimum": 0,
            "maximum": 2,
            "description": "Ambient-occlusion map intensity."
          },
          "envMapIntensity": {
            "type": "number",
            "minimum": 0,
            "maximum": 4,
            "description": "Environment-map intensity."
          },
          "emissive": {
            "type": "string",
            "pattern": "^#[0-9a-fA-F]{6}$",
            "description": "Emissive color as 6-digit hex."
          },
          "emissiveIntensity": {
            "type": "number",
            "minimum": 0,
            "maximum": 16,
            "description": "Emissive intensity."
          },
          "side": {
            "type": "string",
            "enum": [
              "front",
              "back",
              "double"
            ],
            "description": "Which face sides to render."
          },
          "flatShading": {
            "type": "boolean",
            "description": "Enable flat shading."
          },
          "wireframe": {
            "type": "boolean",
            "description": "Render as wireframe."
          },
          "normalScale": {
            "type": "number",
            "minimum": 0,
            "maximum": 3,
            "description": "Normal-map scale."
          },
          "bumpScale": {
            "type": "number",
            "minimum": 0,
            "maximum": 1,
            "description": "Bump-map scale."
          },
          "displacementScale": {
            "type": "number",
            "minimum": 0,
            "maximum": 2,
            "description": "Displacement-map scale."
          },
          "textureMaps": {
            "type": "object",
            "properties": {
              "map": {
                "type": "boolean"
              },
              "normalMap": {
                "type": "boolean"
              },
              "roughnessMap": {
                "type": "boolean"
              },
              "metalnessMap": {
                "type": "boolean"
              },
              "aoMap": {
                "type": "boolean"
              },
              "emissiveMap": {
                "type": "boolean"
              }
            },
            "additionalProperties": false,
            "description": "Per-map toggles: false removes that texture map for the render; true is the GLB default (no-op — a removed map cannot be restored over the wire)."
          },
          "mapAssetId": {
            "type": "string",
            "pattern": "^[A-Za-z0-9_-]{1,64}$",
            "description": "Texture asset id (mint via POST /v1/assets/textures) replacing this material's base-color map; a wrong id silently keeps the original map."
          },
          "transmission": {
            "type": "number",
            "minimum": 0,
            "maximum": 1,
            "description": "Transmission (Physical materials only)."
          },
          "clearcoat": {
            "type": "number",
            "minimum": 0,
            "maximum": 1,
            "description": "Clearcoat amount (Physical only)."
          },
          "clearcoatRoughness": {
            "type": "number",
            "minimum": 0,
            "maximum": 1,
            "description": "Clearcoat roughness (Physical only)."
          },
          "sheen": {
            "type": "number",
            "minimum": 0,
            "maximum": 1,
            "description": "Sheen amount (Physical only)."
          },
          "sheenRoughness": {
            "type": "number",
            "minimum": 0,
            "maximum": 1,
            "description": "Sheen roughness (Physical only)."
          },
          "sheenColor": {
            "type": "string",
            "pattern": "^#[0-9a-fA-F]{6}$",
            "description": "Sheen color as 6-digit hex (Physical only)."
          },
          "iridescence": {
            "type": "number",
            "minimum": 0,
            "maximum": 1,
            "description": "Iridescence amount (Physical only)."
          },
          "iridescenceIOR": {
            "type": "number",
            "minimum": 1,
            "maximum": 3,
            "description": "Iridescence index of refraction (Physical only)."
          },
          "specularIntensity": {
            "type": "number",
            "minimum": 0,
            "maximum": 1,
            "description": "Specular intensity (Physical only)."
          },
          "specularColor": {
            "type": "string",
            "pattern": "^#[0-9a-fA-F]{6}$",
            "description": "Specular color as 6-digit hex (Physical only)."
          },
          "thickness": {
            "type": "number",
            "minimum": 0,
            "maximum": 10,
            "description": "Volume thickness (Physical only)."
          },
          "ior": {
            "type": "number",
            "minimum": 1,
            "maximum": 3,
            "description": "Index of refraction (Physical only)."
          },
          "attenuationColor": {
            "type": "string",
            "pattern": "^#[0-9a-fA-F]{6}$",
            "description": "Attenuation color as 6-digit hex (Physical only)."
          }
        }
      },
      "Decal": {
        "type": "object",
        "required": [
          "id",
          "imageAssetId",
          "position",
          "normal",
          "euler",
          "size"
        ],
        "description": "A decal (sticker) image projected onto the model; projected against every visible mesh whose bounding box intersects the projector cube.",
        "properties": {
          "id": {
            "type": "string",
            "minLength": 1,
            "maxLength": 32,
            "description": "Client-chosen decal identifier."
          },
          "imageAssetId": {
            "type": "string",
            "minLength": 1,
            "maxLength": 64,
            "description": "Overlay asset id of the decal image (layer \"decal\")."
          },
          "position": {
            "$ref": "#/components/schemas/Vec3",
            "description": "Projector position [x, y, z] in normalized model space."
          },
          "normal": {
            "$ref": "#/components/schemas/Vec3",
            "description": "Projection direction normal [x, y, z]."
          },
          "euler": {
            "$ref": "#/components/schemas/Vec3",
            "description": "Projector rotation [x, y, z] in radians."
          },
          "size": {
            "type": "number",
            "exclusiveMinimum": 0,
            "maximum": 100,
            "description": "Decal size."
          },
          "imageAspect": {
            "type": "number",
            "exclusiveMinimum": 0,
            "maximum": 100,
            "description": "Aspect ratio of the decal image."
          },
          "placementScale": {
            "$ref": "#/components/schemas/Vec3",
            "description": "Model scale [x, y, z] at stamp time (each axis magnitude 0.01-20): the decal bakes at this scale and the final model scale rides on top, so a non-uniform model scale stretches the sticker; omit to bake at the final scale (undistorted)."
          }
        }
      },
      "Keyframe": {
        "type": "object",
        "required": [
          "pose"
        ],
        "description": "One keyframe of a custom camera fly-through (effect: \"keyframes\").",
        "properties": {
          "pose": {
            "type": "object",
            "required": [
              "position",
              "lookAt",
              "fov"
            ],
            "description": "Absolute world-space camera pose; position must differ from lookAt (distance >= 0.0001).",
            "properties": {
              "position": {
                "$ref": "#/components/schemas/Vec3",
                "description": "Camera position [x, y, z]."
              },
              "lookAt": {
                "$ref": "#/components/schemas/Vec3",
                "description": "Camera look-at target [x, y, z]."
              },
              "fov": {
                "type": "number",
                "minimum": 10,
                "maximum": 130,
                "description": "Field of view in degrees."
              }
            }
          },
          "dwell": {
            "type": "boolean",
            "description": "Legacy dwell flag (older clients); superseded by easing: \"dwell\"."
          },
          "easing": {
            "type": "string",
            "enum": [
              "linear",
              "ease-in",
              "ease-out",
              "ease-in-out",
              "dwell"
            ],
            "description": "Per-keyframe easing of the time profile into this keyframe."
          }
        }
      },
      "CameraOverride": {
        "type": "object",
        "required": [
          "target",
          "radius",
          "theta",
          "phi",
          "fov"
        ],
        "description": "Explicit camera pose override: the renderer skips its perspective-fit auto-framer and matches Studio's normalization. All five fields are required when present.",
        "properties": {
          "target": {
            "$ref": "#/components/schemas/Vec3",
            "description": "Orbit target [x, y, z]."
          },
          "radius": {
            "type": "number",
            "exclusiveMinimum": 0,
            "maximum": 1000,
            "description": "Orbit radius."
          },
          "theta": {
            "type": "number",
            "description": "Azimuthal angle in radians."
          },
          "phi": {
            "type": "number",
            "exclusiveMinimum": 0,
            "exclusiveMaximum": 3.141592653589793,
            "description": "Polar angle in radians, in the open interval (0, pi)."
          },
          "fov": {
            "type": "number",
            "minimum": 10,
            "maximum": 130,
            "description": "Field of view in degrees."
          }
        }
      },
      "BgGradient": {
        "type": "object",
        "required": [
          "stops",
          "angle"
        ],
        "description": "Linear gradient background; stop positions are evenly distributed server-side.",
        "properties": {
          "stops": {
            "type": "array",
            "minItems": 2,
            "maxItems": 8,
            "items": {
              "type": "object",
              "required": [
                "color"
              ],
              "properties": {
                "color": {
                  "type": "string",
                  "pattern": "^#[0-9a-fA-F]{6}$",
                  "description": "Stop color as 6-digit hex."
                }
              }
            },
            "description": "2 to 8 gradient color stops."
          },
          "angle": {
            "type": "number",
            "minimum": 0,
            "exclusiveMaximum": 360,
            "description": "Gradient angle in degrees (0-359)."
          }
        }
      },
      "Vec3": {
        "type": "array",
        "minItems": 3,
        "maxItems": 3,
        "items": {
          "type": "number"
        },
        "description": "A 3-component vector [x, y, z] of finite numbers."
      },
      "AnimationClip": {
        "type": "object",
        "required": [
          "name",
          "duration",
          "channels"
        ],
        "description": "An animation clip embedded in the model's GLB.",
        "properties": {
          "name": {
            "type": "string",
            "description": "Clip name; pass it (or \"*\" for the first clip) as renderSettings.animation."
          },
          "duration": {
            "type": "number",
            "description": "Clip duration in seconds."
          },
          "channels": {
            "type": "integer",
            "description": "Number of animation channels in the clip."
          }
        }
      },
      "Model": {
        "type": "object",
        "description": "Model detail as returned by GET /v1/models/:id, including all its renders.",
        "required": [
          "id",
          "status",
          "originalName",
          "title",
          "sku",
          "tags",
          "fileSize",
          "sha256",
          "animations",
          "meshNames",
          "materialNames",
          "createdAt",
          "renders"
        ],
        "properties": {
          "id": {
            "type": "string",
            "description": "Short opaque model ID (e.g. \"Ab3kF9x2qL1m\")."
          },
          "status": {
            "type": "string",
            "enum": [
              "active",
              "archived"
            ],
            "description": "Model status; deleted models are never returned."
          },
          "originalName": {
            "type": "string",
            "description": "Original uploaded filename (e.g. \"chair.glb\")."
          },
          "title": {
            "type": [
              "string",
              "null"
            ],
            "description": "Display title (null when unset; cleared by PATCHing null or \"\")."
          },
          "sku": {
            "type": [
              "string",
              "null"
            ],
            "description": "Product code (null when unset)."
          },
          "tags": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "maxItems": 20,
            "description": "Tags (lowercased, trimmed; up to 20)."
          },
          "fileSize": {
            "type": "integer",
            "description": "Model file size in bytes."
          },
          "sha256": {
            "type": "string",
            "description": "SHA-256 hex digest of the file (used for dedup)."
          },
          "animations": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/AnimationClip"
            },
            "description": "Animation clips embedded in the GLB (empty array when none)."
          },
          "meshNames": {
            "type": [
              "array",
              "null"
            ],
            "items": {
              "type": "string"
            },
            "description": "Render-time mesh names, exactly as hiddenMeshNames matches them (spaces become _, duplicates gain _1/_2 suffixes); null when the model predates inventory extraction."
          },
          "materialNames": {
            "type": [
              "array",
              "null"
            ],
            "items": {
              "type": "string"
            },
            "description": "Material names, exactly the keys materialEdits matches; null when the model predates inventory extraction."
          },
          "createdAt": {
            "type": "string",
            "format": "date-time",
            "description": "Creation timestamp (ISO 8601)."
          },
          "renders": {
            "type": "array",
            "description": "All renders of this model, newest first (summary rows).",
            "items": {
              "type": "object",
              "required": [
                "id",
                "status",
                "renderSettings",
                "createdAt",
                "updatedAt"
              ],
              "properties": {
                "id": {
                  "type": "string",
                  "description": "Short opaque render ID."
                },
                "status": {
                  "$ref": "#/components/schemas/RenderStatus"
                },
                "renderSettings": {
                  "$ref": "#/components/schemas/RenderSettings"
                },
                "createdAt": {
                  "type": "string",
                  "format": "date-time",
                  "description": "Creation timestamp."
                },
                "updatedAt": {
                  "type": "string",
                  "format": "date-time",
                  "description": "Last modification timestamp."
                }
              }
            }
          }
        }
      },
      "ModelListItem": {
        "type": "object",
        "description": "Model row as returned by GET /v1/models (list). Carries animationCount instead of the full animations/meshNames/materialNames inventories.",
        "required": [
          "id",
          "status",
          "originalName",
          "title",
          "sku",
          "tags",
          "fileSize",
          "sha256",
          "animationCount",
          "createdAt",
          "updatedAt"
        ],
        "properties": {
          "id": {
            "type": "string",
            "description": "Short opaque model ID."
          },
          "status": {
            "type": "string",
            "enum": [
              "active",
              "archived"
            ],
            "description": "Model status (present on every row; filter with ?status=active|archived|any)."
          },
          "originalName": {
            "type": "string",
            "description": "Original uploaded filename."
          },
          "title": {
            "type": [
              "string",
              "null"
            ],
            "description": "Display title (null when unset)."
          },
          "sku": {
            "type": [
              "string",
              "null"
            ],
            "description": "Product code (null when unset)."
          },
          "tags": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Tags."
          },
          "fileSize": {
            "type": "integer",
            "description": "Model file size in bytes."
          },
          "sha256": {
            "type": "string",
            "description": "SHA-256 hex digest of the file."
          },
          "animationCount": {
            "type": "integer",
            "description": "Number of animation clips in the model (details via GET /v1/models/:id -> animations)."
          },
          "createdAt": {
            "type": "string",
            "format": "date-time",
            "description": "Creation timestamp."
          },
          "updatedAt": {
            "type": "string",
            "format": "date-time",
            "description": "Last modification timestamp."
          }
        }
      },
      "RenderStatus": {
        "type": "string",
        "enum": [
          "pending",
          "poster-processing",
          "poster-done",
          "video-processing",
          "done",
          "error",
          "failed"
        ],
        "description": "Render lifecycle status: pending (queued, cancellable) -> poster-processing -> video-processing -> done; any status can transition to error; poster-done is a recovery state (poster uploaded, video pass interrupted — a worker resumes it); failed is an errored render dismissed via PATCH /v1/renders/:id/dismiss. Treat intermediate statuses as opaque in-progress states and poll for done or error."
      },
      "Render": {
        "type": "object",
        "description": "Render resource as returned by GET /v1/renders/:id; the same fields appear on each item of GET /v1/renders. POST /v1/renders responses carry the subset id/modelId/status/renderSettings/isPreview (plus deduped: true on an idempotent replay).",
        "required": [
          "id",
          "modelId",
          "status",
          "renderSettings",
          "fileSize",
          "errorMessage",
          "failureCode",
          "hasWebVariant",
          "hasThumb",
          "isPreview",
          "isSystemPreview",
          "unlockedAt",
          "hlsStatus",
          "hlsReady",
          "datasetZipSize",
          "createdAt",
          "updatedAt"
        ],
        "properties": {
          "id": {
            "type": "string",
            "description": "Short opaque render ID (e.g. \"Xz7pQ4w8nR2k\")."
          },
          "modelId": {
            "type": "string",
            "description": "ID of the model this render belongs to."
          },
          "status": {
            "$ref": "#/components/schemas/RenderStatus"
          },
          "renderSettings": {
            "$ref": "#/components/schemas/RenderSettings",
            "description": "The settings this render was created with; may include a server-stamped preview: true on free-pool-funded renders — strip it before re-submitting these settings to a new render."
          },
          "fileSize": {
            "type": [
              "integer",
              "null"
            ],
            "description": "Video file size in bytes (null until done)."
          },
          "errorMessage": {
            "type": [
              "string",
              "null"
            ],
            "description": "Human-readable failure reason when status is error (first line of the worker's report), else null."
          },
          "failureCode": {
            "type": [
              "string",
              "null"
            ],
            "enum": [
              null,
              "model_download_failed",
              "env_download_failed",
              "overlay_missing",
              "overlay_download_failed",
              "renderer_crash",
              "renderer_error",
              "timeout",
              "display_lost",
              "encode_failed",
              "upload_failed",
              "reaped"
            ],
            "description": "Machine-readable failure class when status is error, else null; branch on this, not on message text (renderer_error = bad/unparseable model, reaped = render node stopped responding, overlay_missing = the configured overlay asset was deleted or is out of workspace scope, overlay_download_failed = the overlay frame could not be fetched). Failed renders are refunded."
          },
          "hasWebVariant": {
            "type": "boolean",
            "description": "Whether a web-optimized MP4 variant is available."
          },
          "hasThumb": {
            "type": "boolean",
            "description": "Whether a thumbnail video is available."
          },
          "isPreview": {
            "type": "boolean",
            "description": "Whether this render was funded from the free-credit pool (legacy preview variant; set strictCredits to refuse the fallback)."
          },
          "isSystemPreview": {
            "type": "boolean",
            "description": "Whether this is the free system preview created by a bare model upload (no renderSettings)."
          },
          "unlockedAt": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "Timestamp a preview render was unlocked (legacy — null for API-created renders)."
          },
          "hlsStatus": {
            "type": "string",
            "enum": [
              "pending",
              "processing",
              "ready",
              "failed",
              "skipped"
            ],
            "description": "HLS packaging status."
          },
          "hlsReady": {
            "type": "boolean",
            "description": "Whether HLS playback is ready (hlsStatus === \"ready\")."
          },
          "datasetZipSize": {
            "type": [
              "integer",
              "null"
            ],
            "description": "Dataset ZIP size in bytes (dataset renders only; null otherwise)."
          },
          "deduped": {
            "type": "boolean",
            "description": "Present (as true) only on idempotent-replay responses of POST /v1/renders and /v1/renders/batch (HTTP 200 — nothing was charged)."
          },
          "createdAt": {
            "type": "string",
            "format": "date-time",
            "description": "Creation timestamp (ISO 8601)."
          },
          "updatedAt": {
            "type": "string",
            "format": "date-time",
            "description": "Last modification timestamp (status changes update it — ideal for updatedSince completion polling)."
          }
        }
      },
      "ErrorResponse": {
        "type": "object",
        "required": [
          "error"
        ],
        "description": "Error envelope returned by every non-2xx JSON response.",
        "properties": {
          "error": {
            "type": "object",
            "required": [
              "code",
              "message",
              "status"
            ],
            "properties": {
              "code": {
                "type": "string",
                "description": "Machine-readable error code (e.g. model_not_found, no_credits, invalid_settings, rate_limited)."
              },
              "message": {
                "type": "string",
                "description": "Human-readable error description."
              },
              "status": {
                "type": "integer",
                "description": "HTTP status code, echoed in the body."
              }
            }
          }
        }
      },
      "ListMeta": {
        "type": "object",
        "required": [
          "total",
          "offset",
          "limit"
        ],
        "description": "Pagination metadata for offset-mode list responses ({ data: [...], meta: {...} }).",
        "properties": {
          "total": {
            "type": "integer",
            "description": "Total number of rows matching the query."
          },
          "offset": {
            "type": "integer",
            "description": "Offset of the first returned row."
          },
          "limit": {
            "type": "integer",
            "maximum": 100,
            "description": "Page size (default 50, max 100)."
          }
        }
      },
      "ListMetaCursor": {
        "type": "object",
        "required": [
          "limit",
          "nextCursor"
        ],
        "description": "Pagination metadata for cursor (keyset) mode on GET /v1/models and GET /v1/renders: pass cursor= (empty for page 1, then meta.nextCursor) and walk until nextCursor is null; no total is computed in this mode.",
        "properties": {
          "limit": {
            "type": "integer",
            "maximum": 100,
            "description": "Page size (default 50, max 100)."
          },
          "nextCursor": {
            "type": [
              "string",
              "null"
            ],
            "description": "Opaque cursor for the next page (do not parse), or null when there are no more rows."
          }
        }
      },
      "ModelUploadResult": {
        "type": "object",
        "description": "Result of registering a model via multipart upload, URL ingest, or staged-upload finalize. A render is always created (or, for bare duplicates, the existing system preview is returned — a new free one is created if the duplicate model has none).",
        "required": [
          "modelId",
          "renderId"
        ],
        "properties": {
          "modelId": {
            "type": "string",
            "description": "ID of the created (or reused) model."
          },
          "renderId": {
            "type": "string",
            "description": "ID of the render for this upload — billed when renderSettings were given, otherwise the free system preview (isSystemPreview: true in render responses)."
          },
          "duplicate": {
            "type": "boolean",
            "description": "Present and true when the file's SHA-256 matched an existing ACTIVE model in the workspace (archived/deleted models are not matched); returned with HTTP 200 instead of 201."
          }
        }
      },
      "ModelIngestRequest": {
        "type": "object",
        "required": [
          "url"
        ],
        "properties": {
          "url": {
            "type": "string",
            "format": "uri",
            "description": "http(s) URL of the GLB to download (max 1 GB; GLB only — use multipart ZIP upload for OBJ scans). Presigned S3/R2 GET URLs are supported; the availability HEAD probe is skipped for signed URLs."
          },
          "renderSettings": {
            "$ref": "#/components/schemas/RenderSettings",
            "description": "Omit for a free system preview; provide to create a billed render."
          }
        }
      },
      "ModelUpdateRequest": {
        "type": "object",
        "description": "Partial update — only fields present in the body change. Passing null or \"\" explicitly clears title/sku. At least one of title, sku, tags must be present.",
        "anyOf": [
          {
            "required": [
              "title"
            ]
          },
          {
            "required": [
              "sku"
            ]
          },
          {
            "required": [
              "tags"
            ]
          }
        ],
        "properties": {
          "title": {
            "type": [
              "string",
              "null"
            ],
            "description": "Display title. null or \"\" clears it."
          },
          "sku": {
            "type": [
              "string",
              "null"
            ],
            "description": "Product code. null or \"\" clears it."
          },
          "tags": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Up to 20 tags; lowercased and trimmed server-side. Duplicates are removed and tags beyond 20 are silently dropped — the request is not rejected. A non-array value returns 400 bad_request."
          }
        }
      },
      "ModelStagedUploadTicket": {
        "type": "object",
        "required": [
          "url",
          "ticket",
          "expiresIn",
          "maxBytes"
        ],
        "properties": {
          "url": {
            "type": "string",
            "format": "uri",
            "description": "Presigned PUT URL to the staging area — PUT the raw GLB bytes here (e.g. curl -T model.glb \"<url>\")."
          },
          "ticket": {
            "type": "string",
            "description": "Opaque ticket identifying the staged object; pass to POST /models/finalize."
          },
          "expiresIn": {
            "type": "integer",
            "description": "PUT URL lifetime in seconds."
          },
          "maxBytes": {
            "type": "integer",
            "description": "Maximum accepted file size in bytes (1 GB)."
          }
        }
      },
      "ModelFinalizeRequest": {
        "type": "object",
        "required": [
          "ticket"
        ],
        "properties": {
          "ticket": {
            "type": "string",
            "pattern": "^[A-Za-z0-9_-]{6,32}$",
            "description": "Ticket from POST /models/create-upload-url."
          },
          "filename": {
            "type": "string",
            "default": "model.glb",
            "maxLength": 200,
            "description": "Original filename; must end .glb (staged uploads are GLB-only). Truncated to 200 characters."
          },
          "renderSettings": {
            "$ref": "#/components/schemas/RenderSettings",
            "description": "Omit for a free system preview; provide to create a billed render."
          }
        }
      },
      "RenderCreateRequest": {
        "type": "object",
        "required": [
          "modelId"
        ],
        "properties": {
          "modelId": {
            "type": "string",
            "description": "Model ID (short opaque string) — from POST /models or GET /models."
          },
          "renderSettings": {
            "$ref": "#/components/schemas/RenderSettings",
            "description": "Render configuration. Optional — omitted fields take their documented defaults (note `quality` defaults to `standard`, 4 credits/sec). Server-set keys `preview`/`thumbOnly` are rejected if sent."
          }
        }
      },
      "RenderCreateResult": {
        "type": "object",
        "required": [
          "id",
          "modelId",
          "status",
          "renderSettings",
          "isPreview"
        ],
        "properties": {
          "id": {
            "type": "string",
            "description": "ID of the created render."
          },
          "modelId": {
            "type": "string",
            "description": "The model this render belongs to."
          },
          "status": {
            "type": "string",
            "const": "pending",
            "description": "Always `pending` on creation — poll GET /renders/{renderId}."
          },
          "renderSettings": {
            "$ref": "#/components/schemas/RenderSettings",
            "description": "Echo of the submitted settings (`{}` when omitted)."
          },
          "isPreview": {
            "type": "boolean",
            "description": "True when the render was funded from the account's free-credit pool — it ships watermarked. Check this (or set `strictCredits: true`) in unattended pipelines."
          },
          "deduped": {
            "type": "boolean",
            "const": true,
            "description": "Present (true) when the request replayed an existing render — via Idempotency-Key or the automatic dedupe of an identical model+settings resubmit within a few seconds (both return HTTP 200). The original render was returned and nothing was charged."
          }
        }
      },
      "BatchRenderRequest": {
        "type": "object",
        "required": [
          "renders"
        ],
        "properties": {
          "renders": {
            "type": "array",
            "minItems": 1,
            "maxItems": 100,
            "items": {
              "$ref": "#/components/schemas/RenderCreateRequest"
            },
            "description": "1–100 render specs. Credits for all items are deducted atomically."
          }
        }
      },
      "BatchRenderResult": {
        "type": "object",
        "required": [
          "id",
          "modelId",
          "status",
          "isPreview"
        ],
        "properties": {
          "id": {
            "type": "string",
            "description": "ID of the created render."
          },
          "modelId": {
            "type": "string",
            "description": "The model this render belongs to."
          },
          "status": {
            "type": "string",
            "const": "pending"
          },
          "isPreview": {
            "type": "boolean",
            "description": "True when funded from the free-credit pool (watermarked)."
          },
          "deduped": {
            "type": "boolean",
            "const": true,
            "description": "Present (true) when this item replayed an existing render — via Idempotency-Key or the automatic dedupe of an identical model+settings resubmit within a few seconds — nothing was charged for it."
          }
        }
      },
      "RenderDownloadUrl": {
        "type": "object",
        "required": [
          "url",
          "asset",
          "expiresIn"
        ],
        "properties": {
          "url": {
            "type": "string",
            "format": "uri",
            "description": "Presigned direct-from-bucket GET URL. Unauthenticated capability link until expiry — treat as a secret."
          },
          "asset": {
            "type": "string",
            "enum": [
              "video",
              "web",
              "poster",
              "full_poster",
              "thumb",
              "dataset"
            ],
            "description": "The asset the URL downloads."
          },
          "expiresIn": {
            "type": "integer",
            "minimum": 60,
            "maximum": 604800,
            "description": "Effective link lifetime in seconds (requested value clamped to 60–604800). Edge cases: `expires=0` or a non-numeric `expires` signs the URL for the 3600 default while this field may misreport (60, or null for non-numeric input)."
          }
        }
      },
      "RenderHlsInfo": {
        "type": "object",
        "required": [
          "id",
          "status",
          "masterUrl",
          "thumbnailsVttUrl"
        ],
        "properties": {
          "id": {
            "type": "string",
            "description": "Render ID."
          },
          "status": {
            "type": [
              "string",
              "null"
            ],
            "enum": [
              "pending",
              "processing",
              "ready",
              "failed",
              "skipped",
              null
            ],
            "description": "HLS packaging status; null when packaging hasn't been recorded for this render."
          },
          "masterUrl": {
            "type": [
              "string",
              "null"
            ],
            "description": "`/v1/renders/{renderId}/hls/master.m3u8` when status is `ready`, else null."
          },
          "thumbnailsVttUrl": {
            "type": [
              "string",
              "null"
            ],
            "description": "`/v1/renders/{renderId}/hls/thumbs.vtt` when status is `ready`, else null."
          }
        }
      },
      "TextureAsset": {
        "type": "object",
        "description": "Workspace texture asset — a base-color map replacement referenced by materialEdits.<name>.mapAssetId.",
        "required": [
          "id",
          "name",
          "filename",
          "fileSize",
          "createdAt"
        ],
        "properties": {
          "id": {
            "type": "string",
            "description": "Asset id (12-char opaque string)."
          },
          "name": {
            "type": "string",
            "description": "Display name."
          },
          "filename": {
            "type": "string",
            "description": "Original upload filename."
          },
          "fileSize": {
            "type": "integer",
            "description": "Stored size in bytes."
          },
          "createdAt": {
            "type": "string",
            "format": "date-time",
            "description": "Creation timestamp."
          }
        }
      },
      "EnvironmentAsset": {
        "type": "object",
        "description": "Workspace HDRI environment asset (gainmap triple) referenced by envId in render settings.",
        "required": [
          "id",
          "name",
          "fileSize",
          "createdAt"
        ],
        "properties": {
          "id": {
            "type": "string",
            "description": "Asset id (12-char opaque string)."
          },
          "name": {
            "type": "string",
            "description": "Display name."
          },
          "fileSize": {
            "type": "integer",
            "description": "Total stored size in bytes (all parts)."
          },
          "width": {
            "type": [
              "integer",
              "null"
            ],
            "description": "Source width in pixels, if provided at upload."
          },
          "height": {
            "type": [
              "integer",
              "null"
            ],
            "description": "Source height in pixels, if provided at upload."
          },
          "createdAt": {
            "type": "string",
            "format": "date-time",
            "description": "Creation timestamp."
          }
        }
      },
      "TextureCreateResult": {
        "type": "object",
        "description": "Result of minting a texture asset. Pass id as materialEdits.<materialName>.mapAssetId.",
        "required": [
          "id",
          "name"
        ],
        "properties": {
          "id": {
            "type": "string",
            "description": "Asset id (12-char opaque string)."
          },
          "name": {
            "type": "string",
            "description": "Display name."
          },
          "deduped": {
            "type": "boolean",
            "const": true,
            "description": "Present (true) only on a content-sha dedup replay (HTTP 200) — same uploader (token user) + workspace only — the id is the pre-existing asset's."
          }
        }
      },
      "EnvironmentCreateResult": {
        "type": "object",
        "description": "Result of minting an environment asset. Pass id as envId in render settings.",
        "required": [
          "id",
          "name"
        ],
        "properties": {
          "id": {
            "type": "string",
            "description": "Asset id (12-char opaque string)."
          },
          "name": {
            "type": "string",
            "description": "Display name."
          }
        }
      },
      "AssetDeleteResult": {
        "type": "object",
        "description": "Acknowledgement of a successful asset deletion.",
        "required": [
          "ok"
        ],
        "properties": {
          "ok": {
            "type": "boolean",
            "const": true
          }
        }
      },
      "OverlayLayer": {
        "type": "string",
        "enum": [
          "background",
          "foreground",
          "decal",
          "overlay",
          "texture"
        ],
        "description": "Overlay asset layer. `background` (renderSettings.bgImageId), `foreground`, `decal` (decals[].imageAssetId), `overlay` (full-frame composite baked on top of the finished render), `texture` (materialEdits.<name>.mapAssetId base-color swap)."
      },
      "OverlayAlignment": {
        "type": "string",
        "enum": [
          "left",
          "center",
          "right"
        ],
        "description": "Horizontal anchor when the image is used as a background: which width slice of the 16:9-canonical cover the output keeps. Legacy 9-point values collapse to their horizontal token."
      },
      "OverlayUploadResult": {
        "type": "object",
        "description": "Result of POST /overlays.",
        "required": [
          "id",
          "layer",
          "alignment",
          "name",
          "description"
        ],
        "properties": {
          "id": {
            "type": "string",
            "description": "Overlay asset id (12-char opaque string). Reference it from renderSettings."
          },
          "layer": {
            "$ref": "#/components/schemas/OverlayLayer"
          },
          "alignment": {
            "$ref": "#/components/schemas/OverlayAlignment"
          },
          "name": {
            "type": [
              "string",
              "null"
            ],
            "maxLength": 100,
            "description": "Display name, or null if not provided."
          },
          "description": {
            "type": [
              "string",
              "null"
            ],
            "maxLength": 500,
            "description": "Short description, or null if not provided."
          }
        }
      },
      "OverlayAsset": {
        "type": "object",
        "description": "Overlay asset list item (GET /overlays).",
        "required": [
          "id",
          "layer",
          "alignment",
          "name",
          "description",
          "filename",
          "fileSize",
          "width",
          "height",
          "opacity",
          "isSystem",
          "createdAt"
        ],
        "properties": {
          "id": {
            "type": "string",
            "description": "Overlay asset id (12-char opaque string)."
          },
          "layer": {
            "$ref": "#/components/schemas/OverlayLayer"
          },
          "alignment": {
            "$ref": "#/components/schemas/OverlayAlignment"
          },
          "name": {
            "type": [
              "string",
              "null"
            ],
            "maxLength": 100,
            "description": "Display name, or null."
          },
          "description": {
            "type": [
              "string",
              "null"
            ],
            "maxLength": 500,
            "description": "Short description, or null."
          },
          "filename": {
            "type": "string",
            "description": "Original upload filename."
          },
          "fileSize": {
            "type": "integer",
            "description": "Image file size in bytes."
          },
          "width": {
            "type": [
              "integer",
              "null"
            ],
            "description": "Image width in pixels (null if extraction failed)."
          },
          "height": {
            "type": [
              "integer",
              "null"
            ],
            "description": "Image height in pixels (null if extraction failed)."
          },
          "opacity": {
            "type": "number",
            "minimum": 0,
            "maximum": 1,
            "description": "Overlay opacity, 0–1."
          },
          "isSystem": {
            "type": "boolean",
            "description": "True for published system overlays (available to all users; not deletable via the API)."
          },
          "createdAt": {
            "type": "string",
            "format": "date-time",
            "description": "Creation timestamp."
          }
        }
      },
      "OverlayResolveRequest": {
        "type": "object",
        "description": "Body of POST /overlays/resolve. Exactly one of `sha256` or its alias `sha` must be provided; a missing or malformed value returns 400 `bad_request`. If both are present, `sha256` wins.",
        "properties": {
          "sha256": {
            "type": "string",
            "pattern": "^[0-9a-fA-F]{64}$",
            "description": "SHA-256 of the source image bytes, 64 hex chars (case-insensitive; normalized to lowercase)."
          },
          "sha": {
            "type": "string",
            "pattern": "^[0-9a-fA-F]{64}$",
            "description": "Alias for `sha256` (same format). Ignored when `sha256` is also present."
          },
          "layer": {
            "$ref": "#/components/schemas/OverlayLayer",
            "default": "background",
            "description": "Layer to match against. Defaults to `background`."
          }
        }
      },
      "OverlayResolveResult": {
        "type": "object",
        "description": "Result of POST /overlays/resolve.",
        "required": [
          "id",
          "exists"
        ],
        "properties": {
          "id": {
            "type": [
              "string",
              "null"
            ],
            "description": "Existing overlay asset id, or null if no match."
          },
          "exists": {
            "type": "boolean",
            "description": "True if an asset with this content sha and layer, uploaded by this token's user, already exists in the workspace."
          }
        }
      },
      "HealthStatus": {
        "type": "object",
        "required": [
          "ok",
          "ts",
          "db"
        ],
        "properties": {
          "ok": {
            "type": "boolean",
            "const": true
          },
          "ts": {
            "type": "string",
            "format": "date-time",
            "description": "Database server time (SELECT now())."
          },
          "db": {
            "type": "string",
            "description": "Connected database name."
          }
        }
      },
      "Account": {
        "type": "object",
        "required": [
          "credits",
          "paid",
          "free",
          "total",
          "email",
          "id",
          "scopes",
          "workspace"
        ],
        "properties": {
          "id": {
            "type": "string",
            "description": "Account short id (12-char opaque string). For a workspace-bound token: the workspace owner's account."
          },
          "email": {
            "type": "string",
            "format": "email",
            "description": "Account email."
          },
          "credits": {
            "type": "integer",
            "description": "Spendable credit balance (back-compat; equals paid)."
          },
          "paid": {
            "type": "integer",
            "description": "Ordinary render credits — the signup bonus and any purchases land here."
          },
          "free": {
            "type": "integer",
            "description": "Legacy free-credit grant pool; normally 0."
          },
          "total": {
            "type": "integer",
            "description": "paid + free — matches what POST /quote reports as creditsAvailable."
          },
          "scopes": {
            "type": "array",
            "items": {
              "type": "string",
              "enum": [
                "read",
                "render"
              ]
            },
            "description": "Scopes granted to the token making this request ([] if scopeless)."
          },
          "workspace": {
            "type": [
              "object",
              "null"
            ],
            "description": "Workspace this key is bound to, or null for a personal key. For a bound key the balances above are the workspace owner's pool — the one that funds this key's renders.",
            "required": [
              "id",
              "name"
            ],
            "properties": {
              "id": {
                "type": "string",
                "description": "Workspace short id."
              },
              "name": {
                "type": "string",
                "description": "Workspace name."
              }
            }
          }
        }
      },
      "QuoteRequest": {
        "type": "object",
        "required": [
          "renders"
        ],
        "properties": {
          "renders": {
            "type": "array",
            "minItems": 1,
            "maxItems": 100,
            "description": "Renders to price. Maximum 100 items per quote request.",
            "items": {
              "$ref": "#/components/schemas/QuoteRenderItem"
            }
          }
        }
      },
      "QuoteRenderItem": {
        "type": "object",
        "description": "Cost-relevant render fields for one quoted item.",
        "properties": {
          "quality": {
            "type": "string",
            "enum": [
              "share",
              "standard",
              "4k",
              "pro"
            ],
            "default": "standard",
            "description": "Video quality tier. Per-second rates: share 1, standard 4, 4k 16, pro 64 credits."
          },
          "output": {
            "type": "string",
            "enum": [
              "video",
              "dataset"
            ],
            "default": "video",
            "description": "Set to dataset for a flat-priced dataset render."
          },
          "datasetQuality": {
            "type": "string",
            "enum": [
              "100x800",
              "196x1024",
              "400x2048"
            ],
            "default": "100x800",
            "description": "Dataset tier (flat cost: 40 / 160 / 640 credits)."
          },
          "length": {
            "type": "number",
            "default": 6,
            "description": "Video duration in seconds. Cost = tier rate × ceil(seconds), minimum 1 s."
          },
          "videoLength": {
            "type": "number",
            "description": "Legacy alias for length."
          },
          "effect": {
            "type": "string",
            "description": "Legacy alias: effect \"dataset\" marks the item as a dataset render but always prices at the 100x800 tier (40 credits) — datasetQuality is ignored unless output \"dataset\" is used."
          }
        }
      },
      "QuoteResponse": {
        "type": "object",
        "required": [
          "creditsRequired",
          "creditsAvailable",
          "sufficient",
          "breakdown"
        ],
        "properties": {
          "creditsRequired": {
            "type": "integer",
            "description": "Total credits needed for all quoted renders."
          },
          "creditsAvailable": {
            "type": "integer",
            "description": "Current balance (paid + free) of the pool that would fund the renders — the workspace owner's pool for bound tokens."
          },
          "sufficient": {
            "type": "boolean",
            "description": "Whether the available balance covers creditsRequired."
          },
          "breakdown": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/QuoteBreakdownItem"
            },
            "description": "Per-item credit cost, in request order."
          }
        }
      },
      "QuoteBreakdownItem": {
        "type": "object",
        "required": [
          "credits"
        ],
        "properties": {
          "credits": {
            "type": "integer",
            "description": "Credits this item costs."
          },
          "quality": {
            "type": "string",
            "enum": [
              "share",
              "standard",
              "4k",
              "pro"
            ],
            "description": "Video tier priced (video items only)."
          },
          "length": {
            "type": "number",
            "description": "Duration priced, in seconds (video items only; default 6)."
          },
          "datasetQuality": {
            "type": "string",
            "enum": [
              "100x800",
              "196x1024",
              "400x2048"
            ],
            "description": "Dataset tier echoed from the request (dataset items only); for legacy effect=\"dataset\" items the priced tier is always 100x800 regardless of this value."
          }
        }
      },
      "Product": {
        "type": "object",
        "required": [
          "id",
          "name",
          "credits",
          "priceCents",
          "stripePriceId"
        ],
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid",
            "description": "Product id."
          },
          "name": {
            "type": "string",
            "description": "Display name of the credit pack."
          },
          "credits": {
            "type": "integer",
            "description": "Credits granted by the pack."
          },
          "priceCents": {
            "type": "integer",
            "description": "Price in cents."
          },
          "stripePriceId": {
            "type": "string",
            "description": "Stripe price id used at checkout."
          }
        }
      },
      "Purchase": {
        "type": "object",
        "required": [
          "id",
          "createdAt",
          "type",
          "credits",
          "priceCents",
          "source",
          "description",
          "renderId"
        ],
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid",
            "description": "Ledger entry id."
          },
          "createdAt": {
            "type": "string",
            "format": "date-time",
            "description": "When the entry was created."
          },
          "type": {
            "type": "string",
            "enum": [
              "purchase",
              "unlock",
              "render",
              "refund",
              "promo",
              "referral",
              "admin_grant",
              "signup_bonus"
            ],
            "description": "Ledger entry type."
          },
          "credits": {
            "type": "integer",
            "description": "Credit delta for this entry."
          },
          "priceCents": {
            "type": "integer",
            "description": "Price paid in cents (0 for non-purchase entries)."
          },
          "source": {
            "type": "string",
            "enum": [
              "portal",
              "storefront",
              "promo",
              "referral",
              "admin",
              "api"
            ],
            "description": "Origin of the entry."
          },
          "description": {
            "type": [
              "string",
              "null"
            ],
            "description": "Human-readable description."
          },
          "renderId": {
            "type": [
              "string",
              "null"
            ],
            "description": "Render short id for entries tied to a specific render (e.g. an unlock), otherwise null."
          }
        }
      }
    }
  }
}
