openapi: 3.1.0
info:
  title: StrataKit API
  description: |
    Video generation API: JSON → MP4/WebM/GIF

    ## TL;DR
    1. `POST /productions` with canvas JSON → returns `{id, status: "pending"}`
    2. Poll `GET /productions/{id}` until `status: "completed"`
    3. Download from `output_url` (GET only — see below)

    ## Presigned URL semantics (READ THIS BEFORE PROBING)
    `output_url`, `thumbnail_url`, `hls_url`, and `proxy_video_url` are
    **presigned R2 (AWS S3 SigV4) URLs signed for the GET method only.**
    AWS SigV4 binds the signature to the HTTP method, so:

    - The **authoritative total size** for a finished render is the
      `output_size` integer (bytes) on the production payload. Prefer it
      over probing the URL, especially in browsers.
    - `HEAD <output_url>` is **not authorized** by the signature. AWS S3
      returns `403 Forbidden` for HEAD against a GET-signed URL. R2 may
      currently respond inconsistently (some accounts have observed
      `200 OK` with a full `Content-Length`); treat any HEAD response as
      best-effort and do not rely on it for size or existence.
    - To probe headers (content-type, existence, total size) without
      downloading the full asset, issue a **ranged GET**: `Range: bytes=0-0`.
      The response is 1 byte (HTTP `206 Partial Content`) and exposes
      `Content-Type` / `ETag` directly. `Content-Length` on a ranged
      response reports the **range size** (e.g. `1` for `bytes=0-0`),
      not the full asset size — when the storage backend includes
      `Content-Range` (e.g. `bytes 0-0/123456`), it carries the total
      object size; when it does not, fall back to `output_size` from the
      API payload.
    - In browsers, a failed probe can look like a CORS failure because R2
      may omit CORS headers on auth/signature failures. Treat that as a
      GET-only signature issue; use the URL directly in media elements
      (which need only ranged GETs) or rely on `output_size`.
    - URLs expire 1 hour after issue. `url_expires_at` (ISO-8601) on the
      production payload is authoritative — re-fetch
      `GET /productions/{id}` to mint a fresh URL.

    ```bash
    # ❌ Returns 403 — signature does not authorize HEAD
    curl -I "$OUTPUT_URL"

    # ✅ Cheap header probe — works with a GET-signed URL
    curl -sS -r 0-0 -o /dev/null -D - "$OUTPUT_URL"
    ```

    ## Auth
    ```
    Authorization: Bearer <YOUR_API_KEY>
    ```

    ## Core Flow
    ```
    POST /productions     → Create video job (async)
    GET /productions/{id} → Poll status: pending→processing→completed|failed
    ```

    Status values: `pending`, `processing`, `completed`, `failed`, `cancelled`

    ## Minimal Request
    ```json
    {
      "canvas": {
        "preset": "youtube_short",
        "segments": [
          { "type": "video", "url": "VIDEO_URL", "duration": 10 }
        ]
      }
    }
    ```

    ## `segments` Format
    Use flat `segments` for all requests. Base content (video, image, color)
    auto-sequences. Overlays (text, shape, html, waveform) render ON TOP of base
    content. TTS/audio each get their own track automatically.

    ```json
    {
      "canvas": {
        "preset": "youtube_short",
        "segments": [
          { "type": "video", "url": "VIDEO_URL", "duration": 10 },
          { "type": "text", "text": "HELLO", "fontSize": 64, "position": "center", "start": 1, "duration": 3 },
          { "type": "tts", "text": "Hello world", "voice": "Sarah" },
          { "type": "audio", "url": "MUSIC_URL", "volume": 0.3, "loop": true }
        ]
      }
    }
    ```

    **TTS Voices:** Two tiers available. **Standard (Kokoro)** voice IDs like `af_bella`, `am_adam` (3 cr/1K chars) or **Premium (ElevenLabs)** names like `Sarah`, `Liam` (13 cr/1K chars). Voice name determines routing automatically.

    ## Key Types
    - **preset**: youtube_short, tiktok, instagram_reel (9:16) | youtube_landscape (16:9) | instagram_feed (1:1)
    - **assets**: video, image, color, text, audio, tts, caption, waveform
    - **codec**: h264 (default), h265, vp9, av1, prores

    ## Sample Media (No Setup Required)
    ```
    VIDEO_9x16="https://assets.stratakit.io/samples/video/8366020-hd_1080_1920_25fps.mp4"
    VIDEO_16x9="https://assets.stratakit.io/samples/video/12221134_3840_2160_24fps.mp4"
    IMAGE="https://assets.stratakit.io/samples/image/sample-landscape-720p.jpg"
    AUDIO="https://assets.stratakit.io/samples/audio/Waltz%20in%20B%20minor%2C%20Op.%2069%20no.%202.mp3"
    ```

    ## Rate Limits
    | Plan | Requests/min | Concurrent |
    |------|--------------|------------|
    | Free | 60 | 1 |
    | Starter | 120 | 3 |
    | Creator | 120 | 3 |
    | Pro | 300 | 5 |
    | Scale | 1000 | 10 |

    ## More Info
    - LLM-optimized reference: https://stratakit.io/llms.txt
    - Full docs: https://stratakit.io/docs
  version: 1.0.0
  contact:
    name: StrataKit Support
    email: support@stratakit.io
  license:
    name: MIT

servers:
  - url: https://stratakit.io/api/v1
    description: Production
  - url: http://localhost:3000/api/v1
    description: Local Development

tags:
  - name: Productions
    description: Create and manage video productions
  - name: Blueprints
    description: >-
      Reusable video templates. The StrataKit product UI calls these
      "Templates" (user-facing), while the API path and JSON schema names
      keep `blueprint` for backward compatibility. `Template` and
      `Blueprint` refer to the same resource.
  - name: Media
    description: Upload media files for use in productions
  - name: Health
    description: API health status
  - name: Custom Effects
    description: AI-generated custom FFmpeg filter effects
  - name: Voices
    description: Available text-to-speech voices
  - name: Sharing
    description: Production visibility and share link management
  - name: Tags
    description: Visual effect tags for Mix & Match (free, no auth)
  - name: Preview
    description: Free low-resolution video preview rendering (480p, rate-limited)
  - name: Account
    description: Account usage and quota snapshot (credits, storage, rate limits)

security:
  - BearerAuth: []

paths:
  /health:
    get:
      operationId: healthCheck
      summary: Check API health status (shallow)
      description: |
        Fast liveness probe — no authentication required, suitable for uptime
        monitors. Returns 200 with a per-component status snapshot.

        For full connectivity checks against all dependencies, use the
        authenticated `GET /health/deep` endpoint.

        > **Rate-limit exempt.** Shallow `GET /health` and `HEAD /health` are
        > NOT counted against the per-IP anonymous rate-limit bucket and
        > emit no `RateLimit-*` / `X-RateLimit-*` headers. Uptime monitors
        > can poll without backoff. Deep mode (`/health/deep`) is still
        > authenticated and rate-limited via the caller's API key.

        > **Back-compat note:** `GET /health?deep=true` continues to work for
        > existing scripts (it requires the same Bearer API-key auth as
        > `/health/deep` — both paths now flow through the standard
        > Bearer-token gate so the `WWW-Authenticate: Bearer` 401 challenge
        > is accurate), but the spec only documents the `/health/deep` form
        > going forward — a separate path is the only standards-compliant
        > way to declare an auth requirement in OpenAPI 3.x (security
        > attaches to operations, not query params).
      tags: [Health]
      security: []
      x-ratelimit-exempt: true
      responses:
        '200':
          description: Shallow health snapshot (always 200 in shallow mode)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HealthCheckResponse'

  /health/deep:
    get:
      operationId: healthCheckDeep
      summary: Check API health status (deep — full dependency probe)
      description: |
        Runs full connectivity checks against every backing service (DB,
        R2, queues, Modal, fal.ai). Authenticated — requires a Bearer API
        key (same scheme as every other `/api/v1/*` operation).

        Returns `200` when the system is `healthy` or `degraded`, and
        `503` when any component reports `unhealthy`.
      tags: [Health]
      security:
        - BearerAuth: []
      responses:
        '200':
          description: Deep health snapshot — system is healthy or degraded
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HealthCheckResponse'
        '401':
          description: |
            Missing or invalid API key. The response includes an RFC 6750
            `WWW-Authenticate: Bearer` challenge so Bearer-aware clients
            see a uniform contract across `/api/v1/*`.
          headers:
            WWW-Authenticate:
              schema:
                type: string
              description: |
                `Bearer realm="stratakit", error="invalid_request"` when the
                `Authorization` header is absent, or
                `Bearer realm="stratakit", error="invalid_token"` when the
                supplied API key is malformed, unknown, or revoked.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '429':
          description: Rate limit exceeded
        '503':
          description: At least one component is unhealthy
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HealthCheckResponse'

  /usage:
    get:
      operationId: getUsage
      summary: Snapshot of the authenticated user's quota and usage
      description: |
        Returns the authenticated user's current credit balance, plan,
        storage usage, and rate-limit ceilings. Use this to throttle a
        client preemptively or display a "credits remaining" UI without
        scraping the dashboard.

        The response is for the authenticated user only — it does not
        leak any other user's data, Stripe IDs, or admin state.
      tags: [Account]
      responses:
        '200':
          description: Current usage snapshot
          content:
            application/json:
              schema:
                type: object
                required: [plan, plan_name, credits_remaining, storage, rate_limits, subscription]
                properties:
                  plan:
                    type: string
                    enum: [free, starter, creator, pro, scale]
                  plan_name:
                    type: string
                    description: Human-readable plan name (e.g., "Pay As You Go", "Pro").
                  plan_credits_per_month:
                    type: integer
                    nullable: true
                    description: Monthly credit grant for paid tiers; `null` for Pay As You Go.
                  credits_remaining:
                    type: integer
                    description: Credits available right now.
                  storage:
                    type: object
                    required: [bytes_used, bytes_limit]
                    properties:
                      bytes_used:
                        type: integer
                        format: int64
                      bytes_limit:
                        type: integer
                        format: int64
                  rate_limits:
                    type: object
                    required: [api_per_minute, production_per_hour, concurrent_jobs]
                    properties:
                      api_per_minute:
                        type: integer
                        description: Per-API-key requests per minute (general API).
                      production_per_hour:
                        type: integer
                        description: Production creates per hour for this plan.
                      concurrent_jobs:
                        type: integer
                        description: Maximum concurrent production jobs.
                  subscription:
                    type: object
                    required: [is_billable]
                    properties:
                      status:
                        type: string
                        nullable: true
                        description: Stripe subscription status, or `null` if no subscription.
                      is_billable:
                        type: boolean
                        description: True when status is `active` or `trialing`.
        '401':
          description: Missing or invalid API key
        '429':
          description: Rate limit exceeded

  /productions:
    post:
      operationId: createProduction
      summary: Create a new video production
      description: |
        Start a new video rendering job. The production will be queued and processed asynchronously.

        You can either provide a `canvas` specification directly, or reference a `templateId` (blueprint)
        with variable `bindings`.

        ### Preview (dry-run)
        Pass `?preview=true` to run the same validation pipeline (Zod shape, canvas
        resolution, plan-scaled TTS cap) and get back a credit estimate **without**
        enqueueing the job, debiting credits, or persisting any production row.
        Returns a `200` with `{ preview: true, total_credits, credits_breakdown,
        estimated_duration_seconds }`. Useful for "see the price before submitting"
        UX flows.
      tags: [Productions]
      parameters:
        - name: preview
          in: query
          description: |
            When `true`, runs the request as a dry-run and returns a credit estimate
            without queueing the job, debiting credits, or persisting any database
            row. Response is a `200` with `{ preview: true, total_credits,
            credits_breakdown, estimated_duration_seconds }` instead of the regular
            `202` create envelope.
          required: false
          schema:
            type: boolean
            default: false
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateProductionRequest'
            examples:
              colorBackground:
                summary: Simple color background
                description: Creates a 5-second video with solid indigo background
                value:
                  canvas:
                    preset: youtube_short
                    segments:
                      - type: color
                        color: "#4F46E5"
                        duration: 5
              videoClip:
                summary: Single video clip
                description: Process a video file with trim
                value:
                  canvas:
                    preset: instagram_reel
                    segments:
                      - type: video
                        url: "https://assets.stratakit.io/samples/video/8366020-hd_1080_1920_25fps.mp4"
                        trim:
                          start: 0
                          end: 10
                        duration: 10
                  webhookUrl: "https://yourapp.com/webhooks/stratakit"
              multiSegment:
                summary: Multi-segment slideshow with TTS
                description: Sequential video and image clips with auto-sequencing, plus TTS narration
                value:
                  canvas:
                    preset: tiktok
                    segments:
                      - type: video
                        url: "https://assets.stratakit.io/samples/video/8366020-hd_1080_1920_25fps.mp4"
                        duration: 5
                      - type: image
                        url: "https://assets.stratakit.io/samples/image/sample-vertical-9x16.jpg"
                        duration: 5
                      - type: tts
                        text: "Welcome to StrataKit, the easiest way to create videos programmatically."
                        voice: "Sarah"
              ttsWithMusic:
                summary: TTS with Premium voice (ElevenLabs) and background music
                description: Image slideshow with Premium ElevenLabs voiceover (13 cr/1K chars) and background music
                value:
                  canvas:
                    preset: youtube_landscape
                    segments:
                      - type: image
                        url: "https://assets.stratakit.io/samples/image/sample-landscape-720p.jpg"
                        duration: 10
                      - type: tts
                        text: "Welcome to StrataKit, the easiest way to create videos programmatically."
                        voice: "Sarah"
                      - type: audio
                        url: "https://assets.stratakit.io/samples/audio/Waltz%20in%20B%20minor%2C%20Op.%2069%20no.%202.mp3"
                        volume: 0.3
                        loop: true
              ttsKokoroStandard:
                summary: TTS with Standard voice (Kokoro)
                description: Cost-effective TTS with Kokoro Standard voice (3 cr/1K chars). Kokoro voice IDs use the pattern {lang}{gender}_{name} (e.g., af_bella = American female).
                value:
                  canvas:
                    preset: youtube_short
                    segments:
                      - type: image
                        url: "https://assets.stratakit.io/samples/image/sample-vertical-9x16.jpg"
                        duration: 10
                      - type: tts
                        text: "Welcome to our product demo. This uses the cost-effective Kokoro Standard voice."
                        voice: "af_bella"
              textOverlay:
                summary: Video with text overlay
                description: Text segments automatically render on top of base video content
                value:
                  canvas:
                    preset: tiktok
                    segments:
                      - type: video
                        url: "https://assets.stratakit.io/samples/video/8366020-hd_1080_1920_25fps.mp4"
                        duration: 10
                      - type: text
                        text: "Hello World!"
                        fontSize: 72
                        fontColor: "#FFFFFF"
                        position: center
                        start: 1
                        duration: 3
                        animationIn: fadeIn
                        animationOut: fadeOut
              templateWithBindings:
                summary: Use template with variables
                description: Render a blueprint template with dynamic values
                value:
                  templateId: "550e8400-e29b-41d4-a716-446655440000"
                  bindings:
                    headline: "Summer Sale - 50% Off!"
                    product_video: "https://example.com/product.mp4"
                    logo: "https://example.com/logo.png"
      responses:
        '200':
          description: |
            Preview (dry-run) result. Returned only when the request is sent with
            `?preview=true`. No production is queued and no credits are debited.
          content:
            application/json:
              schema:
                type: object
                required: [preview, total_credits, credits_breakdown, estimated_duration_seconds]
                properties:
                  preview:
                    type: boolean
                    enum: [true]
                    description: Always `true` on this response shape.
                  total_credits:
                    type: integer
                    description: Total credits the production would consume if submitted.
                  estimated_duration_seconds:
                    type: number
                    description: Total canvas duration in seconds (longest segment span).
                  credits_breakdown:
                    type: object
                    required: [base_fee, video_render, tts, effects_processing]
                    properties:
                      base_fee:
                        type: integer
                      video_render:
                        type: integer
                      tts:
                        type: integer
                      effects_processing:
                        type: integer
        '202':
          description: Production created and queued
          headers:
            x-request-id:
              $ref: '#/components/headers/XRequestId'
            X-Credits-Used:
              schema:
                type: integer
              description: Credits consumed by this production
            X-Credits-Remaining:
              schema:
                type: integer
              description: Remaining credits balance after this production
            X-Cost-USD:
              schema:
                type: string
              description: |
                Cost of this production in USD, formatted as a fixed-precision
                decimal string (4 fractional digits, e.g. `"0.0500"`). Mirrors
                `credits_used` (1 credit = $0.01) for clients that bill in
                fiat. Always present on a 202 — for admin-mode callers (no
                credits deducted) the value is `"0.0000"`.
            X-Plan:
              schema:
                type: string
              description: Current plan. Values: free (Pay As You Go), starter, creator, pro, scale
            X-StrataKit-Admin-Mode:
              schema:
                type: string
                enum: ["true"]
              description: |
                Present and set to `"true"` ONLY when the caller is an
                admin-allowlisted account (`ADMIN_USER_IDS`). Admin mode
                bypasses the concurrent-jobs cap (`max_concurrent: 999`)
                and credit deduction (`credits_used: 0`, `credits_breakdown`
                all zero). Header presence-or-absence IS the signal —
                non-admin responses omit this header entirely; the server
                does NOT emit `X-StrataKit-Admin-Mode: false`. Replaced
                the legacy `is_admin` body field on 2026-04-30 to keep
                role state out of the JSON wire format.
            X-RateLimit-Limit:
              schema:
                type: integer
              description: Maximum requests allowed per minute
            X-RateLimit-Remaining:
              schema:
                type: integer
              description: Requests remaining in current window
            X-RateLimit-Reset:
              schema:
                type: integer
              description: Unix timestamp when rate limit resets
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ProductionResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '402':
          $ref: '#/components/responses/InsufficientCredits'
        '429':
          $ref: '#/components/responses/RateLimited'
        '503':
          $ref: '#/components/responses/ServiceUnavailable'

    get:
      operationId: listProductions
      summary: List all productions
      description: |
        Returns paginated list of productions. Supports two pagination modes:
        - **Cursor pagination (recommended for large datasets):** Use the `cursor` query
          parameter with the `next_cursor` value from a previous response. The cursor format
          is a compound `{ISO_timestamp}|{uuid}` (deterministic ordering even with duplicate
          timestamps). Plain timestamps are NOT accepted and return 400.
        - **Offset pagination:** Use `offset` and `limit` for simple paging. Less efficient
          for large datasets.
      tags: [Productions]
      parameters:
        - name: limit
          in: query
          schema:
            type: integer
            minimum: 1
            maximum: 100
            default: 20
        - name: offset
          in: query
          schema:
            type: integer
            minimum: 0
            default: 0
        - name: cursor
          in: query
          description: |
            Compound cursor in format `{ISO_timestamp}|{uuid}` (e.g.
            `2026-01-15T12:34:56.789Z|550e8400-e29b-41d4-a716-446655440000`).
            Use the `next_cursor` value returned in a previous response.
            Plain timestamps without `|uuid` are rejected with 400.
          schema:
            type: string
        - name: status
          in: query
          schema:
            $ref: '#/components/schemas/ProductionStatus'
        - name: visibility
          in: query
          description: Filter by visibility level
          schema:
            type: string
            enum: [private, unlisted, public]
        - name: template_id
          in: query
          description: Filter by source blueprint/template ID
          schema:
            type: string
            format: uuid
      responses:
        '200':
          description: List of productions
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ProductionList'
        '400':
          description: Invalid query parameter (e.g. malformed cursor format)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                error:
                  code: "VALIDATION_ERROR"
                  message: "Invalid cursor: must be compound format (timestamp|id)"
        '401':
          $ref: '#/components/responses/Unauthorized'

  /productions/{id}:
    get:
      operationId: getProduction
      summary: Get production status and details
      tags: [Productions]
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        '200':
          description: Production details
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Production'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'

    delete:
      operationId: deleteProduction
      summary: Delete a production
      description: |
        Permanently delete a production and its associated files from R2 storage.
        This action cannot be undone. Works for productions in any status (queued, processing, completed, failed).
      tags: [Productions]
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        '200':
          description: Production deleted successfully
          content:
            application/json:
              schema:
                type: object
                properties:
                  deleted:
                    type: boolean
                    description: Whether the production was deleted from the database
                    example: true
                  id:
                    type: string
                    format: uuid
                    description: The deleted production ID
                  video_deleted:
                    type: boolean
                    description: Whether the video file was deleted from storage (false if no video existed)
                    example: true
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'

  /productions/{id}/export:
    get:
      operationId: exportProduction
      summary: Export production timeline as FCP XML
      description: |
        Export a completed production's timeline as a Final Cut Pro XML (FCPXML 1.10) document.
        This allows importing the production's timeline structure into Final Cut Pro, DaVinci Resolve,
        or other NLE software that supports FCPXML import.

        **Requirements:**
        - Production must have status `completed`
        - Canvas data must be available

        **Supported formats:**
        - `fcpxml` - Final Cut Pro XML 1.10

        **Notes:**
        - TTS segments are exported as placeholder gaps with text notes
        - Color segments are exported as gaps with color metadata
        - Text overlays are exported as gaps with text notes
        - Video and image assets reference their original source URLs
      tags: [Productions]
      parameters:
        - name: id
          in: path
          required: true
          description: Production ID
          schema:
            type: string
            format: uuid
        - name: format
          in: query
          required: true
          description: Export format
          schema:
            type: string
            enum: [fcpxml]
      responses:
        '200':
          description: FCP XML document
          headers:
            Content-Disposition:
              schema:
                type: string
              description: 'Attachment filename (e.g., "production-{id}.fcpxml")'
          content:
            application/xml:
              schema:
                type: string
                description: Valid FCPXML 1.10 document
        '400':
          description: Missing or unsupported format, or production not completed
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'

  /productions/batch:
    post:
      operationId: createBatchProductions
      summary: Create multiple productions from a template
      description: |
        Create multiple video productions in a single request using a template (blueprint) with variable bindings.
        This is ideal for generating personalized videos at scale (e.g., personalized marketing videos for each customer).

        **Key Features:**
        - Process up to 50 items per request
        - Each item can have unique bindings (variable substitutions)
        - Single webhook URL for all productions in the batch
        - Idempotency prefix for duplicate prevention
        - Atomic minute balance deduction

        **Usage:**
        1. Create a blueprint template with `{{variable}}` placeholders
        2. Send a batch request with items containing different bindings
        3. Each item becomes a separate production with unique content
      tags: [Productions]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/BatchProductionRequest'
            examples:
              personalizedAds:
                summary: Personalized ad videos
                description: Create personalized ads for multiple customers
                value:
                  templateId: "550e8400-e29b-41d4-a716-446655440000"
                  items:
                    - name: "Ad for John"
                      bindings:
                        customer_name: "John Smith"
                        product_name: "Premium Widget"
                        discount: "20%"
                    - name: "Ad for Jane"
                      bindings:
                        customer_name: "Jane Doe"
                        product_name: "Deluxe Widget"
                        discount: "15%"
                  webhookUrl: "https://yourapp.com/webhooks/stratakit"
                  idempotencyPrefix: "campaign-2024-01"
      responses:
        '202':
          description: All productions created successfully
          headers:
            X-RateLimit-Limit:
              schema:
                type: integer
              description: Maximum requests allowed per minute
            X-RateLimit-Remaining:
              schema:
                type: integer
              description: Requests remaining in current window
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/BatchProductionResponse'
              example:
                message: "Created 2 of 2 productions"
                batchId: "batch_1704067200000"
                productions:
                  - id: "550e8400-e29b-41d4-a716-446655440001"
                    name: "Ad for John"
                    status: "pending"
                    creditsEstimate: 5
                    index: 0
                  - id: "550e8400-e29b-41d4-a716-446655440002"
                    name: "Ad for Jane"
                    status: "pending"
                    creditsEstimate: 5
                    index: 1
                summary:
                  totalRequested: 2
                  created: 2
                  failed: 0
                  totalCredits: 10
                  testMode: false
        '207':
          description: Partial success (some productions failed)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/BatchProductionResponse'
              example:
                message: "Created 1 of 2 productions"
                batchId: "batch_1704067200000"
                productions:
                  - id: "550e8400-e29b-41d4-a716-446655440001"
                    name: "Ad for John"
                    status: "pending"
                    creditsEstimate: 5
                    index: 0
                errors:
                  - index: 1
                    name: "Ad for Jane"
                    error: "Missing required binding: product_name"
                summary:
                  totalRequested: 2
                  created: 1
                  failed: 1
                  totalCredits: 5
                  testMode: false
        '400':
          description: All productions failed or invalid request
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '402':
          $ref: '#/components/responses/InsufficientCredits'
        '404':
          description: Template not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                error:
                  code: "TEMPLATE_NOT_FOUND"
                  message: "The specified template does not exist. Verify the templateId and try again."
        '429':
          $ref: '#/components/responses/RateLimited'

  /blueprints:
    post:
      operationId: createBlueprint
      summary: Create a reusable template
      description: |
        Create a blueprint (template) that can be reused with variable bindings.
        Use `{{variable_name}}` syntax in your canvas to define bindable values.
      tags: [Blueprints]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateBlueprintRequest'
            examples:
              socialTemplate:
                summary: Social media promo template
                value:
                  name: "Product Promo"
                  description: "9:16 product showcase with headline and CTA"
                  category: "social"
                  canvas:
                    preset: instagram_reel
                    segments:
                      - type: color
                        color: "#111827"
                        duration: 10
                      - type: text
                        text: "{{headline}}"
                        fontSize: 64
                        fontWeight: bold
                        fontColor: "#FFFFFF"
                        duration: 3
                        position:
                          y: "20%"
                          anchor: top-center
                  defaultBindings:
                    headline:
                      type: text
                      label: Headline
                      defaultValue: "Launch day"
                      required: true
                  isPublic: false
      responses:
        '201':
          description: Blueprint created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Blueprint'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'

    get:
      operationId: listBlueprints
      summary: List all blueprints
      tags: [Blueprints]
      parameters:
        - name: limit
          in: query
          schema:
            type: integer
            minimum: 1
            maximum: 100
            default: 20
        - name: offset
          in: query
          schema:
            type: integer
            minimum: 0
            default: 0
        - name: isPublic
          in: query
          description: Filter by public/private status
          schema:
            type: boolean
      responses:
        '200':
          description: List of blueprints
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/BlueprintList'
        '401':
          $ref: '#/components/responses/Unauthorized'

  /blueprints/{id}:
    get:
      operationId: getBlueprint
      summary: Get blueprint details
      tags: [Blueprints]
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        '200':
          description: Blueprint details
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Blueprint'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'

    patch:
      operationId: updateBlueprint
      summary: Update a blueprint
      tags: [Blueprints]
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/UpdateBlueprintRequest'
      responses:
        '200':
          description: Blueprint updated
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Blueprint'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'

    delete:
      operationId: deleteBlueprint
      summary: Delete a blueprint
      tags: [Blueprints]
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        '200':
          description: Blueprint deleted
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    example: true
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'

  /blueprints/{id}/fork:
    post:
      operationId: forkBlueprint
      summary: Fork a blueprint to your collection
      description: |
        Copy a public blueprint (or one you own) to your own collection.
        The forked blueprint will be private by default and can be customized.
      tags: [Blueprints]
      parameters:
        - name: id
          in: path
          required: true
          description: ID of the blueprint to fork
          schema:
            type: string
            format: uuid
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                name:
                  type: string
                  description: Custom name for the forked blueprint (defaults to "Original Name (Copy)")
            example:
              name: "My Custom Product Promo"
      responses:
        '200':
          description: Blueprint forked successfully
          content:
            application/json:
              schema:
                type: object
                properties:
                  id:
                    type: string
                    format: uuid
                    description: ID of the newly forked blueprint
                  name:
                    type: string
                  description:
                    type: string
                    nullable: true
                  category:
                    type: string
                    nullable: true
                  forkedFrom:
                    type: string
                    format: uuid
                    deprecated: true
                    description: Deprecated camelCase alias — use `forked_from`.
                  forked_from:
                    type: string
                    format: uuid
                    description: |
                      ID of the original blueprint. Returned only on this
                      fork endpoint; subsequent `GET /blueprints/{id}` on
                      the forked row does NOT return this field (the
                      source reference isn't persisted on the forked row).
                      Clients that need a stable source lookup should
                      cache the value from this response.
                  createdAt:
                    type: string
                    format: date-time
              example:
                id: "550e8400-e29b-41d4-a716-446655440001"
                name: "My Custom Product Promo"
                description: "9:16 product showcase with headline and CTA"
                category: "social"
                forkedFrom: "550e8400-e29b-41d4-a716-446655440000"
                forked_from: "550e8400-e29b-41d4-a716-446655440000"
                createdAt: "2024-01-15T10:30:00Z"
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          description: Blueprint not found or not accessible (must be public or owned by user)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                error:
                  code: "NOT_FOUND"
                  message: "Blueprint not found or not accessible"

  /media/upload-url:
    post:
      summary: Get presigned upload URL
      description: |
        Generate a presigned URL for direct-to-R2 upload. Upload your media files directly
        to cloud storage without going through our servers.

        **Supported file types:**
        - Video: MP4, WebM, MOV (max 5 GB)
        - Image: JPEG, PNG, WebP, GIF (max 50 MB)
        - Audio: MP3, WAV, AAC, OGG (max 500 MB)

        **Usage:**
        1. Call this endpoint to get a presigned URL
        2. PUT your file directly to the `uploadUrl`
        3. Use the `publicUrl` in your production requests
      operationId: getUploadUrl
      tags:
        - Media
      security:
        - BearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/UploadUrlRequest'
            examples:
              video:
                summary: Video upload
                value:
                  filename: "my-video.mp4"
                  contentType: "video/mp4"
                  fileSize: 10485760
              image:
                summary: Image upload
                value:
                  filename: "background.jpg"
                  contentType: "image/jpeg"
                  fileSize: 2097152
              audio:
                summary: Audio upload
                value:
                  filename: "soundtrack.mp3"
                  contentType: "audio/mpeg"
                  fileSize: 5242880
      responses:
        '200':
          description: Presigned URL generated successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/UploadUrlResponse'
              example:
                uploadUrl: "https://stratakit-videos-enam.r2.cloudflarestorage.com/uploads/usr_xxx/abc123/my-video.mp4?X-Amz-Algorithm=..."
                publicUrl: "https://assets.stratakit.io/uploads/usr_xxx/abc123/my-video.mp4"
                key: "uploads/usr_xxx/abc123/my-video.mp4"
                expiresIn: 900
                category: "video"
        '400':
          description: Invalid request (unsupported file type or size exceeded)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              examples:
                unsupportedType:
                  summary: Unsupported file type
                  value:
                    error:
                      code: "VALIDATION_ERROR"
                      message: "Unsupported file type: application/pdf"
                      details:
                        allowedTypes: ["video/mp4", "video/webm", "video/quicktime", "image/jpeg", "image/png", "image/webp", "image/gif", "audio/mpeg", "audio/wav", "audio/aac", "audio/ogg"]
                fileTooLarge:
                  summary: File too large
                  value:
                    error:
                      code: "VALIDATION_ERROR"
                      message: "File too large. Maximum size for video: 5 GB"
                      details:
                        maxSize: 5368709120
        '401':
          $ref: '#/components/responses/Unauthorized'

  /media/confirm:
    post:
      summary: Confirm media upload completion
      description: |
        Confirm that a media file has been successfully uploaded to the presigned URL.
        This endpoint should be called after the file has been PUT to the `uploadUrl` returned
        by `/media/upload-url`.

        **Purpose:**
        - Prevents orphaned files in storage
        - Updates user's storage usage quota
        - Marks the upload as complete for cleanup protection

        **Usage:**
        1. Call `/media/upload-url` to get a presigned URL
        2. PUT your file to the `uploadUrl`
        3. Call this endpoint with the `key` and `size` to confirm
        4. Use the `publicUrl` in your productions
      operationId: confirmMediaUpload
      tags:
        - Media
      security:
        - BearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ConfirmUploadRequest'
            examples:
              videoUpload:
                summary: Confirm video upload
                value:
                  key: "uploads/usr_xxx/abc123/my-video.mp4"
                  size: 10485760
      responses:
        '200':
          description: Upload confirmed successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ConfirmUploadResponse'
              example:
                success: true
                key: "uploads/usr_xxx/abc123/my-video.mp4"
                storage:
                  used: 10485760
                  quota: 524288000
        '400':
          description: Invalid request (missing key or invalid size)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              examples:
                missingKey:
                  summary: Missing key
                  value:
                    error:
                      code: "VALIDATION_ERROR"
                      message: "Missing required field: key"
                invalidSize:
                  summary: Invalid size
                  value:
                    error:
                      code: "VALIDATION_ERROR"
                      message: "Invalid size: must be a non-negative number"
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          description: Key does not belong to this user
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                error:
                  code: "FORBIDDEN"
                  message: "Forbidden: key does not belong to this user"
        '404':
          description: Pending upload not found or expired
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                error:
                  code: "NOT_FOUND"
                  message: "Pending upload not found. The upload URL may have expired."
        '409':
          description: Upload already confirmed
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                error:
                  code: "CONFLICT"
                  message: "Upload already confirmed"

  /canvas-wizard:
    post:
      operationId: canvasWizard
      summary: Apply AI theme to a canvas
      description: |
        Apply a cohesive visual theme (effects, transitions, caption styles) to an existing canvas
        using AI. Returns the modified canvas for preview, or optionally creates a production directly.

        **Themes:** cyberpunk, noir, retro, high_energy, cinematic, minimal, documentary, viral

        **Cost:** 1 credit for the wizard call. If `createProduction` is true, additional production
        credits are charged based on the modified canvas (same as POST /productions).
        Visual effect tags are free (0 credits); there is no extra style surcharge for using tags.

        **Modes:**
        - **Preview mode** (default): Returns the modified canvas and a diff summary
        - **Direct-create mode** (`createProduction: true`): Creates a production with the modified canvas
      tags: [Productions]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                canvas:
                  $ref: '#/components/schemas/Canvas'
                theme:
                  type: string
                  maxLength: 50
                  description: Theme ID to apply
                  enum: [cyberpunk, noir, retro, high_energy, cinematic, minimal, documentary, viral]
                guidance:
                  type: string
                  maxLength: 500
                  description: Custom style guidance for the AI
                createProduction:
                  type: boolean
                  description: If true, create a production with the modified canvas
                  default: false
                name:
                  type: string
                  maxLength: 255
                  description: Production name (only used with createProduction)
                priority:
                  type: string
                  enum: [low, normal, high]
                  description: Production priority (only used with createProduction)
                testMode:
                  type: boolean
                  description: Test mode (only used with createProduction)
                tags:
                  type: array
                  description: |
                    Visual effect tags to apply (alternative to theme). Mix up to 5 tags
                    with individual intensity values. Tags are FREE (0 credits).
                    Use `GET /tags` to list available tags.
                  items:
                    $ref: '#/components/schemas/EffectTag'
                  maxItems: 5
                effect_tags:
                  type: array
                  description: Alias for `tags` (either field accepted, `tags` takes priority)
                  items:
                    $ref: '#/components/schemas/EffectTag'
                  maxItems: 5
                deploy:
                  type: object
                  description: |
                    Auto-publish to social platforms after production completes.
                    Requires `createProduction: true` and an active subscription with social publishing.
                    Title and description are auto-generated from canvas content if not provided.
                  properties:
                    connection_ids:
                      type: array
                      items:
                        type: string
                        format: uuid
                      minItems: 1
                      maxItems: 10
                      description: Social connection IDs to publish to
                    use_defaults:
                      type: boolean
                      description: Use connections marked as default (alternative to connection_ids)
                    title:
                      type: string
                      maxLength: 200
                      description: Override auto-generated title
                    description:
                      type: string
                      maxLength: 5000
                      description: Override auto-generated description
                    tags:
                      type: array
                      items:
                        type: string
                        maxLength: 100
                      maxItems: 30
                    visibility:
                      type: string
                      enum: [public, unlisted, private]
                      default: public
              required:
                - canvas
            examples:
              themePreview:
                summary: Apply cyberpunk theme (preview mode)
                value:
                  canvas:
                    preset: tiktok
                    tracks:
                      - segments:
                          - start: 0
                            duration: 5
                            asset:
                              type: image
                              url: "https://assets.stratakit.io/samples/image/sample-vertical-9x16.jpg"
                  theme: cyberpunk
              customGuidance:
                summary: Custom style guidance
                value:
                  canvas:
                    preset: youtube_short
                    tracks:
                      - segments:
                          - start: 0
                            duration: 10
                            asset:
                              type: video
                              url: "https://assets.stratakit.io/samples/video/8366020-hd_1080_1920_25fps.mp4"
                  guidance: "Make it look like a horror movie trailer with dark grading"
              directCreate:
                summary: Apply theme and create production
                value:
                  canvas:
                    preset: instagram_reel
                    tracks:
                      - segments:
                          - start: 0
                            duration: 5
                            asset:
                              type: image
                              url: "https://assets.stratakit.io/samples/image/sample-vertical-9x16.jpg"
                  theme: viral
                  createProduction: true
                  name: "My Viral Short"
              directCreateWithDeploy:
                summary: Apply theme, create production, and auto-publish
                value:
                  canvas:
                    preset: tiktok
                    tracks:
                      - segments:
                          - start: 0
                            duration: 5
                            asset:
                              type: image
                              url: "https://assets.stratakit.io/samples/image/sample-vertical-9x16.jpg"
                  theme: high_energy
                  createProduction: true
                  name: "Summer Campaign"
                  deploy:
                    use_defaults: true
      responses:
        '200':
          description: Preview mode — modified canvas with diff
          content:
            application/json:
              schema:
                type: object
                properties:
                  canvas:
                    $ref: '#/components/schemas/Canvas'
                  diff:
                    type: object
                    properties:
                      effects_added:
                        type: integer
                      transitions_added:
                        type: integer
                      caption_style_changed:
                        type: boolean
                      summary:
                        type: string
                  theme:
                    type: string
                    nullable: true
                  ai_generated:
                    type: boolean
                  credits_used:
                    type: integer
                  credits_remaining:
                    type: integer
        '202':
          description: Direct-create mode — production created
          headers:
            X-StrataKit-Admin-Mode:
              schema:
                type: string
                enum: ["true"]
              description: |
                Same admin-mode signal as `POST /productions`. Present and
                set to `"true"` only when the caller is admin-allowlisted;
                omitted entirely otherwise.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/CreateProductionResponse'
                  - type: object
                    properties:
                      wizard:
                        type: object
                        properties:
                          ai_generated:
                            type: boolean
                          theme:
                            type: string
                            nullable: true
                          effects_added:
                            type: integer
                          transitions_added:
                            type: integer
                          caption_style_changed:
                            type: boolean
                          summary:
                            type: string
                          wizard_credits_used:
                            type: integer
                      deploy:
                        type: object
                        description: Present when deploy was requested
                        properties:
                          targets:
                            type: integer
                            description: Number of social publish targets
                          connections:
                            type: array
                            items:
                              type: string
                          status:
                            type: string
                            description: Always "pending_production_completion"
        '400':
          description: Validation error or invalid theme
        '402':
          description: Insufficient credits or publish limit reached
        '403':
          description: Forbidden — missing social:deploy scope, plan limits for API deploy, or social publishing temporarily unavailable
      security:
        - BearerAuth: []

  /custom-effects:
    post:
      summary: Generate custom FFmpeg effect
      description: |
        Use AI to generate an FFmpeg filter expression from a natural language description.
        Available on every plan tier. Costs 1 credit for AI-generated results.
        Library and cached matches are free. Common descriptions return instantly from a pre-built library.
      operationId: generateCustomEffect
      tags:
        - Custom Effects
      security:
        - BearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [description]
              properties:
                description:
                  type: string
                  description: Natural language description of the desired effect
                  minLength: 1
                  maxLength: 500
                  example: "dreamy warm glow with soft vignette"
      responses:
        '200':
          description: Effect generated successfully
          content:
            application/json:
              schema:
                type: object
                properties:
                  filter:
                    type: string
                    description: FFmpeg filter expression
                    example: "eq=brightness=0.08:saturation=0.85,gblur=sigma=2,vignette=PI*0.15"
                  description:
                    type: string
                    description: Human-readable effect description
                  confidence:
                    type: number
                    description: AI confidence score (0-1)
                    example: 0.9
                  source:
                    type: string
                    enum: [library, cached, generated]
                    description: Where the effect came from
                  credits_used:
                    type: integer
                    example: 1
                  credits_remaining:
                    type: integer
        '402':
          description: Insufficient credits
        '403':
          description: Forbidden — API key lacks the required production:create scope

  /voices:
    get:
      operationId: listVoices
      summary: List available TTS voices
      description: |
        Returns all available text-to-speech voices across providers.
        No authentication required. Response is cached for 1 hour.

        **Standard voices** (Kokoro): 3 credits/1K characters, 46 voices
        **Premium voices** (ElevenLabs): 13 credits/1K characters, 52 voices

        **Rate limiting:** Same per-IP anonymous bucket and header
        contract as `/tags` (see that endpoint for the full caching
        rules). Only the static `RateLimit-Policy` header is emitted on
        cacheable 200 responses.
      tags: [Voices]
      security: []
      parameters:
        - name: tier
          in: query
          required: false
          schema:
            type: string
            enum: [standard, premium]
          description: Filter by pricing tier
      responses:
        '200':
          description: List of available voices
          content:
            application/json:
              schema:
                type: object
                properties:
                  voices:
                    type: array
                    items:
                      type: object
                      properties:
                        id:
                          type: string
                          description: Voice identifier to use in TTS segments
                          example: af_bella
                        name:
                          type: string
                          example: Bella
                        tier:
                          type: string
                          enum: [standard, premium]
                        provider:
                          type: string
                          enum: [kokoro, elevenlabs]
                        gender:
                          type: string
                          enum: [female, male, neutral]
                        language:
                          type: string
                          example: en
                        creditsPer1kChars:
                          type: integer
                          description: Credit cost per 1,000 characters
                          example: 3
        '400':
          description: Invalid tier parameter
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'

  /tags:
    get:
      operationId: listTags
      summary: List available effect tags
      description: |
        Returns all available visual effect tags for the Tag Mix & Match system,
        grouped by category (mood, color, texture, style, enhancement).

        **Cost:** FREE (0 credits). No authentication required.

        Use these tags in the `tags` field of `POST /canvas-wizard` to apply
        visual effects with fine-grained intensity control (0-100).

        **Rate limiting:** This endpoint shares the per-IP anonymous bucket
        with other public catalog routes. Successful 200 responses are
        CDN-cacheable, so per-client headers from BOTH the legacy
        `X-RateLimit-*` family (`X-RateLimit-Remaining`, `X-RateLimit-Reset`)
        AND the RFC 9331 `RateLimit-*` family (`RateLimit-Remaining`,
        `RateLimit-Reset`) are intentionally omitted (they would leak one
        client's quota via the shared cache). Only the static
        `RateLimit-Policy` response header (RFC 9331 §2.4) is emitted on
        200 to advertise the configured per-window quota.
      tags: [Tags]
      security: []
      responses:
        '200':
          description: All available tags grouped by category
          content:
            application/json:
              schema:
                type: object
                properties:
                  groups:
                    type: array
                    items:
                      type: object
                      properties:
                        group:
                          type: string
                          enum: [mood, color, texture, style, enhancement, motion, glitch, artistic]
                        tags:
                          type: array
                          items:
                            type: object
                            properties:
                              id:
                                type: string
                                description: |
                                  Tag identifier (legacy alias). Same value as `tag`; kept
                                  for back-compat with integrators consuming the previous
                                  field name. Prefer `tag` in new code.
                                example: cinematic
                              tag:
                                type: string
                                description: |
                                  Canonical tag identifier — pass this value in the
                                  `tags[].tag` field of `POST /canvas-wizard`. Identical
                                  to `id`.
                                example: cinematic
                              label:
                                type: string
                                example: Cinematic
                              description:
                                type: string
                                example: Film-like look with warm tones, vignette, and subtle grain
                              group:
                                type: string
                                enum: [mood, color, texture, style, enhancement, motion, glitch, artistic]
                              icon:
                                type: string
                                description: Lucide icon name for UI display
                              conflicts:
                                type: array
                                items:
                                  type: string
                                description: Mutually exclusive tags (higher intensity wins)
                  total:
                    type: integer
                    description: Total number of available tags
                    example: 40

  /productions/{id}/visibility:
    patch:
      operationId: updateProductionVisibility
      summary: Update production visibility
      description: |
        Change the visibility of a completed production.
        - `private`: Only accessible by the owner (default)
        - `unlisted`: Accessible via share link
        - `public`: Accessible by anyone
      tags: [Productions]
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [visibility]
              properties:
                visibility:
                  type: string
                  enum: [private, unlisted, public]
      responses:
        '200':
          description: Visibility updated
          content:
            application/json:
              schema:
                type: object
                properties:
                  id:
                    type: string
                    format: uuid
                  visibility:
                    type: string
                    enum: [private, unlisted, public]
                  share_token_cleared:
                    type: boolean
                    description: Whether the share token was cleared (true when changing to private)
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
      security:
        - BearerAuth: []

  /productions/{id}/share:
    post:
      operationId: createShareLink
      summary: Generate a share link
      description: |
        Generate a share token for a production. Sets visibility to `unlisted` if currently `private`.
        Optionally set an expiration date for the share link.
      tags: [Productions]
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                expires_in_hours:
                  type: integer
                  minimum: 1
                  maximum: 8760
                  description: Optional number of hours until the share link expires (1-8760)
      responses:
        '200':
          description: Share link created
          content:
            application/json:
              schema:
                type: object
                properties:
                  share_url:
                    type: string
                    format: uri
                  share_token:
                    type: string
                  expires_at:
                    type: string
                    format: date-time
                    nullable: true
                  visibility:
                    type: string
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
      security:
        - BearerAuth: []
    delete:
      operationId: revokeShareLink
      summary: Revoke a share link
      description: Removes the share token and sets production visibility to `private`.
      tags: [Productions]
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        '200':
          description: Share link revoked
          content:
            application/json:
              schema:
                type: object
                properties:
                  id:
                    type: string
                    format: uuid
                  visibility:
                    type: string
                    example: private
                  share_token:
                    type: string
                    nullable: true
                    example: null
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
      security:
        - BearerAuth: []

  /stock-media:
    get:
      operationId: searchStockMedia
      summary: Search stock photos and videos (Pexels)
      description: |
        Search Pexels stock library for royalty-free photos and videos.
        Useful for sourcing canvas layers without uploading custom media.
      tags: [Media]
      parameters:
        - name: query
          in: query
          required: true
          description: Search query (e.g., "ocean sunset")
          schema:
            type: string
        - name: type
          in: query
          description: Filter results by media type
          schema:
            type: string
            enum: [photo, video, all]
            default: all
        - name: per_page
          in: query
          description: Results per page (1-80)
          schema:
            type: integer
            minimum: 1
            maximum: 80
            default: 20
        - name: page
          in: query
          description: Page number
          schema:
            type: integer
            minimum: 1
            default: 1
        - name: orientation
          in: query
          description: Orientation filter
          schema:
            type: string
            enum: [landscape, portrait, square]
        - name: size
          in: query
          description: Minimum size
          schema:
            type: string
            enum: [large, medium, small]
      responses:
        '200':
          description: Stock media search results
          content:
            application/json:
              schema:
                type: object
                properties:
                  results:
                    type: array
                    items:
                      type: object
                  total_results:
                    type: integer
                  page:
                    type: integer
                  per_page:
                    type: integer
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '503':
          description: Stock media search not configured (Pexels API key missing)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
      security:
        - BearerAuth: []

  /social/connections:
    get:
      operationId: listSocialConnections
      summary: List connected social accounts
      description: |
        Returns every OAuth-connected social platform account for the authenticated user
        (YouTube, TikTok, Instagram). Tokens are stored AES-256-GCM encrypted and are
        never returned in this response.
      tags: [Social]
      responses:
        '200':
          description: List of social connections
          content:
            application/json:
              schema:
                type: object
                properties:
                  connections:
                    type: array
                    items:
                      type: object
        '401':
          $ref: '#/components/responses/Unauthorized'
      security:
        - BearerAuth: []

  /productions/{id}/deployments:
    get:
      operationId: listProductionDeployments
      summary: List social-media deployments for a production
      description: |
        Returns every social-media publish attempt (queued, processing, succeeded, failed)
        for the production, ordered most-recent first.
      tags: [Productions]
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        '200':
          description: Deployments list
          content:
            application/json:
              schema:
                type: object
                properties:
                  deployments:
                    type: array
                    items:
                      type: object
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          description: API key lacks `production:read` scope
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '404':
          $ref: '#/components/responses/NotFound'
      security:
        - BearerAuth: []

  /shared/{token}:
    get:
      operationId: getSharedProduction
      summary: Get a shared production by token
      description: |
        Retrieve a publicly shared production using its share token.
        No authentication required. Returns production details with presigned media URLs.
      tags: [Sharing]
      security: []
      parameters:
        - name: token
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          description: Shared production details
          content:
            application/json:
              schema:
                type: object
                properties:
                  id:
                    type: string
                    format: uuid
                  name:
                    type: string
                    nullable: true
                  output_url:
                    type: string
                    format: uri
                    nullable: true
                    description: |
                      Presigned R2 URL to download the shared video.
                      **HEAD is not authorized** — the signature is GET-only.
                      S3 returns 403; R2 may currently respond inconsistently
                      (some accounts have observed 200 OK), so treat HEAD as
                      best-effort. Prefer the API `output_size` field for the
                      total size; use GET with `Range: bytes=0-0` to probe
                      headers cheaply (Content-Range carries total size when
                      the backend includes it).
                  hls_url:
                    type: string
                    format: uri
                    nullable: true
                    description: |
                      Presigned URL to the HLS (.m3u8) manifest.
                      **HEAD is not authorized** (GET-only signature). Use a
                      Range GET (`Range: bytes=0-0`) to probe headers; rely
                      on API fields such as `output_size` for canonical size.
                  thumbnail_url:
                    type: string
                    format: uri
                    nullable: true
                    description: |
                      Presigned URL to the thumbnail image.
                      **HEAD is not authorized** (GET-only signature). Use a
                      Range GET (`Range: bytes=0-0`) to probe headers; rely
                      on API fields such as `output_size` for canonical size.
                  url_expires_at:
                    type: string
                    format: date-time
                    nullable: true
                  output_duration:
                    type: number
                    nullable: true
                  output_format:
                    type: string
                    nullable: true
                  output_size:
                    type: integer
                    nullable: true
                    description: Output file size in bytes
                  visibility:
                    type: string
                  created_at:
                    type: string
                    format: date-time
                  completed_at:
                    type: string
                    format: date-time
                    nullable: true
                  creator_name:
                    type: string
                    nullable: true
                    description: Creator display name (only shown if creator has attribution enabled)
        '404':
          description: Share link not found, expired, or revoked

  /preview:
    post:
      operationId: submitPreview
      summary: Submit a low-resolution preview render
      description: |
        Generate a free 480p/15fps preview of a canvas specification. Uses the same
        canvas format as `POST /productions` but renders at low resolution with
        simplified effects for near-instant feedback (~2-4 seconds).

        **Free:** Previews cost 0 credits. Rate-limited by plan tier.

        **Rate limits** (per day): Free: 5 | Starter: 20 | Creator: 50 | Pro: 100 | Scale: 200

        **Webhook support:** Optionally provide a `webhookUrl` to receive a POST
        notification when the preview completes, instead of polling.

        Returns `202 Accepted` with a `preview_id` and `poll_url`. Poll the status
        endpoint until `status` is `completed` or `failed`. Preview results expire
        after 1 hour.
      tags: [Preview]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [canvas]
              properties:
                canvas:
                  type: object
                  description: Same canvas specification as POST /productions
                webhookUrl:
                  type: string
                  format: uri
                  description: Optional HTTPS URL to receive preview completion webhook
            example:
              canvas:
                preset: youtube_short
                tracks:
                  - segments:
                      - type: video
                        url: "https://example.com/clip.mp4"
                        duration: 10
      responses:
        '202':
          description: Preview submitted for processing
          headers:
            X-Preview-Daily-Limit:
              schema: { type: integer }
              description: Maximum previews per day for this plan
            X-Preview-Daily-Remaining:
              schema: { type: integer }
              description: Previews remaining today
            X-Preview-Daily-Reset:
              schema: { type: string, format: date-time }
              description: When the daily limit resets (UTC)
            X-Preview-Minute-Limit:
              schema: { type: integer }
              description: Maximum previews per minute for this plan
            X-Preview-Minute-Remaining:
              schema: { type: integer }
              description: Previews remaining this minute
          content:
            application/json:
              schema:
                type: object
                properties:
                  preview_id:
                    type: string
                    description: "Unique preview ID (format: pv_xxxxxxxxxxxxxxxx)"
                  status:
                    type: string
                    enum: [processing]
                  created_at:
                    type: string
                    format: date-time
                  poll_url:
                    type: string
                    format: uri
                    description: Absolute URL to poll for status
                  expires_at:
                    type: string
                    format: date-time
                    description: When this preview result will expire from KV (1 hour)
        '400':
          description: Invalid canvas specification
        '429':
          description: Rate limit exceeded (daily or per-minute)
          headers:
            Retry-After:
              schema: { type: integer }
              description: Seconds until the limit resets
        '503':
          description: Global capacity limit reached. Retry after a few seconds.

  /preview/{previewId}:
    get:
      operationId: getPreviewStatus
      summary: Poll preview render status
      description: |
        Check the status of a preview render. Poll until `status` is `completed`
        (includes `video_url` and `thumbnail_url`) or `failed` (includes `error`).

        Preview results expire after 1 hour. After expiry, returns `404`.
      tags: [Preview]
      parameters:
        - name: previewId
          in: path
          required: true
          schema:
            type: string
          description: "Preview ID from the submit response (format: pv_xxxxxxxxxxxxxxxx)"
      responses:
        '200':
          description: Preview status
          content:
            application/json:
              schema:
                type: object
                properties:
                  preview_id:
                    type: string
                  status:
                    type: string
                    enum: [processing, completed, failed]
                  created_at:
                    type: string
                    format: date-time
                  video_url:
                    type: string
                    format: uri
                    nullable: true
                    description: |
                      Presigned URL to the 480p preview video (1hr expiry).
                      **HEAD requests return 403** (GET-only signature).
                      Use a Range GET (`Range: bytes=0-0`) to probe.
                  thumbnail_url:
                    type: string
                    format: uri
                    nullable: true
                    description: |
                      Presigned URL to the preview thumbnail (1hr expiry).
                      **HEAD requests return 403** (GET-only signature).
                      Use a Range GET (`Range: bytes=0-0`) to probe.
                  url_expires_at:
                    type: string
                    format: date-time
                    nullable: true
                    description: When the presigned URLs expire
                  resolution:
                    type: string
                    nullable: true
                    description: "Preview resolution (e.g. 854x480)"
                  duration:
                    type: number
                    nullable: true
                    description: Preview duration in seconds
                  processing_time_ms:
                    type: integer
                    nullable: true
                    description: Time to render in milliseconds
                  error:
                    type: string
                    nullable: true
                    description: Error message (only when status=failed)
        '404':
          description: Preview not found or expired (results expire after 1 hour)

  /preview/usage:
    get:
      operationId: getPreviewUsage
      summary: Check preview rate limit usage
      description: |
        Get the current daily preview usage and limits for your API key's plan.
        Use this to check remaining quota before submitting previews.
      tags: [Preview]
      responses:
        '200':
          description: Current preview usage and limits
          content:
            application/json:
              schema:
                type: object
                properties:
                  daily_count:
                    type: integer
                    description: Previews used today
                  daily_limit:
                    type: integer
                    description: Maximum previews per day for this plan
                  daily_remaining:
                    type: integer
                    description: Previews remaining today
                  daily_reset_at:
                    type: string
                    format: date-time
                    description: When the daily limit resets (UTC midnight)
                  minute_limit:
                    type: integer
                    description: Maximum previews per minute
                  plan:
                    type: string
                    description: Current plan tier

components:
  securitySchemes:
    BearerAuth:
      type: http
      scheme: bearer
      description: |
        API key from your dashboard. Format: `sk_live_xxxxx` (24 random characters).

        Example: `Authorization: Bearer sk_live_Ax7Kp2mN9qR4sT6vW8xY0zA`

  schemas:
    # ==========================================
    # SHARED ENUMS
    # ==========================================

    ProductionStatus:
      type: string
      enum: [pending, queued, processing, completed, failed, cancelled]
      description: |
        Lifecycle status of a production. Newly created rows are `queued`
        as soon as the API returns; `pending` is a transient default
        value that exists only as a Postgres column default and may
        appear briefly on rows minted by older code paths. Terminal
        statuses are `completed`, `failed`, and `cancelled`. Single
        source of truth: `web/src/lib/constants.ts` →
        `PRODUCTION_STATUS`.

    # ==========================================
    # REQUEST SCHEMAS
    # ==========================================

    CreateProductionRequest:
      type: object
      description: Request body for creating a new production
      properties:
        idempotencyKey:
          type: string
          maxLength: 255
          description: |
            Unique key to prevent duplicate productions (recommended for reliability).
            You may also supply the `Idempotency-Key` HTTP header instead — the body
            field wins if both are provided.
        canvas:
          $ref: '#/components/schemas/Canvas'
        templateId:
          type: string
          format: uuid
          description: Blueprint ID to use as template
        bindings:
          type: object
          additionalProperties: true
          description: Variable substitutions for templates. Keys match `{{variable}}` placeholders.
          example:
            headline: "Summer Sale!"
            video_url: "https://example.com/video.mp4"
        outputSettings:
          $ref: '#/components/schemas/OutputSettings'
        webhookUrl:
          type: string
          format: uri
          description: |
            HTTPS URL to receive a POST when the production completes or fails.

            **Signing:** When webhook signing is enabled for the current StrataKit
            deployment, deliveries carry an `X-StrataKit-Signature` header in the
            form `t=<unix_seconds>,v1=<sha256_hex>` (Stripe-compatible scheme).
            To verify, compute `HMAC-SHA256(secret, "{t}.{raw_body}")` and
            timing-safe compare the hex digest to `v1`. Reject any delivery where
            `|now - t| > 300` to prevent replay. The signing secret is a
            deployment-level secret configured for the current StrataKit
            deployment, not a per-user or per-endpoint secret. Integrators who
            need to verify signatures must obtain that deployment's webhook
            signing secret from StrataKit for their environment. When signing is
            not enabled the header is omitted; integrators who require a signed
            payload should treat an unsigned delivery as invalid.

            Additional headers sent with every delivery:
            - `X-StrataKit-Delivery` — 1-based retry attempt number.
            - `X-StrataKit-Production-Id` — UUID of the production being reported.
            - `User-Agent: StrataKit-Webhook/1.0`.

            Retries follow backoff `1s → 5s → 30s → 2m → 10m` on non-2xx
            responses; after 5 attempts the delivery is dropped.
        priority:
          type: string
          enum: [low, normal, high]
          default: normal
          description: Processing priority (higher plans have access to high priority)
        testMode:
          type: boolean
          default: false
          description: If true, produces watermarked output without consuming credits

    BatchProductionRequest:
      type: object
      required: [templateId, items]
      description: Request body for creating multiple productions from a template
      properties:
        templateId:
          type: string
          format: uuid
          description: Blueprint ID to use as template for all items
        items:
          type: array
          minItems: 1
          maxItems: 50
          description: List of items to create productions for (max 50)
          items:
            type: object
            required: [bindings]
            properties:
              name:
                type: string
                maxLength: 200
                description: Optional name for this production
              bindings:
                type: object
                additionalProperties: true
                description: Variable substitutions for this item
        webhookUrl:
          type: string
          format: uri
          description: |
            HTTPS URL to receive a POST when each production in the batch
            completes or fails. Same signing/retry semantics as the
            `/productions` endpoint — see the `webhookUrl` description in
            `CreateProductionRequest` for header format (`X-StrataKit-Signature`,
            `X-StrataKit-Delivery`, `X-StrataKit-Production-Id`) and verification steps.
        testMode:
          type: boolean
          default: false
          description: If true, produces watermarked output without consuming credits
        idempotencyPrefix:
          type: string
          maxLength: 100
          description: Prefix for generating unique external IDs (e.g., "campaign-2024-01" creates "campaign-2024-01-0", "campaign-2024-01-1", etc.)

    BatchProductionResponse:
      type: object
      description: Response from batch production creation
      properties:
        message:
          type: string
          description: Summary message of the batch operation
        batchId:
          type: string
          description: Unique identifier for this batch
        productions:
          type: array
          items:
            type: object
            properties:
              id:
                type: string
                format: uuid
              name:
                type: string
                nullable: true
              status:
                type: string
                enum: [queued]
                description: |
                  Always `queued` for batch-created productions — each row is
                  enqueued before being returned. Poll `GET /productions/{id}`
                  to observe transitions to `processing` and beyond. See the
                  shared `ProductionStatus` schema for the full lifecycle.
              creditsEstimate:
                type: number
                description: Estimated credits this production will use
              index:
                type: integer
                description: Index in the original items array
        errors:
          type: array
          description: List of items that failed to create (only present if there are failures)
          items:
            type: object
            properties:
              index:
                type: integer
              name:
                type: string
                nullable: true
              error:
                type: string
        summary:
          type: object
          properties:
            totalRequested:
              type: integer
            created:
              type: integer
            failed:
              type: integer
            totalCredits:
              type: integer
            testMode:
              type: boolean

    ConfirmUploadRequest:
      type: object
      required: [key, size]
      description: Request to confirm a media upload completion
      properties:
        key:
          type: string
          description: The storage key returned from upload-url endpoint
          example: "uploads/usr_xxx/abc123/my-video.mp4"
        size:
          type: integer
          minimum: 0
          description: Actual size of the uploaded file in bytes
          example: 10485760

    ConfirmUploadResponse:
      type: object
      description: Response after confirming an upload
      properties:
        success:
          type: boolean
          description: Whether the confirmation was successful
        key:
          type: string
          description: The storage key that was confirmed
        storage:
          type: object
          nullable: true
          description: Updated storage usage information
          properties:
            used:
              type: integer
              description: Total storage used in bytes
            quota:
              type: integer
              description: Storage quota in bytes

    CreateBlueprintRequest:
      type: object
      required: [name, canvas]
      properties:
        name:
          type: string
          maxLength: 100
          description: Template name
        description:
          type: string
          maxLength: 500
          description: Template description
        category:
          type: string
          maxLength: 50
          default: custom
          description: Template category
        canvas:
          $ref: '#/components/schemas/Canvas'
        defaultBindings:
          type: object
          additionalProperties: true
          description: Default variable bindings available to productions created from this blueprint
        isPublic:
          type: boolean
          default: false
          description: Whether template is visible to other users

    UpdateBlueprintRequest:
      type: object
      properties:
        name:
          type: string
          maxLength: 255
        description:
          type: string
          maxLength: 1000
        canvas:
          $ref: '#/components/schemas/Canvas'
        isPublic:
          type: boolean

    UploadUrlRequest:
      type: object
      required: [filename, contentType]
      description: Request for a presigned upload URL
      properties:
        filename:
          type: string
          maxLength: 255
          description: Original filename (will be sanitized)
          example: "my-video.mp4"
        contentType:
          type: string
          description: MIME type of the file
          enum:
            - video/mp4
            - video/webm
            - video/quicktime
            - image/jpeg
            - image/png
            - image/webp
            - image/gif
            - audio/mpeg
            - audio/wav
            - audio/aac
            - audio/ogg
          example: "video/mp4"
        fileSize:
          type: integer
          minimum: 1
          description: File size in bytes (for validation)
          example: 10485760

    UploadUrlResponse:
      type: object
      description: Presigned URL for direct upload
      properties:
        uploadUrl:
          type: string
          format: uri
          description: Presigned PUT URL (expires in 15 minutes)
        publicUrl:
          type: string
          format: uri
          description: Public URL where file will be accessible after upload
        key:
          type: string
          description: Storage key for the uploaded file
        expiresIn:
          type: integer
          description: Seconds until uploadUrl expires
          example: 900
        category:
          type: string
          enum: [video, image, audio]
          description: File category based on content type

    # ==========================================
    # CANVAS & SEGMENTS
    # ==========================================

    Canvas:
      type: object
      description: |
        Video specification with dimensions, segments, and settings.

        Use **`segments`** to define your content. Base content (video, image, color, luma)
        auto-sequences into a single video track. Overlays (text, shape, html, waveform)
        render on top of base content. TTS and audio each get their own track automatically.
        Sequential start times are auto-calculated for base content when omitted.
        Overlay start times default to 0.
      properties:
        preset:
          $ref: '#/components/schemas/ResolutionPreset'
        width:
          type: integer
          minimum: 100
          maximum: 7680
          description: Custom width in pixels (overrides preset)
        height:
          type: integer
          minimum: 100
          maximum: 4320
          description: Custom height in pixels (overrides preset)
        duration:
          type: number
          minimum: 0.1
          maximum: 7200
          description: Total duration in seconds (auto-calculated from segments if not specified)
        fps:
          type: integer
          minimum: 1
          maximum: 120
          default: 30
          description: Frames per second
        backgroundColor:
          oneOf:
            - type: string
              description: Hex color (e.g., "#000000")
            - $ref: '#/components/schemas/Gradient'
          description: |
            Background color or gradient. Also accepted as `background` (alias —
            normalized to `backgroundColor` server-side before validation).
        segments:
          type: array
          items:
            $ref: '#/components/schemas/FlatSegment'
          minItems: 1
          description: |
            Flat segment list. Base content (video, image, color) auto-sequences.
            Overlays (text, shape, html, waveform) render on top. TTS and audio each
            get their own track automatically.
        masterVolume:
          type: number
          minimum: 0
          maximum: 2
          default: 1
          description: Global volume multiplier
        metadata:
          type: object
          additionalProperties: true
          description: Custom metadata for your application

    FlatSegment:
      type: object
      required: [type]
      description: |
        A flat segment that merges asset fields and segment fields at the top level.
        The `type` field determines the asset type. All other fields from the corresponding
        asset schema and the Segment schema can be used directly.

        Visual types (video, image, color, text, shape, luma, html) auto-sequence into a
        single video track. `start` is auto-calculated when omitted.

        TTS and audio segments each get their own track automatically.
      properties:
        type:
          type: string
          enum: [video, image, color, text, audio, tts, shape, luma, html, caption, waveform]
          description: Asset type
        url:
          type: string
          format: uri
          description: |
            Canonical segment-source key. Media URL for video, image, and audio
            assets. Aliases `src`, `href`, `videoUrl`, and `assetUrl` are accepted
            on input and rewritten to `url` for backward compatibility, but `url`
            is the only canonical name and is the only one returned by the API
            on read (e.g. `GET /productions/{id}` and template responses).
        text:
          type: string
          description: Text content (for text, tts assets)
        color:
          type: string
          description: Color value (for color assets)
        duration:
          type: number
          description: Segment duration in seconds
        start:
          type: number
          description: Start time in seconds (auto-calculated for visual segments when omitted)
        voice:
          type: string
          description: >-
            TTS voice name or ID. Determines provider routing automatically.
            Standard (Kokoro) voice IDs: af_bella, am_adam, bf_emma, etc. (3 cr/1K chars).
            Premium (ElevenLabs) voice names: Sarah, Liam, Charlotte, etc. (13 cr/1K chars).
        provider:
          type: string
          enum: [elevenlabs, kokoro]
          description: >-
            TTS provider. Usually omitted — voice name determines routing automatically.
            Kokoro voice IDs route to Standard tier. ElevenLabs names route to Premium tier.
        volume:
          type: number
          minimum: 0
          maximum: 2
          description: Audio volume (for audio segments)
        loop:
          oneOf:
            - type: boolean
            - type: integer
              minimum: 2
              maximum: 100
          description: "Loop playback. true = loop to fill duration, integer (2-100) = play up to N times, capped by duration (video segments only)"
        fit:
          type: string
          enum: [cover, contain, fill, none]
          description: How media fills the canvas
        trim:
          type: object
          properties:
            start:
              type: number
            end:
              type: number
          description: Trim video to time range
        speed:
          type: number
          description: Playback speed multiplier
        mute:
          type: boolean
          description: Mute video audio
        transition:
          $ref: '#/components/schemas/Transition'
        effectChain:
          type: object
          description: Effect chain for this segment
        position:
          $ref: '#/components/schemas/Position'
        transform:
          $ref: '#/components/schemas/Transform'
      additionalProperties: true

    Track:
      type: object
      required: [segments]
      description: A layer containing one or more segments (advanced format)
      properties:
        id:
          type: string
          description: Track identifier for referencing (e.g., in captions)
        name:
          type: string
          description: Human-readable track name
        type:
          type: string
          enum: [video, audio, text, overlay]
          description: Track type hint
        segments:
          type: array
          items:
            $ref: '#/components/schemas/Segment'
          minItems: 1
        volume:
          type: number
          minimum: 0
          maximum: 2
          description: Track volume multiplier
        mute:
          type: boolean
          description: Mute all audio from this track
        effectChain:
          type: object
          description: Visual effects applied to entire track (recommended)
          properties:
            combination:
              type: string
              description: Preset combination (cinematic, viral, noir, etc.)
            effects:
              type: array
              items:
                $ref: '#/components/schemas/Effect'
        filters:
          type: array
          items:
            $ref: '#/components/schemas/Filter'
          description: "Deprecated: use effectChain instead"
          deprecated: true
        blendMode:
          $ref: '#/components/schemas/BlendMode'
        opacity:
          type: number
          minimum: 0
          maximum: 1
          description: Track opacity

    Segment:
      type: object
      required: [asset, start]
      description: A clip within a track (video, image, text, audio, etc.)
      properties:
        id:
          type: string
          description: Segment identifier
        asset:
          $ref: '#/components/schemas/Asset'
        start:
          type: number
          minimum: 0
          description: Start time in seconds on the timeline
        duration:
          type: number
          minimum: 0
          description: Duration in seconds (required for images, colors, text)
        position:
          $ref: '#/components/schemas/Position'
        transform:
          $ref: '#/components/schemas/Transform'
        crop:
          $ref: '#/components/schemas/Crop'
        transition:
          $ref: '#/components/schemas/Transition'
        transitionIn:
          $ref: '#/components/schemas/Transition'
        transitionOut:
          $ref: '#/components/schemas/Transition'
        animation:
          oneOf:
            - $ref: '#/components/schemas/PresetAnimation'
            - type: array
              items:
                $ref: '#/components/schemas/Animation'
          description: Preset animation name or custom keyframe animations
        animationIn:
          $ref: '#/components/schemas/PresetAnimation'
        animationOut:
          $ref: '#/components/schemas/PresetAnimation'
        filters:
          type: array
          items:
            $ref: '#/components/schemas/Filter'
          description: "Deprecated: use effectChain instead"
          deprecated: true
        blendMode:
          $ref: '#/components/schemas/BlendMode'
        zIndex:
          type: integer
          description: Stack order within track

    # ==========================================
    # ASSETS (11 types)
    # ==========================================

    Asset:
      oneOf:
        - $ref: '#/components/schemas/VideoAsset'
        - $ref: '#/components/schemas/ImageAsset'
        - $ref: '#/components/schemas/ColorAsset'
        - $ref: '#/components/schemas/TextAsset'
        - $ref: '#/components/schemas/AudioAsset'
        - $ref: '#/components/schemas/TTSAsset'
        - $ref: '#/components/schemas/ShapeAsset'
        - $ref: '#/components/schemas/LumaAsset'
        - $ref: '#/components/schemas/HTMLAsset'
        - $ref: '#/components/schemas/CaptionAsset'
        - $ref: '#/components/schemas/WaveformAsset'
      discriminator:
        propertyName: type
        mapping:
          video: '#/components/schemas/VideoAsset'
          image: '#/components/schemas/ImageAsset'
          color: '#/components/schemas/ColorAsset'
          text: '#/components/schemas/TextAsset'
          audio: '#/components/schemas/AudioAsset'
          tts: '#/components/schemas/TTSAsset'
          shape: '#/components/schemas/ShapeAsset'
          luma: '#/components/schemas/LumaAsset'
          html: '#/components/schemas/HTMLAsset'
          caption: '#/components/schemas/CaptionAsset'
          waveform: '#/components/schemas/WaveformAsset'

    VideoAsset:
      type: object
      required: [type]
      description: |
        Video clip asset. Provide EITHER `url` (direct URL) OR `binding` (template variable), not both.
      properties:
        type:
          type: string
          enum: [video]
        url:
          type: string
          format: uri
          description: |
            Canonical segment-source key. Direct URL to video file (must be
            publicly accessible). Required if `binding` is not provided.
            Aliases `src`, `href`, `videoUrl`, and `assetUrl` are accepted
            on input and rewritten to `url`.
        binding:
          type: string
          description: Variable name for template substitution. Required if `url` is not provided.
        trim:
          type: object
          properties:
            start:
              type: number
              minimum: 0
              description: Trim start in seconds
            end:
              type: number
              minimum: 0
              description: Trim end in seconds
        speed:
          type: number
          minimum: 0.1
          maximum: 10
          default: 1
          description: Playback speed multiplier
        loop:
          oneOf:
            - type: boolean
            - type: integer
              minimum: 2
              maximum: 100
          description: "Loop video. true = loop to fill duration, integer = play up to N times (capped by duration)"
        mute:
          type: boolean
          description: Remove audio from video
        reverse:
          type: boolean
          description: Play video in reverse
        chromaKey:
          type: object
          description: Remove background color (green screen)
          properties:
            color:
              type: string
              description: Color to remove (e.g., "#00FF00")
            similarity:
              type: number
              description: Color matching threshold
            smoothness:
              type: number
              description: Edge smoothness

    ImageAsset:
      type: object
      required: [type]
      description: |
        Image asset. Provide EITHER `url` (direct URL) OR `binding` (template variable), not both.
      properties:
        type:
          type: string
          enum: [image]
        url:
          type: string
          format: uri
          description: Direct URL to image file. Required if `binding` is not provided.
        binding:
          type: string
          description: Variable name for template substitution. Required if `url` is not provided.
        fit:
          type: string
          enum: [cover, contain, fill, none]
          default: cover
          description: How to fit image to frame
        kenBurns:
          type: object
          description: Ken Burns pan/zoom effect
          properties:
            startScale:
              type: number
              description: Starting zoom level
            endScale:
              type: number
              description: Ending zoom level
            startPosition:
              $ref: '#/components/schemas/Position'
            endPosition:
              $ref: '#/components/schemas/Position'

    ColorAsset:
      type: object
      required: [type, color]
      properties:
        type:
          type: string
          enum: [color]
        color:
          oneOf:
            - type: string
              description: Hex color (e.g., "#4F46E5")
            - $ref: '#/components/schemas/Gradient'

    TextAsset:
      type: object
      required: [type]
      description: |
        Text overlay asset. Provide EITHER `text` (literal content) OR `binding` (template variable), not both.
      properties:
        type:
          type: string
          enum: [text]
        text:
          type: string
          description: Text content to display. Required if `binding` is not provided.
        binding:
          type: string
          description: Variable name for template substitution. Required if `text` is not provided.
        style:
          $ref: '#/components/schemas/TextStyle'
        richText:
          type: boolean
          description: Enable HTML/rich text formatting
        autoSize:
          type: string
          enum: [none, shrink, resize]
          description: Auto-size text to fit bounds
        maxWidth:
          type: number
          description: Maximum text width in pixels
        maxHeight:
          type: number
          description: Maximum text height in pixels

    AudioAsset:
      type: object
      required: [type]
      description: |
        Audio file asset. Provide EITHER `url` (direct URL) OR `binding` (template variable), not both.
      properties:
        type:
          type: string
          enum: [audio]
        url:
          type: string
          format: uri
          description: Direct URL to audio file (MP3, WAV, AAC). Required if `binding` is not provided.
        binding:
          type: string
          description: Variable name for template substitution. Required if `url` is not provided.
        settings:
          $ref: '#/components/schemas/AudioSettings'

    TTSAsset:
      type: object
      required: [type]
      description: |
        Text-to-speech audio generation with two provider tiers. Provide EITHER `text` (literal content) OR `binding` (template variable), not both.

        **Standard (Kokoro):** 46 voices, 9 languages. Voice IDs like `af_bella`, `am_adam`. 3 cr/1K chars. Speed 0.5-2.0. Word-level timestamps for English only.

        **Premium (ElevenLabs):** 52 voices, multilingual. Names like `Sarah`, `Liam`. 13 cr/1K chars. Speed 0.7-1.2. Full voice control. Word-level timestamps for all languages.

        Voice name determines routing automatically.
      properties:
        type:
          type: string
          enum: [tts]
        text:
          type: string
          description: Text to convert to speech. Required if `binding` is not provided.
        binding:
          type: string
          description: Variable name for template substitution. Required if `text` is not provided.
        settings:
          $ref: '#/components/schemas/TTSSettings'

    ShapeAsset:
      type: object
      required: [type, shape]
      description: Vector shape (rectangle, circle, polygon, etc.)
      properties:
        type:
          type: string
          enum: [shape]
        shape:
          $ref: '#/components/schemas/Shape'

    LumaAsset:
      type: object
      required: [type, url]
      description: Luma matte for compositing
      properties:
        type:
          type: string
          enum: [luma]
        url:
          type: string
          format: uri
          description: URL to grayscale luma matte video/image
        inverted:
          type: boolean
          description: Invert the matte

    HTMLAsset:
      type: object
      required: [type, html, width, height]
      description: Render HTML/CSS content
      properties:
        type:
          type: string
          enum: [html]
        html:
          type: string
          description: HTML content to render
        css:
          type: string
          description: Optional CSS styles
        width:
          type: integer
          description: Render width in pixels
        height:
          type: integer
          description: Render height in pixels

    CaptionAsset:
      type: object
      required: [type]
      description: |
        Animated captions/subtitles. The `source` field determines how captions are obtained:
        - `tts`: Auto-generate from a TTS track (uses word timestamps from ElevenLabs Premium or Kokoro Standard English voices)
        - `transcribe`: Auto-transcribe audio from a track
        - `inline`: Provide captions array directly
        - `url`: Load from external SRT/VTT file
      properties:
        type:
          type: string
          enum: [caption]
        source:
          type: string
          enum: [tts, url, transcribe, inline]
          default: tts
          description: Caption source. Defaults to `tts` if trackId is provided.
        url:
          type: string
          format: uri
          description: URL to SRT/VTT caption file
        trackId:
          type: string
          description: Track ID to transcribe audio from
        captions:
          type: array
          items:
            type: object
            properties:
              start:
                type: number
              end:
                type: number
              text:
                type: string
              words:
                type: array
                items:
                  type: object
                  properties:
                    word:
                      type: string
                    start:
                      type: number
                    end:
                      type: number
        settings:
          $ref: '#/components/schemas/CaptionSettings'

    WaveformAsset:
      type: object
      required: [type, trackId]
      description: Audio waveform visualization that syncs with audio playback.
      properties:
        type:
          type: string
          enum: [waveform]
        trackId:
          type: string
          description: Track ID to visualize audio from (must match an audio or TTS track's id)
        style:
          type: string
          enum: [bars, line, wave, points, mirror, circular]
          default: bars
          description: Visualization style
        preset:
          type: string
          enum: [compact, medium, prominent, full_width]
          default: prominent
          description: Size preset for 9:16 vertical video
        color:
          oneOf:
            - type: string
              description: Hex color (e.g., "#00FF88")
            - $ref: '#/components/schemas/Gradient'
          default: "#FFFFFF"
        opacity:
          type: number
          minimum: 0
          maximum: 1
          default: 0.85
        barWidth:
          type: number
          description: Width of each bar in pixels
        barGap:
          type: number
          description: Gap between bars in pixels
        smoothing:
          type: number
          description: Smoothing factor for waveform animation
        height:
          type: number
          description: Height of waveform in pixels

    # ==========================================
    # STYLING & EFFECTS
    # ==========================================

    Position:
      type: object
      properties:
        x:
          oneOf:
            - type: number
            - type: string
          description: X position (number in pixels or string like "50%")
        y:
          oneOf:
            - type: number
            - type: string
          description: Y position (number in pixels or string like "50%")
        anchor:
          type: string
          enum:
            - top-left
            - top-center
            - top-right
            - center-left
            - center
            - center-right
            - bottom-left
            - bottom-center
            - bottom-right
          description: Anchor point for positioning

    Transform:
      type: object
      properties:
        scale:
          oneOf:
            - type: number
            - type: object
              properties:
                x:
                  type: number
                y:
                  type: number
        rotation:
          type: number
          description: Rotation in degrees
        skew:
          type: object
          properties:
            x:
              type: number
            y:
              type: number
        opacity:
          type: number
          minimum: 0
          maximum: 1

    Crop:
      type: object
      properties:
        x:
          type: number
        y:
          type: number
        width:
          type: number
        height:
          type: number

    Gradient:
      type: object
      required: [type, colors]
      properties:
        type:
          type: string
          enum: [linear, radial]
        colors:
          type: array
          items:
            type: object
            properties:
              color:
                type: string
              position:
                type: number
                minimum: 0
                maximum: 1
        angle:
          type: number
          description: Angle for linear gradient (degrees)
        centerX:
          type: number
          description: Center X for radial gradient
        centerY:
          type: number
          description: Center Y for radial gradient

    TextStyle:
      type: object
      properties:
        fontFamily:
          type: string
          default: Inter
        fontSize:
          type: integer
          minimum: 1
          maximum: 500
        fontWeight:
          type: string
          enum: ["normal", "bold", "100", "200", "300", "400", "500", "600", "700", "800", "900"]
        fontStyle:
          type: string
          enum: [normal, italic]
        color:
          type: string
          description: Text color (hex)
        backgroundColor:
          type: string
          description: Background color behind text
        backgroundPadding:
          type: number
        backgroundRadius:
          type: number
        textAlign:
          type: string
          enum: [left, center, right, justify]
        verticalAlign:
          type: string
          enum: [top, middle, bottom]
        lineHeight:
          type: number
        letterSpacing:
          type: number
        stroke:
          type: object
          properties:
            color:
              type: string
            width:
              type: number
        shadow:
          type: object
          properties:
            color:
              type: string
            blur:
              type: number
            offsetX:
              type: number
            offsetY:
              type: number
        gradient:
          type: object
          properties:
            type:
              type: string
              enum: [linear, radial]
            colors:
              type: array
              items:
                type: string
            angle:
              type: number

    AudioSettings:
      type: object
      properties:
        volume:
          type: number
          minimum: 0
          maximum: 2
          default: 1
        fadeIn:
          type: number
          minimum: 0
          description: Fade in duration in seconds
        fadeOut:
          type: number
          minimum: 0
          description: Fade out duration in seconds
        speed:
          type: number
          minimum: 0.5
          maximum: 2
        pitch:
          type: number
          description: Pitch adjustment
        pan:
          type: number
          minimum: -1
          maximum: 1
          description: Stereo pan (-1 = left, 1 = right)
        loop:
          type: boolean
          description: Loop audio playback
        normalize:
          type: boolean
          description: Normalize audio levels
        ducking:
          type: object
          description: Duck audio when other audio segments play
          properties:
            enabled:
              type: boolean
            threshold:
              type: number
            reduction:
              type: number

    TTSSettings:
      type: object
      description: >-
        TTS configuration with two provider tiers.
        Standard (Kokoro): 46 voices, 9 languages. 3 cr/1K chars. Speed 0.5-2.0.
        Premium (ElevenLabs): 52 voices, multilingual. 13 cr/1K chars. Speed 0.7-1.2.
        Voice name determines routing. stability, similarity, and style are ElevenLabs-only.
      properties:
        provider:
          type: string
          enum: [elevenlabs, kokoro]
          description: >-
            TTS provider. Usually omitted — voice name determines routing automatically.
            Kokoro voice IDs (e.g., af_bella) route to Standard tier.
            ElevenLabs names (e.g., Sarah) route to Premium tier.
        voice:
          type: string
          description: >-
            Voice name or ID. Standard (Kokoro) IDs: af_heart, af_bella, am_adam, bf_emma, etc.
            Premium (ElevenLabs) names: Sarah, Liam, Charlotte, Rachel, Brian, etc.
        language:
          type: string
          description: Language code (e.g., "en-US"). For Kokoro, language is determined by voice ID prefix.
        speed:
          type: number
          minimum: 0.5
          maximum: 2
          description: Playback speed. Standard (Kokoro) range 0.5-2.0, Premium (ElevenLabs) range 0.7-1.2.
        pitch:
          type: number
          minimum: -20
          maximum: 20
        volume:
          type: number
        ssml:
          type: string
          description: SSML markup for advanced control
        stability:
          type: number
          minimum: 0
          maximum: 1
          description: Voice stability (ElevenLabs Premium only, ignored for Kokoro)
        similarity:
          type: number
          minimum: 0
          maximum: 1
          description: Voice similarity boost (ElevenLabs Premium only, ignored for Kokoro)
        style:
          type: number
          minimum: 0
          maximum: 1
          description: Voice style exaggeration (ElevenLabs Premium only, ignored for Kokoro)

    CaptionSettings:
      type: object
      properties:
        style:
          type: string
          enum:
            - default
            - minimal
            - bold
            - tiktok
            - instagram
            - youtube
            - tiktok_viral
            - instagram_viral
            - youtube_viral
            - viral
            - viral_highlight
            - viral_bounce
            - viral_pop
            - viral_zoom
            - viral_shake
            - viral_wave
            - karaoke
            - wordByWord
            - outline
            - boxed
            - highlight
            - typewriter
            - bounce
        position:
          type: string
          enum: [top, center, bottom]
        maxWordsPerLine:
          type: integer
          minimum: 1
          maximum: 20
        maxCharsPerLine:
          type: integer
          minimum: 10
          maximum: 100
        textStyle:
          $ref: '#/components/schemas/TextStyle'
        highlightColor:
          type: string
          description: Color for highlighting active word
        animation:
          $ref: '#/components/schemas/PresetAnimation'

    Shape:
      type: object
      required: [type]
      properties:
        type:
          type: string
          enum:
            - rectangle
            - roundedRectangle
            - circle
            - ellipse
            - triangle
            - polygon
            - star
            - line
            - arrow
            - custom
        width:
          type: number
        height:
          type: number
        radius:
          type: number
        cornerRadius:
          oneOf:
            - type: number
            - type: object
              properties:
                topLeft:
                  type: number
                topRight:
                  type: number
                bottomLeft:
                  type: number
                bottomRight:
                  type: number
        points:
          type: integer
          description: Number of points for polygon/star
        innerRadius:
          type: number
          description: Inner radius for star
        path:
          type: string
          description: SVG path for custom shape
        fill:
          oneOf:
            - type: string
            - $ref: '#/components/schemas/Gradient'
        stroke:
          type: object
          properties:
            color:
              type: string
            width:
              type: number
            dashArray:
              type: array
              items:
                type: number
        shadow:
          type: object
          properties:
            color:
              type: string
            blur:
              type: number
            offsetX:
              type: number
            offsetY:
              type: number
        opacity:
          type: number
          minimum: 0
          maximum: 1

    # ==========================================
    # TRANSITIONS & ANIMATIONS
    # ==========================================

    Transition:
      type: object
      required: [type]
      description: Transition effect between segments.
      properties:
        type:
          $ref: '#/components/schemas/TransitionType'
        duration:
          type: number
          minimum: 0
          maximum: 10
          default: 0.5
          description: Transition duration in seconds
        easing:
          $ref: '#/components/schemas/Easing'
        lumaUrl:
          type: string
          format: uri
          description: URL to luma matte for lumaMatte transition type
        inverted:
          type: boolean
          description: Invert the transition effect

    TransitionType:
      type: string
      enum:
        # Fade
        - fade
        - fadeSlow
        - fadeFast
        - crossfade
        - dissolve
        # Wipe
        - wipeLeft
        - wipeRight
        - wipeUp
        - wipeDown
        - wipeDiagonalLeftUp
        - wipeDiagonalRightUp
        - wipeDiagonalLeftDown
        - wipeDiagonalRightDown
        # Slide
        - slideLeft
        - slideRight
        - slideUp
        - slideDown
        - slideLeftSlow
        - slideRightSlow
        # Zoom
        - zoomIn
        - zoomOut
        # Circle/Box
        - circleIn
        - circleOut
        - boxIn
        - boxOut
        # Shuffle
        - shuffleLeft
        - shuffleRight
        - shuffleUp
        - shuffleDown
        # Special
        - lumaMatte
        - none

    PresetAnimation:
      type: string
      enum:
        - fadeIn
        - fadeOut
        - fadeInOut
        - slideInLeft
        - slideInRight
        - slideInUp
        - slideInDown
        - slideOutLeft
        - slideOutRight
        - slideOutUp
        - slideOutDown
        - zoomIn
        - zoomOut
        - zoomInOut
        - rotateIn
        - rotateOut
        - bounceIn
        - bounceOut
        - flipInX
        - flipInY
        - flipOutX
        - flipOutY
        - pulseIn
        - pulseOut
        - typewriter
        - reveal
        - kenBurns
        - kenBurnsZoomIn
        - kenBurnsZoomOut
        - shake
        - wobble
        - swing
        - rubberBand
        - none

    Animation:
      type: object
      required: [property, keyframes]
      description: Custom keyframe animation
      properties:
        property:
          type: string
          description: CSS property to animate (e.g., "opacity", "scale", "x")
        keyframes:
          type: array
          items:
            type: object
            required: [time, value]
            properties:
              time:
                type: number
                minimum: 0
                description: Time in seconds
              value:
                oneOf:
                  - type: number
                  - type: string
                  - type: object
              easing:
                $ref: '#/components/schemas/Easing'
          minItems: 1

    Easing:
      type: string
      enum:
        - linear
        - ease
        - ease-in
        - ease-out
        - ease-in-out
        - easeInQuad
        - easeOutQuad
        - easeInOutQuad
        - easeInCubic
        - easeOutCubic
        - easeInOutCubic
        - easeInQuart
        - easeOutQuart
        - easeInOutQuart
        - easeInBounce
        - easeOutBounce
        - easeInOutBounce
        - easeInElastic
        - easeOutElastic
        - easeInOutElastic

    Effect:
      type: object
      required: [type]
      description: |
        Modern effect via the effectChain system. Discriminated by `type`.
        Supported types: brightness, contrast, saturation, temperature, gamma,
        grayscale, sepia, invert, blur, sharpen, pixelate, filmGrain, vignette,
        denoise, cinematic, lut, zoom, pan, rotate, speed, reverse, boomerang,
        rgbShift, vhs, screenShake, datamosh, colorGlitch, mirrorGlitch,
        karaoke, viralHighlight, typewriter, wordByWord, bounceCaptions,
        customFilter.
      properties:
        type:
          type: string
          description: Effect type identifier
        preset:
          type: string
          enum: [subtle, medium, strong, extreme]
          description: Intensity preset
        url:
          type: string
          format: uri
          description: URL to external resource (e.g., .cube LUT file for lut effect)
        interpolation:
          type: string
          enum: [tetrahedral, trilinear, nearest]
          description: Interpolation method for LUT effects (default tetrahedral)
        look:
          type: string
          enum: [warm_sunset, cool_teal, desaturated, high_contrast, vintage, noir]
          description: Cinematic look preset (for cinematic effect type)
        direction:
          type: string
          description: Direction for zoom (in/out) or pan (left/right/up/down)
        intensity:
          type: number
          minimum: 0
          maximum: 1
          description: Effect intensity (0.0 - 1.0)
        filter:
          type: string
          description: FFmpeg filter expression for customFilter type (validated by 7-stage security pipeline)
          maxLength: 2000
          example: "gblur=sigma=5,eq=brightness=0.1,vignette=PI*0.2"

    CustomFilterEffect:
      type: object
      required: [type, filter]
      properties:
        type:
          type: string
          enum: [customFilter]
        filter:
          type: string
          description: FFmpeg filter expression (validated by security pipeline)
          maxLength: 2000
          example: "gblur=sigma=5,eq=brightness=0.1,vignette=PI*0.2"
        description:
          type: string
          description: Human-readable description of the effect
          maxLength: 500

    Filter:
      type: object
      required: [type]
      description: |
        Visual effect filter. Use EITHER `preset` for quick configuration OR individual parameters for fine control.
        See the EFFECTS REFERENCE section in llms.txt for complete documentation.
      properties:
        type:
          $ref: '#/components/schemas/FilterType'
        preset:
          type: string
          enum: [subtle, medium, strong, extreme]
          description: Quick preset (overrides individual parameters)
        intensity:
          type: number
          minimum: 0
          maximum: 2
          description: Effect intensity multiplier
        value:
          type: number
          description: Effect-specific value (varies by filter type)
        color:
          type: string
          description: Primary color for color-based effects (hex)
        color2:
          type: string
          description: Secondary color for duotone, etc.
        radius:
          type: number
          description: Radius for blur, vignette, etc.
        angle:
          type: number
          description: Angle in degrees for directional effects
        x:
          type: number
        y:
          type: number

    FilterType:
      type: string
      enum:
        # Color filters
        - grayscale
        - sepia
        - invert
        - saturate
        - desaturate
        - brightness
        - contrast
        - hue-rotate
        # Blur
        - blur
        - gaussianBlur
        - motionBlur
        - radialBlur
        # Stylize
        - sharpen
        - emboss
        - posterize
        - pixelate
        - vignette
        - filmGrain
        - vintage
        - retro
        # Color adjustments
        - colorBalance
        - curves
        - levels
        - vibrance
        # Special
        - glitch
        - chromaAberration
        - duotone
        - glow
        - dropShadow
        - innerShadow

    BlendMode:
      type: string
      enum:
        - normal
        - multiply
        - screen
        - overlay
        - darken
        - lighten
        - color-dodge
        - color-burn
        - hard-light
        - soft-light
        - difference
        - exclusion

    # ==========================================
    # OUTPUT SETTINGS
    # ==========================================

    OutputSettings:
      type: object
      properties:
        format:
          type: string
          enum: [mp4, webm, mov, gif, png, jpg, mp3, wav]
          default: mp4
        codec:
          type: string
          enum: [h264, h265, vp9, av1, prores]
          default: h264
          description: Video codec (h264=most compatible, h265=better compression, vp9=open source WebM, av1=modern open codec, prores=professional editing)
        quality:
          type: string
          enum: [low, medium, high, ultra, lossless]
          default: high
        bitrate:
          type: integer
          description: Video bitrate in kbps
        crf:
          type: integer
          minimum: 0
          maximum: 51
          description: Constant Rate Factor (lower = better quality)
        audioCodec:
          type: string
          enum: [aac, mp3, opus, pcm]
        audioBitrate:
          type: integer
          description: Audio bitrate in kbps
        sampleRate:
          oneOf:
            - type: string
              enum: ["44100", "48000", "96000"]
            - type: integer
        fps:
          type: integer
          minimum: 1
          maximum: 120
          description: Output frame rate (overrides canvas fps)
        hdr:
          type: object
          description: "HDR10 output settings. Requires codec h265 or av1. Cannot be used with hdrToSdr."
          properties:
            enabled:
              type: boolean
              description: Enable HDR10 output with BT.2020/PQ color space
            maxCLL:
              type: integer
              minimum: 0
              maximum: 10000
              description: Maximum Content Light Level in nits
            maxFALL:
              type: integer
              minimum: 0
              maximum: 10000
              description: Maximum Frame-Average Light Level in nits
            masterDisplay:
              type: string
              description: "SMPTE ST 2086 mastering display metadata (CIE 1931 coordinates)"
        hdrToSdr:
          type: boolean
          description: Convert HDR input content to SDR output
        thumbnail:
          type: object
          description: Generate thumbnail image
          properties:
            time:
              type: number
              minimum: 0
              description: Time to capture thumbnail
            format:
              type: string
              enum: [jpg, png]
            width:
              type: integer
            height:
              type: integer
        destinations:
          type: array
          description: Upload output to external storage
          items:
            type: object
            properties:
              type:
                type: string
                enum: [s3, r2, gcs, azure, url]
              url:
                type: string
              bucket:
                type: string
              key:
                type: string
              credentials:
                type: object
                additionalProperties:
                  type: string

    # ==========================================
    # RESOLUTION PRESETS
    # ==========================================

    ResolutionPreset:
      type: string
      enum:
        # Social Media - Vertical (9:16)
        - youtube_short
        - instagram_story
        - instagram_reel
        - tiktok
        - snapchat
        - pinterest_pin
        # Social Media - Square (1:1)
        - instagram_feed
        - facebook_square
        # Social Media - Landscape (16:9)
        - youtube_landscape
        - facebook_video
        - twitter_video
        - linkedin_video
        # Standard Resolutions
        - hd          # 1280x720
        - full_hd     # 1920x1080
        - 2k          # 2560x1440
        - 4k          # 3840x2160
        # Aspect Ratios (auto-size)
        - "16:9"
        - "9:16"
        - "1:1"
        - "4:3"
        - "4:5"
        - "21:9"

    # ==========================================
    # RESPONSE SCHEMAS
    # ==========================================

    ProductionResponse:
      type: object
      properties:
        id:
          type: string
          format: uuid
        status:
          type: string
          enum: [queued]
          description: |
            Always `queued` for newly created productions — the create
            endpoint enqueues the job synchronously before returning.
            Poll `GET /productions/{id}` to observe transitions to
            `processing`, `completed`, `failed`, or `cancelled`. See
            the shared `ProductionStatus` schema for the full lifecycle.
        priority:
          type: string
          enum: [low, normal, high]
        estimated_wait_seconds:
          type: integer
          description: Estimated wait time in seconds
        created_at:
          type: string
          format: date-time
        credits_used:
          type: integer
          description: Credits consumed by this production
        credits_remaining:
          type: integer
          description: Remaining credits balance
        credits_breakdown:
          type: object
          description: "Sums to `credits_used` when nonzero; all fields are zero when `credits_used` is `0` (for example, admin allowlist requests or `testMode` bypasses)."
          properties:
            base_fee:
              type: integer
            video_render:
              type: integer
            tts:
              type: integer
            effects_processing:
              type: integer
        concurrent_jobs:
          type: integer
          description: Number of currently active jobs for this user
        max_concurrent:
          type: integer
          description: |
            Maximum concurrent jobs allowed for this plan. Admin-allowlisted
            accounts see `999` (the plan cap is bypassed) — when this
            distinction matters, detect admin mode via the
            `X-StrataKit-Admin-Mode: true` response header rather than
            inferring from the magic number. Regular integrations never
            receive that header in production.

    Production:
      type: object
      properties:
        id:
          type: string
          format: uuid
        status:
          $ref: '#/components/schemas/ProductionStatus'
        output_url:
          type: string
          format: uri
          nullable: true
          description: |
            Direct presigned R2 URL to download the completed video.
            This is the actual production output URL, not an API proxy URL.
            Includes `X-Amz-Algorithm=AWS4-HMAC-SHA256` and expires 1 hour
            after the URL was minted (the server re-signs every time you call
            `GET /productions/{id}`, so the absolute expiry is independent
            of when the production was created). See `url_expires_at` for
            the exact expiry stamp that governs this specific URL.

            **HEAD is not authorized** — S3/R2 presigned URLs are method-scoped
            and the signature authorizes GET only. AWS S3 returns 403; R2 may
            currently respond inconsistently (some accounts have observed
            200 OK with a full Content-Length), so treat HEAD as best-effort
            and do not rely on it for size or existence. For the canonical
            total size, prefer the `output_size` field on this payload. To
            probe existence/content-type, use GET with a `Range: bytes=0-0`
            header — `Content-Range` carries the total size when the backend
            includes it.

            After expiry, re-fetch this endpoint to get a fresh URL.
        output_duration:
          type: number
          nullable: true
          description: Actual output duration in seconds
        processing_time_ms:
          type: integer
          nullable: true
          description: Time taken to process in milliseconds
        error:
          type: string
          nullable: true
          description: Error message if failed
        created_at:
          type: string
          format: date-time
        processing_started_at:
          type: string
          format: date-time
          nullable: true
        completed_at:
          type: string
          format: date-time
          nullable: true
        audio_metrics:
          type: object
          nullable: true
          description: EBU R128 audio loudness metrics for the production output
          properties:
            integrated_lufs:
              type: number
              description: Integrated loudness in LUFS
              example: -14.0
            true_peak_dbfs:
              type: number
              description: True peak level in dBFS
              example: -1.1
            loudness_range_lu:
              type: number
              description: Loudness Range in LU
              example: 7.2
            lra_low_lufs:
              type: number
              description: Low boundary of loudness range in LUFS
              example: -18.7
            lra_high_lufs:
              type: number
              description: High boundary of loudness range in LUFS
              example: -11.5
        thumbnail_url:
          type: string
          format: uri
          nullable: true
          description: |
            Presigned URL to the production's thumbnail image (a still
            frame extracted from the output). Expires after 1 hour.
            **HEAD is not authorized** — like `output_url`, the signature
            authorizes GET only. Use a Range GET (`Range: bytes=0-0`) to
            probe headers; treat HEAD behavior as best-effort.
        hls_url:
          type: string
          format: uri
          nullable: true
          description: |
            Presigned URL to the HLS (.m3u8) manifest for streaming
            playback. Expires after 1 hour. **HEAD is not authorized**
            — like `output_url`, the signature authorizes GET only. Use
            a Range GET (`Range: bytes=0-0`) to probe headers; treat HEAD
            behavior as best-effort.
        proxy_video_url:
          type: string
          format: uri
          nullable: true
          description: |
            Presigned URL to a proxy/low-res variant of the output video,
            used by the dashboard for fast preview. Expires after 1 hour.
            **HEAD is not authorized** — like `output_url`, the signature
            authorizes GET only. Use a Range GET (`Range: bytes=0-0`) to
            probe headers; treat HEAD behavior as best-effort.
        share_url:
          type: string
          format: uri
          nullable: true
          description: Public share URL (only set when `visibility` is `unlisted` or `public` AND a share token exists).
        webhook_delivered_at:
          type: string
          format: date-time
          nullable: true
          description: Timestamp of the most recent successful webhook delivery for this production, or null if no webhook has been delivered yet.
        url_expires_at:
          type: string
          format: date-time
          nullable: true
          description: |
            Absolute ISO-8601 timestamp at which `output_url`, `hls_url`,
            `thumbnail_url`, and `proxy_video_url` stop being accepted by
            R2. Derived from the server's presigned-GET TTL at the moment
            this response was produced — the URL TTL and this field are
            always computed from the same value, so `url_expires_at` is
            safe to use as the absolute expiry for any cache layer you
            build on top. `null` when the production has no signed URLs
            yet (e.g., still queued/processing).
        credits_used:
          type: integer
          nullable: true
          description: |
            Credits charged to the owner's balance for this production.
            Zero is legitimate for admin-exempt renders (see
            `pricing_mode` = `"admin"`); use `total_cost_usd` +
            `implied_credits_used` for display when reconciling.
        compute_tier:
          type: string
          nullable: true
          enum: [cpu, gpu_light, gpu_heavy]
          description: |
            Which Modal GPU/CPU tier this render consumed. Populated by
            the queue consumer on completion. `null` for in-flight and
            pre-cost-tracking rows.
        compute_cost_usd:
          type: number
          nullable: true
          description: Compute-time portion of the per-production cost, in USD.
        tts_cost_usd:
          type: number
          nullable: true
          description: TTS-provider portion of the per-production cost, in USD.
        tts_characters_used:
          type: integer
          nullable: true
          description: Count of TTS characters synthesized for this production.
        transcription_cost_usd:
          type: number
          nullable: true
          description: Transcription-provider portion of the per-production cost, in USD.
        transcription_seconds_used:
          type: number
          nullable: true
          description: Audio duration sent to transcription, in seconds.
        storage_cost_usd:
          type: number
          nullable: true
          description: R2 storage portion of the per-production cost, in USD.
        total_cost_usd:
          type: number
          nullable: true
          description: |
            Sum of compute + tts + transcription + storage cost columns
            in USD. `null` for in-flight and pre-cost-tracking rows.
            When `pricing_mode` = `"admin"` the owner was not charged —
            compare against `implied_credits_used` for what a non-exempt
            user would have been billed.
        pricing_mode:
          type: string
          enum: [user, admin]
          description: |
            Display-only flag distinguishing regular-user billing
            (`"user"`) from admin-exempt renders (`"admin"`) where the
            credit ledger did not deduct credits despite a nonzero cost.
            Regular integrations will always see `"user"`.
        implied_credits_used:
          type: integer
          nullable: true
          description: |
            Display-only. Only non-null when `pricing_mode` = `"admin"`
            AND `total_cost_usd` is non-null. Equals
            `ceil(total_cost_usd / 0.01)` — the credit integer a
            non-exempt user would have been billed. Does not reflect
            any actual ledger entry.

    ProductionList:
      type: object
      properties:
        data:
          type: array
          items:
            $ref: '#/components/schemas/Production'
        pagination:
          type: object
          properties:
            limit:
              type: integer
            offset:
              type: integer
              nullable: true
              description: Current offset (null when using cursor pagination)
            page:
              type: integer
              nullable: true
              description: Current page number (null when using cursor pagination)
            total:
              type: integer
              nullable: true
              description: Total count (null when using cursor pagination for performance)
            total_pages:
              type: integer
              nullable: true
              description: Total page count (null when using cursor pagination)
            has_more:
              type: boolean
              description: Whether more results exist beyond this page
            next_cursor:
              type: string
              nullable: true
              description: Compound cursor (`{timestamp}|{uuid}`) for the next page; null if no more results

    Blueprint:
      type: object
      description: |
        Blueprint response object.

        **Field casing note (2026-04-20):** Historically, blueprint responses
        used camelCase field names (`isPublic`, `createdAt`, `updatedAt`,
        `usageCount`, `isOwner`), while production responses used snake_case
        (`is_public`, `created_at`, `updated_at`). SDK clients that normalize
        on one casing broke when the shape differed across endpoints. As of
        2026-04-20 blueprints now emit BOTH camelCase AND snake_case keys
        for every top-level field. The camelCase variants are
        **deprecated** and will be removed in a future API version — new
        clients SHOULD read the snake_case keys. Existing clients continue
        to work unchanged.
      properties:
        id:
          type: string
          format: uuid
        name:
          type: string
        description:
          type: string
          nullable: true
        category:
          type: string
          nullable: true
          description: Optional category/taxonomy label (e.g. "marketing", "tutorial").
        thumbnail:
          type: string
          nullable: true
          description: Optional thumbnail image URL for blueprint gallery display.
        canvas:
          $ref: '#/components/schemas/Canvas'
        bindings:
          type: object
          nullable: true
          description: |
            Optional default bindings applied when this blueprint is used
            to create a new production. Mirrors the `bindings` map on
            `POST /productions`.
          additionalProperties: true
        isPublic:
          type: boolean
          deprecated: true
          description: Deprecated camelCase alias — use `is_public`.
        is_public:
          type: boolean
        isOwner:
          type: boolean
          deprecated: true
          description: Deprecated camelCase alias — use `is_owner`.
        is_owner:
          type: boolean
          description: True when the authenticated caller owns this blueprint.
        usageCount:
          type: integer
          minimum: 0
          deprecated: true
          description: Deprecated camelCase alias — use `usage_count`.
        usage_count:
          type: integer
          minimum: 0
          description: Number of productions created from this blueprint.
        isArchived:
          type: boolean
          deprecated: true
          description: Deprecated camelCase alias — use `is_archived`.
        is_archived:
          type: boolean
          description: True when the blueprint is archived (hidden from default lists).
        createdAt:
          type: string
          format: date-time
          deprecated: true
          description: Deprecated camelCase alias — use `created_at`.
        created_at:
          type: string
          format: date-time
        updatedAt:
          type: string
          format: date-time
          deprecated: true
          description: Deprecated camelCase alias — use `updated_at`.
        updated_at:
          type: string
          format: date-time

    BlueprintList:
      type: object
      properties:
        data:
          type: array
          items:
            $ref: '#/components/schemas/Blueprint'
        pagination:
          type: object
          properties:
            limit:
              type: integer
            offset:
              type: integer
            total:
              type: integer
            hasMore:
              type: boolean

    EffectTag:
      type: object
      description: |
        A visual effect tag with optional intensity. Used in the Tag Mix & Match system
        to apply curated visual effects to a canvas. Tags are FREE (0 credits).
        Use `GET /tags` to list all available tag IDs.

        Exactly one of `tag` (canonical) or `id` (alias) is required. Both fields
        accept the same identifier — `id` mirrors the field name returned by
        `GET /tags` so integrators can pipe its response straight into a
        canvas-wizard payload without a rename step.
      properties:
        tag:
          type: string
          maxLength: 50
          description: Tag identifier (e.g., "cinematic", "warm", "grain"). Canonical field name.
          example: cinematic
        id:
          type: string
          maxLength: 50
          description: Alias for `tag`. Matches the field name returned by `GET /tags`.
          example: cinematic
        intensity:
          type: number
          minimum: 0
          maximum: 100
          default: 50
          description: Effect intensity from 0 (minimum) to 100 (maximum)
      oneOf:
        - required: [tag]
        - required: [id]
      example:
        tag: cinematic
        intensity: 75

    HealthCheckResponse:
      type: object
      required: [status, timestamp, version, components, mode]
      description: |
        Per-component health snapshot returned by both `/health` (shallow)
        and `/health/deep`. Shallow responses strip component messages /
        details to prevent information leakage to anonymous callers; deep
        responses (authenticated) include the full diagnostic context.
      properties:
        status:
          type: string
          enum: [healthy, degraded, unhealthy]
        timestamp:
          type: string
          format: date-time
        version:
          type: string
          description: Health check schema version
        uptime_check:
          type: boolean
        mode:
          type: string
          enum: [shallow, deep]
        check_duration_ms:
          type: number
          description: Total health check duration in milliseconds
        components:
          type: object
          description: |
            Per-component health status. Shallow `/health` responses strip
            `message` and `details` to prevent information leakage to
            unauthenticated callers; deep `/health/deep` responses include
            the full diagnostic context on every component.
          additionalProperties:
            type: object
            required: [status, latency_ms]
            properties:
              status:
                type: string
                enum: [healthy, degraded, unhealthy, unknown]
              latency_ms:
                type: number
              message:
                type: string
                description: |
                  Human-readable status detail (deep mode only). E.g.
                  "connected", "R2 returned 503", "auth credentials invalid (401)".
              details:
                type: object
                description: |
                  Component-specific diagnostic payload (deep mode only).
                  Free-form per component — treat as informational, not API.
                additionalProperties: true

    Error:
      type: object
      required: [error]
      properties:
        error:
          type: object
          required: [code, message]
          properties:
            code:
              type: string
              description: Machine-readable error code
            message:
              type: string
              description: Human-readable error message
            details:
              type: object
              description: Additional error details

  # ==========================================
  # REUSABLE HEADERS
  # ==========================================

  headers:
    XRequestId:
      schema:
        type: string
        maxLength: 128
        pattern: '^[A-Za-z0-9_\-.]+$'
      description: |
        Server-assigned request identifier. Emitted on every response
        (including 401/404/429/500) so you can correlate client-side
        failures with server logs.

        Format: the middleware accepts any client-provided value
        matching `^[A-Za-z0-9_\-.]{1,128}$` (so distributed traces can
        thread their own request ID through) and otherwise mints a
        fresh UUIDv4. Integrators should treat the value as opaque —
        do NOT parse it as a UUID. Safe to log, never a secret.

        Ryan persona finding 2026-04-22 — a dev-friendly request-id
        response header lives at the top of StrataKit's observability
        story.

    RetryAfter:
      schema:
        type: integer
      description: |
        Seconds to wait before retrying. Emitted on 429 (rate limit)
        and 503 (transient service error). Matches the `X-RateLimit-*`
        window when rate-limit-induced; service-induced backoff may
        exceed the rate-limit window.

  # ==========================================
  # RESPONSE TEMPLATES
  # ==========================================

  responses:
    BadRequest:
      description: Invalid request
      headers:
        x-request-id:
          $ref: '#/components/headers/XRequestId'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            error:
              code: "VALIDATION_ERROR"
              message: "Validation error"
              details:
                field: "canvas.segments"
                message: "At least one segment is required"

    Unauthorized:
      description: Missing or invalid API key
      headers:
        x-request-id:
          $ref: '#/components/headers/XRequestId'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            error:
              code: "UNAUTHORIZED"
              message: "Invalid API key"

    InsufficientCredits:
      description: No credits remaining
      headers:
        x-request-id:
          $ref: '#/components/headers/XRequestId'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            error:
              code: "INSUFFICIENT_CREDITS"
              message: "Insufficient credits"

    NotFound:
      description: Resource not found
      headers:
        x-request-id:
          $ref: '#/components/headers/XRequestId'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            error:
              code: "NOT_FOUND"
              message: "Production not found"

    RateLimited:
      description: |
        Rate limit exceeded. Note that `X-RateLimit-*` and `Retry-After`
        headers are currently emitted only on successful (2xx) responses
        and on this 429 response — not on 401/404/500. Clients that need
        to meter burn-rate across every response type should fall back
        to their own counter when these headers are absent.
      headers:
        Retry-After:
          $ref: '#/components/headers/RetryAfter'
        x-request-id:
          $ref: '#/components/headers/XRequestId'
        X-RateLimit-Limit:
          schema:
            type: integer
          description: Request limit per minute
        X-RateLimit-Remaining:
          schema:
            type: integer
          description: Requests remaining
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            error:
              code: "RATE_LIMITED"
              message: "Rate limit exceeded"
              details:
                retryAfter: 60

    ServiceUnavailable:
      description: Service temporarily unavailable (circuit breaker open)
      headers:
        Retry-After:
          $ref: '#/components/headers/RetryAfter'
        x-request-id:
          $ref: '#/components/headers/XRequestId'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            error:
              code: "SERVICE_UNAVAILABLE"
              message: "Service temporarily unavailable"
              details:
                retryAfter: 30
