{"x-comment-fields":{"id":{"description":"Comment ID","type":"string","example":"comment-12345"},"projectid":{"description":"project ID the comment belongs to","type":"string","example":"project-67890"},"version":{"description":"Version number","type":"integer","format":"int32","example":1},"userid":{"description":"User ID of the comment creator","type":"string","example":"user-12345"},"displayname":{"description":"Display name of the comment creator","type":"string","example":"John Doe"},"bgcolour":{"description":"Background color of the comment creator","type":"string","example":"#FF5733"},"comment":{"description":"Comment text","type":"string","example":"This looks great!"},"config":{"description":"Configuration for the comment","type":"string","nullable":true,"example":"+VIW+COL"},"attachments":{"description":"Attached projects","type":"array","nullable":true,"items":{"$ref":"#/components/schemas/Attachment"}},"frame":{"description":"Frame number for the comment","type":"integer","format":"int32","nullable":true,"example":0},"labels":{"description":"Labels for the comment","type":"string","nullable":true,"example":"feedback"},"createdat":{"description":"Creation timestamp","type":"string","format":"date-time","example":"2023-04-13T10:30:00Z"},"editedat":{"description":"Last edit timestamp","type":"string","format":"date-time","example":"2023-04-13T11:45:00Z"},"owner":{"description":"Whether the current user owns the comment","type":"boolean","example":true},"completed":{"description":"Whether the comment is marked as completed","type":"boolean","nullable":true,"example":false},"replies":{"type":"array","items":{"$ref":"#/components/schemas/Reply"}},"ai_image_id":{"description":"Legacy. The generated image a comment was made on, from before such comments carried its `rule` in `config`.","type":"string","format":"uuid","nullable":true,"example":"550e8400-e29b-41d4-a716-446655440000"},"approval_number":{"description":"Approval request number this comment was made during, if any","type":"integer","format":"int32","nullable":true,"example":1},"file_path":{"description":"The version's stored file this comment is on, relative to the version's upload root. A comment on a file has no `config`: it is about the file, not a view of the rio.\n","type":"string","nullable":true,"example":"textures/paint.png"},"file_size":{"description":"The file's size in bytes when the comment was made.","type":"integer","format":"int64","nullable":true,"example":174112},"file_etag":{"description":"The file's ETag when the comment was made, if its listing recorded one.","type":"string","nullable":true,"example":"\"9b2cf535f27731c974343645a3985328\""},"file_changed":{"description":"Whether the version's file is no longer the copy the comment was made on: replaced in place, or gone from the version. Decided by size, and by ETag where neither is a multipart one. Null on a comment that isn't on a file.\n","type":"boolean","nullable":true,"example":false}},"x-comment-required":["id","player","createdat","editedat","userid","projectid","version","comment","displayname","bgcolour","owner","replies"],"openapi":"3.1.0","info":{"title":"Renderapp.io API","description":"This API provides endpoints for managing Renderapp projects, handling uploads, version control,\nand project processing. It includes functionality for presigned URLs, multipart uploads,\nand project manipulation with AWS S3.\n","version":"1.0.0","contact":{"name":"Renderapp.io Support","email":"support@renderapp.io","url":"https://renderapp.io"},"x-api-id":"renderapp-api","x-audience":"external-public"},"servers":[{"url":"https://app.renderapp.io/api/v1","description":"Renderapp API v1"}],"paths":{"/.well-known/oauth-authorization-server/api/auth":{"servers":[{"url":"/"}],"get":{"operationId":"getOAuthAuthorizationServerMetadata","summary":"OAuth 2.0 Authorization Server Metadata","description":"RFC 8414 discovery document for the embedded OAuth 2.0 authorization\nserver (issuer `${API_URL}/api/auth`). Clients use this to look up\nthe authorization, token, JWKS, introspection, and revocation\nendpoints rather than hard-coding them.\n","security":[],"tags":["OAuth"],"responses":{"200":{"description":"Authorization server metadata","headers":{"Cache-Control":{"schema":{"type":"string"},"description":"Cache directives (per RFC 8414 §3.3)."}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/OAuthAuthorizationServerMetadata"}}}},"500":{"$ref":"#/components/responses/InternalServerError"},"default":{"$ref":"#/components/responses/InternalServerError"}}}},"/.well-known/oauth-protected-resource/api/v1/mcp":{"servers":[{"url":"/"}],"get":{"operationId":"getMcpProtectedResourceMetadata","summary":"OAuth 2.0 Protected Resource Metadata for the MCP endpoint","description":"RFC 9728 document for `/api/v1/mcp`. A client that receives a 401 from\nthe MCP endpoint follows the `resource_metadata` URL in its\n`WWW-Authenticate` challenge here, then on to the authorization server\nit names to sign the person in and obtain a token for this resource.\n","security":[],"tags":["OAuth"],"responses":{"200":{"description":"Protected resource metadata","headers":{"Cache-Control":{"schema":{"type":"string"},"description":"Cache directives (per RFC 9728 §3.2)."}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/OAuthProtectedResourceMetadata"}}}},"500":{"$ref":"#/components/responses/InternalServerError"},"default":{"$ref":"#/components/responses/InternalServerError"}}}},"/builtin-environments":{"get":{"operationId":"listBuiltinEnvironments","summary":"The instance's installed built-in environments","description":"Readable without an account, so share links and published\nconfigurators can light models with them. One item per environment\nthat has been installed, with short-lived links to its panorama at\neach viewer size. Environments that aren't installed are left out:\nthe viewer keeps the 1k copies it ships with.\n","security":[],"tags":["Generations"],"responses":{"200":{"description":"The installed environments","content":{"application/json":{"schema":{"type":"object","required":["items"],"properties":{"items":{"type":"array","items":{"type":"object","required":["preset","slug","tiers"],"properties":{"preset":{"type":"string","description":"The GLB viewer's environment preset"},"slug":{"type":"string","description":"The Poly Haven asset id"},"tiers":{"type":"array","description":"Radiance .hdr links, valid for an hour, smallest first","items":{"type":"object","required":["width","url"],"properties":{"width":{"type":"integer","format":"int32"},"url":{"type":"string"}}}}}}}}}}}},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/InternalServerError"},"default":{"$ref":"#/components/responses/InternalServerError"}}}},"/openapi":{"get":{"operationId":"getOpenApiDocument","summary":"This OpenAPI document","description":"This document as JSON, readable without an account. `servers[0].url` is\nrewritten to the absolute `/api/v1` base of the instance answering.\n","security":[],"tags":["Docs"],"responses":{"200":{"description":"The OpenAPI 3.1 document describing this API.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OpenApiDocument"}}}},"500":{"$ref":"#/components/responses/InternalServerError"},"default":{"$ref":"#/components/responses/InternalServerError"}}}},"/openapi/authoring":{"get":{"operationId":"getAuthoringOpenApiDocument","summary":"This OpenAPI document, cut to authoring","description":"Only what an agent needs to build a project's configuration and wire and\nlay out a configurator: listing a version's files, generating and\nchecking a rio, drafts, creating a configurator, its projects' settings,\nviewpoints and scenes, the theme and its layout check,\nsaving a layout and layout templates. It carries just the schemas,\nparameters, responses and examples those reference, so it fits in a\nmodel's context beside the work. `/llms.txt` points here.\n`servers[0].url` is rewritten as for `/openapi`.\n","security":[],"tags":["Docs"],"responses":{"200":{"description":"The authoring subset of this OpenAPI 3.1 document.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OpenApiDocument"}}}},"500":{"$ref":"#/components/responses/InternalServerError"},"default":{"$ref":"#/components/responses/InternalServerError"}}}},"/files/{project_id}/{version}/thumb-url":{"get":{"operationId":"getProjectThumbUrl","summary":"Get a presigned URL for a composed default-config thumbnail","description":"Resolves the S3 key of a composed default-config thumb for an image\nproject. Locked to the published long-edge sizes (1280 / 640 / 192).\n\nAccess mirrors `/presigned-url`: PUBLIC files are open to any\nauthenticated caller (Discover cross-account browsing), otherwise the\ncaller needs CASL `read Version` (workspace surfaces), or a matching\n`share_id` / `configurator_id` query param (share + live-project\nviewers). A `configurator_id` counts only while that release is live.\n","parameters":[{"$ref":"#/components/parameters/ProjectIdPath"},{"$ref":"#/components/parameters/VersionPath"},{"name":"long_edge","in":"query","required":true,"description":"Long-edge in pixels of the requested composed thumb.","schema":{"type":"string","enum":["1280","640","192"]}},{"name":"share_id","in":"query","required":false,"description":"Share link ID — grants read access for the share's pinned (project, version) pair.","schema":{"type":"string","format":"uuid"}},{"name":"configurator_id","in":"query","required":false,"description":"Configurator release ID. Grants read access to the project versions it links only while the release is live, meaning its family's current deployment serves it. A draft or replaced release grants nothing, and the caller needs read access of their own.","schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Presigned URL for the requested thumb","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PresignedUrlResponse"}}}},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"description":"Project not found, or the thumb hasn't been composed yet.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/InternalServerError"},"default":{"$ref":"#/components/responses/InternalServerError"}},"security":[{"oauth2":["projects:read"]}],"tags":["Files"]}},"/files/{project_id}/{version}/presigned-url":{"get":{"operationId":"getProjectFilePresignedUrl","summary":"Get a presigned URL for viewing a project file","description":"Generates a presigned URL to download/view a file from S3. Supports share links via share_id query param.","parameters":[{"$ref":"#/components/parameters/ProjectIdPath"},{"$ref":"#/components/parameters/VersionPath"},{"name":"key","in":"query","description":"The file path within the version. When resolution is provided, this should be a bare filename (e.g., \"frame0.jpg\"). When resolution is omitted, this may include a legacy quality prefix (e.g., \"original/frame0.jpg\").","required":true,"allowReserved":true,"schema":{"type":"string"},"example":"frame0.jpg"},{"name":"resolution","in":"query","description":"Resolution tier for file retrieval. When provided, the server constructs the full storage path from the bare key and resolution.","required":false,"schema":{"$ref":"#/components/schemas/ResolutionTier"}},{"name":"share_id","in":"query","description":"Share link ID for unauthenticated access","required":false,"schema":{"type":"string","format":"uuid"}},{"name":"configurator_id","in":"query","description":"Configurator release ID. Grants read access to the media of the project versions it links only while the release is live, meaning its family's current deployment serves it. A draft or replaced release grants nothing. It covers only what the configurator viewer asks for: a key under a `resolution`, or a bare `original/` key. Anything else needs the caller's own read access. Comment media (`attachments/`, `comments/` with no `resolution`) also needs read access to the version's comments, whoever the caller is.","required":false,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Successful operation","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PresignedUrlResponse"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/InternalServerError"},"default":{"$ref":"#/components/responses/InternalServerError"}},"security":[{"oauth2":["projects:read"]}],"tags":["Files"]}},"/files/{project_id}/{version}":{"get":{"operationId":"listVersionFiles","summary":"List a version's files","description":"Lists the files storage holds under the version's upload root now, and compares them with the rio. A file is `used` when a `Maps[].filename` names it and `unused` otherwise; `missing` lists map filenames with no stored file. `listed_at` is when this listing was taken. Anyone who can read the version may list it, except through a share link.\n","parameters":[{"name":"project_id","in":"path","description":"The project ID","required":true,"schema":{"type":"string","format":"uuid"},"example":"2524b742-8c36-413e-b9f6-2269918618af"},{"name":"version","in":"path","description":"The version number","required":true,"schema":{"type":"integer","format":"int32","minimum":1},"example":3}],"responses":{"200":{"description":"Successful operation","content":{"application/json":{"schema":{"$ref":"#/components/schemas/VersionFiles"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/InternalServerError"},"default":{"$ref":"#/components/responses/InternalServerError"}},"security":[{"oauth2":["projects:read"]}],"tags":["Files"]}},"/files/{project_id}/{version}/history":{"get":{"operationId":"getVersionFileHistory","summary":"A file's history across the project's versions","description":"Every version of the project, that the caller can read, whose recorded files include `path`, oldest first, with what each recorded about it. The record is the version's file manifest, relisted after every save, so a version saved moments ago may not be in it yet. `version` names the version the caller is looking from; the history covers all of them.\n","parameters":[{"name":"project_id","in":"path","description":"The project ID","required":true,"schema":{"type":"string","format":"uuid"},"example":"2524b742-8c36-413e-b9f6-2269918618af"},{"name":"version","in":"path","description":"The version number","required":true,"schema":{"type":"integer","format":"int32","minimum":1},"example":3},{"name":"path","in":"query","description":"Path relative to the upload root, the same form as Maps[].filename","required":true,"allowReserved":true,"schema":{"type":"string"},"example":"paint/tasman_blue/f34.png"}],"responses":{"200":{"description":"Successful operation","content":{"application/json":{"schema":{"$ref":"#/components/schemas/VersionFileHistory"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/InternalServerError"},"default":{"$ref":"#/components/responses/InternalServerError"}},"security":[{"oauth2":["projects:read"]}],"tags":["Files"]}},"/files/{project_id}/{version}/get-presigned-url":{"post":{"operationId":"getPresignedUrl","summary":"Get a presigned URL for S3 upload","deprecated":true,"description":"Deprecated. Use `POST /files/{project_id}/{version}/sign-s3-request` with\n`operation: put-object`. Kept for clients written against Uppy's `getUploadParameters`\ncallback in its older, one-endpoint-per-step shape.\n\nGenerates a presigned URL to upload a project directly to S3\n","parameters":[{"name":"project_id","in":"path","description":"The project ID","required":true,"schema":{"type":"string","format":"uuid"},"example":"2524b742-8c36-413e-b9f6-2269918618af"},{"name":"version","in":"path","description":"The project version","required":true,"schema":{"type":"integer","format":"int32","minimum":1},"example":1}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"path":{"description":"The S3 object key","type":"string","example":"projects/document.pdf"},"metadata":{"description":"Ignored. Accepted so older integrations keep working; uploads no longer store caller-supplied metadata.","deprecated":true,"type":"object","additionalProperties":{"type":"string"}}},"required":["path"]}}}},"responses":{"200":{"description":"Successful operation","content":{"application/json":{"schema":{"$ref":"#/components/schemas/LegacyUploadUrlResponse"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/InternalServerError"},"default":{"$ref":"#/components/responses/InternalServerError"}},"security":[{"oauth2":["projects:write"]}],"tags":["Files"]}},"/files/{project_id}/{version}/begin-multipart-upload":{"post":{"operationId":"beginMultipartUpload","summary":"Begin a multipart upload","deprecated":true,"description":"Deprecated. Use `POST /files/{project_id}/{version}/sign-s3-request` with\n`operation: create-multipart-upload`. Kept for clients written against Uppy's `createMultipartUpload`\ncallback in its older, one-endpoint-per-step shape.\n\nInitiates a multipart upload process in S3\n","parameters":[{"name":"project_id","in":"path","description":"The project ID","required":true,"schema":{"type":"string","format":"uuid"},"example":"2524b742-8c36-413e-b9f6-2269918618af"},{"name":"version","in":"path","description":"The project version","required":true,"schema":{"type":"integer","format":"int32","minimum":1},"example":1}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"path":{"description":"The S3 object key","type":"string","example":"projects/document.pdf"},"metadata":{"description":"Ignored. Accepted so older integrations keep working; uploads no longer store caller-supplied metadata.","deprecated":true,"type":"object","additionalProperties":{"type":"string"}},"content_type":{"description":"MIME type of the file, stored on the object so it is served with the right Content-Type","type":"string","example":"image/png"}},"required":["path"]}}}},"responses":{"200":{"description":"Successful operation","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MultipartUploadResponse"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/InternalServerError"},"default":{"$ref":"#/components/responses/InternalServerError"}},"security":[{"oauth2":["projects:write"]}],"tags":["Files"]}},"/files/{project_id}/{version}/list-multipart-upload-parts":{"post":{"summary":"List parts of a multipart upload","deprecated":true,"description":"Deprecated. Use `POST /files/{project_id}/{version}/sign-s3-request` with\n`operation: list-parts`. Kept for clients written against Uppy's `listParts`\ncallback in its older, one-endpoint-per-step shape.\n\nReturns a list of parts that have been uploaded for a specific multipart upload. Used by Uppy to resume interrupted uploads.\n      operationId: listMultipartUploadParts\n","parameters":[{"name":"project_id","in":"path","required":true,"description":"The project identifier","schema":{"type":"string"}},{"name":"version","in":"path","required":true,"description":"The version identifier","schema":{"type":"integer","format":"int32","minimum":1}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["path","upload_id"],"properties":{"path":{"type":"string","description":"The file path within the project/next-version","example":"documents/report.pdf"},"upload_id":{"type":"string","description":"The multipart upload ID returned from initiate_multipart_upload","example":"exampleUploadId123"},"max_parts":{"type":"integer","format":"int32","description":"Maximum number of parts to return (default 1000, max 1000)","minimum":1,"maximum":1000,"example":1000},"part_number_marker":{"type":"string","description":"Part number after which listing begins (for pagination)","example":"0"}}}}}},"responses":{"200":{"description":"List of uploaded parts","content":{"application/json":{"schema":{"type":"object","required":["parts"],"properties":{"parts":{"type":"array","items":{"type":"object","required":["part_number","size","etag"],"properties":{"part_number":{"type":"integer","format":"int32","description":"The part number (1-10000)","example":1},"size":{"type":"integer","format":"int64","description":"Size of the part in bytes","example":5242880},"etag":{"type":"string","description":"Entity tag of the uploaded part","example":"\"abc123def456...\""}}}}}},"example":{"parts":[{"part_number":1,"size":5242880,"etag":"\"abc123def456\""},{"part_number":2,"size":5242880,"etag":"\"def456abc123\""}]}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/InternalServerError"},"default":{"$ref":"#/components/responses/InternalServerError"}},"security":[{"oauth2":["projects:write"]}],"tags":["Files"]}},"/files/{project_id}/{version}/finish-multipart-upload":{"post":{"operationId":"finishMultipartUpload","summary":"Complete a multipart upload","deprecated":true,"description":"Deprecated. Use `POST /files/{project_id}/{version}/sign-s3-request` with\n`operation: complete-multipart-upload`. Kept for clients written against Uppy's `completeMultipartUpload`\ncallback in its older, one-endpoint-per-step shape.\n\nCompletes a multipart upload by combining all parts\n","parameters":[{"name":"project_id","in":"path","description":"The project ID","required":true,"schema":{"type":"string","format":"uuid"},"example":"2524b742-8c36-413e-b9f6-2269918618af"},{"name":"version","in":"path","description":"The project version","required":true,"schema":{"type":"integer","format":"int32","minimum":1},"example":1}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"path":{"description":"The S3 object key","type":"string","example":"projects/document.pdf"},"upload_id":{"description":"The ID of the multipart upload","type":"string","example":"VXBsb2FkIElEIGZvciA2aWWpbmcncyBteS1tb3ZpZS5tMnRzIHVwbG9hZ..."},"parts":{"description":"List of uploaded parts","type":"array","items":{"type":"object","properties":{"part_number":{"type":"integer","format":"int32","example":1},"etag":{"type":"string","example":"\"a54357aff0632cce46d942af68356b38\""}}}}},"required":["path","upload_id","parts"]}}}},"responses":{"200":{"description":"Successful operation","content":{"application/json":{"schema":{"type":"object"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/InternalServerError"},"default":{"$ref":"#/components/responses/InternalServerError"}},"security":[{"oauth2":["projects:write"]}],"tags":["Files"]}},"/files/{project_id}/{version}/abort-multipart-upload":{"post":{"operationId":"abortMultipartUpload","summary":"Abort a multipart upload","deprecated":true,"description":"Deprecated. Use `POST /files/{project_id}/{version}/sign-s3-request` with\n`operation: abort-multipart-upload`. Kept for clients written against Uppy's `abortMultipartUpload`\ncallback in its older, one-endpoint-per-step shape.\n\nAborts an in-progress multipart upload\n","parameters":[{"name":"project_id","in":"path","description":"The project ID","required":true,"schema":{"type":"string","format":"uuid"},"example":"2524b742-8c36-413e-b9f6-2269918618af"},{"name":"version","in":"path","description":"The project version","required":true,"schema":{"type":"integer","format":"int32","minimum":1},"example":1}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"path":{"description":"The S3 object key","type":"string","example":"projects/document.pdf"},"upload_id":{"description":"The ID of the multipart upload","type":"string","example":"VXBsb2FkIElEIGZvciA2aWWpbmcncyBteS1tb3ZpZS5tMnRzIHVwbG9hZ..."}},"required":["path","upload_id"]}}}},"responses":{"200":{"description":"Successful operation","content":{"application/json":{"schema":{"type":"object"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/InternalServerError"},"default":{"$ref":"#/components/responses/InternalServerError"}},"security":[{"oauth2":["projects:write"]}],"tags":["Files"]}},"/files/{project_id}/{version}/get-multipart-upload-presigned-url":{"post":{"operationId":"getMultipartUploadPresignedUrl","summary":"Get a presigned URL for a multipart upload part","deprecated":true,"description":"Deprecated. Use `POST /files/{project_id}/{version}/sign-s3-request` with\n`operation: upload-part`. Kept for clients written against Uppy's `signPart`\ncallback in its older, one-endpoint-per-step shape.\n\nGenerates a presigned URL for uploading a part in a multipart upload\n","parameters":[{"name":"project_id","in":"path","description":"The project ID","required":true,"schema":{"type":"string","format":"uuid"},"example":"2524b742-8c36-413e-b9f6-2269918618af"},{"name":"version","in":"path","description":"The project version","required":true,"schema":{"type":"integer","format":"int32","minimum":1},"example":1}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"path":{"description":"The S3 object key","type":"string","example":"projects/document.pdf"},"upload_id":{"description":"The ID of the multipart upload","type":"string","example":"VXBsb2FkIElEIGZvciA2aWWpbmcncyBteS1tb3ZpZS5tMnRzIHVwbG9hZ..."},"part":{"description":"The part number","type":"integer","format":"int32","example":1}},"required":["path","upload_id","part"]}}}},"responses":{"200":{"description":"Successful operation","content":{"application/json":{"schema":{"type":"object","properties":{"url":{"description":"The presigned URL for uploading the part","type":"string","example":"https://s3.amazonaws.com/mybucket/mykey?partNumber=1&uploadId=..."}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/InternalServerError"},"default":{"$ref":"#/components/responses/InternalServerError"}},"security":[{"oauth2":["projects:write"]}],"tags":["Files"]}},"/files/{project_id}/{version}/sign-s3-request":{"post":{"operationId":"signS3Request","summary":"Presign one S3 request so the client can perform it directly","description":"Returns a presigned URL for a single S3 operation against a key inside this version's upload prefix. The caller performs the operation itself; the server only signs it. The key must resolve inside `{project_id}/{version}/{project_id}_{version}/`, and a key outside that prefix is rejected with 400.\n","parameters":[{"name":"project_id","in":"path","description":"The project ID","required":true,"schema":{"type":"string","format":"uuid"},"example":"2524b742-8c36-413e-b9f6-2269918618af"},{"name":"version","in":"path","description":"The project version","required":true,"schema":{"type":"integer","format":"int32","minimum":1},"example":1}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"operation":{"description":"The S3 operation to presign","type":"string","enum":["put-object","create-multipart-upload","upload-part","complete-multipart-upload","abort-multipart-upload","list-parts"],"example":"upload-part"},"key":{"description":"The full S3 object key, which must sit inside this version's upload prefix\n","type":"string","example":"2524b742-8c36-413e-b9f6-2269918618af/1/2524b742-8c36-413e-b9f6-2269918618af_1/body/paint.png"},"upload_id":{"description":"The multipart upload ID. Required for every operation except put-object and create-multipart-upload.\n","type":"string","example":"VXBsb2FkIElEIGZvciA2aWWpbmcncyBteS1tb3ZpZS5tMnRzIHVwbG9hZ..."},"part_number":{"description":"The part number. Required for upload-part.","type":"integer","format":"int32","minimum":1,"example":1},"content_type":{"description":"MIME type stored on the object, for put-object and create-multipart-upload","type":"string","example":"image/png"},"content_length":{"description":"Byte length of the object. Required for put-object, where it is signed into the URL: S3 rejects a PUT whose body length differs, so the ceiling cannot be bypassed by a client that declares a smaller size than it sends.\n","type":"integer","format":"int64","minimum":0,"example":1048576},"metadata":{"description":"Ignored. Accepted so older integrations keep working; uploads no longer store caller-supplied metadata.","deprecated":true,"type":"object","additionalProperties":{"type":"string"}}},"required":["operation","key"]}}}},"responses":{"200":{"description":"Successful operation","content":{"application/json":{"schema":{"type":"object","properties":{"url":{"description":"The presigned URL for the requested operation","type":"string","example":"https://s3.amazonaws.com/mybucket/mykey?partNumber=1&uploadId=..."}},"required":["url"]}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/InternalServerError"},"default":{"$ref":"#/components/responses/InternalServerError"}},"security":[{"oauth2":["projects:write"]}]}},"/files/decrypt-rio-file":{"post":{"operationId":"decryptRioproject","summary":"Decrypt a RIO project","description":"Decrypts an encrypted RIO project","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"data":{"description":"The encrypted data in base64 format","type":"string","example":"AQIDBAUGBwgJCgsMDQ4PEA=="}},"required":["data"]}}}},"responses":{"200":{"description":"Successful operation","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DecryptedResponse"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/InternalServerError"},"default":{"$ref":"#/components/responses/InternalServerError"}},"security":[{"oauth2":["projects:read"]}],"tags":["Files"]}},"/files/{project_id}/cdn-access":{"post":{"operationId":"setCdnAccessCookies","summary":"Issue CloudFront signed cookies for a project","description":"Sets CloudFront signed cookies scoped to assets under the given project, allowing the browser to fetch them directly from the CDN without per-object presigned URLs. Authenticated with the standard OIDC bearer; API keys are rejected. Supports share links via the share_id query param — the same guest/invited/team-member rules that gate share-linked project reads elsewhere apply here.\n","parameters":[{"$ref":"#/components/parameters/ProjectIdPath"},{"$ref":"#/components/parameters/ShareIdQuery"}],"responses":{"204":{"description":"Cookies set"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/InternalServerError"},"503":{"description":"CDN cookies are not configured on this deployment","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"default":{"$ref":"#/components/responses/InternalServerError"}},"security":[{"oauth2":["projects:read"]}],"tags":["Files"]}},"/configurators/{id}/cdn-access":{"post":{"operationId":"setCdnConfiguratorAccessCookies","summary":"Issue CloudFront signed cookies for a configurator and all its projects","description":"Sets CloudFront signed cookies covering both the configurator's own S3 prefix (`configurators/<id>/*`) and every project the configurator contains (`<project_id>/*`). Guests can only access published configurators. API keys are rejected.\n","parameters":[{"$ref":"#/components/parameters/ConfiguratorIdPath"}],"responses":{"204":{"description":"Cookies set"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/InternalServerError"},"503":{"description":"CDN cookies are not configured on this deployment","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"default":{"$ref":"#/components/responses/InternalServerError"}},"security":[{"oauth2":["configurators:read"]}],"tags":["Configurators"]}},"/configurators/{id}/preview-link":{"post":{"operationId":"createConfiguratorPreviewLink","summary":"Mint a short-lived link to view a configurator in a browser","description":"Returns a signed `/preview/{id}` URL that opens the configurator as it is built now, drafts included, in a browser with no session, such as an agent's own Playwright or Chrome. The link can only view: it reads this configurator and the project versions it uses, and nothing else. It expires at `expires_at` and cannot be revoked before then; mint a new one when it lapses. A preview link cannot mint another. The caller must hold `configurators:read` and `projects:read`, the scopes the page reads through; the link holds no more than those.\n","parameters":[{"$ref":"#/components/parameters/ConfiguratorIdPath"}],"responses":{"200":{"description":"Preview link","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ConfiguratorPreviewLink"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"500":{"$ref":"#/components/responses/InternalServerError"},"default":{"$ref":"#/components/responses/InternalServerError"}},"security":[{"oauth2":["configurators:read"]}],"tags":["Configurators"]}},"/configurators/{id}/review-links":{"get":{"operationId":"listConfiguratorReviewLinks","summary":"List a configurator's review links","description":"The 100 newest review links made for this configurator, revoked and expired ones included. A link that still works carries its `url` again, so it can be copied later, when the caller holds the scopes the page reads through.\n","parameters":[{"$ref":"#/components/parameters/ConfiguratorIdPath"}],"responses":{"200":{"description":"Review links","content":{"application/json":{"schema":{"type":"object","properties":{"links":{"type":"array","items":{"$ref":"#/components/schemas/ConfiguratorReviewLink"}}},"required":["links"]}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/InternalServerError"},"default":{"$ref":"#/components/responses/InternalServerError"}},"security":[{"oauth2":["configurators:write"]}],"tags":["Configurators"]},"post":{"operationId":"createConfiguratorReviewLink","summary":"Make a link people can review a configurator with","description":"A `/preview/{id}` URL, like a preview link, for people outside the workspace to look at the configurator as it is being built, drafts included, with no sign-in. It shows each saved change within seconds. It lasts `days` (7 by default) and can be revoked before then. Anyone who has the URL can view, so share it only with the people meant to see it. The caller must hold `configurators:read` and `projects:read` as well, the scopes the page reads through.\n","parameters":[{"$ref":"#/components/parameters/ConfiguratorIdPath"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"description":"Who or what the link is for, to tell links apart","type":"string","maxLength":120,"example":"Dealer sign-off"},"days":{"description":"How long the link works","type":"integer","format":"int32","enum":[1,7,30],"default":7}}}}}},"responses":{"201":{"description":"Review link","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ConfiguratorReviewLink"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/InternalServerError"},"default":{"$ref":"#/components/responses/InternalServerError"}},"security":[{"oauth2":["configurators:write"]}],"tags":["Configurators"]}},"/configurators/{id}/review-links/{link_id}":{"delete":{"operationId":"revokeConfiguratorReviewLink","summary":"Revoke a review link","description":"The link stops working at once. Revoking one already revoked changes nothing.","parameters":[{"$ref":"#/components/parameters/ConfiguratorIdPath"},{"name":"link_id","in":"path","description":"The review link ID","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"204":{"description":"Revoked"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/InternalServerError"},"default":{"$ref":"#/components/responses/InternalServerError"}},"security":[{"oauth2":["configurators:write"]}],"tags":["Configurators"]}},"/projects/{project_id}":{"get":{"operationId":"getproject","summary":"Get a project","description":"Retrieves a project by ID","parameters":[{"name":"project_id","in":"path","description":"The project ID","required":true,"schema":{"type":"string","format":"uuid"},"example":"2524b742-8c36-413e-b9f6-2269918618af"},{"name":"share_id","in":"query","required":false,"description":"Share link ID used to filter versions to only those allowed by the share","schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Successful operation","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProjectsResponse"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/InternalServerError"},"default":{"$ref":"#/components/responses/InternalServerError"}},"security":[{"oauth2":["projects:read"]}],"tags":["Projects"]},"post":{"operationId":"createNextVersion","summary":"Create next version","description":"Computes the next available version number, creates a draft version record, and returns the version number. Idempotent — if the version already exists, returns it without error.","parameters":[{"name":"project_id","in":"path","description":"The project ID","required":true,"schema":{"type":"string","format":"uuid"},"example":"2524b742-8c36-413e-b9f6-2269918618af"}],"responses":{"201":{"description":"Draft version created","content":{"application/json":{"schema":{"type":"object","required":["version"],"properties":{"version":{"type":"integer","format":"int32","description":"The next version number","example":4}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/InternalServerError"},"default":{"$ref":"#/components/responses/InternalServerError"}},"security":[{"oauth2":["projects:write"]}],"tags":["Projects"]},"delete":{"operationId":"deleteProject","summary":"Delete a project","description":"Moves a project to the trash, where it can be restored for 30 days before it and its versions are deleted for good. Returns 409 while a configurator uses the project.\n","parameters":[{"name":"project_id","in":"path","description":"The project ID","required":true,"schema":{"type":"string","format":"uuid"},"example":"2524b742-8c36-413e-b9f6-2269918618af"}],"responses":{"204":{"description":"Project moved to the trash"},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"$ref":"#/components/responses/Conflict"},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/InternalServerError"},"default":{"$ref":"#/components/responses/InternalServerError"}},"security":[{"oauth2":["projects:write"]}],"tags":["Projects"]},"patch":{"operationId":"updateProject","summary":"Rename or move a project","description":"Renames a project, moves it, or both. Including `parent_id` or `workspace_id` makes it a move, and an absent `parent_id` then means the top level.\n","parameters":[{"name":"project_id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","minProperties":1,"additionalProperties":false,"properties":{"name":{"type":"string","minLength":1,"maxLength":256,"example":"Renamed"},"parent_id":{"description":"The folder to move the project into, or null for the top level of the workspace. The folder must be in the destination workspace.\n","type":["string","null"],"format":"uuid"},"workspace_id":{"description":"The workspace to move the project into. Defaults to the workspace of `parent_id`, or the project's current workspace when moving to the top level. Moving out of a workspace needs permission to delete there.\n","type":"string","format":"uuid"}}}}}},"responses":{"204":{"description":"Project updated"},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/InternalServerError"},"default":{"$ref":"#/components/responses/InternalServerError"}},"security":[{"oauth2":["projects:write"]}],"tags":["Projects"]}},"/projects/{project_id}/parents":{"get":{"operationId":"getParentFolders","summary":"Get parent folders","description":"Gets all parent folders of a project","parameters":[{"name":"project_id","in":"path","description":"The project ID","required":true,"schema":{"type":"string","format":"uuid"},"example":"2524b742-8c36-413e-b9f6-2269918618af"}],"responses":{"200":{"description":"Successful operation","content":{"application/json":{"schema":{"type":"object","properties":{"parents":{"type":"array","items":{"$ref":"#/components/schemas/FolderResponse"}},"workspace_id":{"type":"string","nullable":true,"description":"The workspace ID the project belongs to"}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/InternalServerError"},"default":{"$ref":"#/components/responses/InternalServerError"}},"security":[{"oauth2":["projects:read"]}],"tags":["Projects"]}},"/projects/{project_id}/{version}":{"get":{"operationId":"getprojectVersion","summary":"Get a project version","description":"Gets a specific version of a project","parameters":[{"name":"project_id","in":"path","description":"The project ID","required":true,"schema":{"type":"string","format":"uuid"},"example":"2524b742-8c36-413e-b9f6-2269918618af"},{"name":"version","in":"path","description":"The version number (optional, defaults to the latest)","required":true,"schema":{"type":"integer","format":"int32","minimum":1},"example":3},{"name":"share_id","in":"query","required":false,"description":"Share link ID used to restrict accessible versions","schema":{"type":"string","format":"uuid","example":"2524b742-8c36-413e-b9f6-2269918618af"}},{"name":"configurator_id","in":"query","required":false,"description":"Configurator release ID. Grants read access to the project versions it links only while the release is live, meaning its family's current deployment serves it.","schema":{"type":"string","format":"uuid"}},{"name":"original_rio","in":"query","required":false,"description":"If \"true\", fetches Rio from projectId/version/projectId_version/.rio instead of projectId/version/.rio","schema":{"type":"string","enum":["true","false"]},"example":"false"}],"responses":{"200":{"description":"Successful operation","content":{"application/json":{"schema":{"$ref":"#/components/schemas/VersionResponse"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/InternalServerError"},"default":{"$ref":"#/components/responses/InternalServerError"}},"security":[{"oauth2":["projects:read"]}],"tags":["Projects"]},"delete":{"operationId":"deleteVersion","summary":"Delete a version","description":"Deletes a specific version of a project","parameters":[{"name":"project_id","in":"path","description":"The project ID","required":true,"schema":{"type":"string","format":"uuid"},"example":"2524b742-8c36-413e-b9f6-2269918618af"},{"name":"version","in":"path","description":"The version number","required":true,"schema":{"type":"integer","format":"int32","minimum":1},"example":3}],"responses":{"200":{"description":"Successful operation","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"description":"Success indicator","type":"boolean","example":true}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/InternalServerError"},"default":{"$ref":"#/components/responses/InternalServerError"}},"security":[{"oauth2":["projects:write"]}],"tags":["Projects"]},"patch":{"operationId":"updateVersionFields","summary":"Update version fields (size, settings)","description":"Updates the field of a project version","parameters":[{"name":"project_id","in":"path","description":"The project ID","required":true,"schema":{"type":"string","format":"uuid"},"example":"2524b742-8c36-413e-b9f6-2269918618af"},{"name":"version","in":"path","description":"The version number","required":true,"schema":{"type":"integer","format":"int32","minimum":1},"example":3}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/VersionFieldUpdateRequest"}}}},"responses":{"200":{"description":"Successful operation","content":{"application/json":{"schema":{"type":"object"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"422":{"$ref":"#/components/responses/ConfigurationRefused"},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/InternalServerError"},"default":{"$ref":"#/components/responses/InternalServerError"}},"security":[{"oauth2":["projects:write"]}],"tags":["Projects"]}},"/projects/activity":{"get":{"operationId":"listProjectActivity","summary":"List projects with work in flight","description":"Projects currently being copied, zipped, optimised, transcoded or otherwise processed, scoped to workspaces the caller can list. At most one row per version — a tile shows one word, so where several stages run at once the highest-priority one wins.\n\nProgress comes from the version's jobs, except for copying, whose `job` row counts objects and so reports a real percentage where its one job can only say 0 or 100. Work that has reported nothing for an hour counts as stalled and is left out. The `activity` feed on /api/internal/stream carries the same list as it changes.\n","responses":{"200":{"description":"Successful operation","content":{"application/json":{"schema":{"type":"object","properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/ProjectActivity"}}},"required":["items"]}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"429":{"$ref":"#/components/responses/TooManyRequests"},"default":{"$ref":"#/components/responses/InternalServerError"}},"security":[{"oauth2":["projects:read"]}],"tags":["Projects"]}},"/projects/{project_id}/duplicate":{"post":{"operationId":"duplicateProject","summary":"Duplicate a project into a new one","description":"Copies a project into a brand-new project in the target workspace and folder. Only the source's writers can copy it, only into a workspace in the same studio, and the copy starts private and can never be made more visible than the source. Every live version comes across with its version number intact, unless `version` names a single one. Comments and their replies come with it, and so does an image kept into a version, as one of its views; share links, approvals, generation records and cached archives deliberately do not, so a copy starts clean rather than inheriting state that would be wrong or unsafe in another workspace.\n\nThe database rows are written synchronously and the new project is readable immediately, but its media is copied by a background job per version — poll `GET /jobs/{project_id}/{version}/status` (or the job ids returned here) until they complete before treating the copy as viewable.\n","parameters":[{"name":"project_id","in":"path","description":"The project ID to copy from","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"workspace_id":{"description":"Destination workspace","type":"string","format":"uuid"},"parent_id":{"description":"Destination folder. Omit for the workspace root.","type":"string","format":"uuid"},"name":{"description":"Name for the copy. Defaults to the source name suffixed with \"(copy)\".","type":"string","minLength":1,"maxLength":255},"version":{"description":"Copy only this version. Omit to copy every live version.","type":"integer","format":"int32","minimum":1}},"required":["workspace_id"]}}}},"responses":{"202":{"description":"Rows copied; media copy accepted and running in the background","content":{"application/json":{"schema":{"type":"object","properties":{"project_id":{"description":"The new project","type":"string","format":"uuid"},"name":{"type":"string"},"versions":{"description":"Version numbers copied, preserved from the source","type":"array","items":{"type":"integer","format":"int32"}},"comment_count":{"description":"Number of comments copied across all versions","type":"integer","format":"int32"},"jobs":{"description":"The media-copy job for each copied version","type":"array","items":{"type":"object","properties":{"version":{"type":"integer","format":"int32"},"job_id":{"description":"The media copy's job. If it fails, re-run it with POST /jobs/{job_id}/restart.\n","type":"string","format":"uuid"},"queued":{"description":"Always true: the copy is queued in the same transaction that creates the duplicate.\n","type":"boolean"}},"required":["version","job_id","queued"]}}},"required":["project_id","name","versions","comment_count","jobs"]}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"$ref":"#/components/responses/PaymentRequired"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/TooManyRequests"},"default":{"$ref":"#/components/responses/InternalServerError"}},"security":[{"oauth2":["projects:write"]}],"tags":["Projects"]}},"/projects/{project_id}/{version}/archive":{"get":{"operationId":"getArchiveUrl","summary":"Get archive download URL","description":"Gets a presigned URL for downloading a project version archive. Needs `downloads:read`, which a studio role, an API key or an OAuth client grants; share links never do. Supports filtering by file prefixes or by prefix hash.","parameters":[{"name":"project_id","in":"path","description":"The project ID","required":true,"schema":{"type":"string","format":"uuid"},"example":"2524b742-8c36-413e-b9f6-2269918618af"},{"name":"version","in":"path","description":"The version number","required":true,"schema":{"type":"integer","format":"int32","minimum":1},"example":3},{"name":"prefixes","in":"query","description":"Optional array of file path prefixes to filter which files are included in the archive","required":false,"schema":{"type":"array","items":{"type":"string"}},"example":["textures/","models/"]},{"name":"prefix_hash","in":"query","description":"Optional hash of the prefixes array. Used to validate and retrieve archives created with specific prefix filters.","required":false,"schema":{"type":"string"},"example":"abc123def456"}],"responses":{"200":{"description":"Successful operation","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ArchiveUrlResponse"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/InternalServerError"},"default":{"$ref":"#/components/responses/InternalServerError"}},"security":[{"oauth2":["projects:read","downloads:read"]}],"tags":["Projects"]}},"/projects":{"get":{"operationId":"listProjects","summary":"List projects","description":"Lists projects accessible to the authenticated user across their account, optionally filtered to a single workspace.","parameters":[{"name":"workspace_id","in":"query","required":false,"description":"Restrict results to projects in the specified workspace","schema":{"type":"string","format":"uuid"}},{"name":"parent_id","in":"query","required":false,"description":"Limit results to one level of the folder tree: a folder ID for that folder's direct children, or `root` for the top level. Omit it for every level at once.\n","schema":{"anyOf":[{"type":"string","format":"uuid"},{"type":"string","enum":["root"]}]}}],"responses":{"200":{"description":"Successful operation","content":{"application/json":{"schema":{"type":"object","properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/ProjectListItem"}}},"required":["items"]}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/InternalServerError"},"default":{"$ref":"#/components/responses/InternalServerError"}},"security":[{"oauth2":["projects:read"]}],"tags":["Projects"]},"post":{"operationId":"createProject","summary":"Create a new project","description":"Creates a new project record in the database","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AddProjectRequest"}}}},"responses":{"201":{"description":"Project created successfully","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProjectsResponse"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/InternalServerError"},"default":{"$ref":"#/components/responses/InternalServerError"}},"security":[{"oauth2":["projects:write"]}],"tags":["Projects"]}},"/projects/{project_id}/{version}/artifacts":{"get":{"operationId":"listArtifacts","summary":"List produced artifacts for a project version","description":"Returns the durable artifact records (archives today) produced for the version. Metadata only — call the archive endpoint with a prefix_hash to obtain a download URL for a given entry.","parameters":[{"name":"project_id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"version","in":"path","required":true,"schema":{"type":"integer","minimum":1}}],"responses":{"200":{"description":"Artifact list","content":{"application/json":{"schema":{"type":"object","properties":{"artifacts":{"type":"array","items":{"$ref":"#/components/schemas/Artifact"}}},"required":["artifacts"]}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/InternalServerError"},"default":{"$ref":"#/components/responses/InternalServerError"}},"security":[{"oauth2":["projects:read"]}],"tags":["Projects"]}},"/projects/{project_id}/{version}/upload-complete":{"put":{"operationId":"finishUpload","summary":"Finish project upload","description":"Completes a project upload by adding version information","parameters":[{"name":"project_id","in":"path","description":"The project ID","required":true,"schema":{"type":"string","format":"uuid"},"example":"2524b742-8c36-413e-b9f6-2269918618af"},{"name":"version","in":"path","description":"The version number","required":true,"schema":{"type":"integer","format":"int32","minimum":1},"example":3}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/FinishUploadRequest"}}}},"responses":{"200":{"description":"Successful operation","content":{"application/json":{"schema":{"type":"object","properties":{"version":{"description":"The version number","type":"integer","format":"int32","example":3}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"$ref":"#/components/responses/PaymentRequired"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"422":{"$ref":"#/components/responses/ConfigurationRefused"},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/InternalServerError"},"default":{"$ref":"#/components/responses/InternalServerError"}},"security":[{"oauth2":["projects:write"]}],"tags":["Projects"]}},"/projects/{project_id}/{version}/draft":{"get":{"operationId":"getVersionDraft","summary":"Get a version's change set draft","description":"The saved, unapplied file changes for a version. 404 when there is no draft.\n","parameters":[{"name":"project_id","in":"path","description":"The project ID","required":true,"schema":{"type":"string","format":"uuid"},"example":"2524b742-8c36-413e-b9f6-2269918618af"},{"name":"version","in":"path","description":"The target version number","required":true,"schema":{"type":"integer","format":"int32","minimum":1},"example":13}],"responses":{"200":{"description":"Successful operation","content":{"application/json":{"schema":{"$ref":"#/components/schemas/VersionDraft"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/InternalServerError"},"default":{"$ref":"#/components/responses/InternalServerError"}},"security":[{"oauth2":["projects:read"]}],"tags":["Projects"]},"put":{"operationId":"saveVersionDraft","summary":"Save a version's change set draft","description":"Replaces the version's draft. Each path may appear in only one change. In `upload` mode the target is a version that hasn't been uploaded yet, and `base_version` is the earlier live version its files start from. In `edit` mode the target is an uploaded version whose files change in place; new and replacement files are uploaded to its draft staging prefix first. A draft that is being applied can't be changed. A `rio` with an `error` finding is refused with 422.\n","parameters":[{"name":"project_id","in":"path","description":"The project ID","required":true,"schema":{"type":"string","format":"uuid"},"example":"2524b742-8c36-413e-b9f6-2269918618af"},{"name":"version","in":"path","description":"The target version number","required":true,"schema":{"type":"integer","format":"int32","minimum":1},"example":13}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SaveVersionDraftRequest"}}}},"responses":{"200":{"description":"Draft saved","content":{"application/json":{"schema":{"$ref":"#/components/schemas/VersionDraft"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"$ref":"#/components/responses/Conflict"},"422":{"$ref":"#/components/responses/ConfigurationRefused"},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/InternalServerError"},"default":{"$ref":"#/components/responses/InternalServerError"}},"security":[{"oauth2":["projects:write"]}],"tags":["Projects"]},"delete":{"operationId":"deleteVersionDraft","summary":"Discard a version's change set draft","parameters":[{"name":"project_id","in":"path","description":"The project ID","required":true,"schema":{"type":"string","format":"uuid"},"example":"2524b742-8c36-413e-b9f6-2269918618af"},{"name":"version","in":"path","description":"The target version number","required":true,"schema":{"type":"integer","format":"int32","minimum":1},"example":13}],"responses":{"204":{"description":"Draft discarded"},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/InternalServerError"},"default":{"$ref":"#/components/responses/InternalServerError"}},"security":[{"oauth2":["projects:write"]}],"tags":["Projects"]}},"/projects/{project_id}/{version}/draft/apply":{"post":{"operationId":"applyVersionDraft","summary":"Apply a version's change set draft","description":"In `upload` mode, writes the target version's rio from the base version's with the changes applied, then queues a worker job that copies every unchanged file and its renditions forward from the base version and processes only the added and replaced files. In `edit` mode, queues a worker job that moves staged files into place, swaps in the new rio, then deletes what was removed. When comments or approvals are on files the edit replaces or deletes, it answers 409 unless `override_feedback` is set, which needs `override` on Version. Returns immediately; follow the job for progress.\n","parameters":[{"name":"project_id","in":"path","description":"The project ID","required":true,"schema":{"type":"string","format":"uuid"},"example":"2524b742-8c36-413e-b9f6-2269918618af"},{"name":"version","in":"path","description":"The target version number","required":true,"schema":{"type":"integer","format":"int32","minimum":1},"example":13}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApplyVersionDraftRequest"}}}},"responses":{"202":{"description":"Changes applied; files are being copied","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApplyVersionDraftResponse"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"$ref":"#/components/responses/Conflict"},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/InternalServerError"},"default":{"$ref":"#/components/responses/InternalServerError"}},"security":[{"oauth2":["projects:write"]}],"tags":["Projects"]}},"/projects/{project_id}/{version}/configuration/generate":{"post":{"operationId":"generateVersionConfiguration","summary":"Build a configuration from a version's file names","description":"Runs a naming scheme over the version's stored files, or over the paths sent in `files`, and returns the rio it builds, the files it couldn't place and why, and files that land in the same place. Send `files` to iterate on a local folder before uploading it. Nothing is saved unless `save_draft` is set; otherwise publish the rio through a draft. Without a scheme in the body, the project's saved scheme is used. 409 when there's no scheme to use.\n","parameters":[{"name":"project_id","in":"path","description":"The project ID","required":true,"schema":{"type":"string","format":"uuid"},"example":"2524b742-8c36-413e-b9f6-2269918618af"},{"name":"version","in":"path","description":"The version number","required":true,"schema":{"type":"integer","format":"int32","minimum":1},"example":3}],"requestBody":{"required":false,"content":{"application/json":{"examples":{"example":{"$ref":"#/components/examples/NamingSchemeExample"},"layered":{"$ref":"#/components/examples/NamingSchemeLayersExample"}},"schema":{"type":"object","properties":{"scheme":{"$ref":"#/components/schemas/NamingScheme"},"files":{"description":"Paths, relative to the version, to use in place of the version's stored files: the folder you will upload, so a rio can be built and checked before any bytes are sent. Use the same paths the upload will.","type":"array","maxItems":20000,"items":{"type":"string","minLength":1,"maxLength":1024}},"save_draft":{"description":"Also save the rio as the version's draft, in `edit` mode with no file changes, so the rio never has to be sent back: `configuration/validate` and `configuration/evaluate` read the draft when sent no `rio`, and `draft/apply` publishes it. The response then leaves out `rio`, giving its `inputs` and the saved `draft` instead. A rio with an `error` finding isn't saved, and `draft` is absent. Needs the `projects:write` scope and an uploaded version, and can't be sent with `files`. 409 when the version's draft has file changes or is being applied.","type":"boolean","default":false}}}}}},"responses":{"200":{"description":"Successful operation","content":{"application/json":{"schema":{"$ref":"#/components/schemas/GeneratedConfiguration"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"$ref":"#/components/responses/Conflict"},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/InternalServerError"},"default":{"$ref":"#/components/responses/InternalServerError"}},"security":[{"oauth2":["projects:read"]}],"tags":["Projects"]}},"/projects/{project_id}/{version}/configuration/validate":{"post":{"operationId":"validateVersionConfiguration","summary":"Check a rio against a version","description":"Reports what in a rio can never work: options no configuration can select, categories left with none, conditions on rules nothing defines or that form a cycle, and art that no option or view can reach. Maps naming a file the version doesn't hold are listed too, checked against storage or against the paths sent in `files`, so a rio for a local folder can be checked before uploading it. Without a `rio`, the version draft's rio is checked, and 409 when the draft has none. Nothing is saved: publish the rio through a draft. A rio that breaks the schema is refused with 400, its `errors` naming each failing path.\n","parameters":[{"name":"project_id","in":"path","description":"The project ID","required":true,"schema":{"type":"string","format":"uuid"},"example":"2524b742-8c36-413e-b9f6-2269918618af"},{"name":"version","in":"path","description":"The version number","required":true,"schema":{"type":"integer","format":"int32","minimum":1},"example":3}],"requestBody":{"required":true,"content":{"application/json":{"examples":{"example":{"$ref":"#/components/examples/RioExample"}},"schema":{"type":"object","properties":{"rio":{"$ref":"#/components/schemas/Rio"},"files":{"description":"Paths, relative to the version, to use in place of the version's stored files: the folder you will upload, so a rio can be built and checked before any bytes are sent. Use the same paths the upload will.","type":"array","maxItems":20000,"items":{"type":"string","minLength":1,"maxLength":1024}}}}}}},"responses":{"200":{"description":"Successful operation","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ConfigurationValidation"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"$ref":"#/components/responses/Conflict"},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/InternalServerError"},"default":{"$ref":"#/components/responses/InternalServerError"}},"security":[{"oauth2":["projects:read"]}],"tags":["Projects"]}},"/projects/{project_id}/{version}/configuration/evaluate":{"post":{"operationId":"evaluateVersionConfiguration","summary":"Show what a rio draws for a set of choices","description":"Starts from the rio's defaults and makes each choice in turn as the viewer's sidebar would: a Radio choice replaces its category's, a Checkbox choice toggles, `Implies` brings options along, and options whose `Conditions` stop holding give way. Returns the options that end up chosen and, per frame of the chosen view, the files drawn, bottom layer first. Use it to check that Conditions and Implies do what you meant before saving. Without a `rio`, the version draft's rio is used, and 409 when the draft has none. Nothing is saved. 400 when a choice isn't any option's `FrontEndRule`.\n","parameters":[{"name":"project_id","in":"path","description":"The project ID","required":true,"schema":{"type":"string","format":"uuid"},"example":"2524b742-8c36-413e-b9f6-2269918618af"},{"name":"version","in":"path","description":"The version number","required":true,"schema":{"type":"integer","format":"int32","minimum":1},"example":3}],"requestBody":{"required":true,"content":{"application/json":{"examples":{"example":{"$ref":"#/components/examples/EvaluateExample"}},"schema":{"type":"object","properties":{"rio":{"$ref":"#/components/schemas/Rio"},"choices":{"description":"FrontEndRules, chosen in this order after the defaults","type":"array","maxItems":200,"items":{"type":"string"},"default":[]},"frames":{"description":"Only these frames in `frames` of the response, numbered as there. Every frame of the chosen view when left out.\n","type":"array","maxItems":1000,"items":{"type":"integer","format":"int32"}}}}}}},"responses":{"200":{"description":"Successful operation","content":{"application/json":{"schema":{"$ref":"#/components/schemas/EvaluatedConfiguration"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"$ref":"#/components/responses/Conflict"},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/InternalServerError"},"default":{"$ref":"#/components/responses/InternalServerError"}},"security":[{"oauth2":["projects:read"]}],"tags":["Projects"]}},"/projects/{project_id}/{version}/size":{"put":{"operationId":"updateVersionSize","summary":"Update version size","description":"Updates the size of a project version","parameters":[{"name":"project_id","in":"path","description":"The project ID","required":true,"schema":{"type":"string","format":"uuid"},"example":"2524b742-8c36-413e-b9f6-2269918618af"},{"name":"version","in":"path","description":"The version number","required":true,"schema":{"type":"integer","format":"int32","minimum":1},"example":3}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/VersionSizeUpdateRequest"}}}},"responses":{"200":{"description":"Successful operation","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","example":true}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"$ref":"#/components/responses/PaymentRequired"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/InternalServerError"},"default":{"$ref":"#/components/responses/InternalServerError"}},"security":[{"oauth2":["projects:write"]}],"tags":["Projects"]}},"/projects/{project_id}/labels":{"put":{"operationId":"updateProjectLabels","summary":"Update project labels","description":"Updates the labels associated with a project","parameters":[{"name":"project_id","in":"path","description":"The project ID","required":true,"schema":{"type":"string","format":"uuid"},"example":"2524b742-8c36-413e-b9f6-2269918618af"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/EditLabelsRequest"}}}},"responses":{"200":{"description":"Successful operation","content":{"application/json":{"schema":{"$ref":"#/components/schemas/EditLabelsResponse"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/InternalServerError"},"default":{"$ref":"#/components/responses/InternalServerError"}},"security":[{"oauth2":["comments:write"]}],"tags":["Project"]}},"/projects/{project_id}/naming-scheme":{"parameters":[{"name":"project_id","in":"path","description":"The project ID","required":true,"schema":{"type":"string","format":"uuid"},"example":"2524b742-8c36-413e-b9f6-2269918618af"}],"get":{"operationId":"getProjectNamingScheme","summary":"Get the project's naming scheme","description":"Returns the naming scheme saved with the project, which says how file names map to options, views and frames. 404 when the project has none.\n","responses":{"200":{"description":"Successful operation","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SavedNamingScheme"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/InternalServerError"},"default":{"$ref":"#/components/responses/InternalServerError"}},"security":[{"oauth2":["projects:read"]}],"tags":["Projects"]},"put":{"operationId":"saveProjectNamingScheme","summary":"Save the project's naming scheme","description":"Saves the naming scheme new uploads and file edits build options from. It changes no version; generate or publish a configuration to apply it. The body is `{ \"scheme\": … }`, as `configuration/generate` takes it and the `GET` answers it.\n","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"scheme":{"$ref":"#/components/schemas/NamingScheme"}},"required":["scheme"],"additionalProperties":false}}}},"responses":{"200":{"description":"Successful operation","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SavedNamingScheme"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/InternalServerError"},"default":{"$ref":"#/components/responses/InternalServerError"}},"security":[{"oauth2":["projects:write"]}],"tags":["Projects"]},"delete":{"operationId":"deleteProjectNamingScheme","summary":"Remove the project's naming scheme","description":"Removes the saved scheme. Versions keep the options already built from it.","responses":{"204":{"description":"Removed"},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/InternalServerError"},"default":{"$ref":"#/components/responses/InternalServerError"}},"security":[{"oauth2":["projects:write"]}],"tags":["Projects"]}},"/projects/{project_id}/visibility":{"put":{"operationId":"updateProjectVisibility","summary":"Update project visibility","description":"Updates the three-tier visibility of a project. It can't be more visible than its workspace, nor, for a copy, than the project it was copied from; either is a 400.\n","parameters":[{"name":"project_id","in":"path","description":"The project ID","required":true,"schema":{"type":"string","format":"uuid"},"example":"2524b742-8c36-413e-b9f6-2269918618af"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["visibility"],"properties":{"visibility":{"$ref":"#/components/schemas/Visibility"}}}}}},"responses":{"200":{"description":"Successful operation","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"description":"Updated project ID","type":"string","format":"uuid"}},"required":["id"]}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/InternalServerError"},"default":{"$ref":"#/components/responses/InternalServerError"}},"security":[{"oauth2":["projects:write"]}],"tags":["Project"]}},"/projects/{project_id}/comment-labels":{"patch":{"operationId":"updateCommentLabels","summary":"Rename comment labels","description":"Batch renames comment labels across a project","parameters":[{"$ref":"#/components/parameters/ProjectIdPath"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["labels"],"properties":{"labels":{"description":"Map of old label names to new label names","type":"object","additionalProperties":{"type":"string"}}}}}}},"responses":{"200":{"description":"Successful operation","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SuccessResponse"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/InternalServerError"},"default":{"$ref":"#/components/responses/InternalServerError"}},"security":[{"oauth2":["comments:write"]}],"tags":["Projects"]}},"/generations/capability":{"get":{"operationId":"getWorkspaceGenerationCapability","summary":"What a workspace can generate","description":"Whether the workspace's studio has generation set up, the kinds it can make and the models it can ask for. The same answer `GET /generations/{project_id}/enabled` gives for any of the workspace's projects.","parameters":[{"name":"workspace_id","in":"query","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"The workspace's generation capability","content":{"application/json":{"schema":{"$ref":"#/components/schemas/GenerationCapability"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/InternalServerError"},"default":{"$ref":"#/components/responses/InternalServerError"}},"security":[{"oauth2":["projects:read"]}],"tags":["Generations"]}},"/generations/rules":{"put":{"operationId":"setWorkspaceGenerationRules","summary":"Set a workspace's brand rules","description":"Rules every generation in the workspace keeps to, whatever its prompt: `legal` is added to the prompt as rules the scene obeys, and `avoid` to what the result must be free of. Either may hold `{Category}` tokens. An empty string clears that rule.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","additionalProperties":false,"properties":{"workspace_id":{"type":"string","format":"uuid"},"legal":{"type":"string","maxLength":2000},"avoid":{"type":"string","maxLength":2000}},"required":["workspace_id","legal","avoid"]}}}},"responses":{"200":{"description":"The workspace's rules as saved","content":{"application/json":{"schema":{"$ref":"#/components/schemas/GenerationRules"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/InternalServerError"},"default":{"$ref":"#/components/responses/InternalServerError"}},"security":[{"oauth2":["workspaces:write"]}],"tags":["Generations"]}},"/generations/enhance":{"post":{"operationId":"enhanceGenerationPrompt","summary":"Rewrite a prompt in more detail","description":"Rewrites what a person asked for into a fuller description for the model, keeping every `{Category}` token and staying inside the chosen place, light and camera. With an empty `prompt` it suggests one from the context. Nothing is generated or stored; the caller decides whether to use the rewrite.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","additionalProperties":false,"properties":{"workspace_id":{"type":"string","format":"uuid"},"kind":{"$ref":"#/components/schemas/GenerationKind"},"prompt":{"type":"string","maxLength":2000,"description":"What the person wrote. Empty asks for a suggestion."},"context":{"type":"object","additionalProperties":false,"description":"The rest of the recipe, so the rewrite doesn't contradict it","properties":{"environment":{"type":"string","maxLength":2000},"style":{"type":"string","maxLength":2000},"framing":{"type":"string","maxLength":2000},"motion":{"type":"string","maxLength":2000},"configuration":{"type":"string","maxLength":2000,"description":"The car as configured, such as \"Body Paint: Midnight Black; Grade: Premium\""}}}},"required":["workspace_id","kind","prompt"]}}}},"responses":{"200":{"description":"The rewritten prompt","content":{"application/json":{"schema":{"type":"object","properties":{"prompt":{"type":"string"}},"required":["prompt"]}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"409":{"$ref":"#/components/responses/Conflict"},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/InternalServerError"},"default":{"$ref":"#/components/responses/InternalServerError"}},"security":[{"oauth2":["projects:write"]}],"tags":["Generations"]}},"/generations/mine":{"get":{"operationId":"listMyGenerations","summary":"List your own generations, across workspaces","description":"The takes you started, in every workspace of the current studio you can still read, sorted for an inbox: `running` (still rendering), `ready` (finished in the last 14 days and not yet kept or hidden), `failed` (in the last 14 days, not hidden) and `kept` (the latest kept). A kept take carried into later versions is listed once. `counts` sizes the first three; with `limit=0` only `counts` is filled, which is what a badge needs. `workspaces` and `projects` name what the takes belong to.","parameters":[{"name":"workspace_id","in":"query","required":false,"description":"Only takes from this workspace","schema":{"type":"string","format":"uuid"}},{"name":"kind","in":"query","required":false,"schema":{"$ref":"#/components/schemas/GenerationKind"}},{"name":"limit","in":"query","required":false,"description":"How many takes each section returns at most","schema":{"type":"integer","format":"int32","minimum":0,"maximum":100,"default":48}}],"responses":{"200":{"description":"Your takes, by where they are in their life","content":{"application/json":{"schema":{"type":"object","properties":{"running":{"type":"array","items":{"$ref":"#/components/schemas/Generation"}},"ready":{"type":"array","items":{"$ref":"#/components/schemas/Generation"}},"failed":{"type":"array","items":{"$ref":"#/components/schemas/Generation"}},"kept":{"type":"array","items":{"$ref":"#/components/schemas/Generation"}},"counts":{"type":"object","properties":{"running":{"type":"integer","format":"int32"},"ready":{"type":"integer","format":"int32"},"failed":{"type":"integer","format":"int32"}},"required":["running","ready","failed"]},"workspaces":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"name":{"type":"string"}},"required":["id","name"]}},"projects":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"name":{"type":["string","null"]},"workspace_id":{"type":"string","format":"uuid"}},"required":["id","name","workspace_id"]}}},"required":["running","ready","failed","kept","counts","workspaces","projects"]}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/InternalServerError"},"default":{"$ref":"#/components/responses/InternalServerError"}},"security":[{"oauth2":["projects:read"]}],"tags":["Generations"]}},"/generations":{"get":{"operationId":"listWorkspaceGenerations","summary":"List a workspace's generations","description":"Every take made from the workspace's projects that is not in the trash, newest first. A kept take carried into later versions is listed once, as the version it was made from. `counts` holds the size of each status tab for the other filters.","parameters":[{"name":"workspace_id","in":"query","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"project_id","in":"query","required":false,"schema":{"type":"string","format":"uuid"}},{"name":"kind","in":"query","required":false,"schema":{"$ref":"#/components/schemas/GenerationKind"}},{"name":"status","in":"query","required":false,"description":"Which tab: `all` is every take that isn't hidden; `kept` and `hidden` are the takes kept into their version and hidden from the studio.","schema":{"type":"string","enum":["all","pending","complete","failed","kept","hidden"],"default":"all"}},{"name":"creator","in":"query","required":false,"description":"Only the takes this person started","schema":{"type":"string","format":"uuid"}},{"name":"before","in":"query","required":false,"description":"Only takes made before this moment; pass the last page's `next_before`","schema":{"type":"string","format":"date-time"}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","format":"int32","minimum":1,"maximum":100,"default":48}}],"responses":{"200":{"description":"A page of the workspace's takes","content":{"application/json":{"schema":{"type":"object","properties":{"generations":{"type":"array","items":{"$ref":"#/components/schemas/Generation"}},"counts":{"$ref":"#/components/schemas/GenerationCounts"},"next_before":{"type":["string","null"],"format":"date-time","description":"Where the next page starts, or null on the last page"}},"required":["generations","counts","next_before"]}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/InternalServerError"},"default":{"$ref":"#/components/responses/InternalServerError"}},"security":[{"oauth2":["projects:read"]}],"tags":["Generations"]}},"/generations/{project_id}/enabled":{"get":{"operationId":"getGenerationCapability","summary":"Check what a project can generate","description":"Whether generation is enabled for this project, and the kinds it can make. It is enabled when the studio and the workspace both allow image generation and the studio has a Vertex integration. `kinds` is empty when it is not.","parameters":[{"$ref":"#/components/parameters/ProjectIdPath"}],"responses":{"200":{"description":"What the project can generate","content":{"application/json":{"schema":{"$ref":"#/components/schemas/GenerationCapability"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/InternalServerError"},"default":{"$ref":"#/components/responses/InternalServerError"}},"security":[{"oauth2":["projects:read"]}],"tags":["Generations"]}},"/generations/{project_id}/{version}":{"get":{"operationId":"listGenerations","summary":"List a version's generations","description":"Every generation made from this version that is not in the trash, newest first. A kept one is also one of the version's views: select its `rule` to show it.","parameters":[{"$ref":"#/components/parameters/ProjectIdPath"},{"$ref":"#/components/parameters/VersionPath"},{"name":"options","in":"query","description":"Only the generations made from exactly this configuration","required":false,"schema":{"type":"string"},"example":"+VIW+COL"}],"responses":{"200":{"description":"The version's generations","content":{"application/json":{"schema":{"type":"object","properties":{"generations":{"type":"array","items":{"$ref":"#/components/schemas/Generation"}}},"required":["generations"]}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/InternalServerError"},"default":{"$ref":"#/components/responses/InternalServerError"}},"security":[{"oauth2":["projects:read"]}],"tags":["Generations"]},"post":{"operationId":"createGeneration","summary":"Start a generation","description":"Starts a round of `takes` generations from this version. `kind` picks what is made: today that is `image`, a change to the frame uploaded as `primary_key`, steered by up to `max_references` `reference_keys`. Upload both with `POST /generations/{project_id}/{version}/inputs` first; any other key is refused. Describe the change in `fields`, or in `prompt` alone. The server builds the prompt the model receives from them, filling `{Category}` tokens from `options`. Answers 202 with the takes' ids. Each stays `pending` until it completes or fails.","parameters":[{"$ref":"#/components/parameters/ProjectIdPath"},{"$ref":"#/components/parameters/VersionPath"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/GenerationRequest"}}}},"responses":{"202":{"description":"The generation has started","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","format":"uuid","description":"The first take's id","example":"550e8400-e29b-41d4-a716-446655440000"},"ids":{"type":"array","description":"Every take's id, in order","items":{"type":"string","format":"uuid"}},"batch_id":{"type":"string","format":"uuid","description":"The round the takes belong to"}},"required":["id","ids","batch_id"]}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"$ref":"#/components/responses/Conflict"},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/InternalServerError"},"default":{"$ref":"#/components/responses/InternalServerError"}},"security":[{"oauth2":["projects:write"]}],"tags":["Generations"]}},"/generations/{project_id}/{version}/inputs":{"post":{"operationId":"signGenerationInput","summary":"Sign an upload for a generation input","description":"Signs the upload of an image a generation of this version reads. PUT the file to `url` with exactly `headers`, then name `key` in `primary_key` or `reference_keys`.","parameters":[{"$ref":"#/components/parameters/ProjectIdPath"},{"$ref":"#/components/parameters/VersionPath"}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","properties":{"content_type":{"type":"string","enum":["image/png","image/jpeg","image/webp"],"description":"The image's media type. The generation reads it as this type.","default":"image/png","example":"image/png"}}}}}},"responses":{"200":{"description":"The signed upload","content":{"application/json":{"schema":{"type":"object","properties":{"url":{"type":"string","description":"Where to PUT the file"},"key":{"type":"string","description":"The key to name in the generation request","example":"2524b742-8c36-413e-b9f6-2269918618af/1/ai-queue/9b2f6c1e-7d4a-4c1b-9e0f-3a5d8c2b1e47.png"},"headers":{"type":"object","description":"Headers the upload must send exactly; they are part of the signature","additionalProperties":{"type":"string"},"example":{"Content-Type":"image/png","x-amz-tagging":"status=temporary"}}},"required":["url","key","headers"]}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/InternalServerError"},"default":{"$ref":"#/components/responses/InternalServerError"}},"security":[{"oauth2":["projects:write"]}],"tags":["Generations"]}},"/generations/{project_id}/{version}/{generation_id}":{"patch":{"operationId":"updateGeneration","summary":"Hide, show or tag a generation","description":"Hiding only takes it out of the studio's shots; a kept take stays in its version.","parameters":[{"$ref":"#/components/parameters/ProjectIdPath"},{"$ref":"#/components/parameters/VersionPath"},{"$ref":"#/components/parameters/GenerationIdPath"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","additionalProperties":false,"minProperties":1,"properties":{"hidden":{"type":"boolean"},"tags":{"$ref":"#/components/schemas/GenerationTags"}}}}}},"responses":{"200":{"description":"Updated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SuccessResponse"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/InternalServerError"},"default":{"$ref":"#/components/responses/InternalServerError"}},"security":[{"oauth2":["projects:write"]}],"tags":["Generations"]},"delete":{"operationId":"discardGeneration","summary":"Discard a generation","description":"Moves a generation to the trash. A kept image leaves the version's rio first, and the comments made on it go to the trash with it. Its stored file stays until the version's media is purged.","parameters":[{"$ref":"#/components/parameters/ProjectIdPath"},{"$ref":"#/components/parameters/VersionPath"},{"$ref":"#/components/parameters/GenerationIdPath"}],"responses":{"200":{"description":"The generation is in the trash","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SuccessResponse"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/InternalServerError"},"default":{"$ref":"#/components/responses/InternalServerError"}},"security":[{"oauth2":["projects:write"]}],"tags":["Generations"]}},"/generations/{project_id}/{version}/{generation_id}/lineage":{"get":{"operationId":"getGenerationLineage","summary":"A generation's lineage","description":"The takes it was iterated from, oldest first and ending with itself, and the takes made from it directly. Each iterate, reroll and fix is a take of its own, so any of them can be the starting point again.","parameters":[{"$ref":"#/components/parameters/ProjectIdPath"},{"$ref":"#/components/parameters/VersionPath"},{"$ref":"#/components/parameters/GenerationIdPath"}],"responses":{"200":{"description":"Its lineage","content":{"application/json":{"schema":{"type":"object","properties":{"ancestors":{"type":"array","description":"From the first take to this one, inclusive","items":{"$ref":"#/components/schemas/Generation"}},"children":{"type":"array","items":{"$ref":"#/components/schemas/Generation"}}},"required":["ancestors","children"]}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/InternalServerError"},"default":{"$ref":"#/components/responses/InternalServerError"}},"security":[{"oauth2":["projects:read"]}],"tags":["Generations"]}},"/generations/{project_id}/{version}/{generation_id}/keep":{"post":{"operationId":"keepGeneration","summary":"Keep a generated image","description":"Keeps a complete image, optionally under a new name. It joins the version's rio as a hidden Still view beside the view it was generated from, showing only its frame, and its renditions are made like an upload's. Selecting its `rule` shows it in place of the original, which returns when the configuration changes.","parameters":[{"$ref":"#/components/parameters/ProjectIdPath"},{"$ref":"#/components/parameters/VersionPath"},{"$ref":"#/components/parameters/GenerationIdPath"}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","description":"The name to keep it under. It keeps its current one when this is absent.","example":"Front 3/4, blue tweak"}}}}}},"responses":{"200":{"description":"The image is one of the version's views","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SuccessResponse"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"$ref":"#/components/responses/Conflict"},"422":{"$ref":"#/components/responses/ConfigurationRefused"},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/InternalServerError"},"default":{"$ref":"#/components/responses/InternalServerError"}},"security":[{"oauth2":["projects:write"]}],"tags":["Generations"]}},"/generations/{project_id}/{version}/{generation_id}/retry":{"post":{"operationId":"retryGeneration","summary":"Retry a failed generation","description":"Runs a failed generation again with what it was started with. It is `pending` again until it completes or fails.","parameters":[{"$ref":"#/components/parameters/ProjectIdPath"},{"$ref":"#/components/parameters/VersionPath"},{"$ref":"#/components/parameters/GenerationIdPath"}],"responses":{"202":{"description":"The generation has started again","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SuccessResponse"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"$ref":"#/components/responses/Conflict"},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/InternalServerError"},"default":{"$ref":"#/components/responses/InternalServerError"}},"security":[{"oauth2":["projects:write"]}],"tags":["Generations"]}},"/generations/{project_id}/{version}/{generation_id}/extend":{"post":{"operationId":"extendGeneration","summary":"Extend a clip by seven seconds","description":"Starts a new take that continues this finished clip for another 7 seconds, up to 37 in all, with the same model, resolution and sound. `fields.motion` says what happens next; the clip's own fields are used when it is left out. The new take is a child of this one in the lineage. Veo reads the clip from Cloud Storage, so the studio's Vertex integration needs a `staging_bucket`; without one the request is refused with 409.","parameters":[{"$ref":"#/components/parameters/ProjectIdPath"},{"$ref":"#/components/parameters/VersionPath"},{"$ref":"#/components/parameters/GenerationIdPath"}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","additionalProperties":false,"properties":{"fields":{"$ref":"#/components/schemas/GenerationFields"},"seed":{"type":"integer","format":"int32","minimum":0}}}}}},"responses":{"202":{"description":"The extension has started","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","format":"uuid","description":"The new take's id"},"batch_id":{"type":"string","format":"uuid"}},"required":["id","batch_id"]}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"$ref":"#/components/responses/Conflict"},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/InternalServerError"},"default":{"$ref":"#/components/responses/InternalServerError"}},"security":[{"oauth2":["projects:write"]}],"tags":["Generations"]}},"/generation-templates":{"get":{"operationId":"listGenerationTemplates","summary":"List a workspace's generation templates","description":"Recipes saved for a workspace to start generations from, most recently changed first. Filter by `kind` to list only those for one kind.","parameters":[{"name":"workspace_id","in":"query","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"kind","in":"query","required":false,"schema":{"$ref":"#/components/schemas/GenerationKind"}}],"responses":{"200":{"description":"The workspace's templates","content":{"application/json":{"schema":{"type":"object","properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/GenerationTemplate"}}},"required":["items"]}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/InternalServerError"},"default":{"$ref":"#/components/responses/InternalServerError"}},"security":[{"oauth2":["projects:read"]}],"tags":["Generations"]},"post":{"operationId":"createGenerationTemplate","summary":"Save a generation template","description":"Saves a recipe for everyone in the workspace to start from. A name is unique within a workspace and kind, ignoring case.","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/GenerationTemplateCreate"}}}},"responses":{"201":{"description":"The template was saved","content":{"application/json":{"schema":{"$ref":"#/components/schemas/GenerationTemplate"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"409":{"$ref":"#/components/responses/Conflict"},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/InternalServerError"},"default":{"$ref":"#/components/responses/InternalServerError"}},"security":[{"oauth2":["projects:write"]}],"tags":["Generations"]}},"/generation-templates/{template_id}":{"put":{"operationId":"updateGenerationTemplate","summary":"Change a generation template","description":"Replaces the template's name, fields and settings.","parameters":[{"$ref":"#/components/parameters/TemplateIdPath"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/GenerationTemplateUpdate"}}}},"responses":{"200":{"description":"The template as saved","content":{"application/json":{"schema":{"$ref":"#/components/schemas/GenerationTemplate"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"$ref":"#/components/responses/Conflict"},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/InternalServerError"},"default":{"$ref":"#/components/responses/InternalServerError"}},"security":[{"oauth2":["projects:write"]}],"tags":["Generations"]},"delete":{"operationId":"deleteGenerationTemplate","summary":"Delete a generation template","description":"Puts the template in the trash. Generations made from it keep their recipe.","parameters":[{"$ref":"#/components/parameters/TemplateIdPath"}],"responses":{"200":{"description":"The template is in the trash","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SuccessResponse"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/InternalServerError"},"default":{"$ref":"#/components/responses/InternalServerError"}},"security":[{"oauth2":["projects:write"]}],"tags":["Generations"]}},"/generation-locations":{"get":{"operationId":"listGenerationLocations","summary":"List a workspace's generation locations","description":"Places the workspace keeps generating in, by name. `q` matches the name, slug or description; `category` narrows to one category.","parameters":[{"name":"workspace_id","in":"query","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"category","in":"query","required":false,"schema":{"type":"string","maxLength":60}},{"name":"q","in":"query","required":false,"schema":{"type":"string","maxLength":120}}],"responses":{"200":{"description":"The workspace's locations","content":{"application/json":{"schema":{"type":"object","properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/GenerationLocation"}},"categories":{"type":"array","description":"Every category the workspace's locations use, for a filter","items":{"type":"string"}}},"required":["items","categories"]}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/InternalServerError"},"default":{"$ref":"#/components/responses/InternalServerError"}},"security":[{"oauth2":["projects:read"]}],"tags":["Generations"]},"post":{"operationId":"createGenerationLocation","summary":"Add a generation location","description":"Adds a place for everyone in the workspace to generate in. Its slug is unique within the workspace, ignoring case; left out, it is made from the name.","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/GenerationLocationCreate"}}}},"responses":{"201":{"description":"The location was added","content":{"application/json":{"schema":{"$ref":"#/components/schemas/GenerationLocation"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"409":{"$ref":"#/components/responses/Conflict"},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/InternalServerError"},"default":{"$ref":"#/components/responses/InternalServerError"}},"security":[{"oauth2":["projects:write"]}],"tags":["Generations"]}},"/generation-locations/{location_id}":{"get":{"operationId":"getGenerationLocation","summary":"Get a generation location","parameters":[{"$ref":"#/components/parameters/LocationIdPath"},{"name":"workspace_id","in":"query","required":false,"description":"The workspace a built-in location is being looked at from. Required for a built-in, which belongs to no workspace; ignored otherwise.","schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"The location","content":{"application/json":{"schema":{"$ref":"#/components/schemas/GenerationLocation"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/InternalServerError"},"default":{"$ref":"#/components/responses/InternalServerError"}},"security":[{"oauth2":["projects:read"]}],"tags":["Generations"]},"put":{"operationId":"updateGenerationLocation","summary":"Change a generation location","description":"Replaces the location's name, slug, category, description and fields.","parameters":[{"$ref":"#/components/parameters/LocationIdPath"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/GenerationLocationUpdate"}}}},"responses":{"200":{"description":"The location as saved","content":{"application/json":{"schema":{"$ref":"#/components/schemas/GenerationLocation"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"$ref":"#/components/responses/Conflict"},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/InternalServerError"},"default":{"$ref":"#/components/responses/InternalServerError"}},"security":[{"oauth2":["projects:write"]}],"tags":["Generations"]},"delete":{"operationId":"deleteGenerationLocation","summary":"Delete a generation location","description":"Puts the location in the trash. Takes made in it keep their recipe.","parameters":[{"$ref":"#/components/parameters/LocationIdPath"}],"responses":{"200":{"description":"The location is in the trash","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SuccessResponse"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/InternalServerError"},"default":{"$ref":"#/components/responses/InternalServerError"}},"security":[{"oauth2":["projects:write"]}],"tags":["Generations"]}},"/generation-locations/{location_id}/rounds":{"get":{"operationId":"listGenerationLocationRounds","summary":"List a location's curation rounds","description":"The rounds of candidates made for this location, newest first, with the candidates of each. A round still running lists its pending candidates; poll until none is pending.","parameters":[{"$ref":"#/components/parameters/LocationIdPath"}],"responses":{"200":{"description":"The location's rounds","content":{"application/json":{"schema":{"type":"object","properties":{"rounds":{"type":"array","items":{"$ref":"#/components/schemas/GenerationLocationRound"}}},"required":["rounds"]}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/InternalServerError"},"default":{"$ref":"#/components/responses/InternalServerError"}},"security":[{"oauth2":["projects:read"]}],"tags":["Generations"]},"post":{"operationId":"createGenerationLocationRound","summary":"Generate a round of candidates for a location","description":"Makes empty plates of the place from the location's Environment, Style, Legal and Avoid fields, with no car and no project, each with its own seed. Promote the best through `PUT /generation-locations/{location_id}/anchors`.","parameters":[{"$ref":"#/components/parameters/LocationIdPath"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","additionalProperties":false,"properties":{"model":{"type":"string","description":"An image model from `GET /generations/capability`","example":"gemini-3-pro-image"},"candidates":{"type":"integer","format":"int32","minimum":1,"maximum":8,"default":4},"seed":{"type":"integer","format":"int32","minimum":0,"description":"The first candidate's seed; each next one adds one. Random when left out."}},"required":["model"]}}}},"responses":{"202":{"description":"The round is queued","content":{"application/json":{"schema":{"type":"object","properties":{"batch_id":{"type":"string","format":"uuid"},"ids":{"type":"array","items":{"type":"string","format":"uuid"}}},"required":["batch_id","ids"]}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"$ref":"#/components/responses/Conflict"},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/InternalServerError"},"default":{"$ref":"#/components/responses/InternalServerError"}},"security":[{"oauth2":["projects:write"]}],"tags":["Generations"]}},"/generation-locations/{location_id}/rounds/{batch_id}":{"delete":{"operationId":"deleteGenerationLocationRound","summary":"Discard a location's round","description":"Puts the round's candidates in the trash. Anchors already promoted from it keep their stills.","parameters":[{"$ref":"#/components/parameters/LocationIdPath"},{"name":"batch_id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"The round is in the trash","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SuccessResponse"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/InternalServerError"},"default":{"$ref":"#/components/responses/InternalServerError"}},"security":[{"oauth2":["projects:write"]}],"tags":["Generations"]}},"/generation-locations/{location_id}/anchors":{"put":{"operationId":"setGenerationLocationAnchors","summary":"Promote takes as a location's anchors","description":"Replaces the location's anchors with these takes, in order. An anchor is a still that goes in as a reference wherever the location is chosen, so the place stays the same from shot to shot. Each must be a finished image take in the location's workspace or a finished candidate from one of this location's rounds. An empty list clears them; takes already made keep the anchors they were made with.","parameters":[{"$ref":"#/components/parameters/LocationIdPath"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","additionalProperties":false,"properties":{"generation_ids":{"type":"array","maxItems":2,"uniqueItems":true,"items":{"type":"string","format":"uuid"}}},"required":["generation_ids"]}}}},"responses":{"200":{"description":"The location with its new anchors","content":{"application/json":{"schema":{"$ref":"#/components/schemas/GenerationLocation"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/InternalServerError"},"default":{"$ref":"#/components/responses/InternalServerError"}},"security":[{"oauth2":["projects:write"]}],"tags":["Generations"]}},"/generation-locations/{location_id}/references":{"post":{"operationId":"signGenerationLocationReference","summary":"Sign an upload for a location's reference image","description":"Signs the upload of a reference image for this location: a photo of the real place, or a still from an earlier campaign. PUT the file to `url` with exactly `headers`, then add `key` to the location with `PUT /generation-locations/{location_id}/references`.","parameters":[{"$ref":"#/components/parameters/LocationIdPath"}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","additionalProperties":false,"properties":{"content_type":{"type":"string","enum":["image/png","image/jpeg","image/webp"],"default":"image/png","example":"image/jpeg"}}}}}},"responses":{"200":{"description":"The signed upload","content":{"application/json":{"schema":{"type":"object","properties":{"url":{"type":"string","description":"Where to PUT the file"},"key":{"type":"string","description":"The key to name in `PUT /generation-locations/{location_id}/references`"},"headers":{"type":"object","description":"Headers the upload must send exactly; they are part of the signature","additionalProperties":{"type":"string"}}},"required":["url","key","headers"]}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/InternalServerError"},"default":{"$ref":"#/components/responses/InternalServerError"}},"security":[{"oauth2":["projects:write"]}],"tags":["Generations"]},"put":{"operationId":"setGenerationLocationReferences","summary":"Set a location's reference images","description":"Replaces the location's reference images with these keys, in order. Every round of candidates sends them to the model with the prompt; a round whose model takes fewer references than the location has is refused. Each key must come from `POST /generation-locations/{location_id}/references`. An empty list clears them.","parameters":[{"$ref":"#/components/parameters/LocationIdPath"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","additionalProperties":false,"properties":{"keys":{"type":"array","maxItems":6,"uniqueItems":true,"items":{"type":"string","maxLength":512}}},"required":["keys"]}}}},"responses":{"200":{"description":"The location with its new reference images","content":{"application/json":{"schema":{"$ref":"#/components/schemas/GenerationLocation"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/InternalServerError"},"default":{"$ref":"#/components/responses/InternalServerError"}},"security":[{"oauth2":["projects:write"]}],"tags":["Generations"]}},"/artifacts":{"get":{"operationId":"listMyArtifacts","summary":"List the caller's produced archives","description":"Produced artifacts (archives) across every project the caller can see in their current account and may download (`downloads:read`), newest first. Metadata only — fetch a download URL from the archive endpoint by prefix_hash. Powers the Downloads page.","responses":{"200":{"description":"Artifact list","content":{"application/json":{"schema":{"type":"object","properties":{"artifacts":{"type":"array","items":{"$ref":"#/components/schemas/ArchiveListItem"}}},"required":["artifacts"]}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/InternalServerError"},"default":{"$ref":"#/components/responses/InternalServerError"}},"security":[{"oauth2":["projects:read"]}],"tags":["Projects"]}},"/approvals":{"get":{"operationId":"getMyApprovals","summary":"List approvals for the current user","description":"Lists approvals where the current user is either the requester or the reviewer.\nJoins project name and filetype. Used by the Review hub page.\n","parameters":[{"name":"role","in":"query","description":"Filter to approvals where the current user is the reviewer (`reviewer`), the requester (`requester`), or either (`any`)","required":false,"schema":{"type":"string","enum":["reviewer","requester","any"],"default":"any"}},{"name":"state","in":"query","description":"Filter by open approvals (pending or in_review), closed approvals (approved or changes_requested), or all","required":false,"schema":{"type":"string","enum":["open","closed","all"],"default":"all"}}],"responses":{"200":{"description":"Successful operation","content":{"application/json":{"schema":{"type":"object","properties":{"approvals":{"type":"array","items":{"$ref":"#/components/schemas/ApprovalListItem"}}},"required":["approvals"]}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/InternalServerError"},"default":{"$ref":"#/components/responses/InternalServerError"}},"security":[{"oauth2":["approvals:read"]}],"tags":["Approvals"]}},"/approvals/{project_id}/{version}":{"get":{"operationId":"getApprovals","summary":"List approvals","description":"Gets the approval history for a project version","parameters":[{"$ref":"#/components/parameters/ProjectIdPath"},{"$ref":"#/components/parameters/VersionPath"}],"responses":{"200":{"description":"Successful operation","content":{"application/json":{"schema":{"type":"object","properties":{"approvals":{"type":"array","items":{"$ref":"#/components/schemas/Approval"}}},"required":["approvals"]}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/InternalServerError"},"default":{"$ref":"#/components/responses/InternalServerError"}},"security":[{"oauth2":["approvals:read"]}],"tags":["Approvals"]},"post":{"operationId":"requestApproval","summary":"Request approval","description":"Requests a new approval for a project version","parameters":[{"$ref":"#/components/parameters/ProjectIdPath"},{"$ref":"#/components/parameters/VersionPath"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"reviewer_id":{"description":"The user ID of the assigned reviewer","type":"string","format":"uuid","example":"550e8400-e29b-41d4-a716-446655440000"}},"required":["reviewer_id"]}}}},"responses":{"201":{"description":"Approval requested successfully","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"number":{"type":"integer","format":"int32"}},"required":["id","number"]}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"description":"Conflict - an approval is already open for this version","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/InternalServerError"},"default":{"$ref":"#/components/responses/InternalServerError"}},"security":[{"oauth2":["approvals:write"]}],"tags":["Approvals"]}},"/approvals/{project_id}/{version}/{approval_id}/status":{"put":{"operationId":"setApprovalStatus","summary":"Transition approval status","description":"Reviewer-driven state machine for an approval. Replaces the\nper-action endpoints (approve, start-review, submit-changes, revert, reopen).\n\nAllowed transitions:\n- pending → in_review (quiet — reviewer enters review mode)\n- pending → approved (notifies requester)\n- in_review → approved (notifies requester)\n- in_review → changes_requested (notifies requester)\n- in_review → pending (revert; trashes the notes written during this review)\n- approved → pending (reopen; notifies requester)\n- changes_requested → pending (reopen; notifies requester)\n","parameters":[{"$ref":"#/components/parameters/ProjectIdPath"},{"$ref":"#/components/parameters/VersionPath"},{"$ref":"#/components/parameters/ApprovalIdPath"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"string","enum":["pending","in_review","approved","changes_requested"]}},"required":["status"]}}}},"responses":{"200":{"description":"Status updated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SuccessResponse"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"description":"Conflict - transition not allowed from current state","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/InternalServerError"},"default":{"$ref":"#/components/responses/InternalServerError"}},"security":[{"oauth2":["approvals:write"]}],"tags":["Approvals"]}},"/approvals/{project_id}/{version}/{approval_id}/seen":{"put":{"operationId":"markApprovalViewsSeen","summary":"Mark views seen in a review","description":"The reviewer records the views they have looked at while reviewing, so the round shows\nhow much of the version was seen before the decision. Marks only add, kept in the order\nsent; a decision keeps them, and cancelling the review or starting a new one clears them.\n","parameters":[{"$ref":"#/components/parameters/ProjectIdPath"},{"$ref":"#/components/parameters/VersionPath"},{"$ref":"#/components/parameters/ApprovalIdPath"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"views":{"type":"array","maxItems":200,"items":{"type":"string","minLength":1,"maxLength":200}}},"required":["views"]}}}},"responses":{"200":{"description":"The views seen in this review so far","content":{"application/json":{"schema":{"type":"object","properties":{"seen_views":{"type":"array","items":{"type":"string"}}},"required":["seen_views"]}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"$ref":"#/components/responses/Conflict"},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/InternalServerError"},"default":{"$ref":"#/components/responses/InternalServerError"}},"security":[{"oauth2":["approvals:write"]}],"tags":["Approvals"]}},"/approvals/{project_id}/{version}/{approval_id}/approve":{"put":{"operationId":"approveApproval","summary":"Approve","deprecated":true,"description":"Deprecated. Use `PUT /approvals/{project_id}/{version}/{approval_id}/status`\nwith `{ \"status\": \"approved\" }`.\n","parameters":[{"$ref":"#/components/parameters/ProjectIdPath"},{"$ref":"#/components/parameters/VersionPath"},{"$ref":"#/components/parameters/ApprovalIdPath"}],"responses":{"200":{"description":"Successful operation","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SuccessResponse"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"description":"Conflict - approval is already closed","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/InternalServerError"},"default":{"$ref":"#/components/responses/InternalServerError"}},"security":[{"oauth2":["approvals:write"]}],"tags":["Approvals"]}},"/approvals/{project_id}/{version}/{approval_id}/start-review":{"put":{"operationId":"startReview","summary":"Start review","deprecated":true,"description":"Deprecated. Use `PUT /approvals/{project_id}/{version}/{approval_id}/status`\nwith `{ \"status\": \"in_review\" }`.\n\nReviewer enters review mode, tagging subsequent comments to this approval.\n","parameters":[{"$ref":"#/components/parameters/ProjectIdPath"},{"$ref":"#/components/parameters/VersionPath"},{"$ref":"#/components/parameters/ApprovalIdPath"}],"responses":{"200":{"description":"Successful operation","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SuccessResponse"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"description":"Conflict - approval is not in pending state","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/InternalServerError"},"default":{"$ref":"#/components/responses/InternalServerError"}},"security":[{"oauth2":["approvals:write"]}],"tags":["Approvals"]}},"/approvals/{project_id}/{version}/{approval_id}/submit-changes":{"put":{"operationId":"submitChanges","summary":"Submit change requests","deprecated":true,"description":"Deprecated. Use `PUT /approvals/{project_id}/{version}/{approval_id}/status`\nwith `{ \"status\": \"changes_requested\" }`.\n\nReviewer submits their change requests, closing the review.\n","parameters":[{"$ref":"#/components/parameters/ProjectIdPath"},{"$ref":"#/components/parameters/VersionPath"},{"$ref":"#/components/parameters/ApprovalIdPath"}],"responses":{"200":{"description":"Successful operation","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SuccessResponse"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"description":"Conflict - approval is not in review mode","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/InternalServerError"},"default":{"$ref":"#/components/responses/InternalServerError"}},"security":[{"oauth2":["approvals:write"]}],"tags":["Approvals"]}},"/approvals/{project_id}/{version}/{approval_id}/revert":{"put":{"operationId":"revertApproval","summary":"Revert to pending","deprecated":true,"description":"Deprecated. Use `PUT /approvals/{project_id}/{version}/{approval_id}/status`\nwith `{ \"status\": \"pending\" }` from an in_review approval.\n\nReviewer reverts the approval back to pending, deleting any comments made during this review session.\n","parameters":[{"$ref":"#/components/parameters/ProjectIdPath"},{"$ref":"#/components/parameters/VersionPath"},{"$ref":"#/components/parameters/ApprovalIdPath"}],"responses":{"200":{"description":"Successful operation","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SuccessResponse"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"description":"Conflict - approval is not in review mode","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/InternalServerError"},"default":{"$ref":"#/components/responses/InternalServerError"}},"security":[{"oauth2":["approvals:write"]}],"tags":["Approvals"]}},"/approvals/{project_id}/{version}/{approval_id}/nudge":{"post":{"operationId":"nudgeApproval","summary":"Nudge the reviewer","description":"Requester re-sends the `approval_requested` notification to the assigned reviewer.\nOnly valid for open approvals (pending or in_review). Idempotent within a\n30-minute window — repeated calls return 200 but suppress the notification.\n","parameters":[{"$ref":"#/components/parameters/ProjectIdPath"},{"$ref":"#/components/parameters/VersionPath"},{"$ref":"#/components/parameters/ApprovalIdPath"}],"responses":{"200":{"description":"Notification re-sent","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SuccessResponse"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"description":"Conflict - approval is closed","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/InternalServerError"},"default":{"$ref":"#/components/responses/InternalServerError"}},"security":[{"oauth2":["approvals:write"]}],"tags":["Approvals"]}},"/approvals/{project_id}/{version}/{approval_id}/reopen":{"put":{"operationId":"reopenApproval","summary":"Reopen a closed approval","deprecated":true,"description":"Deprecated. Use `PUT /approvals/{project_id}/{version}/{approval_id}/status`\nwith `{ \"status\": \"pending\" }` from an approved or changes_requested approval.\n\nReviewer flips a closed approval (approved or changes_requested) back to pending.\nUsed to undo an accidental Approve or Request changes. Notifies the requester.\n","parameters":[{"$ref":"#/components/parameters/ProjectIdPath"},{"$ref":"#/components/parameters/VersionPath"},{"$ref":"#/components/parameters/ApprovalIdPath"}],"responses":{"200":{"description":"Approval reopened","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SuccessResponse"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"description":"Conflict - approval is not in a closed state","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/InternalServerError"},"default":{"$ref":"#/components/responses/InternalServerError"}},"security":[{"oauth2":["approvals:write"]}],"tags":["Approvals"]}},"/approvals/{project_id}/{version}/{approval_id}/cancel":{"put":{"operationId":"cancelApproval","summary":"Cancel approval request","description":"Requester cancels the approval request while it is still pending","parameters":[{"$ref":"#/components/parameters/ProjectIdPath"},{"$ref":"#/components/parameters/VersionPath"},{"$ref":"#/components/parameters/ApprovalIdPath"}],"responses":{"200":{"description":"Successful operation","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SuccessResponse"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"description":"Conflict - approval cannot be cancelled in its current state","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/InternalServerError"},"default":{"$ref":"#/components/responses/InternalServerError"}},"security":[{"oauth2":["approvals:write"]}],"tags":["Approvals"]}},"/comments/{project_id}/mention-candidates":{"get":{"operationId":"getMentionCandidates","summary":"Search candidates for @-mention in a comment","description":"Returns people the caller can @-mention on this project. For v1 this\nis account-scoped (the project's workspace's account members); future\nrevisions will merge in share recipients for shares of this project.\nCaller must be a workspace member of the project's workspace; share\nguests get a 403 and their comment UI falls back to free-form text.\n","parameters":[{"$ref":"#/components/parameters/ProjectIdPath"},{"name":"q","in":"query","required":true,"schema":{"type":"string","minLength":1,"maxLength":255}},{"name":"limit","in":"query","schema":{"type":"integer","minimum":1,"maximum":25,"default":10}}],"responses":{"200":{"description":"Search results","content":{"application/json":{"schema":{"type":"object","required":["matches"],"properties":{"matches":{"type":"array","items":{"$ref":"#/components/schemas/MentionCandidate"}}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/InternalServerError"},"default":{"$ref":"#/components/responses/InternalServerError"}},"security":[{"oauth2":["members:read"]}],"tags":["Comments"]}},"/comments/{project_id}/{version}":{"get":{"operationId":"getComments","summary":"List comments","description":"Gets all comments for a project version. A comment written under an approval that is\nstill in review is listed only for its author, the reviewer, until they approve or\nrequest changes; cancelling the review trashes it.\n\n`on=files` lists the comments made on the version's stored files instead, each naming\nits `file_path`. Those need the same access as the file list: read access to the version,\nnot through a share link, and the `projects:read` scope.\n","parameters":[{"$ref":"#/components/parameters/ProjectIdPath"},{"$ref":"#/components/parameters/VersionPath"},{"$ref":"#/components/parameters/ShareIdQuery"},{"name":"on","in":"query","description":"Which comments to list, the version's (the default) or its files'.","required":false,"schema":{"type":"string","enum":["version","files"],"default":"version"}}],"responses":{"200":{"description":"Successful operation","content":{"application/json":{"schema":{"type":"object","properties":{"comments":{"type":"array","items":{"$ref":"#/components/schemas/Comment"}}},"required":["comments"]}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/InternalServerError"},"default":{"$ref":"#/components/responses/InternalServerError"}},"security":[{"oauth2":["comments:read"]}],"tags":["Comments"]},"post":{"operationId":"createComment","summary":"Create comment","description":"Creates a new comment on a project version","parameters":[{"$ref":"#/components/parameters/ProjectIdPath"},{"$ref":"#/components/parameters/VersionPath"},{"$ref":"#/components/parameters/ShareIdQuery"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"comment":{"description":"The comment text","type":"string","example":"This looks great!"},"player":{"description":"Player type when the comment was created","type":"string","enum":["video","audio","text","pano","model","still","stills","animation","pdf","exr"],"example":"still"},"config":{"description":"Configuration string for the comment","type":"string","example":"+VIW+COL"},"frame":{"description":"Frame number","type":"integer","format":"int32","example":0},"settings":{"description":"Comment settings (marker, position, etc.)","$ref":"#/components/schemas/CommentSettings"},"labels":{"description":"Comment label","type":"string","example":"feedback"},"attachments":{"description":"Attachments for the comment","type":"array","items":{"$ref":"#/components/schemas/Attachment"}},"approval_id":{"description":"The review this comment is a note in. It must be the caller's own review of\nthis version, in progress; the note stays visible only to them until they\ndecide.\n","type":"string","format":"uuid","nullable":true,"example":"550e8400-e29b-41d4-a716-446655440000"},"ai_image_id":{"description":"Legacy. A comment on a kept generated image carries the image's `rule` in `config` instead.","deprecated":true,"type":"string","format":"uuid","example":"550e8400-e29b-41d4-a716-446655440000"},"file_path":{"description":"One of the version's stored files, to comment on that file rather than a view: `config` must then be empty. Needs read access to the version, not through a share link, and the `projects:read` scope, as listing the files does.\n","type":"string","minLength":1,"example":"textures/paint.png"}},"required":["comment","player","config","frame","settings","labels"]}}}},"responses":{"201":{"description":"Comment created successfully","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"description":"The created comment ID","type":"string","format":"uuid"}},"required":["id"]}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"$ref":"#/components/responses/Conflict"},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/InternalServerError"},"default":{"$ref":"#/components/responses/InternalServerError"}},"security":[{"oauth2":["comments:write"]}],"tags":["Comments"]}},"/comments/{project_id}/{version}/{comment_id}":{"patch":{"operationId":"updateComment","summary":"Update comment","description":"Updates a comment's text","parameters":[{"$ref":"#/components/parameters/ProjectIdPath"},{"$ref":"#/components/parameters/VersionPath"},{"$ref":"#/components/parameters/CommentIdPath"},{"$ref":"#/components/parameters/ShareIdQuery"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"message":{"description":"The updated comment text","minLength":1,"type":"string","example":"Updated feedback text"}},"required":["message"]}}}},"responses":{"200":{"description":"Successful operation","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SuccessResponse"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/InternalServerError"},"default":{"$ref":"#/components/responses/InternalServerError"}},"security":[{"oauth2":["comments:write"]}],"tags":["Comments"]},"delete":{"operationId":"deleteComment","summary":"Delete comment","description":"Deletes a comment and its attachments","parameters":[{"$ref":"#/components/parameters/ProjectIdPath"},{"$ref":"#/components/parameters/VersionPath"},{"$ref":"#/components/parameters/CommentIdPath"},{"$ref":"#/components/parameters/ShareIdQuery"}],"responses":{"200":{"description":"Successful operation","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SuccessResponse"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/InternalServerError"},"default":{"$ref":"#/components/responses/InternalServerError"}},"security":[{"oauth2":["comments:write"]}],"tags":["Comments"]}},"/comments/{project_id}/{version}/{comment_id}/replies":{"post":{"operationId":"createReply","summary":"Create reply","description":"Adds a reply to a comment","parameters":[{"$ref":"#/components/parameters/ProjectIdPath"},{"$ref":"#/components/parameters/VersionPath"},{"$ref":"#/components/parameters/CommentIdPath"},{"$ref":"#/components/parameters/ShareIdQuery"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"comment":{"description":"The reply text","type":"string","example":"Thanks for the feedback!"}},"required":["comment"]}}}},"responses":{"201":{"description":"Reply created successfully","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"description":"The created reply ID","type":"string","format":"uuid"}},"required":["id"]}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/InternalServerError"},"default":{"$ref":"#/components/responses/InternalServerError"}},"security":[{"oauth2":["comments:write"]}],"tags":["Comments"]}},"/comments/{project_id}/{version}/{comment_id}/replies/{reply_id}":{"patch":{"operationId":"updateReply","summary":"Update reply","description":"Updates a reply's text","parameters":[{"$ref":"#/components/parameters/ProjectIdPath"},{"$ref":"#/components/parameters/VersionPath"},{"$ref":"#/components/parameters/CommentIdPath"},{"$ref":"#/components/parameters/ReplyIdPath"},{"$ref":"#/components/parameters/ShareIdQuery"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"message":{"minLength":1,"description":"The updated reply text","type":"string","example":"Updated reply text"}},"required":["message"]}}}},"responses":{"200":{"description":"Successful operation","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SuccessResponse"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/InternalServerError"},"default":{"$ref":"#/components/responses/InternalServerError"}},"security":[{"oauth2":["comments:write"]}],"tags":["Comments"]},"delete":{"operationId":"deleteReply","summary":"Delete reply","description":"Deletes a reply","parameters":[{"$ref":"#/components/parameters/ProjectIdPath"},{"$ref":"#/components/parameters/VersionPath"},{"$ref":"#/components/parameters/CommentIdPath"},{"$ref":"#/components/parameters/ReplyIdPath"},{"$ref":"#/components/parameters/ShareIdQuery"}],"responses":{"200":{"description":"Successful operation","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SuccessResponse"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/InternalServerError"},"default":{"$ref":"#/components/responses/InternalServerError"}},"security":[{"oauth2":["comments:write"]}],"tags":["Comments"]}},"/comments/{project_id}/{version}/{comment_id}/completed":{"put":{"operationId":"setCommentCompleted","summary":"Set completed status","description":"Updates the completed status of a comment","parameters":[{"$ref":"#/components/parameters/ProjectIdPath"},{"$ref":"#/components/parameters/VersionPath"},{"$ref":"#/components/parameters/CommentIdPath"},{"$ref":"#/components/parameters/ShareIdQuery"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"completed":{"description":"Whether the comment is completed","type":"boolean","example":true}},"required":["completed"]}}}},"responses":{"200":{"description":"Successful operation","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SuccessResponse"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/InternalServerError"},"default":{"$ref":"#/components/responses/InternalServerError"}},"security":[{"oauth2":["comments:write"]}],"tags":["Comments"]}},"/comments/{project_id}/{version}/{comment_id}/label":{"put":{"operationId":"setCommentLabel","summary":"Set comment label","description":"Updates the label of a comment","parameters":[{"$ref":"#/components/parameters/ProjectIdPath"},{"$ref":"#/components/parameters/VersionPath"},{"$ref":"#/components/parameters/CommentIdPath"},{"$ref":"#/components/parameters/ShareIdQuery"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"label":{"description":"The new label","type":"string","example":"urgent"}},"required":["label"]}}}},"responses":{"200":{"description":"Successful operation","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SuccessResponse"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/InternalServerError"},"default":{"$ref":"#/components/responses/InternalServerError"}},"security":[{"oauth2":["comments:write"]}],"tags":["Comments"]}},"/comments/{project_id}/labels":{"get":{"operationId":"getLabels","summary":"List labels","description":"Gets all labels for a project","parameters":[{"$ref":"#/components/parameters/ProjectIdPath"},{"$ref":"#/components/parameters/ShareIdQuery"}],"responses":{"200":{"description":"Successful operation","content":{"application/json":{"schema":{"type":"object","properties":{"labels":{"type":"array","items":{"type":"string"},"example":["feedback","urgent","design"]}},"required":["labels"]}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/InternalServerError"},"default":{"$ref":"#/components/responses/InternalServerError"}},"security":[{"oauth2":["comments:read"]}],"tags":["Comments"]},"put":{"operationId":"renameLabels","summary":"Rename labels","description":"Batch rename labels across a project","parameters":[{"$ref":"#/components/parameters/ProjectIdPath"},{"$ref":"#/components/parameters/ShareIdQuery"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"labels":{"description":"Map of old label names to new label names","type":"object","additionalProperties":{"type":"string"},"example":{"feedback":"urgent-feedback","design":"ui-design"}}},"required":["labels"]}}}},"responses":{"200":{"description":"Successful operation","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SuccessResponse"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/InternalServerError"},"default":{"$ref":"#/components/responses/InternalServerError"}},"security":[{"oauth2":["comments:write"]}],"tags":["Comments"]}},"/comments/attachments":{"post":{"operationId":"getAttachmentUploadUrls","summary":"Get upload URLs","description":"Returns presigned S3 URLs for direct browser uploads (max 5 files, jpeg/jpg/png only)","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"project_id":{"description":"Project the attachment belongs to. The caller must have `create Comment` on it.","type":"string","format":"uuid"},"version":{"description":"Version the comment (and its attachment) belongs to. Determines the version-scoped storage folder.","type":"integer","format":"int32"},"folder":{"description":"Storage sub-folder. `attachments` for files attached to a comment, `comments` for images embedded inline in a comment body.","type":"string","enum":["attachments","comments"],"default":"attachments"},"filenames":{"description":"Array of filenames to upload (max 5, jpeg/jpg/png only). Plain leaf names — the server builds the storage path.","type":"array","items":{"type":"string"},"example":["screenshot.jpg","photo.png"]}},"required":["project_id","version","filenames"]}}}},"responses":{"200":{"description":"Successful operation","content":{"application/json":{"schema":{"type":"object","properties":{"attachments":{"type":"array","items":{"type":"object","properties":{"originalname":{"description":"Original filename","type":"string","example":"screenshot.jpg"},"key":{"description":"Version-scoped S3 key for the attachment","type":"string","example":"3fa85f64-5717-4562-b3fc-2c963f66afa6/4/attachments/1618324567-screenshot.jpg"},"presigned_url":{"description":"Presigned URL for direct S3 upload","type":"string","example":"https://s3.amazonaws.com/bucket/..."}}}}},"required":["attachments"]}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/InternalServerError"},"default":{"$ref":"#/components/responses/InternalServerError"}},"security":[{"oauth2":["comments:write"]}],"tags":["Comments"]}},"/accounts/{account_id}/webhooks":{"get":{"operationId":"listWebhookEndpoints","summary":"List a studio's webhook endpoints","description":"Every endpoint the studio sends events to, oldest first.","parameters":[{"$ref":"#/components/parameters/WebhookAccountIdPath"}],"responses":{"200":{"description":"The studio's endpoints","content":{"application/json":{"schema":{"type":"object","properties":{"endpoints":{"type":"array","items":{"$ref":"#/components/schemas/WebhookEndpoint"}}},"required":["endpoints"]}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/InternalServerError"},"default":{"$ref":"#/components/responses/InternalServerError"}},"security":[{"oauth2":["webhooks:read"]}],"tags":["Webhooks"]},"post":{"operationId":"createWebhookEndpoint","summary":"Add a webhook endpoint","description":"Starts sending the chosen events to `url`, signed as [Standard Webhooks](https://www.standardwebhooks.com/) describes with the `secret` in the response. Use `\"*\"` in `event_types` for every event, including ones added later. A studio may have at most 20 endpoints.","parameters":[{"$ref":"#/components/parameters/WebhookAccountIdPath"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookEndpointCreate"}}}},"responses":{"201":{"description":"The endpoint, with its signing secret","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookEndpointWithSecret"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"409":{"description":"The studio already has 20 endpoints","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/InternalServerError"},"default":{"$ref":"#/components/responses/InternalServerError"}},"security":[{"oauth2":["webhooks:write"]}],"tags":["Webhooks"]}},"/accounts/{account_id}/webhooks/{endpoint_id}":{"get":{"operationId":"getWebhookEndpoint","summary":"Read one webhook endpoint","parameters":[{"$ref":"#/components/parameters/WebhookAccountIdPath"},{"$ref":"#/components/parameters/WebhookEndpointIdPath"}],"responses":{"200":{"description":"The endpoint","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookEndpoint"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/InternalServerError"},"default":{"$ref":"#/components/responses/InternalServerError"}},"security":[{"oauth2":["webhooks:read"]}],"tags":["Webhooks"]},"patch":{"operationId":"updateWebhookEndpoint","summary":"Change a webhook endpoint","description":"Changes its URL, description or events, or turns it off and on. A disabled endpoint is sent nothing, and events raised while it is off are not kept for it. Changing the URL clears a pause and sends what it held.","parameters":[{"$ref":"#/components/parameters/WebhookAccountIdPath"},{"$ref":"#/components/parameters/WebhookEndpointIdPath"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookEndpointUpdate"}}}},"responses":{"200":{"description":"The endpoint as it is now","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookEndpoint"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/InternalServerError"},"default":{"$ref":"#/components/responses/InternalServerError"}},"security":[{"oauth2":["webhooks:write"]}],"tags":["Webhooks"]},"delete":{"operationId":"deleteWebhookEndpoint","summary":"Remove a webhook endpoint","description":"Stops sending to it at once. Deliveries not yet made are dropped.","parameters":[{"$ref":"#/components/parameters/WebhookAccountIdPath"},{"$ref":"#/components/parameters/WebhookEndpointIdPath"}],"responses":{"204":{"description":"Removed"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/InternalServerError"},"default":{"$ref":"#/components/responses/InternalServerError"}},"security":[{"oauth2":["webhooks:write"]}],"tags":["Webhooks"]}},"/accounts/{account_id}/webhooks/{endpoint_id}/secret":{"get":{"operationId":"getWebhookSecret","summary":"Read a webhook endpoint's signing secret","parameters":[{"$ref":"#/components/parameters/WebhookAccountIdPath"},{"$ref":"#/components/parameters/WebhookEndpointIdPath"}],"responses":{"200":{"description":"The secret","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookSecret"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/InternalServerError"},"default":{"$ref":"#/components/responses/InternalServerError"}},"security":[{"oauth2":["webhooks:write"]}],"tags":["Webhooks"]},"post":{"operationId":"rotateWebhookSecret","summary":"Replace a webhook endpoint's signing secret","description":"Signs with a new secret from now on. For 24 hours each request also carries a signature made with the old one, so a receiver can switch over without dropping anything.","parameters":[{"$ref":"#/components/parameters/WebhookAccountIdPath"},{"$ref":"#/components/parameters/WebhookEndpointIdPath"}],"responses":{"200":{"description":"The new secret","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookSecret"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/InternalServerError"},"default":{"$ref":"#/components/responses/InternalServerError"}},"security":[{"oauth2":["webhooks:write"]}],"tags":["Webhooks"]}},"/accounts/{account_id}/webhooks/{endpoint_id}/test":{"post":{"operationId":"sendWebhookTest","summary":"Send a test event to a webhook endpoint","description":"Sends a `webhook.test` event straight away, even to a paused or disabled endpoint. Poll the delivery to see how it went.","parameters":[{"$ref":"#/components/parameters/WebhookAccountIdPath"},{"$ref":"#/components/parameters/WebhookEndpointIdPath"}],"responses":{"202":{"description":"The test delivery, queued","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookDelivery"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/InternalServerError"},"default":{"$ref":"#/components/responses/InternalServerError"}},"security":[{"oauth2":["webhooks:write"]}],"tags":["Webhooks"]}},"/accounts/{account_id}/webhooks/{endpoint_id}/resume":{"post":{"operationId":"resumeWebhookEndpoint","summary":"Resume a paused webhook endpoint","description":"An endpoint that has failed for 24 hours is paused, and events for it are held rather than sent. Resuming sends everything it held, oldest first, and starts the 24 hours afresh.","parameters":[{"$ref":"#/components/parameters/WebhookAccountIdPath"},{"$ref":"#/components/parameters/WebhookEndpointIdPath"}],"responses":{"200":{"description":"The endpoint as it is now","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookEndpoint"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/InternalServerError"},"default":{"$ref":"#/components/responses/InternalServerError"}},"security":[{"oauth2":["webhooks:write"]}],"tags":["Webhooks"]}},"/accounts/{account_id}/webhooks/{endpoint_id}/deliveries":{"get":{"operationId":"listWebhookDeliveries","summary":"List a webhook endpoint's recent deliveries","description":"Its 50 newest deliveries, newest first. Deliveries are kept for 90 days.","parameters":[{"$ref":"#/components/parameters/WebhookAccountIdPath"},{"$ref":"#/components/parameters/WebhookEndpointIdPath"},{"name":"status","in":"query","required":false,"description":"Only deliveries in this state.","schema":{"$ref":"#/components/schemas/WebhookDeliveryStatus"}}],"responses":{"200":{"description":"The deliveries","content":{"application/json":{"schema":{"type":"object","properties":{"deliveries":{"type":"array","items":{"$ref":"#/components/schemas/WebhookDelivery"}}},"required":["deliveries"]}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/InternalServerError"},"default":{"$ref":"#/components/responses/InternalServerError"}},"security":[{"oauth2":["webhooks:read"]}],"tags":["Webhooks"]}},"/accounts/{account_id}/webhooks/{endpoint_id}/deliveries/{delivery_id}/redeliver":{"post":{"operationId":"redeliverWebhook","summary":"Send a delivery again","description":"Sends a `delivered` or `failed` delivery again, with the same `webhook-id`, so a receiver that already handled it can tell.","parameters":[{"$ref":"#/components/parameters/WebhookAccountIdPath"},{"$ref":"#/components/parameters/WebhookEndpointIdPath"},{"name":"delivery_id","in":"path","required":true,"description":"The delivery ID","schema":{"type":"string","format":"uuid"}}],"responses":{"202":{"description":"The delivery, queued again","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookDelivery"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"description":"The delivery is still being sent, or is held by a pause","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/InternalServerError"},"default":{"$ref":"#/components/responses/InternalServerError"}},"security":[{"oauth2":["webhooks:write"]}],"tags":["Webhooks"]}},"/accounts/{account_id}/audit-log/files":{"get":{"operationId":"listAuditLogFiles","summary":"List a studio's audit log files","description":"The Parquet files holding the studio's audit events for a time range, each with a presigned download URL valid for 15 minutes. Every event is an OCSF record (`metadata.version` names the OCSF release). Files never change once written, so a client may cache them by `key` and `etag`. A span is served by the coarsest file that covers it (month, then day, then hour), so the files do not overlap. Events received after `written_through` are not in any file yet; read them from `GET /accounts/{account_id}/audit-log/recent` with `since` set to that instant.","parameters":[{"$ref":"#/components/parameters/AuditAccountIdPath"},{"name":"from","in":"query","description":"Start of the range. Defaults to 24 hours before `to`.","schema":{"type":"string","format":"date-time","example":"2026-11-01T00:00:00Z"}},{"name":"to","in":"query","description":"End of the range, exclusive. Defaults to now.","schema":{"type":"string","format":"date-time","example":"2026-11-02T00:00:00Z"}},{"$ref":"#/components/parameters/AuditClassQuery"},{"$ref":"#/components/parameters/AuditKindQuery"}],"responses":{"200":{"description":"The files overlapping the range, oldest first","content":{"application/json":{"schema":{"type":"object","properties":{"files":{"type":"array","items":{"$ref":"#/components/schemas/AuditLogFile"}},"written_through":{"type":"string","format":"date-time","example":"2026-11-02T15:00:00Z","description":"Every event received before this instant is in a file. Absent before the first hour has been written."},"expires_at":{"type":"string","format":"date-time","example":"2026-11-02T15:52:00Z","description":"When the URLs stop working."},"retention_days":{"type":"integer","format":"int32","example":2557,"description":"Days a file is kept after the month it covers ends."}},"required":["files","expires_at","retention_days"]}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/InternalServerError"},"default":{"$ref":"#/components/responses/InternalServerError"}},"security":[{"oauth2":["audit:read"]}],"tags":["Audit log"]}},"/accounts/{account_id}/audit-log/exports":{"post":{"operationId":"requestAuditLogExport","summary":"Export a range of a studio's audit log as one file","description":"Asks for every event in the range, across every class, as a single gzipped NDJSON file of OCSF records, oldest first within each event class. The export is built in the background: poll `GET /accounts/{account_id}/audit-log/exports/{export_id}` until `status` is `ready`, then download it from `url`. The file is kept for 7 days. Exports are built one at a time per instance, so a large one may wait for another to finish. Events received after the files' `written_through` are not in any file yet, so are not exported. A studio may have at most 3 exports pending at once.","parameters":[{"$ref":"#/components/parameters/AuditAccountIdPath"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"from":{"type":"string","format":"date-time","example":"2026-10-01T00:00:00Z"},"to":{"type":"string","format":"date-time","example":"2026-11-01T00:00:00Z","description":"End of the range, exclusive."},"include_reads":{"type":"boolean","default":false,"description":"Also export API Activity reads (`GET` requests), which are far more numerous than writes."}},"required":["from","to"]}}}},"responses":{"202":{"description":"The export was accepted and is being built","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AuditExport"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"409":{"description":"The studio already has 3 exports pending","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/InternalServerError"},"default":{"$ref":"#/components/responses/InternalServerError"}},"security":[{"oauth2":["audit:read"]}],"tags":["Audit log"]},"get":{"operationId":"listAuditLogExports","summary":"List a studio's audit log exports","description":"The 20 most recent exports, newest first, whatever their status.","parameters":[{"$ref":"#/components/parameters/AuditAccountIdPath"}],"responses":{"200":{"description":"The studio's exports","content":{"application/json":{"schema":{"type":"object","properties":{"exports":{"type":"array","items":{"$ref":"#/components/schemas/AuditExport"}}},"required":["exports"]}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/InternalServerError"},"default":{"$ref":"#/components/responses/InternalServerError"}},"security":[{"oauth2":["audit:read"]}],"tags":["Audit log"]}},"/accounts/{account_id}/audit-log/exports/{export_id}":{"get":{"operationId":"getAuditLogExport","summary":"Read one audit log export","description":"Its status, and once `ready`, a download URL valid for 15 minutes.","parameters":[{"$ref":"#/components/parameters/AuditAccountIdPath"},{"name":"export_id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"The export","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AuditExport"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/InternalServerError"},"default":{"$ref":"#/components/responses/InternalServerError"}},"security":[{"oauth2":["audit:read"]}],"tags":["Audit log"]}},"/accounts/{account_id}/audit-log/ledger":{"get":{"operationId":"listAuditLedger","summary":"Read a studio's audit ledger","description":"One entry per hour the log wrote for a class and kind: how many events it holds and a digest of which, each chained to the one before. To check the files: group their events by `metadata.logged_time` into UTC hours; an hour's `digest` is the SHA-256 (lowercase hex) of its `metadata.uid` values sorted by code unit and joined with \"\\n\"; an entry's `hash` is the SHA-256 of `prev_hash` (empty for the first), `hour` (as returned), `events` and `digest`, joined with \"\\n\". A removed, added or altered hour changes its digest, and every hash after it. Hours older than the retention period have no files to check them against.","parameters":[{"$ref":"#/components/parameters/AuditAccountIdPath"},{"name":"from","in":"query","description":"First hour to return. Defaults to 24 hours before `to`.","schema":{"type":"string","format":"date-time","example":"2026-11-01T00:00:00Z"}},{"name":"to","in":"query","description":"End of the range, exclusive. Defaults to now.","schema":{"type":"string","format":"date-time","example":"2026-11-02T00:00:00Z"}},{"$ref":"#/components/parameters/AuditClassQuery"},{"$ref":"#/components/parameters/AuditKindQuery"}],"responses":{"200":{"description":"The range's entries, oldest first","content":{"application/json":{"schema":{"type":"object","properties":{"entries":{"type":"array","items":{"$ref":"#/components/schemas/AuditLedgerEntry"}}},"required":["entries"]}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/InternalServerError"},"default":{"$ref":"#/components/responses/InternalServerError"}},"security":[{"oauth2":["audit:read"]}],"tags":["Audit log"]}},"/accounts/{account_id}/audit-log/recent":{"get":{"operationId":"listRecentAuditEvents","summary":"Read a studio's audit events not yet written to a file","description":"Audit events received since `since`, oldest first, read straight from the event stream. Use it for the newest hour, which `GET /accounts/{account_id}/audit-log/files` does not cover yet. The stream keeps events for 7 days. When `next_cursor` is set there are more events: repeat the request with `cursor` set to it.","parameters":[{"$ref":"#/components/parameters/AuditAccountIdPath"},{"name":"since","in":"query","description":"Return events received at or after this instant. Defaults to the files' `written_through`. Ignored when `cursor` is given.","schema":{"type":"string","format":"date-time","example":"2026-11-02T15:00:00Z"}},{"name":"cursor","in":"query","description":"The `next_cursor` of the previous page.","schema":{"type":"integer","format":"int64","minimum":1}},{"name":"limit","in":"query","schema":{"type":"integer","minimum":1,"maximum":5000,"default":1000}},{"$ref":"#/components/parameters/AuditClassQuery"},{"$ref":"#/components/parameters/AuditKindQuery"}],"responses":{"200":{"description":"A page of events","content":{"application/json":{"schema":{"type":"object","properties":{"events":{"type":"array","items":{"$ref":"#/components/schemas/AuditEvent"}},"next_cursor":{"type":"integer","format":"int64","description":"Pass as `cursor` for the next page. Absent when this page reached the newest event."}},"required":["events"]}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/InternalServerError"},"default":{"$ref":"#/components/responses/InternalServerError"}},"security":[{"oauth2":["audit:read"]}],"tags":["Audit log"]}},"/folders":{"get":{"operationId":"listFolders","summary":"List folders","description":"Lists the folders in a workspace. Projects are listed by `GET /projects`.","parameters":[{"name":"workspace_id","in":"query","required":true,"description":"The workspace to list folders from","schema":{"type":"string","format":"uuid"}},{"name":"parent_id","in":"query","required":false,"description":"Limit results to one level of the folder tree: a folder ID for that folder's direct children, or `root` for the top level. Omit it for every level at once.\n","schema":{"anyOf":[{"type":"string","format":"uuid"},{"type":"string","enum":["root"]}]}}],"responses":{"200":{"description":"Successful operation","content":{"application/json":{"schema":{"type":"object","required":["items"],"properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/Folder"}}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/InternalServerError"},"default":{"$ref":"#/components/responses/InternalServerError"}},"security":[{"oauth2":["projects:read"]}],"tags":["Folders"]},"post":{"operationId":"createFolder","summary":"Create a folder","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","additionalProperties":false,"required":["name","workspace_id"],"properties":{"name":{"type":"string","minLength":1,"maxLength":256,"example":"Campaign assets"},"workspace_id":{"type":"string","format":"uuid"},"parent_id":{"description":"The folder to create it in. Omit or send null for the top level.","type":["string","null"],"format":"uuid"}}}}}},"responses":{"201":{"description":"Folder created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Folder"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/InternalServerError"},"default":{"$ref":"#/components/responses/InternalServerError"}},"security":[{"oauth2":["projects:write"]}],"tags":["Folders"]},"delete":{"operationId":"deleteFolders","summary":"Delete folders","description":"Moves folders to the trash, and nothing else. Folders are never removed: anything already in the trash inside one restores back into it, and restoring it brings back any trashed folders above it. A folder must be empty of live folders and projects; otherwise the request returns 409 and nothing is trashed.\n","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["folder_ids"],"properties":{"folder_ids":{"description":"The folders to delete","type":"array","minItems":1,"maxItems":500,"items":{"type":"string","format":"uuid"},"example":["550e8400-e29b-41d4-a716-446655440000"]}}}}}},"responses":{"200":{"description":"Successful operation","content":{"application/json":{"schema":{"type":"object","required":["items"],"properties":{"items":{"type":"array","items":{"type":"object","required":["id"],"properties":{"id":{"description":"ID of a folder moved to the trash","type":"string","format":"uuid"}}}}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"$ref":"#/components/responses/Conflict"},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/InternalServerError"},"default":{"$ref":"#/components/responses/InternalServerError"}},"security":[{"oauth2":["projects:write"]}],"tags":["Folders"]}},"/folders/{folder_id}":{"patch":{"operationId":"updateFolder","summary":"Update a folder","description":"Renames, recolours or moves a folder, in any combination. A move takes the folder's contents with it; it is refused with 409 when the destination is the folder itself or one of its subfolders. Including `parent_id` or `workspace_id` makes it a move, and an absent `parent_id` then means the top level.\n","parameters":[{"name":"folder_id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","minProperties":1,"additionalProperties":false,"properties":{"name":{"type":"string","minLength":1,"maxLength":256,"example":"Renamed"},"colour":{"type":"string","maxLength":24,"example":"var(--folder-red)"},"parent_id":{"description":"The folder to move the folder into, or null for the top level of the workspace. The folder must be in the destination workspace.\n","type":["string","null"],"format":"uuid"},"workspace_id":{"description":"The workspace to move the folder into. Defaults to the workspace of `parent_id`, or the folder's current workspace when moving to the top level. Moving out of a workspace needs permission to delete there.\n","type":"string","format":"uuid"}}}}}},"responses":{"204":{"description":"Folder updated"},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"$ref":"#/components/responses/Conflict"},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/InternalServerError"},"default":{"$ref":"#/components/responses/InternalServerError"}},"security":[{"oauth2":["projects:write"]}],"tags":["Folders"]}},"/folders/{folder_id}/ancestors":{"get":{"operationId":"getFolderAncestors","summary":"Get folder ancestors","description":"Returns the ancestor chain from root to immediate parent using a recursive CTE","parameters":[{"name":"folder_id","in":"path","required":true,"schema":{"type":"string","format":"uuid"},"description":"The folder ID to get ancestors for"}],"responses":{"200":{"description":"Successful operation","content":{"application/json":{"schema":{"type":"object","required":["items","folder_name"],"properties":{"folder_name":{"description":"Name of the requested folder","type":"string"},"folder_colour":{"description":"Colour of the requested folder (CSS var string, e.g. \"var(--folder-red)\"), or null when unset","type":"string","nullable":true},"items":{"type":"array","items":{"type":"object","required":["id","name"],"properties":{"id":{"description":"Folder ID","type":"string","format":"uuid"},"name":{"description":"Folder name","type":"string"},"colour":{"description":"Colour of the ancestor folder (CSS var string, e.g. \"var(--folder-red)\"), or null when unset","type":"string","nullable":true},"parentid":{"description":"Parent folder ID","type":"string","format":"uuid","nullable":true}}}}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/InternalServerError"},"default":{"$ref":"#/components/responses/InternalServerError"}},"security":[{"oauth2":["projects:read"]}],"tags":["Folders"]}},"/shares":{"get":{"operationId":"listShares","summary":"List shares","description":"Gets all shares created by or shared with the user","responses":{"200":{"description":"Successful operation","content":{"application/json":{"schema":{"type":"object","required":["items"],"properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/LinkInfo"}}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/InternalServerError"},"default":{"$ref":"#/components/responses/InternalServerError"}},"security":[{"oauth2":["shares:read"]}],"tags":["Shares"]},"post":{"operationId":"createShare","summary":"Create share","description":"Creates a new sharing link","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"projectid":{"description":"The project ID","type":"string","example":"project-12345"},"name":{"description":"Link name","type":"string","minLength":3,"example":"Client Preview"},"version":{"description":"Current version","type":"integer","format":"int32","example":3},"versions":{"description":"Available versions","type":"array","items":{"type":"integer","format":"int32"},"example":[1,2,3]},"isprivate":{"description":"Whether the link is private","type":"boolean","example":true},"allowteammembers":{"description":"Whether workspace members can view","type":"boolean","example":true},"showcomments":{"description":"Whether comments are visible","type":"boolean","example":true},"allowcommenting":{"description":"Whether commenting is allowed","type":"boolean","example":true}},"required":["projectid","version","versions","isprivate","showcomments","allowcommenting"]}}}},"responses":{"200":{"description":"Successful operation","content":{"application/json":{"schema":{"type":"object","properties":{"url":{"description":"Link URL","type":"string","example":"https://example.com/link/abcdef123456"},"id":{"description":"Link ID","type":"string","example":"link-12345"}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/InternalServerError"},"default":{"$ref":"#/components/responses/InternalServerError"}},"security":[{"oauth2":["shares:write"]}],"tags":["Shares"]},"delete":{"operationId":"deleteShares","summary":"Delete shares","description":"Deletes specified shares (bulk operation)","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"ids":{"description":"Array of share IDs to delete","type":"array","items":{"type":"string"},"example":["link-12345","link-67890"]}},"required":["ids"]}}}},"responses":{"200":{"description":"Successful operation","content":{"application/json":{"schema":{"type":"object","properties":{"deleted":{"type":"array","items":{"type":"string"},"example":["link-12345","link-67890"]}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/InternalServerError"},"default":{"$ref":"#/components/responses/InternalServerError"}},"security":[{"oauth2":["shares:write"]}],"tags":["Shares"]}},"/shares/{id}":{"parameters":[{"name":"id","in":"path","description":"The share ID","required":true,"schema":{"type":"string","format":"uuid"},"example":"2524b742-8c36-413e-b9f6-2269918618af"}],"get":{"operationId":"getShare","summary":"Get share","description":"Gets data for a share link. Returns full details (members, invitations, etc.) when the user created it.","responses":{"200":{"description":"Successful operation","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"description":"Link ID","type":"string","example":"2524b742-8c36-413e-b9f6-2269918618af"},"fileid":{"description":"File/Project ID","type":"string","example":"file-12345"},"version":{"description":"Current version number","type":"integer","format":"int32","example":1},"versions":{"description":"Available versions","type":"array","items":{"type":"integer","format":"int32"},"example":[1,2,3]},"name":{"description":"Link name","type":"string","example":"Client Preview"},"projectname":{"description":"Name of the project this link shares (used to derive a default link name)","type":"string","example":"Aurora Sneaker"},"private":{"description":"Whether the link is private","type":"boolean","example":true},"showcomments":{"description":"Whether comments are visible","type":"boolean","example":true},"allowcommenting":{"description":"Whether commenting is allowed","type":"boolean","example":true},"allowteammembers":{"description":"Whether team members can access (creator only)","type":"boolean","example":true},"url":{"description":"Public link URL (creator only)","type":"string","example":"https://example.com/link/abcdef123456"},"logourl":{"description":"URL for the link logo","type":"string","example":"https://example.com/logo.jpg"},"projectdescriptions":{"description":"Descriptions for each project version (creator only)","type":"array","items":{"type":"string"},"example":["Initial version","Updated design","Final version"]},"project_versions":{"description":"Available project version numbers (creator only)","type":"array","items":{"type":"integer","format":"int32"},"example":[1,2,3]},"members":{"description":"Users with access to this share (creator only)","type":"array","items":{"$ref":"#/components/schemas/ShareMember"}},"invited":{"description":"Pending invitations (creator only)","type":"array","items":{"type":"object","properties":{"email":{"description":"Invited email","type":"string","example":"invited.user@example.com"}}}}},"required":["id","fileid","version","versions","name","private","showcomments","allowcommenting"]}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/InternalServerError"},"default":{"$ref":"#/components/responses/InternalServerError"}},"security":[{"oauth2":["shares:read"]}],"tags":["Shares"]},"put":{"operationId":"updateShare","summary":"Update share","description":"Updates share settings","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateShareRequest"}}}},"responses":{"200":{"description":"Successful operation","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"description":"Link ID","type":"string","example":"link-12345"}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/InternalServerError"},"default":{"$ref":"#/components/responses/InternalServerError"}},"security":[{"oauth2":["shares:write"]}],"tags":["Shares"]}},"/shares/{id}/logo":{"parameters":[{"name":"id","in":"path","description":"The share ID","required":true,"schema":{"type":"string","format":"uuid"},"example":"2524b742-8c36-413e-b9f6-2269918618af"}],"post":{"operationId":"uploadShareLogo","summary":"Get presigned URL for logo upload","description":"Returns a presigned S3 URL for direct browser upload of a share link logo. Uses content hash for deduplication.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["content_hash"],"properties":{"content_hash":{"$ref":"#/components/schemas/ContentHash"}}}}}},"responses":{"200":{"description":"Successful operation","content":{"application/json":{"schema":{"type":"object","properties":{"logo_key":{"description":"S3 key for the logo (to be saved via PUT /shares/{id})","type":"string","example":"accounts/abc-123/logos/hash-logo.webp"},"upload_url":{"description":"Presigned URL for direct S3 upload (null if file already exists)","type":"string","nullable":true,"example":"https://s3.amazonaws.com/bucket/..."}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/InternalServerError"},"default":{"$ref":"#/components/responses/InternalServerError"}},"security":[{"oauth2":["shares:write"]}],"tags":["Shares"]},"delete":{"operationId":"deleteShareLogo","summary":"Delete share logo","description":"Deletes the logo for a share","responses":{"200":{"description":"Successful operation","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"description":"Success indicator","type":"boolean","example":true}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/InternalServerError"},"default":{"$ref":"#/components/responses/InternalServerError"}},"security":[{"oauth2":["shares:write"]}],"tags":["Shares"]}},"/shares/membership":{"delete":{"operationId":"leaveShares","summary":"Leave shared links","description":"Removes the user from specified shared links (bulk operation)","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"ids":{"description":"Array of share IDs to leave","type":"array","items":{"type":"string"},"example":["link-12345","link-67890"]}},"required":["ids"]}}}},"responses":{"200":{"description":"Successful operation","content":{"application/json":{"schema":{"type":"object","properties":{"left":{"type":"array","items":{"type":"string"},"example":["link-12345","link-67890"]}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/InternalServerError"},"default":{"$ref":"#/components/responses/InternalServerError"}},"security":[{"oauth2":["members:write"]}],"tags":["Shares"]}},"/workspaces":{"get":{"operationId":"listWorkspaces","summary":"List workspaces","description":"Gets all workspaces the user is a member of","responses":{"200":{"description":"Successful operation","content":{"application/json":{"schema":{"type":"object","properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/Team"}}},"required":["items"]}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/InternalServerError"},"default":{"$ref":"#/components/responses/InternalServerError"}},"security":[{"oauth2":["workspaces:read"]}],"tags":["Workspaces"]},"post":{"operationId":"createWorkspace","summary":"Create workspace","description":"Creates a new workspace","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"description":"Workspace name","type":"string","example":"Design Team"},"emails":{"description":"Emails to invite to the workspace","type":"array","items":{"type":"string","format":"email"},"example":["jane.smith@example.com","john.doe@example.com"]},"ai_images_enabled":{"description":"Whether image generation is enabled","type":"boolean","example":false},"default_bg_colour":{"description":"Default viewer background colour for this workspace","type":"string","nullable":true},"visibility":{"$ref":"#/components/schemas/Visibility"}},"required":["name"]}}}},"responses":{"200":{"description":"Successful operation","content":{"application/json":{"schema":{"type":"object","properties":{"items":{"type":"array","items":{"type":"object","properties":{"id":{"description":"Created workspace ID","type":"string","example":"550e8400-e29b-41d4-a716-446655440000"},"name":{"description":"Workspace name","type":"string","example":"Design Team"},"editedat":{"description":"Creation timestamp","type":"string","format":"date-time","example":"2023-04-13T10:30:00Z"}}}}},"required":["items"]}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"$ref":"#/components/responses/PaymentRequired"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/InternalServerError"},"default":{"$ref":"#/components/responses/InternalServerError"}},"security":[{"oauth2":["workspaces:write"]}],"tags":["Workspaces"]},"delete":{"operationId":"deleteWorkspaces","summary":"Delete workspaces","description":"Soft-deletes workspaces the caller administers; they stop appearing in listings.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"workspace_ids":{"description":"Array of workspace IDs to delete","type":"array","items":{"type":"string","format":"uuid"},"example":["550e8400-e29b-41d4-a716-446655440000"]}},"required":["workspace_ids"]}}}},"responses":{"200":{"description":"Successful operation","content":{"application/json":{"schema":{"type":"object","properties":{"items":{"type":"array","items":{"type":"object","properties":{"id":{"description":"Deleted workspace ID","type":"string","example":"550e8400-e29b-41d4-a716-446655440000"}}}}},"required":["items"]}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/InternalServerError"},"default":{"$ref":"#/components/responses/InternalServerError"}},"security":[{"oauth2":["workspaces:write"]}],"tags":["Workspaces"]}},"/workspaces/{workspace_id}":{"get":{"operationId":"getWorkspace","summary":"Get workspace","description":"Gets a specific workspace by ID","parameters":[{"name":"workspace_id","in":"path","description":"The workspace ID","required":true,"schema":{"type":"string","format":"uuid"},"example":"550e8400-e29b-41d4-a716-446655440000"}],"responses":{"200":{"description":"Successful operation","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Team"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/InternalServerError"},"default":{"$ref":"#/components/responses/InternalServerError"}},"security":[{"oauth2":["workspaces:read"]}],"tags":["Workspaces"]},"put":{"operationId":"updateWorkspace","summary":"Update workspace","description":"Updates workspace details and manages members","parameters":[{"name":"workspace_id","in":"path","description":"The workspace ID","required":true,"schema":{"type":"string","format":"uuid"},"example":"550e8400-e29b-41d4-a716-446655440000"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"description":"New workspace name","type":"string","example":"Design & Development Team"},"description":{"description":"Optional free-text workspace description. Pass an empty string to clear.","type":"string","nullable":true,"example":"Soft-form footwear and leather goods."},"remove_accepted":{"description":"Member emails to remove","type":"array","items":{"type":"string","format":"email"},"example":["jane.smith@example.com"]},"remove_pending":{"description":"Pending invite emails to remove","type":"array","items":{"type":"string","format":"email"},"example":["john.doe@example.com"]},"ai_images_enabled":{"description":"Whether image generation is enabled","type":"boolean","nullable":true},"default_bg_colour":{"description":"Default viewer background colour for this workspace","type":"string","nullable":true},"embed_allowed_origins":{"$ref":"#/components/schemas/EmbedAllowedOrigins"},"visibility":{"$ref":"#/components/schemas/Visibility"}}}}}},"responses":{"200":{"description":"Successful operation","content":{"application/json":{"schema":{"type":"object","properties":{"items":{"type":"array","items":{"type":"object","properties":{"id":{"description":"Updated workspace ID","type":"string","example":"550e8400-e29b-41d4-a716-446655440000"}}}}},"required":["items"]}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/InternalServerError"},"default":{"$ref":"#/components/responses/InternalServerError"}},"security":[{"oauth2":["workspaces:write"]}],"tags":["Workspaces"]}},"/workspaces/{workspace_id}/members":{"get":{"operationId":"getWorkspaceMembers","summary":"Get workspace members","description":"Returns accepted members and pending invitations as separate lists. Each has its own shape — accepted members carry person details; pending invitations carry only the invite metadata.","parameters":[{"name":"workspace_id","in":"path","description":"The workspace ID","required":true,"schema":{"type":"string","format":"uuid"},"example":"550e8400-e29b-41d4-a716-446655440000"}],"responses":{"200":{"description":"Successful operation","content":{"application/json":{"schema":{"type":"object","properties":{"members":{"type":"array","items":{"$ref":"#/components/schemas/WorkspaceMember"}},"pending_invitations":{"type":"array","items":{"$ref":"#/components/schemas/WorkspaceInvitation"}}},"required":["members","pending_invitations"]}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"$ref":"#/components/responses/PaymentRequired"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/InternalServerError"},"default":{"$ref":"#/components/responses/InternalServerError"}},"security":[{"oauth2":["members:read"]}],"tags":["Workspaces"]}},"/workspaces/leave":{"delete":{"operationId":"leaveWorkspaces","summary":"Leave workspaces","description":"Leaves workspaces the user is a member of","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"workspace_ids":{"description":"Array of workspace IDs to leave","type":"array","items":{"type":"string","format":"uuid"},"example":["550e8400-e29b-41d4-a716-446655440000"]}},"required":["workspace_ids"]}}}},"responses":{"200":{"description":"Successful operation"},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/InternalServerError"},"default":{"$ref":"#/components/responses/InternalServerError"}},"security":[{"oauth2":["members:write"]}],"tags":["Workspaces"]}},"/workspaces/{workspace_id}/members/{person_id}/role":{"put":{"operationId":"updateWorkspaceMemberRole","summary":"Update workspace member role","description":"Updates the role of a workspace member","parameters":[{"name":"workspace_id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"person_id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"role":{"$ref":"#/components/schemas/WorkspaceRole"}},"required":["role"]}}}},"responses":{"204":{"description":"Role updated successfully"},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/InternalServerError"},"default":{"$ref":"#/components/responses/InternalServerError"}},"security":[{"oauth2":["members:write"]}],"tags":["Workspaces"]}},"/configurators":{"get":{"operationId":"listConfigurators","summary":"List configurators","description":"Lists all configurators for projects accessible to the authenticated user","responses":{"200":{"description":"Successful operation","content":{"application/json":{"schema":{"type":"object","required":["items"],"properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/Configurator"}}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/InternalServerError"},"default":{"$ref":"#/components/responses/InternalServerError"}},"security":[{"oauth2":["configurators:read"]}],"tags":["Configurators"]},"post":{"operationId":"createConfigurator","summary":"Create configurator","description":"Creates a new configurator for a project version","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"description":"Internal name for the configurator","type":"string"},"project_id":{"description":"Optional initial project ID","type":"string","format":"uuid"},"version":{"description":"Optional initial project version","type":"integer","format":"int32","example":1},"workspace_id":{"description":"Workspace to create the configurator in. Required when project_id is omitted.","type":"string","format":"uuid"},"layout_template_id":{"description":"A layout template to start from. It must belong to the configurator's workspace. The configurator takes the template's theme and a copy of its layout.\n","type":"string","format":"uuid"},"layout_document":{"description":"A layout to start from, as read from an exported layout file. The configurator takes its theme. Uploaded images, custom fonts, written content and values the theme no longer accepts are left out. Cannot be sent with `layout_template_id`.\n","allOf":[{"$ref":"#/components/schemas/LayoutDocument"}]},"theme":{"description":"Visual theme to start with. Ignored when `layout_template_id` or `layout_document` is given, as their theme is used.\n","type":"string","enum":["hero","hero-generic","automotive","automotive-generic","automotive-honda","embed"]}}}}}},"responses":{"201":{"description":"Configurator created","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"description":"The configurator ID","type":"string","format":"uuid"},"project_entry_id":{"description":"The configurator_project ID for the initially added project, if one was provided","type":"string","format":"uuid","nullable":true}},"required":["id"]}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"422":{"$ref":"#/components/responses/LayoutRefused"},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/InternalServerError"},"default":{"$ref":"#/components/responses/InternalServerError"}},"security":[{"oauth2":["configurators:write"]}],"tags":["Configurators"]}},"/configurators/{id}":{"get":{"operationId":"getConfigurator","summary":"Get configurator","description":"Gets a configurator by ID including project rio data.","parameters":[{"$ref":"#/components/parameters/ConfiguratorIdPath"}],"responses":{"200":{"description":"Successful operation","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Configurator"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/InternalServerError"},"default":{"$ref":"#/components/responses/InternalServerError"}},"security":[{"oauth2":["configurators:read"]}],"tags":["Configurators"]},"put":{"operationId":"updateConfigurator","summary":"Update configurator","description":"Replaces the fields sent. `settings` is replaced whole, so another editor's change since you read it is lost: send the `seq` you read as `If-Match` and a stale copy is refused with 412, or edit with POST /configurators/{id}/ops, which merges.\n","parameters":[{"$ref":"#/components/parameters/ConfiguratorIdPath"},{"$ref":"#/components/parameters/IfMatchSeq"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"logo_key":{"description":"S3 key for logo image","type":"string","nullable":true},"settings":{"description":"Checked against `ConfiguratorSettings` and refused with 400 if it doesn't match. Errors the stored settings already had don't count, so an edit elsewhere still saves.\n","type":"object","nullable":true,"additionalProperties":true},"cta_email":{"description":"Email address for enquiry notifications","type":"string","format":"email","nullable":true},"cta_fields":{"description":"Form field configuration for CTA form","type":"array","items":{"$ref":"#/components/schemas/ConfiguratorCtaField"},"nullable":true},"title":{"description":"Custom display title","type":"string","nullable":true},"webhook_url":{"description":"Webhook URL for enquiry notifications (alternative to email). Must use https.","type":"string","format":"uri","nullable":true},"theme":{"description":"Visual theme for the configurator layout","type":"string","enum":["hero","hero-generic","automotive","automotive-generic","automotive-honda","embed"]},"color_scheme":{"description":"Default color scheme for the configurator","type":"string","enum":["system","light","dark"]},"color_scheme_allow_toggle":{"description":"Whether end-users can toggle light/dark mode","type":"boolean"},"name":{"description":"Internal name for the configurator","type":"string","nullable":true}}}}}},"responses":{"200":{"description":"Successful operation","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"description":"The configurator ID","type":"string","format":"uuid"}},"required":["id"]}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"412":{"$ref":"#/components/responses/PreconditionFailed"},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/InternalServerError"},"default":{"$ref":"#/components/responses/InternalServerError"}},"security":[{"oauth2":["configurators:write"]}],"tags":["Configurators"]},"delete":{"operationId":"deleteConfigurator","summary":"Delete configurator","description":"Deletes a configurator","parameters":[{"$ref":"#/components/parameters/ConfiguratorIdPath"}],"responses":{"200":{"description":"Successful operation","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"description":"The deleted configurator ID","type":"string","format":"uuid"}},"required":["id"]}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/InternalServerError"},"default":{"$ref":"#/components/responses/InternalServerError"}},"security":[{"oauth2":["configurators:write"]}],"tags":["Configurators"]}},"/configurators/{id}/projects":{"post":{"operationId":"addConfiguratorProject","summary":"Add project to configurator","description":"Adds a project version to a configurator","parameters":[{"$ref":"#/components/parameters/ConfiguratorIdPath"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"project_id":{"description":"The project ID","type":"string","format":"uuid"},"version":{"description":"The project version","type":"integer","format":"int32"}},"required":["project_id","version"]}}}},"responses":{"201":{"description":"Project added","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"description":"The configurator_project ID","type":"string","format":"uuid"}},"required":["id"]}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/InternalServerError"},"default":{"$ref":"#/components/responses/InternalServerError"}},"security":[{"oauth2":["configurators:write"]}],"tags":["Configurators"]}},"/configurators/{id}/projects/{project_id}":{"put":{"operationId":"updateConfiguratorProject","summary":"Replace configurator project settings","description":"Replaces a configurator project's settings with the body's `settings`; a property left out is removed, saved viewpoints, scenes and focus points included. To change some fields and keep the rest, use `PATCH` on the same path. An option image key not from this configurator's option-image-upload-url is refused with 400.\n","parameters":[{"$ref":"#/components/parameters/ConfiguratorIdPath"},{"name":"project_id","in":"path","description":"The configurator_project ID","required":true,"schema":{"type":"string","format":"uuid"}},{"$ref":"#/components/parameters/IfMatchSeq"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"settings":{"$ref":"#/components/schemas/ConfiguratorProjectSettings"}}}}}},"responses":{"200":{"description":"Project updated","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","format":"uuid"}},"required":["id"]}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"412":{"$ref":"#/components/responses/PreconditionFailed"},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/InternalServerError"},"default":{"$ref":"#/components/responses/InternalServerError"}},"security":[{"oauth2":["configurators:write"]}],"tags":["Configurators"]},"patch":{"operationId":"patchConfiguratorProject","summary":"Update configurator project settings","description":"Applies an RFC 7396 JSON Merge Patch to the same `{ \"settings\": … }` document `PUT` replaces: objects merge key by key, `null` deletes a key, and arrays (saved viewpoints among them) are replaced whole. Fields the patch doesn't name are kept. The merged document must pass the schema `PUT` enforces, or the response is a 422 whose `errors` point into the merged document. Concurrent patches are applied one after the other.\n","parameters":[{"$ref":"#/components/parameters/ConfiguratorIdPath"},{"name":"project_id","in":"path","description":"The configurator_project ID","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application/merge-patch+json":{"schema":{"type":"object","additionalProperties":false,"properties":{"settings":{"description":"Merged into the stored settings; `null` clears them.","oneOf":[{"type":"object","additionalProperties":true},{"type":"null"}]}}}}}},"responses":{"200":{"description":"The settings after the patch","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"settings":{"$ref":"#/components/schemas/ConfiguratorProjectSettings"}},"required":["id","settings"]}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"422":{"$ref":"#/components/responses/UnprocessableEntity"},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/InternalServerError"},"default":{"$ref":"#/components/responses/InternalServerError"}},"security":[{"oauth2":["configurators:write"]}],"tags":["Configurators"]},"delete":{"operationId":"removeConfiguratorProject","summary":"Remove project from configurator","description":"Removes a project from a configurator","parameters":[{"$ref":"#/components/parameters/ConfiguratorIdPath"},{"name":"project_id","in":"path","description":"The configurator_project ID","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Project removed","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"description":"The removed configurator_project ID","type":"string","format":"uuid"}},"required":["id"]}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/InternalServerError"},"default":{"$ref":"#/components/responses/InternalServerError"}},"security":[{"oauth2":["configurators:write"]}],"tags":["Configurators"]}},"/configurators/{id}/projects/{project_id}/viewpoints":{"post":{"operationId":"addConfiguratorViewpoint","summary":"Add a saved viewpoint to a project","description":"Atomically appends a viewpoint to the project's saved_viewpoints\nlist via a single UPDATE. Use this instead of read-modify-write\nthrough PUT .../projects/{project_id} — concurrent saves (e.g. two\nviewpoints saved in quick succession, before the client's cache of\n`settings` has refreshed) would otherwise silently drop each\nother's entries. `id` and `createdat` are generated server-side.\n","parameters":[{"$ref":"#/components/parameters/ConfiguratorIdPath"},{"name":"project_id","in":"path","description":"The configurator_project ID","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SavedViewpointInput"}}}},"responses":{"200":{"description":"The created viewpoint","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SavedViewpoint"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/InternalServerError"},"default":{"$ref":"#/components/responses/InternalServerError"}},"security":[{"oauth2":["configurators:write"]}],"tags":["Configurators"]}},"/configurators/{id}/projects/{project_id}/viewpoints/{viewpoint_id}":{"delete":{"operationId":"deleteConfiguratorViewpoint","summary":"Delete a saved viewpoint from a project","description":"Atomically removes a viewpoint from the project's saved_viewpoints list via a single UPDATE.","parameters":[{"$ref":"#/components/parameters/ConfiguratorIdPath"},{"name":"project_id","in":"path","description":"The configurator_project ID","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"viewpoint_id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Viewpoint removed","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"description":"The removed viewpoint ID","type":"string","format":"uuid"}},"required":["id"]}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/InternalServerError"},"default":{"$ref":"#/components/responses/InternalServerError"}},"security":[{"oauth2":["configurators:write"]}],"tags":["Configurators"]}},"/configurators/{id}/projects/{project_id}/scenes":{"post":{"operationId":"addConfiguratorScene","summary":"Add a saved scene to a project","description":"Atomically appends an environment/background preset to the\nproject's saved_scenes list via a single UPDATE. Use this instead\nof read-modify-write through PUT .../projects/{project_id} —\nconcurrent saves (e.g. two scenes saved in quick succession,\nbefore the client's cache of `settings` has refreshed) would\notherwise silently drop each other's entries. `id` and `createdat`\nare generated server-side.\n","parameters":[{"$ref":"#/components/parameters/ConfiguratorIdPath"},{"name":"project_id","in":"path","description":"The configurator_project ID","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SavedSceneInput"}}}},"responses":{"200":{"description":"The created scene","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SavedScene"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/InternalServerError"},"default":{"$ref":"#/components/responses/InternalServerError"}},"security":[{"oauth2":["configurators:write"]}],"tags":["Configurators"]}},"/configurators/{id}/projects/{project_id}/scenes/{scene_id}":{"delete":{"operationId":"deleteConfiguratorScene","summary":"Delete a saved scene from a project","description":"Atomically removes a scene from the project's saved_scenes list via a single UPDATE.","parameters":[{"$ref":"#/components/parameters/ConfiguratorIdPath"},{"name":"project_id","in":"path","description":"The configurator_project ID","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"scene_id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Scene removed","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"description":"The removed scene ID","type":"string","format":"uuid"}},"required":["id"]}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/InternalServerError"},"default":{"$ref":"#/components/responses/InternalServerError"}},"security":[{"oauth2":["configurators:write"]}],"tags":["Configurators"]}},"/configurators/{id}/projects/{project_id}/focus-points":{"post":{"operationId":"addConfiguratorFocusPoint","summary":"Add a focus point to a project","description":"Atomically appends a focus point to the project's focus_points list\nvia a single UPDATE, same rationale as POST .../viewpoints. `id` and\n`createdat` are generated server-side.\n","parameters":[{"$ref":"#/components/parameters/ConfiguratorIdPath"},{"name":"project_id","in":"path","description":"The configurator_project ID","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/FocusPointInput"}}}},"responses":{"200":{"description":"The created focus point","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FocusPoint"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/InternalServerError"},"default":{"$ref":"#/components/responses/InternalServerError"}},"security":[{"oauth2":["configurators:write"]}],"tags":["Configurators"]}},"/configurators/{id}/projects/{project_id}/focus-points/{focus_point_id}":{"patch":{"operationId":"updateConfiguratorFocusPoint","summary":"Update a focus point on a project","description":"Atomically merges the given fields into the matching focus point via a single UPDATE, same rationale as the sibling POST/DELETE. Only the properties present in the body are changed; omitted ones keep their current value.\n","parameters":[{"$ref":"#/components/parameters/ConfiguratorIdPath"},{"name":"project_id","in":"path","description":"The configurator_project ID","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"focus_point_id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/FocusPointPatch"}}}},"responses":{"200":{"description":"The updated focus point","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FocusPoint"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/InternalServerError"},"default":{"$ref":"#/components/responses/InternalServerError"}},"security":[{"oauth2":["configurators:write"]}],"tags":["Configurators"]},"delete":{"operationId":"deleteConfiguratorFocusPoint","summary":"Delete a focus point from a project","description":"Atomically removes a focus point from the project's focus_points list via a single UPDATE.","parameters":[{"$ref":"#/components/parameters/ConfiguratorIdPath"},{"name":"project_id","in":"path","description":"The configurator_project ID","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"focus_point_id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Focus point removed","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"description":"The removed focus point ID","type":"string","format":"uuid"}},"required":["id"]}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/InternalServerError"},"default":{"$ref":"#/components/responses/InternalServerError"}},"security":[{"oauth2":["configurators:write"]}],"tags":["Configurators"]}},"/configurators/{id}/submit":{"post":{"operationId":"submitConfiguratorEnquiry","summary":"Submit enquiry form","description":"Submits a public enquiry form for a configurator. No authentication required.","parameters":[{"$ref":"#/components/parameters/ConfiguratorIdPath"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"fields":{"description":"Form field values","type":"object","additionalProperties":{"type":"string"}},"session_id":{"description":"The analytics session that sent the enquiry","type":"string","maxLength":64},"selected_options":{"description":"Configuration options selected by the user","type":"array","items":{"type":"object","properties":{"code":{"type":"string","description":"FrontEndRule code (e.g. \"+QY89\")"},"name":{"type":"string","description":"Human-readable option name"}},"required":["code","name"]}}},"required":["fields"]}}}},"responses":{"201":{"description":"Enquiry submitted","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}},"required":["success"]}}}},"400":{"$ref":"#/components/responses/BadRequest"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/InternalServerError"},"default":{"$ref":"#/components/responses/InternalServerError"}},"security":[{"oauth2":["configurators:read"]}],"tags":["Configurators"]}},"/configurators/{id}/logo-upload-url":{"get":{"operationId":"getConfiguratorLogoUploadUrl","summary":"Get logo upload URL","description":"Gets a presigned URL for uploading a configurator logo","parameters":[{"$ref":"#/components/parameters/ConfiguratorIdPath"},{"$ref":"#/components/parameters/ContentHashQuery"}],"responses":{"200":{"description":"Successful operation","content":{"application/json":{"schema":{"type":"object","properties":{"upload_url":{"description":"Presigned PUT URL for upload","type":"string"},"logo_key":{"description":"S3 key for the logo","type":"string"}},"required":["upload_url","logo_key"]}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/InternalServerError"},"default":{"$ref":"#/components/responses/InternalServerError"}},"security":[{"oauth2":["configurators:write"]}],"tags":["Configurators"]}},"/layout-templates":{"get":{"operationId":"listLayoutTemplates","summary":"List a workspace's layout templates","description":"Templates are saved copies of a configurator's layout, shared by every configurator in the workspace. Filter by theme to list only those a configurator can start from.\n","parameters":[{"name":"workspace_id","in":"query","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"theme","in":"query","required":false,"schema":{"type":"string"}}],"responses":{"200":{"description":"Successful operation","content":{"application/json":{"schema":{"type":"object","properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/LayoutTemplateSummary"}}},"required":["items"]}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/InternalServerError"},"default":{"$ref":"#/components/responses/InternalServerError"}},"security":[{"oauth2":["configurators:read"]}],"tags":["Configurators"]},"post":{"operationId":"createLayoutTemplate","summary":"Save a configurator's layout as a template","description":"Copies the configurator's current layout into a workspace template. Only what does not depend on this configurator's content is kept: brand colours and fonts, component settings, placements and the styles for each type of option, subcategory and category. Uploaded images and copy written for this car (text boxes, title highlights, the summary title and footnote) are left out. A name already used in the workspace, ignoring case, is refused with 409.\n","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"configurator_id":{"type":"string","format":"uuid"},"name":{"type":"string","minLength":1,"maxLength":80}},"required":["configurator_id","name"]}}}},"responses":{"201":{"description":"Created","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","format":"uuid"}},"required":["id"]}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"$ref":"#/components/responses/Conflict"},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/InternalServerError"},"default":{"$ref":"#/components/responses/InternalServerError"}},"security":[{"oauth2":["configurators:write"]}],"tags":["Configurators"]}},"/layout-templates/{id}":{"put":{"operationId":"replaceLayoutTemplate","summary":"Replace a layout template with a configurator's layout","description":"Overwrites the template's document with the configurator's current layout, kept to the same portable parts as when saving a new one. The name stays. The previous document is not kept. Configurators that already started from the template are not changed. The configurator must be in the template's workspace.\n","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"configurator_id":{"type":"string","format":"uuid"}},"required":["configurator_id"]}}}},"responses":{"200":{"description":"Replaced","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","format":"uuid"}},"required":["id"]}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/InternalServerError"},"default":{"$ref":"#/components/responses/InternalServerError"}},"security":[{"oauth2":["configurators:write"]}],"tags":["Configurators"]},"get":{"operationId":"getLayoutTemplate","summary":"Get a layout template with its document","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Successful operation","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"name":{"type":"string"},"theme":{"type":"string"},"document":{"$ref":"#/components/schemas/LayoutDocument"}},"required":["id","name","theme","document"]}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/InternalServerError"},"default":{"$ref":"#/components/responses/InternalServerError"}},"security":[{"oauth2":["configurators:read"]}],"tags":["Configurators"]},"delete":{"operationId":"deleteLayoutTemplate","summary":"Delete a layout template","description":"Configurators that already started from it are not changed.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Deleted","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","format":"uuid"}},"required":["id"]}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/InternalServerError"},"default":{"$ref":"#/components/responses/InternalServerError"}},"security":[{"oauth2":["configurators:write"]}],"tags":["Configurators"]}},"/configurator-themes/{theme_id}":{"get":{"operationId":"getConfiguratorTheme","summary":"Get a configurator theme","description":"Everything a layout document for this theme may set: brand and layout settings, components with their settings and whether they can be hidden or moved, the slots they can be placed in, fonts, palette presets and the per-option-type settings. Each setting gives its type, limits, default and help text. A layout document's keys are these settings' ids.\n","parameters":[{"$ref":"#/components/parameters/ConfiguratorThemeIdPath"}],"responses":{"200":{"description":"Successful operation","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ConfiguratorTheme"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/InternalServerError"},"default":{"$ref":"#/components/responses/InternalServerError"}},"security":[{"oauth2":["configurators:read"]}],"tags":["Configurators"]}},"/configurator-themes/{theme_id}/validate":{"post":{"operationId":"validateConfiguratorLayout","summary":"Check a layout document against a theme","description":"Reports the values a theme won't accept, components it doesn't have, placements its slots refuse and text that is too low in contrast to read. `error` issues would break the configurator; `warning` issues are ignored or degrade it. Nothing is saved: save with `PUT /configurators/{id}/layout`. A document that breaks the schema is refused with 400, its `errors` naming each failing path.\n","parameters":[{"$ref":"#/components/parameters/ConfiguratorThemeIdPath"}],"requestBody":{"required":true,"content":{"application/json":{"examples":{"example":{"$ref":"#/components/examples/LayoutDocumentExample"}},"schema":{"type":"object","properties":{"document":{"$ref":"#/components/schemas/LayoutDocument"}},"required":["document"]}}}},"responses":{"200":{"description":"Successful operation","content":{"application/json":{"schema":{"$ref":"#/components/schemas/LayoutValidation"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/InternalServerError"},"default":{"$ref":"#/components/responses/InternalServerError"}},"security":[{"oauth2":["configurators:read"]}],"tags":["Configurators"]}},"/configurators/{id}/ops":{"post":{"operationId":"editConfigurator","summary":"Edit a configurator draft with operations","description":"Applies `ops` in order, each on its own, to the draft's `settings`, its projects' `settings` and its layout, and saves the result in one step. Edits by others since you read are kept: an op changes only what its path names, so two editors changing different properties both land, and the later of two edits to one property wins. A path starts at `settings`, `layout` or `projects` and a ConfiguratorProject id, then names object keys, and list items by `{ \"id\": … }` rather than by position, so it reaches the same item however the list was reordered. An op whose target has gone (removed by someone else) is skipped and its index listed in `dropped`. The saved result is checked as the PUT endpoints check theirs; anything they refuse is refused here and nothing is saved.\n","parameters":[{"$ref":"#/components/parameters/ConfiguratorIdPath"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"ops":{"type":"array","minItems":1,"maxItems":500,"items":{"$ref":"#/components/schemas/ConfiguratorOp"}}},"required":["ops"]},"example":{"ops":[{"op":"set","path":["settings","automotive_title"],"value":"Defender 110"},{"op":"move","path":["settings","automotive_categories",{"id":"5f1d0c2e-wheels"}],"after":"0a7e9b41-paint"}]}}}},"responses":{"200":{"description":"Applied","content":{"application/json":{"schema":{"type":"object","properties":{"seq":{"description":"The configurator's `seq` after the edit","type":"integer","format":"int32"},"dropped":{"description":"Indexes of the ops skipped because their target has gone","type":"array","items":{"type":"integer","format":"int32"}}},"required":["seq","dropped"]}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"$ref":"#/components/responses/Conflict"},"422":{"$ref":"#/components/responses/UnprocessableEntity"},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/InternalServerError"},"default":{"$ref":"#/components/responses/InternalServerError"}},"security":[{"oauth2":["configurators:write"]}],"tags":["Configurators"]}},"/configurators/{id}/layout":{"put":{"operationId":"updateConfiguratorLayout","summary":"Save the layout document","description":"Replaces the configurator's layout document, which is stored apart from `settings`. Send the `revision` you read it at (0 when the configurator has no stored layout yet). A stale revision is refused with 409, so a tab holding an old copy cannot overwrite a newer save. Checked against the configurator's theme as its `validate` checks; a new `error` issue is refused with 422.\n","parameters":[{"$ref":"#/components/parameters/ConfiguratorIdPath"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"document":{"$ref":"#/components/schemas/LayoutDocument"},"revision":{"description":"The revision the document was read at","type":"integer","format":"int32","minimum":0}},"required":["document","revision"]}}}},"responses":{"200":{"description":"Saved","content":{"application/json":{"schema":{"type":"object","properties":{"revision":{"description":"The revision now stored","type":"integer","format":"int32"}},"required":["revision"]}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"$ref":"#/components/responses/Conflict"},"422":{"$ref":"#/components/responses/LayoutRefused"},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/InternalServerError"},"default":{"$ref":"#/components/responses/InternalServerError"}},"security":[{"oauth2":["configurators:write"]}],"tags":["Configurators"]}},"/configurators/{id}/publish":{"post":{"operationId":"publishConfigurator","summary":"Publish the draft as a new release","description":"Copies the draft into a new, permanently frozen release and leaves the draft editable. Optionally makes the new release live in the same step.\n","parameters":[{"$ref":"#/components/parameters/ConfiguratorIdPath"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"deploy":{"description":"Also make this release the live version","type":"boolean"}},"required":["deploy"]}}}},"responses":{"200":{"description":"Published","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"description":"The new release","type":"string","format":"uuid"},"version_number":{"description":"The release number","type":"integer","format":"int32"},"published_at":{"type":"string","format":"date-time"},"deployed":{"type":"boolean"}},"required":["id","version_number","published_at","deployed"]}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"$ref":"#/components/responses/Conflict"},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/InternalServerError"},"default":{"$ref":"#/components/responses/InternalServerError"}},"security":[{"oauth2":["configurators:write"]}],"tags":["Configurators"]}},"/configurators/{id}/fork":{"post":{"operationId":"forkConfigurator","summary":"Create a draft from a release","description":"Copies a published release into a new draft. A configurator has at most one draft, so this is refused with 409 while one exists.\n","parameters":[{"$ref":"#/components/parameters/ConfiguratorIdPath"}],"responses":{"201":{"description":"Draft created","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","format":"uuid"}},"required":["id"]}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"$ref":"#/components/responses/Conflict"},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/InternalServerError"},"default":{"$ref":"#/components/responses/InternalServerError"}},"security":[{"oauth2":["configurators:write"]}],"tags":["Configurators"]}},"/configurators/{id}/restore":{"post":{"operationId":"restoreConfiguratorDraft","summary":"Restore the draft from a release","description":"Replaces everything in the draft with a published release. The draft as it was is kept as a backup, replacing any earlier backup. Omit `source_id` to undo the last restore by putting the backup back.\n","parameters":[{"$ref":"#/components/parameters/ConfiguratorIdPath"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"source_id":{"description":"The release to restore. Omit to restore the backup.","type":"string","format":"uuid"}}}}}},"responses":{"204":{"description":"Restored"},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"$ref":"#/components/responses/Conflict"},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/InternalServerError"},"default":{"$ref":"#/components/responses/InternalServerError"}},"security":[{"oauth2":["configurators:write"]}],"tags":["Configurators"]}},"/configurator-families/{family_id}":{"get":{"operationId":"getConfiguratorFamily","summary":"Get a configurator family and its versions","parameters":[{"$ref":"#/components/parameters/ConfiguratorFamilyIdPath"}],"responses":{"200":{"description":"Successful operation","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ConfiguratorFamily"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/InternalServerError"},"default":{"$ref":"#/components/responses/InternalServerError"}},"security":[{"oauth2":["configurators:read"]}],"tags":["Configurators"]}},"/configurator-families/{family_id}/embedding":{"put":{"operationId":"setConfiguratorEmbedding","summary":"Set the domains allowed to embed this configurator","description":"Sets this configurator's own list, replacing the workspace's or, with `extend_workspace`, adding to it. An empty list that replaces the workspace's allows no site. Delete to go back to the workspace's list.\n","parameters":[{"$ref":"#/components/parameters/ConfiguratorFamilyIdPath"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"allowed_origins":{"$ref":"#/components/schemas/EmbedAllowedOrigins"},"extend_workspace":{"description":"Allow the workspace's domains as well as these","type":"boolean"}},"required":["allowed_origins","extend_workspace"]}}}},"responses":{"204":{"description":"Saved"},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/InternalServerError"},"default":{"$ref":"#/components/responses/InternalServerError"}},"security":[{"oauth2":["configurators:write"]}],"tags":["Configurators"]},"delete":{"operationId":"resetConfiguratorEmbedding","summary":"Use the workspace's allowed domains for this configurator","parameters":[{"$ref":"#/components/parameters/ConfiguratorFamilyIdPath"}],"responses":{"204":{"description":"Reset"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/InternalServerError"},"default":{"$ref":"#/components/responses/InternalServerError"}},"security":[{"oauth2":["configurators:write"]}],"tags":["Configurators"]}},"/configurator-families/{family_id}/analytics":{"put":{"operationId":"setConfiguratorAnalytics","summary":"Set which analytics this configurator collects","description":"Sets how much visitor activity the live configurator reports. Nothing is collected until a level above `off` is chosen.\n","parameters":[{"$ref":"#/components/parameters/ConfiguratorFamilyIdPath"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"level":{"$ref":"#/components/schemas/AnalyticsLevel"}},"required":["level"]}}}},"responses":{"204":{"description":"Saved"},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/InternalServerError"},"default":{"$ref":"#/components/responses/InternalServerError"}},"security":[{"oauth2":["configurators:write"]}],"tags":["Configurators"]}},"/configurator-families/{family_id}/deployment":{"get":{"operationId":"resolveConfiguratorDeployment","summary":"Resolve the version a family's stable link serves","description":"Returns the version to show a visitor. When the current deployment has several variants, one is chosen by weight unless the visitor's earlier assignment is given.\n","parameters":[{"$ref":"#/components/parameters/ConfiguratorFamilyIdPath"},{"name":"assigned","in":"query","required":false,"description":"The release this visitor was shown before. It is honoured while that release is still part of the deployment named by `assigned_deployment`, so a visitor keeps seeing the same one for the length of a test.\n","schema":{"type":"string","format":"uuid"}},{"name":"assigned_deployment","in":"query","required":false,"description":"The deployment the visitor's earlier assignment was made under. An assignment from an earlier deployment is ignored, so a new test starts from a fresh split.\n","schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Successful operation","content":{"application/json":{"schema":{"type":"object","properties":{"configurator_id":{"type":"string","format":"uuid"},"deployment_id":{"type":"string","format":"uuid"},"variant_count":{"description":"How many releases the deployment splits visitors between. Above one, the answer differs between visitors and must not be cached.\n","type":"integer","format":"int32"}},"required":["configurator_id","deployment_id","variant_count"]}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/InternalServerError"},"default":{"$ref":"#/components/responses/InternalServerError"}},"security":[{"oauth2":["configurators:read"]}],"tags":["Configurators"]},"put":{"operationId":"deployConfigurator","summary":"Make a published release the live version","description":"Replaces the current deployment with one that serves only this release. The version must be published.\n","parameters":[{"$ref":"#/components/parameters/ConfiguratorFamilyIdPath"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"configurator_id":{"type":"string","format":"uuid"}},"required":["configurator_id"]}}}},"responses":{"200":{"description":"Deployed","content":{"application/json":{"schema":{"type":"object","properties":{"deployment_id":{"type":"string","format":"uuid"}},"required":["deployment_id"]}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"$ref":"#/components/responses/Conflict"},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/InternalServerError"},"default":{"$ref":"#/components/responses/InternalServerError"}},"security":[{"oauth2":["configurators:write"]}],"tags":["Configurators"]},"delete":{"operationId":"undeployConfigurator","summary":"Take the configurator offline","parameters":[{"$ref":"#/components/parameters/ConfiguratorFamilyIdPath"}],"responses":{"204":{"description":"Withdrawn"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/InternalServerError"},"default":{"$ref":"#/components/responses/InternalServerError"}},"security":[{"oauth2":["configurators:write"]}],"tags":["Configurators"]}},"/shared-layouts":{"get":{"operationId":"listSharedLayouts","summary":"List a workspace's shared layouts","description":"A shared layout belongs to one source configurator, whose layout the configurators linked to it use as it is edited. Filter by theme to list only those a configurator can link to.\n","parameters":[{"name":"workspace_id","in":"query","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"theme","in":"query","required":false,"schema":{"type":"string"}}],"responses":{"200":{"description":"Successful operation","content":{"application/json":{"schema":{"type":"object","properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/SharedLayout"}}},"required":["items"]}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/InternalServerError"},"default":{"$ref":"#/components/responses/InternalServerError"}},"security":[{"oauth2":["configurators:read"]}],"tags":["Configurators"]},"post":{"operationId":"createSharedLayout","summary":"Make a configurator the source of a shared layout","description":"Names the configurator's layout and makes the configurator its source: only the source edits the layout. Nothing is copied. Only a configurator with a saved layout, that is not itself linked to a shared layout, can be a source, and it can be the source of one layout. A name already used in the workspace, ignoring case, is refused with 409.\n","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"configurator_id":{"type":"string","format":"uuid"},"name":{"type":"string","minLength":1,"maxLength":80}},"required":["configurator_id","name"]}}}},"responses":{"201":{"description":"Created","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","format":"uuid"}},"required":["id"]}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"$ref":"#/components/responses/Conflict"},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/InternalServerError"},"default":{"$ref":"#/components/responses/InternalServerError"}},"security":[{"oauth2":["configurators:write"]}],"tags":["Configurators"]}},"/shared-layouts/{id}":{"get":{"operationId":"getSharedLayout","summary":"Get a shared layout and the configurators using it","parameters":[{"$ref":"#/components/parameters/SharedLayoutIdPath"}],"responses":{"200":{"description":"Successful operation","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SharedLayoutDetail"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"$ref":"#/components/responses/Conflict"},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/InternalServerError"},"default":{"$ref":"#/components/responses/InternalServerError"}},"security":[{"oauth2":["configurators:read"]}],"tags":["Configurators"]},"delete":{"operationId":"deleteSharedLayout","summary":"Stop sharing a layout","description":"Detaches every configurator using the layout, as detaching each one would, then removes the layout from the shared list. The source keeps its own layout. No configurator is deleted, and releases already published are not changed.\n","parameters":[{"$ref":"#/components/parameters/SharedLayoutIdPath"}],"responses":{"200":{"description":"Deleted","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SharedLayoutDetached"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"$ref":"#/components/responses/Conflict"},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/InternalServerError"},"default":{"$ref":"#/components/responses/InternalServerError"}},"security":[{"oauth2":["configurators:write"]}],"tags":["Configurators"]}},"/shared-layouts/{id}/owner":{"put":{"operationId":"changeSharedLayoutOwner","summary":"Make another configurator the source of a shared layout","description":"The new source must be a draft using the layout. It takes over the layout, images included. Every other configurator using the layout, the previous source included, then follows the new source.\n","parameters":[{"$ref":"#/components/parameters/SharedLayoutIdPath"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"configurator_id":{"type":"string","format":"uuid"}},"required":["configurator_id"]}}}},"responses":{"204":{"description":"Changed"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"$ref":"#/components/responses/Conflict"},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/InternalServerError"},"default":{"$ref":"#/components/responses/InternalServerError"}},"security":[{"oauth2":["configurators:write"]}],"tags":["Configurators"]}},"/configurators/{id}/shared-layout":{"put":{"operationId":"linkSharedLayout","summary":"Use a shared layout","description":"Links the configurator's draft to the shared layout and switches it to the layout's theme. The draft then uses the source's layout as it is edited, images included, and cannot edit the layout itself; only the text written for this configurator's own content stays its own. The configurator's own stored layout is left in place and ignored while linked. A published release cannot be linked. The layout must belong to the configurator's workspace, and a source cannot link to a layout.\n","parameters":[{"$ref":"#/components/parameters/ConfiguratorIdPath"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"shared_layout_id":{"description":"The `id` of a shared layout, listed by `/shared-layouts`","type":"string","format":"uuid"}},"required":["shared_layout_id"]}}}},"responses":{"204":{"description":"Linked"},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"$ref":"#/components/responses/Conflict"},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/InternalServerError"},"default":{"$ref":"#/components/responses/InternalServerError"}},"security":[{"oauth2":["configurators:write"]}],"tags":["Configurators"]},"delete":{"operationId":"detachSharedLayout","summary":"Detach from the shared layout","description":"Copies the layout the draft is using, images included, into the draft and unlinks it, so the draft edits the layout itself from here on and no longer follows the source. Releases already published are not changed.\n","parameters":[{"$ref":"#/components/parameters/ConfiguratorIdPath"}],"responses":{"200":{"description":"Detached","content":{"application/json":{"schema":{"type":"object","properties":{"revision":{"description":"The revision of the layout now stored","type":"integer","format":"int32"}},"required":["revision"]}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"$ref":"#/components/responses/Conflict"},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/InternalServerError"},"default":{"$ref":"#/components/responses/InternalServerError"}},"security":[{"oauth2":["configurators:write"]}],"tags":["Configurators"]}},"/configurators/{id}/apply-layout-template":{"post":{"operationId":"applyLayoutTemplate","summary":"Replace the configurator's layout with a template","description":"Copies the template's layout over the configurator's, and switches the configurator to the template's theme, in one step: either both change or neither does. Send the `revision` you read the layout at (0 when it has no stored layout yet); a stale revision is refused with 409. The template must belong to the configurator's workspace.\n","parameters":[{"$ref":"#/components/parameters/ConfiguratorIdPath"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"layout_template_id":{"type":"string","format":"uuid"},"revision":{"description":"The revision the configurator's layout was read at","type":"integer","format":"int32","minimum":0}},"required":["layout_template_id","revision"]}}}},"responses":{"200":{"description":"Applied","content":{"application/json":{"schema":{"type":"object","properties":{"revision":{"description":"The revision now stored","type":"integer","format":"int32"}},"required":["revision"]}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"$ref":"#/components/responses/Conflict"},"422":{"$ref":"#/components/responses/LayoutRefused"},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/InternalServerError"},"default":{"$ref":"#/components/responses/InternalServerError"}},"security":[{"oauth2":["configurators:write"]}],"tags":["Configurators"]}},"/configurators/{id}/theme-image-upload-url":{"get":{"operationId":"getConfiguratorThemeImageUploadUrl","summary":"Get theme image upload URL","description":"Gets a presigned URL for uploading an image used by a theme setting (for example the brand bar logo). The returned `image_key` is what the theme settings document stores as the setting's `assetId`.\n","parameters":[{"$ref":"#/components/parameters/ConfiguratorIdPath"},{"$ref":"#/components/parameters/ContentHashQuery"}],"responses":{"200":{"description":"Successful operation","content":{"application/json":{"schema":{"type":"object","properties":{"upload_url":{"description":"Presigned PUT URL for upload","type":"string"},"image_key":{"description":"S3 key for the image","type":"string"}},"required":["upload_url","image_key"]}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/InternalServerError"},"default":{"$ref":"#/components/responses/InternalServerError"}},"security":[{"oauth2":["configurators:write"]}],"tags":["Configurators"]}},"/configurators/{id}/theme-font-upload-url":{"get":{"operationId":"getConfiguratorThemeFontUploadUrl","summary":"Get theme font upload URL","description":"Gets a presigned URL for uploading a font file for the layout's custom fonts. The returned `font_key` is what the layout document stores as a file's `assetId`.\n","parameters":[{"$ref":"#/components/parameters/ConfiguratorIdPath"},{"$ref":"#/components/parameters/ContentHashQuery"},{"name":"format","in":"query","description":"The font file's format","required":true,"schema":{"type":"string","enum":["woff2","woff","ttf","otf"]}}],"responses":{"200":{"description":"Successful operation","content":{"application/json":{"schema":{"type":"object","properties":{"upload_url":{"description":"Presigned PUT URL for upload","type":"string"},"font_key":{"description":"S3 key for the font file","type":"string"}},"required":["upload_url","font_key"]}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/InternalServerError"},"default":{"$ref":"#/components/responses/InternalServerError"}},"security":[{"oauth2":["configurators:write"]}],"tags":["Configurators"]}},"/configurators/{id}/viewpoint-upload-url":{"get":{"operationId":"getConfiguratorViewpointUploadUrl","summary":"Get saved-viewpoint thumbnail upload URL","description":"Gets a presigned URL for uploading a saved-viewpoint thumbnail image (GLB/pano captures)","parameters":[{"$ref":"#/components/parameters/ConfiguratorIdPath"},{"$ref":"#/components/parameters/ContentHashQuery"}],"responses":{"200":{"description":"Successful operation","content":{"application/json":{"schema":{"type":"object","properties":{"upload_url":{"description":"Presigned PUT URL for upload","type":"string"},"image_key":{"description":"S3 key for the image","type":"string"}},"required":["upload_url","image_key"]}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/InternalServerError"},"default":{"$ref":"#/components/responses/InternalServerError"}},"security":[{"oauth2":["configurators:write"]}],"tags":["Configurators"]}},"/configurators/{id}/viewpoint-thumbnail-url":{"get":{"operationId":"getConfiguratorViewpointThumbnailUrl","summary":"Get saved-viewpoint thumbnail read URL","description":"Presigned read URL for a saved-viewpoint thumbnail previously\nuploaded via viewpoint-upload-url. `key` must be the exact\n`image_key` returned by that endpoint — 403 if it doesn't belong to\nthis configurator's viewpoints namespace, 404 if nothing was\nuploaded to it yet.\n","parameters":[{"$ref":"#/components/parameters/ConfiguratorIdPath"},{"name":"key","in":"query","description":"The image_key returned by viewpoint-upload-url","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Successful operation","content":{"application/json":{"schema":{"type":"object","properties":{"url":{"type":"string"}},"required":["url"]}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/InternalServerError"},"default":{"$ref":"#/components/responses/InternalServerError"}},"security":[{"oauth2":["configurators:read"]}],"tags":["Configurators"]}},"/configurators/{id}/scene-upload-url":{"get":{"operationId":"getConfiguratorSceneUploadUrl","summary":"Get saved-scene thumbnail upload URL","description":"Gets a presigned URL for uploading a saved-scene thumbnail image (GLB viewport capture)","parameters":[{"$ref":"#/components/parameters/ConfiguratorIdPath"},{"$ref":"#/components/parameters/ContentHashQuery"}],"responses":{"200":{"description":"Successful operation","content":{"application/json":{"schema":{"type":"object","properties":{"upload_url":{"description":"Presigned PUT URL for upload","type":"string"},"image_key":{"description":"S3 key for the image","type":"string"}},"required":["upload_url","image_key"]}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/InternalServerError"},"default":{"$ref":"#/components/responses/InternalServerError"}},"security":[{"oauth2":["configurators:write"]}],"tags":["Configurators"]}},"/configurators/{id}/scene-thumbnail-url":{"get":{"operationId":"getConfiguratorSceneThumbnailUrl","summary":"Get saved-scene thumbnail read URL","description":"Presigned read URL for a saved-scene thumbnail previously uploaded\nvia scene-upload-url. `key` must be the exact `image_key` returned\nby that endpoint — 403 if it doesn't belong to this configurator's\nscenes namespace, 404 if nothing was uploaded to it yet.\n","parameters":[{"$ref":"#/components/parameters/ConfiguratorIdPath"},{"name":"key","in":"query","description":"The image_key returned by scene-upload-url","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Successful operation","content":{"application/json":{"schema":{"type":"object","properties":{"url":{"type":"string"}},"required":["url"]}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/InternalServerError"},"default":{"$ref":"#/components/responses/InternalServerError"}},"security":[{"oauth2":["configurators:read"]}],"tags":["Configurators"]}},"/configurators/{id}/option-image-upload-url":{"get":{"operationId":"getConfiguratorOptionImageUploadUrl","summary":"Get option image upload URL","description":"Gets a presigned URL for uploading an option tile image","parameters":[{"$ref":"#/components/parameters/ConfiguratorIdPath"},{"name":"option_key","in":"query","description":"The FrontEndRule key for the option","required":true,"schema":{"type":"string"}},{"$ref":"#/components/parameters/ContentHashQuery"}],"responses":{"200":{"description":"Successful operation","content":{"application/json":{"schema":{"type":"object","properties":{"upload_url":{"description":"Presigned PUT URL for upload","type":"string"},"image_key":{"description":"S3 key for the image","type":"string"}},"required":["upload_url","image_key"]}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/InternalServerError"},"default":{"$ref":"#/components/responses/InternalServerError"}},"security":[{"oauth2":["configurators:write"]}],"tags":["Configurators"]}},"/configurators/{id}/thumbnail":{"post":{"operationId":"triggerConfiguratorThumbnail","summary":"Regenerate configurator thumbnail","description":"Phase 1 manual trigger — enqueues a headless render of this\nconfigurator's /preview route and, once it completes, overwrites its\ncover thumbnail. Fire-and-forget: enqueues and returns immediately,\nno debounce yet (every call enqueues a render). Owner-only.\n","parameters":[{"$ref":"#/components/parameters/ConfiguratorIdPath"}],"responses":{"202":{"description":"Thumbnail render enqueued","content":{"application/json":{"schema":{"type":"object","properties":{"enqueued":{"type":"boolean"}},"required":["enqueued"]}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/InternalServerError"},"default":{"$ref":"#/components/responses/InternalServerError"}},"security":[{"oauth2":["configurators:write"]}],"tags":["Configurators"]}},"/configurators/{id}/thumbnail-url":{"get":{"operationId":"getConfiguratorThumbnailUrl","summary":"Get configurator cover thumbnail URL","description":"Presigned URL for the configurator's cover thumbnail (rendered from\nits primary project's default configuration). 404 if none has been\ngenerated yet — the frontend falls back to a placeholder, same as\nthe per-project thumb-url endpoint.\n","parameters":[{"$ref":"#/components/parameters/ConfiguratorIdPath"}],"responses":{"200":{"description":"Successful operation","content":{"application/json":{"schema":{"type":"object","properties":{"url":{"type":"string"}},"required":["url"]}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/InternalServerError"},"default":{"$ref":"#/components/responses/InternalServerError"}},"security":[{"oauth2":["configurators:read"]}],"tags":["Configurators"]}}},"webhooks":{"configurator.enquiry.submitted":{"post":{"operationId":"configuratorEnquirySubmittedWebhook","summary":"A visitor sent an enquiry from one of the studio's configurators","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ConfiguratorEnquirySubmittedEvent"}}}},"responses":{"200":{"description":"Received"}},"tags":["Webhooks"]}},"project.created":{"post":{"operationId":"projectCreatedWebhook","summary":"A project was created in one of the studio's workspaces","description":"Also sent for a copy made with Duplicate, which sends no event for the versions it copies.","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProjectEvent"}}}},"responses":{"200":{"description":"Received"}},"tags":["Webhooks"]}},"project.updated":{"post":{"operationId":"projectUpdatedWebhook","summary":"A project was renamed, moved or had its visibility changed","description":"`changes` holds the previous value of each field that changed. Moving a folder to another workspace sends this for each project inside it.","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProjectEvent"}}}},"responses":{"200":{"description":"Received"}},"tags":["Webhooks"]}},"project.deleted":{"post":{"operationId":"projectDeletedWebhook","summary":"A project was moved to the trash","description":"It can be restored from the trash until it is purged.","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProjectEvent"}}}},"responses":{"200":{"description":"Received"}},"tags":["Webhooks"]}},"project.restored":{"post":{"operationId":"projectRestoredWebhook","summary":"A project was restored from the trash","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProjectEvent"}}}},"responses":{"200":{"description":"Received"}},"tags":["Webhooks"]}},"version.created":{"post":{"operationId":"versionCreatedWebhook","summary":"A version was added to a project","description":"Sent when the version is made, before its files arrive. `version.uploaded` follows once they have.","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/VersionEvent"}}}},"responses":{"200":{"description":"Received"}},"tags":["Webhooks"]}},"version.uploaded":{"post":{"operationId":"versionUploadedWebhook","summary":"A version's files finished uploading","description":"Sent by an upload, including one into a version that already had files, and when a draft fills an empty version. Also sent when changes saved in the file editor are applied to a version that already had files, credited to whoever applied them.","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/VersionEvent"}}}},"responses":{"200":{"description":"Received"}},"tags":["Webhooks"]}},"version.updated":{"post":{"operationId":"versionUpdatedWebhook","summary":"A version's description was changed","description":"`changes` holds the previous description.","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/VersionEvent"}}}},"responses":{"200":{"description":"Received"}},"tags":["Webhooks"]}},"version.deleted":{"post":{"operationId":"versionDeletedWebhook","summary":"A version was moved to the trash","description":"Also sent, by `system`, when an empty version that was never uploaded is cleared away.","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/VersionEvent"}}}},"responses":{"200":{"description":"Received"}},"tags":["Webhooks"]}},"version.restored":{"post":{"operationId":"versionRestoredWebhook","summary":"A version was restored from the trash","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/VersionEvent"}}}},"responses":{"200":{"description":"Received"}},"tags":["Webhooks"]}},"comment.created":{"post":{"operationId":"commentCreatedWebhook","summary":"Someone commented on a version, or replied to a comment","description":"`comment.in_reply_to` is set on a reply. A note a reviewer writes during a review is held until the review is decided: approving or requesting changes sends `comment.created` for each of its notes and the replies under them, and cancelling the review discards them unsent.","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CommentEvent"}}}},"responses":{"200":{"description":"Received"}},"tags":["Webhooks"]}},"comment.updated":{"post":{"operationId":"commentUpdatedWebhook","summary":"A comment or reply was edited, or a comment's label changed","description":"`changes` holds the previous value of each field that changed. Renaming a label across a project sends `comment.labels_renamed` instead.","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CommentEvent"}}}},"responses":{"200":{"description":"Received"}},"tags":["Webhooks"]}},"comment.deleted":{"post":{"operationId":"commentDeletedWebhook","summary":"A comment or reply was moved to the trash","description":"Also sent for each comment on a generated still when the still is removed from its version.","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CommentEvent"}}}},"responses":{"200":{"description":"Received"}},"tags":["Webhooks"]}},"comment.resolved":{"post":{"operationId":"commentResolvedWebhook","summary":"A comment was marked resolved","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CommentEvent"}}}},"responses":{"200":{"description":"Received"}},"tags":["Webhooks"]}},"comment.unresolved":{"post":{"operationId":"commentUnresolvedWebhook","summary":"A resolved comment was opened again","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CommentEvent"}}}},"responses":{"200":{"description":"Received"}},"tags":["Webhooks"]}},"approval.requested":{"post":{"operationId":"approvalRequestedWebhook","summary":"Someone asked for a version to be approved","description":"A request made of a reviewer who is already reviewing the version joins that review, and is sent `in_review`.","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApprovalRequestEvent"}}}},"responses":{"200":{"description":"Received"}},"tags":["Webhooks"]}},"approval.review_started":{"post":{"operationId":"approvalReviewStartedWebhook","summary":"A reviewer started reviewing a version","description":"Sent for each of the reviewer's requests on the version.","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApprovalRequestEvent"}}}},"responses":{"200":{"description":"Received"}},"tags":["Webhooks"]}},"approval.approved":{"post":{"operationId":"approvalApprovedWebhook","summary":"A reviewer approved a version","description":"Sent for each of the reviewer's requests on the version.","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApprovalRequestEvent"}}}},"responses":{"200":{"description":"Received"}},"tags":["Webhooks"]}},"approval.changes_requested":{"post":{"operationId":"approvalChangesRequestedWebhook","summary":"A reviewer asked for changes to a version","description":"Sent for each of the reviewer's requests on the version.","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApprovalRequestEvent"}}}},"responses":{"200":{"description":"Received"}},"tags":["Webhooks"]}},"approval.review_cancelled":{"post":{"operationId":"approvalReviewCancelledWebhook","summary":"A reviewer abandoned a review without deciding","description":"The requests go back to `pending`, and the notes written during the review are discarded.","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApprovalRequestEvent"}}}},"responses":{"200":{"description":"Received"}},"tags":["Webhooks"]}},"approval.reopened":{"post":{"operationId":"approvalReopenedWebhook","summary":"A decided request was opened again","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApprovalRequestEvent"}}}},"responses":{"200":{"description":"Received"}},"tags":["Webhooks"]}},"approval.cancelled":{"post":{"operationId":"approvalCancelledWebhook","summary":"Whoever asked for an approval withdrew the request","description":"The request no longer exists afterwards.","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApprovalRequestEvent"}}}},"responses":{"200":{"description":"Received"}},"tags":["Webhooks"]}},"folder.created":{"post":{"operationId":"folderCreatedWebhook","summary":"A folder was created in one of the studio's workspaces","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/FolderEvent"}}}},"responses":{"200":{"description":"Received"}},"tags":["Webhooks"]}},"folder.updated":{"post":{"operationId":"folderUpdatedWebhook","summary":"A folder was renamed, recoloured or moved","description":"`changes` holds the previous value of each field that changed. Moving a folder to another workspace also sends this for each folder inside it, and `project.updated` for each project inside it, trashed ones included.","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/FolderEvent"}}}},"responses":{"200":{"description":"Received"}},"tags":["Webhooks"]}},"folder.deleted":{"post":{"operationId":"folderDeletedWebhook","summary":"A folder was moved to the trash","description":"Only an empty folder can be deleted.","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/FolderEvent"}}}},"responses":{"200":{"description":"Received"}},"tags":["Webhooks"]}},"folder.restored":{"post":{"operationId":"folderRestoredWebhook","summary":"A folder was restored from the trash","description":"Restoring a folder, or a project in a trashed folder, also restores every trashed folder above it, and this is sent for each.","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/FolderEvent"}}}},"responses":{"200":{"description":"Received"}},"tags":["Webhooks"]}},"workspace.created":{"post":{"operationId":"workspaceCreatedWebhook","summary":"A workspace was created in the studio","description":"`workspace.member_added` follows for its creator, who joins it as an admin.","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/WorkspaceEvent"}}}},"responses":{"200":{"description":"Received"}},"tags":["Webhooks"]}},"workspace.updated":{"post":{"operationId":"workspaceUpdatedWebhook","summary":"A workspace's settings were changed","description":"`changes` holds the previous value of each field that changed.","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/WorkspaceEvent"}}}},"responses":{"200":{"description":"Received"}},"tags":["Webhooks"]}},"workspace.deleted":{"post":{"operationId":"workspaceDeletedWebhook","summary":"A workspace was moved to the trash","description":"Its projects and folders stay as they were, and send nothing.","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/WorkspaceEvent"}}}},"responses":{"200":{"description":"Received"}},"tags":["Webhooks"]}},"workspace.restored":{"post":{"operationId":"workspaceRestoredWebhook","summary":"A workspace was restored from the trash","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/WorkspaceEvent"}}}},"responses":{"200":{"description":"Received"}},"tags":["Webhooks"]}},"workspace.member_added":{"post":{"operationId":"workspaceMemberAddedWebhook","summary":"Someone joined a workspace","description":"Sent when an invitation is accepted, when someone already in the studio is added directly, and for a workspace's creator.","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/WorkspaceMemberEvent"}}}},"responses":{"200":{"description":"Received"}},"tags":["Webhooks"]}},"workspace.member_removed":{"post":{"operationId":"workspaceMemberRemovedWebhook","summary":"Someone left or was removed from a workspace","description":"Also sent for each of their workspaces when someone is removed from the studio, after `studio.member_removed`. Not sent when an instance admin merges two people into one.","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/WorkspaceMemberEvent"}}}},"responses":{"200":{"description":"Received"}},"tags":["Webhooks"]}},"workspace.member_role_changed":{"post":{"operationId":"workspaceMemberRoleChangedWebhook","summary":"A workspace member's role was changed","description":"`changes.role.from` holds the previous role.","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/WorkspaceMemberEvent"}}}},"responses":{"200":{"description":"Received"}},"tags":["Webhooks"]}},"share.created":{"post":{"operationId":"shareCreatedWebhook","summary":"A share link was made for a project","description":"A share that isn't private opens for anyone who has its ID, so treat `share.id` as a secret.","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ShareEvent"}}}},"responses":{"200":{"description":"Received"}},"tags":["Webhooks"]}},"share.updated":{"post":{"operationId":"shareUpdatedWebhook","summary":"A share link's settings were changed","description":"`changes` holds the previous value of each setting that changed. A share that isn't private opens for anyone who has its ID, so treat `share.id` as a secret.","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ShareEvent"}}}},"responses":{"200":{"description":"Received"}},"tags":["Webhooks"]}},"share.deleted":{"post":{"operationId":"shareDeletedWebhook","summary":"A share link was moved to the trash","description":"A share that isn't private opens for anyone who has its ID, so treat `share.id` as a secret.","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ShareEvent"}}}},"responses":{"200":{"description":"Received"}},"tags":["Webhooks"]}},"studio.member_added":{"post":{"operationId":"studioMemberAddedWebhook","summary":"Someone joined the studio","description":"Sent when a studio invitation is accepted, when a workspace invitation brings someone in, when SCIM provisions them, and when signing in with the studio's single sign-on adds them. Not sent when an instance admin merges two people into one.","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/StudioMemberEvent"}}}},"responses":{"200":{"description":"Received"}},"tags":["Webhooks"]}},"studio.member_removed":{"post":{"operationId":"studioMemberRemovedWebhook","summary":"Someone was removed from the studio","description":"By a studio admin or by SCIM. `workspace.member_removed` follows for each of their workspaces. Not sent when an instance admin merges two people into one.","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/StudioMemberEvent"}}}},"responses":{"200":{"description":"Received"}},"tags":["Webhooks"]}},"studio.member_role_changed":{"post":{"operationId":"studioMemberRoleChangedWebhook","summary":"A studio member's role was changed","description":"`changes.role.from` holds the previous role. Not sent when an instance admin merges two people into one.","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/StudioMemberEvent"}}}},"responses":{"200":{"description":"Received"}},"tags":["Webhooks"]}},"invitation.created":{"post":{"operationId":"invitationCreatedWebhook","summary":"Someone was invited to the studio, a workspace or a share","description":"Inviting someone who already has a pending invitation to the same place cancels that one first. A studio invitation is sent just after it is saved rather than with it.","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/InvitationEvent"}}}},"responses":{"200":{"description":"Received"}},"tags":["Webhooks"]}},"invitation.accepted":{"post":{"operationId":"invitationAcceptedWebhook","summary":"An invitation was accepted","description":"Including one accepted for them when they sign in, and one an instance admin accepts on their behalf. `studio.member_added` or `workspace.member_added` follows when it brings them in.","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/InvitationEvent"}}}},"responses":{"200":{"description":"Received"}},"tags":["Webhooks"]}},"invitation.declined":{"post":{"operationId":"invitationDeclinedWebhook","summary":"Someone declined an invitation","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/InvitationEvent"}}}},"responses":{"200":{"description":"Received"}},"tags":["Webhooks"]}},"invitation.cancelled":{"post":{"operationId":"invitationCancelledWebhook","summary":"An invitation was withdrawn or replaced","description":"Sent when someone withdraws it, when a newer invitation to the same place replaces it, and when the person joins another way.","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/InvitationEvent"}}}},"responses":{"200":{"description":"Received"}},"tags":["Webhooks"]}},"generation.completed":{"post":{"operationId":"generationCompletedWebhook","summary":"A generated take finished","description":"Sent by `system` once the take's image or video is ready. It isn't in the payload: open the version to see it.","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/GenerationEvent"}}}},"responses":{"200":{"description":"Received"}},"tags":["Webhooks"]}},"generation.failed":{"post":{"operationId":"generationFailedWebhook","summary":"A generated take failed","description":"Sent by `system` once the take has run out of attempts or was cancelled. A failed take can be retried, so `generation.completed` or another `generation.failed` may follow for the same `generation.id`.","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/GenerationEvent"}}}},"responses":{"200":{"description":"Received"}},"tags":["Webhooks"]}},"generation.kept":{"post":{"operationId":"generationKeptWebhook","summary":"A generated take was kept in its version","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/GenerationEvent"}}}},"responses":{"200":{"description":"Received"}},"tags":["Webhooks"]}},"generation.discarded":{"post":{"operationId":"generationDiscardedWebhook","summary":"A generated take was discarded","description":"Discarding a kept take also sends `comment.deleted` for each comment on it.","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/GenerationEvent"}}}},"responses":{"200":{"description":"Received"}},"tags":["Webhooks"]}},"configurator.created":{"post":{"operationId":"configuratorCreatedWebhook","summary":"A configurator was created","description":"Also sent for the new draft when one is made from a release.","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ConfiguratorEvent"}}}},"responses":{"200":{"description":"Received"}},"tags":["Webhooks"]}},"configurator.published":{"post":{"operationId":"configuratorPublishedWebhook","summary":"A configurator's draft was published as a new release","description":"`configurator` is the new release. `configurator.deployed` is sent too when it was published straight to live.","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ConfiguratorEvent"}}}},"responses":{"200":{"description":"Received"}},"tags":["Webhooks"]}},"configurator.deployed":{"post":{"operationId":"configuratorDeployedWebhook","summary":"Visitors now see a different release, or a split of several","description":"Sent when a release is made live and when an A/B test starts or picks a winner. `releases` lists every live release with its share of visitors, which add up to 100.","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ConfiguratorDeploymentEvent"}}}},"responses":{"200":{"description":"Received"}},"tags":["Webhooks"]}},"configurator.undeployed":{"post":{"operationId":"configuratorUndeployedWebhook","summary":"A configurator was taken offline","description":"Also sent when an A/B test ends without a winner. `releases` is empty.","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ConfiguratorDeploymentEvent"}}}},"responses":{"200":{"description":"Received"}},"tags":["Webhooks"]}},"configurator.deleted":{"post":{"operationId":"configuratorDeletedWebhook","summary":"A configurator's draft or one of its releases was moved to the trash","description":"A live release can't be deleted.","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ConfiguratorEvent"}}}},"responses":{"200":{"description":"Received"}},"tags":["Webhooks"]}},"version.processed":{"post":{"operationId":"versionProcessedWebhook","summary":"A version's files finished processing","description":"Sent by `system` about ten seconds after the last of a version's files has been processed, with `processing.status` `complete`, or `failed` when any of them couldn't be. Sent again each time the version is processed again, such as after a retry, a reprocess, files uploaded over its own, or renderapp making a file's sizes again. Archives and thumbnails aren't processing.","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/VersionProcessedEvent"}}}},"responses":{"200":{"description":"Received"}},"tags":["Webhooks"]}},"comment.labels_renamed":{"post":{"operationId":"commentLabelsRenamedWebhook","summary":"A project's comment labels were renamed","description":"Sent once for the rename rather than for each comment it relabels. `renames` gives each label's old and new name and how many comments it moved; an empty `to` means the label was removed from them. Only comments everyone can see are counted, and a label that moved none isn't listed.","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CommentLabelsRenamedEvent"}}}},"responses":{"200":{"description":"Received"}},"tags":["Webhooks"]}}},"components":{"examples":{"RioExample":{"summary":"A small configurator","description":"Trims, paints, wheels, a pack and two views. Matte Black and 20-inch wheels need Sport; the Carbon pack needs Sport and not White, and brings the 20-inch wheels with it. Paint, wheels and the pack are layered by depth over the exterior spin's two frames; the interior is one still per trim. Every option can be chosen and every map can show, so the analysis finds nothing.\n","value":{"rio":{"Inputs":[{"Name":"Trim","Type":"Radio","Group":"+TRIM","Options":[{"Name":"Sport","Type":"NotAView","FrontEndRule":"+SPORT","BackEndRule":"+SPORT","Conditions":[],"Price":0},{"Name":"Touring","Type":"NotAView","FrontEndRule":"+TOUR","BackEndRule":"+TOUR","Conditions":[],"Price":2500}]},{"Name":"Paint","Type":"Radio","Group":"+PAINT","Options":[{"Name":"Red","Type":"NotAView","FrontEndRule":"+RED","BackEndRule":"+RED","Conditions":[],"Colour":"#b3001b"},{"Name":"White","Type":"NotAView","FrontEndRule":"+WHITE","BackEndRule":"+WHITE","Conditions":[],"Colour":"#f4f4f2"},{"Name":"Matte Black","Type":"NotAView","FrontEndRule":"+MATTE","BackEndRule":"+MATTE","Conditions":["+SPORT"],"Colour":"#1c1c1c","Price":1200}]},{"Name":"Wheels","Type":"Radio","Group":"+WHEEL","Options":[{"Name":"18-inch alloy","Type":"NotAView","FrontEndRule":"+W18","BackEndRule":"+W18","Conditions":[]},{"Name":"20-inch forged","Type":"NotAView","FrontEndRule":"+W20","BackEndRule":"+W20","Conditions":["+SPORT"],"Price":1800}]},{"Name":"Packs","Type":"Checkbox","Group":"+PACK","Options":[{"Name":"Carbon pack","Type":"NotAView","FrontEndRule":"+CARBON","BackEndRule":"+CARBON","Conditions":["+SPORT+!WHITE"],"Implies":["+W20"],"Price":3200}]},{"Name":"View","Type":"Radio","Group":"+VEW","Options":[{"Name":"Exterior","Type":"Animation","FrontEndRule":"+EXT","BackEndRule":"+EXT","Conditions":[],"Frames":[0,1]},{"Name":"Interior","Type":"Still","FrontEndRule":"+INT","BackEndRule":"+INT","Conditions":[],"Frames":[0]}]}],"Maps":[{"filename":"exterior/red_00.jpg","configuration":"+RED+EXT","depth":1,"frame":0},{"filename":"exterior/red_01.jpg","configuration":"+RED+EXT","depth":1,"frame":1},{"filename":"exterior/white_00.jpg","configuration":"+WHITE+EXT","depth":1,"frame":0},{"filename":"exterior/white_01.jpg","configuration":"+WHITE+EXT","depth":1,"frame":1},{"filename":"exterior/matte_00.jpg","configuration":"+MATTE+EXT","depth":1,"frame":0},{"filename":"exterior/matte_01.jpg","configuration":"+MATTE+EXT","depth":1,"frame":1},{"filename":"exterior/wheels-18_00.png","configuration":"+W18+EXT","depth":2,"frame":0},{"filename":"exterior/wheels-18_01.png","configuration":"+W18+EXT","depth":2,"frame":1},{"filename":"exterior/wheels-20_00.png","configuration":"+W20+EXT","depth":2,"frame":0},{"filename":"exterior/wheels-20_01.png","configuration":"+W20+EXT","depth":2,"frame":1},{"filename":"exterior/carbon_00.png","configuration":"+CARBON+EXT","depth":3,"frame":0},{"filename":"exterior/carbon_01.png","configuration":"+CARBON+EXT","depth":3,"frame":1},{"filename":"interior/sport.jpg","configuration":"+SPORT+INT","depth":1,"frame":0},{"filename":"interior/touring.jpg","configuration":"+TOUR+INT","depth":1,"frame":0}]}}},"EvaluateExample":{"summary":"The Carbon pack on a Sport","description":"`RioExample`'s rio with Sport and then the Carbon pack chosen. The pack implies the 20-inch wheels, so each exterior frame draws the red paint, the 20-inch wheels and the carbon parts, in that order.\n","value":{"rio":{"Inputs":[{"Name":"Trim","Type":"Radio","Group":"+TRIM","Options":[{"Name":"Sport","Type":"NotAView","FrontEndRule":"+SPORT","BackEndRule":"+SPORT","Conditions":[],"Price":0},{"Name":"Touring","Type":"NotAView","FrontEndRule":"+TOUR","BackEndRule":"+TOUR","Conditions":[],"Price":2500}]},{"Name":"Paint","Type":"Radio","Group":"+PAINT","Options":[{"Name":"Red","Type":"NotAView","FrontEndRule":"+RED","BackEndRule":"+RED","Conditions":[],"Colour":"#b3001b"},{"Name":"White","Type":"NotAView","FrontEndRule":"+WHITE","BackEndRule":"+WHITE","Conditions":[],"Colour":"#f4f4f2"},{"Name":"Matte Black","Type":"NotAView","FrontEndRule":"+MATTE","BackEndRule":"+MATTE","Conditions":["+SPORT"],"Colour":"#1c1c1c","Price":1200}]},{"Name":"Wheels","Type":"Radio","Group":"+WHEEL","Options":[{"Name":"18-inch alloy","Type":"NotAView","FrontEndRule":"+W18","BackEndRule":"+W18","Conditions":[]},{"Name":"20-inch forged","Type":"NotAView","FrontEndRule":"+W20","BackEndRule":"+W20","Conditions":["+SPORT"],"Price":1800}]},{"Name":"Packs","Type":"Checkbox","Group":"+PACK","Options":[{"Name":"Carbon pack","Type":"NotAView","FrontEndRule":"+CARBON","BackEndRule":"+CARBON","Conditions":["+SPORT+!WHITE"],"Implies":["+W20"],"Price":3200}]},{"Name":"View","Type":"Radio","Group":"+VEW","Options":[{"Name":"Exterior","Type":"Animation","FrontEndRule":"+EXT","BackEndRule":"+EXT","Conditions":[],"Frames":[0,1]},{"Name":"Interior","Type":"Still","FrontEndRule":"+INT","BackEndRule":"+INT","Conditions":[],"Frames":[0]}]}],"Maps":[{"filename":"exterior/red_00.jpg","configuration":"+RED+EXT","depth":1,"frame":0},{"filename":"exterior/red_01.jpg","configuration":"+RED+EXT","depth":1,"frame":1},{"filename":"exterior/white_00.jpg","configuration":"+WHITE+EXT","depth":1,"frame":0},{"filename":"exterior/white_01.jpg","configuration":"+WHITE+EXT","depth":1,"frame":1},{"filename":"exterior/matte_00.jpg","configuration":"+MATTE+EXT","depth":1,"frame":0},{"filename":"exterior/matte_01.jpg","configuration":"+MATTE+EXT","depth":1,"frame":1},{"filename":"exterior/wheels-18_00.png","configuration":"+W18+EXT","depth":2,"frame":0},{"filename":"exterior/wheels-18_01.png","configuration":"+W18+EXT","depth":2,"frame":1},{"filename":"exterior/wheels-20_00.png","configuration":"+W20+EXT","depth":2,"frame":0},{"filename":"exterior/wheels-20_01.png","configuration":"+W20+EXT","depth":2,"frame":1},{"filename":"exterior/carbon_00.png","configuration":"+CARBON+EXT","depth":3,"frame":0},{"filename":"exterior/carbon_01.png","configuration":"+CARBON+EXT","depth":3,"frame":1},{"filename":"interior/sport.jpg","configuration":"+SPORT+INT","depth":1,"frame":0},{"filename":"interior/touring.jpg","configuration":"+TOUR+INT","depth":1,"frame":0}]},"choices":["+SPORT","+CARBON"]}},"NamingSchemeExample":{"summary":"Trim folders, then paint, view and frame in the name","description":"For a version holding `Sport/Red_Spin_00.jpg`, `Sport/Red_Spin_01.jpg`, `Touring/White_Interior_00.jpg` and so on: split the path on `/`, `_` and `.` (the extension is dropped first), so the folder names the trim, then come paint, view and frame. Each name becomes an option in its category and each view an Animation.\n","value":{"scheme":{"source":"path","split":{"delimiters":["/","_","."],"tokens":[{"role":"option","category":"Trim"},{"role":"option","category":"Paint"},{"role":"view"},{"role":"frame"}]}}}},"NamingSchemeLayersExample":{"summary":"A spin rendered in layers, one folder per layer and a folder per paint","description":"For a version holding `01_Base/Red/spin_00.jpg`, `02_Paint/Red/spin_00.jpg`, `03_Carbon_Pack/Red/spin_00.jpg` and so on, where each folder is a layer drawn over the one before and the paint folders repeat in each. Folder names hold `_` themselves, so each layer matches with a `pattern` rather than delimiters. Every pattern names its paint group `Paint`, so one Paint choice drives all three layers. The pack's own group matches its folder's literal name, which makes it an option of a \"Carbon pack\" Checkbox; the option is named `03_Carbon_Pack` after the folder, so rename it in the generated rio. `frame` gives each file its place in the one Spin view.\n","value":{"scheme":{"source":"path","split":{"pattern":"^01_Base/(?<Paint>[^/]+)/spin_(?<frame>\\d+)$"},"view":{"name":"Spin","type":"Animation"},"checkboxes":["Carbon pack"],"layers":[{"name":"Paint","source":"path","depth":1,"include":["02_Paint/**"],"split":{"pattern":"^02_Paint/(?<Paint>[^/]+)/spin_(?<frame>\\d+)$"}},{"name":"Carbon pack","source":"path","depth":2,"include":["03_Carbon_Pack/**"],"split":{"pattern":"^(?<Carbon_pack>03_Carbon_Pack)/(?<Paint>[^/]+)/spin_(?<frame>\\d+)$","names":{"Carbon_pack":"Carbon pack"}}}]}}},"LayoutDocumentExample":{"summary":"A brand palette and a few changes from the theme's defaults","description":"Only the values that differ from the theme's defaults are listed: a brand palette and fonts, a smaller corner radius, the price moved to the left of the top bar beside the title, the summary link shown on the right, and tighter option boxes. `schemaVersion` and `theme.version` follow `GET /configurator-themes/automotive-generic`.\n","value":{"document":{"schemaVersion":1,"theme":{"id":"automotive-generic","version":3},"brand":{"palette":{"light":{"primary":"#0b3d91","secondary":"#4a5a75","tertiary":"#9aa6b8"},"dark":{"primary":"#8fb4ff","secondary":"#a9b6cc","tertiary":"#5d6a80"}},"fonts":{"heading":"barlow","body":"inter"},"values":{"radius":8}},"layout":{"values":{"page.background":{"role":"surface"}},"placements":{"price":"header-start","topBarSummary":"header-end"}},"components":{"title":{"values":{"size":20,"colour":{"role":"primary"}}},"topBarSummary":{"values":{"visible":true,"content":{"text":"Review your build","marks":[]}}}},"options":{"scopes":{"configurator":{"optionBox.radius":4,"categoryHeader.text.size":18},"types":{"collapsible+single":{"categoryCollapse.startOpen":true}}}}}}}},"schemas":{"AuditLedgerEntry":{"type":"object","properties":{"hour":{"type":"string","format":"date-time","example":"2026-11-02T09:00:00.000Z","description":"Start of the UTC hour, exactly as hashed."},"events":{"type":"integer","format":"int32"},"digest":{"type":"string","description":"SHA-256 of the hour's sorted event uids."},"prev_hash":{"type":"string","description":"The previous entry's hash. Absent on the partition's first entry."},"hash":{"type":"string"}},"required":["hour","events","digest","hash"]},"AuditLogFile":{"type":"object","properties":{"key":{"type":"string","description":"The object's key. Stable for as long as the file exists.","example":"audit/0b9e1f5c-3c3a-4e43-9a51-2f6d8f0c1a77/api_activity/write/day/2026-11-01.parquet"},"url":{"type":"string","format":"uri","description":"Presigned GET URL."},"size":{"type":"integer","format":"int64"},"etag":{"type":"string"},"granularity":{"type":"string","enum":["hour","day","month"]},"from":{"type":"string","format":"date-time","example":"2026-11-01T00:00:00Z","description":"Start of the period the file covers."},"to":{"type":"string","format":"date-time","example":"2026-11-02T00:00:00Z","description":"End of the period the file covers, exclusive."}},"required":["key","url","size","etag","granularity","from","to"]},"WebhookEventType":{"type":"string","description":"An event a webhook endpoint can subscribe to. Each is delivered on its own, so two events from one change, or from changes close together, can arrive in either order.","enum":["configurator.enquiry.submitted","project.created","project.updated","project.deleted","project.restored","version.created","version.uploaded","version.updated","version.deleted","version.restored","version.processed","comment.created","comment.updated","comment.deleted","comment.resolved","comment.unresolved","comment.labels_renamed","approval.requested","approval.review_started","approval.approved","approval.changes_requested","approval.review_cancelled","approval.reopened","approval.cancelled","folder.created","folder.updated","folder.deleted","folder.restored","workspace.created","workspace.updated","workspace.deleted","workspace.restored","workspace.member_added","workspace.member_removed","workspace.member_role_changed","share.created","share.updated","share.deleted","studio.member_added","studio.member_removed","studio.member_role_changed","invitation.created","invitation.accepted","invitation.declined","invitation.cancelled","generation.completed","generation.failed","generation.kept","generation.discarded","configurator.created","configurator.published","configurator.deployed","configurator.undeployed","configurator.deleted"]},"WebhookEndpoint":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"url":{"type":"string","format":"uri"},"description":{"type":["string","null"]},"event_types":{"type":"array","description":"The events sent to it. `\"*\"` is every event, including ones added later.","items":{"anyOf":[{"$ref":"#/components/schemas/WebhookEventType"},{"type":"string","const":"*"}]}},"disabled":{"type":"boolean","description":"Turned off by someone in the studio. A disabled endpoint is sent nothing."},"failing_since":{"type":["string","null"],"format":"date-time","description":"When its current run of failed deliveries began. Null while it is succeeding."},"paused_at":{"type":["string","null"],"format":"date-time","description":"When it was paused for failing 24 hours. Events are held for it until it is resumed."},"held_count":{"type":"integer","format":"int32","description":"Deliveries waiting for it to be resumed."},"failed_count":{"type":"integer","format":"int32","description":"Deliveries that ran out of attempts or could not be sent, among those still kept."},"last_delivery":{"description":"Its newest delivery, a test included. Null before the first.","oneOf":[{"type":"null"},{"type":"object","properties":{"status":{"$ref":"#/components/schemas/WebhookDeliveryStatus"},"response_status":{"type":["integer","null"],"format":"int32"},"at":{"type":"string","format":"date-time","description":"When it was last attempted, or created if it hasn't been yet."}},"required":["status","response_status","at"]}]},"created_by":{"type":["string","null"],"format":"uuid"},"created_by_name":{"type":["string","null"],"description":"Who added it, by name or else email. Null once they're gone."},"created_at":{"type":"string","format":"date-time"},"updated_at":{"type":"string","format":"date-time"}},"required":["id","url","description","event_types","disabled","failing_since","paused_at","held_count","failed_count","last_delivery","created_by","created_by_name","created_at","updated_at"]},"WebhookEndpointWithSecret":{"allOf":[{"$ref":"#/components/schemas/WebhookEndpoint"},{"$ref":"#/components/schemas/WebhookSecret"}]},"WebhookSecret":{"type":"object","properties":{"secret":{"type":"string","description":"The signing secret, `whsec_` and then base64 of the key, as Standard Webhooks writes it.","example":"whsec_MfKQ9r8GKYqrTwjUPD8ILPZIo2LaLaSw"}},"required":["secret"]},"WebhookEndpointCreate":{"type":"object","additionalProperties":false,"properties":{"url":{"type":"string","format":"uri","maxLength":2048,"description":"Must use https."},"description":{"type":["string","null"],"maxLength":200},"event_types":{"$ref":"#/components/schemas/WebhookEndpointEventTypes"}},"required":["url","event_types"]},"WebhookEndpointUpdate":{"type":"object","additionalProperties":false,"minProperties":1,"properties":{"url":{"type":"string","format":"uri","maxLength":2048,"description":"Must use https."},"description":{"type":["string","null"],"maxLength":200},"event_types":{"$ref":"#/components/schemas/WebhookEndpointEventTypes"},"disabled":{"type":"boolean"}}},"WebhookEndpointEventTypes":{"type":"array","minItems":1,"uniqueItems":true,"items":{"anyOf":[{"$ref":"#/components/schemas/WebhookEventType"},{"type":"string","const":"*"}]}},"WebhookDeliveryStatus":{"type":"string","description":"`queued` and `retrying` are waiting to be sent, `held` waits for a paused endpoint to be resumed, `delivered` was answered with a 2xx, and `failed` ran out of attempts or could not be sent at all.","enum":["queued","retrying","held","delivered","failed"]},"WebhookDelivery":{"type":"object","properties":{"id":{"type":"string","format":"uuid","description":"Sent as the `webhook-id` header, the same on every attempt."},"event_type":{"type":"string","description":"A `WebhookEventType`, or `webhook.test`."},"status":{"$ref":"#/components/schemas/WebhookDeliveryStatus"},"attempts":{"type":"integer","format":"int32"},"response_status":{"type":["integer","null"],"format":"int32","description":"The status code of the last answer, if there was one."},"duration_ms":{"type":["integer","null"],"format":"int32","description":"How long the last attempt took."},"error":{"type":["string","null"]},"payload":{"type":"object","additionalProperties":true,"description":"The body sent."},"created_at":{"type":"string","format":"date-time"},"attempted_at":{"type":["string","null"],"format":"date-time"},"delivered_at":{"type":["string","null"],"format":"date-time"}},"required":["id","event_type","status","attempts","response_status","duration_ms","error","payload","created_at","attempted_at","delivered_at"]},"WebhookActor":{"type":"object","description":"Who made the change. `api_key` is one of the studio's API keys, which acts for the studio rather than a person. `system` is renderapp itself, such as a scheduled clean-up.","properties":{"type":{"type":"string","enum":["person","api_key","system"]},"id":{"type":["string","null"],"format":"uuid","description":"The person's or the key's ID. Null for `system`."},"name":{"type":["string","null"]},"email":{"type":["string","null"],"description":"Set for a person only."}},"required":["type","id","name","email"]},"WebhookWorkspace":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"name":{"type":"string"}},"required":["id","name"]},"WebhookProject":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"name":{"type":"string"},"url":{"type":["string","null"],"format":"uri","description":"The project's latest version in the viewer. Null while it has no versions."},"workspace":{"$ref":"#/components/schemas/WebhookWorkspace"},"folder_id":{"type":["string","null"],"format":"uuid","description":"The folder it sits in. Null at the top of its workspace."},"visibility":{"type":"string","enum":["PRIVATE","INTERNAL","PUBLIC"]},"latest_version":{"type":["integer","null"],"format":"int32","description":"The highest version not in the trash."},"created_by":{"type":["string","null"],"format":"uuid"},"created_at":{"type":"string","format":"date-time"}},"required":["id","name","url","workspace","folder_id","visibility","latest_version","created_by","created_at"]},"WebhookVersion":{"type":"object","properties":{"number":{"type":"integer","format":"int32"},"description":{"type":["string","null"]},"url":{"type":"string","format":"uri","description":"The version in the viewer"},"created_by":{"type":["string","null"],"format":"uuid"},"created_at":{"type":"string","format":"date-time"}},"required":["number","description","url","created_by","created_at"]},"ProjectEvent":{"type":"object","properties":{"type":{"type":"string","enum":["project.created","project.updated","project.deleted","project.restored"]},"timestamp":{"type":"string","format":"date-time"},"data":{"type":"object","properties":{"project":{"$ref":"#/components/schemas/WebhookProject"},"actor":{"$ref":"#/components/schemas/WebhookActor"},"changes":{"type":"object","description":"On `project.updated`, the previous value of each field that changed.","additionalProperties":false,"properties":{"name":{"type":"object","properties":{"from":{"type":"string"}},"required":["from"]},"workspace":{"type":"object","properties":{"from":{"$ref":"#/components/schemas/WebhookWorkspace"}},"required":["from"]},"folder_id":{"type":"object","properties":{"from":{"type":["string","null"],"format":"uuid"}},"required":["from"]},"visibility":{"type":"object","properties":{"from":{"type":"string","enum":["PRIVATE","INTERNAL","PUBLIC"]}},"required":["from"]}}}},"required":["project","actor"]}},"required":["type","timestamp","data"]},"VersionEvent":{"type":"object","properties":{"type":{"type":"string","enum":["version.created","version.uploaded","version.updated","version.deleted","version.restored"]},"timestamp":{"type":"string","format":"date-time"},"data":{"type":"object","properties":{"project":{"$ref":"#/components/schemas/WebhookProject"},"version":{"$ref":"#/components/schemas/WebhookVersion"},"actor":{"$ref":"#/components/schemas/WebhookActor"},"changes":{"type":"object","description":"On `version.updated`, the previous value of each field that changed.","additionalProperties":false,"properties":{"description":{"type":"object","properties":{"from":{"type":["string","null"]}},"required":["from"]}}}},"required":["project","version","actor"]}},"required":["type","timestamp","data"]},"WebhookPerson":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"name":{"type":["string","null"]},"email":{"type":"string"}},"required":["id","name","email"]},"WebhookComment":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"in_reply_to":{"type":["string","null"],"format":"uuid","description":"The comment this replies to. Null on a comment."},"body":{"type":"string","description":"The text as written, mentions included in their stored form."},"author":{"$ref":"#/components/schemas/WebhookPerson"},"labels":{"type":"array","items":{"type":"string"},"description":"Always empty on a reply."},"resolved":{"type":["boolean","null"],"description":"Null on a reply."},"file_path":{"type":["string","null"],"description":"The file in the version it is about, for a comment on one file."},"created_at":{"type":"string","format":"date-time"},"edited_at":{"type":"string","format":"date-time","description":"When its text was last edited, or its `created_at` if it never was."}},"required":["id","in_reply_to","body","author","labels","resolved","file_path","created_at","edited_at"]},"WebhookApproval":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"number":{"type":"integer","format":"int32","description":"Counts the requests on its version, from 1."},"status":{"type":"string","enum":["pending","in_review","approved","changes_requested","cancelled"],"description":"The status after the change."},"reviewer":{"$ref":"#/components/schemas/WebhookPerson"},"requested_by":{"$ref":"#/components/schemas/WebhookPerson"},"created_at":{"type":"string","format":"date-time"}},"required":["id","number","status","reviewer","requested_by","created_at"]},"CommentEvent":{"type":"object","properties":{"type":{"type":"string","enum":["comment.created","comment.updated","comment.deleted","comment.resolved","comment.unresolved"]},"timestamp":{"type":"string","format":"date-time"},"data":{"type":"object","properties":{"project":{"$ref":"#/components/schemas/WebhookProject"},"version":{"$ref":"#/components/schemas/WebhookVersion"},"comment":{"$ref":"#/components/schemas/WebhookComment"},"actor":{"$ref":"#/components/schemas/WebhookActor"},"changes":{"type":"object","description":"On `comment.updated`, the previous value of each field that changed.","additionalProperties":false,"properties":{"body":{"type":"object","properties":{"from":{"type":"string"}},"required":["from"]},"labels":{"type":"object","properties":{"from":{"type":"array","items":{"type":"string"}}},"required":["from"]}}}},"required":["project","version","comment","actor"]}},"required":["type","timestamp","data"]},"ApprovalRequestEvent":{"type":"object","properties":{"type":{"type":"string","enum":["approval.requested","approval.review_started","approval.approved","approval.changes_requested","approval.review_cancelled","approval.reopened","approval.cancelled"]},"timestamp":{"type":"string","format":"date-time"},"data":{"type":"object","properties":{"project":{"$ref":"#/components/schemas/WebhookProject"},"version":{"$ref":"#/components/schemas/WebhookVersion"},"approval":{"$ref":"#/components/schemas/WebhookApproval"},"actor":{"$ref":"#/components/schemas/WebhookActor"}},"required":["project","version","approval","actor"]}},"required":["type","timestamp","data"]},"WebhookWorkspaceDetails":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"name":{"type":"string"},"description":{"type":["string","null"]},"visibility":{"type":"string","enum":["PRIVATE","INTERNAL","PUBLIC"]},"created_by":{"type":["string","null"],"format":"uuid"},"created_at":{"type":"string","format":"date-time"}},"required":["id","name","description","visibility","created_by","created_at"]},"WebhookFolder":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"name":{"type":"string"},"colour":{"type":["string","null"]},"parent_id":{"type":["string","null"],"format":"uuid","description":"The folder it sits in. Null at the top of its workspace."},"workspace":{"$ref":"#/components/schemas/WebhookWorkspace"},"created_at":{"type":"string","format":"date-time"}},"required":["id","name","colour","parent_id","workspace","created_at"]},"WebhookShare":{"type":"object","properties":{"id":{"type":"string","format":"uuid","description":"Opens the share for anyone who has it, unless the share is private."},"project":{"$ref":"#/components/schemas/WebhookProject"},"name":{"type":"string"},"versions":{"type":"array","items":{"type":"integer","format":"int32"}},"private":{"type":"boolean","description":"Only the people it was sent to can open it."},"allow_team_members":{"type":"boolean"},"show_comments":{"type":"boolean"},"allow_commenting":{"type":"boolean"},"created_by":{"type":"string","format":"uuid"},"created_at":{"type":"string","format":"date-time"}},"required":["id","project","name","versions","private","allow_team_members","show_comments","allow_commenting","created_by","created_at"]},"FolderEvent":{"type":"object","properties":{"type":{"type":"string","enum":["folder.created","folder.updated","folder.deleted","folder.restored"]},"timestamp":{"type":"string","format":"date-time"},"data":{"type":"object","properties":{"folder":{"$ref":"#/components/schemas/WebhookFolder"},"actor":{"$ref":"#/components/schemas/WebhookActor"},"changes":{"type":"object","description":"On an update, the previous value of each field that changed.","additionalProperties":false,"properties":{"name":{"type":"object","properties":{"from":{"type":"string"}},"required":["from"]},"colour":{"type":"object","properties":{"from":{"type":["string","null"]}},"required":["from"]},"parent_id":{"type":"object","properties":{"from":{"type":["string","null"],"format":"uuid"}},"required":["from"]},"workspace":{"type":"object","properties":{"from":{"$ref":"#/components/schemas/WebhookWorkspace"}},"required":["from"]}}}},"required":["folder","actor"]}},"required":["type","timestamp","data"]},"WorkspaceEvent":{"type":"object","properties":{"type":{"type":"string","enum":["workspace.created","workspace.updated","workspace.deleted","workspace.restored"]},"timestamp":{"type":"string","format":"date-time"},"data":{"type":"object","properties":{"workspace":{"$ref":"#/components/schemas/WebhookWorkspaceDetails"},"actor":{"$ref":"#/components/schemas/WebhookActor"},"changes":{"type":"object","description":"On an update, the previous value of each field that changed.","additionalProperties":false,"properties":{"name":{"type":"object","properties":{"from":{"type":"string"}},"required":["from"]},"description":{"type":"object","properties":{"from":{"type":["string","null"]}},"required":["from"]},"visibility":{"type":"object","properties":{"from":{"type":"string","enum":["PRIVATE","INTERNAL","PUBLIC"]}},"required":["from"]},"ai_images_enabled":{"type":"object","properties":{"from":{"type":"boolean"}},"required":["from"]},"default_bg_colour":{"type":"object","properties":{"from":{"type":["string","null"]}},"required":["from"]},"embed_allowed_origins":{"type":"object","properties":{"from":{"type":"array","items":{"type":"string"}}},"required":["from"]}}}},"required":["workspace","actor"]}},"required":["type","timestamp","data"]},"WorkspaceMemberEvent":{"type":"object","properties":{"type":{"type":"string","enum":["workspace.member_added","workspace.member_removed","workspace.member_role_changed"]},"timestamp":{"type":"string","format":"date-time"},"data":{"type":"object","properties":{"workspace":{"$ref":"#/components/schemas/WebhookWorkspaceDetails"},"member":{"type":"object","properties":{"person":{"$ref":"#/components/schemas/WebhookPerson"},"role":{"type":"string","description":"Their role in the workspace. On `workspace.member_removed`, the role they had."}},"required":["person","role"]},"actor":{"$ref":"#/components/schemas/WebhookActor"},"changes":{"type":"object","description":"On an update, the previous value of each field that changed.","additionalProperties":false,"properties":{"role":{"type":"object","properties":{"from":{"type":"string"}},"required":["from"]}}}},"required":["workspace","member","actor"]}},"required":["type","timestamp","data"]},"ShareEvent":{"type":"object","properties":{"type":{"type":"string","enum":["share.created","share.updated","share.deleted"]},"timestamp":{"type":"string","format":"date-time"},"data":{"type":"object","properties":{"share":{"$ref":"#/components/schemas/WebhookShare"},"actor":{"$ref":"#/components/schemas/WebhookActor"},"changes":{"type":"object","description":"On an update, the previous value of each field that changed.","additionalProperties":false,"properties":{"name":{"type":"object","properties":{"from":{"type":"string"}},"required":["from"]},"versions":{"type":"object","properties":{"from":{"type":"array","items":{"type":"integer","format":"int32"}}},"required":["from"]},"private":{"type":"object","properties":{"from":{"type":"boolean"}},"required":["from"]},"allow_team_members":{"type":"object","properties":{"from":{"type":"boolean"}},"required":["from"]},"show_comments":{"type":"object","properties":{"from":{"type":"boolean"}},"required":["from"]},"allow_commenting":{"type":"object","properties":{"from":{"type":"boolean"}},"required":["from"]}}}},"required":["share","actor"]}},"required":["type","timestamp","data"]},"WebhookStudio":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"name":{"type":"string"}},"required":["id","name"]},"WebhookInvitation":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"email":{"type":"string","description":"Who was invited"},"target":{"type":"object","description":"Where it invites them. A share's `id` opens it for anyone who has it unless the share is private, so treat it as a secret.","properties":{"type":{"type":"string","enum":["studio","workspace","share"]},"id":{"type":["string","null"],"format":"uuid"}},"required":["type","id"]},"role":{"type":"string"},"status":{"type":"string","enum":["pending","accepted","declined","cancelled"],"description":"The status after the change."},"invited_by":{"oneOf":[{"$ref":"#/components/schemas/WebhookPerson"},{"type":"null"}]},"created_at":{"type":"string","format":"date-time"},"expires_at":{"type":["string","null"],"format":"date-time"}},"required":["id","email","target","role","status","invited_by","created_at","expires_at"]},"StudioMemberEvent":{"type":"object","properties":{"type":{"type":"string","enum":["studio.member_added","studio.member_removed","studio.member_role_changed"]},"timestamp":{"type":"string","format":"date-time"},"data":{"type":"object","properties":{"studio":{"$ref":"#/components/schemas/WebhookStudio"},"member":{"type":"object","properties":{"person":{"$ref":"#/components/schemas/WebhookPerson"},"role":{"type":"string","description":"Their role in the studio. On `studio.member_removed`, the role they had."}},"required":["person","role"]},"actor":{"$ref":"#/components/schemas/WebhookActor"},"changes":{"type":"object","additionalProperties":false,"properties":{"role":{"type":"object","properties":{"from":{"type":"string"}},"required":["from"]}}}},"required":["studio","member","actor"]}},"required":["type","timestamp","data"]},"InvitationEvent":{"type":"object","properties":{"type":{"type":"string","enum":["invitation.created","invitation.accepted","invitation.declined","invitation.cancelled"]},"timestamp":{"type":"string","format":"date-time"},"data":{"type":"object","properties":{"invitation":{"$ref":"#/components/schemas/WebhookInvitation"},"actor":{"$ref":"#/components/schemas/WebhookActor"}},"required":["invitation","actor"]}},"required":["type","timestamp","data"]},"WebhookGeneration":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"name":{"type":"string"},"kind":{"type":"string","description":"What it makes, such as `image` or `video`."},"round_id":{"type":["string","null"],"format":"uuid","description":"The takes asked for together share it."},"width":{"type":["integer","null"],"format":"int32"},"height":{"type":["integer","null"],"format":"int32"},"duration_seconds":{"type":["number","null"]},"created_by":{"type":"string","format":"uuid"},"created_at":{"type":"string","format":"date-time"}},"required":["id","name","kind","round_id","width","height","duration_seconds","created_by","created_at"]},"WebhookConfigurator":{"type":"object","properties":{"id":{"type":"string","format":"uuid","description":"The draft's or the release's ID"},"family_id":{"type":"string","format":"uuid","description":"Shared by a configurator's draft and all of its releases"},"name":{"type":["string","null"]},"title":{"type":["string","null"]},"status":{"type":"string","description":"`draft` or `published`."},"version_number":{"type":["integer","null"],"format":"int32","description":"The release's number. Null on the draft."},"url":{"type":"string","format":"uri"},"workspace":{"$ref":"#/components/schemas/WebhookWorkspace"},"published_at":{"type":["string","null"],"format":"date-time"},"created_by":{"type":"string","format":"uuid"},"created_at":{"type":"string","format":"date-time"}},"required":["id","family_id","name","title","status","version_number","url","workspace","published_at","created_by","created_at"]},"GenerationEvent":{"type":"object","properties":{"type":{"type":"string","enum":["generation.completed","generation.failed","generation.kept","generation.discarded"]},"timestamp":{"type":"string","format":"date-time"},"data":{"type":"object","properties":{"project":{"$ref":"#/components/schemas/WebhookProject"},"version":{"$ref":"#/components/schemas/WebhookVersion"},"generation":{"$ref":"#/components/schemas/WebhookGeneration"},"actor":{"$ref":"#/components/schemas/WebhookActor"}},"required":["project","version","generation","actor"]}},"required":["type","timestamp","data"]},"ConfiguratorEvent":{"type":"object","properties":{"type":{"type":"string","enum":["configurator.created","configurator.published","configurator.deleted"]},"timestamp":{"type":"string","format":"date-time"},"data":{"type":"object","properties":{"configurator":{"$ref":"#/components/schemas/WebhookConfigurator"},"actor":{"$ref":"#/components/schemas/WebhookActor"}},"required":["configurator","actor"]}},"required":["type","timestamp","data"]},"ConfiguratorDeploymentEvent":{"type":"object","properties":{"type":{"type":"string","enum":["configurator.deployed","configurator.undeployed"]},"timestamp":{"type":"string","format":"date-time"},"data":{"type":"object","properties":{"family_id":{"type":"string","format":"uuid"},"releases":{"type":"array","items":{"type":"object","properties":{"configurator":{"$ref":"#/components/schemas/WebhookConfigurator"},"weight":{"type":"integer","format":"int32","description":"Its share of visitors, out of 100"}},"required":["configurator","weight"]}},"actor":{"$ref":"#/components/schemas/WebhookActor"}},"required":["family_id","releases","actor"]}},"required":["type","timestamp","data"]},"VersionProcessedEvent":{"type":"object","properties":{"type":{"type":"string","enum":["version.processed"]},"timestamp":{"type":"string","format":"date-time"},"data":{"type":"object","properties":{"project":{"$ref":"#/components/schemas/WebhookProject"},"version":{"$ref":"#/components/schemas/WebhookVersion"},"processing":{"type":"object","properties":{"status":{"type":"string","enum":["complete","failed"],"description":"`failed` while any of the version's files couldn't be processed."},"total":{"type":"integer","format":"int32","description":"The version's processing jobs: one for each size made of each file, and a few for the version as a whole."},"failed":{"type":"integer","format":"int32","description":"The jobs among them that couldn't be done."},"ended_at":{"type":"string","format":"date-time"}},"required":["status","total","failed","ended_at"]},"actor":{"$ref":"#/components/schemas/WebhookActor"}},"required":["project","version","processing","actor"]}},"required":["type","timestamp","data"]},"CommentLabelsRenamedEvent":{"type":"object","properties":{"type":{"type":"string","enum":["comment.labels_renamed"]},"timestamp":{"type":"string","format":"date-time"},"data":{"type":"object","properties":{"project":{"$ref":"#/components/schemas/WebhookProject"},"renames":{"type":"array","items":{"type":"object","properties":{"from":{"type":"string"},"to":{"type":"string"},"comments":{"type":"integer","format":"int32","description":"The comments that had the old label"}},"required":["from","to","comments"]}},"actor":{"$ref":"#/components/schemas/WebhookActor"}},"required":["project","renames","actor"]}},"required":["type","timestamp","data"]},"ConfiguratorEnquirySubmittedEvent":{"type":"object","properties":{"type":{"type":"string","const":"configurator.enquiry.submitted"},"timestamp":{"type":"string","format":"date-time"},"data":{"type":"object","properties":{"id":{"type":"string","format":"uuid","description":"The enquiry ID"},"submitted_at":{"type":"string","format":"date-time"},"configurator":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"name":{"type":"string"},"url":{"type":"string","format":"uri","description":"The configurator, opened on the visitor's options"}},"required":["id","name","url"]},"fields":{"type":"array","items":{"type":"object","properties":{"key":{"type":"string"},"label":{"type":"string"},"type":{"type":"string"},"value":{"type":"string"}},"required":["key","label","type","value"]}},"selected_options":{"type":"array","items":{"type":"object","properties":{"code":{"type":"string"},"name":{"type":"string"}},"required":["code","name"]}}},"required":["id","submitted_at","configurator","fields","selected_options"]}},"required":["type","timestamp","data"]},"AuditExport":{"type":"object","description":"One request for a range of a studio's audit log as a single gzipped NDJSON file of OCSF events.","properties":{"id":{"type":"string","format":"uuid"},"status":{"type":"string","enum":["pending","running","ready","failed","expired"],"description":"`pending` until a worker picks it up and `running` while the file is being built; `ready` once it can be downloaded; `failed` if it could not be built (see `error`); `expired` once the file has been deleted, 7 days after it was ready."},"from":{"type":"string","format":"date-time"},"to":{"type":"string","format":"date-time","description":"End of the range, exclusive."},"include_reads":{"type":"boolean"},"requested_by":{"type":"string","format":"uuid"},"created_at":{"type":"string","format":"date-time"},"finished_at":{"type":"string","format":"date-time","description":"When it became `ready` or `failed`."},"expires_at":{"type":"string","format":"date-time","description":"When the file will be deleted. Set once `ready`."},"size":{"type":"integer","format":"int64","description":"The file's size in bytes. Set once `ready`."},"events":{"type":"integer","format":"int32","description":"How many events the file holds. Set once `ready`."},"error":{"type":"string","description":"Why it `failed`."},"url":{"type":"string","format":"uri","description":"Presigned download URL, valid for 15 minutes. Only while `ready`."}},"required":["id","status","from","to","include_reads","requested_by","created_at"]},"AuditEvent":{"type":"object","description":"An OCSF event, as written to the audit log files. API Activity (class_uid 6003) is the only class today. See https://schema.ocsf.io for the full schema of the release named in `metadata.version`.","additionalProperties":true,"properties":{"class_uid":{"type":"integer","format":"int32"},"time":{"type":"integer","format":"int64","description":"When the request finished, in milliseconds since the epoch."},"metadata":{"type":"object","additionalProperties":true,"properties":{"uid":{"type":"string"},"version":{"type":"string"}},"required":["uid","version"]}},"required":["class_uid","time","metadata"]},"ContentHash":{"description":"Hash of the file's content, used as one segment of its storage key","type":"string","pattern":"^[A-Za-z0-9_-]{1,64}$","example":"3f2a9c1b7d4e"},"ArchiveListItem":{"type":"object","description":"A produced archive with its project context, for the Downloads list.","properties":{"id":{"type":"string","format":"uuid"},"project_id":{"type":"string","format":"uuid"},"project_name":{"type":"string"},"version":{"type":"integer","format":"int32"},"kind":{"type":"string"},"prefix_hash":{"type":"string","nullable":true,"description":"Prefix-subset hash; null for a whole-version archive."},"size_bytes":{"type":"integer","format":"int64","nullable":true},"created_at":{"type":"string","format":"date-time"}},"required":["id","project_id","project_name","version","kind","created_at"]},"Artifact":{"type":"object","description":"A durable produced-file record (archives today; exports/renders later).","properties":{"id":{"type":"string","format":"uuid"},"kind":{"type":"string","description":"archive | export | render"},"prefix_hash":{"type":"string","nullable":true,"description":"Prefix-subset hash; null for a whole-version archive."},"size_bytes":{"type":"integer","format":"int64","nullable":true},"status":{"type":"string"},"created_at":{"type":"string","format":"date-time"},"expires_at":{"type":"string","format":"date-time","nullable":true}},"required":["id","kind","status","created_at"]},"OpenApiDocument":{"type":"object","description":"An OpenAPI 3.1 document, passed through verbatim. Hand it to an OpenAPI parser; its shape is the OpenAPI meta-schema's.","additionalProperties":true},"OAuthAuthorizationServerMetadata":{"type":"object","description":"OAuth 2.0 Authorization Server Metadata as defined by RFC 8414,\nextended with OpenID Connect Discovery 1.0 fields when `openid` is\namong the supported scopes.\n","required":["issuer","authorization_endpoint","token_endpoint","response_types_supported"],"properties":{"issuer":{"type":"string","format":"uri","example":"https://renderapp.io/api/auth"},"authorization_endpoint":{"type":"string","format":"uri","example":"https://renderapp.io/api/auth/oauth2/authorize"},"token_endpoint":{"type":"string","format":"uri","example":"https://renderapp.io/api/auth/oauth2/token"},"jwks_uri":{"type":"string","format":"uri","example":"https://renderapp.io/api/auth/jwks"},"introspection_endpoint":{"type":"string","format":"uri"},"revocation_endpoint":{"type":"string","format":"uri"},"registration_endpoint":{"type":"string","format":"uri","description":"Present when dynamic client registration is enabled."},"scopes_supported":{"type":"array","items":{"type":"string"}},"response_types_supported":{"type":"array","items":{"type":"string"},"example":["code"]},"response_modes_supported":{"type":"array","items":{"type":"string"},"example":["query"]},"grant_types_supported":{"type":"array","items":{"type":"string"},"example":["authorization_code","client_credentials","refresh_token"]},"token_endpoint_auth_methods_supported":{"type":"array","items":{"type":"string"},"example":["client_secret_basic","client_secret_post"]},"introspection_endpoint_auth_methods_supported":{"type":"array","items":{"type":"string"}},"revocation_endpoint_auth_methods_supported":{"type":"array","items":{"type":"string"}},"code_challenge_methods_supported":{"type":"array","items":{"type":"string"},"example":["S256"]},"authorization_response_iss_parameter_supported":{"type":"boolean"},"userinfo_endpoint":{"description":"OIDC — present when `openid` scope is supported.","type":"string","format":"uri"},"end_session_endpoint":{"description":"OIDC — RP-initiated logout endpoint.","type":"string","format":"uri"},"subject_types_supported":{"description":"OIDC — subject identifier types the server supports.","type":"array","items":{"type":"string"},"example":["public"]},"id_token_signing_alg_values_supported":{"description":"OIDC — JWS algorithms used to sign ID tokens.","type":"array","items":{"type":"string"},"example":["EdDSA"]},"claims_supported":{"description":"OIDC — claim names returnable in ID tokens / userinfo.","type":"array","items":{"type":"string"}},"acr_values_supported":{"description":"OIDC — Authentication Context Class Reference values.","type":"array","items":{"type":"string"}},"prompt_values_supported":{"description":"OIDC — values accepted for the `prompt` parameter.","type":"array","items":{"type":"string"},"example":["login","consent","create","select_account","none"]}}},"OAuthProtectedResourceMetadata":{"type":"object","description":"OAuth 2.0 Protected Resource Metadata as defined by RFC 9728.","required":["resource","authorization_servers"],"properties":{"resource":{"type":"string","format":"uri","description":"The resource identifier a client passes as `resource` and tokens carry as `aud`.","example":"https://renderapp.io/api/v1/mcp"},"authorization_servers":{"type":"array","items":{"type":"string","format":"uri"},"example":["https://renderapp.io/api/auth"]},"scopes_supported":{"type":"array","items":{"type":"string"},"example":["projects:read","projects:write","configurators:read","configurators:write","workspaces:read"]},"bearer_methods_supported":{"type":"array","items":{"type":"string"},"example":["header"]},"resource_name":{"type":"string","example":"Renderapp"}}},"Folder":{"type":"object","required":["id","name","colour","editedat","has_children","projectsize"],"properties":{"id":{"type":"string","format":"uuid"},"name":{"type":"string","example":"Project Assets"},"colour":{"type":["string","null"],"example":"#FF5733"},"editedat":{"type":"string","format":"date-time"},"has_children":{"description":"Whether this folder contains other folders","type":"boolean"},"projectsize":{"description":"Summed size in bytes of every project beneath this folder, at any depth","type":"number","format":"double"}}},"Attachment":{"type":"object","description":"File attachment for a comment","properties":{"originalname":{"description":"Original filename","type":"string","example":"screenshot.jpg"},"key":{"description":"S3 key for the attachment","type":"string","example":"Attachments/1618324567-screenshot.jpg"}}},"CommentRegion":{"type":"object","description":"Region annotation on a comment (drawing, text, inline image, etc.)","properties":{"T":{"description":"Region type (region, arrow, freehand, text, inline)","type":"string","example":"text"},"K":{"description":"Key reference for inline images","type":"string"},"CL":{"description":"Class/style properties","type":"object","additionalProperties":{"type":"string"}},"X":{"description":"X coordinate","type":"string"},"Y":{"description":"Y coordinate","type":"string"},"W":{"description":"Width","type":"string"},"H":{"description":"Height","type":"string"},"D":{"description":"SVG path data for freehand drawings","type":"string"},"TC":{"description":"Text content","type":"string"},"FS":{"description":"Font size","type":"string"},"AX":{"description":"Arrow start X","type":"string"},"AY":{"description":"Arrow start Y","type":"string"},"BX":{"description":"Arrow end X","type":"string"},"BY":{"description":"Arrow end Y","type":"string"},"R":{"description":"Rotation","type":"string"}}},"CommentCompareSettings":{"type":"object","description":"Compare-mode context captured with the comment. Absent means the comment was created with compare mode off.","properties":{"displayMode":{"description":"Compare display layout.","type":"string","enum":["toggle","side-by-side","delta","swipe"]},"version":{"description":"Version number of the compare-side file","type":"integer","format":"int32"},"selectedOptions":{"description":"Selected option codes for the compare side (configurator chain)","type":"array","items":{"type":"string"}}},"required":["displayMode","version","selectedOptions"]},"CommentSettings":{"type":"object","description":"Comment display settings including regions/annotations","properties":{"regions":{"description":"List of region annotations","type":"array","items":{"$ref":"#/components/schemas/CommentRegion"}},"marker":{"description":"Video marker settings","type":"object","nullable":true,"properties":{"time":{"description":"Marker time in seconds","type":"number","format":"double"},"title":{"description":"Marker title","type":"string","nullable":true}}},"camera":{"description":"Camera state when comment was created (pano or 3D model)","type":"object","nullable":true,"properties":{"lat":{"description":"Panoramic camera latitude","type":"number","format":"double"},"lon":{"description":"Panoramic camera longitude","type":"number","format":"double"},"fov":{"description":"Panoramic camera field of view","type":"number","format":"double"},"position":{"description":"3D model camera position","type":"object","properties":{"x":{"type":"number","format":"double"},"y":{"type":"number","format":"double"},"z":{"type":"number","format":"double"}},"required":["x","y","z"]},"target":{"description":"3D model camera target","type":"object","properties":{"x":{"type":"number","format":"double"},"y":{"type":"number","format":"double"},"z":{"type":"number","format":"double"}},"required":["x","y","z"]}}},"anchor":{"$ref":"#/components/schemas/ModelAnchor"},"view":{"$ref":"#/components/schemas/ModelViewSettings"},"text":{"$ref":"#/components/schemas/TextAnchor"},"compare":{"$ref":"#/components/schemas/CommentCompareSettings"}}},"TextAnchor":{"type":"object","description":"The lines of a text file, or the rows of a CSV or TSV, that a comment is about. Numbered from 1, inclusive.\n","required":["unit","start","end"],"properties":{"unit":{"type":"string","enum":["line","row"],"example":"line"},"start":{"type":"integer","format":"int32","minimum":1,"example":12},"end":{"type":"integer","format":"int32","minimum":1,"example":14},"quote":{"description":"The start of the anchored text, as it read when the comment was made","type":"string","maxLength":500,"example":"max_speed: 180"}}},"TextCommentSettings":{"type":"object","description":"Settings for text player comments","required":["text"],"properties":{"text":{"$ref":"#/components/schemas/TextAnchor"},"regions":{"description":"Always empty; text comments are anchored to lines, not drawn regions","type":"array","items":{"$ref":"#/components/schemas/CommentRegion"}},"compare":{"$ref":"#/components/schemas/CommentCompareSettings"}}},"ModelViewSettings":{"type":"object","description":"How the commenter was looking at the model, as against how the version is presented: shading, overlays, what they had isolated or hidden. Session state, restored when the comment is selected so the reply sees what the comment is about. Every field is optional — a comment written before this existed carries none of them, and reads as the defaults.\n","nullable":true,"properties":{"shading_mode":{"description":"Inspector shading mode in force when the comment was made","type":"string","enum":["shaded","albedo","wireframe","normals","uv","matcap","metalness","roughness","ao"]},"wireframe_overlay":{"description":"Whether the wireframe was composited over the shaded model","type":"boolean"},"isolated_node":{"description":"Node shown on its own, everything else hidden","type":"string","nullable":true},"hidden_nodes":{"description":"Nodes the commenter had hidden","type":"array","items":{"type":"string"}}}},"VideoCommentSettings":{"type":"object","description":"Settings for video player comments","required":["marker"],"properties":{"regions":{"description":"List of region annotations","type":"array","items":{"$ref":"#/components/schemas/CommentRegion"}},"marker":{"description":"Video marker. Every video comment is pinned to a timestamp — general (untimestamped) comments are not supported.\n","type":"object","required":["time"],"properties":{"time":{"description":"Marker time in seconds","type":"number","format":"double","minimum":0},"title":{"description":"Marker title","type":"string","nullable":true}}},"compare":{"$ref":"#/components/schemas/CommentCompareSettings"}}},"PanoCommentSettings":{"type":"object","description":"Settings for panoramic player comments","properties":{"regions":{"description":"List of region annotations","type":"array","items":{"$ref":"#/components/schemas/CommentRegion"}},"camera":{"description":"Panoramic camera state","type":"object","nullable":true,"properties":{"lat":{"description":"Camera latitude","type":"number","format":"double"},"lon":{"description":"Camera longitude","type":"number","format":"double"},"fov":{"description":"Field of view","type":"number","format":"double"}},"required":["lat","lon","fov"]},"compare":{"$ref":"#/components/schemas/CommentCompareSettings"}}},"ModelAnchor":{"type":"object","description":"Point on the model a comment is pinned to, in the scene's world space, with the node it was struck on. The node travels with the point because a model gets reconfigured underneath a comment — once a part is hidden, swapped for a variant or isolated away, a bare point hangs in mid-air with nothing to say what it was pointing at.","properties":{"point":{"description":"Where the ray met the model","type":"object","properties":{"x":{"type":"number","format":"double"},"y":{"type":"number","format":"double"},"z":{"type":"number","format":"double"}},"required":["x","y","z"]},"node":{"description":"Name of the node the point was struck on","type":"string"}},"required":["point","node"]},"ModelCommentSettings":{"type":"object","description":"Settings for 3D model player comments","properties":{"regions":{"description":"List of region annotations","type":"array","items":{"$ref":"#/components/schemas/CommentRegion"}},"camera":{"description":"3D camera state","type":"object","nullable":true,"properties":{"position":{"description":"Camera position","type":"object","properties":{"x":{"type":"number","format":"double"},"y":{"type":"number","format":"double"},"z":{"type":"number","format":"double"}},"required":["x","y","z"]},"target":{"description":"Camera target","type":"object","properties":{"x":{"type":"number","format":"double"},"y":{"type":"number","format":"double"},"z":{"type":"number","format":"double"}},"required":["x","y","z"]}},"required":["position","target"]},"anchor":{"$ref":"#/components/schemas/ModelAnchor"},"view":{"$ref":"#/components/schemas/ModelViewSettings"},"compare":{"$ref":"#/components/schemas/CommentCompareSettings"}}},"ImageCommentSettings":{"type":"object","description":"Settings for image player comments (still, stills, animation)","properties":{"regions":{"description":"List of region annotations","type":"array","items":{"$ref":"#/components/schemas/CommentRegion"}},"compare":{"$ref":"#/components/schemas/CommentCompareSettings"}}},"Error":{"type":"object","properties":{"error":{"description":"Error message","type":"string","example":"Missing required parameters"}}},"Problem":{"type":"object","description":"RFC 9457 Problem Details for HTTP APIs","properties":{"type":{"type":"string","format":"uri","description":"URI reference identifying the problem type","default":"about:blank"},"title":{"type":"string","description":"Short human-readable summary of the problem"},"status":{"type":"integer","format":"int32","description":"HTTP status code"},"detail":{"type":"string","description":"Human-readable explanation specific to this occurrence"},"instance":{"type":"string","format":"uri","description":"URI reference identifying the specific occurrence"},"state":{"description":"Where the resource stands, when the request was refused because of it","type":"string"},"actions":{"description":"What the caller can do from that state, when the problem names one","type":"array","items":{"type":"string"}},"errors":{"description":"Each part of the request that failed validation","type":"array","items":{"type":"object","properties":{"path":{"description":"Where the failure is, e.g. /body/rio/Inputs/0/Options/2/Type","type":"string"},"message":{"type":"string"}},"required":["path","message"]}}},"required":["title","status"]},"SharedLayout":{"type":"object","properties":{"id":{"description":"The source configurator's family id","type":"string","format":"uuid"},"name":{"type":"string"},"theme":{"type":"string"},"source_configurator_id":{"description":"The source's draft, where the layout is edited","type":"string","format":"uuid"},"source_name":{"type":"string"},"used_by":{"description":"How many configurators are linked to the layout","type":"integer","format":"int32"},"created_at":{"type":"string","format":"date-time"}},"required":["id","name","theme","source_configurator_id","used_by","created_at"]},"SharedLayoutDetail":{"allOf":[{"$ref":"#/components/schemas/SharedLayout"},{"type":"object","properties":{"source_live":{"description":"Whether the source has a live release","type":"boolean"},"dependents":{"description":"The draft configurators using the layout","type":"array","items":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"name":{"type":"string"},"live":{"description":"Whether the configurator has a live release","type":"boolean"}},"required":["id","live"]}}},"required":["source_live","dependents"]}]},"SharedLayoutDetached":{"type":"object","properties":{"detached":{"description":"How many configurators were detached","type":"integer","format":"int32"}},"required":["detached"]},"LayoutTemplateSummary":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"name":{"type":"string"},"theme":{"type":"string"},"created_at":{"type":"string","format":"date-time"}},"required":["id","name","theme","created_at"]},"AnalyticsLevel":{"description":"How much visitor activity a live configurator reports. `off` reports nothing; each level above it adds events to the one below.\n","type":"string","enum":["off","basic","standard","detailed"]},"ConfiguratorFamily":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"workspace_id":{"type":"string","format":"uuid"},"current_deployment_id":{"type":"string","format":"uuid","nullable":true},"has_backup":{"description":"Whether a restore left a backup of the draft that can be put back","type":"boolean"},"embed_allowed_origins":{"$ref":"#/components/schemas/EmbedAllowedOrigins"},"embed_extends_workspace":{"type":"boolean"},"workspace_embed_allowed_origins":{"$ref":"#/components/schemas/EmbedAllowedOrigins"},"analytics_level":{"$ref":"#/components/schemas/AnalyticsLevel"},"versions":{"description":"Newest first","type":"array","items":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"version_number":{"type":"integer","format":"int32"},"name":{"type":"string","nullable":true},"published":{"type":"boolean"},"deployed":{"type":"boolean"},"forked_from_id":{"type":"string","format":"uuid","nullable":true},"published_at":{"type":"string","format":"date-time","nullable":true},"created_at":{"type":"string","format":"date-time"}},"required":["id","version_number","name","published","deployed","forked_from_id","published_at","created_at"]}}},"required":["id","workspace_id","current_deployment_id","has_backup","versions"]},"ConfiguratorReviewLink":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"name":{"type":["string","null"]},"url":{"type":"string","format":"uri","description":"Present while the link works. It carries its own credential, so anyone who has it can view the configurator until it expires or is revoked.\n"},"created_by_name":{"type":["string","null"]},"created_at":{"type":"string","format":"date-time"},"expires_at":{"type":"string","format":"date-time"},"revoked_at":{"type":["string","null"],"format":"date-time"},"live":{"description":"Neither revoked nor expired, so the link works","type":"boolean"}},"required":["id","name","created_at","expires_at","revoked_at","live"]},"ConfiguratorPreviewLink":{"type":"object","properties":{"url":{"type":"string","format":"uri","description":"Open this in a browser. It carries its own credential, so treat it like a password until it expires.\n"},"expires_at":{"type":"string","format":"date-time","example":"2026-10-22T12:15:00Z"}},"required":["url","expires_at"]},"ConfiguratorSettings":{"description":"A configurator's settings. `PUT /configurators/{id}` replaces all of them, so read them from `GET /configurators/{id}` and send them back whole.\n","type":"object","nullable":true,"properties":{"primary_project_id":{"description":"ConfiguratorProject entry id shown first. The API sets it when the first project joins a configurator on an automotive theme.\n","type":"string","nullable":true},"secondary_project_id":{"description":"ConfiguratorProject entry id for the second slot, where a theme offers one. No fallback — absent means there is no second slot.\n","type":"string","nullable":true},"notifications_enabled":{"description":"Whether the configurator takes enquiries through the configured `cta_email` or `webhook_url`. Absent means it does whenever either is set.\n","type":"boolean"},"automotive_title":{"description":"The automotive themes' page title; without one it reads \"Configurator\".","type":"string","nullable":true},"automotive_base_price":{"description":"The automotive themes' starting price, which each chosen option's `price` adds to. While unset, no price is shown.\n","type":"number","format":"double","nullable":true},"automotive_subtitle":{"description":"Shown under the automotive themes' title.","type":"string","nullable":true},"automotive_summary_viewpoint":{"type":"object","nullable":true,"description":"The saved viewpoint (from any project in the configurator) that the summary page's final configuration preview is shown from.\n","properties":{"project_entry_id":{"description":"The ConfiguratorProject id the viewpoint belongs to","type":"string","format":"uuid"},"viewpoint_id":{"type":"string","format":"uuid"}},"required":["project_entry_id","viewpoint_id"]},"category_links":{"type":"array","nullable":true,"items":{"type":"object","properties":{"groups":{"type":"array","minItems":2,"items":{"type":"object","properties":{"project_entry_id":{"type":"string","format":"uuid"},"group":{"type":"string","description":"RIO Group identifier e.g. \"+CAT1\""}},"required":["project_entry_id","group"]}},"option_links":{"type":"array","items":{"type":"array","minItems":2,"items":{"type":"string"},"description":"One FrontEndRule per project, in the same order as groups"}}},"required":["groups","option_links"]}},"automotive_categories":{"description":"The automotive themes' sidebar: a section per entry, each with `subcategories` that show one rio category, named by `project_entry_id` and `category_group` (the category's `Group`). Each project that joins the configurator gets a section named after it, with a subcategory per rio category but the view. On an automotive theme the first project to join becomes `primary_project_id` and the second `secondary_project_id`. A project that leaves takes its subcategories and role with it.\n","type":"array","nullable":true,"items":{"type":"object","properties":{"id":{"description":"Stable id that ops address the section by. The server gives one to any section or subcategory written without it.\n","type":"string"},"name":{"type":"string"},"deleted_at":{"type":"string","format":"date-time","nullable":true,"description":"Soft-delete marker. A deleted category and its subcategories are hidden from the live configurator but stay in settings so they can be restored. Deleting a category does not set deleted_at on its subcategories individually — an unset subcategory is hidden by an ancestor's deleted_at and reappears automatically when the category is restored, unless it also carries its own deleted_at.\n"},"linked_viewpoint":{"type":"object","nullable":true,"description":"Links this category to a saved viewpoint (from any project in the configurator). Scrolling onto it in the sidebar swaps the active project (if needed) and camera to match.\n","properties":{"project_entry_id":{"description":"The ConfiguratorProject id the viewpoint belongs to","type":"string","format":"uuid"},"viewpoint_id":{"type":"string","format":"uuid"}},"required":["project_entry_id","viewpoint_id"]},"subcategories":{"type":"array","items":{"type":"object","properties":{"id":{"description":"Stable id that ops address the subcategory by","type":"string"},"name":{"type":"string"},"deleted_at":{"type":"string","format":"date-time","nullable":true,"description":"Soft-delete marker, independent of the parent category's own deleted_at. Restoring the parent category does not clear this.\n"},"project_entry_id":{"type":"string","format":"uuid","nullable":true},"category_group":{"type":"string","nullable":true},"type":{"oneOf":[{"type":"string","enum":["radio","checkbox"]},{"type":"null"}]},"is_dropdown":{"type":"boolean","nullable":true,"description":"Whether this subcategory renders as a collapsible dropdown."},"honda":{"type":"object","nullable":true,"description":"Honda-theme-only extras for this subcategory.","properties":{"view_features":{"type":"boolean","description":"Show a \"View features\" pill beneath this subcategory's rendered options; expands to the performance overview.\n"},"performance_overview_enabled":{"type":"boolean"},"performance_overview":{"type":"array","description":"Key/value pairs shown in the performance overview (e.g. \"Engine Displacement (cc)\" -> \"1084cm3\"). Ignored unless performance_overview_enabled is true.\n","items":{"type":"object","properties":{"key":{"type":"string"},"value":{"type":"string"}},"required":["key","value"]}}}},"options":{"type":"array","nullable":true,"items":{"type":"object","properties":{"id":{"type":"string","description":"frontEndRule for RIO-backed options, UUID for custom options"},"name":{"type":"string"},"price":{"type":"number","format":"double","nullable":true},"colour":{"description":"Shows the option as a swatch of this colour, such as `#1b2a4a`.","type":"string","nullable":true},"finish":{"description":"The look of this option's colour swatch: `gloss` has a hard light line across the middle, `matte` a soft gradient, `chrome` a bright reflection, `none` a flat colour. Absent takes the layout's swatch shine. Swatches made from an image are never shaded. Only the automotive themes show it; hero-generic ignores it.\n","type":"string","enum":["gloss","matte","chrome","none"]},"label":{"description":"What visitors see in place of the rio option's name. `name` is not shown.\n","type":"string","nullable":true},"description":{"type":"string","nullable":true,"description":"Shown directly after the option's title."},"image_key":{"type":"string","nullable":true,"description":"A key from this configurator's option-image-upload-url. PUT refuses any other key with 400, and GET signs no URL for one already stored.\n"},"card_image_key":{"type":"string","nullable":true,"description":"A key from this configurator's option-image-upload-url, under the same rule as `image_key`.\n"},"hidden":{"type":"boolean","nullable":true},"package_selections":{"type":"array","nullable":true,"description":"Makes this a package option: selecting it also selects each of these targets (Rio options in this or other linked projects), applied the same way a direct option click is, including any category_links cascade. Absent or empty for a plain option.\n","items":{"type":"object","properties":{"project_entry_id":{"description":"The ConfiguratorProject id the target option belongs to","type":"string","format":"uuid"},"option_id":{"description":"FrontEndRule of the target option within that project's Rio","type":"string"}},"required":["project_entry_id","option_id"]}},"linked_viewpoint":{"type":"object","nullable":true,"description":"Links this option to a saved viewpoint (from any project in the configurator). Selecting the option swaps the active project (if needed) and animates the camera to match.\n","properties":{"project_entry_id":{"description":"The ConfiguratorProject id the viewpoint belongs to","type":"string","format":"uuid"},"viewpoint_id":{"type":"string","format":"uuid"}},"required":["project_entry_id","viewpoint_id"]},"honda":{"type":"object","nullable":true,"description":"Honda-theme-only extras for this option.","properties":{"choose_colour_next_step":{"type":"boolean","description":"Shows a \"Choose your colour at the next step\" hint (with an overlapping two-colour swatch icon) beneath the option.\n"},"info_enabled":{"type":"boolean"},"info_text":{"type":"string","nullable":true,"description":"Shown in a popover from the info icon beside the option. Ignored unless info_enabled is true.\n"},"pack_contains":{"type":"array","nullable":true,"description":"Plain-text contents list for a package option (e.g. \"Adventure\", \"Sport\", \"Wheel Stripes\"). Only meaningful when this option has package_selections.\n","items":{"type":"string"}}}}},"required":["id","name"]}}},"required":["name"]}}},"required":["name","subcategories"]}}},"additionalProperties":true},"ConfiguratorCtaField":{"type":"object","properties":{"key":{"type":"string"},"label":{"type":"string"},"type":{"type":"string","enum":["text","email","tel","textarea"]},"required":{"type":"boolean"},"default_country":{"description":"For a `tel` field, the ISO 3166-1 alpha-2 country it starts on.","type":"string","pattern":"^[A-Z]{2}$"},"allow_country_change":{"description":"For a `tel` field, whether a visitor may pick another country. False only takes numbers from `default_country`. Absent means they may.\n","type":"boolean"}},"required":["key","label","type","required"]},"Configurator":{"type":"object","properties":{"id":{"description":"Configurator ID","type":"string","format":"uuid"},"seq":{"description":"Counts every change to `settings`, a project's `settings` or the layout. Send it as `If-Match` on a PUT so a stale copy is refused rather than written over newer work.\n","type":"integer","format":"int32"},"workspace_id":{"description":"Workspace this configurator belongs to","type":"string","format":"uuid"},"settings":{"$ref":"#/components/schemas/ConfiguratorSettings"},"analytics_level":{"$ref":"#/components/schemas/AnalyticsLevel"},"created_at":{"description":"Creation timestamp","type":"string"},"logo_url":{"description":"Presigned URL for configurator logo","type":"string","nullable":true},"cta_email":{"description":"Email address for enquiry notifications. Absent unless the caller is a member of the configurator's workspace.","type":"string","format":"email","nullable":true},"cta_fields":{"description":"Form field configuration for CTA form","type":"array","items":{"$ref":"#/components/schemas/ConfiguratorCtaField"},"nullable":true},"title":{"description":"Custom display title","type":"string","nullable":true},"webhook_url":{"description":"Webhook URL for enquiry notifications. Absent unless the caller is a member of the configurator's workspace.","type":"string","nullable":true},"has_enquiry":{"description":"Whether the configurator takes enquiries, by email or webhook, with notifications switched on. Viewers read this rather than `cta_email` or `webhook_url`, which only members of the configurator's workspace are shown.\n","type":"boolean"},"published":{"description":"Whether this version is published. A published version is frozen for good: its layout, content and settings can no longer change.\n","type":"boolean"},"deployed":{"description":"Whether this release is live: served at its family's stable link, as a variant of the family's current deployment.\n","type":"boolean"},"family_id":{"description":"The family this version belongs to. It is also the stable link.","type":"string","format":"uuid"},"version_number":{"description":"Position of this version within its family, starting at 1","type":"integer","format":"int32"},"live_release_number":{"description":"In the configurator list, the number of the release that is live, if any.\n","type":"integer","format":"int32"},"forked_from_id":{"description":"The version this one was forked from","type":"string","format":"uuid","nullable":true},"published_at":{"description":"When this version was published","type":"string","format":"date-time","nullable":true},"theme":{"description":"Visual theme for the configurator layout","type":"string","enum":["hero","hero-generic","automotive","automotive-generic","automotive-honda","embed"],"default":"hero"},"color_scheme":{"description":"Default color scheme for the configurator","type":"string","enum":["system","light","dark"],"default":"system"},"option_image_urls":{"description":"Presigned display URLs for automotive option swatches, keyed \"<project_entry_id|custom>:<option_id>\" and \"card:\" prefixed for the card variant. Resolved across every automotive category, so it is configurator-wide rather than per project.\n","type":"object","nullable":true,"additionalProperties":{"type":"string"}},"layout":{"description":"The layout document, stored apart from `settings`. A configurator saved before the split has none until its layout is next saved, and the client falls back to `settings.theme_settings` until then.\n","type":"object","properties":{"document":{"type":"object","additionalProperties":true},"revision":{"type":"integer","format":"int32"}},"required":["document","revision"]},"shared_layout":{"description":"Set when the configurator is the source of a shared layout or is linked to one. A linked draft's `layout` is the source's layout with its own content text laid over it, and cannot be saved.\n","type":"object","properties":{"id":{"type":"string","format":"uuid"},"name":{"type":"string"},"role":{"type":"string","enum":["source","linked"]},"source_configurator_id":{"type":"string","format":"uuid"},"used_by":{"type":"integer","format":"int32"}},"required":["id","name","role","source_configurator_id","used_by"]},"theme_asset_urls":{"description":"Presigned display URLs for the images a theme settings document refers to, keyed by the `assetId` stored in the document.\n","type":"object","nullable":true,"additionalProperties":{"type":"string"}},"color_scheme_allow_toggle":{"description":"Whether end-users can toggle light/dark mode","type":"boolean","default":true},"name":{"description":"Internal name for the configurator","type":"string","nullable":true},"layout_name":{"description":"Name of the shared layout this configurator owns or is linked to. Absent when it has none. Set on the list only.\n","type":"string"},"layout_role":{"description":"Whether this configurator owns that layout or uses it","type":"string","enum":["source","linked"]},"projects":{"description":"Associated projects with their RIO data and per-project settings","type":"array","items":{"$ref":"#/components/schemas/ConfiguratorProject"}}},"required":["id"]},"ConfiguratorProject":{"type":"object","properties":{"id":{"description":"ConfiguratorProject ID","type":"string","format":"uuid"},"project_id":{"description":"Project ID","type":"string","format":"uuid"},"version":{"description":"Project version","type":"integer","format":"int32"},"projectname":{"description":"Project name","type":"string"},"rio":{"description":"Project RIO data","$ref":"#/components/schemas/Rio"},"settings":{"description":"Per-project content settings","$ref":"#/components/schemas/ConfiguratorProjectSettings"},"option_image_urls":{"description":"Presigned display URLs for custom swatch images","type":"object","nullable":true,"additionalProperties":{"type":"string"}}},"required":["id","project_id","version","projectname","settings","option_image_urls"]},"ConfiguratorProjectSettings":{"description":"How a configurator presents one of its projects. `PUT /configurators/{id}/projects/{project_id}` replaces all of it, `saved_viewpoints`, `saved_scenes` and `focus_points` included, with no revision check, so read it from `GET /configurators/{id}` and send it back whole. Add and remove viewpoints, scenes and focus points through their own operations, not by editing those lists here.\n","type":"object","nullable":true,"properties":{"categories":{"description":"Per-category presentation, keyed by the rio category's Group","type":"object","additionalProperties":{"$ref":"#/components/schemas/CategorySettings"}},"role":{"description":"Role of this project in themes that support primary/secondary layout. Null when no role is set.","oneOf":[{"type":"string","enum":["primary","secondary"]},{"type":"null"}]},"zoom":{"type":"number","format":"double","description":"Zoom multiplier for the image player in the automotive configurator. 1.0 is default, <1 zooms out, >1 zooms in."},"saved_viewpoints":{"type":"array","items":{"$ref":"#/components/schemas/SavedViewpoint"},"description":"Editor-saved camera/frame viewpoints for this project, newest last. Independent of categories and option config."},"saved_scenes":{"type":"array","items":{"$ref":"#/components/schemas/SavedScene"},"description":"Editor-saved environment/background presets for this project, newest last. Independent of categories and option config."},"carousel_mode":{"type":"boolean","description":"When true, frame changes (Stills/Animation player types only) animate as a sliding carousel transition instead of cutting instantly."},"default_viewpoint_id":{"type":"string","format":"uuid","nullable":true,"description":"The saved viewpoint this project opens on. When unset, or when it no longer matches a saved viewpoint, the first one is used.\n"},"lock_viewpoints":{"type":"boolean","description":"When true, visitors cannot spin, orbit, pan or step frames in this project's player; the saved viewpoints are the only way to move. Editors keep full control so they can author viewpoints.\n"},"focus_points":{"type":"array","items":{"$ref":"#/components/schemas/FocusPoint"},"description":"\"Take a closer look\" hotspots on the GLB model — clicking one isolates a part (hides the rest of the model) and animates to its linked viewpoint. Independent of categories and option config, GLB (model) projects only.\n"},"driver_view":{"oneOf":[{"$ref":"#/components/schemas/DriverView"},{"type":"null"}],"description":"Configured driver/rider-view camera for GLB automotive projects — a captured base camera whose look-at target need not be the model's centre (e.g. aimed at a steering wheel), plus how far a visitor may orbit away from it (zoom is disabled entirely). GLB (model) projects only.\n"}}},"FocusPoint":{"type":"object","description":"A clickable marker anchored to a point on a GLB model. Isolates a named scene-graph node (hides every other node) and animates to its own captured camera when clicked. Authored as a three-stage wizard — place the marker point, pick the part from the model's node list, then compose and capture the camera shot — so the camera is this focus point's own capture, not a shared SavedViewpoint.\n","properties":{"id":{"type":"string","format":"uuid"},"name":{"type":"string","description":"Doubles as the visitor-facing title, shown once the focus point's zoom-in settles."},"description":{"type":"string","description":"Visitor-facing copy shown under the title once the focus point's zoom-in settles. Optional — existing focus points predate this field.\n"},"createdat":{"type":"string","format":"date-time"},"target_node":{"type":"string","description":"Name of the scene-graph node to isolate — every mesh outside this node's subtree is hidden while active.\n"},"marker_position":{"type":"object","required":["x","y","z"],"description":"World-space point clicked when placing this focus point; anchors the marker.","properties":{"x":{"type":"number","format":"double"},"y":{"type":"number","format":"double"},"z":{"type":"number","format":"double"}}},"camera":{"type":"object","description":"Camera captured in the wizard's final step, animated to when this focus point is clicked.","properties":{"position":{"type":"object","required":["x","y","z"],"properties":{"x":{"type":"number","format":"double"},"y":{"type":"number","format":"double"},"z":{"type":"number","format":"double"}}},"target":{"type":"object","required":["x","y","z"],"properties":{"x":{"type":"number","format":"double"},"y":{"type":"number","format":"double"},"z":{"type":"number","format":"double"}}},"fov":{"type":"number","format":"double"}},"required":["position","target"]}},"required":["id","name","createdat","target_node","marker_position","camera"]},"FocusPointInput":{"type":"object","description":"Fields a client submits when creating a FocusPoint. `id` and `createdat` are server-generated, not accepted here.\n","properties":{"name":{"type":"string","description":"Doubles as the visitor-facing title, shown once the focus point's zoom-in settles."},"description":{"type":"string","description":"Visitor-facing copy shown under the title once the focus point's zoom-in settles. Optional."},"target_node":{"type":"string"},"marker_position":{"type":"object","required":["x","y","z"],"properties":{"x":{"type":"number","format":"double"},"y":{"type":"number","format":"double"},"z":{"type":"number","format":"double"}}},"camera":{"type":"object","properties":{"position":{"type":"object","required":["x","y","z"],"properties":{"x":{"type":"number","format":"double"},"y":{"type":"number","format":"double"},"z":{"type":"number","format":"double"}}},"target":{"type":"object","required":["x","y","z"],"properties":{"x":{"type":"number","format":"double"},"y":{"type":"number","format":"double"},"z":{"type":"number","format":"double"}}},"fov":{"type":"number","format":"double"}},"required":["position","target"]}},"required":["name","camera","target_node","marker_position"]},"FocusPointPatch":{"type":"object","description":"Fields a client may change on an existing FocusPoint. Every property is optional — only what's present is updated; `id` and `createdat` are immutable.\n","properties":{"name":{"type":"string"},"description":{"type":"string"},"target_node":{"type":"string"},"marker_position":{"type":"object","required":["x","y","z"],"properties":{"x":{"type":"number","format":"double"},"y":{"type":"number","format":"double"},"z":{"type":"number","format":"double"}}},"camera":{"type":"object","properties":{"position":{"type":"object","required":["x","y","z"],"properties":{"x":{"type":"number","format":"double"},"y":{"type":"number","format":"double"},"z":{"type":"number","format":"double"}}},"target":{"type":"object","required":["x","y","z"],"properties":{"x":{"type":"number","format":"double"},"y":{"type":"number","format":"double"},"z":{"type":"number","format":"double"}}},"fov":{"type":"number","format":"double"}},"required":["position","target"]}}},"DriverView":{"type":"object","description":"A captured base camera for the automotive configurator's \"driver/rider view\" (GLB projects only), plus how far a visitor may orbit away from it (zoom is disabled entirely in this view). The target need not be the model's centre — e.g. aimed at the back of a steering wheel or handlebars.\n","properties":{"camera":{"type":"object","description":"Camera captured when the editor confirms the driver view's centre point.","properties":{"position":{"type":"object","required":["x","y","z"],"properties":{"x":{"type":"number","format":"double"},"y":{"type":"number","format":"double"},"z":{"type":"number","format":"double"}}},"target":{"type":"object","required":["x","y","z"],"properties":{"x":{"type":"number","format":"double"},"y":{"type":"number","format":"double"},"z":{"type":"number","format":"double"}}},"fov":{"type":"number","format":"double"}},"required":["position","target"]},"deviation_pct":{"type":"number","format":"double","description":"How far, as a percentage of full rotation, a visitor may orbit the camera away from the captured azimuth/polar angle in either direction while in driver view. Zoom is disabled entirely in driver view, not range-limited.\n"}},"required":["camera","deviation_pct"]},"SavedViewpoint":{"type":"object","description":"An editor-captured camera/frame snapshot for a project in a configurator, saved as a standalone named entry. Distinct from the pipeline-authored `Viewpoint` schema used in `Rio.viewpoints[]`.\n","properties":{"id":{"type":"string","format":"uuid"},"name":{"type":"string"},"createdat":{"type":"string","format":"date-time"},"frame":{"description":"0-based position in the view's Frames, not a Map frame number","type":"integer","format":"int32"},"position":{"type":"object","required":["x","y","z"],"properties":{"x":{"type":"number","format":"double"},"y":{"type":"number","format":"double"},"z":{"type":"number","format":"double"}}},"target":{"type":"object","required":["x","y","z"],"properties":{"x":{"type":"number","format":"double"},"y":{"type":"number","format":"double"},"z":{"type":"number","format":"double"}}},"lat":{"type":"number","format":"double"},"lon":{"type":"number","format":"double"},"fov":{"type":"number","format":"double"},"thumbnail_key":{"type":"string","nullable":true,"description":"S3 key for an uploaded thumbnail screenshot — GLB/pano render capture, or an image-spin frame capture. Every viewpoint type uploads its thumbnail at save time; nothing resolves a thumbnail from project asset keys or presigned URLs, since those expire.\n"},"thumbnail_url":{"type":"string","nullable":true,"description":"Direct, non-expiring hosted image URL for the thumbnail. Must NOT be a presigned S3/CDN URL — those expire and the image silently 403s once the signature lapses. Null when thumbnail_key is set instead.\n"}},"required":["id","name","createdat"]},"SavedViewpointInput":{"type":"object","description":"Fields a client submits when creating a SavedViewpoint. `id` and `createdat` are server-generated, not accepted here. Send `frame` for a spin or stills, `position`/`target`/`fov` for a 3D model, `lat`/`lon`/`fov` for a panorama.\n","properties":{"name":{"type":"string"},"frame":{"description":"0-based position in the view's Frames, not a Map frame number","type":"integer","format":"int32"},"position":{"type":"object","required":["x","y","z"],"properties":{"x":{"type":"number","format":"double"},"y":{"type":"number","format":"double"},"z":{"type":"number","format":"double"}}},"target":{"type":"object","required":["x","y","z"],"properties":{"x":{"type":"number","format":"double"},"y":{"type":"number","format":"double"},"z":{"type":"number","format":"double"}}},"lat":{"type":"number","format":"double"},"lon":{"type":"number","format":"double"},"fov":{"type":"number","format":"double"},"thumbnail_key":{"type":"string","nullable":true},"thumbnail_url":{"type":"string","nullable":true}},"required":["name"]},"SavedScene":{"type":"object","description":"An editor-captured environment/background preset for a project in a configurator, saved as a standalone named entry. Applying it live-overrides only the environment and background portion of the model's scene settings — camera, post-fx, lighting rig and ground/shadow settings are untouched.\n","properties":{"id":{"type":"string","format":"uuid"},"name":{"type":"string"},"createdat":{"type":"string","format":"date-time"},"environment":{"type":"string","enum":["studio","warehouse","apartment","lobby","city","park","forest","dawn","sunset","night","road","asphalt","cityStreet","model","none"]},"background":{"type":"string","enum":["canvas","environment","ground"]},"background_blur":{"type":"number","format":"double"},"environment_rotation":{"type":"number","format":"double"},"environment_intensity":{"type":"number","format":"double"},"thumbnail_key":{"type":"string","nullable":true,"description":"S3 key for an auto-captured GLB viewport screenshot. Every scene uploads its thumbnail at save time, same as SavedViewpoint — never resolved from project asset keys or presigned URLs, since those expire.\n"},"thumbnail_url":{"type":"string","nullable":true,"description":"Direct, non-expiring hosted image URL for the thumbnail. Must NOT be a presigned S3/CDN URL — those expire and the image silently 403s once the signature lapses. Null when thumbnail_key is set instead.\n"}},"required":["id","name","createdat","environment","background","background_blur","environment_rotation","environment_intensity"]},"SavedSceneInput":{"type":"object","description":"Fields a client submits when creating a SavedScene. `id` and `createdat` are server-generated, not accepted here.\n","properties":{"name":{"type":"string"},"environment":{"type":"string","enum":["studio","warehouse","apartment","lobby","city","park","forest","dawn","sunset","night","road","asphalt","cityStreet","model","none"]},"background":{"type":"string","enum":["canvas","environment","ground"]},"background_blur":{"type":"number","format":"double"},"environment_rotation":{"type":"number","format":"double"},"environment_intensity":{"type":"number","format":"double"},"thumbnail_key":{"type":"string","nullable":true},"thumbnail_url":{"type":"string","nullable":true}},"required":["name","environment","background","background_blur","environment_rotation","environment_intensity"]},"CategorySettings":{"description":"How a configurator presents one rio category. Changes the configurator only, never the rio.\n","type":"object","properties":{"hidden":{"description":"Hide the category from visitors and lock it to `default_value`.","type":"boolean","default":false},"default_value":{"type":"string","nullable":true,"description":"FrontEndRule of the option to lock to when hidden. Null uses first option."},"lock_viewport":{"description":"Visitors can't move the view while this category is open.","type":"boolean","default":false},"default_viewport":{"type":"object","nullable":true,"description":"What the player shows when this category is opened: `position`/`target` for a 3D model, `lat`/`lon`/`fov` for a panorama, otherwise `frame`.\n","properties":{"frame":{"description":"0-based position in the open view's Frames, not a Map frame number","type":"integer","format":"int32"},"position":{"type":"object","required":["x","y","z"],"properties":{"x":{"type":"number","format":"double"},"y":{"type":"number","format":"double"},"z":{"type":"number","format":"double"}}},"target":{"type":"object","required":["x","y","z"],"properties":{"x":{"type":"number","format":"double"},"y":{"type":"number","format":"double"},"z":{"type":"number","format":"double"}}},"lat":{"type":"number","format":"double"},"lon":{"type":"number","format":"double"},"fov":{"type":"number","format":"double"}}},"label":{"type":"string","nullable":true,"description":"Display name override for this category."},"swatch_colours":{"type":"object","nullable":true,"additionalProperties":{"type":"string"},"description":"Per-option swatch colour overrides keyed by FrontEndRule."},"option_prices":{"type":"object","nullable":true,"additionalProperties":{"type":"number","format":"double"},"description":"Per-option price overrides keyed by FrontEndRule."},"option_images":{"type":"object","nullable":true,"additionalProperties":{"type":"string"},"description":"Per-option custom swatch image S3 keys keyed by FrontEndRule. Each must come from the configurator's option-image-upload-url; PUT refuses any other key with 400, and GET signs no URL for one already stored.\n"},"card_option_images":{"type":"object","nullable":true,"additionalProperties":{"type":"string"},"description":"Per-option card image S3 keys, under the same rule as `option_images`. Keyed by FrontEndRule for RIO-backed options, or by option ID (UUID) for custom options. Used in dropdown layout only — landscape aspect ratio. Separate from option_images which is used for the square swatch thumbnail.\n"},"hidden_swatches":{"type":"object","nullable":true,"additionalProperties":{"type":"boolean"},"description":"Per-option swatch visibility overrides keyed by FrontEndRule."},"option_labels":{"type":"object","nullable":true,"additionalProperties":{"type":"string"},"description":"Display name overrides for options keyed by FrontEndRule."}}},"VideoComment":{"type":"object","properties":{"id":{"description":"Comment ID","type":"string","example":"comment-12345"},"projectid":{"description":"project ID the comment belongs to","type":"string","example":"project-67890"},"version":{"description":"Version number","type":"integer","format":"int32","example":1},"userid":{"description":"User ID of the comment creator","type":"string","example":"user-12345"},"displayname":{"description":"Display name of the comment creator","type":"string","example":"John Doe"},"bgcolour":{"description":"Background color of the comment creator","type":"string","example":"#FF5733"},"comment":{"description":"Comment text","type":"string","example":"This looks great!"},"config":{"description":"Configuration for the comment","type":"string","nullable":true,"example":"+VIW+COL"},"attachments":{"description":"Attached projects","type":"array","nullable":true,"items":{"$ref":"#/components/schemas/Attachment"}},"frame":{"description":"Frame number for the comment","type":"integer","format":"int32","nullable":true,"example":0},"labels":{"description":"Labels for the comment","type":"string","nullable":true,"example":"feedback"},"createdat":{"description":"Creation timestamp","type":"string","format":"date-time","example":"2023-04-13T10:30:00Z"},"editedat":{"description":"Last edit timestamp","type":"string","format":"date-time","example":"2023-04-13T11:45:00Z"},"owner":{"description":"Whether the current user owns the comment","type":"boolean","example":true},"completed":{"description":"Whether the comment is marked as completed","type":"boolean","nullable":true,"example":false},"replies":{"type":"array","items":{"$ref":"#/components/schemas/Reply"}},"ai_image_id":{"description":"Legacy. The generated image a comment was made on, from before such comments carried its `rule` in `config`.","type":"string","format":"uuid","nullable":true,"example":"550e8400-e29b-41d4-a716-446655440000"},"approval_number":{"description":"Approval request number this comment was made during, if any","type":"integer","format":"int32","nullable":true,"example":1},"file_path":{"description":"The version's stored file this comment is on, relative to the version's upload root. A comment on a file has no `config`: it is about the file, not a view of the rio.\n","type":"string","nullable":true,"example":"textures/paint.png"},"file_size":{"description":"The file's size in bytes when the comment was made.","type":"integer","format":"int64","nullable":true,"example":174112},"file_etag":{"description":"The file's ETag when the comment was made, if its listing recorded one.","type":"string","nullable":true,"example":"\"9b2cf535f27731c974343645a3985328\""},"file_changed":{"description":"Whether the version's file is no longer the copy the comment was made on: replaced in place, or gone from the version. Decided by size, and by ETag where neither is a multipart one. Null on a comment that isn't on a file.\n","type":"boolean","nullable":true,"example":false},"player":{"description":"Player type","type":"string","enum":["video","audio"]},"settings":{"$ref":"#/components/schemas/VideoCommentSettings"}},"required":["id","player","createdat","editedat","userid","projectid","version","comment","displayname","bgcolour","owner","replies"]},"PanoComment":{"type":"object","properties":{"id":{"description":"Comment ID","type":"string","example":"comment-12345"},"projectid":{"description":"project ID the comment belongs to","type":"string","example":"project-67890"},"version":{"description":"Version number","type":"integer","format":"int32","example":1},"userid":{"description":"User ID of the comment creator","type":"string","example":"user-12345"},"displayname":{"description":"Display name of the comment creator","type":"string","example":"John Doe"},"bgcolour":{"description":"Background color of the comment creator","type":"string","example":"#FF5733"},"comment":{"description":"Comment text","type":"string","example":"This looks great!"},"config":{"description":"Configuration for the comment","type":"string","nullable":true,"example":"+VIW+COL"},"attachments":{"description":"Attached projects","type":"array","nullable":true,"items":{"$ref":"#/components/schemas/Attachment"}},"frame":{"description":"Frame number for the comment","type":"integer","format":"int32","nullable":true,"example":0},"labels":{"description":"Labels for the comment","type":"string","nullable":true,"example":"feedback"},"createdat":{"description":"Creation timestamp","type":"string","format":"date-time","example":"2023-04-13T10:30:00Z"},"editedat":{"description":"Last edit timestamp","type":"string","format":"date-time","example":"2023-04-13T11:45:00Z"},"owner":{"description":"Whether the current user owns the comment","type":"boolean","example":true},"completed":{"description":"Whether the comment is marked as completed","type":"boolean","nullable":true,"example":false},"replies":{"type":"array","items":{"$ref":"#/components/schemas/Reply"}},"ai_image_id":{"description":"Legacy. The generated image a comment was made on, from before such comments carried its `rule` in `config`.","type":"string","format":"uuid","nullable":true,"example":"550e8400-e29b-41d4-a716-446655440000"},"approval_number":{"description":"Approval request number this comment was made during, if any","type":"integer","format":"int32","nullable":true,"example":1},"file_path":{"description":"The version's stored file this comment is on, relative to the version's upload root. A comment on a file has no `config`: it is about the file, not a view of the rio.\n","type":"string","nullable":true,"example":"textures/paint.png"},"file_size":{"description":"The file's size in bytes when the comment was made.","type":"integer","format":"int64","nullable":true,"example":174112},"file_etag":{"description":"The file's ETag when the comment was made, if its listing recorded one.","type":"string","nullable":true,"example":"\"9b2cf535f27731c974343645a3985328\""},"file_changed":{"description":"Whether the version's file is no longer the copy the comment was made on: replaced in place, or gone from the version. Decided by size, and by ETag where neither is a multipart one. Null on a comment that isn't on a file.\n","type":"boolean","nullable":true,"example":false},"player":{"description":"Player type","type":"string","enum":["pano"]},"settings":{"oneOf":[{"$ref":"#/components/schemas/PanoCommentSettings"},{"type":"null"}]}},"required":["id","player","createdat","editedat","userid","projectid","version","comment","displayname","bgcolour","owner","replies"]},"ModelComment":{"type":"object","properties":{"id":{"description":"Comment ID","type":"string","example":"comment-12345"},"projectid":{"description":"project ID the comment belongs to","type":"string","example":"project-67890"},"version":{"description":"Version number","type":"integer","format":"int32","example":1},"userid":{"description":"User ID of the comment creator","type":"string","example":"user-12345"},"displayname":{"description":"Display name of the comment creator","type":"string","example":"John Doe"},"bgcolour":{"description":"Background color of the comment creator","type":"string","example":"#FF5733"},"comment":{"description":"Comment text","type":"string","example":"This looks great!"},"config":{"description":"Configuration for the comment","type":"string","nullable":true,"example":"+VIW+COL"},"attachments":{"description":"Attached projects","type":"array","nullable":true,"items":{"$ref":"#/components/schemas/Attachment"}},"frame":{"description":"Frame number for the comment","type":"integer","format":"int32","nullable":true,"example":0},"labels":{"description":"Labels for the comment","type":"string","nullable":true,"example":"feedback"},"createdat":{"description":"Creation timestamp","type":"string","format":"date-time","example":"2023-04-13T10:30:00Z"},"editedat":{"description":"Last edit timestamp","type":"string","format":"date-time","example":"2023-04-13T11:45:00Z"},"owner":{"description":"Whether the current user owns the comment","type":"boolean","example":true},"completed":{"description":"Whether the comment is marked as completed","type":"boolean","nullable":true,"example":false},"replies":{"type":"array","items":{"$ref":"#/components/schemas/Reply"}},"ai_image_id":{"description":"Legacy. The generated image a comment was made on, from before such comments carried its `rule` in `config`.","type":"string","format":"uuid","nullable":true,"example":"550e8400-e29b-41d4-a716-446655440000"},"approval_number":{"description":"Approval request number this comment was made during, if any","type":"integer","format":"int32","nullable":true,"example":1},"file_path":{"description":"The version's stored file this comment is on, relative to the version's upload root. A comment on a file has no `config`: it is about the file, not a view of the rio.\n","type":"string","nullable":true,"example":"textures/paint.png"},"file_size":{"description":"The file's size in bytes when the comment was made.","type":"integer","format":"int64","nullable":true,"example":174112},"file_etag":{"description":"The file's ETag when the comment was made, if its listing recorded one.","type":"string","nullable":true,"example":"\"9b2cf535f27731c974343645a3985328\""},"file_changed":{"description":"Whether the version's file is no longer the copy the comment was made on: replaced in place, or gone from the version. Decided by size, and by ETag where neither is a multipart one. Null on a comment that isn't on a file.\n","type":"boolean","nullable":true,"example":false},"player":{"description":"Player type","type":"string","enum":["model"]},"settings":{"oneOf":[{"$ref":"#/components/schemas/ModelCommentSettings"},{"type":"null"}]}},"required":["id","player","createdat","editedat","userid","projectid","version","comment","displayname","bgcolour","owner","replies"]},"ImageComment":{"type":"object","properties":{"id":{"description":"Comment ID","type":"string","example":"comment-12345"},"projectid":{"description":"project ID the comment belongs to","type":"string","example":"project-67890"},"version":{"description":"Version number","type":"integer","format":"int32","example":1},"userid":{"description":"User ID of the comment creator","type":"string","example":"user-12345"},"displayname":{"description":"Display name of the comment creator","type":"string","example":"John Doe"},"bgcolour":{"description":"Background color of the comment creator","type":"string","example":"#FF5733"},"comment":{"description":"Comment text","type":"string","example":"This looks great!"},"config":{"description":"Configuration for the comment","type":"string","nullable":true,"example":"+VIW+COL"},"attachments":{"description":"Attached projects","type":"array","nullable":true,"items":{"$ref":"#/components/schemas/Attachment"}},"frame":{"description":"Frame number for the comment","type":"integer","format":"int32","nullable":true,"example":0},"labels":{"description":"Labels for the comment","type":"string","nullable":true,"example":"feedback"},"createdat":{"description":"Creation timestamp","type":"string","format":"date-time","example":"2023-04-13T10:30:00Z"},"editedat":{"description":"Last edit timestamp","type":"string","format":"date-time","example":"2023-04-13T11:45:00Z"},"owner":{"description":"Whether the current user owns the comment","type":"boolean","example":true},"completed":{"description":"Whether the comment is marked as completed","type":"boolean","nullable":true,"example":false},"replies":{"type":"array","items":{"$ref":"#/components/schemas/Reply"}},"ai_image_id":{"description":"Legacy. The generated image a comment was made on, from before such comments carried its `rule` in `config`.","type":"string","format":"uuid","nullable":true,"example":"550e8400-e29b-41d4-a716-446655440000"},"approval_number":{"description":"Approval request number this comment was made during, if any","type":"integer","format":"int32","nullable":true,"example":1},"file_path":{"description":"The version's stored file this comment is on, relative to the version's upload root. A comment on a file has no `config`: it is about the file, not a view of the rio.\n","type":"string","nullable":true,"example":"textures/paint.png"},"file_size":{"description":"The file's size in bytes when the comment was made.","type":"integer","format":"int64","nullable":true,"example":174112},"file_etag":{"description":"The file's ETag when the comment was made, if its listing recorded one.","type":"string","nullable":true,"example":"\"9b2cf535f27731c974343645a3985328\""},"file_changed":{"description":"Whether the version's file is no longer the copy the comment was made on: replaced in place, or gone from the version. Decided by size, and by ETag where neither is a multipart one. Null on a comment that isn't on a file.\n","type":"boolean","nullable":true,"example":false},"player":{"description":"Player type","type":"string","enum":["still","stills","animation","pdf","exr"]},"settings":{"oneOf":[{"$ref":"#/components/schemas/ImageCommentSettings"},{"type":"null"}]}},"required":["id","player","createdat","editedat","userid","projectid","version","comment","displayname","bgcolour","owner","replies"]},"TextComment":{"type":"object","properties":{"id":{"description":"Comment ID","type":"string","example":"comment-12345"},"projectid":{"description":"project ID the comment belongs to","type":"string","example":"project-67890"},"version":{"description":"Version number","type":"integer","format":"int32","example":1},"userid":{"description":"User ID of the comment creator","type":"string","example":"user-12345"},"displayname":{"description":"Display name of the comment creator","type":"string","example":"John Doe"},"bgcolour":{"description":"Background color of the comment creator","type":"string","example":"#FF5733"},"comment":{"description":"Comment text","type":"string","example":"This looks great!"},"config":{"description":"Configuration for the comment","type":"string","nullable":true,"example":"+VIW+COL"},"attachments":{"description":"Attached projects","type":"array","nullable":true,"items":{"$ref":"#/components/schemas/Attachment"}},"frame":{"description":"Frame number for the comment","type":"integer","format":"int32","nullable":true,"example":0},"labels":{"description":"Labels for the comment","type":"string","nullable":true,"example":"feedback"},"createdat":{"description":"Creation timestamp","type":"string","format":"date-time","example":"2023-04-13T10:30:00Z"},"editedat":{"description":"Last edit timestamp","type":"string","format":"date-time","example":"2023-04-13T11:45:00Z"},"owner":{"description":"Whether the current user owns the comment","type":"boolean","example":true},"completed":{"description":"Whether the comment is marked as completed","type":"boolean","nullable":true,"example":false},"replies":{"type":"array","items":{"$ref":"#/components/schemas/Reply"}},"ai_image_id":{"description":"Legacy. The generated image a comment was made on, from before such comments carried its `rule` in `config`.","type":"string","format":"uuid","nullable":true,"example":"550e8400-e29b-41d4-a716-446655440000"},"approval_number":{"description":"Approval request number this comment was made during, if any","type":"integer","format":"int32","nullable":true,"example":1},"file_path":{"description":"The version's stored file this comment is on, relative to the version's upload root. A comment on a file has no `config`: it is about the file, not a view of the rio.\n","type":"string","nullable":true,"example":"textures/paint.png"},"file_size":{"description":"The file's size in bytes when the comment was made.","type":"integer","format":"int64","nullable":true,"example":174112},"file_etag":{"description":"The file's ETag when the comment was made, if its listing recorded one.","type":"string","nullable":true,"example":"\"9b2cf535f27731c974343645a3985328\""},"file_changed":{"description":"Whether the version's file is no longer the copy the comment was made on: replaced in place, or gone from the version. Decided by size, and by ETag where neither is a multipart one. Null on a comment that isn't on a file.\n","type":"boolean","nullable":true,"example":false},"player":{"description":"Player type","type":"string","enum":["text"]},"settings":{"$ref":"#/components/schemas/TextCommentSettings"}},"required":["id","player","createdat","editedat","userid","projectid","version","comment","displayname","bgcolour","owner","replies"]},"Comment":{"oneOf":[{"$ref":"#/components/schemas/VideoComment"},{"$ref":"#/components/schemas/TextComment"},{"$ref":"#/components/schemas/PanoComment"},{"$ref":"#/components/schemas/ModelComment"},{"$ref":"#/components/schemas/ImageComment"}],"discriminator":{"propertyName":"player","mapping":{"video":"#/components/schemas/VideoComment","audio":"#/components/schemas/VideoComment","text":"#/components/schemas/TextComment","pano":"#/components/schemas/PanoComment","model":"#/components/schemas/ModelComment","still":"#/components/schemas/ImageComment","stills":"#/components/schemas/ImageComment","animation":"#/components/schemas/ImageComment","pdf":"#/components/schemas/ImageComment","exr":"#/components/schemas/ImageComment"}}},"MentionCandidate":{"type":"object","description":"A single hit from the mention-candidates endpoint.","properties":{"id":{"type":"string","format":"uuid"},"display_name":{"type":"string"},"email":{"type":"string","format":"email"},"bg_colour":{"type":"string"},"avatar":{"type":"string"}},"required":["id","display_name","email"]},"Viewpoint":{"description":"A view of the project saved in the rio. In a configurator, the newest viewpoint naming a category does two things. When the configurator loads with no selection given, the category starts on the option whose FrontEndRule appears in `config`, unless a hidden-category lock overrides it. When the category is opened, the player goes to `frame`, or for a 3D model to the camera in `settings`, unless the category's `default_viewport` is set.\n","type":"object","properties":{"id":{"type":"string","format":"uuid"},"createdat":{"type":"string"},"frame":{"description":"The Map `frame` number shown","type":"integer","format":"int32","nullable":true},"config":{"description":"The selected FrontEndRules, joined, like `+SPORT+RED+EXT`. Matched by substring, so avoid a FrontEndRule that is the start of another, like `+RED` beside `+REDX`.\n","type":"string","nullable":true},"category":{"description":"The Name of the category the viewpoint belongs to","type":"string","nullable":true},"settings":{"$ref":"#/components/schemas/CommentSettings"},"thumbnail":{"type":"string","nullable":true}},"required":["id","createdat"]},"Reply":{"type":"object","properties":{"id":{"description":"Reply ID","type":"string","example":"reply-12345"},"commentid":{"description":"Comment ID this reply belongs to","type":"string","example":"comment-67890"},"version":{"description":"Version number","type":"integer","format":"int32","example":1},"createdat":{"description":"Creation timestamp","type":"string","format":"date-time","example":"2023-04-13T12:30:00Z"},"owner":{"description":"Whether the current user owns the reply","type":"boolean","example":false},"comment":{"description":"Reply text","type":"string","example":"Thanks for the feedback!"},"displayname":{"description":"Display name of the reply creator","type":"string","example":"Jane Smith"},"bgcolour":{"description":"Background color of the reply creator","type":"string","example":"#33FF57"}},"required":["id","commentid","version","createdat","owner","comment","displayname","bgcolour"]},"EmbedAllowedOrigins":{"description":"Origins allowed to embed this workspace's configurators in an iframe. An empty list allows\nno site. The whole list is replaced on update.\n","type":"array","maxItems":50,"items":{"type":"string","maxLength":253,"pattern":"^(https://(\\*\\.)?[A-Za-z0-9-]+(\\.[A-Za-z0-9-]+)+(:[0-9]{1,5})?|http://localhost(:[0-9]{1,5})?)$","example":"https://shop.example.com"}},"Team":{"type":"object","required":["id","name","visibility"],"properties":{"id":{"description":"Team ID","type":"string","example":"team-12345"},"name":{"description":"Team name","type":"string","example":"Design Team"},"description":{"description":"Optional free-text workspace description.","type":"string","nullable":true,"example":"Soft-form footwear and leather goods."},"createdat":{"description":"Creation timestamp","type":"string","format":"date-time","example":"2023-04-13T10:30:00Z"},"editedat":{"description":"Last edit timestamp","type":"string","format":"date-time","example":"2023-04-13T10:30:00Z"},"project_count":{"description":"Number of projects in this workspace. Populated on the list endpoint only.","type":"integer","format":"int32","example":47},"storage_used_bytes":{"description":"Sum of project file sizes in this workspace. Populated on the list and single-workspace GET endpoints.","type":"integer","format":"int64","example":4294967296},"embed_allowed_origins":{"$ref":"#/components/schemas/EmbedAllowedOrigins"},"owned":{"description":"Whether the current user created the workspace","type":"boolean","example":true},"owner":{"description":"Display name of the person who created the workspace. Use `created_by_name`.","type":"string","deprecated":true,"example":"John Doe"},"created_by_name":{"description":"Display name of the person who created the workspace","type":"string","example":"John Doe"},"accountid":{"description":"Account ID associated with this workspace","type":"string","format":"uuid","nullable":true,"example":"550e8400-e29b-41d4-a716-446655440000"},"ai_images_enabled":{"description":"Whether image generation is enabled for this workspace","type":"boolean","nullable":true,"example":false},"default_bg_colour":{"description":"Default viewer background colour for projects in this workspace (6-char hex, optionally with \"00\" for checkered pattern)","type":"string","nullable":true,"example":"f0f0f000"},"visibility":{"$ref":"#/components/schemas/Visibility"}}},"ShareMember":{"type":"object","properties":{"id":{"description":"User ID","type":"string","example":"user-12345"},"displayname":{"description":"Display name of the member","type":"string","example":"Jane Smith"},"email":{"description":"Email address of the member","type":"string","format":"email","example":"jane.smith@example.com"},"bgcolour":{"description":"Background color of the member","type":"string","example":"#33FF57"}}},"UpdateShareRequest":{"type":"object","properties":{"name":{"description":"Link name","type":"string","minLength":3,"example":"Updated Client Preview"},"versions":{"description":"Available versions","type":"array","items":{"type":"integer","format":"int32"},"example":[1,2,3,4]},"allowteammembers":{"description":"Whether team members can access","type":"boolean","example":true},"isprivate":{"description":"Whether the link is private","type":"boolean","example":true},"showcomments":{"description":"Whether comments are visible","type":"boolean","example":true},"allowcommenting":{"description":"Whether commenting is allowed","type":"boolean","example":true},"logo_key":{"description":"S3 key for the logo (from POST /shares/{id}/logo)","type":"string","nullable":true,"example":"accounts/abc-123/logos/hash-logo.webp"}},"required":["name","versions","allowteammembers","isprivate","showcomments","allowcommenting"]},"WorkspaceMember":{"type":"object","description":"A workspace member with an accepted seat.","properties":{"id":{"type":"string","format":"uuid"},"displayname":{"description":"Display name of the member","type":"string","example":"Jane Smith"},"email":{"description":"Email address of the member","type":"string","format":"email","example":"jane.smith@example.com"},"bgcolour":{"description":"Background color of the member's avatar","type":"string","example":"#33FF57"},"role":{"$ref":"#/components/schemas/WorkspaceRole"}},"required":["id","displayname","email","bgcolour","role"]},"WorkspaceRole":{"type":"string","enum":["admin","editor","reviewer","member"],"description":"A person's role in a workspace. Reviewers view, download, comment and\napprove; editors also upload, edit and share; admins also manage the\nworkspace and its members. What the role allows is limited by the\nperson's role in the workspace's studio.\n\n`member` is deprecated and means `editor`. It is accepted on writes,\nstored as `editor`, and may still appear on older memberships.\n"},"WorkspaceInvitation":{"type":"object","description":"A pending workspace invitation. The invitee has not accepted yet.","properties":{"email":{"type":"string","format":"email","example":"jane.smith@example.com"},"role":{"$ref":"#/components/schemas/WorkspaceRole"}},"required":["email","role"]},"LinkInfo":{"type":"object","required":["id","createdat","name","owned"],"properties":{"id":{"description":"Link ID","type":"string","example":"link-12345"},"fileid":{"description":"Associated project/file ID","type":"string","example":"file-12345"},"createdat":{"description":"Creation timestamp","type":"string","format":"date-time","example":"2023-04-13T10:30:00Z"},"name":{"description":"Link name","type":"string","example":"Client Preview"},"version":{"description":"Version number","type":"integer","format":"int32","example":1},"versions":{"description":"Available versions","type":"array","items":{"type":"integer","format":"int32"},"example":[1,2,3]},"projectname":{"description":"Associated project name","nullable":true,"type":"string","example":"Product Website"},"owned":{"description":"Whether the current user created the link","type":"boolean","example":true},"private":{"description":"Whether the link is private (invite/workspace only) vs an open link","type":"boolean","example":true},"allowteammembers":{"description":"Whether workspace members can view (creator-only; omitted on links you didn't create)","type":"boolean","example":true},"owner":{"description":"Display name of the person who created the link. Use `created_by_name`.","nullable":true,"type":"string","deprecated":true,"example":"John Doe"},"created_by_name":{"description":"Display name of the person who created the link","nullable":true,"type":"string","example":"John Doe"},"scope":{"description":"How the current user can see this link.\n`mine` — created by the current user.\n`shared` — explicitly shared with the current user via link membership.\n`account_public` — a non-private link on the current user's account, visible to all account members.\n","type":"string","enum":["mine","shared","account_public"],"example":"mine"},"bookmarked":{"description":"Whether the current user has bookmarked this link.","type":"boolean","example":false}}},"PresignedUrlResponse":{"type":"object","properties":{"url":{"description":"The presigned URL for reading the object","type":"string","example":"https://s3.amazonaws.com/mybucket/mykey?AWSAccessKeyId=..."}},"required":["url"]},"LegacyUploadUrlResponse":{"type":"object","properties":{"url":{"description":"The presigned URL for S3 upload","type":"string","example":"https://s3.amazonaws.com/mybucket/mykey?AWSAccessKeyId=..."},"fields":{"description":"Form fields required for the upload","type":"object","additionalProperties":true}}},"MultipartUploadResponse":{"type":"object","properties":{"upload_id":{"description":"The ID of the multipart upload","type":"string","example":"VXBsb2FkIElEIGZvciA2aWWpbmcncyBteS1tb3ZpZS5tMnRzIHVwbG9hZ..."},"key":{"description":"The key for the upload","type":"string","example":"projects/document.pdf"}}},"UploadTarget":{"type":"object","properties":{"method":{"description":"How the body must be sent. S3 returns a POST policy so Cache-Control,\nContent-Type and metadata are pinned to the object; GCS's interop API\ndoesn't accept AWS-style policies and returns a PUT URL instead.\n","type":"string","enum":["POST","PUT"],"example":"POST"},"url":{"description":"The URL to send the upload to","type":"string","example":"https://mybucket.s3.amazonaws.com/"},"fields":{"description":"Form fields to submit with a POST upload. Empty for PUT.","type":"object","additionalProperties":{"type":"string"}}},"required":["method","url","fields"]},"DecryptedResponse":{"type":"object","properties":{"data":{"description":"The decrypted content","type":"string","example":"Decrypted project content"}}},"ProjectListItem":{"type":"object","properties":{"id":{"description":"The project ID","type":"string","format":"uuid"},"name":{"description":"The project name","type":"string"},"filetype":{"description":"The project type tags. A project can carry multiple badges.","type":"array","items":{"type":"string"},"nullable":true},"editedat":{"description":"When the project was last edited","type":"string","format":"date-time"},"filesize":{"description":"The project size in bytes","type":"integer","format":"int64","nullable":true},"created_by":{"description":"The person who created the project. It grants no permissions; those come from the workspace.\n","type":"string","nullable":true},"created_by_name":{"description":"Display name of the person who created the project","type":"string","nullable":true},"parentid":{"description":"The parent folder ID","type":"string","nullable":true},"workspace_id":{"description":"The workspace ID","type":"string","format":"uuid"},"latestversion":{"description":"The latest non-deleted version that has been uploaded. Null when nothing has been.","type":"integer","format":"int32","nullable":true},"empty_version":{"description":"The latest non-deleted version created with nothing uploaded to it yet. Null when there is none.","type":"integer","format":"int32","nullable":true},"thumbnail_version":{"description":"Version number that has a confirmed composed thumbnail in S3. Null when no thumb has been generated yet.","type":"integer","format":"int32","nullable":true},"visibility":{"$ref":"#/components/schemas/Visibility"}},"required":["id","name","editedat","workspace_id","visibility"]},"ProjectsResponse":{"type":"object","properties":{"id":{"description":"The project ID","type":"string","example":"project-12345"},"name":{"description":"The project name","type":"string","example":"document.pdf"},"projecttype":{"description":"The project type","type":"string","example":"pdf"},"editedat":{"description":"When the project was last edited","type":"string","format":"date-time","example":"2023-04-13T10:30:00Z"},"projectsize":{"description":"The project size in bytes","type":"integer","format":"int64","example":1024000},"parentid":{"description":"The parent folder ID","type":"string","nullable":true,"example":"folder-12345"},"created_by":{"description":"The person who created the project. It grants no permissions; those come from the workspace.\n","type":"string","example":"user-12345"},"teamid":{"description":"The workspace (team) ID the project belongs to","type":"string","example":"team-6789"},"accountid":{"description":"The studio (account) the project's workspace belongs to. Null unless the caller is a member of that studio.\n","type":"string","format":"uuid","nullable":true,"example":"550e8400-e29b-41d4-a716-446655440000"},"labels":{"description":"Labels associated with the project","type":"array","items":{"type":"string"},"example":["important","documentation"]},"rio":{"description":"The project-level rio content (from file.filecontent)","oneOf":[{"$ref":"#/components/schemas/Rio"},{"type":"null"}]},"cache_dir":{"description":"Local filesystem directory the user last uploaded from. Persists\nacross versions — the uploader uses it to suggest a starting\npoint on the next upload. Null when the project has never\nreceived a cache_dir.\n","type":"string","nullable":true},"visibility":{"$ref":"#/components/schemas/Visibility"},"versions":{"description":"Version information for the project","type":"array","items":{"type":"object","properties":{"version":{"type":"integer","format":"int32"},"description":{"type":"string","nullable":true},"displayname":{"type":"string"},"size":{"type":"integer","format":"int64","nullable":true},"createdat":{"type":"string","format":"date-time"}}}}}},"FolderResponse":{"type":"object","properties":{"id":{"description":"The folder ID","type":"string","example":"folder-12345"},"name":{"description":"The folder name","type":"string","example":"Documents"},"parentid":{"description":"The parent folder ID","type":"string","nullable":true,"example":"folder-6789"},"teamid":{"description":"The team ID if the folder belongs to a team","type":"string","nullable":true,"example":"team-6789"}}},"VersionResponse":{"type":"object","properties":{"version":{"description":"The current version number","type":"integer","format":"int32","example":3},"displayname":{"description":"The display name of the user who created the version","type":"string","example":"John Doe"},"settings":{"description":"The settings for the version (background pattern)","$ref":"#/components/schemas/CanvasSettings"},"rio":{"description":"The project content for the current version","oneOf":[{"$ref":"#/components/schemas/Rio"},{"type":"null"}]},"logo_url":{"description":"Custom logo URL (share logo, account logo, or null for default)","type":"string","nullable":true,"example":"https://s3.example.com/presigned-logo-url"},"created_at":{"description":"When the version was created","type":"string","format":"date-time"},"uploaded_from_url":{"description":"Base URL of the server the version was uploaded from","type":"string"},"views":{"description":"Aggregated view history per person (omitted for share link requests)","type":"array","items":{"type":"object","properties":{"person_id":{"type":"string","format":"uuid"},"displayname":{"type":"string"},"last_viewed_at":{"type":"string","format":"date-time"},"view_count":{"type":"integer","format":"int32"}},"required":["person_id","displayname","last_viewed_at","view_count"]}}}},"ArchiveUrlResponse":{"type":"object","required":["url"],"properties":{"url":{"description":"The presigned URL for downloading the archive","type":"string","example":"https://s3.amazonaws.com/mybucket/archive.zip?AWSAccessKeyId=..."}}},"AddProjectRequest":{"type":"object","properties":{"name":{"description":"The project name","type":"string","example":"document.pdf"},"project_type":{"description":"The project type","type":"string","example":"pdf"},"rio":{"description":"The project content","$ref":"#/components/schemas/Rio","example":"project content here"},"parent_id":{"description":"The parent folder ID","type":"string","example":"folder-12345"},"workspace_id":{"description":"The workspace ID if this is a team project","type":"string","example":"team-6789"}},"required":["name","rio"]},"FinishUploadRequest":{"type":"object","properties":{"version_size":{"description":"The size of the version in bytes","type":"integer","format":"int64","example":1024000},"version_description":{"description":"The description of the version","type":"string","example":"Added new features"},"rio":{"description":"The updated project content","$ref":"#/components/schemas/Rio","example":"Updated project content"},"processed_rio":{"description":"The processed project content with webp filenames and dimensions","$ref":"#/components/schemas/Rio"},"cache_dir":{"description":"Local filesystem directory the user uploaded from. Stored on the\nproject (file.cache_dir) so subsequent uploads can suggest the\nsame starting point. Project-level — each upload overwrites it.\n","type":"string","nullable":true},"uploaded_paths":{"description":"The files this upload wrote, not those it skipped as already stored. When the version was already uploaded, these are overwritten in place: each gets a new media revision and has its renditions made again.\n","type":"array","items":{"type":"string"}}},"required":["version_size","version_description","rio"]},"VersionSizeUpdateRequest":{"type":"object","properties":{"version_size":{"description":"The updated size in bytes","type":"integer","format":"int64","example":1024000}},"required":["version_size"]},"VersionFieldUpdateRequest":{"type":"object","minProperties":1,"properties":{"settings":{"description":"Settings to merge into the version's existing settings, one level deep. Omitted keys keep their stored value; sending `pattern: null` still clears the colour.","$ref":"#/components/schemas/CanvasSettings"},"description":{"description":"The updated version description","type":"string","example":"Updated description"},"rio":{"$ref":"#/components/schemas/Rio"}}},"EditLabelsRequest":{"type":"object","properties":{"labels":{"description":"Updated labels for the project.","type":"array","items":{"type":"string","minLength":1},"example":["important","documentation","new-label"]},"showcomments":{"description":"Flag for share link comments access","type":"boolean","example":true}},"required":["labels"]},"EditLabelsResponse":{"type":"object","properties":{"success":{"type":"boolean","example":true},"updated":{"type":"integer","format":"int32","example":1}}},"CanvasSettings":{"type":"object","properties":{"pattern":{"description":"The background pattern (3 or 6 character hex code or null)","type":"string","nullable":true,"pattern":"^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$"},"model":{"description":"How the 3D player presents the scene. Ignored by 2D players.","$ref":"#/components/schemas/ModelSceneSettings"},"model_content":{"description":"What the 3D player shows, as opposed to how it presents it. Kept apart from `model` because that is presentation and this is content — the distinction decides which survives a re-upload.","$ref":"#/components/schemas/ModelContentSettings"}}},"ModelContentSettings":{"type":"object","description":"Content decisions for a model version that are not configuration. Set in the viewer and applied wherever the model is shown, live configurators included.","properties":{"hidden_nodes":{"description":"Names of glTF nodes locked off for this version, whatever the configuration selects. Names rather than object ids: ids are a runtime counter and would not survive the reload this outlives.","type":"array","items":{"type":"string"}}}},"ModelSceneSettings":{"type":"object","description":"Presentation settings for the model player — lighting and framing, not model content. Every field is optional; the viewer falls back to its own defaults for anything absent.","properties":{"environment":{"description":"Image-based lighting preset. \"none\" swaps in a plain three-point rig.","type":"string","enum":["studio","warehouse","apartment","lobby","city","park","forest","dawn","sunset","night","road","asphalt","cityStreet","model","none"]},"background":{"description":"What sits behind the model — the canvas colour from `pattern`, or the environment map itself as a backdrop.","type":"string","enum":["canvas","environment","ground"]},"background_blur":{"description":"Blurs the environment backdrop. No effect when background is \"canvas\".","type":"number","format":"float","minimum":0,"maximum":1},"environment_intensity":{"description":"Multiplier on the light the environment casts.","type":"number","format":"float","minimum":0,"maximum":3},"environment_rotation":{"description":"Spins the environment around the vertical axis, in degrees.","type":"number","format":"float","minimum":0,"maximum":360},"fov":{"description":"Vertical field of view, in degrees.","type":"number","format":"float","minimum":15,"maximum":90},"exposure":{"description":"Tone-mapping exposure multiplier.","type":"number","format":"float","minimum":0.1,"maximum":3},"auto_rotate":{"type":"boolean"},"ground_shadow":{"type":"boolean"},"lock_to_ground":{"description":"Stops the camera orbiting below the model.","type":"boolean"},"opening_fit":{"description":"How the camera frames the model when it first opens. \"sphere\" fits the model's bounding sphere, which is the same size from every angle, so turning the model never pushes it into the frame edge. \"box\" fits the silhouette the model presents at that moment, which uses the frame better on a wide canvas but is tighter once turned.","type":"string","enum":["sphere","box"]},"drag_smoothing":{"description":"Seconds of lag between the pointer and the camera while dragging. Zero tracks the pointer exactly; higher values feel heavier.","type":"number","format":"float","minimum":0,"maximum":0.5},"post_fx":{"description":"Screen-space effects composited after the scene renders. Each is off unless enabled; they flatter a model rather than describe it, so nothing applies by default.","type":"object","properties":{"ambient_occlusion":{"type":"object","properties":{"enabled":{"type":"boolean"},"intensity":{"type":"number","format":"float","minimum":0,"maximum":3},"radius":{"type":"number","format":"float","minimum":0.02,"maximum":1}}},"bloom":{"type":"object","properties":{"enabled":{"type":"boolean"},"intensity":{"type":"number","format":"float","minimum":0,"maximum":3},"threshold":{"type":"number","format":"float","minimum":0,"maximum":1}}},"vignette":{"type":"object","properties":{"enabled":{"type":"boolean"},"darkness":{"type":"number","format":"float","minimum":0,"maximum":1}}}}},"lights":{"description":"Three-point rig, applied on top of the environment. Aimed in degrees rather than world units so the rig reads the same whatever the model's scale.","type":"array","items":{"type":"object","properties":{"enabled":{"type":"boolean"},"intensity":{"type":"number","format":"float","minimum":0,"maximum":5},"colour":{"type":"string","pattern":"^#([0-9a-fA-F]{6})$"},"azimuth":{"description":"Degrees around the vertical axis.","type":"number","format":"float","minimum":-180,"maximum":180},"elevation":{"description":"Degrees above the horizon.","type":"number","format":"float","minimum":-20,"maximum":90}}}}}},"ProjectActivity":{"type":"object","description":"Work in flight for one project version.","properties":{"project_id":{"type":"string","format":"uuid"},"version":{"type":"integer","format":"int32"},"kind":{"description":"What is happening, for the tile's label","type":"string","enum":["copying","preparing","zipping","optimising","transcoding","processing"]},"done":{"type":"integer","format":"int32"},"total":{"description":"0 before a denominator is known; treat progress as indeterminate","type":"integer","format":"int32"},"pct":{"type":"integer","format":"int32"}},"required":["project_id","version","kind","done","total","pct"]},"Rio":{"description":"A project version's options and art. The viewer selects options from `Inputs`; every `Maps` entry whose `configuration` the selection satisfies is drawn, in `depth` order. Check one with `POST /projects/{project_id}/{version}/configuration/validate`.\n","type":"object","properties":{"Loader":{"description":"The name of the loader","type":"string","example":"RenderMap"},"Exporter":{"description":"The name of the exporter","type":"string","example":"Renderappio Visualiser"},"ExporterType":{"description":"The type of the exporter","type":"string","example":"projectnames to rio"},"CacheDir":{"description":"The cache directory","type":"string","example":"C:/Users/User/Desktop/Cache/"},"Inputs":{"description":"The categories, in display order. Default selections are made in this order, so list a category after the categories its options' Conditions refer to. When there are any, at least one option must be a view (`Type` other than `NotAView`).\n","type":"array","items":{"$ref":"#/components/schemas/Category"}},"Maps":{"description":"The art. Maps are linked to options only through their `configuration`.","type":"array","items":{"$ref":"#/components/schemas/Map"}},"viewpoints":{"description":"Viewpoints saved while reviewing","type":"array","items":{"$ref":"#/components/schemas/Viewpoint"}},"resolutions":{"description":"Available resolution tiers for this project","type":"array","items":{"$ref":"#/components/schemas/Resolution"}},"FastMode":{"description":"Whether legacy fastmode images exist for this project. Deprecated — new projects use the resolutions array instead.","type":"boolean","deprecated":true}},"required":["Inputs","Maps"],"if":{"properties":{"Inputs":{"minItems":1}},"required":["Inputs"]},"then":{"properties":{"Inputs":{"contains":{"type":"object","properties":{"Options":{"type":"array","contains":{"type":"object","properties":{"Type":{"not":{"const":"NotAView"}}},"required":["Type"]}}},"required":["Options"]}}}}},"ApprovalAction":{"type":"string","description":"A step in an approval's lifecycle","enum":["start_review","approve","request_changes","revert","reopen","cancel"]},"Approval":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"number":{"type":"integer","format":"int32","description":"Sequential approval number scoped to this project version (AR1, AR2, AR3...)"},"status":{"type":"string","enum":["pending","in_review","approved","changes_requested"]},"actions":{"type":"array","description":"What the caller can do to this request now. Empty when nothing is open to them.","items":{"$ref":"#/components/schemas/ApprovalAction"}},"requested_by":{"type":"string","format":"uuid"},"reviewer_id":{"type":"string","format":"uuid"},"requester_name":{"type":"string"},"reviewer_name":{"type":"string"},"created_at":{"type":"string","format":"date-time"},"updated_at":{"type":"string","format":"date-time"},"seen_views":{"type":"array","description":"The views the reviewer has marked seen in the current or last review.","items":{"type":"string"}},"events":{"type":"array","description":"Every transition of this request, oldest first.","items":{"$ref":"#/components/schemas/ApprovalEvent"}}},"required":["actions","id","number","status","requested_by","reviewer_id","requester_name","reviewer_name","created_at","updated_at","seen_views","events"]},"ApprovalEvent":{"type":"object","properties":{"kind":{"type":"string","enum":["requested","reviewing","review_cancelled","approved","changes_requested","reopened"]},"actor_name":{"type":"string"},"at":{"type":"string","format":"date-time"}},"required":["kind","actor_name","at"]},"ApprovalListItem":{"type":"object","description":"Approval enriched with project name and version metadata for the hub Review page","properties":{"id":{"type":"string","format":"uuid"},"number":{"type":"integer","format":"int32"},"status":{"type":"string","enum":["pending","in_review","approved","changes_requested"]},"actions":{"type":"array","description":"What the caller can do to this request now. Empty when nothing is open to them.","items":{"$ref":"#/components/schemas/ApprovalAction"}},"project_id":{"type":"string","format":"uuid"},"project_name":{"type":"string"},"filetype":{"type":"array","items":{"type":"string"},"nullable":true},"version":{"type":"integer","format":"int32"},"requested_by":{"type":"string","format":"uuid"},"reviewer_id":{"type":"string","format":"uuid"},"requester_name":{"type":"string"},"reviewer_name":{"type":"string"},"created_at":{"type":"string","format":"date-time"},"updated_at":{"type":"string","format":"date-time"}},"required":["actions","id","number","status","project_id","project_name","version","requested_by","reviewer_id","requester_name","reviewer_name","created_at","updated_at"]},"Category":{"description":"A set of options the viewer chooses between.","type":"object","properties":{"Type":{"description":"`Radio`: exactly one available option is selected. `Checkbox`: any number, including none.\n","type":"string","enum":["Radio","Checkbox"]},"Name":{"description":"Shown to viewers. Saved viewpoints and naming-scheme option refs name categories by it.","type":"string","example":"Paint"},"Group":{"description":"The category's id, unique in the rio: `+` and one token, like a FrontEndRule. Configurator settings are keyed by it (`settings.categories`). `+VEW`, `+VIEW` and `+VIEWS` mark the category holding the views.\n","type":"string","example":"+PAINT"},"stable_id":{"description":"Optional stable identity used by the version-compare diff. Authoring tools may emit this so a category survives Group or Name renames as the \"same\" category; older rios omit it and the diff falls back to content hash, position-in-tree, then Name / Group.","type":"string"},"Visibility":{"description":"`false` hides the category from viewers. Its options are still selected, by Defaults, so a hidden category fixes a choice.\n","type":"boolean","default":true},"Effect":{"description":"What choosing an option does. `configuration`: its BackEndRule joins the string Maps are matched against. The others never touch Maps, and their options' BackEndRule is not a rule: `material_variant`, the name of a glTF material variant to apply; `camera_preset`, a JSON camera (`{\"position\":{x,y,z},\"target\":{x,y,z},\"fov\":45}`); `visibility`, the name of a 3D model node, shown while the option is selected and hidden otherwise.\n","type":"string","enum":["configuration","material_variant","camera_preset","visibility"],"default":"configuration"},"Swatch":{"description":"Whether options in this category are displayed as colour swatches. Defaults to true for categories whose name contains \"paint\", \"colour\", or \"color\".","type":"boolean"},"Options":{"description":"The list of options for this category","type":"array","items":{"$ref":"#/components/schemas/Option"}}},"required":["Type","Name","Group","Options"]},"Option":{"description":"One choice in a category. Rule strings, used by Conditions and Map.configuration, are terms each starting with `+`, all of which must hold: `+SPORT+RED`. Within a term, `/` separates alternatives, any one of which holds it: `+RED/WHITE`. `!` before a token holds when that token is not selected: `+SPORT+!WHITE`.\n","type":"object","properties":{"Name":{"description":"Shown to viewers.","type":"string","example":"360 Spin"},"Type":{"description":"`NotAView` for an ordinary option. Anything else makes the option a view: selecting it picks the player (`Animation` a spin through Frames, `Still` one image, `Model` a glTF, and so on) and limits which Maps' frames show.\n","type":"string","enum":["NotAView","Still","Stills","Animation","Pano","Video","Model","Pdf","Exr","Audio","Text","Files"]},"FrontEndRule":{"description":"The option's id, unique in the rio: `+` and one token, with no `+`, `/` or `!` in the token. Conditions are matched against the FrontEndRules selected; configurator settings key options by it.\n","type":"string","example":"+RED"},"BackEndRule":{"description":"In a `configuration` category, what selecting the option adds to the string Maps are matched against: one or more `+` tokens, like `+PNT1+GLOSS`. Usually the same as FrontEndRule. In other categories it is the category Effect's value.\n","type":"string","example":"+PNT1+GLOSS"},"stable_id":{"description":"Optional stable identity used by the version-compare diff. Authoring tools may emit this so an option survives FrontEndRule or Name renames as the \"same\" option; older rios omit it and the diff falls back to content hash, position-in-tree, then Name / FrontEndRule.","type":"string"},"Defaults":{"description":"Non-empty marks the option as a default; the values themselves are not read. A Radio category starts on its first available option with Defaults, else its first available option. A Checkbox category starts with every available option that has Defaults ticked.\n","oneOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}],"example":["default"]},"Conditions":{"description":"When the option is available, as rule strings over the selected FrontEndRules. Any one string holding is enough; an empty list means always. An option whose Conditions stop holding is deselected.\n","type":"array","items":{"type":"string"},"example":["+SPORT+!WHITE","+TOUR+RED"]},"Implies":{"description":"FrontEndRules of options that are selected whenever this one is. Selecting this option selects them too, and deselecting one of them deselects this option.","type":"array","items":{"type":"string"},"example":["+UJZDE8G"]},"Frames":{"description":"On a view, the Map `frame` numbers it shows, in playback order. Without Frames a view shows every matching Map at once, whatever its frame.\n","type":"array","items":{"type":"integer","format":"int32"},"example":[0,1,2,3,4,5]},"Price":{"description":"Optional price for this option","type":"number","format":"double"},"Colour":{"description":"Optional manually-overridden swatch colour (CSS colour string, e.g. \"#FF5733\")","type":"string"},"Visibility":{"description":"Whether this option is visible","type":"boolean","default":true}},"required":["Name","Type","FrontEndRule","BackEndRule","Conditions"]},"CameraPreset":{"type":"object","properties":{"name":{"type":"string","description":"Human-readable preset name (e.g. \"Front\", \"Detail\")"},"position":{"type":"object","properties":{"x":{"type":"number","format":"double"},"y":{"type":"number","format":"double"},"z":{"type":"number","format":"double"}},"required":["x","y","z"]},"target":{"type":"object","properties":{"x":{"type":"number","format":"double"},"y":{"type":"number","format":"double"},"z":{"type":"number","format":"double"}},"required":["x","y","z"]},"fov":{"type":"number","format":"double","description":"Field of view in degrees (default 45)"}},"required":["name","position","target"]},"Map":{"description":"One stored file and when it is drawn.","type":"object","properties":{"configuration":{"description":"A rule string (see Option) matched against the BackEndRules of the selected options in `configuration` categories. The map is drawn while it holds. An empty string holds for every selection. Include the view's rule, or the map shows in every view.\n","type":"string","example":"+RED+EXT+!CARBON"},"depth":{"description":"Draw order among the maps drawn together. Higher draws on top.","type":"integer","format":"int32","example":3},"frame":{"description":"Which of the view's Frames the map belongs to. Absent means 0. When every view has Frames, a map on a frame none of them lists never shows.\n","type":"integer","format":"int32","example":0},"filename":{"description":"The file's path in the version, exactly as `GET .../files` lists it\n","type":"string","example":"03_Mid_Spec/Fuji White/vr_1_00.jpg"},"sha256":{"description":"SHA-256 of the file's bytes. The server fills it in on read once the file has been processed; a rio you write needn't carry it.\n","type":"string","pattern":"^[a-f0-9]{64}$"},"dhash":{"description":"64-bit perceptual hash of the image (9x8 grey, neighbour compare). The server fills it in on read once the file has been processed, and leaves it out for a file it couldn't decode; a rio you write needn't carry it.\n","type":"string","pattern":"^[a-f0-9]{16}$"},"thumbhash":{"description":"Base64-encoded ThumbHash bytes (~5-25 bytes raw). Compact placeholder\nfor \"blur up\" image loading: decode with thumbHashToDataURL at the\nclient. Sourced from the `versions_integrity` table; omitted when\nthe worker could not decode the file (non-image attachments).\n","type":"string"},"width":{"description":"Image width in pixels","type":"integer","format":"int32"},"height":{"description":"Image height in pixels","type":"integer","format":"int32"},"role":{"description":"What the file is for, when it isn't a picture to draw. A `matte` is never drawn: it is a cut-out of the product on its frame, white or opaque where the product is, from the renderer's alpha or matte pass. A Faithful generation pastes the render back through it so the product keeps its exact pixels.\n","type":"string","x-extensible-enum":["matte"],"example":"matte"},"resolutions":{"description":"Resolution tiers that have been processed for this map entry. The source of truth is the `version_map_resolution` table; this field is populated from it on read. Values stored in the rio document itself are a stale fallback, used only for versions with no rows. `source` marks a video whose upload browsers can play as it is.\n","type":"array","items":{"type":"string","enum":["source","lossless","retina","hd","compressed"]}},"failed":{"description":"Tiers whose conversion gave up after its retries, from the `version_map_failure` table. A tier listed here and missing from `resolutions` won't arrive unless the version is processed again; `source` means a video couldn't be probed at all.\n","type":"array","items":{"type":"string","enum":["source","lossless","retina","hd","compressed"]}},"revision":{"description":"Changes whenever the stored source file does: its ETag in the version's file listing. Clients put it in the cache key of the source tier's URL, so a replaced file is never served from cache. Absent until the version's files are listed.\n","type":"string","example":"9e107d9d372bb6826bd81d3542a419d6"},"resolution_revisions":{"description":"Per processed tier, a token that changes whenever that tier's rendition is written again (a replaced source, or reprocessing). The cache key for that tier's URL, as `revision` is for the source.\n","type":"object","additionalProperties":{"type":"string"},"example":{"hd":"lrm4k2x1"}}},"required":["configuration","depth","filename"]},"SuccessResponse":{"type":"object","properties":{"success":{"description":"Success indicator","type":"boolean","example":true}},"required":["success"]},"FileChange":{"type":"object","properties":{"op":{"type":"string","enum":["add","replace","delete","move"],"example":"move"},"path":{"description":"The file the change applies to; for a move, where it moves from","type":"string","minLength":1,"example":"paint/tasman_blue/f34.png"},"to":{"description":"Where a move puts the file","type":"string","minLength":1,"example":"paint/tasman_blue_2027/f34.png"}},"required":["op","path"]},"VersionDraft":{"type":"object","properties":{"mode":{"type":"string","enum":["upload","edit"],"example":"upload"},"base_version":{"description":"The version an upload draft's files start from","type":"integer","format":"int32","example":12},"changes":{"type":"array","items":{"$ref":"#/components/schemas/FileChange"}},"rio":{"description":"The options and maps applying writes, when they were rebuilt from the files","allOf":[{"$ref":"#/components/schemas/Rio"}]},"apply":{"description":"An edit draft's last `draft/apply`, once one has started. The draft is removed when its apply finishes, so a 404 from `GET …/draft` after applying means it was applied. While `applying` is false and this is here, the apply stopped: `failed_reason` says why, and the draft can be changed and applied again. An upload draft is removed as its apply starts, so it never carries this.\n","type":"object","properties":{"job_id":{"type":"string","format":"uuid"},"failed_reason":{"type":"string"}},"required":["job_id"]},"updated_at":{"type":"string","format":"date-time","example":"2026-09-16T12:00:00Z"},"applying":{"description":"Being applied now. The draft is removed when the apply finishes, and stays, able to be changed and applied again, if it fails. Only reading a draft says.\n","type":"boolean","example":false}},"required":["mode","changes","updated_at"]},"SaveVersionDraftRequest":{"type":"object","properties":{"mode":{"type":"string","enum":["upload","edit"],"example":"upload"},"base_version":{"type":"integer","format":"int32","minimum":1,"example":12},"changes":{"type":"array","items":{"$ref":"#/components/schemas/FileChange"}},"rio":{"description":"The options and maps to write when the draft is applied, rebuilt from the files the changes leave. Every map must name one of those files. Without it, applying keeps the current rio with deleted files' maps removed and moved files' maps renamed, and added files are drawn nowhere. Saving a draft without it clears one saved earlier.\n","allOf":[{"$ref":"#/components/schemas/Rio"}]}},"required":["mode","changes"]},"ApplyVersionDraftRequest":{"type":"object","properties":{"version_description":{"type":"string","example":"Gloss wheels and a re-rendered Tasman Blue"},"override_feedback":{"description":"Apply an in-place edit even though comments or approvals are on files it changes","type":"boolean","example":false}}},"ApplyVersionDraftResponse":{"type":"object","properties":{"version":{"type":"integer","format":"int32","example":13},"job_id":{"type":"string","format":"uuid","example":"0191f1c2-6a3b-7c4d-8e5f-60718293a4b5"}},"required":["version","job_id"]},"NamingScheme":{"description":"How file names map to options, views and frames. `configuration/generate` builds a rio from it: one Radio category per category name (Checkbox if listed in `checkboxes`), one option per value found, a `View` category when names carry a view or frame, and a map per placed file. Rules are hashes of the category and option names, so they are the same on every run until a name changes.\n","type":"object","properties":{"source":{"description":"Split the file name alone (`name`), or its folders and name (`path`). The extension is dropped first either way.\n","type":"string","enum":["name","path"],"example":"name"},"split":{"$ref":"#/components/schemas/NamingSchemeSplit"},"include":{"description":"Globs a path must match to be placed by the scheme or any of its layers. A path outside them is listed in `skipped`. To narrow one layer, give that layer its own `include` instead. Globs follow picomatch with dotfiles matched, and `**/` matches no folders as well as many: `**/Thumbs.db` catches a `Thumbs.db` at the top too.\n","type":"array","items":{"type":"string"},"example":["renders/**"]},"exclude":{"description":"Globs that leave a path out of the scheme and every layer; listed in `skipped`","type":"array","items":{"type":"string"},"example":["**/*_wip.*"]},"structure":{"description":"`nested` makes each option available only under the options of earlier categories it was found with; `flat`, the default, offers every option everywhere.\n","type":"string","enum":["flat","nested"]},"view":{"description":"The name of the view files are placed in when their names don't say one, and a type every view takes instead of the one its files' extensions suggest\n","type":"object","additionalProperties":false,"properties":{"name":{"type":"string","minLength":1,"example":"Spin"},"type":{"type":"string","enum":["Still","Stills","Animation","Pano","Video","Model","Pdf","Exr","Audio","Text","Files"],"example":"Animation"}}},"layers":{"description":"Further matchers, tried in order after the scheme's own. A path goes to the first that matches it.\n","type":"array","maxItems":20,"items":{"$ref":"#/components/schemas/NamingSchemeLayer"}},"checkboxes":{"description":"Categories offered as checkboxes, any number selected, instead of one choice","type":"array","items":{"type":"string","minLength":1},"example":["Accessories"]},"hidden_by":{"description":"The scheme's own files are hidden while any of these options is selected","type":"array","items":{"$ref":"#/components/schemas/NamingSchemeOptionRef"}},"hide_single_options":{"description":"Hide categories that offer only one choice, such as the one model or grade every file shares, from the viewer's sidebar. Checkbox categories stay, since one option there is still something to tick.\n","type":"boolean","example":true}},"required":["source","split"]},"NamingSchemeLayer":{"description":"Files drawn at their own depth, such as paint or accessories over a base render. A layer has its own `source`, `split` and optional `include`, and only the scheme's top-level `include` and `exclude` apply to it besides. Groups that name the same category in the scheme and its layers fill one category, so a colour folder in every layer becomes one paint choice.\n","type":"object","additionalProperties":false,"properties":{"name":{"type":"string","example":"Accessories"},"source":{"type":"string","enum":["name","path"],"example":"path"},"split":{"$ref":"#/components/schemas/NamingSchemeSplit"},"include":{"description":"Globs a path must match for this layer to take it","type":"array","items":{"type":"string"},"example":["Accessories/**"]},"depth":{"description":"The tier the layer draws in. The scheme's own files are tier 0. A map's `depth` in the generated rio is the tier × 100 plus the number of options its configuration names, not counting the view, so every tier draws over the ones below it.\n","type":"integer","format":"int32","minimum":0,"maximum":1000,"example":1},"hidden_by":{"description":"The layer's files are hidden while any of these options is selected","type":"array","items":{"$ref":"#/components/schemas/NamingSchemeOptionRef"}},"role":{"description":"`matte` makes the layer's files the product's cut-outs (alpha or matte passes) rather than pictures to draw. Its maps carry the role and the viewer never draws them.\n","type":"string","enum":["matte"],"example":"matte"}},"required":["source","split","depth"]},"NamingSchemeOptionRef":{"type":"object","additionalProperties":false,"properties":{"category":{"type":"string","minLength":1,"example":"Accessories"},"option":{"type":"string","minLength":1,"example":"RoofRack"}},"required":["category","option"]},"NamingSchemeSplit":{"description":"How a file name or path is split into parts","oneOf":[{"type":"object","additionalProperties":false,"properties":{"delimiters":{"type":"array","items":{"type":"string","minLength":1},"example":["_"]},"tokens":{"description":"What each part of the split name is, in order","type":"array","items":{"$ref":"#/components/schemas/NamingSchemeToken"}}},"required":["delimiters","tokens"]},{"type":"object","additionalProperties":false,"properties":{"pattern":{"description":"A regular expression with named groups. `category` and `option` name one option, `view` and `frame` place the file, and any other group name is a category whose option is the group's value.\n","type":"string","minLength":1,"maxLength":1000,"example":"^(?<Model>[a-z]+)_(?<Paint>[a-z]+)_(?<view>[a-z]+)$"},"names":{"description":"The category a group fills, for categories whose names can't be group names, such as ones with spaces. An empty name leaves the group's own. Option names are the text the group matched; to show another name, change the option's `Name` in the generated rio before saving. Its rules stay the same.\n","type":"object","additionalProperties":{"type":"string"},"example":{"Body_Paint":"Body Paint"}}},"required":["pattern"]}]},"NamingSchemeToken":{"type":"object","additionalProperties":false,"properties":{"role":{"description":"`category`: the part names the category of the next `option` part. `option`: the part is an option of `category`, or of the category part before it. `view`: the part names the view. `frame`: the part is the frame; a view's frame values are sorted naturally and numbered from 1. `ignore`: the part is skipped.\n","type":"string","enum":["ignore","category","option","view","frame"],"example":"option"},"category":{"description":"The category an option part belongs to. Without it, the category part before it names one.","type":"string","minLength":1,"example":"Paint"}},"required":["role"]},"SavedNamingScheme":{"type":"object","properties":{"scheme":{"$ref":"#/components/schemas/NamingScheme"},"updated_at":{"type":"string","format":"date-time","example":"2026-09-17T12:00:00Z"}},"required":["scheme","updated_at"]},"EvaluatedConfiguration":{"type":"object","additionalProperties":false,"properties":{"selected":{"description":"The options chosen once every choice has been made, in the rio's order","type":"array","items":{"type":"object","additionalProperties":false,"properties":{"category":{"type":"string","example":"Wheels"},"option":{"type":"string","example":"20-inch forged"},"rule":{"description":"The option's FrontEndRule","type":"string","example":"+W20"}},"required":["category","option","rule"]}},"parked":{"description":"Checkbox options chosen earlier but held back because a later choice broke their Conditions. They aren't selected or drawn, and the viewer ticks them again as soon as their Conditions hold, so they aren't in `dropped`.\n","type":"array","items":{"type":"string"}},"dropped":{"description":"Choices that aren't chosen at the end, and why","type":"array","items":{"type":"object","additionalProperties":false,"properties":{"rule":{"description":"The choice's FrontEndRule","type":"string","example":"+MATTE"},"reason":{"type":"string","example":"Its Conditions don't hold with the other choices"}},"required":["rule","reason"]}},"frames":{"description":"Per frame of the chosen view, the files drawn, bottom layer first. Empty when the choices draw nothing, which the viewer shows as an incompatible configuration.\n","type":"array","items":{"type":"object","additionalProperties":false,"properties":{"frame":{"type":"integer","format":"int32"},"files":{"type":"array","items":{"type":"string"}}},"required":["frame","files"]}}},"required":["selected","parked","dropped","frames"]},"GeneratedConfiguration":{"type":"object","properties":{"rio":{"description":"Absent when `save_draft` was sent","allOf":[{"$ref":"#/components/schemas/Rio"}]},"inputs":{"description":"The rio's `Inputs`, when `save_draft` was sent and the rio itself is left out: its categories and options, with the `FrontEndRule`s to evaluate choices by\n","type":"array","items":{"$ref":"#/components/schemas/Category"}},"draft":{"description":"The draft the rio was saved to, when `save_draft` was sent and it was saved","type":"object","properties":{"maps":{"description":"How many maps the saved rio has","type":"integer","format":"int32"},"updated_at":{"type":"string","format":"date-time"}},"required":["maps","updated_at"]},"unmatched":{"description":"Files the scheme and its layers tried and couldn't place, with what to change","type":"array","items":{"type":"object","properties":{"path":{"type":"string","example":"renders/brief.pdf"},"reason":{"type":"string","example":"Splits into 2 parts, the scheme expects 4 — give one token per part, or exclude the file"}},"required":["path","reason"]}},"skipped":{"description":"Files the scheme's `include` or `exclude` left out, so nothing tried to place them. Check it when the rio has fewer maps than you expected.\n","type":"array","items":{"type":"object","properties":{"path":{"type":"string","example":"02_Paint/Fuji White/vr_1_00.jpg"},"reason":{"type":"string","example":"Matches the scheme's exclude globs"}},"required":["path","reason"]}},"conflicts":{"description":"Files placed in the same configuration and frame at the same depth, so which one is drawn is undefined. Files at different depths are layers drawn one over another, and aren't conflicts.\n","type":"array","items":{"type":"object","properties":{"configuration":{"type":"array","items":{"type":"string"},"example":["+a1b2c3","+d4e5f6"]},"frame":{"type":"integer","format":"int32","example":0},"paths":{"type":"array","items":{"type":"string"},"example":["renders/gr_red_front.png","renders/gr_red_front.jpg"]}},"required":["configuration","frame","paths"]}},"findings":{"description":"What in the built rio can never work, as from configuration/validate","type":"array","items":{"$ref":"#/components/schemas/RioFinding"}}},"required":["unmatched","skipped","conflicts","findings"]},"RioFinding":{"type":"object","properties":{"severity":{"description":"An `error` finding stops a save; a `warning` doesn't.","type":"string","enum":["error","warning"]},"kind":{"type":"string","enum":["dead-option","empty-category","unknown-rule","condition-cycle","unreferenced-map-rule","unreachable-frame"]},"message":{"type":"string","example":"\"Gloss Black\" can never be selected — no configuration satisfies its Conditions. Make at least one Conditions entry satisfiable by options that can be selected, or empty its Conditions."},"category":{"description":"The category's id","type":"string"},"option":{"description":"The option's FrontEndRule","type":"string","example":"+a1b2c3"},"rules":{"type":"array","items":{"type":"string"}},"affected_maps":{"description":"How many maps the finding leaves unrendered","type":"integer","format":"int32"}},"required":["severity","kind","message"]},"ConfigurationValidation":{"type":"object","properties":{"findings":{"type":"array","items":{"$ref":"#/components/schemas/RioFinding"}},"missing_files":{"description":"Files maps name that the version doesn't hold, or that aren't in `files` when it was sent. Absent when neither was available to check against.\n","type":"array","items":{"type":"string"},"example":["renders/gr_red_front.png"]}},"required":["findings"]},"ConfiguratorTheme":{"description":"A theme definition. Settings are typed by `type` (colour, select, toggle, range, text, richText, image, font) and carry their own limits and `help`.\n","type":"object","additionalProperties":true,"properties":{"id":{"type":"string","example":"automotive-generic"},"version":{"type":"integer","format":"int32"},"label":{"type":"string"}},"required":["id","version","label"]},"LayoutValidation":{"type":"object","properties":{"issues":{"type":"array","items":{"type":"object","properties":{"severity":{"type":"string","enum":["error","warning"]},"code":{"description":"What is wrong at `path`, and the fix. Setting rules come from the theme's\ndefinition (`GET /configurator-themes/{theme_id}`).\n- `schema-version`: set `schemaVersion` to 1.\n- `theme-mismatch`, `theme-version-ahead`: set `theme` to the theme's `id` and `version`.\n- `theme-version-behind`: written for an older version of the theme; set `theme.version` to the theme's.\n- `unknown-key`, `unknown-component`, `unknown-type`: the theme has no such setting, component or option type (`collapsible`, `static`, `multi`, `single`, or two joined by `+`); remove it or fix its id. An `unknown-key` that is the end of a key the theme has carries that key as `suggestion`.\n- `required-component-hidden`: the component is `required`; drop `visible: false`.\n- `not-boolean`, `not-number`, `not-text`: send the setting's type (`toggle`, `range`, `text`).\n- `not-in-options`: use one of the select setting's `options[].value`.\n- `out-of-range`: keep within the setting's `min`/`max`, narrowed by the component's `limits`.\n- `too-long`: shorten to the setting's `maxLength`.\n- `bad-rich-text`: send `{ text, marks }`, marks ordered, non-overlapping, `start` < `end` ≤ text length.\n- `bad-colour`: a colour setting is `{ role }`, `{ hex: \"#rrggbb\" }` or `{ transparent: true }`; a palette entry is `\"#rrggbb\"`.\n- `role-not-allowed`, `custom-colour-not-allowed`: use a role from the setting's `roles`; `hex` only where `allowCustom` is true.\n- `bad-font`, `font-not-allowed`: a font is `{ role: \"heading\" | \"body\" }` or `{ fontId }` from the setting's `allowed`; `brand.fonts` takes ids from `fonts.registry`.\n- `bad-asset`: send `{ assetId }` or null.\n- `not-movable`, `unknown-slot`, `slot-not-allowed`, `slot-inside-itself`, `slot-full`: place only `movable` components, in a slot that `accepts` their `kind`, is in `movable.only` if set, isn't inside them, and is under its `capacity`.\n- `low-contrast`: text contrast is under 4.5:1; change a palette colour.\n","type":"string","example":"slot-not-allowed"},"path":{"description":"Dotted path into the document","type":"string","example":"layout.placements.summary"},"suggestion":{"description":"For `unknown-key`, the one setting key the theme has that ends with the key sent, such as `optionBox.selected.colour` for `selected.colour`\n","type":"string"}},"required":["severity","code","path"]}}},"required":["issues"]},"ConfiguratorOp":{"description":"`set` writes a value; `remove` deletes a key or list item (already gone counts as done); `insert` adds `value` to the list the path names, after the item whose id is `after` (null for first, absent or gone for last); `move` moves the list item the path names after `after`, into the list at `to` when given.\n","type":"object","properties":{"op":{"type":"string","enum":["set","remove","insert","move"]},"path":{"type":"array","minItems":1,"maxItems":32,"items":{"oneOf":[{"type":"string"},{"type":"object","properties":{"id":{"type":"string"}},"required":["id"],"additionalProperties":false}]}},"value":{},"after":{"type":"string","nullable":true},"to":{"type":"array","minItems":1,"maxItems":32,"items":{"oneOf":[{"type":"string"},{"type":"object","properties":{"id":{"type":"string"}},"required":["id"],"additionalProperties":false}]}}},"required":["op","path"],"additionalProperties":false},"LayoutRefusal":{"allOf":[{"$ref":"#/components/schemas/Problem"},{"$ref":"#/components/schemas/LayoutValidation"}]},"ConfigurationRefusal":{"allOf":[{"$ref":"#/components/schemas/Problem"},{"type":"object","properties":{"findings":{"type":"array","items":{"$ref":"#/components/schemas/RioFinding"}}},"required":["findings"]}]},"LayoutDocument":{"description":"A configurator's layout settings. Keys under each `values` are the ids of the theme's settings at that level (see GET /configurator-themes/{theme_id}).\n","type":"object","properties":{"schemaVersion":{"type":"integer","format":"int32","example":1},"theme":{"type":"object","properties":{"id":{"type":"string","example":"automotive-generic"},"version":{"type":"integer","format":"int32"}},"required":["id","version"]},"brand":{"type":"object","properties":{"palette":{"type":"object","properties":{"light":{"$ref":"#/components/schemas/LayoutPalette"},"dark":{"$ref":"#/components/schemas/LayoutPalette"}}},"fonts":{"type":"object","properties":{"heading":{"type":"string"},"body":{"type":"string"},"custom":{"description":"Fonts uploaded for this configurator, up to six. Their `id` can be used as `heading` or `body`. Each file's `assetId` is the `font_key` from `GET /configurators/{id}/theme-font-upload-url`.\n","type":"array","maxItems":6,"items":{"type":"object","properties":{"id":{"description":"Lowercase letters, digits and hyphens, up to 40 characters","type":"string","pattern":"^[a-z0-9-]{1,40}$"},"family":{"description":"The name shown in the font picker, up to 40 characters","type":"string","minLength":1,"maxLength":40},"files":{"type":"array","minItems":1,"maxItems":3,"items":{"type":"object","properties":{"weight":{"description":"400, 500 or 600","type":"integer","format":"int32"},"assetId":{"type":"string"}},"required":["weight","assetId"]}}},"required":["id","family","files"]}}}},"values":{"$ref":"#/components/schemas/LayoutValues"}},"required":["palette","fonts","values"]},"layout":{"type":"object","properties":{"values":{"$ref":"#/components/schemas/LayoutValues"},"placements":{"description":"Slot id by component id, for components that can move","type":"object","additionalProperties":{"type":"string"}}},"required":["values"]},"components":{"description":"Settings by component id","type":"object","additionalProperties":{"type":"object","properties":{"values":{"$ref":"#/components/schemas/LayoutValues"}},"required":["values"]}},"options":{"type":"object","properties":{"scopes":{"type":"object","properties":{"configurator":{"$ref":"#/components/schemas/LayoutValues"},"types":{"description":"Settings by option type: collapsible, static, multi, single, or a collapse and a choice joined by +, e.g. collapsible+single\n","type":"object","additionalProperties":{"$ref":"#/components/schemas/LayoutValues"}}}}},"required":["scopes"]}},"required":["schemaVersion","theme","brand","layout","components","options"]},"LayoutPalette":{"description":"Hex colours by role. Roles left out are derived from the three required.","type":"object","additionalProperties":false,"properties":{"primary":{"type":"string","example":"#0a84ff"},"secondary":{"type":"string"},"tertiary":{"type":"string"},"background":{"type":"string"},"surface":{"type":"string"},"text":{"type":"string"},"textMuted":{"type":"string"},"border":{"type":"string"},"onPrimary":{"type":"string"}},"required":["primary","secondary","tertiary"]},"LayoutValues":{"description":"Setting values by setting id. What each value may be depends on the setting's `type` in the theme, so validate checks it against the theme rather than the schema: toggle → boolean; select → one of its option values; range → number; text → string; richText → string, or { text, marks: [{ start, end, colour }] }; colour → { role }, { hex } or { transparent: true }; font → { role: heading | body } or { fontId }; image → { assetId } or null.\n","type":"object","additionalProperties":{}},"VersionFileHistory":{"type":"object","properties":{"path":{"description":"The file's path, as asked for","type":"string","example":"paint/tasman_blue/f34.png"},"versions":{"description":"The versions recording a file at this path, oldest first","type":"array","items":{"type":"object","properties":{"version":{"type":"integer","format":"int32","example":2},"size":{"description":"Size in bytes","type":"integer","format":"int64","example":7340032},"etag":{"description":"What the manifest recorded for the stored object: its S3 ETag, or a placeholder for a file overwritten since the last listing. Equal values mean the same bytes.\n","type":"string","example":"9e107d9d372bb6826bd81d3542a419d6"},"last_modified":{"description":"When the stored object was last written","type":"string","format":"date-time","example":"2026-09-16T11:58:02Z"}},"required":["version","size"]}}},"required":["path","versions"]},"VersionFiles":{"type":"object","properties":{"listed_at":{"description":"When this listing was taken","type":"string","format":"date-time","example":"2026-09-16T12:00:00Z"},"files":{"type":"array","items":{"type":"object","properties":{"path":{"description":"Path relative to the upload root, the same form as Maps[].filename","type":"string","example":"paint/tasman_blue/f34.png"},"size":{"description":"Size in bytes","type":"integer","format":"int64","example":7340032},"last_modified":{"description":"When the stored object was last written","type":"string","format":"date-time","example":"2026-09-16T11:58:02Z"},"etag":{"description":"The object's S3 ETag, without quotes: the MD5 of its bytes for a single-part upload, or `<md5 of the part MD5s>-<parts>` for a multipart one\n","type":"string","example":"9e107d9d372bb6826bd81d3542a419d6"},"state":{"type":"string","enum":["used","unused"],"example":"used"}},"required":["path","size","state"]}},"missing":{"description":"Map filenames the rio draws that are not in storage","type":"array","items":{"type":"string","example":"calipers/red/side.png"}}},"required":["listed_at","files","missing"]},"ResolutionTier":{"type":"string","enum":["source","lossless","retina","hd","compressed","fastmode"],"description":"Resolution tier for file retrieval. source is the raw uploaded file; lossless is the original-quality WebP re-encode; hd, compressed, retina, and fastmode are derived resolution tiers generated by the image worker."},"Resolution":{"type":"object","properties":{"width":{"description":"Width in pixels","type":"integer","format":"int32"},"height":{"description":"Height in pixels","type":"integer","format":"int32"},"isOriginal":{"description":"Whether this is the original source resolution","type":"boolean"},"tier":{"description":"The named tier this resolution reads from. `source` is the raw upload — the top row for media with no lossless derivative (video, EXR). `lossless` is the original-quality WebP the image worker writes; the rest are its downscales.","type":"string","enum":["source","lossless","retina","hd","compressed"]}},"required":["width","height","isOriginal"]},"GenerationKind":{"type":"string","x-extensible-enum":["image","video","text"],"description":"What a generation makes. Other kinds may join these, so treat an unknown one as something this client cannot show."},"GenerationCapability":{"type":"object","properties":{"enabled":{"type":"boolean","description":"Whether anything can be generated in this project"},"kinds":{"type":"array","description":"The kinds the project can generate. Empty when generation is not enabled.","items":{"$ref":"#/components/schemas/GenerationKind"}},"models":{"type":"array","description":"The models a generation can ask for. Empty when generation is not enabled.","items":{"$ref":"#/components/schemas/GenerationModel"}},"rules":{"$ref":"#/components/schemas/GenerationRules"}},"required":["enabled","kinds","models","rules"]},"GenerationRules":{"type":"object","description":"A workspace's brand rules, kept to by every generation in it. Each is empty when unset.","properties":{"legal":{"type":"string","description":"Rules every scene obeys, such as the road rules a driving shot keeps to","example":"No red lights; indicate before changing lane."},"avoid":{"type":"string","description":"What every result must be free of","example":"readable number plates, other brands' badges"}},"required":["legal","avoid"]},"GenerationModel":{"type":"object","properties":{"id":{"type":"string","description":"What to send as `model`","example":"gemini-3-pro-image"},"label":{"type":"string","example":"Nano Banana Pro"},"kind":{"$ref":"#/components/schemas/GenerationKind"},"max_references":{"type":"integer","format":"int32","description":"How many `reference_keys` it takes","example":13},"default":{"type":"boolean","description":"Whether it is the one to offer first for its kind"},"durations":{"type":"array","description":"For a video model, the clip lengths it makes, in seconds","items":{"type":"integer","format":"int32"},"example":[4,6,8]},"resolutions":{"type":"array","description":"For a video model, the output resolutions it makes","items":{"$ref":"#/components/schemas/VideoResolution"}},"preview":{"type":"boolean","description":"Whether it is a preview model, with access set up outside renderapp"},"note":{"type":"string","description":"What a preview model needs before it works, to show beside it"},"supports":{"$ref":"#/components/schemas/GenerationModelSupports"}},"required":["id","label","kind","max_references","default","supports"]},"GenerationModelSupports":{"type":"object","description":"Which settings the model acts on. A setting it doesn't support is accepted and not sent to it, so a client should say so next to the control rather than leave it to look applied.","properties":{"seed":{"type":"boolean","description":"Whether `seed` makes a take repeatable"},"fidelity":{"type":"boolean","description":"Whether `fidelity` changes how the model samples"},"negative_prompt":{"type":"boolean","description":"Whether `fields.avoid` goes to the model as a negative prompt. When false it is written into the prompt as what the result is free of."},"end_frame":{"type":"boolean","description":"Whether a clip can be asked to end on `last_frame_key`"},"audio":{"type":"boolean","description":"Whether a clip can be made with sound"},"extend":{"type":"boolean","description":"Whether its clips can be extended with `POST /generations/{project_id}/{version}/{generation_id}/extend`"}},"required":["seed","fidelity","negative_prompt","end_frame","audio","extend"]},"GenerationFidelity":{"type":"string","x-extensible-enum":["faithful","balanced","creative"],"description":"How far a take may stray from its source frame"},"GenerationFields":{"type":"object","additionalProperties":false,"description":"A recipe in parts. Each may hold `{Category}` tokens, filled from the configuration the generation is made from: `{Body Paint}` becomes the selected paint's name.","properties":{"subject":{"type":"string","maxLength":2000,"description":"What the product is and which details must stay exactly as rendered","example":"The {Grade} in {Body Paint}, badges, grille and wheels exactly as rendered."},"change":{"type":"string","maxLength":2000,"description":"What to change","example":"Place the {Body Paint} car on a canyon road at golden hour."},"environment":{"type":"string","maxLength":2000,"example":"Narrow sandstone canyon, unpaved track, warm low sun from camera left."},"style":{"type":"string","maxLength":2000,"example":"Editorial automotive photography, restrained saturation."},"legal":{"type":"string","maxLength":2000,"description":"Rules the scene must obey, such as the road rules a driving shot keeps to","example":"Stays in its lane on the correct side of the road; no red lights."},"avoid":{"type":"string","maxLength":2000,"example":"Aftermarket parts, distorted badges, warped wheels."},"framing":{"type":"string","maxLength":2000,"description":"How the camera may change. Left out, the camera, lens and framing stay identical to the source frame.","example":"Pull back to a wider shot."},"motion":{"type":"string","maxLength":2000,"description":"For a video, what moves and how the camera moves","example":"The camera orbits slowly to the right around the parked car."},"notes":{"type":"string","maxLength":2000,"description":"Anything else, added as written"},"change_original":{"type":"string","maxLength":2000,"description":"What was written before Enhance rewrote `change`. Not sent to the model; kept so the recipe can be restored as written."}}},"GenerationTags":{"type":"array","description":"Labels that group takes, such as the round or the reel they are for","maxItems":20,"items":{"type":"string","minLength":1,"maxLength":40},"example":["land-cruiser","canyon-reel"]},"GenerationCounts":{"type":"object","description":"How many takes each status tab holds, for the listing's other filters","properties":{"all":{"type":"integer","format":"int32","description":"Every take that isn't hidden"},"pending":{"type":"integer","format":"int32"},"complete":{"type":"integer","format":"int32"},"failed":{"type":"integer","format":"int32"},"kept":{"type":"integer","format":"int32"},"hidden":{"type":"integer","format":"int32"}},"required":["all","pending","complete","failed","kept","hidden"]},"GenerationTemplateSettings":{"type":"object","additionalProperties":false,"properties":{"model":{"type":"string","example":"gemini-3-pro-image"},"takes":{"type":"integer","format":"int32","minimum":1,"maximum":4},"fidelity":{"$ref":"#/components/schemas/GenerationFidelity"},"location_id":{"type":["string","null"],"format":"uuid","description":"The location the recipe uses. Loading the recipe chooses it, so the place's description and anchors come from the location as it is now."}}},"GenerationTemplate":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"workspace_id":{"type":"string","format":"uuid"},"kind":{"$ref":"#/components/schemas/GenerationKind"},"name":{"type":"string","example":"Canyon hero"},"fields":{"$ref":"#/components/schemas/GenerationFields"},"settings":{"$ref":"#/components/schemas/GenerationTemplateSettings"},"created_by":{"type":"string","format":"uuid"},"created_by_name":{"type":["string","null"]},"created_at":{"type":"string","format":"date-time"},"updated_at":{"type":"string","format":"date-time"}},"required":["id","workspace_id","kind","name","fields","settings","created_by","created_by_name","created_at","updated_at"]},"GenerationTemplateUpdate":{"type":"object","additionalProperties":false,"properties":{"name":{"type":"string","minLength":1,"maxLength":120},"fields":{"$ref":"#/components/schemas/GenerationFields"},"settings":{"$ref":"#/components/schemas/GenerationTemplateSettings"}},"required":["name","fields","settings"]},"GenerationTemplateCreate":{"type":"object","additionalProperties":false,"properties":{"workspace_id":{"type":"string","format":"uuid"},"kind":{"$ref":"#/components/schemas/GenerationKind"},"name":{"type":"string","minLength":1,"maxLength":120},"fields":{"$ref":"#/components/schemas/GenerationFields"},"settings":{"$ref":"#/components/schemas/GenerationTemplateSettings"}},"required":["workspace_id","kind","name","fields","settings"]},"GenerationLocation":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"workspace_id":{"type":"string","format":"uuid"},"name":{"type":"string","example":"Coastal Road"},"slug":{"type":"string","example":"coastal_road"},"category":{"type":["string","null"],"example":"Coastal"},"description":{"type":["string","null"],"example":"Empty coastal road at first light; navy sky lifting at the horizon."},"fields":{"$ref":"#/components/schemas/GenerationFields"},"anchors":{"type":"array","description":"The stills sent as references wherever this location is chosen","items":{"type":"object","properties":{"key":{"type":"string","description":"The stored still. Choosing the location sends it; don't name it in `reference_keys`."},"url":{"type":["string","null"],"description":"A link to the still. Short-lived for a stored still; a fixed path for a built-in one."},"preview_url":{"type":["string","null"],"description":"A 640 px copy for thumbnails, where one exists (built-in stills)"}},"required":["key","url"]}},"references":{"type":"array","description":"Uploaded images of the place that every round of candidates sends to the model","items":{"type":"object","properties":{"key":{"type":"string"},"url":{"type":["string","null"],"description":"A short-lived link to the image"}},"required":["key","url"]}},"usage":{"type":"object","description":"The takes made here that aren't in the trash, and how many projects they span","properties":{"shots":{"type":"integer","format":"int32"},"projects":{"type":"integer","format":"int32"}},"required":["shots","projects"]},"builtin":{"type":"boolean","description":"One of the instance's built-in locations, listed in every workspace. It can't be changed; duplicate it to make an editable copy."},"created_by":{"type":["string","null"],"format":"uuid","description":"Null for a built-in location"},"created_by_name":{"type":["string","null"]},"created_at":{"type":"string","format":"date-time"},"updated_at":{"type":"string","format":"date-time"}},"required":["id","workspace_id","name","slug","category","description","fields","anchors","references","usage","builtin","created_by","created_by_name","created_at","updated_at"]},"GenerationLocationRound":{"type":"object","properties":{"batch_id":{"type":"string","format":"uuid"},"model":{"type":"string"},"prompt":{"type":"string","description":"The prompt every candidate in the round was made from"},"created_by_name":{"type":["string","null"]},"created_at":{"type":"string","format":"date-time"},"candidates":{"type":"array","items":{"$ref":"#/components/schemas/GenerationLocationCandidate"}}},"required":["batch_id","model","prompt","created_by_name","created_at","candidates"]},"GenerationLocationCandidate":{"type":"object","properties":{"id":{"type":"string","format":"uuid","description":"What to send in `generation_ids` to promote it"},"status":{"type":"string","enum":["pending","complete","failed"]},"seed":{"type":["integer","null"],"format":"int32"},"url":{"type":["string","null"],"description":"A short-lived link to the still, or null until it is complete"},"key":{"type":["string","null"],"description":"Where the still is stored, as a location's `anchors` name it; null until it is complete"},"width":{"type":["integer","null"],"format":"int32"},"height":{"type":["integer","null"],"format":"int32"}},"required":["id","status","seed","url","key","width","height"]},"GenerationLocationUpdate":{"type":"object","additionalProperties":false,"properties":{"name":{"type":"string","minLength":1,"maxLength":120},"slug":{"type":"string","pattern":"^[a-z0-9]+(_[a-z0-9]+)*$","maxLength":120},"category":{"type":["string","null"],"maxLength":60},"description":{"type":["string","null"],"maxLength":2000},"fields":{"$ref":"#/components/schemas/GenerationFields"}},"required":["name","slug","category","description","fields"]},"GenerationLocationCreate":{"type":"object","additionalProperties":false,"properties":{"workspace_id":{"type":"string","format":"uuid"},"name":{"type":"string","minLength":1,"maxLength":120},"slug":{"type":"string","pattern":"^[a-z0-9]+(_[a-z0-9]+)*$","maxLength":120,"description":"Made from the name when left out"},"category":{"type":["string","null"],"maxLength":60},"description":{"type":["string","null"],"maxLength":2000},"fields":{"$ref":"#/components/schemas/GenerationFields"},"from_location_id":{"type":"string","format":"uuid","description":"Duplicate this location (one of the workspace's, or a built-in): the new one starts with its anchors and references."}},"required":["workspace_id","name","fields"]},"GenerationRequest":{"description":"What to generate. `kind` picks the shape.","oneOf":[{"$ref":"#/components/schemas/ImageGenerationRequest"},{"$ref":"#/components/schemas/VideoGenerationRequest"},{"$ref":"#/components/schemas/TextGenerationRequest"}],"discriminator":{"propertyName":"kind","mapping":{"image":"#/components/schemas/ImageGenerationRequest","video":"#/components/schemas/VideoGenerationRequest","text":"#/components/schemas/TextGenerationRequest"}}},"ImageGenerationRequest":{"type":"object","additionalProperties":false,"properties":{"kind":{"type":"string","enum":["image"]},"tags":{"$ref":"#/components/schemas/GenerationTags"},"prompt":{"type":"string","minLength":1,"maxLength":2000,"description":"The change to make, in plain words. Used as `fields.change` when `fields` is not sent, and ignored when it is. Send one or the other.","example":"Make the wheels matte black, keep the rest as-is."},"fields":{"$ref":"#/components/schemas/GenerationFields"},"model":{"type":"string","description":"One of the `models` from `GET /generations/{project_id}/enabled`","example":"gemini-3-pro-image"},"takes":{"type":"integer","format":"int32","minimum":1,"maximum":4,"description":"How many to make from this recipe; 1 when it is not sent"},"seed":{"type":"integer","format":"int32","minimum":0,"description":"The first take's seed; each further take uses the next number. A random one is chosen when it is not sent. Models treat it as a hint, so the same seed can still give a different picture."},"fidelity":{"$ref":"#/components/schemas/GenerationFidelity"},"template_id":{"type":"string","format":"uuid","description":"The saved template the recipe started from"},"location_id":{"type":"string","format":"uuid","description":"A location of the project's workspace to shoot in. Its anchors go in as references after `reference_keys`, and count toward the model's `max_references`. Fill the recipe from the location's fields yourself; they are not merged in."},"parent_id":{"type":"string","format":"uuid","description":"The generation this round iterates on"},"frame":{"type":"integer","format":"int32","minimum":0,"default":0,"description":"The frame of the version the image is made from"},"options":{"type":"string","description":"The configuration the image is made from, as the viewer holds it. A kept image is a view of this configuration.","example":"+VIW+COL"},"primary_key":{"type":"string","minLength":1,"description":"The frame to change, uploaded with `POST /generations/{project_id}/{version}/inputs`. Leave it out to start from `frame` of `options` as the version renders it; that configuration's layers at that frame must all be images, or the request is refused.","example":"2524b742-8c36-413e-b9f6-2269918618af/1/ai-queue/9b2f6c1e-7d4a-4c1b-9e0f-3a5d8c2b1e47.png"},"reference_keys":{"type":"array","maxItems":13,"description":"Images that steer colour, lighting or styling, each uploaded the same way; at most the chosen model's `max_references`","items":{"type":"string","minLength":1}}},"required":["kind","model"]},"VideoResolution":{"type":"string","enum":["720p","1080p"]},"VideoSettings":{"type":"object","additionalProperties":false,"properties":{"duration_seconds":{"type":"integer","format":"int32","enum":[4,6,8],"description":"How long the clip runs"},"resolution":{"$ref":"#/components/schemas/VideoResolution"},"audio":{"type":"boolean","description":"Whether the clip has generated sound"},"last_frame_key":{"type":["string","null"],"description":"The frame the clip ends on, uploaded with `POST /generations/{project_id}/{version}/inputs`"},"loop":{"type":"boolean","description":"Whether the clip ends on the frame it starts on"},"extended_from":{"type":["string","null"],"format":"uuid","description":"The clip this one extends, when it was made with `POST /generations/{project_id}/{version}/{generation_id}/extend`. Its length is `output.duration_seconds`; `duration_seconds` is the first clip's."}},"required":["duration_seconds","resolution","audio","last_frame_key","loop","extended_from"]},"VideoGenerationRequest":{"type":"object","additionalProperties":false,"description":"A clip that starts from a frame of the version. The car stays as rendered; `fields.motion` says what moves. Clips are 16:9 or 9:16, so the frame is padded to the nearer of the two and the clip is cropped back to the frame's shape.","properties":{"kind":{"type":"string","enum":["video"]},"tags":{"$ref":"#/components/schemas/GenerationTags"},"fields":{"$ref":"#/components/schemas/GenerationFields"},"model":{"type":"string","description":"One of the video `models` from `GET /generations/{project_id}/enabled`","example":"veo-3.1-fast-generate-001"},"takes":{"type":"integer","format":"int32","minimum":1,"maximum":4,"description":"How many clips to make from this recipe; 1 when it is not sent"},"seed":{"type":"integer","format":"int32","minimum":0,"description":"The first take's seed; each further take uses the next number"},"template_id":{"type":"string","format":"uuid"},"parent_id":{"type":"string","format":"uuid"},"frame":{"type":"integer","format":"int32","minimum":0,"default":0,"description":"The frame of the version the clip starts from"},"options":{"type":"string","description":"The configuration the clip is made from. A kept clip is a view of this configuration.","example":"+VIW+COL"},"primary_key":{"type":"string","minLength":1,"description":"The frame the clip starts on, uploaded with `POST /generations/{project_id}/{version}/inputs`. Leave it out to start from `frame` of `options` as the version renders it."},"last_frame_key":{"type":"string","minLength":1,"description":"A frame for the clip to end on, uploaded the same way"},"loop":{"type":"boolean","description":"End the clip on the frame it starts on, so it plays seamlessly on repeat; false when it is not sent. Needs a model whose `supports.end_frame` is true, and can't be sent with `last_frame_key`."},"duration_seconds":{"type":"integer","format":"int32","enum":[4,6,8],"default":4},"resolution":{"$ref":"#/components/schemas/VideoResolution"},"audio":{"type":"boolean","default":false,"description":"Whether to generate sound with the clip"}},"required":["kind","model","fields"]},"TextTask":{"type":"string","enum":["caption","alt_text","product_copy"],"description":"What the text is for: a caption for a post or gallery, alt text for the image, or a paragraph of product copy for the car as configured."},"TextGenerationRequest":{"type":"object","additionalProperties":false,"description":"Text written from a frame of the version, as the configuration shows it. The answer is Markdown, stored as a file of the version like any other take; kept, it is a Text view.","properties":{"kind":{"type":"string","enum":["text"]},"task":{"$ref":"#/components/schemas/TextTask"},"tags":{"$ref":"#/components/schemas/GenerationTags"},"fields":{"$ref":"#/components/schemas/GenerationFields"},"model":{"type":"string","description":"One of the text `models` from `GET /generations/capability`","example":"gemini-3.8-flash"},"takes":{"type":"integer","format":"int32","minimum":1,"maximum":4,"description":"How many versions of the text to write; 1 when it is not sent"},"template_id":{"type":"string","format":"uuid"},"parent_id":{"type":"string","format":"uuid"},"frame":{"type":"integer","format":"int32","minimum":0,"description":"The frame of the version the text describes; 0 when it is not sent"},"options":{"type":"string","description":"The configuration the text describes","example":"+VIW+COL"},"primary_key":{"type":"string","minLength":1,"description":"An image to describe instead, uploaded with `POST /generations/{project_id}/{version}/inputs`"}},"required":["kind","task","model"]},"Generation":{"type":"object","properties":{"id":{"type":"string","format":"uuid","example":"550e8400-e29b-41d4-a716-446655440000"},"kind":{"$ref":"#/components/schemas/GenerationKind"},"status":{"type":"string","enum":["pending","complete","failed"],"description":"`pending` while it is made, `complete` once `output` holds it, and `failed` when it gave up. A failed one runs again with `POST …/retry`."},"kept":{"type":"boolean","description":"Whether it was kept into its version. A kept generation is one of the version's views, selected by `rule`."},"name":{"type":"string","description":"Its name. That is the prompt until it is kept under a name of its own.","example":"Front 3/4, blue tweak"},"rule":{"type":"string","description":"The rio option a kept generation is selectable as","example":"+AI_550e8400"},"frame":{"type":"integer","format":"int32","description":"The frame of the version it was made from","example":0},"options":{"type":"string","description":"The configuration it was made from","example":"+VIW+COL"},"prompt":{"type":["string","null"],"description":"The prompt the model received","example":"Make the wheels matte black, keep the rest as-is."},"fields":{"description":"The recipe it was made from, as written","oneOf":[{"$ref":"#/components/schemas/GenerationFields"},{"type":"null"}]},"model":{"type":["string","null"],"example":"gemini-2.5-flash-image"},"seed":{"type":["integer","null"],"format":"int32"},"fidelity":{"oneOf":[{"$ref":"#/components/schemas/GenerationFidelity"},{"type":"null"}]},"batch_id":{"type":["string","null"],"format":"uuid","description":"The round it was made in; takes made together share it"},"parent_id":{"type":["string","null"],"format":"uuid","description":"The generation it iterates on"},"template_id":{"type":["string","null"],"format":"uuid"},"location_id":{"type":["string","null"],"format":"uuid","description":"The location it was shot in"},"primary_key":{"type":["string","null"],"description":"The input it changed"},"reference_keys":{"type":"array","description":"The inputs that steered it","items":{"type":"string"}},"video":{"description":"How a video was asked for; null for an image","oneOf":[{"$ref":"#/components/schemas/VideoSettings"},{"type":"null"}]},"text":{"description":"What a text was written for; null for anything else","oneOf":[{"type":"object","properties":{"task":{"$ref":"#/components/schemas/TextTask"}},"required":["task"]},{"type":"null"}]},"output":{"description":"What it made, once it is complete","oneOf":[{"$ref":"#/components/schemas/GenerationOutput"},{"type":"null"}]},"created_by":{"type":"string","format":"uuid","description":"The person who started it"},"created_by_name":{"type":["string","null"],"example":"Jane Doe"},"created_at":{"type":"string","format":"date-time","example":"2026-10-30T10:30:00Z"},"project_id":{"type":"string","format":"uuid"},"version":{"type":"integer","format":"int32","description":"The version it was made from"},"hidden":{"type":"boolean","description":"Whether it was hidden from the studio's shots. Hidden takes stay in their version."},"tags":{"$ref":"#/components/schemas/GenerationTags"}},"required":["id","kind","status","kept","project_id","version","hidden","tags","name","rule","frame","options","prompt","fields","model","seed","fidelity","batch_id","parent_id","template_id","location_id","primary_key","reference_keys","video","text","output","created_by","created_by_name","created_at"]},"GenerationOutput":{"type":"object","properties":{"key":{"type":"string","description":"Where the file is stored","example":"2524b742-8c36-413e-b9f6-2269918618af/1/2524b742-8c36-413e-b9f6-2269918618af_1/ai-generated/550e8400-e29b-41d4-a716-446655440000.png"},"url":{"type":["string","null"],"description":"A signed URL to download it, valid for an hour, or null when its file cannot be found"},"width":{"type":["integer","null"],"format":"int32","example":2048},"height":{"type":["integer","null"],"format":"int32","example":1152},"content_type":{"type":"string","example":"image/png"},"poster_url":{"type":["string","null"],"description":"For a video, a signed URL to its first frame, valid for an hour"},"duration_seconds":{"type":["number","null"],"description":"For a video, how long it runs","example":4}},"required":["key","url","width","height","content_type","poster_url","duration_seconds"]},"Visibility":{"type":"string","enum":["PRIVATE","INTERNAL","PUBLIC"],"description":"Three-tier visibility for Projects, Live Projects and Workspaces.\n`PRIVATE` — only explicit collaborators.\n`INTERNAL` — workspace (studio) members only.\n`PUBLIC` — appears in the Discover feed (subject to the owning\nstudio's `discoverable` flag) and is indexable.\n"},"ProfileVisibility":{"type":"string","enum":["PRIVATE","PUBLIC"],"description":"Studio profile visibility. `PUBLIC` exposes `/studios/{account_id}`\nand caps the effective visibility of everything the studio owns —\na PRIVATE studio cannot surface PUBLIC items on Discover even if\nthe item itself is marked PUBLIC.\n"}},"parameters":{"AuditAccountIdPath":{"name":"account_id","in":"path","description":"The studio ID","required":true,"schema":{"type":"string","format":"uuid"}},"WebhookAccountIdPath":{"name":"account_id","in":"path","description":"The studio ID","required":true,"schema":{"type":"string","format":"uuid"}},"WebhookEndpointIdPath":{"name":"endpoint_id","in":"path","description":"The webhook endpoint ID","required":true,"schema":{"type":"string","format":"uuid"}},"AuditClassQuery":{"name":"class","in":"query","description":"The OCSF event class: `api_activity` (API Activity, 6003); `authentication` (Authentication, 3002: sign-ins, failed sign-ins and sign-outs); `account_change` (Account Change, 3001: password changes and resets, two-factor on and off); `user_access` (User Access Management, 3005: studio roles given and taken, by people, invitations, SCIM and sign-in auto-join); `entity_management` (Entity Management, 3004: SSO providers). Every class but `api_activity` has only `kind=write`.","schema":{"type":"string","enum":["api_activity","authentication","account_change","user_access","entity_management"],"default":"api_activity"}},"AuditKindQuery":{"name":"kind","in":"query","description":"Reads (`GET`) and writes (everything else) are kept apart, since reads are far more numerous.","schema":{"type":"string","enum":["read","write"],"default":"write"}},"ContentHashQuery":{"name":"content_hash","in":"query","required":true,"schema":{"$ref":"#/components/schemas/ContentHash"}},"ProjectIdPath":{"name":"project_id","in":"path","description":"The project ID","required":true,"schema":{"type":"string","format":"uuid"},"example":"2524b742-8c36-413e-b9f6-2269918618af"},"VersionPath":{"name":"version","in":"path","description":"The version number","required":true,"schema":{"type":"integer","format":"int32","minimum":1},"example":1},"LocationIdPath":{"name":"location_id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}},"TemplateIdPath":{"name":"template_id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}},"GenerationIdPath":{"name":"generation_id","in":"path","description":"The generation's id","required":true,"schema":{"type":"string","format":"uuid"},"example":"550e8400-e29b-41d4-a716-446655440000"},"CommentIdPath":{"name":"comment_id","in":"path","description":"The comment ID","required":true,"schema":{"type":"string","format":"uuid"},"example":"2524b742-8c36-413e-b9f6-2269918618af"},"ReplyIdPath":{"name":"reply_id","in":"path","description":"The reply ID","required":true,"schema":{"type":"string","format":"uuid"},"example":"2524b742-8c36-413e-b9f6-2269918618af"},"ShareIdQuery":{"name":"share_id","in":"query","description":"The share ID for guest access","required":false,"schema":{"type":"string","format":"uuid"},"example":"2524b742-8c36-413e-b9f6-2269918618af"},"SharedLayoutIdPath":{"name":"id","in":"path","description":"The shared layout ID, which is its source's family id","required":true,"schema":{"type":"string","format":"uuid"}},"IfMatchSeq":{"name":"If-Match","in":"header","description":"The configurator's `seq` as you read it (RFC 9110). When it is no longer current the write is refused with 412; leave it out to write regardless.\n","required":false,"schema":{"type":"string","pattern":"^\\s*\"?\\d+\"?\\s*$"},"example":"\"12\""},"ConfiguratorIdPath":{"name":"id","in":"path","description":"The configurator ID","required":true,"schema":{"type":"string","format":"uuid"},"example":"2524b742-8c36-413e-b9f6-2269918618af"},"ConfiguratorThemeIdPath":{"name":"theme_id","in":"path","description":"The theme ID","required":true,"schema":{"type":"string"},"example":"automotive-generic"},"ConfiguratorFamilyIdPath":{"name":"family_id","in":"path","description":"The configurator family ID, which is the configurator's stable link","required":true,"schema":{"type":"string","format":"uuid"}},"ApprovalIdPath":{"name":"approval_id","in":"path","description":"The approval ID","required":true,"schema":{"type":"string","format":"uuid"},"example":"2524b742-8c36-413e-b9f6-2269918618af"}},"responses":{"PreconditionFailed":{"description":"Precondition Failed - `If-Match` names a version that is no longer current","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"BadRequest":{"description":"Bad Request - Invalid input parameters","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"Conflict":{"description":"Conflict - The resource is not in a state that allows this request","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"PaymentRequired":{"description":"Payment Required - Account limit exceeded","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"Unauthorized":{"description":"Unauthorized - Authentication required","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"Forbidden":{"description":"Forbidden - Insufficient permissions","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"NotFound":{"description":"Not Found - Resource does not exist","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"UnprocessableEntity":{"description":"Unprocessable Entity - Request understood but cannot be processed","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"LayoutRefused":{"description":"The layout has `error` issues; `errors` points at each, `issues` lists all.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/LayoutRefusal"}}}},"ConfigurationRefused":{"description":"The rio has `error` findings; `errors` points at each, `findings` lists all.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ConfigurationRefusal"}}}},"TooManyRequests":{"description":"Too Many Requests. Each API key, and each OAuth client per subject, has a request budget per window; upload signing has its own, larger one. Wait `Retry-After` seconds. A 429 from that budget also carries `RateLimit` and `RateLimit-Policy`.","headers":{"Retry-After":{"description":"Seconds to wait before retrying.","schema":{"type":"integer","minimum":0}},"RateLimit":{"description":"`\"<policy>\"; r=<remaining>; t=<seconds to reset>`, per draft-ietf-httpapi-ratelimit-headers.","schema":{"type":"string"},"example":"\"api\"; r=0; t=42"},"RateLimit-Policy":{"description":"`\"<policy>\"; q=<quota>; w=<window seconds>; pk=:<key hash>:`. The policy is `api` or `upload-signing`.","schema":{"type":"string"},"example":"\"api\"; q=600; w=60; pk=:ZmVkY2JhOTg3NjU0:"}},"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"InternalServerError":{"description":"Internal Server Error","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}}},"securitySchemes":{"oauth2":{"type":"oauth2","description":"Every request carries a bearer token: an `rk_` API key, or an access token from `/api/auth/oauth2/token`. A token holds scopes and each operation lists the scope it needs; a token without that scope gets `403`. An API key's scopes are chosen when the key is created, and no token holds more than the role of the person or studio it acts for. Tokens issued before scopes existed carry `read` and `write`, which stand for every `:read` scope but `audit:read`, and every `:write` scope.","flows":{"clientCredentials":{"tokenUrl":"https://app.renderapp.io/api/auth/oauth2/token","scopes":{"projects:read":"Read projects and their versions, files, folders, generations and processing jobs","projects:write":"Create, upload to, change and delete projects, versions, folders and generations","downloads:read":"Download a version's files as an archive","downloads:write":"Reserved; grants nothing yet","comments:read":"Read comments, replies and comment labels","comments:write":"Post, edit, resolve, label and delete comments and replies","approvals:read":"Read approval requests","approvals:write":"Request, review, reopen and cancel approvals","shares:read":"Read share links","shares:write":"Create, change and delete share links","workspaces:read":"Read workspaces","workspaces:write":"Create, change and delete workspaces","configurators:read":"Read configurators, and view and enquire through published ones","configurators:write":"Create, change and delete configurators","members:read":"Read who belongs to studios, workspaces and shares, and their invitations","members:write":"Invite and remove members, change their workspace roles, and leave","audit:read":"Read the studio's audit log. Held by studio owners and admins","audit:write":"Reserved; grants nothing yet","webhooks:read":"Read the studio's webhook endpoints and what was sent to them. Held by studio owners and admins","webhooks:write":"Add, change, test and remove the studio's webhook endpoints, and read their signing secrets"}}}}}},"tags":[{"name":"Projects","description":"Manage Projects and related operations."},{"name":"Workspaces","description":"Manage Workspaces and related operations."},{"name":"Shares","description":"Manage Shares and related operations."},{"name":"Files","description":"A version's stored files. Use these:\n\n- `GET /files/{project_id}/{version}` lists what storage holds.\n- `GET /files/{project_id}/{version}/presigned-url` and `/thumb-url` sign downloads.\n- `POST /files/{project_id}/{version}/sign-s3-request` signs every upload step,\n  single-shot or multipart.\n\nThe rest are deprecated: one endpoint per upload step, kept for clients built\non Uppy's older callbacks.\n"},{"name":"Folders","description":"Manage Folders and related operations."},{"name":"Audit log","description":"A studio's audit log, as OCSF events in Parquet files plus the newest events from the stream."},{"name":"Webhooks","description":"Endpoints a studio sends events to, and what was sent to each. Every event is a JSON POST with the `webhook-id`, `webhook-timestamp` and `webhook-signature` headers of [Standard Webhooks](https://www.standardwebhooks.com/). Answer with any 2xx within 10 seconds. Anything else is retried, and an endpoint that has failed for 24 hours is paused."},{"name":"Comments","description":"Manage Comments and related operations."},{"name":"Configurators","description":"Manage Configurators and related operations."},{"name":"Approvals","description":"Manage approval requests for project versions."},{"name":"Generations","description":"Images generated from a version's frames with a prompt. A kept one joins its version as a view. Other kinds will join `kind`."},{"name":"OAuth","description":"OAuth 2.0 / OIDC discovery and authorization-server metadata."},{"name":"Docs","description":"This API's own OpenAPI document, whole or cut to authoring."}]}