openapi: "3.1.0"
info:
  title: Capture Sweet API
  description: |
    Video as a programmable platform. Record, share, and monetize screen captures.

    **Scope of this spec (v0.2.0):** the video-lifecycle surface — auth, captures, folders, screenshots,
    analytics, comments, sharing, and the legacy Bubble-shaped endpoints the Chrome extension still
    calls. Account-configuration endpoints (destinations, webhooks, API keys, custom domains, team
    management) are out of scope for v0.2 and documented separately in
    `docs/code-audit/webapp-api-reference.md`. Promote those into the spec on partner request.
  version: "0.2.0"
  contact:
    name: Capture Sweet
    url: https://capturesweet.com

servers:
  - url: https://capturesweet.com
    description: Production

security:
  - bearerAuth: []

components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      description: Supabase Bearer token

  schemas:
    CapturePrivacy:
      type: string
      enum: [private, link_only, password, email_gate, public]

    CaptureStatus:
      type: string
      enum: [processing, ready, failed, deleted]

    AnalyticsEventType:
      type: string
      enum:
        - view_start
        - view_end
        - view_progress
        - play
        - pause
        - seek
        - cta_click
        - comment
        - download
        - share
        - checkpoint_completed
        - checkpoint_skipped

    PageConfig:
      type: object
      properties:
        branding:
          type: object
          properties:
            logo_url:
              type: string
            primary_color:
              type: string
            background:
              type: string
            custom_css:
              type: string
            show_capture_sweet_badge:
              type: boolean
        modules:
          type: array
          items:
            $ref: "#/components/schemas/Module"
        personalization:
          type: object
          properties:
            enabled:
              type: boolean
            params:
              type: array
              items:
                type: object
                properties:
                  key:
                    type: string
                  default_value:
                    type: string
                required: [key, default_value]
        sharing:
          type: object
          properties:
            custom_slug:
              type: string
            expiry_date:
              type: string
              format: date-time
            embed_enabled:
              type: boolean
            notifications:
              type: object
              properties:
                on_view:
                  type: boolean
                on_comment:
                  type: boolean
                on_cta_click:
                  type: boolean

    Module:
      oneOf:
        - $ref: "#/components/schemas/TranscriptModule"
        - $ref: "#/components/schemas/CommentsModule"
        - $ref: "#/components/schemas/CtaModule"
        - $ref: "#/components/schemas/StripeModule"
        - $ref: "#/components/schemas/ClaudeArtifactModule"
        - $ref: "#/components/schemas/CustomEmbedModule"
        - $ref: "#/components/schemas/ContactFormModule"
        - $ref: "#/components/schemas/DownloadModule"
      discriminator:
        propertyName: type

    TranscriptModule:
      type: object
      required: [type, enabled, position, searchable]
      properties:
        type:
          type: string
          enum: [transcript]
        enabled:
          type: boolean
        position:
          type: string
          enum: [sidebar, below]
        searchable:
          type: boolean

    CommentsModule:
      type: object
      required: [type, enabled, require_email, allow_timestamps, moderation]
      properties:
        type:
          type: string
          enum: [comments]
        enabled:
          type: boolean
        require_email:
          type: boolean
        allow_timestamps:
          type: boolean
        moderation:
          type: string
          enum: [auto, manual]

    CtaModule:
      type: object
      required: [type, enabled, text, url, style, position]
      properties:
        type:
          type: string
          enum: [cta]
        enabled:
          type: boolean
        text:
          type: string
        url:
          type: string
        style:
          type: string
          enum: [gradient, outline, custom]
        position:
          type: string
          enum: [floating, below, sidebar]
        show_at_percentage:
          type: number
        custom_color:
          type: string

    StripeModule:
      type: object
      required: [type, enabled, product_id, mode, success_message]
      properties:
        type:
          type: string
          enum: [stripe]
        enabled:
          type: boolean
        product_id:
          type: string
        mode:
          type: string
          enum: [embedded, redirect]
        success_message:
          type: string

    ClaudeArtifactModule:
      type: object
      required: [type, enabled, artifact_url]
      properties:
        type:
          type: string
          enum: [claude_artifact]
        enabled:
          type: boolean
        artifact_url:
          type: string
        width:
          type: string
        height:
          type: string

    CustomEmbedModule:
      type: object
      required: [type, enabled, iframe_code, sandbox]
      properties:
        type:
          type: string
          enum: [custom_embed]
        enabled:
          type: boolean
        iframe_code:
          type: string
        width:
          type: string
        height:
          type: string
        sandbox:
          type: boolean

    ContactFormModule:
      type: object
      required: [type, enabled, fields, destination, destination_value, thank_you_message]
      properties:
        type:
          type: string
          enum: [contact_form]
        enabled:
          type: boolean
        fields:
          type: array
          items:
            type: string
        destination:
          type: string
          enum: [email, webhook]
        destination_value:
          type: string
        thank_you_message:
          type: string

    DownloadModule:
      type: object
      required: [type, enabled, allow_video, allow_transcript, allow_screenshots, allow_zip, require_email]
      properties:
        type:
          type: string
          enum: [download]
        enabled:
          type: boolean
        allow_video:
          type: boolean
        allow_transcript:
          type: boolean
        allow_screenshots:
          type: boolean
        allow_zip:
          type: boolean
        require_email:
          type: boolean

    Capture:
      type: object
      properties:
        id:
          type: string
          format: uuid
        user_id:
          type: string
          format: uuid
        title:
          type: string
        description:
          type: string
          nullable: true
        slug:
          type: string
        storage_path:
          type: string
          nullable: true
        video_url:
          type: string
          nullable: true
        duration_seconds:
          type: number
          nullable: true
        file_size_bytes:
          type: number
          nullable: true
        mime_type:
          type: string
        thumbnail_path:
          type: string
          nullable: true
        transcript:
          type: object
          nullable: true
        transcript_text:
          type: string
          nullable: true
        navigation_log:
          type: object
          nullable: true
        privacy:
          $ref: "#/components/schemas/CapturePrivacy"
        page_config:
          $ref: "#/components/schemas/PageConfig"
        view_count:
          type: integer
        unique_viewer_count:
          type: integer
        comment_count:
          type: integer
        avg_watch_percentage:
          type: number
        status:
          $ref: "#/components/schemas/CaptureStatus"
        published:
          type: boolean
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time

    Screenshot:
      type: object
      properties:
        id:
          type: string
          format: uuid
        capture_id:
          type: string
          format: uuid
        storage_path:
          type: string
        timestamp_seconds:
          type: number
          nullable: true
        xpath:
          type: string
          nullable: true
        url:
          type: string
          nullable: true
        metadata:
          type: object
        sort_order:
          type: integer
        created_at:
          type: string
          format: date-time

    Folder:
      type: object
      properties:
        id:
          type: string
          format: uuid
        user_id:
          type: string
          format: uuid
        name:
          type: string
        color:
          type: string
        sort_order:
          type: integer
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time

    FolderWithCount:
      allOf:
        - $ref: "#/components/schemas/Folder"
        - type: object
          properties:
            capture_count:
              type: integer

    Comment:
      type: object
      properties:
        id:
          type: string
          format: uuid
        capture_id:
          type: string
          format: uuid
        user_id:
          type: string
          format: uuid
          nullable: true
        author_name:
          type: string
          nullable: true
        author_email:
          type: string
          nullable: true
        body:
          type: string
        timestamp_seconds:
          type: number
          nullable: true
        parent_id:
          type: string
          format: uuid
          nullable: true
        is_approved:
          type: boolean
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time

    AnalyticsEvent:
      type: object
      properties:
        id:
          type: string
          format: uuid
        capture_id:
          type: string
          format: uuid
        viewer_id:
          type: string
          nullable: true
        viewer_email:
          type: string
          nullable: true
        event_type:
          $ref: "#/components/schemas/AnalyticsEventType"
        watch_percentage:
          type: number
          nullable: true
        watch_duration_seconds:
          type: number
          nullable: true
        timestamp_in_video:
          type: number
          nullable: true
        device_info:
          type: object
        geo_info:
          type: object
        referrer:
          type: string
          nullable: true
        session_id:
          type: string
          nullable: true
        created_at:
          type: string
          format: date-time

    AnalyticsSummary:
      type: object
      properties:
        total_views:
          type: integer
        views_in_period:
          type: integer
        avg_watch_percentage:
          type: number
        cta_clicks:
          type: integer

    ErrorResponse:
      type: object
      properties:
        error:
          type: string
      required: [error]

    SuccessResponse:
      type: object
      properties:
        success:
          type: boolean

paths:
  /api/auth/extension-token:
    post:
      summary: Exchange credentials for a JWT
      description: Exchange email/password or a Google ID token for a Supabase access token. The Chrome extension's first call.
      operationId: authExtensionToken
      tags: [Authentication]
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              oneOf:
                - type: object
                  required: [email, password]
                  properties:
                    email:
                      type: string
                      format: email
                    password:
                      type: string
                - type: object
                  required: [google_id_token]
                  properties:
                    google_id_token:
                      type: string
                      description: A Google ID token (e.g. obtained via Chrome Identity / Google Sign-In).
      responses:
        "200":
          description: Authenticated
          content:
            application/json:
              schema:
                type: object
                properties:
                  access_token:
                    type: string
                  refresh_token:
                    type: string
                  expires_in:
                    type: integer
                    description: Seconds until access_token expires (typically 3600).
                  user:
                    type: object
                    properties:
                      id:
                        type: string
                        format: uuid
                      email:
                        type: string
                      full_name:
                        type: string
                        nullable: true
        "400":
          description: Missing email/password and no google_id_token
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "401":
          description: Invalid credentials
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"

  /api/auth/status:
    get:
      summary: Check authentication status
      description: |
        Lightweight session check. Always returns 200; the `authenticated` boolean indicates
        whether the Bearer token (if any) is valid. Use this to gate UI flows without forcing
        a 401.
      operationId: authStatus
      tags: [Authentication]
      security:
        - bearerAuth: []
        - {}
      responses:
        "200":
          description: Status
          content:
            application/json:
              schema:
                oneOf:
                  - type: object
                    required: [authenticated]
                    properties:
                      authenticated:
                        type: boolean
                        enum: [false]
                  - type: object
                    required: [authenticated, user]
                    properties:
                      authenticated:
                        type: boolean
                        enum: [true]
                      user:
                        type: object
                        properties:
                          id:
                            type: string
                            format: uuid
                          email:
                            type: string

  /api/captures:
    post:
      summary: Create a new capture
      description: Creates a capture record and returns a presigned upload URL for the video file.
      operationId: createCapture
      tags: [Captures]
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                title:
                  type: string
                  default: Untitled Recording
                description:
                  type: string
                privacy:
                  $ref: "#/components/schemas/CapturePrivacy"
      responses:
        "201":
          description: Capture created
          content:
            application/json:
              schema:
                type: object
                properties:
                  id:
                    type: string
                    format: uuid
                  slug:
                    type: string
                  upload_url:
                    type: string
                    format: uri
                    description: Mux direct-upload URL. Speaks tus 1.0 with creation-defer-length.
                  asset_id:
                    type: string
                    description: Mux upload ID; later swapped for the Mux asset ID via webhook.
        "400":
          description: Free tier limit reached
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
    get:
      summary: List captures
      description: List the authenticated user's captures with pagination, filtering, and search.
      operationId: listCaptures
      tags: [Captures]
      parameters:
        - name: page
          in: query
          schema:
            type: integer
            default: 1
        - name: limit
          in: query
          schema:
            type: integer
            default: 20
            maximum: 100
        - name: sort
          in: query
          schema:
            type: string
            enum: [created_at, views, title]
            default: created_at
        - name: privacy
          in: query
          schema:
            type: string
        - name: folder_id
          in: query
          schema:
            type: string
            format: uuid
        - name: search
          in: query
          schema:
            type: string
      responses:
        "200":
          description: Paginated capture list
          content:
            application/json:
              schema:
                type: object
                properties:
                  captures:
                    type: array
                    items:
                      $ref: "#/components/schemas/Capture"
                  total:
                    type: integer
                  page:
                    type: integer
                  limit:
                    type: integer
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"

  /api/captures/{id}:
    get:
      summary: Get a capture
      description: Get a single capture by ID, including screenshots. Public/link_only captures accessible without auth.
      operationId: getCapture
      tags: [Captures]
      security:
        - bearerAuth: []
        - {}
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        "200":
          description: Capture with screenshots
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/Capture"
                  - type: object
                    properties:
                      screenshots:
                        type: array
                        items:
                          $ref: "#/components/schemas/Screenshot"
        "404":
          description: Not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
    patch:
      summary: Update a capture
      description: |
        Update capture metadata. Idempotent — the same capture may be PATCHed multiple times
        during the streaming-finalize flow. Setting `status: "ready"` triggers the
        `capture.ready` destinations dispatcher.
      operationId: updateCapture
      tags: [Captures]
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                title:
                  type: string
                description:
                  type: string
                  nullable: true
                privacy:
                  $ref: "#/components/schemas/CapturePrivacy"
                page_config:
                  $ref: "#/components/schemas/PageConfig"
                status:
                  $ref: "#/components/schemas/CaptureStatus"
                published:
                  type: boolean
                thumbnail_path:
                  type: string
                duration_seconds:
                  type: number
                transcript:
                  type: object
                  nullable: true
                transcript_text:
                  type: string
                  nullable: true
                navigation_log:
                  type: object
                  nullable: true
                mux_asset_id:
                  type: string
                mux_playback_id:
                  type: string
                video_status:
                  type: string
                  enum: [uploading, processing, ready, errored]
      responses:
        "200":
          description: Updated capture
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Capture"
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "403":
          description: Forbidden - not the owner
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "404":
          description: Not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
    delete:
      summary: Delete a capture
      description: Soft-delete a capture (sets status to "deleted"). Succeeds even if the capture has no Mux asset yet.
      operationId: deleteCapture
      tags: [Captures]
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        "204":
          description: Deleted, no body
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"

  /api/captures/{id}/upload-url:
    post:
      summary: Generate upload URL
      description: Generate a new presigned upload URL for a capture's video file.
      operationId: getUploadUrl
      tags: [Captures]
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        "200":
          description: |
            New tus session at `Upload-Offset: 0`. Mux signed URLs ARE the tus session — once one
            expires, bytes shipped to it are unreachable. The caller is responsible for re-streaming
            from persisted chunks.
          content:
            application/json:
              schema:
                type: object
                properties:
                  upload_url:
                    type: string
                    format: uri
                  asset_id:
                    type: string
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "404":
          description: Not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"

  /api/captures/{id}/screenshots:
    post:
      summary: Upload screenshot
      description: Upload a screenshot image for a capture using multipart form data.
      operationId: addScreenshot
      tags: [Captures]
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      requestBody:
        content:
          multipart/form-data:
            schema:
              type: object
              required: [file]
              properties:
                file:
                  type: string
                  format: binary
                timestamp:
                  type: number
                xpath:
                  type: string
                url:
                  type: string
                metadata:
                  type: string
                  description: JSON string of metadata object
      responses:
        "201":
          description: Screenshot created (includes `public_url` for the uploaded asset)
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/Screenshot"
                  - type: object
                    properties:
                      public_url:
                        type: string
                        format: uri
        "400":
          description: No file provided
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "404":
          description: Capture not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"

  /api/captures/{id}/download:
    get:
      summary: Download a capture
      description: |
        Download the video file or transcript. MP4 and transcript are available on all plans;
        GIF and WebM are gated to Basic+ and Premium+ respectively (currently return 501 — coming
        soon). ZIP bundle export is planned (currently 501).

        Auth optional: public/link_only captures download without auth; private captures require
        the owner's Bearer token.
      operationId: downloadCapture
      tags: [Captures]
      security:
        - bearerAuth: []
        - {}
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
        - name: format
          in: query
          schema:
            type: string
            enum: [mp4, gif, webm, transcript, zip]
            default: mp4
      responses:
        "200":
          description: "File stream with `Content-Disposition: attachment`"
          content:
            video/mp4:
              schema:
                type: string
                format: binary
            text/plain:
              schema:
                type: string
                description: Plain-text transcript (when format=transcript)
        "302":
          description: Redirect to Mux MP4 URL when the proxy fetch fails
        "400":
          description: Unsupported format
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "403":
          description: Format requires a higher plan
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "404":
          description: Capture or transcript not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "501":
          description: GIF, WebM, or ZIP — coming soon
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"

  /api/captures/{id}/send:
    post:
      summary: Manually trigger destinations
      description: |
        Re-fire the configured destinations (Drive, ClickUp, Zapier, Make, S3, etc.) for an
        existing capture. Useful when the auto-fire failed or the user wants to push the capture
        to additional destinations. Owner-only.
      operationId: sendCapture
      tags: [Captures]
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                destination_ids:
                  type: array
                  description: Optional subset of destination IDs to fire. Omit to fire all enabled destinations.
                  items:
                    type: string
                    format: uuid
      responses:
        "200":
          description: Dispatch result (per-destination success/failure)
          content:
            application/json:
              schema:
                type: object
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "403":
          description: Forbidden (public keys cannot send)
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "404":
          description: Capture not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"

  /api/captures/{id}/password:
    post:
      summary: Set a viewer password
      description: |
        Set or update the password for a password-gated capture. Stores a SHA-256 hash in
        `captures.password_hash`. Owner-only. To enable the password gate end-to-end, also
        PATCH the capture with `privacy: "password"`.
      operationId: setCapturePassword
      tags: [Captures]
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [password]
              properties:
                password:
                  type: string
                  minLength: 1
      responses:
        "200":
          description: Password set
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/SuccessResponse"
        "400":
          description: Password missing
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "403":
          description: Forbidden (not the owner, or public key)
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "404":
          description: Capture not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"

  /api/captures/{id}/verify-password:
    post:
      summary: Verify a viewer password
      description: |
        Public endpoint used by the viewer page password-gate flow. Returns 200 if the submitted
        password matches the stored hash (or if no password is set on the capture). Returns 401
        on mismatch. No auth required.
      operationId: verifyCapturePassword
      tags: [Captures]
      security: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [password]
              properties:
                password:
                  type: string
      responses:
        "200":
          description: Password correct (or no password set on the capture)
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/SuccessResponse"
        "400":
          description: Password missing
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "401":
          description: Incorrect password
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "404":
          description: Capture not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"

  /api/folders:
    get:
      summary: List folders
      description: List all folders for the authenticated user, including capture counts.
      operationId: listFolders
      tags: [Folders]
      responses:
        "200":
          description: Folder list
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: "#/components/schemas/FolderWithCount"
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
    post:
      summary: Create a folder
      operationId: createFolder
      tags: [Folders]
      requestBody:
        content:
          application/json:
            schema:
              type: object
              required: [name]
              properties:
                name:
                  type: string
                color:
                  type: string
                  default: "#F23755"
      responses:
        "201":
          description: Folder created
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Folder"
        "400":
          description: Name required
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"

  /api/folders/{id}:
    patch:
      summary: Update a folder
      operationId: updateFolder
      tags: [Folders]
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                name:
                  type: string
                color:
                  type: string
                sort_order:
                  type: integer
      responses:
        "200":
          description: Updated folder
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Folder"
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "404":
          description: Not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
    delete:
      summary: Delete a folder
      description: Delete a folder. Captures in the folder are NOT deleted.
      operationId: deleteFolder
      tags: [Folders]
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        "200":
          description: Deleted
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/SuccessResponse"
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"

  /api/folders/{id}/captures:
    post:
      summary: Add captures to folder
      operationId: addCapturesToFolder
      tags: [Folders]
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                capture_id:
                  type: string
                  format: uuid
                capture_ids:
                  type: array
                  items:
                    type: string
                    format: uuid
      responses:
        "200":
          description: Captures added
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/SuccessResponse"
        "400":
          description: capture_id or capture_ids required
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
    delete:
      summary: Remove capture from folder
      operationId: removeCaptureFromFolder
      tags: [Folders]
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      requestBody:
        content:
          application/json:
            schema:
              type: object
              required: [capture_id]
              properties:
                capture_id:
                  type: string
                  format: uuid
      responses:
        "200":
          description: Capture removed
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/SuccessResponse"
        "400":
          description: capture_id required
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"

  /api/analytics:
    post:
      summary: Record analytics event
      description: Record a viewer analytics event. No authentication required.
      operationId: recordAnalyticsEvent
      tags: [Analytics]
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [capture_id, event_type]
              properties:
                capture_id:
                  type: string
                  format: uuid
                event_type:
                  $ref: "#/components/schemas/AnalyticsEventType"
                session_id:
                  type: string
                viewer_id:
                  type: string
                viewer_email:
                  type: string
                watch_percentage:
                  type: number
                watch_duration_seconds:
                  type: number
                timestamp_in_video:
                  type: number
                referrer:
                  type: string
      responses:
        "200":
          description: Event recorded
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/SuccessResponse"
        "400":
          description: Invalid parameters
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"

  /api/analytics/{captureId}:
    get:
      summary: Get capture analytics
      description: Get analytics summary and events for a capture. Owner only.
      operationId: getCaptureAnalytics
      tags: [Analytics]
      parameters:
        - name: captureId
          in: path
          required: true
          schema:
            type: string
            format: uuid
        - name: days
          in: query
          schema:
            type: integer
            default: 7
      responses:
        "200":
          description: Analytics data
          content:
            application/json:
              schema:
                type: object
                properties:
                  summary:
                    $ref: "#/components/schemas/AnalyticsSummary"
                  events:
                    type: array
                    items:
                      $ref: "#/components/schemas/AnalyticsEvent"
        "401":
          description: Unauthorized or not the owner
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"

  /api/comments/{captureId}:
    get:
      summary: List comments
      description: List approved comments for a capture. Public, no auth required.
      operationId: listComments
      tags: [Comments]
      security:
        - bearerAuth: []
        - {}
      parameters:
        - name: captureId
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        "200":
          description: Comment list
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: "#/components/schemas/Comment"
    post:
      summary: Create a comment
      description: Create a comment on a capture. Auth optional; authenticated users get auto-populated name/email.
      operationId: createComment
      tags: [Comments]
      security:
        - bearerAuth: []
        - {}
      parameters:
        - name: captureId
          in: path
          required: true
          schema:
            type: string
            format: uuid
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [body]
              properties:
                body:
                  type: string
                timestamp_seconds:
                  type: number
                author_name:
                  type: string
                author_email:
                  type: string
                parent_id:
                  type: string
                  format: uuid
      responses:
        "201":
          description: Comment created
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Comment"
        "400":
          description: Comment body required
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"

  /api/1.1/wf/productivity-capture:
    post:
      summary: Bubble-shaped metadata upload
      description: |
        Bubble.io compatibility shim used by the Chrome extension's legacy assembled-blob path
        after the video upload completes. Returns the canonical share URL plus capture
        identifiers so callers can use `Url` verbatim and `id` for the paired screenshot
        endpoint — no client-side parsing required.

        Setting `status: "ready"` and firing the destinations dispatcher are side effects.
      operationId: productivityCaptureLegacy
      tags: [Legacy]
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              type: object
              properties:
                user-id:
                  type: string
                  format: uuid
                video-link:
                  type: string
                  format: uri
                  description: URL the video was uploaded to. Capture ID is extracted from the storage path.
                video-title:
                  type: string
                video-transcript:
                  type: string
                  description: JSON string of transcript segments.
                video-screenshot:
                  type: string
                  description: JSON string of the navigation log array.
                mux_asset_id:
                  type: string
                  description: Optional, propagated to the capture row.
      responses:
        "200":
          description: Capture metadata stored, dispatcher fired
          content:
            application/json:
              schema:
                type: object
                properties:
                  response:
                    type: object
                    properties:
                      Url:
                        type: string
                        format: uri
                        description: Absolute share URL (`https://capturesweet.com/v/<slug>`).
                      id:
                        type: string
                        format: uuid
                      slug:
                        type: string
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "500":
          description: Could not match the upload to an existing capture
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"

  /api/1.1/wf/ps-capture-screenshot:
    post:
      summary: Bubble-shaped screenshot upload
      description: |
        Bubble.io compatibility shim, paired with `productivity-capture`. Uploads a screenshot
        image to Supabase Storage and creates the screenshot row.

        **`capture-url` is strictly UUID** — the value becomes `screenshots.capture_id` directly.
        A slug would fail the foreign-key constraint.
      operationId: psCaptureScreenshotLegacy
      tags: [Legacy]
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              type: object
              required: [screenshot-file]
              properties:
                capture-url:
                  type: string
                  format: uuid
                  description: Capture UUID (despite the field name).
                video-link:
                  type: string
                  format: uri
                  description: Fallback used to extract the UUID if `capture-url` is empty.
                screenshot-code:
                  type: string
                  description: JSON string with `{timestamp, xpath, url}` plus arbitrary metadata.
                screenshot-file:
                  type: string
                  format: binary
      responses:
        "200":
          description: Screenshot stored
          content:
            application/json:
              schema:
                type: object
                properties:
                  status:
                    type: string
                    enum: [success]
        "400":
          description: Missing capture ID or file
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"

tags:
  - name: Authentication
    description: Bearer token issuance and session check
  - name: Captures
    description: Video capture management — create, update, share, download
  - name: Folders
    description: Organize captures into folders
  - name: Analytics
    description: Viewer analytics and engagement tracking
  - name: Comments
    description: Timestamped comments on captures
  - name: Legacy
    description: |
      Bubble.io-shaped compatibility endpoints. The Chrome extension still calls these for
      non-streaming uploads. Prefer `/api/captures/*` for new integrations.
