{
  "openapi": "3.1.0",
  "info": {
    "title": "AppCruiser API",
    "description": "REST API for AppCruiser — a SaaS marketplace for vibe-coded app prototypes. Create ideas, upload mockups (HTML or WebContainer), vote, give feedback, donate, generate AI mockups, and produce AI videos.",
    "version": "1.1.0",
    "contact": {
      "name": "AppCruiser Support",
      "url": "https://appcruiser.com/contact"
    },
    "license": {
      "name": "Proprietary"
    }
  },
  "servers": [
    {
      "url": "https://appcruiser.com/api/v1",
      "description": "Production (v1)"
    },
    {
      "url": "https://appcruiser.com",
      "description": "Production (unversioned)"
    },
    {
      "url": "http://localhost:3000",
      "description": "Local development"
    }
  ],
  "security": [],
  "components": {
    "securitySchemes": {
      "clerkSession": {
        "type": "apiKey",
        "in": "cookie",
        "name": "__session",
        "description": "Clerk session cookie (web UI)"
      },
      "apiKey": {
        "type": "http",
        "scheme": "bearer",
        "description": "API key with `ak_` prefix. Generate at /dashboard/settings."
      }
    },
    "schemas": {
      "Error": {
        "type": "object",
        "required": ["error"],
        "properties": {
          "error": { "type": "string", "description": "Human-readable error message" },
          "code": {
            "type": "string",
            "description": "Machine-readable error code (e.g., INVALID_INPUT, RATE_LIMITED)"
          },
          "requestId": { "type": "string", "format": "uuid", "description": "Unique request identifier for debugging" }
        }
      },
      "Category": {
        "type": "string",
        "enum": ["Productivity", "Creative", "Social", "Gaming", "Developer Tools", "Finance", "Education", "Other"]
      },
      "Idea": {
        "type": "object",
        "properties": {
          "id": { "type": "string" },
          "title": { "type": "string" },
          "description": { "type": "string" },
          "category": { "$ref": "#/components/schemas/Category" },
          "tags": { "type": "array", "items": { "type": "string" } },
          "author": {
            "type": "object",
            "properties": {
              "id": { "type": "string" },
              "name": { "type": "string" },
              "avatarUrl": { "type": "string", "nullable": true }
            }
          },
          "_count": {
            "type": "object",
            "properties": {
              "votes": { "type": "integer" },
              "mockups": { "type": "integer" }
            }
          },
          "createdAt": { "type": "string", "format": "date-time" },
          "updatedAt": { "type": "string", "format": "date-time" }
        }
      },
      "Mockup": {
        "type": "object",
        "properties": {
          "id": { "type": "string" },
          "name": { "type": "string" },
          "description": { "type": "string" },
          "category": { "$ref": "#/components/schemas/Category" },
          "tags": { "type": "array", "items": { "type": "string" } },
          "type": { "type": "string", "enum": ["html", "webcontainer"] },
          "status": { "type": "string", "enum": ["draft", "ready"] },
          "startScript": { "type": "string", "nullable": true },
          "creator": {
            "type": "object",
            "properties": {
              "id": { "type": "string" },
              "name": { "type": "string" },
              "avatarUrl": { "type": "string", "nullable": true }
            }
          },
          "_count": {
            "type": "object",
            "properties": {
              "feedbacks": { "type": "integer" }
            }
          },
          "createdAt": { "type": "string", "format": "date-time" },
          "updatedAt": { "type": "string", "format": "date-time" }
        }
      },
      "Feedback": {
        "type": "object",
        "properties": {
          "id": { "type": "string" },
          "content": { "type": "string" },
          "rating": { "type": "integer", "minimum": 1, "maximum": 5, "nullable": true },
          "author": {
            "type": "object",
            "properties": {
              "id": { "type": "string" },
              "name": { "type": "string" },
              "avatarUrl": { "type": "string", "nullable": true }
            }
          },
          "createdAt": { "type": "string", "format": "date-time" }
        }
      },
      "GenerationJob": {
        "type": "object",
        "properties": {
          "id": { "type": "string" },
          "ideaId": { "type": "string" },
          "status": { "type": "string", "enum": ["queued", "processing", "completed", "failed"] },
          "mockupId": { "type": "string", "nullable": true },
          "result": { "type": "string", "nullable": true },
          "createdAt": { "type": "string", "format": "date-time" },
          "startedAt": { "type": "string", "format": "date-time", "nullable": true },
          "completedAt": { "type": "string", "format": "date-time", "nullable": true }
        }
      },
      "GenerateVideoRequest": {
        "type": "object",
        "required": ["brief"],
        "properties": {
          "tier": { "type": "string", "enum": ["thumbnail", "cta"], "default": "cta" },
          "duration": { "type": "integer", "description": "Seconds — thumbnail: 6/8/10, cta: 15/30/45/60" },
          "brief": { "type": "string", "description": "Creative intent for the video" },
          "voice": {
            "type": "string",
            "description": "VoiceCatalog id or name; Business plans may pass a raw ElevenLabs voice id"
          },
          "language": { "type": "string", "description": "BCP-47 tag, e.g. en-US (immutable per lineage)" },
          "orientation": {
            "type": "string",
            "enum": ["landscape", "portrait"],
            "description": "Immutable per lineage"
          },
          "resolution": { "type": "string", "enum": ["720p", "1080p", "4K"] },
          "brandKitId": { "type": "string", "nullable": true }
        }
      },
      "VideoJob": {
        "type": "object",
        "properties": {
          "id": { "type": "string" },
          "jobId": { "type": "string" },
          "status": {
            "type": "string",
            "enum": ["queued", "planning", "voicing", "compiling", "rendering", "completed", "failed"]
          },
          "tier": { "type": "string", "enum": ["thumbnail", "cta"] },
          "durationSec": { "type": "integer" },
          "language": { "type": "string" },
          "orientation": { "type": "string", "enum": ["landscape", "portrait"] },
          "videoUrl": { "type": "string", "nullable": true },
          "videoSizeBytes": { "type": "integer", "nullable": true },
          "creditsConsumed": { "type": "integer", "nullable": true },
          "errorMessage": { "type": "string", "nullable": true },
          "createdAt": { "type": "string", "format": "date-time" },
          "completedAt": { "type": "string", "format": "date-time", "nullable": true }
        }
      },
      "VideoVariant": {
        "type": "object",
        "properties": {
          "language": { "type": "string" },
          "isPrimary": { "type": "boolean" },
          "publishedR2Key": { "type": "string", "nullable": true },
          "publishedOrientation": { "type": "string", "nullable": true },
          "thumbnailR2Key": { "type": "string", "nullable": true },
          "brandKitId": { "type": "string", "nullable": true }
        }
      },
      "Video": {
        "type": "object",
        "properties": {
          "id": { "type": "string" },
          "title": { "type": "string" },
          "purpose": { "type": "string", "enum": ["conceptual", "marketing", "explainer", "promo"], "nullable": true },
          "targetType": { "type": "string", "enum": ["idea", "mockup"] },
          "targetId": { "type": "string" },
          "primaryLanguage": { "type": "string" },
          "isFeatured": { "type": "boolean" },
          "archivedAt": { "type": "string", "format": "date-time", "nullable": true },
          "galleryPublishedAt": { "type": "string", "format": "date-time", "nullable": true },
          "variants": { "type": "array", "items": { "$ref": "#/components/schemas/VideoVariant" } }
        }
      },
      "ApiKey": {
        "type": "object",
        "properties": {
          "id": { "type": "string" },
          "name": { "type": "string" },
          "keyPrefix": { "type": "string" },
          "scopes": { "type": "array", "items": { "type": "string" } },
          "lastUsedAt": { "type": "string", "format": "date-time", "nullable": true },
          "expiresAt": { "type": "string", "format": "date-time", "nullable": true },
          "revoked": { "type": "boolean" },
          "createdAt": { "type": "string", "format": "date-time" }
        }
      },
      "Notification": {
        "type": "object",
        "properties": {
          "id": { "type": "string" },
          "type": { "type": "string" },
          "message": { "type": "string" },
          "link": { "type": "string", "nullable": true },
          "read": { "type": "boolean" },
          "createdAt": { "type": "string", "format": "date-time" }
        }
      },
      "PaginationParams": {
        "type": "object",
        "properties": {
          "page": { "type": "integer", "minimum": 1, "default": 1 },
          "limit": { "type": "integer", "minimum": 1, "maximum": 50 }
        }
      }
    },
    "parameters": {
      "page": {
        "name": "page",
        "in": "query",
        "schema": { "type": "integer", "minimum": 1, "default": 1 },
        "description": "Page number (1-indexed)"
      },
      "limit": {
        "name": "limit",
        "in": "query",
        "schema": { "type": "integer", "minimum": 1, "maximum": 50 },
        "description": "Items per page"
      },
      "category": {
        "name": "category",
        "in": "query",
        "schema": { "$ref": "#/components/schemas/Category" },
        "description": "Filter by category"
      },
      "q": {
        "name": "q",
        "in": "query",
        "schema": { "type": "string" },
        "description": "Search query (searches title/name and description)"
      }
    },
    "headers": {
      "X-Request-Id": {
        "description": "Unique request identifier",
        "schema": { "type": "string", "format": "uuid" }
      },
      "X-RateLimit-Limit": {
        "description": "Maximum requests allowed in the window",
        "schema": { "type": "integer" }
      },
      "X-RateLimit-Remaining": {
        "description": "Requests remaining in the current window",
        "schema": { "type": "integer" }
      },
      "X-RateLimit-Reset": {
        "description": "Unix timestamp when the window resets",
        "schema": { "type": "integer" }
      },
      "Retry-After": {
        "description": "Seconds to wait before retrying (only on 429)",
        "schema": { "type": "integer" }
      }
    }
  },
  "paths": {
    "/api/v1/categories": {
      "get": {
        "summary": "List categories",
        "description": "Returns all valid categories with their color schemes. No authentication required.",
        "operationId": "listCategories",
        "tags": ["Categories"],
        "responses": {
          "200": {
            "description": "List of categories",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "categories": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "name": { "$ref": "#/components/schemas/Category" },
                          "colors": {
                            "type": "object",
                            "nullable": true,
                            "properties": {
                              "bg": { "type": "string" },
                              "accent": { "type": "string" },
                              "lightBg": { "type": "string" }
                            }
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/ideas": {
      "get": {
        "summary": "List ideas",
        "description": "Search and list ideas with pagination and category filtering.",
        "operationId": "listIdeas",
        "tags": ["Ideas"],
        "parameters": [
          { "$ref": "#/components/parameters/page" },
          { "$ref": "#/components/parameters/q" },
          { "$ref": "#/components/parameters/category" }
        ],
        "responses": {
          "200": {
            "description": "Paginated list of ideas",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ideas": { "type": "array", "items": { "$ref": "#/components/schemas/Idea" } },
                    "total": { "type": "integer" },
                    "page": { "type": "integer" },
                    "totalPages": { "type": "integer" }
                  }
                }
              }
            }
          }
        }
      },
      "post": {
        "summary": "Create idea",
        "description": "Create a new app idea.",
        "operationId": "createIdea",
        "tags": ["Ideas"],
        "security": [{ "clerkSession": [] }, { "apiKey": [] }],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["title", "description", "category"],
                "properties": {
                  "title": { "type": "string", "maxLength": 200 },
                  "description": { "type": "string" },
                  "category": { "$ref": "#/components/schemas/Category" },
                  "tags": { "type": "array", "items": { "type": "string" } }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Idea created",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Idea" } } }
          },
          "400": {
            "description": "Invalid input",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } }
          },
          "401": {
            "description": "Unauthorized",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } }
          }
        }
      }
    },
    "/api/v1/ideas/{id}": {
      "get": {
        "summary": "Get idea",
        "description": "Get a single idea with author, vote count, and linked mockups.",
        "operationId": "getIdea",
        "tags": ["Ideas"],
        "parameters": [{ "name": "id", "in": "path", "required": true, "schema": { "type": "string" } }],
        "responses": {
          "200": {
            "description": "Idea details",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Idea" } } }
          },
          "404": {
            "description": "Not found",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } }
          }
        }
      },
      "patch": {
        "summary": "Update idea",
        "description": "Update an existing idea (owner only).",
        "operationId": "updateIdea",
        "tags": ["Ideas"],
        "security": [{ "clerkSession": [] }, { "apiKey": [] }],
        "parameters": [{ "name": "id", "in": "path", "required": true, "schema": { "type": "string" } }],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "title": { "type": "string" },
                  "description": { "type": "string" },
                  "category": { "$ref": "#/components/schemas/Category" },
                  "tags": { "type": "array", "items": { "type": "string" } }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Updated",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Idea" } } }
          },
          "403": {
            "description": "Not owner",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } }
          },
          "404": {
            "description": "Not found",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } }
          }
        }
      },
      "delete": {
        "summary": "Delete idea",
        "description": "Soft-delete an idea (owner only). 7-day restore window.",
        "operationId": "deleteIdea",
        "tags": ["Ideas"],
        "security": [{ "clerkSession": [] }, { "apiKey": [] }],
        "parameters": [{ "name": "id", "in": "path", "required": true, "schema": { "type": "string" } }],
        "responses": {
          "200": {
            "description": "Deleted",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": { "type": "boolean" },
                    "deletedAt": { "type": "string", "format": "date-time" }
                  }
                }
              }
            }
          },
          "403": {
            "description": "Not owner",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } }
          }
        }
      }
    },
    "/api/v1/ideas/{id}/vote": {
      "post": {
        "summary": "Toggle vote",
        "description": "Vote or unvote on an idea (toggle).",
        "operationId": "voteIdea",
        "tags": ["Ideas"],
        "security": [{ "clerkSession": [] }, { "apiKey": [] }],
        "parameters": [{ "name": "id", "in": "path", "required": true, "schema": { "type": "string" } }],
        "responses": {
          "200": {
            "description": "Vote toggled",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": { "voted": { "type": "boolean" }, "count": { "type": "integer" } }
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/ideas/{id}/restore": {
      "post": {
        "summary": "Restore idea",
        "description": "Restore a soft-deleted idea within 7-day grace period.",
        "operationId": "restoreIdea",
        "tags": ["Ideas"],
        "security": [{ "clerkSession": [] }, { "apiKey": [] }],
        "parameters": [{ "name": "id", "in": "path", "required": true, "schema": { "type": "string" } }],
        "responses": {
          "200": {
            "description": "Restored",
            "content": {
              "application/json": { "schema": { "type": "object", "properties": { "success": { "type": "boolean" } } } }
            }
          },
          "400": {
            "description": "Not deleted or restore period expired",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } }
          }
        }
      }
    },
    "/api/v1/mockups": {
      "get": {
        "summary": "List mockups",
        "description": "Search and list mockups with pagination and category filtering.",
        "operationId": "listMockups",
        "tags": ["Mockups"],
        "parameters": [
          { "$ref": "#/components/parameters/page" },
          { "$ref": "#/components/parameters/q" },
          { "$ref": "#/components/parameters/category" }
        ],
        "responses": {
          "200": {
            "description": "Paginated list of mockups",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "mockups": { "type": "array", "items": { "$ref": "#/components/schemas/Mockup" } },
                    "total": { "type": "integer" },
                    "page": { "type": "integer" },
                    "totalPages": { "type": "integer" }
                  }
                }
              }
            }
          }
        }
      },
      "post": {
        "summary": "Create mockup",
        "description": "Create a mockup record after uploading the file via the upload-url endpoint.",
        "operationId": "createMockup",
        "tags": ["Mockups"],
        "security": [{ "clerkSession": [] }, { "apiKey": [] }],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["name", "description", "category", "s3Key"],
                "properties": {
                  "name": { "type": "string", "maxLength": 200 },
                  "description": { "type": "string" },
                  "category": { "$ref": "#/components/schemas/Category" },
                  "s3Key": { "type": "string", "description": "S3 key from the upload-url endpoint" },
                  "tags": { "type": "array", "items": { "type": "string" } },
                  "ideaId": { "type": "string", "description": "Link to an idea" },
                  "type": { "type": "string", "enum": ["html", "webcontainer"], "default": "html" },
                  "startScript": {
                    "type": "string",
                    "description": "npm script for webcontainer (e.g., 'start', 'dev')"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Mockup created",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Mockup" } } }
          },
          "400": {
            "description": "Invalid input",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } }
          },
          "403": {
            "description": "Upload limit reached",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } }
          }
        }
      }
    },
    "/api/v1/mockups/{id}": {
      "get": {
        "summary": "Get mockup",
        "description": "Get a single mockup with creator, linked idea, and feedback count.",
        "operationId": "getMockup",
        "tags": ["Mockups"],
        "parameters": [{ "name": "id", "in": "path", "required": true, "schema": { "type": "string" } }],
        "responses": {
          "200": {
            "description": "Mockup details",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Mockup" } } }
          },
          "404": {
            "description": "Not found",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } }
          }
        }
      },
      "patch": {
        "summary": "Update mockup",
        "description": "Update mockup metadata, file, or type (owner only).",
        "operationId": "updateMockup",
        "tags": ["Mockups"],
        "security": [{ "clerkSession": [] }, { "apiKey": [] }],
        "parameters": [{ "name": "id", "in": "path", "required": true, "schema": { "type": "string" } }],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "name": { "type": "string" },
                  "description": { "type": "string" },
                  "category": { "$ref": "#/components/schemas/Category" },
                  "tags": { "type": "array", "items": { "type": "string" } },
                  "type": { "type": "string", "enum": ["html", "webcontainer"] },
                  "startScript": { "type": "string" },
                  "status": { "type": "string", "enum": ["draft", "ready"] }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Updated",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Mockup" } } }
          },
          "403": {
            "description": "Not owner",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } }
          }
        }
      },
      "delete": {
        "summary": "Delete mockup",
        "description": "Soft-delete a mockup (owner only). 7-day restore window.",
        "operationId": "deleteMockup",
        "tags": ["Mockups"],
        "security": [{ "clerkSession": [] }, { "apiKey": [] }],
        "parameters": [{ "name": "id", "in": "path", "required": true, "schema": { "type": "string" } }],
        "responses": {
          "200": {
            "description": "Deleted",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": { "type": "boolean" },
                    "deletedAt": { "type": "string", "format": "date-time" }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/mockups/{id}/feedback": {
      "get": {
        "summary": "List feedback",
        "description": "List feedback for a mockup with pagination.",
        "operationId": "listFeedback",
        "tags": ["Feedback"],
        "parameters": [
          { "name": "id", "in": "path", "required": true, "schema": { "type": "string" } },
          { "$ref": "#/components/parameters/page" }
        ],
        "responses": {
          "200": {
            "description": "Paginated feedback",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "feedbacks": { "type": "array", "items": { "$ref": "#/components/schemas/Feedback" } },
                    "total": { "type": "integer" },
                    "page": { "type": "integer" },
                    "totalPages": { "type": "integer" }
                  }
                }
              }
            }
          }
        }
      },
      "post": {
        "summary": "Post feedback",
        "description": "Post feedback on a mockup with optional rating.",
        "operationId": "createFeedback",
        "tags": ["Feedback"],
        "security": [{ "clerkSession": [] }, { "apiKey": [] }],
        "parameters": [{ "name": "id", "in": "path", "required": true, "schema": { "type": "string" } }],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["content"],
                "properties": {
                  "content": { "type": "string" },
                  "rating": { "type": "integer", "minimum": 1, "maximum": 5 }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Feedback created",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Feedback" } } }
          }
        }
      }
    },
    "/api/v1/mockups/{id}/download": {
      "get": {
        "summary": "Download mockup",
        "description": "Get a presigned S3 download URL for the mockup file.",
        "operationId": "downloadMockup",
        "tags": ["Mockups"],
        "parameters": [{ "name": "id", "in": "path", "required": true, "schema": { "type": "string" } }],
        "responses": {
          "200": {
            "description": "Download URL",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "downloadUrl": {
                      "type": "string",
                      "format": "uri",
                      "description": "Presigned S3 URL (15-min expiry)"
                    },
                    "fileName": { "type": "string" }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/mockups/{id}/reaction": {
      "get": {
        "summary": "Get reactions",
        "description": "Get like/dislike counts and current user's reaction.",
        "operationId": "getReactions",
        "tags": ["Mockups"],
        "parameters": [{ "name": "id", "in": "path", "required": true, "schema": { "type": "string" } }],
        "responses": {
          "200": {
            "description": "Reaction counts",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "likes": { "type": "integer" },
                    "dislikes": { "type": "integer" },
                    "userReaction": { "type": "string", "enum": ["like", "dislike"], "nullable": true }
                  }
                }
              }
            }
          }
        }
      },
      "post": {
        "summary": "React to mockup",
        "description": "Like or dislike a mockup (toggle).",
        "operationId": "reactToMockup",
        "tags": ["Mockups"],
        "security": [{ "clerkSession": [] }, { "apiKey": [] }],
        "parameters": [{ "name": "id", "in": "path", "required": true, "schema": { "type": "string" } }],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["type"],
                "properties": {
                  "type": { "type": "string", "enum": ["like", "dislike"] }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Reaction updated",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "likes": { "type": "integer" },
                    "dislikes": { "type": "integer" },
                    "userReaction": { "type": "string", "enum": ["like", "dislike"], "nullable": true }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/mockups/upload-url": {
      "post": {
        "summary": "Get upload URL",
        "description": "Get a presigned S3 URL for uploading a mockup file. Subject to plan-based upload limits.",
        "operationId": "getUploadUrl",
        "tags": ["Mockups"],
        "security": [{ "clerkSession": [] }, { "apiKey": [] }],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["fileName", "contentType"],
                "properties": {
                  "fileName": { "type": "string" },
                  "contentType": { "type": "string" }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Upload URL and S3 key",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "uploadUrl": { "type": "string", "format": "uri" },
                    "s3Key": { "type": "string" }
                  }
                }
              }
            }
          },
          "403": {
            "description": "Upload limit reached",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } }
          }
        }
      }
    },
    "/api/v1/mockups/{id}/restore": {
      "post": {
        "summary": "Restore mockup",
        "description": "Restore a soft-deleted mockup within 7-day grace period.",
        "operationId": "restoreMockup",
        "tags": ["Mockups"],
        "security": [{ "clerkSession": [] }, { "apiKey": [] }],
        "parameters": [{ "name": "id", "in": "path", "required": true, "schema": { "type": "string" } }],
        "responses": {
          "200": { "description": "Restored" }
        }
      }
    },
    "/api/v1/generations": {
      "get": {
        "summary": "List generation jobs",
        "description": "List your AI generation jobs (latest 20).",
        "operationId": "listGenerations",
        "tags": ["Generations"],
        "security": [{ "clerkSession": [] }, { "apiKey": [] }],
        "responses": {
          "200": {
            "description": "Generation jobs",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "jobs": { "type": "array", "items": { "$ref": "#/components/schemas/GenerationJob" } }
                  }
                }
              }
            }
          }
        }
      },
      "post": {
        "summary": "Generate AI mockup",
        "description": "Trigger AI mockup generation for an idea. Requires Pro+ plan and credits.",
        "operationId": "generateMockup",
        "tags": ["Generations"],
        "security": [{ "clerkSession": [] }, { "apiKey": [] }],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["ideaId"],
                "properties": {
                  "ideaId": { "type": "string" },
                  "model": {
                    "type": "string",
                    "enum": ["claude-sonnet-4-6", "claude-opus-4-6"],
                    "default": "claude-sonnet-4-6"
                  },
                  "maxTokens": { "type": "integer", "enum": [16000, 32000, 64000, 128000], "default": 16000 }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Generation started",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "jobId": { "type": "string" },
                    "existing": { "type": "boolean" }
                  }
                }
              }
            }
          },
          "403": {
            "description": "Insufficient plan or credits",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } }
          }
        }
      }
    },
    "/api/v1/generations/{id}": {
      "get": {
        "summary": "Get generation status",
        "description": "Poll the status of an AI generation job.",
        "operationId": "getGenerationStatus",
        "tags": ["Generations"],
        "security": [{ "clerkSession": [] }, { "apiKey": [] }],
        "parameters": [{ "name": "id", "in": "path", "required": true, "schema": { "type": "string" } }],
        "responses": {
          "200": {
            "description": "Job status",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/GenerationJob" } } }
          },
          "404": {
            "description": "Job not found",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } }
          }
        }
      }
    },
    "/api/v1/subscriptions/status": {
      "get": {
        "summary": "Get subscription status",
        "description": "Get current user's plan, limits, and credit balance.",
        "operationId": "getSubscriptionStatus",
        "tags": ["Subscriptions"],
        "security": [{ "clerkSession": [] }, { "apiKey": [] }],
        "responses": {
          "200": {
            "description": "Subscription status",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "plan": { "type": "string", "enum": ["free", "pro", "business"] },
                    "limits": { "type": "object" },
                    "credits": { "type": "integer" },
                    "subscription": { "type": "object", "nullable": true }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/notifications": {
      "get": {
        "summary": "List notifications",
        "description": "Get user's notifications with unread count.",
        "operationId": "listNotifications",
        "tags": ["Notifications"],
        "security": [{ "clerkSession": [] }, { "apiKey": [] }],
        "parameters": [
          { "name": "limit", "in": "query", "schema": { "type": "integer", "default": 20, "maximum": 50 } }
        ],
        "responses": {
          "200": {
            "description": "Notifications",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "notifications": { "type": "array", "items": { "$ref": "#/components/schemas/Notification" } },
                    "unreadCount": { "type": "integer" }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/api-keys": {
      "get": {
        "summary": "List API keys",
        "description": "List your API keys (prefix only, not full keys).",
        "operationId": "listApiKeys",
        "tags": ["API Keys"],
        "security": [{ "clerkSession": [] }, { "apiKey": [] }],
        "responses": {
          "200": {
            "description": "API keys",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "keys": { "type": "array", "items": { "$ref": "#/components/schemas/ApiKey" } }
                  }
                }
              }
            }
          }
        }
      },
      "post": {
        "summary": "Create API key",
        "description": "Create a new API key. The full key is only returned once in this response.",
        "operationId": "createApiKey",
        "tags": ["API Keys"],
        "security": [{ "clerkSession": [] }, { "apiKey": [] }],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["name", "scopes"],
                "properties": {
                  "name": { "type": "string" },
                  "scopes": {
                    "type": "array",
                    "items": {
                      "type": "string",
                      "enum": [
                        "ideas:read",
                        "ideas:write",
                        "mockups:read",
                        "mockups:write",
                        "feedback:read",
                        "feedback:write",
                        "generations:read",
                        "generations:write",
                        "profile:read"
                      ]
                    },
                    "minItems": 1
                  },
                  "expiresIn": { "type": "string", "enum": ["30d", "90d", "1y"], "nullable": true }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "API key created (full key shown only once)",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "key": { "type": "string", "description": "Full API key (shown only once)" },
                    "prefix": { "type": "string" },
                    "scopes": { "type": "array", "items": { "type": "string" } },
                    "expiresAt": { "type": "string", "format": "date-time", "nullable": true }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Max 10 keys or invalid scopes",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } }
          }
        }
      }
    },
    "/api/v1/api-keys/{id}": {
      "delete": {
        "summary": "Revoke API key",
        "description": "Revoke an API key (soft delete).",
        "operationId": "revokeApiKey",
        "tags": ["API Keys"],
        "security": [{ "clerkSession": [] }, { "apiKey": [] }],
        "parameters": [{ "name": "id", "in": "path", "required": true, "schema": { "type": "string" } }],
        "responses": {
          "200": {
            "description": "Revoked",
            "content": {
              "application/json": { "schema": { "type": "object", "properties": { "success": { "type": "boolean" } } } }
            }
          }
        }
      }
    },
    "/api/v1/reports": {
      "post": {
        "summary": "Report content",
        "description": "Report a mockup, idea, or feedback for moderation.",
        "operationId": "createReport",
        "tags": ["Reports"],
        "security": [{ "clerkSession": [] }, { "apiKey": [] }],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["type", "targetId", "reason"],
                "properties": {
                  "type": { "type": "string", "enum": ["mockup", "idea", "feedback"] },
                  "targetId": { "type": "string" },
                  "reason": { "type": "string" }
                }
              }
            }
          }
        },
        "responses": {
          "201": { "description": "Report created" },
          "409": {
            "description": "Already reported",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } }
          }
        }
      }
    },
    "/api/v1/mockups/{id}/iterate": {
      "post": {
        "summary": "Iterate on a mockup",
        "description": "Submit a chat-based iteration on an existing mockup. Creates a new revision with the updated HTML. Batch mode costs 1 credit (5-20 min), streaming costs 2 credits (1-5 min, Pro+ only).",
        "operationId": "iterateMockup",
        "tags": ["Iterations"],
        "security": [{ "clerkSession": [] }, { "apiKey": [] }],
        "parameters": [
          { "name": "id", "in": "path", "required": true, "schema": { "type": "string" }, "description": "Mockup ID" }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["prompt"],
                "properties": {
                  "prompt": { "type": "string", "maxLength": 2000, "description": "Description of changes to make" },
                  "mode": {
                    "type": "string",
                    "enum": ["batch", "streaming"],
                    "default": "batch",
                    "description": "batch (1 credit, 5-20 min) or streaming (2 credits, 1-5 min, Pro+ only)"
                  },
                  "maxTokens": {
                    "type": "integer",
                    "enum": [16000, 32000, 64000, 128000],
                    "description": "Token budget (default: auto based on mockup size)"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Iteration queued",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "jobId": { "type": "string" },
                    "conversationId": { "type": "string" },
                    "messageId": { "type": "string" },
                    "mode": { "type": "string" },
                    "creditCost": { "type": "integer" }
                  }
                }
              }
            }
          },
          "403": {
            "description": "Insufficient credits or plan required",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } }
          }
        }
      }
    },
    "/api/v1/mockups/{id}/conversation": {
      "get": {
        "summary": "Get iteration conversation",
        "description": "Returns the chat iteration history for a mockup.",
        "operationId": "getMockupConversation",
        "tags": ["Iterations"],
        "security": [{ "clerkSession": [] }, { "apiKey": [] }],
        "parameters": [
          { "name": "id", "in": "path", "required": true, "schema": { "type": "string" }, "description": "Mockup ID" }
        ],
        "responses": {
          "200": {
            "description": "Conversation messages",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "conversationId": { "type": "string", "nullable": true },
                    "messages": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "id": { "type": "string" },
                          "role": { "type": "string", "enum": ["user", "assistant"] },
                          "content": { "type": "string" },
                          "htmlS3Key": { "type": "string", "nullable": true },
                          "createdAt": { "type": "string", "format": "date-time" },
                          "humanVerified": { "type": "boolean" }
                        }
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/mockups/{id}/active-job": {
      "get": {
        "summary": "Get active iteration job",
        "description": "Returns the currently in-progress iteration job for this mockup (if any). Used to resume polling after page refresh.",
        "operationId": "getActiveJob",
        "tags": ["Iterations"],
        "security": [{ "clerkSession": [] }, { "apiKey": [] }],
        "parameters": [
          { "name": "id", "in": "path", "required": true, "schema": { "type": "string" }, "description": "Mockup ID" }
        ],
        "responses": {
          "200": {
            "description": "Active job or null",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "jobId": { "type": "string", "nullable": true },
                    "status": { "type": "string", "nullable": true }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/mockups/{id}/revisions/{revisionId}/rate": {
      "post": {
        "summary": "Rate a revision",
        "description": "Set thumbs up/down rating on a mockup revision (owner only). Send null to clear.",
        "operationId": "rateRevision",
        "tags": ["Revisions"],
        "security": [{ "clerkSession": [] }, { "apiKey": [] }],
        "parameters": [
          { "name": "id", "in": "path", "required": true, "schema": { "type": "string" } },
          { "name": "revisionId", "in": "path", "required": true, "schema": { "type": "string" } }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "rating": { "type": "string", "enum": ["up", "down"], "nullable": true }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Rating updated",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": { "ok": { "type": "boolean" }, "rating": { "type": "string", "nullable": true } }
                }
              }
            }
          },
          "403": {
            "description": "Not the mockup owner",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } }
          }
        }
      }
    },
    "/api/v1/mockups/{id}/revisions/{revisionId}/set-current": {
      "post": {
        "summary": "Set revision as current",
        "description": "Set a specific revision as the active mockup version. Updates the mockup preview to show this revision's HTML. Owner only.",
        "operationId": "setCurrentRevision",
        "tags": ["Revisions"],
        "security": [{ "clerkSession": [] }, { "apiKey": [] }],
        "parameters": [
          { "name": "id", "in": "path", "required": true, "schema": { "type": "string" } },
          { "name": "revisionId", "in": "path", "required": true, "schema": { "type": "string" } }
        ],
        "responses": {
          "200": {
            "description": "Revision activated",
            "content": {
              "application/json": { "schema": { "type": "object", "properties": { "ok": { "type": "boolean" } } } }
            }
          },
          "403": { "description": "Not the mockup owner" }
        }
      }
    },
    "/api/v1/mockups/{id}/revisions/{revisionId}": {
      "delete": {
        "summary": "Delete a revision",
        "description": "Permanently delete a mockup revision and its S3 files. Cannot delete the currently active revision. Owner only.",
        "operationId": "deleteRevision",
        "tags": ["Revisions"],
        "security": [{ "clerkSession": [] }, { "apiKey": [] }],
        "parameters": [
          { "name": "id", "in": "path", "required": true, "schema": { "type": "string" } },
          { "name": "revisionId", "in": "path", "required": true, "schema": { "type": "string" } }
        ],
        "responses": {
          "200": {
            "description": "Revision deleted",
            "content": {
              "application/json": { "schema": { "type": "object", "properties": { "ok": { "type": "boolean" } } } }
            }
          },
          "400": {
            "description": "Cannot delete the active revision",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } }
          },
          "403": { "description": "Not the mockup owner" }
        }
      }
    },
    "/api/v1/push/subscribe": {
      "post": {
        "summary": "Subscribe to push notifications",
        "description": "Register a Web Push subscription for the authenticated user. Used for generation-completion notifications.",
        "operationId": "subscribePush",
        "tags": ["Notifications"],
        "security": [{ "clerkSession": [] }, { "apiKey": [] }],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["endpoint", "keys"],
                "properties": {
                  "endpoint": { "type": "string", "description": "Push subscription endpoint URL" },
                  "keys": {
                    "type": "object",
                    "required": ["p256dh", "auth"],
                    "properties": {
                      "p256dh": { "type": "string" },
                      "auth": { "type": "string" }
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": { "description": "Subscribed" }
        }
      },
      "delete": {
        "summary": "Unsubscribe from push notifications",
        "description": "Remove a Web Push subscription.",
        "operationId": "unsubscribePush",
        "tags": ["Notifications"],
        "security": [{ "clerkSession": [] }, { "apiKey": [] }],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["endpoint"],
                "properties": {
                  "endpoint": { "type": "string" }
                }
              }
            }
          }
        },
        "responses": {
          "200": { "description": "Unsubscribed" }
        }
      }
    },
    "/api/v1/ideas/gallery": {
      "get": {
        "summary": "Browse ideas (cursor)",
        "description": "Public infinite-scroll listing of approved ideas. Keyset cursor pagination (deletion-safe).",
        "operationId": "listIdeasGallery",
        "tags": ["Ideas"],
        "parameters": [
          {
            "name": "cursor",
            "in": "query",
            "schema": { "type": "string" },
            "description": "Keyset cursor from a prior response"
          },
          {
            "name": "limit",
            "in": "query",
            "schema": { "type": "integer", "minimum": 1, "maximum": 24, "default": 12 }
          },
          { "$ref": "#/components/parameters/q" },
          { "$ref": "#/components/parameters/category" },
          { "name": "tag", "in": "query", "schema": { "type": "string" }, "description": "Comma-joined tags" },
          {
            "name": "creator",
            "in": "query",
            "schema": { "type": "string" },
            "description": "Comma-joined creator usernames"
          }
        ],
        "responses": {
          "200": {
            "description": "Cursor page of ideas",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "items": { "type": "array", "items": { "$ref": "#/components/schemas/Idea" } },
                    "nextCursor": { "type": "string", "nullable": true }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/mockups/gallery": {
      "get": {
        "summary": "Browse prototypes (cursor)",
        "description": "Public infinite-scroll listing of approved prototypes. Keyset cursor pagination.",
        "operationId": "listMockupsGallery",
        "tags": ["Mockups"],
        "parameters": [
          { "name": "cursor", "in": "query", "schema": { "type": "string" } },
          {
            "name": "limit",
            "in": "query",
            "schema": { "type": "integer", "minimum": 1, "maximum": 24, "default": 12 }
          },
          { "$ref": "#/components/parameters/q" },
          { "$ref": "#/components/parameters/category" },
          { "name": "tag", "in": "query", "schema": { "type": "string" } },
          { "name": "creator", "in": "query", "schema": { "type": "string" } }
        ],
        "responses": {
          "200": {
            "description": "Cursor page of prototypes",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "items": { "type": "array", "items": { "$ref": "#/components/schemas/Mockup" } },
                    "nextCursor": { "type": "string", "nullable": true }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/ideas/{id}/generate-video": {
      "post": {
        "summary": "Generate a video for an idea",
        "description": "Kick off AI video generation (thumbnail or CTA) for an idea. Returns a jobId to poll via GET /videos/{jobId}. Costs credits; voices are plan-gated.",
        "operationId": "generateIdeaVideo",
        "tags": ["Video"],
        "security": [{ "clerkSession": [] }, { "apiKey": [] }],
        "parameters": [{ "name": "id", "in": "path", "required": true, "schema": { "type": "string" } }],
        "requestBody": {
          "required": true,
          "content": { "application/json": { "schema": { "$ref": "#/components/schemas/GenerateVideoRequest" } } }
        },
        "responses": {
          "200": {
            "description": "Job queued",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/VideoJob" } } }
          },
          "401": {
            "description": "Unauthorized",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } }
          },
          "402": {
            "description": "Insufficient credits",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } }
          },
          "404": {
            "description": "Idea not found",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } }
          }
        }
      }
    },
    "/api/v1/mockups/{id}/generate-video": {
      "post": {
        "summary": "Generate a video for a prototype",
        "description": "Kick off AI video generation for a prototype. Returns a jobId to poll. Costs credits; voices are plan-gated.",
        "operationId": "generateMockupVideo",
        "tags": ["Video"],
        "security": [{ "clerkSession": [] }, { "apiKey": [] }],
        "parameters": [{ "name": "id", "in": "path", "required": true, "schema": { "type": "string" } }],
        "requestBody": {
          "required": true,
          "content": { "application/json": { "schema": { "$ref": "#/components/schemas/GenerateVideoRequest" } } }
        },
        "responses": {
          "200": {
            "description": "Job queued",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/VideoJob" } } }
          },
          "401": {
            "description": "Unauthorized",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } }
          },
          "402": {
            "description": "Insufficient credits",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } }
          },
          "404": {
            "description": "Prototype not found",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } }
          }
        }
      }
    },
    "/api/v1/ideas/{id}/videos": {
      "get": {
        "summary": "List an idea's videos",
        "description": "Editor-scoped gallery of all Videos created for an idea (author, admins, edit-share holders).",
        "operationId": "listIdeaVideos",
        "tags": ["Video"],
        "security": [{ "clerkSession": [] }, { "apiKey": [] }],
        "parameters": [{ "name": "id", "in": "path", "required": true, "schema": { "type": "string" } }],
        "responses": {
          "200": {
            "description": "Videos for the idea",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": { "videos": { "type": "array", "items": { "$ref": "#/components/schemas/Video" } } }
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/mockups/{id}/videos": {
      "get": {
        "summary": "List a prototype's videos",
        "description": "Editor-scoped gallery of all Videos created for a prototype.",
        "operationId": "listMockupVideos",
        "tags": ["Video"],
        "security": [{ "clerkSession": [] }, { "apiKey": [] }],
        "parameters": [{ "name": "id", "in": "path", "required": true, "schema": { "type": "string" } }],
        "responses": {
          "200": {
            "description": "Videos for the prototype",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": { "videos": { "type": "array", "items": { "$ref": "#/components/schemas/Video" } } }
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/ideas/{id}/promote-video": {
      "post": {
        "summary": "Promote a video job to the idea's current CTA (legacy)",
        "description": "Legacy: promote a completed generation job to the idea's current CTA video. Superseded by featuring (POST /videos/library/{videoId}/feature).",
        "operationId": "promoteIdeaVideo",
        "tags": ["Video"],
        "security": [{ "clerkSession": [] }, { "apiKey": [] }],
        "parameters": [{ "name": "id", "in": "path", "required": true, "schema": { "type": "string" } }],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": { "type": "object", "required": ["jobId"], "properties": { "jobId": { "type": "string" } } }
            }
          }
        },
        "responses": {
          "200": { "description": "Promoted" },
          "404": {
            "description": "Not found",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } }
          }
        }
      }
    },
    "/api/v1/videos/generate": {
      "post": {
        "summary": "Generate a video (explicit target)",
        "description": "Kick off AI video generation against an idea or prototype target. Returns a jobId. Equivalent to the entity generate-video endpoints with the target in the body.",
        "operationId": "generateVideo",
        "tags": ["Video"],
        "security": [{ "clerkSession": [] }, { "apiKey": [] }],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "allOf": [
                  { "$ref": "#/components/schemas/GenerateVideoRequest" },
                  {
                    "type": "object",
                    "required": ["targetType", "targetId"],
                    "properties": {
                      "targetType": { "type": "string", "enum": ["idea", "mockup"] },
                      "targetId": { "type": "string" }
                    }
                  }
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Job queued",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/VideoJob" } } }
          },
          "402": {
            "description": "Insufficient credits",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } }
          }
        }
      }
    },
    "/api/v1/videos/{jobId}": {
      "get": {
        "summary": "Poll a video job",
        "description": "Returns the job's status (queued → planning → voicing → compiling → rendering → completed/failed), the plan once authored, and the video URL once rendered.",
        "operationId": "getVideoJob",
        "tags": ["Video"],
        "security": [{ "clerkSession": [] }, { "apiKey": [] }],
        "parameters": [{ "name": "jobId", "in": "path", "required": true, "schema": { "type": "string" } }],
        "responses": {
          "200": {
            "description": "Job status",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/VideoJob" } } }
          },
          "404": {
            "description": "Not found",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } }
          }
        }
      },
      "delete": {
        "summary": "Delete a video job",
        "description": "Delete a video generation job/revision. Clears the entity's denormalized CTA video if this was the published render.",
        "operationId": "deleteVideoJob",
        "tags": ["Video"],
        "security": [{ "clerkSession": [] }, { "apiKey": [] }],
        "parameters": [{ "name": "jobId", "in": "path", "required": true, "schema": { "type": "string" } }],
        "responses": {
          "200": { "description": "Deleted" },
          "404": {
            "description": "Not found",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } }
          }
        }
      }
    },
    "/api/v1/videos/{jobId}/iterate": {
      "post": {
        "summary": "Iterate a video",
        "description": "Surgical scene edit (reuses voiceovers) or full regenerate when duration/voice/regenerate change. ~10 credits.",
        "operationId": "iterateVideo",
        "tags": ["Video"],
        "security": [{ "clerkSession": [] }, { "apiKey": [] }],
        "parameters": [{ "name": "jobId", "in": "path", "required": true, "schema": { "type": "string" } }],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["brief"],
                "properties": {
                  "brief": { "type": "string", "maxLength": 2000 },
                  "duration": { "type": "integer" },
                  "voice": { "type": "string" },
                  "resolution": { "type": "string", "enum": ["720p", "1080p", "4K"] },
                  "regenerate": { "type": "boolean" }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Iteration queued",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/VideoJob" } } }
          },
          "402": {
            "description": "Insufficient credits",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } }
          }
        }
      }
    },
    "/api/v1/videos/{jobId}/render": {
      "post": {
        "summary": "Re-render at a resolution",
        "description": "Pure re-render of an existing video at a new resolution (no AI, no voiceover regen). Inherits orientation + language.",
        "operationId": "rerenderVideo",
        "tags": ["Video"],
        "security": [{ "clerkSession": [] }, { "apiKey": [] }],
        "parameters": [{ "name": "jobId", "in": "path", "required": true, "schema": { "type": "string" } }],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["resolution"],
                "properties": { "resolution": { "type": "string", "enum": ["720p", "1080p", "4K"] } }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Render queued",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/VideoJob" } } }
          }
        }
      }
    },
    "/api/v1/videos/{jobId}/autofit-durations": {
      "post": {
        "summary": "Auto-fit scene durations",
        "description": "Fit per-scene durations to the narration length (bulk, or per-scene overrides).",
        "operationId": "autofitVideoDurations",
        "tags": ["Video"],
        "security": [{ "clerkSession": [] }, { "apiKey": [] }],
        "parameters": [{ "name": "jobId", "in": "path", "required": true, "schema": { "type": "string" } }],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "scenes": {
                    "type": "array",
                    "items": {
                      "type": "object",
                      "properties": { "id": { "type": "string" }, "durationFrames": { "type": "integer" } }
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Auto-fit queued",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/VideoJob" } } }
          }
        }
      }
    },
    "/api/v1/videos/{jobId}/source": {
      "get": {
        "summary": "Download the Remotion source",
        "description": "Presigned URL (1 hour) to download the runnable Remotion project zip. Available once the job reaches the rendering stage.",
        "operationId": "downloadVideoSource",
        "tags": ["Video"],
        "security": [{ "clerkSession": [] }, { "apiKey": [] }],
        "parameters": [{ "name": "jobId", "in": "path", "required": true, "schema": { "type": "string" } }],
        "responses": {
          "200": {
            "description": "Presigned download URL",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "downloadUrl": { "type": "string" },
                    "fileName": { "type": "string" },
                    "expiresInSeconds": { "type": "integer" }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/videos/voices": {
      "get": {
        "summary": "List video voices",
        "description": "Curated ElevenLabs voices available to the caller's plan, optionally filtered by locale. Free → none (system default); Pro → curated; Business → curated + raw IDs.",
        "operationId": "listVideoVoices",
        "tags": ["Video"],
        "security": [{ "clerkSession": [] }, { "apiKey": [] }],
        "parameters": [
          {
            "name": "locale",
            "in": "query",
            "schema": { "type": "string" },
            "description": "BCP-47 filter, e.g. en-US"
          }
        ],
        "responses": {
          "200": {
            "description": "Available voices",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "voices": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "id": { "type": "string" },
                          "name": { "type": "string" },
                          "gender": { "type": "string" },
                          "locale": { "type": "string" },
                          "previewUrl": { "type": "string" },
                          "minPlanTier": { "type": "string" }
                        }
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/videos/gallery": {
      "get": {
        "summary": "Browse the public Video Gallery",
        "description": "Public listing of gallery-visible videos (featured on a public entity OR explicitly published). Keyset cursor pagination.",
        "operationId": "listVideoGallery",
        "tags": ["Video"],
        "parameters": [
          { "name": "cursor", "in": "query", "schema": { "type": "string" } },
          {
            "name": "limit",
            "in": "query",
            "schema": { "type": "integer", "minimum": 1, "maximum": 24, "default": 12 }
          },
          { "$ref": "#/components/parameters/q" },
          { "$ref": "#/components/parameters/category" },
          { "name": "lang", "in": "query", "schema": { "type": "string" }, "description": "BCP-47 language filter" },
          { "name": "v", "in": "query", "schema": { "type": "string" }, "description": "Single-video deep link" }
        ],
        "responses": {
          "200": {
            "description": "Cursor page of gallery videos",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "items": { "type": "array", "items": { "$ref": "#/components/schemas/Video" } },
                    "nextCursor": { "type": "string", "nullable": true }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/videos/library/{videoId}": {
      "get": {
        "summary": "Get a video (Studio payload)",
        "description": "Full video record: every language variant with its published render + revisions (primary first). Owner/editor-scoped.",
        "operationId": "getVideo",
        "tags": ["Video"],
        "security": [{ "clerkSession": [] }, { "apiKey": [] }],
        "parameters": [{ "name": "videoId", "in": "path", "required": true, "schema": { "type": "string" } }],
        "responses": {
          "200": {
            "description": "Video + variants",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Video" } } }
          },
          "404": {
            "description": "Not found",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } }
          }
        }
      },
      "patch": {
        "summary": "Edit video metadata",
        "description": "Rename, set purpose, archive/unarchive, and publish/unpublish to the public gallery.",
        "operationId": "updateVideo",
        "tags": ["Video"],
        "security": [{ "clerkSession": [] }, { "apiKey": [] }],
        "parameters": [{ "name": "videoId", "in": "path", "required": true, "schema": { "type": "string" } }],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "title": { "type": "string", "maxLength": 120 },
                  "purpose": { "type": "string", "enum": ["conceptual", "marketing", "explainer", "promo"] },
                  "archive": { "type": "boolean" },
                  "publishToGallery": { "type": "boolean" }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Updated",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Video" } } }
          }
        }
      },
      "delete": {
        "summary": "Delete a video",
        "description": "Delete the entire Video (cascades its language variants; jobs keep a SetNull audit trail).",
        "operationId": "deleteVideo",
        "tags": ["Video"],
        "security": [{ "clerkSession": [] }, { "apiKey": [] }],
        "parameters": [{ "name": "videoId", "in": "path", "required": true, "schema": { "type": "string" } }],
        "responses": {
          "200": { "description": "Deleted" },
          "404": {
            "description": "Not found",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } }
          }
        }
      }
    },
    "/api/v1/videos/library/{videoId}/feature": {
      "post": {
        "summary": "Set the entity's preview (featured) video",
        "description": "Make this video the featured/preview video for its idea or prototype (drives the public card + detail hero) and re-syncs the CTA denorm. Owner-only.",
        "operationId": "featureVideo",
        "tags": ["Video"],
        "security": [{ "clerkSession": [] }, { "apiKey": [] }],
        "parameters": [{ "name": "videoId", "in": "path", "required": true, "schema": { "type": "string" } }],
        "responses": {
          "200": { "description": "Featured" },
          "404": {
            "description": "Not found",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } }
          }
        }
      },
      "delete": {
        "summary": "Unset the preview (featured) video",
        "description": "Clear the entity's featured video (when it currently points at this video). Falls back to the static thumbnail.",
        "operationId": "unfeatureVideo",
        "tags": ["Video"],
        "security": [{ "clerkSession": [] }, { "apiKey": [] }],
        "parameters": [{ "name": "videoId", "in": "path", "required": true, "schema": { "type": "string" } }],
        "responses": { "200": { "description": "Unfeatured" } }
      }
    },
    "/api/v1/videos/library/{videoId}/translations": {
      "post": {
        "summary": "Add a language translation",
        "description": "Add a language variant, seeded from the primary's published render (full regen at a bumped duration for headroom). Costs credits.",
        "operationId": "addVideoTranslation",
        "tags": ["Video"],
        "security": [{ "clerkSession": [] }, { "apiKey": [] }],
        "parameters": [{ "name": "videoId", "in": "path", "required": true, "schema": { "type": "string" } }],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["language"],
                "properties": { "language": { "type": "string", "description": "BCP-47, e.g. es-MX" } }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Translation queued",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/VideoJob" } } }
          },
          "402": {
            "description": "Insufficient credits",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } }
          }
        }
      }
    },
    "/api/v1/videos/library/{videoId}/translations-batch": {
      "post": {
        "summary": "Batch-add translations",
        "description": "Queue translations for multiple languages at once. Pass estimateOnly to get the credit cost without committing.",
        "operationId": "addVideoTranslationsBatch",
        "tags": ["Video"],
        "security": [{ "clerkSession": [] }, { "apiKey": [] }],
        "parameters": [{ "name": "videoId", "in": "path", "required": true, "schema": { "type": "string" } }],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["languages"],
                "properties": {
                  "languages": { "type": "array", "items": { "type": "string" } },
                  "estimateOnly": { "type": "boolean" }
                }
              }
            }
          }
        },
        "responses": {
          "200": { "description": "Queued (or cost estimate)" },
          "402": {
            "description": "Insufficient credits",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } }
          }
        }
      }
    },
    "/api/v1/videos/library/{videoId}/variants-batch": {
      "post": {
        "summary": "Generate creative variants",
        "description": "Fan out N sibling videos (different hooks/angles) on the same entity, seeded from the primary's published render. estimateOnly returns the cost.",
        "operationId": "generateVideoVariants",
        "tags": ["Video"],
        "security": [{ "clerkSession": [] }, { "apiKey": [] }],
        "parameters": [{ "name": "videoId", "in": "path", "required": true, "schema": { "type": "string" } }],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["count"],
                "properties": {
                  "count": { "type": "integer", "minimum": 2, "maximum": 10 },
                  "variationStrategy": { "type": "string" },
                  "estimateOnly": { "type": "boolean" }
                }
              }
            }
          }
        },
        "responses": {
          "200": { "description": "Queued (or cost estimate)" },
          "402": {
            "description": "Insufficient credits",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } }
          }
        }
      }
    },
    "/api/v1/videos/library/{videoId}/variants/{variantId}/publish": {
      "post": {
        "summary": "Publish a variant's live render",
        "description": "Set which completed generation job is the variant's published MP4. Re-syncs the entity's CTA denorm if the variant is featured.",
        "operationId": "publishVideoVariant",
        "tags": ["Video"],
        "security": [{ "clerkSession": [] }, { "apiKey": [] }],
        "parameters": [
          { "name": "videoId", "in": "path", "required": true, "schema": { "type": "string" } },
          { "name": "variantId", "in": "path", "required": true, "schema": { "type": "string" } }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": { "type": "object", "required": ["jobId"], "properties": { "jobId": { "type": "string" } } }
            }
          }
        },
        "responses": {
          "200": { "description": "Published" },
          "404": {
            "description": "Not found",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } }
          }
        }
      }
    },
    "/api/v1/videos/library/{videoId}/variants/{variantId}/thumbnail": {
      "post": {
        "summary": "Set a variant thumbnail",
        "description": "Upload a custom poster image (raw image/jpeg or image/png bytes) for a language variant.",
        "operationId": "setVideoVariantThumbnail",
        "tags": ["Video"],
        "security": [{ "clerkSession": [] }, { "apiKey": [] }],
        "parameters": [
          { "name": "videoId", "in": "path", "required": true, "schema": { "type": "string" } },
          { "name": "variantId", "in": "path", "required": true, "schema": { "type": "string" } }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "image/jpeg": { "schema": { "type": "string", "format": "binary" } },
            "image/png": { "schema": { "type": "string", "format": "binary" } }
          }
        },
        "responses": { "200": { "description": "Thumbnail set" } }
      },
      "delete": {
        "summary": "Clear a variant thumbnail",
        "description": "Remove the custom poster; revert to a video frame.",
        "operationId": "clearVideoVariantThumbnail",
        "tags": ["Video"],
        "security": [{ "clerkSession": [] }, { "apiKey": [] }],
        "parameters": [
          { "name": "videoId", "in": "path", "required": true, "schema": { "type": "string" } },
          { "name": "variantId", "in": "path", "required": true, "schema": { "type": "string" } }
        ],
        "responses": { "200": { "description": "Thumbnail cleared" } }
      }
    },
    "/api/v1/videos/library/{videoId}/variants/{variantId}/brand-kit": {
      "put": {
        "summary": "Assign a variant brand kit",
        "description": "Set or clear the brand kit (colors/fonts/tone) stamped onto the variant's renders. Pass null to clear.",
        "operationId": "setVideoVariantBrandKit",
        "tags": ["Video"],
        "security": [{ "clerkSession": [] }, { "apiKey": [] }],
        "parameters": [
          { "name": "videoId", "in": "path", "required": true, "schema": { "type": "string" } },
          { "name": "variantId", "in": "path", "required": true, "schema": { "type": "string" } }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": { "type": "object", "properties": { "brandKitId": { "type": "string", "nullable": true } } }
            }
          }
        },
        "responses": { "200": { "description": "Brand kit updated" } }
      }
    },
    "/api/v1/social/connections": {
      "get": {
        "summary": "List connected social accounts",
        "description": "List the social channels/accounts the user has connected via Clerk OAuth. Use a connection's id as connectionId when publishing.",
        "operationId": "listSocialConnections",
        "tags": ["Social"],
        "security": [{ "clerkSession": [] }, { "apiKey": [] }],
        "responses": { "200": { "description": "List of connections" } }
      }
    },
    "/api/v1/social/publish": {
      "post": {
        "summary": "Publish a rendered video to a social channel",
        "description": "Enqueue a VideoLanguageVariant for publishing to a connected channel. Async — returns a queued VideoSocialPost; poll GET /social/posts/{id}. YouTube is fully supported; other platforms are stubbed.",
        "operationId": "publishSocialVideo",
        "tags": ["Social"],
        "security": [{ "clerkSession": [] }, { "apiKey": [] }],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["videoVariantId", "connectionId"],
                "properties": {
                  "videoVariantId": { "type": "string" },
                  "connectionId": { "type": "string", "description": "Target channel (from /social/connections)" },
                  "title": { "type": "string" },
                  "description": { "type": "string" },
                  "tags": { "type": "array", "items": { "type": "string" } },
                  "privacyStatus": { "type": "string", "enum": ["public", "private", "unlisted"] },
                  "uploadCaptions": { "type": "boolean", "description": "Upload narration as a YouTube caption track" }
                }
              }
            }
          }
        },
        "responses": { "201": { "description": "Publish job queued" } }
      }
    },
    "/api/v1/social/posts/{id}": {
      "get": {
        "summary": "Get a social publish job's status",
        "operationId": "getSocialPost",
        "tags": ["Social"],
        "security": [{ "clerkSession": [] }, { "apiKey": [] }],
        "parameters": [{ "name": "id", "in": "path", "required": true, "schema": { "type": "string" } }],
        "responses": { "200": { "description": "Post status" }, "404": { "description": "Not found" } }
      },
      "patch": {
        "summary": "Update a published video's metadata (YouTube)",
        "description": "Metadata-only update via YouTube videos.update (title/description/tags/privacy) plus optional caption insert/delete. Same video URL.",
        "operationId": "updateSocialPost",
        "tags": ["Social"],
        "security": [{ "clerkSession": [] }, { "apiKey": [] }],
        "parameters": [{ "name": "id", "in": "path", "required": true, "schema": { "type": "string" } }],
        "responses": { "200": { "description": "Updated" } }
      }
    },
    "/api/v1/social/posts/{id}/republish": {
      "post": {
        "summary": "Re-publish (replace) a published YouTube video",
        "description": "Deletes the old YouTube video and uploads a fresh render as a NEW video (new URL). The old row is marked replaced. Irreversible.",
        "operationId": "republishSocialPost",
        "tags": ["Social"],
        "security": [{ "clerkSession": [] }, { "apiKey": [] }],
        "parameters": [{ "name": "id", "in": "path", "required": true, "schema": { "type": "string" } }],
        "responses": { "201": { "description": "Re-publish job queued" } }
      }
    }
  },
  "tags": [
    { "name": "Categories", "description": "Valid categories for ideas and mockups" },
    { "name": "Ideas", "description": "App ideas submitted by users" },
    { "name": "Mockups", "description": "HTML and WebContainer prototypes" },
    { "name": "Feedback", "description": "Community feedback on mockups" },
    { "name": "Iterations", "description": "Chat-based mockup iteration — refine mockups by describing changes" },
    { "name": "Revisions", "description": "Mockup revision history — rate, set current, delete revisions" },
    {
      "name": "Generations",
      "description": "AI mockup generation with batch (1 credit) and streaming (2 credits) modes"
    },
    { "name": "Subscriptions", "description": "Plan and billing management" },
    { "name": "Notifications", "description": "User notifications and Web Push subscriptions" },
    { "name": "API Keys", "description": "API key management for programmatic access" },
    { "name": "Reports", "description": "Content moderation reports" },
    {
      "name": "Video",
      "description": "AI video generation, iteration, language variants, featuring/preview, publishing, and the public gallery"
    },
    {
      "name": "Social",
      "description": "Publish rendered videos to connected social channels (YouTube live; others stubbed)"
    }
  ]
}
