openapi: 3.0.3
info:
  title: OpenClip Integration API
  version: 1.0.0
  description: |
    Integration API for external platforms (Discord bots, Telegram bots, enterprise apps)
    to access OpenClip's video processing capabilities.

    ## Authentication
    All endpoints require a Bearer token in the `Authorization` header.

    ## Rate Limits
    - Rate limit is configured per API client (not a fixed value). Check `X-RateLimit-Limit` and `X-RateLimit-Remaining` response headers for your current limit and usage.

    ## Webhooks
    Webhook payloads are signed with HMAC-SHA256. The signature is sent in the `Signature` header.
    Verify the signature by computing `HMAC-SHA256(webhook_secret, raw_request_body)` and comparing
    it to the value in the `Signature` header.

servers:
  - url: https://api.openclip.io/api/integration/v1
    description: Production

security:
  - BearerAuth: []

tags:
  - name: Users
    description: User creation and lookup
  - name: Authentication
    description: Email verification and magic-link login
  - name: Videos
    description: Video submission and status tracking
  - name: Viral Moments
    description: Viral moment retrieval and rendering
  - name: Rendering
    description: Caption rendering and customization
  - name: Caption Presets
    description: Available caption preset styles

paths:
  /users:
    post:
      operationId: createOrFindUser
      tags:
        - Users
      summary: Create or Find User
      description: |
        Creates a new user for the given platform identity, or returns the existing user
        if one already exists with the same platform and platform_user_id.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateUserRequest'
            example:
              platform: discord
              platform_user_id: "123456789012345678"
              username: "CoolUser#1234"
              email: john@example.com
              avatar_url: "https://cdn.discordapp.com/avatars/123/abc.png"
      responses:
        '200':
          description: Existing user found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CreateUserResponse'
              example:
                success: true
                was_created: false
                data:
                  user_id: abc123xyz
                  platform_user_id: "123456789012345678"
                  credits_remaining: 10
        '201':
          description: New user created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CreateUserResponse'
              example:
                success: true
                was_created: true
                data:
                  user_id: abc123xyz
                  platform_user_id: "123456789012345678"
                  credits_remaining: 10
        '401':
          description: Invalid or missing authentication token
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/StandardError'
              example:
                success: false
                message: Invalid or missing token
        '403':
          description: Missing required ability for this endpoint
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/StandardError'
              example:
                success: false
                message: You do not have the required ability to perform this action
        '422':
          $ref: '#/components/responses/ValidationError'

  /users/{platform}/{platform_user_id}:
    get:
      operationId: getUser
      tags:
        - Users
      summary: Get User Info
      description: Retrieves user information by platform and platform user ID.
      parameters:
        - name: platform
          in: path
          required: true
          schema:
            $ref: '#/components/schemas/Platform'
        - name: platform_user_id
          in: path
          required: true
          schema:
            type: string
          description: The user's ID on the external platform.
          example: "123456789012345678"
      responses:
        '200':
          description: User found
          content:
            application/json:
              schema:
                type: object
                required:
                  - success
                  - data
                properties:
                  success:
                    type: boolean
                    example: true
                  data:
                    $ref: '#/components/schemas/UserInfo'
              example:
                success: true
                data:
                  user_id: abc123xyz
                  platform: discord
                  platform_user_id: "123456789012345678"
                  username: "CoolUser#1234"
                  credits_remaining: 10
                  created_at: "2025-01-15T10:00:00Z"
        '400':
          description: Invalid platform value
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/StandardError'
              example:
                success: false
                message: Invalid platform specified.
        '401':
          description: Invalid or missing authentication token
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/StandardError'
              example:
                success: false
                message: Invalid or missing token
        '403':
          description: Missing required ability for this endpoint
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/StandardError'
              example:
                success: false
                message: You do not have the required ability to perform this action
        '404':
          $ref: '#/components/responses/NotFoundError'

  /auth/request-code:
    post:
      operationId: requestVerificationCode
      tags:
        - Authentication
      summary: Request Verification Code
      description: |
        Sends a 6-digit verification code to the specified email address.
        The code expires after 15 minutes (900 seconds).
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/RequestCodeRequest'
            example:
              platform: discord
              platform_user_id: "123456789012345678"
              email: john@example.com
      responses:
        '200':
          description: Verification code sent
          content:
            application/json:
              schema:
                type: object
                required:
                  - success
                  - message
                  - expires_in
                properties:
                  success:
                    type: boolean
                    example: true
                  message:
                    type: string
                    example: Verification code sent to your email.
                  expires_in:
                    type: integer
                    description: Seconds until the code expires.
                    example: 900
        '401':
          description: Invalid or missing authentication token
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/StandardError'
              example:
                success: false
                message: Invalid or missing token
        '403':
          description: Missing required ability for this endpoint
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/StandardError'
              example:
                success: false
                message: You do not have the required ability to perform this action
        '422':
          $ref: '#/components/responses/ValidationError'
        '429':
          description: Too many code requests
          content:
            application/json:
              schema:
                type: object
                required:
                  - success
                  - message
                  - retry_after
                properties:
                  success:
                    type: boolean
                    example: false
                  message:
                    type: string
                    example: Too many code requests. Please try again later.
                  retry_after:
                    type: integer
                    description: Seconds until a new code can be requested.
                    example: 2700

  /auth/verify-code:
    post:
      operationId: verifyCode
      tags:
        - Authentication
      summary: Verify Code & Get Login URL
      description: |
        Verifies the 6-digit code and returns a magic-link login URL.
        If the email matches an existing account, the platform identity is merged
        and `account_merged` is set to `true`.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/VerifyCodeRequest'
            example:
              platform: discord
              platform_user_id: "123456789012345678"
              email: john@example.com
              code: "482910"
      responses:
        '200':
          description: Code verified successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/VerifyCodeResponse'
              examples:
                normal:
                  summary: Normal verification
                  value:
                    success: true
                    login_url: "https://openclip.app/magic-link/login/abc123?signature=..."
                    expires_in: 1800
                    account_merged: false
                    user:
                      user_id: abc123xyz
                      email: john@example.com
                      credits_remaining: 10
                merged:
                  summary: Account merged
                  value:
                    success: true
                    login_url: "https://openclip.app/magic-link/login/abc123?signature=..."
                    expires_in: 1800
                    account_merged: true
                    message: Your Discord account has been linked to your existing OpenClip account.
                    user:
                      user_id: abc123xyz
                      email: john@example.com
                      credits_remaining: 10
        '400':
          description: Invalid or expired code
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AuthError'
              examples:
                invalid_code:
                  summary: Invalid code
                  value:
                    success: false
                    error: INVALID_CODE
                    message: Invalid or expired verification code
                expired_code:
                  summary: Expired code
                  value:
                    success: false
                    error: EXPIRED_CODE
                    message: Verification code has expired
                already_verified:
                  summary: Code already used
                  value:
                    success: false
                    error: CODE_ALREADY_VERIFIED
                    message: This code has already been verified
                not_found:
                  summary: Code not found
                  value:
                    success: false
                    error: CODE_NOT_FOUND
                    message: No verification code found for this email
        '401':
          description: Invalid or missing authentication token
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/StandardError'
              example:
                success: false
                message: Invalid or missing token
        '403':
          description: Missing required ability for this endpoint
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/StandardError'
              example:
                success: false
                message: You do not have the required ability to perform this action
        '422':
          $ref: '#/components/responses/ValidationError'
        '429':
          description: Too many verification attempts
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AuthError'
              example:
                success: false
                error: EXCEEDED_ATTEMPTS
                message: Too many failed attempts. Please request a new code.

  /auth/redeem-invite:
    post:
      operationId: redeemInviteCode
      tags:
        - Authentication
      summary: Redeem Invite Code
      description: |
        Redeems an invite code for a platform user. Supports stacking: users can
        redeem multiple different codes.

        - First code: creates a trial subscription (`trial_activated: true`)
        - Subsequent codes: extends the existing trial (`trial_extended: true`)
        - Paid subscribers: skip trial creation, credit bonus minutes (`trial_skipped: true`)

        Requires the `users:create` token ability.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/RedeemInviteRequest'
            example:
              platform: discord
              platform_user_id: "123456789012345678"
              code: "WELCOME2026"
      responses:
        '200':
          description: Invite code redeemed successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/RedeemInviteResponse'
              examples:
                trial_activated:
                  summary: Trial activated (first code)
                  value:
                    success: true
                    message: "Invite code redeemed: Trial activated for 14 days, 30 bonus minutes credited."
                    bonus_minutes_credited: 30
                    trial_activated: true
                    trial_extended: false
                    trial_skipped: false
                    trial_plan: Pro
                    trial_ends_at: "2026-02-11T00:00:00+00:00"
                    credits_remaining: 30
                trial_extended:
                  summary: Trial extended (subsequent code)
                  value:
                    success: true
                    message: "Invite code redeemed: Trial extended by 7 days, 15 bonus minutes credited."
                    bonus_minutes_credited: 15
                    trial_activated: false
                    trial_extended: true
                    trial_skipped: false
                    trial_plan: Pro
                    trial_ends_at: "2026-02-18T00:00:00+00:00"
                    credits_remaining: 45
                paid_subscriber:
                  summary: Already subscribed (trial skipped)
                  value:
                    success: true
                    message: "Invite code redeemed: Trial skipped (already subscribed), 30 bonus minutes credited."
                    bonus_minutes_credited: 30
                    trial_activated: false
                    trial_extended: false
                    trial_skipped: true
                    trial_plan: null
                    trial_ends_at: null
                    credits_remaining: 130
        '400':
          description: Invalid or already-redeemed invite code
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AuthError'
              examples:
                invalid_code:
                  summary: Invalid code
                  value:
                    success: false
                    error: INVALID_CODE
                    message: Invalid invite code
                already_redeemed:
                  summary: Code already redeemed
                  value:
                    success: false
                    error: ALREADY_REDEEMED
                    message: You have already redeemed this invite code
        '401':
          description: Invalid or missing authentication token
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/StandardError'
              example:
                success: false
                message: Invalid or missing token
        '403':
          description: Missing required ability for this endpoint
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/StandardError'
              example:
                success: false
                message: You do not have the required ability to perform this action
        '404':
          description: Platform user not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AuthError'
              example:
                success: false
                error: USER_NOT_FOUND
                message: Platform user not found
        '422':
          $ref: '#/components/responses/ValidationError'

  /videos:
    post:
      operationId: submitVideo
      tags:
        - Videos
      summary: Submit Video
      description: |
        Submits a video URL for processing. The video will be downloaded, processed,
        and analyzed for viral moments. Consumes one credit.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/SubmitVideoRequest'
            example:
              platform: discord
              platform_user_id: "123456789012345678"
              video_url: "https://www.youtube.com/watch?v=dQw4w9WgXcQ"
              webhooks:
                progress: "https://myapp.com/webhooks/progress"
                completion: "https://myapp.com/webhooks/complete"
              metadata:
                channel_id: "987654321"
                message_id: "111222333"
      responses:
        '202':
          description: Video submitted for processing
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SubmitVideoResponse'
              example:
                success: true
                data:
                  job_id: job_abc123xyz789
                  video_id: null
                  status: queued
                  user:
                    user_id: abc123xyz
                    credits_remaining: 9
        '401':
          description: Invalid or missing authentication token
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/StandardError'
              example:
                success: false
                message: Invalid or missing token
        '402':
          description: |
            **Legacy — no longer returned by the current API implementation.**
            The API now accepts submissions regardless of credit balance and
            returns a 202 with `pending_credits` status instead. This response
            is retained for backwards compatibility documentation only.
          content:
            application/json:
              schema:
                type: object
                required:
                  - success
                  - message
                  - data
                properties:
                  success:
                    type: boolean
                    example: false
                  message:
                    type: string
                    example: Insufficient credits.
                  data:
                    type: object
                    required:
                      - credits_remaining
                    properties:
                      credits_remaining:
                        type: integer
                        example: 0
        '403':
          description: Missing required ability for this endpoint
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/StandardError'
              example:
                success: false
                message: You do not have the required ability to perform this action
        '422':
          $ref: '#/components/responses/ValidationError'

  /videos/{job_id}/status:
    get:
      operationId: getVideoStatus
      tags:
        - Videos
      summary: Get Video Status
      description: Returns the current processing status of a submitted video job.
      parameters:
        - name: job_id
          in: path
          required: true
          schema:
            type: string
          description: The job ID returned from the video submission endpoint.
          example: job_abc123xyz789
      responses:
        '200':
          description: Video status retrieved
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/VideoStatusResponse'
              example:
                success: true
                data:
                  job_id: job_abc123xyz789
                  video_id: vid_xyz123
                  status: processing
                  provider: youtube
                  platform: discord
                  platform_user_id: "123456789012345678"
                  created_at: "2025-01-15T10:30:00Z"
                  video:
                    id: vid_xyz123
                    title: My Podcast Episode
                    status: processing
                    progress: 50
        '401':
          description: Invalid or missing authentication token
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/StandardError'
              example:
                success: false
                message: Invalid or missing token
        '403':
          description: Missing required ability for this endpoint
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/StandardError'
              example:
                success: false
                message: You do not have the required ability to perform this action
        '404':
          $ref: '#/components/responses/NotFoundError'

  /videos/{job_id}/viral-moments:
    get:
      operationId: getViralMoments
      tags:
        - Viral Moments
      summary: Get Viral Moments
      description: |
        Returns the detected viral moments for a completed video job.
        Returns 404 if the video submission or video does not exist yet (e.g., still downloading).
      parameters:
        - name: job_id
          in: path
          required: true
          schema:
            type: string
          description: The job ID returned from the video submission endpoint.
          example: job_abc123xyz789
      responses:
        '200':
          description: Viral moments retrieved
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ViralMomentsResponse'
              example:
                success: true
                data:
                  job_id: job_abc123xyz789
                  video_id: vid_xyz123
                  video_status: completed
                  viral_moments_count: 2
                  viral_moments:
                    - id: vm_abc123
                      title: The Big Reveal
                      hook: "You won't believe what happens next..."
                      category: reveal
                      start_time_ms: 45000
                      end_time_ms: 75000
                      duration_ms: 30000
                      virality_score: 8.5
                      viral_score_details:
                        hookStrength: 9
                        shareability: 8
                        rewatchability: 7
                        surpriseFactor: 8
                        emotionalImpact: 9
                        total: 85
                      clip:
                        url: "https://cdn.openclip.io/clips/vm_abc123.mp4"
                        size_bytes: 5242880
                        type: combined
                      clip_watermarked:
                        url: "https://cdn.openclip.io/clips/vm_abc123_wm.mp4"
                        size_bytes: 5300000
                      thumbnail_url: "https://cdn.openclip.io/thumbs/vm_abc123.jpg"
                      social_copy:
                        tiktok: "POV: When the unexpected happens"
                        twitter: "You won't believe what happens next... #viral"
                        linkedin: "A masterclass in storytelling"
                        youtube_shorts: "The moment everyone's talking about"
                        instagram_reels: "The moment everyone's talking about"
                      quote: This is the exact moment everything changed.
                      platforms:
                        - tiktok
                        - instagram
                        - youtube_shorts
        '401':
          description: Invalid or missing authentication token
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/StandardError'
              example:
                success: false
                message: Invalid or missing token
        '403':
          description: Missing required ability for this endpoint
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/StandardError'
              example:
                success: false
                message: You do not have the required ability to perform this action
        '404':
          $ref: '#/components/responses/NotFoundError'

  /viral-moments/{viral_moment_id}/render:
    post:
      operationId: triggerRender
      tags:
        - Rendering
      summary: Trigger Render
      description: |
        Submits a render job for the specified viral moment. Optionally provide a
        caption preset, custom caption style overrides, and a logo overlay configuration.
        A webhook is sent to `callback_url` when rendering completes.
      parameters:
        - name: viral_moment_id
          in: path
          required: true
          schema:
            type: string
          description: The viral moment ID.
          example: vm_abc123
      requestBody:
        required: false
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/TriggerRenderRequest'
            example:
              callback_url: "https://myapp.com/webhooks/render-complete"
              caption_preset: viral
              caption_style:
                fontSize: 32
                textColor: "#FFFFFF"
                position: bottom-center
                wordAnimation: pop
              logo_overlay:
                enabled: true
                url: "https://myapp.com/logo.png"
                position: top-right
                opacity: 0.8
                scale: 0.15
      responses:
        '202':
          description: Render job submitted
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TriggerRenderResponse'
              example:
                success: true
                data:
                  render_job_id: render-vm_abc123-xyz789
                  viral_moment_id: vm_abc123
                  status: queued
                  message: Render job submitted. You will receive a webhook when complete.
        '401':
          description: Invalid or missing authentication token
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/StandardError'
              example:
                success: false
                message: Invalid or missing token
        '403':
          description: Missing required ability for this endpoint
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/StandardError'
              example:
                success: false
                message: You do not have the required ability to perform this action
        '404':
          $ref: '#/components/responses/NotFoundError'
        '422':
          $ref: '#/components/responses/ValidationError'

  /caption-presets:
    get:
      operationId: listCaptionPresets
      tags:
        - Caption Presets
      summary: List Caption Presets
      description: Returns all available caption presets, customizable properties, and position options.
      responses:
        '200':
          description: Caption presets retrieved
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CaptionPresetsResponse'
              example:
                success: true
                data:
                  presets:
                    - key: viral
                      name: Viral
                      description: "Clean and bold - the universal choice for any content"
                      category: professional
                    - key: louis
                      name: Louis
                      description: "High energy style with vibrant colors - perfect for hype content"
                      category: creative
                    - key: classic
                      name: Classic
                      description: "Timeless black pill with white text - professional and readable"
                      category: professional
                    - key: mozi
                      name: Mozi
                      description: "Business coaching style - bold with strategic emphasis"
                      category: professional
                    - key: beast
                      name: Beast
                      description: "Entertainment style - bold impact with vibrant highlights"
                      category: creative
                    - key: cinema
                      name: Cinema
                      description: "Cinematic style for storytelling and documentary content"
                      category: professional
                  available_properties:
                    fontSize:
                      type: number
                      min: 8
                      max: 120
                      default: 28
                      description: Font size in pixels
                    textColor:
                      type: color
                      default: "#FFFFFF"
                      description: "Default text color (hex or rgba)"
                    position:
                      type: enum
                      options:
                        - bottom-center
                        - lower-third
                        - center
                        - upper-third
                        - top-center
                      default: bottom-center
                      description: Vertical position on screen
                    wordAnimation:
                      type: enum
                      options:
                        - static
                        - pop
                        - kinetic
                        - burst
                        - fade
                      default: pop
                      description: Per-word animation style
                  position_options:
                    - value: top-center
                      label: Top
                      description: Top of video frame
                    - value: upper-third
                      label: Upper Third
                      description: Upper third of video
                    - value: center
                      label: Center
                      description: Center of video frame
                    - value: lower-third
                      label: Lower Third
                      description: Lower third of video
                    - value: bottom-center
                      label: Bottom
                      description: "Bottom of video frame (default)"
        '401':
          description: Invalid or missing authentication token
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/StandardError'
              example:
                success: false
                message: Invalid or missing token
        '403':
          description: Missing required ability for this endpoint
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/StandardError'
              example:
                success: false
                message: You do not have the required ability to perform this action

components:
  securitySchemes:
    BearerAuth:
      type: http
      scheme: bearer
      description: API token passed as a Bearer token in the Authorization header.

  schemas:
    # --- Shared / Reusable ---

    Platform:
      type: string
      enum:
        - discord
        - telegram
        - slack
        - enterprise
      description: The external platform the user originates from.
      example: discord

    CaptionPresetKey:
      type: string
      enum:
        - viral
        - louis
        - classic
        - mozi
        - beast
        - cinema
      description: Identifier for a built-in caption preset.
      example: viral

    VideoJobStatus:
      type: string
      enum:
        - queued
        - pending
        - pending_credits
        - downloading
        - processing
        - completed
        - failed
      description: Current status of a video processing job.
      example: processing

    PositionOption:
      type: string
      enum:
        - bottom-center
        - lower-third
        - center
        - upper-third
        - top-center
      description: Vertical caption position on the video frame.
      example: bottom-center

    StandardError:
      type: object
      required:
        - success
        - message
      properties:
        success:
          type: boolean
          example: false
        message:
          type: string
          description: Human-readable error message.
          example: The given data was invalid.
        errors:
          type: object
          additionalProperties:
            type: array
            items:
              type: string
          description: Field-level validation errors.
          example:
            email:
              - The email field is required.

    AuthError:
      type: object
      required:
        - success
        - error
        - message
      properties:
        success:
          type: boolean
          example: false
        error:
          type: string
          description: Machine-readable error code.
          enum:
            - INVALID_CODE
            - EXPIRED_CODE
            - CODE_ALREADY_VERIFIED
            - CODE_NOT_FOUND
            - EXCEEDED_ATTEMPTS
            - ALREADY_REDEEMED
            - USER_NOT_FOUND
          example: INVALID_CODE
        message:
          type: string
          description: Human-readable error description.
          example: Invalid or expired verification code

    # --- Users ---

    CreateUserRequest:
      type: object
      required:
        - platform
        - platform_user_id
      properties:
        platform:
          $ref: '#/components/schemas/Platform'
        platform_user_id:
          type: string
          description: The user's unique ID on the external platform.
          example: "123456789012345678"
        username:
          type: string
          description: Display name on the external platform.
          example: "CoolUser#1234"
        email:
          type: string
          format: email
          description: User's email address.
          example: john@example.com
        avatar_url:
          type: string
          format: uri
          description: URL to the user's avatar image.
          example: "https://cdn.discordapp.com/avatars/123/abc.png"

    CreateUserResponse:
      type: object
      required:
        - success
        - was_created
        - data
      properties:
        success:
          type: boolean
          example: true
        was_created:
          type: boolean
          description: "`true` if a new user was created, `false` if an existing user was found."
          example: true
        data:
          type: object
          required:
            - user_id
            - platform_user_id
            - credits_remaining
          properties:
            user_id:
              type: string
              description: Internal OpenClip user ID.
              example: abc123xyz
            platform_user_id:
              type: string
              description: The user's ID on the external platform.
              example: "123456789012345678"
            credits_remaining:
              type: integer
              description: Number of processing credits remaining.
              example: 10

    UserInfo:
      type: object
      required:
        - user_id
        - platform
        - platform_user_id
        - credits_remaining
        - created_at
      properties:
        user_id:
          type: string
          description: Internal OpenClip user ID.
          example: abc123xyz
        platform:
          $ref: '#/components/schemas/Platform'
        platform_user_id:
          type: string
          description: The user's ID on the external platform.
          example: "123456789012345678"
        username:
          type: string
          description: Display name on the external platform.
          example: "CoolUser#1234"
        credits_remaining:
          type: integer
          description: Number of processing credits remaining.
          example: 10
        created_at:
          type: string
          format: date-time
          description: Account creation timestamp (ISO 8601).
          example: "2025-01-15T10:00:00Z"

    # --- Authentication ---

    RequestCodeRequest:
      type: object
      required:
        - platform
        - platform_user_id
        - email
      properties:
        platform:
          $ref: '#/components/schemas/Platform'
        platform_user_id:
          type: string
          description: The user's ID on the external platform.
          example: "123456789012345678"
        email:
          type: string
          format: email
          description: Email address to send the verification code to.
          example: john@example.com

    VerifyCodeRequest:
      type: object
      required:
        - platform
        - platform_user_id
        - email
        - code
      properties:
        platform:
          $ref: '#/components/schemas/Platform'
        platform_user_id:
          type: string
          description: The user's ID on the external platform.
          example: "123456789012345678"
        email:
          type: string
          format: email
          description: Email address the code was sent to.
          example: john@example.com
        code:
          type: string
          pattern: '^\d{6}$'
          description: The 6-digit verification code.
          example: "482910"

    VerifyCodeResponse:
      type: object
      required:
        - success
        - login_url
        - expires_in
        - account_merged
        - user
      properties:
        success:
          type: boolean
          example: true
        login_url:
          type: string
          format: uri
          description: Magic-link URL for browser login. Expires after `expires_in` seconds.
          example: "https://openclip.app/magic-link/login/abc123?signature=..."
        expires_in:
          type: integer
          description: Seconds until the login URL expires.
          example: 1800
        account_merged:
          type: boolean
          description: "`true` if the platform identity was merged into an existing account."
          example: false
        message:
          type: string
          nullable: true
          description: Present when `account_merged` is `true`, explaining the merge. `null` otherwise.
          example: Your Discord account has been linked to your existing OpenClip account.
        user:
          type: object
          required:
            - user_id
            - email
            - credits_remaining
          properties:
            user_id:
              type: string
              description: Internal OpenClip user ID.
              example: abc123xyz
            email:
              type: string
              format: email
              example: john@example.com
            credits_remaining:
              type: integer
              example: 10

    RedeemInviteRequest:
      type: object
      required:
        - platform
        - platform_user_id
        - code
      properties:
        platform:
          $ref: '#/components/schemas/Platform'
        platform_user_id:
          type: string
          description: The user's ID on the external platform.
          example: "123456789012345678"
        code:
          type: string
          description: The invite code to redeem.
          example: "WELCOME2026"

    RedeemInviteResponse:
      type: object
      required:
        - success
        - message
        - bonus_minutes_credited
        - trial_activated
        - trial_extended
        - trial_skipped
        - credits_remaining
      properties:
        success:
          type: boolean
          example: true
        message:
          type: string
          description: Human-readable message describing what happened.
          example: "Invite code redeemed: Trial activated for 14 days, 30 bonus minutes credited."
        bonus_minutes_credited:
          type: number
          description: Number of bonus minutes credited from this invite code.
          example: 30
        trial_activated:
          type: boolean
          description: Whether a new trial subscription was created.
          example: true
        trial_extended:
          type: boolean
          description: Whether an existing trial was extended.
          example: false
        trial_skipped:
          type: boolean
          description: Whether trial creation was skipped because user is already subscribed.
          example: false
        trial_plan:
          type: string
          nullable: true
          description: Name of the trial plan, or `null` if trial was skipped.
          example: Pro
        trial_ends_at:
          type: string
          format: date-time
          nullable: true
          description: When the trial ends (ISO 8601), or `null` if trial was skipped.
          example: "2026-02-11T00:00:00+00:00"
        credits_remaining:
          type: integer
          description: Total credits remaining after redemption.
          example: 30

    # --- Videos ---

    SubmitVideoRequest:
      type: object
      required:
        - platform
        - platform_user_id
        - video_url
      properties:
        platform:
          $ref: '#/components/schemas/Platform'
        platform_user_id:
          type: string
          description: The user's ID on the external platform.
          example: "123456789012345678"
        video_url:
          type: string
          format: uri
          description: URL of the video to process (YouTube, Vimeo, direct link, etc.).
          example: "https://www.youtube.com/watch?v=dQw4w9WgXcQ"
        webhooks:
          type: object
          description: Optional webhook URLs for async status updates.
          properties:
            progress:
              type: string
              format: uri
              description: URL to receive progress updates during processing.
              example: "https://myapp.com/webhooks/progress"
            completion:
              type: string
              format: uri
              description: URL to receive the final completion or failure notification.
              example: "https://myapp.com/webhooks/complete"
        metadata:
          type: object
          additionalProperties: true
          description: Arbitrary metadata to associate with the job (returned in webhooks).
          example:
            channel_id: "987654321"
            message_id: "111222333"

    SubmitVideoResponse:
      type: object
      required:
        - success
        - data
      properties:
        success:
          type: boolean
          example: true
        data:
          type: object
          required:
            - job_id
            - video_id
            - status
            - user
          properties:
            job_id:
              type: string
              description: Unique job identifier for tracking this submission.
              example: job_abc123xyz789
            video_id:
              type: string
              nullable: true
              description: Video ID, `null` until the video record is created during processing.
              example: null
            status:
              $ref: '#/components/schemas/VideoJobStatus'
            user:
              type: object
              required:
                - user_id
                - credits_remaining
              properties:
                user_id:
                  type: string
                  example: abc123xyz
                credits_remaining:
                  type: integer
                  description: Credits remaining after this submission.
                  example: 9

    VideoStatusResponse:
      type: object
      required:
        - success
        - data
      properties:
        success:
          type: boolean
          example: true
        data:
          type: object
          required:
            - job_id
            - video_id
            - status
            - platform
            - platform_user_id
            - created_at
          properties:
            job_id:
              type: string
              example: job_abc123xyz789
            video_id:
              type: string
              nullable: true
              description: Video ID, `null` if not yet created.
              example: vid_xyz123
            status:
              $ref: '#/components/schemas/VideoJobStatus'
            provider:
              type: string
              nullable: true
              description: Detected video provider. `null` if URL detection failed.
              example: youtube
            platform:
              $ref: '#/components/schemas/Platform'
            platform_user_id:
              type: string
              example: "123456789012345678"
            created_at:
              type: string
              format: date-time
              example: "2025-01-15T10:30:00Z"
            video:
              type: object
              nullable: true
              description: Video details, `null` if the video record has not been created yet.
              required:
                - id
                - title
                - status
                - progress
              properties:
                id:
                  type: string
                  example: vid_xyz123
                title:
                  type: string
                  example: My Podcast Episode
                status:
                  type: string
                  description: Internal video processing status.
                  example: processing
                progress:
                  type: integer
                  minimum: 0
                  maximum: 100
                  description: Processing progress percentage.
                  example: 50
                viral_moments_count:
                  type: integer
                  description: Number of detected viral moments. Present when video processing is completed.
                  example: 5

    # --- Viral Moments ---

    ViralMomentsResponse:
      type: object
      required:
        - success
        - data
      properties:
        success:
          type: boolean
          example: true
        data:
          type: object
          required:
            - job_id
            - video_id
            - video_status
            - viral_moments_count
            - viral_moments
          properties:
            job_id:
              type: string
              example: job_abc123xyz789
            video_id:
              type: string
              example: vid_xyz123
            video_status:
              type: string
              description: Current status of the parent video.
              example: completed
            viral_moments_count:
              type: integer
              description: Total number of viral moments detected.
              example: 2
            viral_moments:
              type: array
              items:
                $ref: '#/components/schemas/ViralMoment'

    ViralMoment:
      type: object
      required:
        - id
        - title
        - hook
        - category
        - start_time_ms
        - end_time_ms
        - duration_ms
        - virality_score
        - platforms
      properties:
        id:
          type: string
          description: Unique viral moment identifier.
          example: vm_abc123
        title:
          type: string
          description: AI-generated title for the moment.
          example: The Big Reveal
        hook:
          type: string
          description: Attention-grabbing hook text for social media.
          example: "You won't believe what happens next..."
        category:
          type: string
          description: Category of the viral moment.
          example: reveal
        start_time_ms:
          type: integer
          description: Start time in milliseconds from the beginning of the source video.
          example: 45000
        end_time_ms:
          type: integer
          description: End time in milliseconds from the beginning of the source video.
          example: 75000
        duration_ms:
          type: integer
          description: Duration of the clip in milliseconds.
          example: 30000
        virality_score:
          type: number
          minimum: 0
          maximum: 10
          description: Overall virality score (0-10, float rounded to 1 decimal).
          example: 8.5
        viral_score_details:
          type: object
          nullable: true
          description: Detailed virality score breakdown (camelCase keys). `null` if scoring was skipped.
          required:
            - hookStrength
            - shareability
            - rewatchability
            - surpriseFactor
            - emotionalImpact
          properties:
            hookStrength:
              type: integer
              description: How strong the opening hook is (0-10).
              example: 9
            shareability:
              type: integer
              description: Likelihood of being shared (0-10).
              example: 8
            rewatchability:
              type: integer
              description: How likely viewers will rewatch (0-10).
              example: 7
            surpriseFactor:
              type: integer
              description: How unexpected or surprising the moment is (0-10).
              example: 8
            emotionalImpact:
              type: integer
              description: Emotional resonance of the content (0-10).
              example: 9
            total:
              type: integer
              nullable: true
              description: Pre-calculated total score (0-100). Present when provided by the AI model.
              example: 85
        clip:
          type: object
          nullable: true
          description: Unwatermarked clip details. `null` if not yet generated.
          required:
            - url
            - size_bytes
            - type
          properties:
            url:
              type: string
              format: uri
              description: CDN URL to the clip file.
              example: "https://cdn.openclip.io/clips/vm_abc123.mp4"
            size_bytes:
              type: integer
              description: File size in bytes.
              example: 5242880
            type:
              type: string
              description: Clip type.
              example: combined
        clip_watermarked:
          type: object
          nullable: true
          description: Watermarked clip details. `null` if not yet generated.
          required:
            - url
            - size_bytes
          properties:
            url:
              type: string
              format: uri
              description: CDN URL to the watermarked clip file.
              example: "https://cdn.openclip.io/clips/vm_abc123_wm.mp4"
            size_bytes:
              type: integer
              description: File size in bytes.
              example: 5300000
        thumbnail_url:
          type: string
          format: uri
          nullable: true
          description: CDN URL to the thumbnail image.
          example: "https://cdn.openclip.io/thumbs/vm_abc123.jpg"
        social_copy:
          type: object
          nullable: true
          description: AI-generated social media copy. Keys are present only when copy was generated for that platform.
          properties:
            tiktok:
              type: string
              nullable: true
              description: Copy optimized for TikTok.
            twitter:
              type: string
              nullable: true
              description: Copy optimized for Twitter/X.
            linkedin:
              type: string
              nullable: true
              description: Copy optimized for LinkedIn.
            youtube_shorts:
              type: string
              nullable: true
              description: Copy optimized for YouTube Shorts.
            instagram_reels:
              type: string
              nullable: true
              description: Copy optimized for Instagram Reels.
        quote:
          type: string
          nullable: true
          description: Notable quote from the clip.
          example: This is the exact moment everything changed.
        detected_start_time_ms:
          type: integer
          nullable: true
          description: Original detected start time in milliseconds (before any adjustments). `null` for older records.
          example: 44500
        detected_end_time_ms:
          type: integer
          nullable: true
          description: Original detected end time in milliseconds (before any adjustments). `null` for older records.
          example: 75500
        rendered_clip:
          type: object
          nullable: true
          description: Rendered clip with captions. `null` if not yet rendered.
          properties:
            url:
              type: string
              format: uri
              description: CDN URL to the rendered clip.
              example: "https://cdn.openclip.io/rendered/vm_abc123.mp4"
            size_bytes:
              type: integer
              description: File size in bytes.
              example: 8388608
        platforms:
          type: array
          items:
            type: string
          description: Recommended social platforms for this clip.
          example:
            - tiktok
            - instagram
            - youtube_shorts

    # --- Rendering ---

    TriggerRenderRequest:
      type: object
      properties:
        callback_url:
          type: string
          format: uri
          nullable: true
          maxLength: 2048
          description: Webhook URL to receive a notification when rendering completes.
          example: "https://myapp.com/webhooks/render-complete"
        caption_preset:
          type: string
          nullable: true
          maxLength: 100
          description: Caption preset key. Accepts any registered preset string.
          example: viral
        style_mode:
          type: string
          enum:
            - preset
            - custom
          description: |
            Whether to use a preset or fully custom caption styling.
            `preset` applies the chosen `caption_preset` with optional overrides.
            `custom` uses only the values in `caption_style`.
          example: preset
        caption_style:
          $ref: '#/components/schemas/CaptionStyle'
        segments:
          type: array
          nullable: true
          description: |
            Custom transcript segments for rendering. Each segment defines a time range
            and content to render. If omitted, segments are pulled from the database.
          items:
            type: object
            required:
              - start
              - end
            properties:
              start:
                type: integer
                minimum: 0
                description: Segment start time in milliseconds.
                example: 1500
              end:
                type: integer
                minimum: 0
                description: Segment end time in milliseconds.
                example: 4200
              id:
                type: string
                description: Optional segment identifier.
                example: seg_001
        logo_overlay:
          $ref: '#/components/schemas/LogoOverlay'

    CaptionStyle:
      type: object
      description: |
        Custom caption style overrides. When used with a `caption_preset`, these values
        override the preset defaults. All properties are optional.
      properties:
        # Mode
        mode:
          type: string
          enum:
            - word-by-word
            - kinetic
            - typewriter
            - burst
            - static
          description: Caption display mode.
          example: word-by-word
        # Typography
        fontFamily:
          type: string
          description: Font family name.
          example: Inter
        fontSize:
          type: integer
          minimum: 8
          maximum: 120
          description: Font size in pixels.
          example: 32
        fontWeight:
          type: integer
          minimum: 100
          maximum: 900
          description: Font weight (100-900).
          example: 700
        lineHeight:
          type: number
          minimum: 0.5
          maximum: 3
          description: Line height multiplier.
          example: 1.4
        letterSpacing:
          type: integer
          minimum: -10
          maximum: 50
          description: Letter spacing in pixels.
          example: 0
        kerning:
          type: string
          enum:
            - metrics
            - optical
            - none
          description: Kerning mode.
          example: metrics
        baselineShift:
          type: integer
          minimum: -50
          maximum: 50
          description: Baseline shift in pixels.
          example: 0
        textTransform:
          type: string
          enum:
            - none
            - uppercase
            - lowercase
            - capitalize
          description: Text transformation.
          example: uppercase
        italic:
          type: boolean
          description: Whether to render text in italic.
          example: false
        align:
          type: string
          enum:
            - left
            - center
            - right
          description: Text alignment.
          example: center

        # Colors
        color:
          type: string
          description: "Legacy: default text color. Prefer `textColor`."
          maxLength: 50
          example: "#FFFFFF"
        textColor:
          type: string
          description: Default text color (hex or rgba).
          example: "#FFFFFF"
        activeColor:
          type: string
          description: Color of the currently spoken word.
          example: "#FFD700"
        inactiveColor:
          type: string
          description: Color of words not currently being spoken.
          example: "#CCCCCC"
        activeWordPattern:
          type: string
          description: Pattern for active word matching.
          example: ""

        # Stroke & Shadow
        strokeWidth:
          type: number
          minimum: 0
          maximum: 20
          description: Text stroke width in pixels.
          example: 2
        strokeColor:
          type: string
          description: Text stroke color (hex or rgba).
          example: "#000000"
        strokeJoin:
          type: string
          enum:
            - round
            - miter
            - bevel
          description: Stroke join style.
          example: round
        shadowBlur:
          type: number
          minimum: 0
          maximum: 100
          description: Shadow blur radius in pixels.
          example: 4
        shadowColor:
          type: string
          description: Shadow color (hex or rgba).
          example: "rgba(0,0,0,0.5)"
        shadowOffsetX:
          type: integer
          minimum: -50
          maximum: 50
          description: Shadow horizontal offset in pixels.
          example: 2
        shadowOffsetY:
          type: integer
          minimum: -50
          maximum: 50
          description: Shadow vertical offset in pixels.
          example: 2
        glowIntensity:
          type: number
          minimum: 0
          maximum: 1
          description: Glow effect intensity (0 = off, 1 = maximum).
          example: 0

        # Background
        backgroundColor:
          type: string
          description: Caption background color (hex or rgba).
          example: "rgba(0,0,0,0.6)"
        backgroundPadding:
          type: array
          items:
            type: integer
          minItems: 4
          maxItems: 4
          description: "Background padding in pixels: [top, right, bottom, left]."
          example: [8, 16, 8, 16]
        backgroundRadius:
          type: integer
          minimum: 0
          maximum: 100
          description: Background corner radius in pixels.
          example: 8

        # Position & Layout
        position:
          $ref: '#/components/schemas/PositionOption'
        maxLines:
          type: integer
          minimum: 1
          maximum: 10
          description: Maximum number of caption lines visible at once.
          example: 2
        maxWidthRatio:
          type: number
          minimum: 0.1
          maximum: 1
          description: Maximum caption width as a ratio of video width.
          example: 0.9
        visibleWordCount:
          type: integer
          description: Number of words visible at a time.
          example: 5

        # Animation
        animationModeGroup:
          type: string
          enum:
            - word
            - segment
          description: Whether animations apply per word or per segment.
          example: word
        wordAnimation:
          type: string
          enum:
            - static
            - pop
            - kinetic
            - burst
            - fade
          description: Per-word animation style.
          example: pop
        segmentAnimation:
          type: string
          enum:
            - static
            - typewriter
            - kinetic
            - burst
          description: Per-segment animation style.
          example: static
        transition:
          type: string
          enum:
            - none
            - fade
          description: Transition effect between caption groups.
          example: fade
        fadeInFrames:
          type: integer
          minimum: 0
          maximum: 60
          description: Number of frames for fade-in transition.
          example: 5
        fadeOutFrames:
          type: integer
          minimum: 0
          maximum: 60
          description: Number of frames for fade-out transition.
          example: 5

        # Highlight
        highlightActiveWord:
          type: boolean
          description: Whether to highlight the active (currently spoken) word.
          example: true
        highlightColor:
          type: string
          description: Color used for active word highlighting.
          example: "#FFD700"
        highlightStyle:
          type: string
          enum:
            - color
            - background
            - glow
            - bold
            - none
          description: Style of active word highlighting.
          example: color

    LogoOverlay:
      type: object
      description: Configuration for an optional logo overlay on the rendered video.
      properties:
        enabled:
          type: boolean
          description: Whether the logo overlay is enabled.
          example: true
        url:
          type: string
          format: uri
          description: URL to the logo image file.
          example: "https://myapp.com/logo.png"
        position:
          type: string
          enum:
            - top-left
            - top-right
            - bottom-left
            - bottom-right
          description: Corner position for the logo.
          example: top-right
        opacity:
          type: number
          minimum: 0
          maximum: 1
          description: Logo opacity (0 = transparent, 1 = fully opaque).
          example: 0.8
        scale:
          type: number
          minimum: 0.01
          maximum: 1
          description: Logo scale as a ratio of the video width.
          example: 0.15

    TriggerRenderResponse:
      type: object
      required:
        - success
        - data
      properties:
        success:
          type: boolean
          example: true
        data:
          type: object
          required:
            - render_job_id
            - viral_moment_id
            - status
            - message
          properties:
            render_job_id:
              type: string
              format: uuid
              description: UUID identifier for the render job.
              example: "550e8400-e29b-41d4-a716-446655440000"
            viral_moment_id:
              type: string
              description: The viral moment being rendered.
              example: vm_abc123
            status:
              type: string
              description: Current render job status.
              example: queued
            message:
              type: string
              description: Human-readable status message.
              example: Render job submitted. You will receive a webhook when complete.

    # --- Caption Presets ---

    CaptionPresetsResponse:
      type: object
      required:
        - success
        - data
      properties:
        success:
          type: boolean
          example: true
        data:
          type: object
          required:
            - presets
            - available_properties
            - position_options
          properties:
            presets:
              type: array
              items:
                $ref: '#/components/schemas/CaptionPreset'
            available_properties:
              type: object
              description: Describes customizable caption properties and their constraints.
              properties:
                fontSize:
                  type: object
                  properties:
                    type:
                      type: string
                      example: number
                    min:
                      type: integer
                      example: 8
                    max:
                      type: integer
                      example: 120
                    default:
                      type: integer
                      example: 28
                    description:
                      type: string
                      example: Font size in pixels
                textColor:
                  type: object
                  properties:
                    type:
                      type: string
                      example: color
                    default:
                      type: string
                      example: "#FFFFFF"
                    description:
                      type: string
                      example: "Default text color (hex or rgba)"
                position:
                  type: object
                  properties:
                    type:
                      type: string
                      example: enum
                    options:
                      type: array
                      items:
                        type: string
                      example:
                        - bottom-center
                        - lower-third
                        - center
                        - upper-third
                        - top-center
                    default:
                      type: string
                      example: bottom-center
                    description:
                      type: string
                      example: Vertical position on screen
                wordAnimation:
                  type: object
                  properties:
                    type:
                      type: string
                      example: enum
                    options:
                      type: array
                      items:
                        type: string
                      example:
                        - static
                        - pop
                        - kinetic
                        - burst
                        - fade
                    default:
                      type: string
                      example: pop
                    description:
                      type: string
                      example: Per-word animation style
            position_options:
              type: array
              items:
                $ref: '#/components/schemas/PositionOptionDetail'

    CaptionPreset:
      type: object
      required:
        - key
        - name
        - description
        - category
      properties:
        key:
          $ref: '#/components/schemas/CaptionPresetKey'
        name:
          type: string
          description: Human-readable preset name.
          example: Viral
        description:
          type: string
          description: Brief description of the preset style.
          example: "Clean and bold - the universal choice for any content"
        category:
          type: string
          enum:
            - professional
            - creative
          description: Preset category.
          example: professional

    PositionOptionDetail:
      type: object
      required:
        - value
        - label
        - description
      properties:
        value:
          $ref: '#/components/schemas/PositionOption'
        label:
          type: string
          description: Human-readable label.
          example: Bottom
        description:
          type: string
          description: Description of the position.
          example: "Bottom of video frame (default)"

    # --- Webhook Event Schemas ---

    WebhookVideoProgress:
      type: object
      description: |
        Sent to the `webhooks.progress` URL during video processing.
        Signed with HMAC-SHA256 in the `Signature` header.

        Note: During the download phase, `step_label`, `platform`, `platform_user_id`,
        and `metadata` may be absent. Only `video_id`, `progress`, `step`, and
        `step_progress` are guaranteed on every progress event.
      required:
        - event
        - progress
        - step
        - step_progress
        - timestamp
      properties:
        event:
          type: string
          enum:
            - video.progress
          description: The webhook event type.
          example: video.progress
        job_id:
          type: string
          description: The job ID for this video submission.
          example: job_abc123xyz789
        video_id:
          type: string
          description: The video ID once the video record has been created.
          example: vid_xyz123
        progress:
          type: integer
          minimum: 0
          maximum: 100
          description: "Overall progress (0-100). Download=0-10%, Processing=10-99%."
          example: 45
        step:
          type: string
          description: Current processing step identifier.
          example: ai_clipper_viral_moments
        step_label:
          type: string
          description: Human-readable step name.
          example: Finding Moments
        step_progress:
          type: integer
          minimum: 0
          maximum: 100
          description: Progress within the current step (0-100).
          example: 50
        platform:
          type: string
          description: The platform the user originates from.
          example: discord
        platform_user_id:
          type: string
          description: The user's ID on the external platform.
          example: "123456789012345678"
        timestamp:
          type: string
          format: date-time
          description: ISO 8601 timestamp of the event.
          example: "2025-01-15T10:35:00Z"
        metadata:
          type: object
          nullable: true
          additionalProperties: true
          description: The metadata object provided during video submission. `null` if not provided.
          example:
            channel_id: "987654321"

    WebhookVideoCompleted:
      type: object
      description: |
        Sent to the `webhooks.completion` URL when video processing completes successfully.
        Signed with HMAC-SHA256 in the `Signature` header.
      required:
        - event
        - job_id
        - video_id
        - status
        - result
        - user
        - processing_time_seconds
        - timestamp
        - metadata
      properties:
        event:
          type: string
          enum:
            - video.completed
          description: The webhook event type.
          example: video.completed
        job_id:
          type: string
          description: The job ID for this video submission.
          example: job_abc123xyz789
        video_id:
          type: string
          description: The video ID.
          example: vid_xyz123
        platform:
          type: string
          description: The platform the user originates from.
          example: discord
        platform_user_id:
          type: string
          description: The user's ID on the external platform.
          example: "123456789012345678"
        status:
          type: string
          enum:
            - completed
          description: Processing status.
          example: completed
        result:
          type: object
          description: Processing result containing video info and viral moments.
          required:
            - video
            - viral_moments_count
            - viral_moments
          properties:
            video:
              type: object
              description: Video metadata.
              required:
                - id
                - title
              properties:
                id:
                  type: string
                  example: vid_xyz123
                title:
                  type: string
                  example: My Video
                duration_seconds:
                  type: integer
                  nullable: true
                  description: Video duration in seconds. `null` if not yet determined.
                  example: 1800
                thumbnail_url:
                  type: string
                  format: uri
                  nullable: true
                  description: CDN URL to the video thumbnail.
                  example: "https://cdn.openclip.io/thumbs/vid_xyz123.jpg"
            transcript:
              type: object
              description: Transcript availability and stats.
              required:
                - available
                - word_count
              properties:
                available:
                  type: boolean
                  description: Whether a transcript was generated for this video.
                  example: true
                word_count:
                  type: integer
                  description: Total word count of the transcript.
                  example: 4250
            caption_presets_available:
              type: array
              description: Caption presets available for rendering.
              items:
                type: string
              example:
                - youtube-bold
                - tiktok
                - karaoke
                - hormozi
                - neon-glow
                - bold-statement
                - instagram-minimal
                - typewriter
                - beastly
            viral_moments_count:
              type: integer
              description: Number of viral moments detected.
              example: 5
            viral_moments:
              type: array
              description: Array of detected viral moments.
              items:
                type: object
        user:
          type: object
          description: User info including remaining credits.
          required:
            - id
            - credits_remaining
          properties:
            id:
              type: string
              description: Internal user ID (hashid).
              example: usr_abc123
            platform_user_id:
              type: string
              description: The user's ID on the external platform.
              example: "123456789012345678"
            credits_remaining:
              type: integer
              description: Credits remaining after processing.
              example: 8
        processing_time_seconds:
          type: integer
          description: Total processing time in seconds.
          example: 245
        timestamp:
          type: string
          format: date-time
          description: ISO 8601 timestamp of the event.
          example: "2025-01-15T10:45:00Z"
        metadata:
          type: object
          nullable: true
          additionalProperties: true
          description: The metadata object provided during video submission. `null` if not provided.
          example:
            channel_id: "987654321"

    WebhookVideoFailed:
      type: object
      description: |
        Sent to the `webhooks.completion` URL when video processing fails.
        Signed with HMAC-SHA256 in the `Signature` header.

        Note: For download failures, `video_id`, `user`, and `metadata` may be absent
        since the video record may not exist yet.
      required:
        - event
        - status
        - error
        - timestamp
      properties:
        event:
          type: string
          enum:
            - video.failed
          description: The webhook event type.
          example: video.failed
        job_id:
          type: string
          nullable: true
          description: The job ID for this video submission (may be absent on early failures).
          example: job_abc123xyz789
        video_id:
          type: string
          nullable: true
          description: The video ID. `null` if the video record was not created before failure.
          example: vid_xyz123
        platform:
          type: string
          description: The platform the user originates from.
          example: discord
        platform_user_id:
          type: string
          description: The user's ID on the external platform.
          example: "123456789012345678"
        status:
          type: string
          enum:
            - failed
          description: Processing status.
          example: failed
        error:
          type: object
          description: Structured error information indicating which step failed.
          required:
            - code
            - message
          properties:
            code:
              type: string
              description: Machine-readable error code.
              example: TRANSCRIPTION_FAILED
            message:
              type: string
              description: Human-readable error message.
              example: Transcription service unavailable
            step:
              type: string
              description: The processing step where the failure occurred (if applicable).
              example: generate_transcript
            step_label:
              type: string
              description: Human-readable name of the failed step (if applicable).
              example: Transcribing
        user:
          type: object
          nullable: true
          description: User info. May be absent on download failures.
          properties:
            id:
              type: string
              description: Internal user ID (hashid).
              example: usr_abc123
            platform_user_id:
              type: string
              description: The user's ID on the external platform.
              example: "123456789012345678"
        timestamp:
          type: string
          format: date-time
          description: ISO 8601 timestamp of the event.
          example: "2025-01-15T10:45:00Z"
        metadata:
          type: object
          nullable: true
          additionalProperties: true
          description: The metadata object provided during video submission. `null` if not provided.
          example:
            channel_id: "987654321"

    WebhookVideoPendingCredits:
      type: object
      description: |
        Sent when a video requires credits that the user does not have.
        Signed with HMAC-SHA256 in the `Signature` header.
      required:
        - event
        - video_id
        - status
        - error
        - credits_remaining
        - timestamp
      properties:
        event:
          type: string
          enum:
            - video.pending_credits
          example: video.pending_credits
        job_id:
          type: string
          description: The job ID for this video submission.
          example: job_abc123xyz789
        video_id:
          type: string
          description: The video ID.
          example: vid_xyz123
        platform:
          type: string
          description: The platform the user originates from.
          example: discord
        platform_user_id:
          type: string
          description: The user's ID on the external platform.
          example: "123456789012345678"
        status:
          type: string
          enum:
            - pending_credits
          example: pending_credits
        error:
          type: object
          description: Error details explaining the credits requirement.
          required:
            - code
            - message
          properties:
            code:
              type: string
              description: Machine-readable error code.
              example: INSUFFICIENT_CREDITS
            message:
              type: string
              description: Human-readable error message.
              example: Account has insufficient processing credits
        credits_remaining:
          type: integer
          description: Current credit balance.
          example: 0
        timestamp:
          type: string
          format: date-time
          description: ISO 8601 timestamp of the event.
          example: "2025-01-15T10:35:00Z"
        metadata:
          type: object
          nullable: true
          additionalProperties: true
          description: The metadata object provided during video submission. `null` if not provided.
          example:
            channel_id: "987654321"

    WebhookClipRendered:
      type: object
      description: |
        Sent to the `callback_url` provided in the render request when rendering completes.
        Signed with HMAC-SHA256 in the `Signature` header.
      required:
        - event
        - job_id
        - viral_moment_id
        - status
        - rendered_url
        - timestamp
      properties:
        event:
          type: string
          enum:
            - clip.rendered
          example: clip.rendered
        job_id:
          type: string
          format: uuid
          description: The render job UUID (render_uuid from the original request).
          example: "550e8400-e29b-41d4-a716-446655440000"
        viral_moment_id:
          type: string
          example: vm_abc123
        status:
          type: string
          enum:
            - completed
          example: completed
        rendered_url:
          type: string
          format: uri
          description: CDN URL to the rendered clip with captions.
          example: "https://cdn.openclip.io/rendered/vm_abc123.mp4"
        clip_id:
          type: integer
          description: The clip ID associated with this render.
          example: 42
        timestamp:
          type: string
          format: date-time
          example: "2025-01-15T11:00:00Z"

    WebhookClipRenderFailed:
      type: object
      description: |
        Sent to the `callback_url` provided in the render request when rendering fails.
        Signed with HMAC-SHA256 in the `Signature` header.
      required:
        - event
        - job_id
        - viral_moment_id
        - status
        - error
        - timestamp
      properties:
        event:
          type: string
          enum:
            - clip.render_failed
          example: clip.render_failed
        job_id:
          type: string
          format: uuid
          description: The render job UUID (render_uuid from the original request).
          example: "550e8400-e29b-41d4-a716-446655440000"
        viral_moment_id:
          type: string
          example: vm_abc123
        status:
          type: string
          enum:
            - failed
          example: failed
        error:
          type: object
          description: Structured error information.
          required:
            - code
            - message
          properties:
            code:
              type: string
              description: Machine-readable error code.
              example: RENDER_FAILED
            message:
              type: string
              description: Human-readable sanitized error description.
              example: Rendering failed due to an internal error. Please try again.
            step:
              type: string
              description: The processing step where the failure occurred (if applicable).
              example: caption_render
            step_label:
              type: string
              description: Human-readable name of the failed step (if applicable).
              example: Caption Rendering
        timestamp:
          type: string
          format: date-time
          example: "2025-01-15T11:05:00Z"

  responses:
    ValidationError:
      description: Validation error
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/StandardError'
          example:
            success: false
            message: The given data was invalid.
            errors:
              platform:
                - The platform field is required.
              platform_user_id:
                - The platform user id field is required.

    NotFoundError:
      description: Resource not found
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/StandardError'
          example:
            success: false
            message: Resource not found.

x-webhooks:
  videoProgress:
    post:
      operationId: webhookVideoProgress
      summary: "Webhook: Video Progress"
      description: |
        Sent to the `webhooks.progress` URL during video processing.
        Signed with HMAC-SHA256 in the `Signature` header.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/WebhookVideoProgress'
      responses:
        '200':
          description: Webhook received successfully

  videoCompleted:
    post:
      operationId: webhookVideoCompleted
      summary: "Webhook: Video Completed"
      description: |
        Sent to the `webhooks.completion` URL when video processing finishes successfully.
        Signed with HMAC-SHA256 in the `Signature` header.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/WebhookVideoCompleted'
      responses:
        '200':
          description: Webhook received successfully

  videoFailed:
    post:
      operationId: webhookVideoFailed
      summary: "Webhook: Video Failed"
      description: |
        Sent to the `webhooks.completion` URL when video processing fails.
        Signed with HMAC-SHA256 in the `Signature` header.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/WebhookVideoFailed'
      responses:
        '200':
          description: Webhook received successfully

  videoPendingCredits:
    post:
      operationId: webhookVideoPendingCredits
      summary: "Webhook: Video Pending Credits"
      description: |
        Sent when a video submission requires credits that the user does not have.
        Signed with HMAC-SHA256 in the `Signature` header.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/WebhookVideoPendingCredits'
      responses:
        '200':
          description: Webhook received successfully

  clipRendered:
    post:
      operationId: webhookClipRendered
      summary: "Webhook: Clip Rendered"
      description: |
        Sent to the `callback_url` when a render job completes successfully.
        Signed with HMAC-SHA256 in the `Signature` header.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/WebhookClipRendered'
      responses:
        '200':
          description: Webhook received successfully

  clipRenderFailed:
    post:
      operationId: webhookClipRenderFailed
      summary: "Webhook: Clip Render Failed"
      description: |
        Sent to the `callback_url` when a render job fails.
        Signed with HMAC-SHA256 in the `Signature` header.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/WebhookClipRenderFailed'
      responses:
        '200':
          description: Webhook received successfully
