openapi: 3.1.0
info:
  title: Heista API
  version: 1.0.0
  description: |
    Creative intelligence as an API. Decode any video ad, extract brand intelligence from any URL or document, generate scripts in any brand's voice, and browse the decoded corpus for free.

    ## Surfaces
    - **Decode** — turn any video ad URL into a structural formula. Beats, hooks, psychology, visual playbook.
    - **PowerSource** — 14-section brand intelligence from a URL, internal documents, or both.
    - **Adscript** — generate 1–5 ad scripts from a proven structure + a PowerSource. Token-metered.
    - **Files** — upload PDF/DOCX/TXT/MD for use in PowerSource docs mode.
    - **Intelligence reads** — free corpus access (hook patterns, ad formulas, decoded ads).
    - **Account** — credit balance, monthly usage, pricing.

    ## Pricing
    - Decode: **$0.15** (≤60s) / **$0.20** (61–120s)
    - PowerSource: **$1.00** (URL or docs only) / **$2.00** (URL + docs)
    - Adscript: **token-metered**, typically **$0.02–$0.05** per script
    - Intelligence reads, Files API: **free**
    - Failed jobs auto-refund. Duplicate inputs (same org) return cached results at no charge.

    ## Authentication
    Bearer header on every request:
    ```
    Authorization: Bearer hst_live_your_key
    ```
    Production keys are prefixed `hst_live_`; test keys are `hst_test_`. Get yours at [heista.co/api-console](https://heista.co/api-console).

    ## Errors
    All errors follow [RFC 9457 Problem Details](https://www.rfc-editor.org/rfc/rfc9457). The same envelope shape across every endpoint; see the `ProblemDetails` schema.

    Spending-cap and circuit-breaker errors include a `reason` field (`cap_hourly`, `cap_daily`, `cap_monthly`, `cap_key_daily`, `cap_key_monthly`, `circuit_open`). Insufficient-credits errors include `top_up_url`, `current_balance`, and `required` extensions.

    ## Rate limits
    Per API key, per endpoint group. Every response carries `X-RateLimit-Limit`, `X-RateLimit-Remaining`, and `X-RateLimit-Reset` (epoch ms). On 429 you also get `Retry-After` (seconds).
  contact:
    name: Heista Support
    email: support@heista.co
    url: https://heista.co
  termsOfService: https://heista.co/terms
  license:
    name: Proprietary
    url: https://heista.co/terms

servers:
  - url: https://www.heista.co/api/v1
    description: Production
  - url: http://localhost:3000/api/v1
    description: Local development

security:
  - bearerAuth: []

tags:
  - name: Decode
    description: Decode any video ad URL into a beat-by-beat structural breakdown.
  - name: PowerSource
    description: Extract 14-section brand intelligence from a URL, internal documents, or both.
  - name: Adscript
    description: Generate direct-response ad scripts from a structural source + a PowerSource.
  - name: Files
    description: Upload PDF/DOCX/TXT/MD files for use in PowerSource docs mode.
  - name: Intelligence
    description: Free, read-only access to Heista's decoded corpus (hooks, formulas, decoded ads).
  - name: Account
    description: Credit balance, monthly usage, pricing.

paths:
  # ─── Decode ───────────────────────────────────────────────────────────
  /decode:
    post:
      tags: [Decode]
      summary: Submit a video ad for decoding
      description: |
        Accepts any public video URL. Reserves the worst-case price up front, runs the multi-agent decoding pipeline in the background, and returns a job ID for polling.
      operationId: submitDecode
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/DecodeSubmitRequest'
      responses:
        '202':
          description: Job accepted (queued)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DecodeJobQueued'
        '200':
          description: Deduplicated or idempotent — existing result
          content:
            application/json:
              schema:
                oneOf:
                  - $ref: '#/components/schemas/DecodeJobCompletedRef'
                  - $ref: '#/components/schemas/DecodeJobInflightRef'
                  - $ref: '#/components/schemas/DecodeJobIdempotentRef'
        '400':
          $ref: '#/components/responses/ValidationError'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '402':
          $ref: '#/components/responses/InsufficientCredits'
        '403':
          $ref: '#/components/responses/CircuitBreaker'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'

  /decode/{id}:
    get:
      tags: [Decode]
      summary: Poll decode job / retrieve completed result
      description: Returns job status while processing; full 5-tab decode report when completed.
      operationId: getDecode
      parameters:
        - name: id
          in: path
          required: true
          schema: { type: string, format: uuid }
      responses:
        '200':
          description: Job status or completed decode
          content:
            application/json:
              schema:
                oneOf:
                  - $ref: '#/components/schemas/DecodeJobQueued'
                  - $ref: '#/components/schemas/DecodeJobProcessing'
                  - $ref: '#/components/schemas/DecodeJobCompleted'
                  - $ref: '#/components/schemas/DecodeJobFailed'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/RateLimited'

  # ─── PowerSource ──────────────────────────────────────────────────────
  /powersource:
    post:
      tags: [PowerSource]
      summary: Submit a PowerSource brand intelligence job
      description: |
        Three modes auto-detected from body shape:
        - **URL only** → $1.00, ~75s
        - **Documents only** (file_ids / document_urls / documents_inline) → $1.00, ~90s
        - **URL + documents** → $2.00, ~130s

        `context_url` is mutually exclusive with `url`: pass `url` to use the page as a primary source (URL or full mode), or `context_url` for supplementary live brand context when the documents are primary.

        Optional `webhook_url` receives a signed POST when the job completes or fails — eliminates polling. See `webhooks.powersource_complete` below.
      operationId: submitPowerSource
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PowerSourceSubmitRequest'
      responses:
        '202':
          description: Job accepted (queued)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PowerSourceJobQueued'
        '200':
          description: Deduplicated, in-flight, or idempotent existing job
          content:
            application/json:
              schema:
                oneOf:
                  - $ref: '#/components/schemas/PowerSourceJobCompletedRef'
                  - $ref: '#/components/schemas/PowerSourceJobInflightRef'
                  - $ref: '#/components/schemas/PowerSourceJobIdempotentRef'
        '400':
          $ref: '#/components/responses/ValidationError'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '402':
          $ref: '#/components/responses/InsufficientCredits'
        '403':
          $ref: '#/components/responses/CircuitBreaker'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'

  /powersource/{id}:
    get:
      tags: [PowerSource]
      summary: Poll PowerSource job / retrieve completed brand intelligence
      operationId: getPowerSource
      parameters:
        - name: id
          in: path
          required: true
          schema: { type: string, format: uuid }
      responses:
        '200':
          description: Job status or completed PowerSource
          content:
            application/json:
              schema:
                oneOf:
                  - $ref: '#/components/schemas/PowerSourceJobQueued'
                  - $ref: '#/components/schemas/PowerSourceJobProcessing'
                  - $ref: '#/components/schemas/PowerSourceJobCompleted'
                  - $ref: '#/components/schemas/PowerSourceJobFailed'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/RateLimited'

  # ─── Adscript ─────────────────────────────────────────────────────────
  /adscript:
    post:
      tags: [Adscript]
      summary: Generate 1–5 ad scripts
      description: |
        Synchronous, single API call. Combines a structural source (decoded ad or formula) with a PowerSource brand profile, then generates 1–5 finished scripts. Token-metered: typical 2–5 cents per script.

        Pass `source_id` + `source_type` for the structure, `powersource_id` for the brand. Optional `count` (1–5), `script_mode` (`blueprint` or `remix`), `duration` (10–120s for remix), `audience`, `tension`, `selling_points`, `voice_mode` (`creator` or `brand`).
      operationId: generateAdscript
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/AdscriptRequest'
      responses:
        '200':
          description: Generation complete
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AdscriptResponse'
        '400':
          $ref: '#/components/responses/ValidationError'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '402':
          $ref: '#/components/responses/InsufficientCredits'
        '403':
          $ref: '#/components/responses/CircuitBreaker'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'

  # ─── Files ────────────────────────────────────────────────────────────
  /files:
    post:
      tags: [Files]
      summary: Upload a file
      description: |
        Multipart upload or JSON+base64. Accepted types: PDF, DOCX, DOC, TXT, MD. Max 10MB per file. Files expire 30 days after upload.

        Use the returned `id` in PowerSource `file_ids[]`.
      operationId: uploadFile
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              type: object
              properties:
                file:
                  type: string
                  format: binary
              required: [file]
          application/json:
            schema:
              $ref: '#/components/schemas/InlineFileUpload'
      responses:
        '201':
          description: File created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicFile'
        '400':
          $ref: '#/components/responses/ValidationError'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '429':
          $ref: '#/components/responses/RateLimited'
    get:
      tags: [Files]
      summary: List files
      operationId: listFiles
      parameters:
        - name: limit
          in: query
          schema: { type: integer, default: 50, minimum: 1, maximum: 100 }
      responses:
        '200':
          description: List of files
          content:
            application/json:
              schema:
                type: object
                properties:
                  files:
                    type: array
                    items: { $ref: '#/components/schemas/PublicFile' }
                  count:
                    type: integer

  # ─── Intelligence ─────────────────────────────────────────────────────
  /intelligence/hooks:
    get:
      tags: [Intelligence]
      summary: Hook intelligence from the decoded corpus
      description: |
        Returns proven hook patterns matched to vertical / hook_type / marketing_angle. Free.
      operationId: getHookIntelligence
      parameters:
        - { name: vertical,        in: query, schema: { type: string } }
        - { name: hook_type,       in: query, schema: { type: string } }
        - { name: marketing_angle, in: query, schema: { type: string } }
      responses:
        '200':
          description: Hook intelligence
          content:
            application/json:
              schema:
                type: object
                properties:
                  filters: { type: object }
                  count: { type: integer }
                  results:
                    type: array
                    items: { $ref: '#/components/schemas/HookIntelligenceResult' }

  /intelligence/formulas:
    get:
      tags: [Intelligence]
      summary: Ad formula blueprints
      description: Clustered structural blueprints. Filter by vertical, creative_format, marketing_angle, algo_intent, hook_type. Free.
      operationId: getAdFormulas
      parameters:
        - { name: vertical,         in: query, schema: { type: string } }
        - { name: creative_format,  in: query, schema: { type: string } }
        - { name: marketing_angle,  in: query, schema: { type: string } }
        - { name: algo_intent,      in: query, schema: { type: string } }
        - { name: hook_type,        in: query, schema: { type: string } }
        - { name: limit,            in: query, schema: { type: integer, default: 5, minimum: 1, maximum: 10 } }
      responses:
        '200':
          description: Formula corpus results
          content:
            application/json:
              schema:
                type: object
                properties:
                  filters: { type: object }
                  count: { type: integer }
                  formulas:
                    type: array
                    items: { $ref: '#/components/schemas/Formula' }

  /intelligence/decoder:
    get:
      tags: [Intelligence]
      summary: Individual decoded ads from the corpus
      description: Sorted by active_days descending, deduplicated by brand. Filter by vertical, creative_format, marketing_angle, hook_type, algo_intent, brand. Free.
      operationId: getDecodedAds
      parameters:
        - { name: vertical,         in: query, schema: { type: string } }
        - { name: creative_format,  in: query, schema: { type: string } }
        - { name: marketing_angle,  in: query, schema: { type: string } }
        - { name: hook_type,        in: query, schema: { type: string } }
        - { name: algo_intent,      in: query, schema: { type: string } }
        - { name: brand,            in: query, schema: { type: string, maxLength: 100 } }
        - { name: limit,            in: query, schema: { type: integer, default: 5, minimum: 1, maximum: 10 } }
      responses:
        '200':
          description: Decoded ads
          content:
            application/json:
              schema:
                type: object
                properties:
                  filters: { type: object }
                  count: { type: integer }
                  ads:
                    type: array
                    items: { $ref: '#/components/schemas/DecodedAd' }

  # ─── Account ──────────────────────────────────────────────────────────
  /account/balance:
    get:
      tags: [Account]
      summary: Account credit balance and monthly usage
      operationId: getAccountBalance
      responses:
        '200':
          description: Balance + monthly usage + pricing reference
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AccountBalance'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '429':
          $ref: '#/components/responses/RateLimited'

webhooks:
  powersource_complete:
    post:
      summary: PowerSource job completion notification
      description: |
        Sent to `webhook_url` (if provided on submission) when a PowerSource job reaches a terminal state (`completed` or `failed`).

        The body is signed with HMAC-SHA256 using your org's webhook secret. Verify the `X-Heista-Signature` header — format: `t=<unix_timestamp>,v1=<hex_hmac>`. Compute `HMAC-SHA256(secret, "{t}.{raw_body}")` and compare.
      requestBody:
        content:
          application/json:
            schema:
              type: object
              required: [event, job_id, status]
              properties:
                event: { type: string, enum: [powersource.completed, powersource.failed] }
                job_id: { type: string, format: uuid }
                brief_id: { type: string, format: uuid, nullable: true }
                status: { type: string, enum: [completed, failed] }
                price_cents: { type: integer, nullable: true }
                completed_at: { type: string, format: date-time, nullable: true }
                error: { type: string, nullable: true }
      responses:
        '2XX':
          description: Webhook acknowledged. Heista retries non-2XX responses with exponential backoff (up to 5 attempts).

components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: 'hst_live_xxx (production) or hst_test_xxx (test)'
      description: API key issued at heista.co/api-console.

  responses:
    Unauthorized:
      description: Missing or invalid API key
      content:
        application/problem+json:
          schema: { $ref: '#/components/schemas/ProblemDetails' }
    InsufficientCredits:
      description: Balance too low. Includes `current_balance`, `required`, and `top_up_url`.
      content:
        application/problem+json:
          schema: { $ref: '#/components/schemas/InsufficientCreditsProblem' }
    CircuitBreaker:
      description: 'Account paused (anomaly detection) — `reason: circuit_open`. Review at the API console.'
      content:
        application/problem+json:
          schema: { $ref: '#/components/schemas/SpendingCapProblem' }
    RateLimited:
      description: Rate limit exceeded. Includes `Retry-After` (seconds).
      content:
        application/problem+json:
          schema: { $ref: '#/components/schemas/ProblemDetails' }
    NotFound:
      description: Resource not found or not owned by this org
      content:
        application/problem+json:
          schema: { $ref: '#/components/schemas/ProblemDetails' }
    ValidationError:
      description: Request validation failed
      content:
        application/problem+json:
          schema: { $ref: '#/components/schemas/ProblemDetails' }
    InternalError:
      description: Internal error
      content:
        application/problem+json:
          schema: { $ref: '#/components/schemas/ProblemDetails' }

  schemas:
    # ─── Errors ────────────────────────────────────────────────────────
    ProblemDetails:
      type: object
      description: RFC 9457 Problem Details envelope used for every error.
      required: [type, title, status]
      properties:
        type: { type: string, format: uri, example: 'https://api.heista.co/errors/validation-error' }
        title: { type: string, example: 'Validation error' }
        status: { type: integer, example: 400 }
        detail: { type: string }
        instance: { type: string, format: uri }
        request_id: { type: string, example: 'req_a1b2c3d4' }

    InsufficientCreditsProblem:
      allOf:
        - $ref: '#/components/schemas/ProblemDetails'
        - type: object
          properties:
            current_balance: { type: integer, description: 'Current balance in cents' }
            required: { type: integer, description: 'Cents required for this operation' }
            top_up_url: { type: string, format: uri, example: 'https://heista.co/api-console/billing' }
            resource: { type: string, example: 'decode' }

    SpendingCapProblem:
      allOf:
        - $ref: '#/components/schemas/ProblemDetails'
        - type: object
          properties:
            reason:
              type: string
              enum: [cap_hourly, cap_daily, cap_monthly, cap_key_daily, cap_key_monthly, circuit_open]
            top_up_url: { type: string, format: uri }

    # ─── Decode ────────────────────────────────────────────────────────
    DecodeSubmitRequest:
      type: object
      properties:
        url:
          type: string
          format: uri
          description: 'Video URL (Facebook Ad Library, TikTok, Instagram, YouTube, or direct .mp4).'
          example: 'https://www.facebook.com/ads/library/?id=12345'
        video_url:
          type: string
          format: uri
          description: 'Alias of `url`. Use either.'
        idempotency_key:
          type: string
          maxLength: 128
          description: 'Optional. Prevents duplicate charges on retries.'
      oneOf:
        - required: [url]
        - required: [video_url]

    DecodeJobQueued:
      type: object
      properties:
        id: { type: string, format: uuid }
        status: { type: string, enum: [queued] }
        estimated_seconds: { type: integer }
        created_at: { type: string, format: date-time }
        poll_url: { type: string, example: '/v1/decode/abc-123' }

    DecodeJobProcessing:
      type: object
      properties:
        id: { type: string, format: uuid }
        status: { type: string, enum: [processing] }
        estimated_seconds: { type: integer }
        created_at: { type: string, format: date-time }

    DecodeJobFailed:
      type: object
      properties:
        id: { type: string, format: uuid }
        status: { type: string, enum: [failed] }
        error:
          type: object
          properties:
            code: { type: string }
            message: { type: string }
        price: { type: number, example: 0 }
        created_at: { type: string, format: date-time }

    DecodeJobCompleted:
      allOf:
        - type: object
          properties:
            id: { type: string, format: uuid }
            status: { type: string, enum: [completed] }
            price: { type: number, description: 'USD price charged ($0.15 or $0.20).' }
            created_at: { type: string, format: date-time }
            completed_at: { type: string, format: date-time }
        - $ref: '#/components/schemas/DecodeReport'

    DecodeJobCompletedRef:
      type: object
      properties:
        id: { type: string, format: uuid }
        status: { type: string, enum: [completed] }
        deduplicated: { type: boolean, enum: [true] }
        price: { type: number, example: 0 }
        created_at: { type: string, format: date-time }
        completed_at: { type: string, format: date-time }
        poll_url: { type: string }

    DecodeJobInflightRef:
      type: object
      properties:
        id: { type: string, format: uuid }
        status: { type: string, enum: [queued, processing] }
        deduplicated: { type: boolean, enum: [true] }
        price: { type: number, nullable: true }
        created_at: { type: string, format: date-time }
        poll_url: { type: string }

    DecodeJobIdempotentRef:
      type: object
      properties:
        id: { type: string, format: uuid }
        status: { type: string }
        idempotent: { type: boolean, enum: [true] }
        price: { type: number, nullable: true }
        created_at: { type: string, format: date-time }

    DecodeReport:
      type: object
      description: 'Full 5-tab decoded report — video, transcript, patternmap, formula, beats, visual_playbook, psychology, ad_intel, total_beats.'
      additionalProperties: true

    # ─── PowerSource ───────────────────────────────────────────────────
    PowerSourceSubmitRequest:
      type: object
      properties:
        url:
          type: string
          description: 'Primary URL. Required for URL-only or full mode.'
        context_url:
          type: string
          description: 'Supplementary URL — only for docs mode. Mutually exclusive with `url`.'
        file_ids:
          type: array
          items: { type: string, format: uuid }
          maxItems: 10
          description: 'File IDs from /v1/files.'
        document_urls:
          type: array
          items: { type: string, format: uri }
          maxItems: 10
        documents_inline:
          type: array
          maxItems: 10
          items:
            type: object
            required: [filename, content_base64]
            properties:
              filename: { type: string }
              content_base64: { type: string, description: 'Max 5MB per file.' }
        force_refresh:
          type: boolean
          description: 'Bypass the 30-day brand cache. Rate-limited to once per 24h per domain.'
        webhook_url:
          type: string
          format: uri
          description: 'HTTPS URL to receive completion notification. See `webhooks.powersource_complete`.'
        idempotency_key:
          type: string
          maxLength: 128

    PowerSourceJobQueued:
      type: object
      properties:
        id: { type: string, format: uuid }
        status: { type: string, enum: [queued] }
        estimated_seconds: { type: integer }
        created_at: { type: string, format: date-time }
        poll_url: { type: string }
        warnings:
          type: array
          items: { type: string }

    PowerSourceJobProcessing:
      type: object
      properties:
        id: { type: string, format: uuid }
        status: { type: string, enum: [processing] }
        estimated_seconds: { type: integer }
        created_at: { type: string, format: date-time }
        progress:
          type: object
          description: 'Phase/scan progress with partial intelligence as agents complete.'
          additionalProperties: true

    PowerSourceJobCompleted:
      type: object
      properties:
        id: { type: string, format: uuid }
        brief_id: { type: string, format: uuid }
        status: { type: string, enum: [completed] }
        price: { type: number, description: 'USD price charged.' }
        created_at: { type: string, format: date-time }
        completed_at: { type: string, format: date-time }
        powersource: { $ref: '#/components/schemas/PowerSource' }

    PowerSourceJobCompletedRef:
      type: object
      properties:
        id: { type: string, format: uuid }
        brief_id: { type: string, format: uuid }
        status: { type: string, enum: [completed] }
        deduplicated: { type: boolean, enum: [true] }
        price: { type: number, example: 0 }
        created_at: { type: string, format: date-time }
        poll_url: { type: string }

    PowerSourceJobInflightRef:
      type: object
      properties:
        id: { type: string, format: uuid }
        status: { type: string }
        deduplicated: { type: boolean, enum: [true] }
        price: { type: number, nullable: true }
        created_at: { type: string, format: date-time }
        poll_url: { type: string }

    PowerSourceJobIdempotentRef:
      type: object
      properties:
        id: { type: string, format: uuid }
        status: { type: string }
        idempotent: { type: boolean, enum: [true] }
        price: { type: number, nullable: true }
        created_at: { type: string, format: date-time }
        poll_url: { type: string }

    PowerSourceJobFailed:
      type: object
      properties:
        id: { type: string, format: uuid }
        status: { type: string, enum: [failed] }
        error_code: { type: string }
        error_message: { type: string }

    PowerSource:
      type: object
      description: |
        14-section brand intelligence response. Each section carries a `_scope` tag: `product`, `brand`, `synthesized`, or `mixed`.
      additionalProperties: true
      properties:
        identity: { type: object }
        offer: { type: object }
        selling_points:
          type: array
          items: { type: object }
        brand_story: { type: object }
        brand_style: { type: object }
        brand_assets: { type: object }
        brand_voice: { type: string, nullable: true }
        buyer_profile: { type: object, nullable: true }
        tensions:
          type: array
          items: { type: object }
        angles: { type: object }
        emotional_arcs: { type: object, nullable: true }
        ctas: { type: object }
        proof_assets: { type: object }
        narrative: { type: object, nullable: true }

    # ─── Adscript ──────────────────────────────────────────────────────
    AdscriptRequest:
      type: object
      required: [source_id, source_type, powersource_id]
      properties:
        source_id: { type: string, maxLength: 128 }
        source_type: { type: string, enum: [decode, formula] }
        powersource_id: { type: string, maxLength: 128 }
        count: { type: integer, minimum: 1, maximum: 5 }
        script_mode: { type: string, enum: [blueprint, remix] }
        duration: { type: integer, minimum: 10, maximum: 120, description: 'Remix mode only.' }
        audience: { type: string, maxLength: 128 }
        tension: { type: string, maxLength: 256 }
        selling_points:
          type: array
          items: { type: string, maxLength: 512 }
          maxItems: 5
        voice_mode: { type: string, enum: [creator, brand] }
        idempotency_key: { type: string, maxLength: 128 }

    AdscriptResponse:
      type: object
      properties:
        status: { type: string, enum: [completed] }
        source_id: { type: string }
        source_type: { type: string }
        powersource_id: { type: string }
        scripts:
          type: array
          items: { $ref: '#/components/schemas/AdscriptOutput' }
        credits_charged: { type: integer, description: 'Total cents charged across all scripts.' }
        price: { type: number, description: 'Total USD price charged.' }
        created_at: { type: string, format: date-time }

    AdscriptOutput:
      type: object
      properties:
        index: { type: integer }
        script: { type: string }
        formula_name: { type: string }
        identity:
          type: object
          properties:
            creative_format: { type: string }
            algo_intent: { type: string }
            marketing_angle: { type: string }
            psychological_mission: { type: string }
            behavioural_role: { type: string }
        beat_count: { type: integer }
        duration_seconds: { type: number }
        credits_cost: { type: integer, description: 'Cents charged for this specific script.' }

    # ─── Files ─────────────────────────────────────────────────────────
    InlineFileUpload:
      type: object
      required: [filename, content_base64]
      properties:
        filename: { type: string, maxLength: 256 }
        content_base64: { type: string }
        mime_type: { type: string, maxLength: 128 }

    PublicFile:
      type: object
      properties:
        id: { type: string, format: uuid }
        filename: { type: string }
        mime_type: { type: string }
        size_bytes: { type: integer }
        expires_at: { type: string, format: date-time }
        created_at: { type: string, format: date-time }

    # ─── Intelligence ──────────────────────────────────────────────────
    HookIntelligenceResult:
      type: object
      properties:
        hook_type: { type: string }
        hook_type_name: { type: string }
        psychology: { type: string }
        short_def: { type: string }
        long_description: { type: string }
        corpus_count: { type: integer }
        intelligence:
          type: object
          additionalProperties: true

    Formula:
      type: object
      properties:
        id: { type: string }
        name: { type: string }
        rationale: { type: string, nullable: true }
        identity: { type: object, additionalProperties: true }
        structure:
          type: object
          properties:
            beat_count: { type: integer }
            total_duration: { type: number }
            beats:
              type: array
              items: { type: object, additionalProperties: true }
        visual_direction: { type: object, nullable: true, additionalProperties: true }
        confidence_level: { type: string, enum: [HIGH, MEDIUM, LOW] }
        confidence_score: { type: number }
        avg_active_days: { type: integer, nullable: true }
        source_ad_count: { type: integer }
        source_ads:
          type: array
          items:
            type: object
            properties:
              id: { type: string }
              url: { type: string, format: uri }
              brand: { type: string, nullable: true }
        vertical: { type: string }
        product_type: { type: string, nullable: true }

    DecodedAd:
      type: object
      properties:
        id: { type: string }
        decode_url: { type: string, format: uri }
        brand: { type: string }
        vertical: { type: string }
        marketing_angle: { type: string }
        active_days: { type: integer, nullable: true }
        duration_seconds: { type: number, nullable: true }
        classification:
          type: object
          properties:
            creative_format: { type: string }
            video_type: { type: string }
            algo_intent: { type: string }
            behavioural_role: { type: string }
            psychological_mission: { type: string }
            offer_state: { type: string }
        beat_count: { type: integer }
        beats:
          type: array
          items: { type: object, additionalProperties: true }
        beat_sequence: { type: string }

    # ─── Account ───────────────────────────────────────────────────────
    AccountBalance:
      type: object
      properties:
        balance_cents: { type: integer }
        total_topped_up_cents: { type: integer }
        total_spent_cents: { type: integer }
        month:
          type: object
          properties:
            start: { type: string, format: date-time }
            spend_cents: { type: integer }
            counts:
              type: object
              properties:
                decode: { type: integer }
                powersource: { type: integer }
                powersource_docs: { type: integer }
                powersource_full: { type: integer }
                adscript: { type: integer }
                other: { type: integer }
        pricing:
          type: object
          properties:
            decode_short_cents: { type: integer, example: 15 }
            decode_long_cents: { type: integer, example: 20 }
            powersource_url_cents: { type: integer, example: 100 }
            powersource_docs_cents: { type: integer, example: 100 }
            powersource_full_cents: { type: integer, example: 200 }
            adscript_per_script_min_cents: { type: integer, example: 2 }
            adscript_per_script_typical_max_cents: { type: integer, example: 5 }
            intelligence_reads: { type: integer, example: 0 }
            files: { type: integer, example: 0 }
        top_up_url: { type: string, format: uri }
