openapi: 3.1.0
info:
  title: Repackage API
  description: |
    The Repackage API exposes YouTube transcript fetching as a stateless HTTP
    surface. Authentication is via organization-scoped Bearer keys minted in
    the dashboard.

    All errors share a single envelope; check `error.code` rather than the
    human-readable message.
  version: '0.2.0'
  contact:
    email: hi@repackage.app
servers:
  - url: https://api.repackage.app
    description: Production
  - url: http://localhost:8010
    description: Local dev
tags:
  - name: Transcripts
    description: Single-video, bulk, playlist, and channel transcript endpoints.
  - name: Account
    description: Account and usage metadata.
paths:
  /transcript:
    get:
      tags: [Transcripts]
      operationId: getTranscript
      summary: Fetch transcript and metadata for a single video.
      parameters:
        - in: query
          name: id
          schema: { type: string, pattern: '^[A-Za-z0-9_-]{11}$' }
          description: 11-character YouTube video ID. One of `id` or `url` is required.
        - in: query
          name: url
          schema: { type: string }
          description: Any YouTube URL form. One of `id` or `url` is required.
      security:
        - bearerAuth: []
        - {}
      responses:
        '200':
          description: Transcript and metadata.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TranscriptResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '402':
          $ref: '#/components/responses/PlanLimitExceeded'
        '422':
          $ref: '#/components/responses/Unprocessable'
        '429':
          $ref: '#/components/responses/RateLimited'
        '502':
          $ref: '#/components/responses/UpstreamFailure'
  /transcript/raw:
    get:
      tags: [Transcripts]
      operationId: getTranscriptRaw
      summary: Same data as /transcript, returned as plain text.
      parameters:
        - in: query
          name: id
          schema: { type: string, pattern: '^[A-Za-z0-9_-]{11}$' }
        - in: query
          name: url
          schema: { type: string }
      security:
        - bearerAuth: []
        - {}
      responses:
        '200':
          description: Plain-text transcript.
          content:
            text/plain:
              schema: { type: string }
        '400':
          description: Invalid input.
          content:
            text/plain:
              schema: { type: string }
  /transcripts/bulk:
    post:
      tags: [Transcripts]
      operationId: postBulkTranscripts
      summary: Fetch transcripts for many videos in one call. Pro plan.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/BulkRequest'
      security:
        - bearerAuth: []
      responses:
        '200':
          description: Per-video results plus a summary.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/BatchResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '402':
          $ref: '#/components/responses/PlanLimitExceeded'
        '403':
          $ref: '#/components/responses/FeatureNotInPlan'
  /playlists/{id}/transcripts:
    get:
      tags: [Transcripts]
      operationId: getPlaylistTranscripts
      summary: Fetch transcripts for every video in a playlist. Pro plan.
      parameters:
        - in: path
          name: id
          required: true
          schema: { type: string }
          description: YouTube playlist ID or URL-encoded URL.
        - in: query
          name: limit
          schema: { type: integer, minimum: 1, maximum: 500, default: 50 }
      security:
        - bearerAuth: []
      responses:
        '200':
          description: Playlist metadata, per-video results, and a summary.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PlaylistBatchResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '402':
          $ref: '#/components/responses/PlanLimitExceeded'
        '403':
          $ref: '#/components/responses/FeatureNotInPlan'
        '404':
          $ref: '#/components/responses/NotFound'
  /channels/{id}/transcripts:
    get:
      tags: [Transcripts]
      operationId: getChannelTranscripts
      summary: Fetch transcripts for a channel's most recent uploads. Pro plan.
      parameters:
        - in: path
          name: id
          required: true
          schema: { type: string }
          description: Channel handle (@name), channel ID (UC...), or URL.
        - in: query
          name: limit
          schema: { type: integer, minimum: 1, maximum: 500, default: 25 }
      security:
        - bearerAuth: []
      responses:
        '200':
          description: Channel metadata, per-video results, and a summary.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ChannelBatchResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '402':
          $ref: '#/components/responses/PlanLimitExceeded'
        '403':
          $ref: '#/components/responses/FeatureNotInPlan'
        '404':
          $ref: '#/components/responses/NotFound'
  /me:
    get:
      tags: [Account]
      operationId: getMe
      summary: Current user and active organization.
      security: [{ bearerAuth: [] }]
      responses:
        '200':
          description: Active session.
          content:
            application/json:
              schema:
                type: object
                properties:
                  user:
                    type: object
                    properties:
                      id: { type: string, format: uuid }
                      name: { type: string }
                      email: { type: string, format: email }
                      image: { type: string, nullable: true }
                  organization:
                    type: object
                    nullable: true
                    properties:
                      id: { type: string, format: uuid }
                      name: { type: string }
                      slug: { type: string }
                      logo: { type: string, nullable: true }
                      activePlanTier:
                        type: string
                        enum: [free, pro]
  /me/usage:
    get:
      tags: [Account]
      operationId: getUsage
      summary: Current month transcript usage and feature flags.
      security: [{ bearerAuth: [] }]
      responses:
        '200':
          description: Usage state.
          content:
            application/json:
              schema:
                type: object
                properties:
                  organizationId: { type: string, format: uuid }
                  tier: { type: string, enum: [free, pro] }
                  transcripts:
                    type: object
                    properties:
                      used: { type: integer }
                      limit: { type: integer }
                      resetAt: { type: string, format: date-time }
                  features:
                    type: object
                    properties:
                      bulk: { type: boolean }
                      playlist: { type: boolean }
                      channel: { type: boolean }
                      maxBulkSize: { type: integer }
components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: rpk_
  schemas:
    Error:
      type: object
      required: [error]
      properties:
        error:
          type: object
          required: [code, message, retryable]
          properties:
            code: { type: string }
            message: { type: string }
            retryable: { type: boolean }
            limit: { type: integer }
            used: { type: integer }
            resetAt: { type: string, format: date-time }
            scope: { type: string }
            tier: { type: string }
            feature: { type: string }
    TranscriptSegment:
      type: object
      properties:
        start: { type: number }
        duration: { type: number }
        text: { type: string }
    VideoMetadata:
      type: object
      properties:
        id: { type: string }
        title: { type: string }
        description: { type: string }
        channelId: { type: string }
        channelName: { type: string }
        uploadDate: { type: string }
        duration: { type: integer }
        viewCount: { type: integer }
        likeCount: { type: integer, nullable: true }
        tags:
          type: array
          items: { type: string }
        thumbnail: { type: string }
        language: { type: string, nullable: true }
    TranscriptResponse:
      type: object
      properties:
        metadata: { $ref: '#/components/schemas/VideoMetadata' }
        transcript:
          type: object
          additionalProperties:
            type: array
            items: { $ref: '#/components/schemas/TranscriptSegment' }
        source: { type: string, enum: [innertube, ytdlp] }
        cached: { type: boolean }
    BulkRequest:
      type: object
      required: [videos]
      properties:
        videos:
          type: array
          items: { type: string }
          minItems: 1
          maxItems: 500
    BatchSummary:
      type: object
      properties:
        total: { type: integer }
        succeeded: { type: integer }
        failed: { type: integer }
        cached: { type: integer }
        fetched: { type: integer }
    BatchItem:
      oneOf:
        - type: object
          properties:
            videoId: { type: string }
            ok: { type: boolean, enum: [true] }
            metadata: { $ref: '#/components/schemas/VideoMetadata' }
            transcript:
              type: object
              additionalProperties:
                type: array
                items: { $ref: '#/components/schemas/TranscriptSegment' }
            cached: { type: boolean }
            source: { type: string, enum: [innertube, ytdlp] }
        - type: object
          properties:
            videoId: { type: string }
            ok: { type: boolean, enum: [false] }
            error: { $ref: '#/components/schemas/Error/properties/error' }
    BatchResponse:
      type: object
      properties:
        results:
          type: array
          items: { $ref: '#/components/schemas/BatchItem' }
        summary: { $ref: '#/components/schemas/BatchSummary' }
    PlaylistBatchResponse:
      allOf:
        - type: object
          properties:
            playlist:
              type: object
              properties:
                id: { type: string }
                title: { type: string }
                channelName: { type: string }
                videoCount: { type: integer }
        - $ref: '#/components/schemas/BatchResponse'
    ChannelBatchResponse:
      allOf:
        - type: object
          properties:
            channel:
              type: object
              properties:
                id: { type: string }
                name: { type: string }
                handle: { type: string, nullable: true }
        - $ref: '#/components/schemas/BatchResponse'
  responses:
    BadRequest:
      description: Bad request — invalid input.
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }
    Unauthorized:
      description: Missing or invalid credentials.
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }
    PlanLimitExceeded:
      description: Monthly transcript quota exceeded.
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }
    FeatureNotInPlan:
      description: Endpoint requires the Pro plan.
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }
    NotFound:
      description: Resource not found.
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }
    Unprocessable:
      description: Input shape is right but values aren't usable.
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }
    RateLimited:
      description: Too many requests in the current window.
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }
    UpstreamFailure:
      description: YouTube or a downstream service is unhappy. Retry with backoff.
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }
