openapi: 3.1.0
info:
  title: Videosays API
  version: 1.0.0
  description: >-
    Create asynchronous transcription tasks from supported public video links,
    poll their status, process batches, and inspect account credit usage.
  contact:
    name: Videosays
    url: https://videosays.com/docs
  license:
    name: Proprietary
    url: https://videosays.com/terms
servers:
  - url: https://api.videosays.com
    description: Production
externalDocs:
  description: Videosays API and CLI documentation
  url: https://videosays.com/docs
tags:
  - name: Transcription
  - name: Batches
  - name: Account
security:
  - apiKey: []
paths:
  /api/v1/transcribe:
    post:
      operationId: createTranscription
      summary: Create a transcription task
      description: >-
        Accepts a supported public video link or share text and returns an
        asynchronous task. Poll the returned task ID until it reaches a terminal status.
      tags: [Transcription]
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateTranscriptionRequest'
            examples:
              tiktok:
                value:
                  input: https://www.tiktok.com/@creator/video/123456
                  options:
                    language: auto
                    sourceMode: auto
                    duplicatePolicy: reuse
      responses:
        '200':
          description: Existing completed transcript reused without another charge
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Task' }
        '202':
          description: Task accepted
          headers:
            Location:
              schema: { type: string }
            Retry-After:
              schema: { type: integer }
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/Task'
                  - type: object
                    properties:
                      success: { type: boolean, const: true }
                      retryAfterSeconds: { type: integer, example: 5 }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '402': { $ref: '#/components/responses/InsufficientCredits' }
        '422': { $ref: '#/components/responses/UnsupportedInput' }
        '409': { $ref: '#/components/responses/Conflict' }
        '429': { $ref: '#/components/responses/RateLimited' }
  /api/v1/transcribe/{taskId}:
    get:
      operationId: getTranscription
      summary: Get task status and result
      tags: [Transcription]
      parameters:
        - $ref: '#/components/parameters/TaskId'
      responses:
        '200':
          description: Current task state
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/Task'
                  - type: object
                    properties:
                      retryAfterSeconds: { type: integer, example: 5 }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '404': { $ref: '#/components/responses/NotFound' }
  /api/v1/batches:
    post:
      operationId: createBatch
      summary: Create a resumable batch
      description: Creates ordinary transcription tasks for 1 to 100 inputs.
      tags: [Batches]
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateBatchRequest'
      responses:
        '202':
          description: Batch accepted
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Batch' }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '402': { $ref: '#/components/responses/InsufficientCredits' }
        '422': { $ref: '#/components/responses/UnsupportedInput' }
        '409': { $ref: '#/components/responses/Conflict' }
        '429': { $ref: '#/components/responses/RateLimited' }
  /api/v1/batches/{batchId}:
    get:
      operationId: getBatch
      summary: Get batch status
      tags: [Batches]
      parameters:
        - $ref: '#/components/parameters/BatchId'
        - $ref: '#/components/parameters/BatchView'
      responses:
        '200':
          description: Current batch state
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Batch' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '404': { $ref: '#/components/responses/NotFound' }
  /api/v1/batches/{batchId}/continue:
    post:
      operationId: continueBatch
      summary: Continue a batch after adding credits
      tags: [Batches]
      parameters:
        - $ref: '#/components/parameters/BatchId'
        - $ref: '#/components/parameters/BatchView'
      responses:
        '200':
          description: Batch resumed
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/Batch'
                  - type: object
                    properties:
                      continuedItems: { type: integer }
                      resumedItems: { type: integer }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '402': { $ref: '#/components/responses/InsufficientCredits' }
        '404': { $ref: '#/components/responses/NotFound' }
        '429': { $ref: '#/components/responses/RateLimited' }
  /api/v1/transcribe/{taskId}/reuse-decision:
    post:
      operationId: decideTranscriptionReuse
      summary: Reuse the existing result or run a fresh transcription
      tags: [Transcription]
      parameters:
        - $ref: '#/components/parameters/TaskId'
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [decision]
              properties:
                decision: { type: string, enum: [reuse, force_new] }
      responses:
        '200': { description: Existing result selected }
        '202': { description: Fresh transcription queued }
        '409': { $ref: '#/components/responses/Conflict' }
  /api/v1/batches/{batchId}/reuse-decision:
    post:
      operationId: decideBatchReuse
      summary: Apply one duplicate decision to waiting items in a batch
      tags: [Batches]
      parameters:
        - $ref: '#/components/parameters/BatchId'
        - $ref: '#/components/parameters/IdempotencyKey'
        - $ref: '#/components/parameters/BatchView'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [decision]
              properties:
                decision: { type: string, enum: [reuse, force_new] }
      responses:
        '200': { description: Batch decisions applied }
        '409': { $ref: '#/components/responses/Conflict' }
  /api/v1/batches/{batchId}/cancel:
    post:
      operationId: cancelBatch
      summary: Cancel pending work in a batch
      tags: [Batches]
      parameters:
        - $ref: '#/components/parameters/BatchId'
        - $ref: '#/components/parameters/BatchView'
      responses:
        '200':
          description: Updated batch state
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Batch' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '404': { $ref: '#/components/responses/NotFound' }
  /api/v1/credits:
    get:
      operationId: getCredits
      summary: Get credit balance
      tags: [Account]
      responses:
        '200':
          description: Credit totals in minutes
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Credits'
        '401': { $ref: '#/components/responses/Unauthorized' }
  /api/v1/history:
    get:
      operationId: getHistory
      summary: Get transcription history
      tags: [Account]
      parameters:
        - name: page
          in: query
          schema: { type: integer, minimum: 1, default: 1 }
        - name: limit
          in: query
          schema: { type: integer, minimum: 1, maximum: 50, default: 20 }
        - name: search
          in: query
          schema: { type: string }
      responses:
        '200':
          description: Paginated task history
          content:
            application/json:
              schema:
                type: object
                required: [tasks, pagination]
                properties:
                  tasks:
                    type: array
                    items: { $ref: '#/components/schemas/Task' }
                  pagination:
                    $ref: '#/components/schemas/Pagination'
        '401': { $ref: '#/components/responses/Unauthorized' }
components:
  securitySchemes:
    apiKey:
      type: apiKey
      in: header
      name: X-API-Key
      description: Create an API key in the Videosays developer dashboard.
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: Supabase JWT
  parameters:
    IdempotencyKey:
      name: Idempotency-Key
      in: header
      required: true
      description: UUID. Same key and request replay safely; a different request returns 409.
      schema: { type: string, format: uuid }
    TaskId:
      name: taskId
      in: path
      required: true
      schema: { type: string, format: uuid }
    BatchId:
      name: batchId
      in: path
      required: true
      schema: { type: string, format: uuid }
    BatchView:
      name: view
      in: query
      required: false
      description: Use status for lightweight polling without transcript text, timeline segments, or media payloads.
      schema:
        type: string
        enum: [full, status]
        default: full
  schemas:
    CreateTranscriptionRequest:
      type: object
      required: [input]
      properties:
        input:
          type: string
          description: Supported public video URL or share text containing one.
        language:
          type: string
          deprecated: true
          description: Prefer options.language.
        options:
          type: object
          properties:
            language: { type: string, default: auto }
            sourceMode:
              type: string
              enum: [auto, asr, platform]
              default: auto
            duplicatePolicy:
              type: string
              enum: [reuse, prompt, force_new]
              default: reuse
        tracking:
          type: object
          additionalProperties: true
    CreateBatchRequest:
      type: object
      required: [items]
      properties:
        items:
          type: array
          minItems: 1
          maxItems: 100
          items: { type: string }
        options:
          type: object
          properties:
            language: { type: string, default: auto }
            sourceMode:
              type: string
              enum: [auto, asr, platform]
              default: auto
            duplicatePolicy:
              type: string
              enum: [reuse, prompt, force_new]
              default: reuse
        tracking:
          type: object
          additionalProperties: true
    Task:
      type: object
      required: [taskId, id, status, input, video, billing, result]
      properties:
        taskId: { type: string, format: uuid }
        id: { type: string, format: uuid }
        status:
          type: string
          enum: [pending, analyzing, submitting, processing, awaiting_reuse, completed, failed, cancelled]
        input: { type: string }
        video:
          type: object
          properties:
            platform: { type: [string, 'null'] }
            platformVideoId: { type: [string, 'null'] }
            inputUrl: { type: string }
            title: { type: [string, 'null'] }
            author: { type: [string, 'null'] }
            coverUrl: { type: [string, 'null'], format: uri }
            durationSeconds: { type: [number, 'null'] }
        billing:
          type: object
          properties:
            creditMinutes: { type: number }
            creditStatus: { type: string }
            reservedAt: { type: [string, 'null'], format: date-time }
            settledAt: { type: [string, 'null'], format: date-time }
            releasedAt: { type: [string, 'null'], format: date-time }
        result:
          type: object
          properties:
            text: { type: [string, 'null'] }
            segments:
              type: [array, 'null']
              items: { type: object, additionalProperties: true }
            sourceType: { type: [string, 'null'] }
            provider: { type: [string, 'null'] }
        media:
          type: [object, 'null']
          additionalProperties: true
        error:
          oneOf:
            - type: 'null'
            - $ref: '#/components/schemas/Error'
        reuse:
          type: object
          required: [state, reused, canonicalTaskId, detectedBy, decisionExpiresAt]
          properties:
            state:
              type: string
              enum: [none, awaiting_decision, redirected_active, redirected_completed, force_new, timed_out]
            reused: { type: boolean }
            canonicalTaskId: { type: [string, 'null'], format: uuid }
            detectedBy: { type: [string, 'null'], enum: [request_fingerprint, video_identity, null] }
            decisionExpiresAt: { type: [string, 'null'], format: date-time }
        createdAt: { type: [string, 'null'], format: date-time }
        startedAt: { type: [string, 'null'], format: date-time }
        completedAt: { type: [string, 'null'], format: date-time }
    Batch:
      type: object
      required: [batchId, status, summary, items]
      properties:
        batchId: { type: string, format: uuid }
        status: { type: string }
        summary:
          type: object
          additionalProperties: true
        items:
          type: array
          items:
            type: object
            additionalProperties: true
        createdAt: { type: [string, 'null'], format: date-time }
        completedAt: { type: [string, 'null'], format: date-time }
        stopReason: { type: [string, 'null'] }
        stoppedAt: { type: [string, 'null'], format: date-time }
        stoppedTaskId: { type: [string, 'null'], format: uuid }
        continuation:
          oneOf:
            - type: 'null'
            - type: object
              required: [required, reason, resumableItems]
              properties:
                required: { type: boolean, const: true }
                reason: { type: string, enum: [insufficient_credits] }
                resumableItems: { type: integer, minimum: 1 }
        retryAfterSeconds: { type: [integer, 'null'] }
    Credits:
      type: object
      required: [balance, reservedBalance, availableBalance, totalPurchased, totalUsed]
      properties:
        balance: { type: number }
        reservedBalance: { type: number }
        availableBalance: { type: number }
        totalPurchased: { type: number }
        totalUsed: { type: number }
    Pagination:
      type: object
      required: [page, limit, total, totalPages, hasMore]
      properties:
        page: { type: integer }
        limit: { type: integer }
        total: { type: integer }
        totalPages: { type: integer }
        hasMore: { type: boolean }
    Error:
      type: object
      properties:
        error: { type: string }
        code: { type: string }
        retryable: { type: boolean }
        decisionTaskId:
          type: string
          format: uuid
          description: Shadow task to use with the reuse-decision endpoint when code is duplicate_completed.
        candidate:
          $ref: '#/components/schemas/Task'
  responses:
    BadRequest:
      description: Invalid request
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }
    Unauthorized:
      description: API key or bearer token is missing or invalid
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }
    InsufficientCredits:
      description: Not enough transcription credit
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }
    UnsupportedInput:
      description: Unsupported platform or link format
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }
    NotFound:
      description: Resource not found or not owned by the authenticated user
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }
    Conflict:
      description: Idempotency conflict or duplicate decision required/unavailable
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }
    RateLimited:
      description: Too many requests
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }
