{
  "openapi": "3.1.0",
  "info": {
    "title": "Voxobox API",
    "version": "1.0.0",
    "summary": "Browser-based 3D model viewer and asset library. Upload, organize, edit, and share GLB/GLTF models programmatically.",
    "description": "Public REST API for Voxobox (https://voxobox.com). All endpoints are mounted under `/api`. Requests and responses are JSON unless noted. Two auth modes are supported: API key (`X-API-Key`, recommended for agents) and Bearer JWT with cookie-based refresh + CSRF (browser-style sessions).",
    "contact": {
      "name": "Voxobox",
      "email": "davidtgill@gmail.com",
      "url": "https://voxobox.com/contact/"
    },
    "license": { "name": "Proprietary" }
  },
  "servers": [
    { "url": "https://voxobox.com/api", "description": "Production" }
  ],
  "tags": [
    { "name": "Auth" },
    { "name": "Files" },
    { "name": "Folders" },
    { "name": "Presets" },
    { "name": "Shares" },
    { "name": "Health" }
  ],
  "security": [
    { "ApiKeyAuth": [] },
    { "BearerAuth": [] }
  ],
  "paths": {
    "/auth/signup": {
      "post": {
        "tags": ["Auth"],
        "summary": "Create a new account",
        "security": [],
        "requestBody": {
          "required": true,
          "content": { "application/json": { "schema": { "$ref": "#/components/schemas/SignupRequest" } } }
        },
        "responses": {
          "201": { "description": "Account created", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/AuthResponse" } } } },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "409": { "$ref": "#/components/responses/Conflict" },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      }
    },
    "/auth/login": {
      "post": {
        "tags": ["Auth"],
        "summary": "Authenticate an existing user",
        "security": [],
        "requestBody": {
          "required": true,
          "content": { "application/json": { "schema": { "$ref": "#/components/schemas/LoginRequest" } } }
        },
        "responses": {
          "200": { "description": "Authenticated", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/AuthResponse" } } } },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      }
    },
    "/auth/refresh": {
      "post": {
        "tags": ["Auth"],
        "summary": "Rotate the refresh token and mint a new access token",
        "description": "Reads the `refresh` cookie. Requires `X-CSRF-Token` matching the `csrf` cookie.",
        "security": [{ "BearerAuth": [] }],
        "parameters": [{ "$ref": "#/components/parameters/CsrfToken" }],
        "responses": {
          "200": { "description": "Refreshed", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/AuthResponse" } } } },
          "401": { "$ref": "#/components/responses/Unauthorized" }
        }
      }
    },
    "/auth/logout": {
      "post": {
        "tags": ["Auth"],
        "summary": "Revoke the refresh-token family and clear cookies",
        "security": [{ "BearerAuth": [] }],
        "parameters": [{ "$ref": "#/components/parameters/CsrfToken" }],
        "responses": {
          "200": { "description": "OK", "content": { "application/json": { "schema": { "type": "object", "properties": { "ok": { "type": "boolean" } } } } } }
        }
      }
    },
    "/auth/change-password": {
      "post": {
        "tags": ["Auth"],
        "summary": "Change the caller's password",
        "requestBody": {
          "required": true,
          "content": { "application/json": { "schema": {
            "type": "object",
            "required": ["currentPassword", "newPassword"],
            "properties": {
              "currentPassword": { "type": "string" },
              "newPassword": { "type": "string", "minLength": 10 }
            }
          } } }
        },
        "responses": {
          "200": { "description": "Password changed" },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "401": { "$ref": "#/components/responses/Unauthorized" }
        }
      }
    },
    "/auth/me": {
      "get": {
        "tags": ["Auth"],
        "summary": "Current user and plan",
        "responses": {
          "200": { "description": "OK", "content": { "application/json": { "schema": {
            "type": "object",
            "properties": {
              "user": { "$ref": "#/components/schemas/User" },
              "plan": { "$ref": "#/components/schemas/Plan" }
            }
          } } } }
        }
      }
    },
    "/files/upload-url": {
      "post": {
        "tags": ["Files"],
        "summary": "Get a presigned PUT URL for a new model",
        "parameters": [{ "$ref": "#/components/parameters/CsrfToken" }],
        "requestBody": {
          "required": true,
          "content": { "application/json": { "schema": {
            "type": "object",
            "required": ["filename", "contentType", "sizeBytes"],
            "properties": {
              "filename": { "type": "string" },
              "contentType": { "type": "string", "enum": ["model/gltf-binary", "model/gltf+json", "image/png", "image/jpeg", "image/vnd.radiance", "image/x-exr"] },
              "sizeBytes": { "type": "integer", "minimum": 1 },
              "folderId": { "type": ["string", "null"], "format": "uuid" }
            }
          } } }
        },
        "responses": {
          "200": { "description": "Presigned URL issued", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/UploadUrlResponse" } } } },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "413": { "$ref": "#/components/responses/PayloadTooLarge" },
          "415": { "$ref": "#/components/responses/UnsupportedMedia" }
        }
      }
    },
    "/files/confirm": {
      "post": {
        "tags": ["Files"],
        "summary": "Confirm a completed upload",
        "parameters": [{ "$ref": "#/components/parameters/CsrfToken" }],
        "requestBody": {
          "required": true,
          "content": { "application/json": { "schema": {
            "type": "object",
            "required": ["modelId"],
            "properties": {
              "modelId": { "type": "string", "format": "uuid" },
              "etag": { "type": "string" }
            }
          } } }
        },
        "responses": {
          "200": { "description": "Confirmed", "content": { "application/json": { "schema": {
            "type": "object",
            "properties": {
              "ok": { "type": "boolean" },
              "alreadyConfirmed": { "type": "boolean" },
              "model": { "$ref": "#/components/schemas/Model" }
            }
          } } } },
          "404": { "$ref": "#/components/responses/NotFound" },
          "409": { "$ref": "#/components/responses/Conflict" }
        }
      }
    },
    "/files": {
      "get": {
        "tags": ["Files"],
        "summary": "List the caller's models",
        "responses": {
          "200": { "description": "OK", "content": { "application/json": { "schema": {
            "type": "object",
            "properties": { "models": { "type": "array", "items": { "$ref": "#/components/schemas/Model" } } }
          } } } }
        }
      }
    },
    "/files/view-url/{modelId}": {
      "get": {
        "tags": ["Files"],
        "summary": "Get a 1-hour presigned URL for loading a model and its textures",
        "parameters": [{ "name": "modelId", "in": "path", "required": true, "schema": { "type": "string", "format": "uuid" } }],
        "responses": {
          "200": { "description": "OK", "content": { "application/json": { "schema": {
            "type": "object",
            "properties": {
              "url": { "type": "string", "format": "uri" },
              "expiresIn": { "type": "integer" },
              "materialSettings": { "type": "object" },
              "materialTextureUrls": { "type": "object", "additionalProperties": { "type": "string", "format": "uri" } }
            }
          } } } },
          "404": { "$ref": "#/components/responses/NotFound" },
          "409": { "$ref": "#/components/responses/Conflict" }
        }
      }
    },
    "/files/{modelId}": {
      "patch": {
        "tags": ["Files"],
        "summary": "Rename, move, or update material settings",
        "parameters": [
          { "name": "modelId", "in": "path", "required": true, "schema": { "type": "string", "format": "uuid" } },
          { "$ref": "#/components/parameters/CsrfToken" }
        ],
        "requestBody": {
          "required": true,
          "content": { "application/json": { "schema": {
            "type": "object",
            "properties": {
              "name": { "type": "string" },
              "folderId": { "type": ["string", "null"], "format": "uuid" },
              "materialSettings": { "type": "object" }
            }
          } } }
        },
        "responses": {
          "200": { "description": "OK", "content": { "application/json": { "schema": { "type": "object", "properties": { "model": { "$ref": "#/components/schemas/Model" } } } } } },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "404": { "$ref": "#/components/responses/NotFound" }
        }
      },
      "delete": {
        "tags": ["Files"],
        "summary": "Delete a model",
        "parameters": [
          { "name": "modelId", "in": "path", "required": true, "schema": { "type": "string", "format": "uuid" } },
          { "$ref": "#/components/parameters/CsrfToken" }
        ],
        "responses": {
          "200": { "description": "Deleted", "content": { "application/json": { "schema": { "type": "object", "properties": { "ok": { "type": "boolean" } } } } } },
          "404": { "$ref": "#/components/responses/NotFound" }
        }
      }
    },
    "/files/material-textures/upload-urls": {
      "post": {
        "tags": ["Files"],
        "summary": "Batch-request presigned PUT URLs for material textures",
        "parameters": [{ "$ref": "#/components/parameters/CsrfToken" }],
        "requestBody": {
          "required": true,
          "content": { "application/json": { "schema": {
            "type": "object",
            "required": ["textures"],
            "properties": {
              "textures": {
                "type": "array",
                "maxItems": 32,
                "items": {
                  "type": "object",
                  "required": ["assetId", "filename", "contentType", "sizeBytes"],
                  "properties": {
                    "assetId": { "type": "string" },
                    "filename": { "type": "string" },
                    "contentType": { "type": "string" },
                    "sizeBytes": { "type": "integer" }
                  }
                }
              }
            }
          } } }
        },
        "responses": {
          "200": { "description": "OK", "content": { "application/json": { "schema": {
            "type": "object",
            "properties": {
              "uploads": {
                "type": "array",
                "items": {
                  "type": "object",
                  "properties": {
                    "assetId": { "type": "string" },
                    "key": { "type": "string" },
                    "uploadUrl": { "type": "string", "format": "uri" }
                  }
                }
              }
            }
          } } } }
        }
      }
    },
    "/folders": {
      "get": {
        "tags": ["Folders"],
        "summary": "List folders",
        "responses": {
          "200": { "description": "OK", "content": { "application/json": { "schema": {
            "type": "object",
            "properties": { "folders": { "type": "array", "items": { "$ref": "#/components/schemas/Folder" } } }
          } } } }
        }
      },
      "post": {
        "tags": ["Folders"],
        "summary": "Create a folder",
        "parameters": [{ "$ref": "#/components/parameters/CsrfToken" }],
        "requestBody": {
          "required": true,
          "content": { "application/json": { "schema": {
            "type": "object",
            "required": ["name"],
            "properties": {
              "name": { "type": "string", "maxLength": 64 },
              "parentId": { "type": ["string", "null"], "format": "uuid" },
              "sortOrder": { "type": "integer" }
            }
          } } }
        },
        "responses": {
          "200": { "description": "Created", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Folder" } } } },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "409": { "$ref": "#/components/responses/Conflict" },
          "413": { "$ref": "#/components/responses/PayloadTooLarge" }
        }
      }
    },
    "/folders/{id}": {
      "patch": {
        "tags": ["Folders"],
        "summary": "Rename, re-parent, or re-sort a folder",
        "parameters": [
          { "name": "id", "in": "path", "required": true, "schema": { "type": "string", "format": "uuid" } },
          { "$ref": "#/components/parameters/CsrfToken" }
        ],
        "requestBody": {
          "required": true,
          "content": { "application/json": { "schema": {
            "type": "object",
            "properties": {
              "name": { "type": "string" },
              "parentId": { "type": ["string", "null"], "format": "uuid" },
              "sortOrder": { "type": "integer" }
            }
          } } }
        },
        "responses": {
          "200": { "description": "OK" },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "404": { "$ref": "#/components/responses/NotFound" },
          "409": { "$ref": "#/components/responses/Conflict" }
        }
      },
      "delete": {
        "tags": ["Folders"],
        "summary": "Delete a folder",
        "parameters": [
          { "name": "id", "in": "path", "required": true, "schema": { "type": "string", "format": "uuid" } },
          { "$ref": "#/components/parameters/CsrfToken" }
        ],
        "requestBody": {
          "content": { "application/json": { "schema": {
            "type": "object",
            "properties": { "moveContentsTo": { "type": ["string", "null"], "format": "uuid" } }
          } } }
        },
        "responses": {
          "200": { "description": "Deleted" },
          "404": { "$ref": "#/components/responses/NotFound" },
          "409": { "$ref": "#/components/responses/Conflict" }
        }
      }
    },
    "/folders/{id}/move-contents": {
      "post": {
        "tags": ["Folders"],
        "summary": "Move child folders and models into another folder",
        "parameters": [
          { "name": "id", "in": "path", "required": true, "schema": { "type": "string", "format": "uuid" } },
          { "$ref": "#/components/parameters/CsrfToken" }
        ],
        "requestBody": {
          "required": true,
          "content": { "application/json": { "schema": {
            "type": "object",
            "required": ["targetFolderId"],
            "properties": { "targetFolderId": { "type": ["string", "null"], "format": "uuid" } }
          } } }
        },
        "responses": { "200": { "description": "OK" } }
      }
    },
    "/presets": {
      "get": {
        "tags": ["Presets"],
        "summary": "List presets (metadata only)",
        "parameters": [
          { "name": "kind", "in": "query", "schema": { "type": "string", "enum": ["lights", "materials"] } }
        ],
        "responses": {
          "200": { "description": "OK", "content": { "application/json": { "schema": {
            "type": "object",
            "properties": { "presets": { "type": "array", "items": { "$ref": "#/components/schemas/PresetSummary" } } }
          } } } }
        }
      },
      "post": {
        "tags": ["Presets"],
        "summary": "Create a preset",
        "parameters": [{ "$ref": "#/components/parameters/CsrfToken" }],
        "requestBody": {
          "required": true,
          "content": { "application/json": { "schema": { "$ref": "#/components/schemas/PresetWrite" } } }
        },
        "responses": {
          "200": { "description": "Created", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Preset" } } } },
          "413": { "$ref": "#/components/responses/PayloadTooLarge" }
        }
      }
    },
    "/presets/export": {
      "get": {
        "tags": ["Presets"],
        "summary": "Export all presets as a JSON file",
        "responses": {
          "200": {
            "description": "Attachment",
            "content": { "application/json": { "schema": {
              "type": "object",
              "properties": {
                "version": { "type": "integer" },
                "exportedAt": { "type": "string", "format": "date-time" },
                "presets": { "type": "array", "items": { "$ref": "#/components/schemas/Preset" } }
              }
            } } }
          }
        }
      }
    },
    "/presets/{id}": {
      "get": {
        "tags": ["Presets"],
        "summary": "Get a single preset including settings",
        "parameters": [{ "name": "id", "in": "path", "required": true, "schema": { "type": "string", "format": "uuid" } }],
        "responses": {
          "200": { "description": "OK", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Preset" } } } },
          "404": { "$ref": "#/components/responses/NotFound" }
        }
      },
      "put": {
        "tags": ["Presets"],
        "summary": "Update a preset",
        "parameters": [
          { "name": "id", "in": "path", "required": true, "schema": { "type": "string", "format": "uuid" } },
          { "$ref": "#/components/parameters/CsrfToken" }
        ],
        "requestBody": {
          "required": true,
          "content": { "application/json": { "schema": { "$ref": "#/components/schemas/PresetWrite" } } }
        },
        "responses": {
          "200": { "description": "OK" },
          "404": { "$ref": "#/components/responses/NotFound" }
        }
      },
      "delete": {
        "tags": ["Presets"],
        "summary": "Delete a preset",
        "parameters": [
          { "name": "id", "in": "path", "required": true, "schema": { "type": "string", "format": "uuid" } },
          { "$ref": "#/components/parameters/CsrfToken" }
        ],
        "responses": { "200": { "description": "Deleted" } }
      }
    },
    "/share/{id}": {
      "get": {
        "tags": ["Shares"],
        "summary": "Public share metadata for a shared model",
        "security": [],
        "parameters": [{ "name": "id", "in": "path", "required": true, "schema": { "type": "string" } }],
        "responses": {
          "200": { "description": "OK" },
          "404": { "$ref": "#/components/responses/NotFound" }
        }
      }
    },
    "/health": {
      "get": {
        "tags": ["Health"],
        "summary": "Liveness + DB + Spaces check",
        "security": [],
        "responses": {
          "200": { "description": "OK" },
          "503": { "description": "Degraded" }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "ApiKeyAuth": {
        "type": "apiKey",
        "in": "header",
        "name": "X-API-Key",
        "description": "Per-account API key. Recommended for AI agents and server-side integrations. Does not require CSRF."
      },
      "BearerAuth": {
        "type": "http",
        "scheme": "bearer",
        "bearerFormat": "JWT",
        "description": "15-minute Ed25519 access token from `/auth/login`, `/auth/signup`, or `/auth/refresh`. Mutating requests in this mode also require `X-CSRF-Token`."
      }
    },
    "parameters": {
      "CsrfToken": {
        "name": "X-CSRF-Token",
        "in": "header",
        "required": false,
        "schema": { "type": "string" },
        "description": "Required for mutating requests under BearerAuth. Must match the `csrf` cookie. Not required when using ApiKeyAuth."
      }
    },
    "schemas": {
      "SignupRequest": {
        "type": "object",
        "required": ["email", "confirmEmail", "password"],
        "properties": {
          "email": { "type": "string", "format": "email" },
          "confirmEmail": { "type": "string", "format": "email" },
          "password": { "type": "string", "minLength": 10 }
        }
      },
      "LoginRequest": {
        "type": "object",
        "required": ["email", "password"],
        "properties": {
          "email": { "type": "string", "format": "email" },
          "password": { "type": "string" }
        }
      },
      "AuthResponse": {
        "type": "object",
        "properties": {
          "accessToken": { "type": "string" },
          "accessExpiresInSeconds": { "type": "integer" },
          "user": { "$ref": "#/components/schemas/User" },
          "plan": { "$ref": "#/components/schemas/Plan" }
        }
      },
      "User": {
        "type": "object",
        "properties": {
          "id": { "type": "string", "format": "uuid" },
          "email": { "type": "string", "format": "email" },
          "plan": { "type": "string" },
          "storageBytes": { "type": "integer" },
          "createdAt": { "type": "string", "format": "date-time" }
        }
      },
      "Plan": {
        "type": "object",
        "properties": {
          "key": { "type": "string" },
          "label": { "type": "string" },
          "maxModels": { "type": "integer" },
          "maxStorageBytes": { "type": "integer" }
        }
      },
      "Model": {
        "type": "object",
        "properties": {
          "id": { "type": "string", "format": "uuid" },
          "name": { "type": "string" },
          "size_bytes": { "type": "integer" },
          "folder_id": { "type": ["string", "null"], "format": "uuid" },
          "status": { "type": "string", "enum": ["pending", "ready"] },
          "created_at": { "type": "string", "format": "date-time" },
          "material_settings": { "type": "object" }
        }
      },
      "Folder": {
        "type": "object",
        "properties": {
          "id": { "type": "string", "format": "uuid" },
          "name": { "type": "string" },
          "parentId": { "type": ["string", "null"], "format": "uuid" },
          "sortOrder": { "type": "integer" },
          "createdAt": { "type": "string", "format": "date-time" },
          "updatedAt": { "type": "string", "format": "date-time" }
        }
      },
      "PresetSummary": {
        "type": "object",
        "properties": {
          "id": { "type": "string", "format": "uuid" },
          "name": { "type": "string" },
          "is_default": { "type": "boolean" },
          "created_at": { "type": "string", "format": "date-time" },
          "updated_at": { "type": "string", "format": "date-time" }
        }
      },
      "Preset": {
        "allOf": [
          { "$ref": "#/components/schemas/PresetSummary" },
          { "type": "object", "properties": { "settings": { "type": "object" } } }
        ]
      },
      "PresetWrite": {
        "type": "object",
        "required": ["name", "settings"],
        "properties": {
          "name": { "type": "string" },
          "settings": { "type": "object" },
          "isDefault": { "type": "boolean" }
        }
      },
      "UploadUrlResponse": {
        "type": "object",
        "properties": {
          "modelId": { "type": "string", "format": "uuid" },
          "uploadUrl": { "type": "string", "format": "uri" },
          "key": { "type": "string" },
          "expiresIn": { "type": "integer" },
          "requiredHeaders": { "type": "object", "additionalProperties": { "type": "string" } },
          "model": { "$ref": "#/components/schemas/Model" }
        }
      },
      "Error": {
        "type": "object",
        "properties": {
          "error": {
            "type": "object",
            "properties": {
              "message": { "type": "string" },
              "code": { "type": "string" },
              "details": { "type": "object" }
            }
          }
        }
      }
    },
    "responses": {
      "BadRequest": { "description": "Invalid input", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
      "Unauthorized": { "description": "Authentication required or failed", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
      "NotFound": { "description": "Not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
      "Conflict": { "description": "Conflict", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
      "PayloadTooLarge": { "description": "Payload too large or quota exceeded", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
      "UnsupportedMedia": { "description": "Unsupported media type", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
      "RateLimited": { "description": "Too many requests", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }
    }
  }
}
