openapi: 3.1.0
info:
  title: Media Caption Public API
  version: 1.0.0
  description: |
    Public API for reading account and credit balance details, fetching YouTube
    transcripts, creating bulk transcript jobs, polling jobs, retrieving retained
    transcriptions, and receiving job-level webhooks.
  contact:
    name: Media Caption
    url: https://mediacaption.io
  license:
    name: Proprietary
    url: https://mediacaption.io/terms
servers:
  - url: https://api.mediacaption.io/v1
    description: Production
security:
  - bearerApiKey: []
  - headerApiKey: []
tags:
  - name: User
    description: Authenticated account endpoints.
  - name: Balance
    description: Authenticated account credit balance endpoints.
  - name: Transcripts
    description: Synchronous and async public YouTube transcript endpoints.
  - name: Transcriptions
    description: Retained transcript lookup endpoints.
  - name: Jobs
    description: Async job status and item result endpoints.
  - name: Webhooks
    description: Job-level webhook event contracts.
paths:
  /user:
    get:
      tags:
        - User
      summary: Fetch the authenticated user
      description: Returns account identity and subscription state for the authenticated account. This endpoint does not consume credits.
      operationId: getUser
      security:
        - bearerApiKey: []
        - headerApiKey: []
      responses:
        "200":
          description: User found
          headers:
            X-Request-Id:
              $ref: "#/components/headers/XRequestId"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/UserResponse"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "429":
          $ref: "#/components/responses/RateLimited"
        "500":
          $ref: "#/components/responses/InternalError"
  /balance:
    get:
      tags:
        - Balance
      summary: Fetch the authenticated user's credit balance
      description: Returns the current credit balance for the authenticated account. This endpoint does not consume credits.
      operationId: getBalance
      security:
        - bearerApiKey: []
        - headerApiKey: []
      responses:
        "200":
          description: Balance found
          headers:
            X-Request-Id:
              $ref: "#/components/headers/XRequestId"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/BalanceResponse"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "429":
          $ref: "#/components/responses/RateLimited"
        "500":
          $ref: "#/components/responses/InternalError"
  /transcripts:
    post:
      tags:
        - Transcripts
      summary: Fetch one public YouTube transcript
      description: Synchronously fetches a public YouTube transcript, reserves 1 credit before processing, and charges 1 credit on success.
      operationId: createTranscript
      security:
        - bearerApiKey: []
        - headerApiKey: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/TranscriptRequest"
            examples:
              url:
                value:
                  url: https://www.youtube.com/watch?v=dQw4w9WgXcQ
                  language: en
              videoId:
                value:
                  videoId: dQw4w9WgXcQ
      responses:
        "200":
          description: Transcript fetched
          headers:
            X-Request-Id:
              $ref: "#/components/headers/XRequestId"
            X-RateLimit-Limit:
              $ref: "#/components/headers/XRateLimitLimit"
            X-RateLimit-Remaining:
              $ref: "#/components/headers/XRateLimitRemaining"
            X-RateLimit-Reset:
              $ref: "#/components/headers/XRateLimitReset"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/TranscriptResponse"
        "400":
          $ref: "#/components/responses/InvalidRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "402":
          $ref: "#/components/responses/InsufficientCredits"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "429":
          $ref: "#/components/responses/RateLimited"
        "500":
          $ref: "#/components/responses/InternalError"
  /transcripts/bulk:
    post:
      tags:
        - Transcripts
        - Jobs
      summary: Create a bulk public transcript job
      description: Creates an async job for 1 to 100 public YouTube transcript lookups. The API reserves 1 credit per item when the job is accepted and settles credits after all items finish.
      operationId: createBulkTranscriptJob
      security:
        - bearerApiKey: []
        - headerApiKey: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/BulkTranscriptRequest"
      responses:
        "202":
          description: Job accepted
          headers:
            Location:
              description: Relative URL for polling the accepted job.
              schema:
                type: string
                example: /v1/jobs/job_01jz7m91bxkpbqha0wt8whm76r
            X-Request-Id:
              $ref: "#/components/headers/XRequestId"
            X-RateLimit-Limit:
              $ref: "#/components/headers/XRateLimitLimit"
            X-RateLimit-Remaining:
              $ref: "#/components/headers/XRateLimitRemaining"
            X-RateLimit-Reset:
              $ref: "#/components/headers/XRateLimitReset"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/BulkTranscriptAcceptedResponse"
        "400":
          $ref: "#/components/responses/InvalidRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "402":
          $ref: "#/components/responses/InsufficientCredits"
        "429":
          $ref: "#/components/responses/ConcurrentJobLimitExceeded"
        "500":
          $ref: "#/components/responses/InternalError"
  /transcriptions/{id}:
    get:
      tags:
        - Transcriptions
      summary: Fetch a retained transcription
      description: Fetches a completed API transcription by account-owned transcription access ID. Transcript bodies are retained for 3 days after completion.
      operationId: getTranscription
      security:
        - bearerApiKey: []
        - headerApiKey: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            pattern: ^tr_[0-9a-fA-F-]{36}$
          example: tr_3f6e5c6a-0d31-4f8c-9b1d-7f3b9e6a2c11
      responses:
        "200":
          description: Transcription found
          headers:
            X-Request-Id:
              $ref: "#/components/headers/XRequestId"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Transcription"
        "400":
          $ref: "#/components/responses/InvalidRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "410":
          $ref: "#/components/responses/TranscriptionExpired"
        "429":
          $ref: "#/components/responses/RateLimited"
        "500":
          $ref: "#/components/responses/InternalError"
  /jobs/{id}:
    get:
      tags:
        - Jobs
      summary: Fetch a bulk job
      description: Returns job status, progress, credit totals, item summaries, and a cursor for additional items.
      operationId: getJob
      security:
        - bearerApiKey: []
        - headerApiKey: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            pattern: ^job_[A-Za-z0-9_-]+$
          example: job_01jz7m91bxkpbqha0wt8whm76r
        - name: limit
          in: query
          required: false
          schema:
            type: integer
            minimum: 1
            maximum: 100
            default: 100
        - name: cursor
          in: query
          required: false
          schema:
            type: string
            description: Opaque cursor returned as nextCursor from the previous response.
      responses:
        "200":
          description: Job found
          headers:
            X-Request-Id:
              $ref: "#/components/headers/XRequestId"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Job"
        "400":
          $ref: "#/components/responses/InvalidRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "429":
          $ref: "#/components/responses/RateLimited"
        "500":
          $ref: "#/components/responses/InternalError"
webhooks:
  jobCompleted:
    post:
      tags:
        - Webhooks
      summary: job.completed
      operationId: receiveJobCompletedWebhook
      description: Delivered once after a bulk job completes successfully.
      requestBody:
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/WebhookEvent"
  jobCompletedWithErrors:
    post:
      tags:
        - Webhooks
      summary: job.completed_with_errors
      operationId: receiveJobCompletedWithErrorsWebhook
      description: Delivered once after a bulk job completes with at least one failed item.
      requestBody:
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/WebhookEvent"
  jobFailed:
    post:
      tags:
        - Webhooks
      summary: job.failed
      operationId: receiveJobFailedWebhook
      description: Delivered once after a bulk job fails.
      requestBody:
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/WebhookEvent"
components:
  securitySchemes:
    bearerApiKey:
      type: http
      scheme: bearer
      bearerFormat: Media Caption API key
      description: "Use `Authorization: Bearer mc_live_xxx`."
    headerApiKey:
      type: apiKey
      in: header
      name: X-API-Key
      description: Alternative API key header.
  headers:
    XRequestId:
      description: Request ID for troubleshooting.
      schema:
        type: string
        example: req_01jz7mb36gp7h2nm5rwd1ah4zz
    XRateLimitLimit:
      description: Maximum requests allowed in the current fixed window.
      schema:
        type: integer
        example: 60
    XRateLimitRemaining:
      description: Requests remaining in the current fixed window.
      schema:
        type: integer
        example: 58
    XRateLimitReset:
      description: ISO 8601 timestamp when the current fixed window resets.
      schema:
        type: string
        format: date-time
        example: "2026-05-11T10:01:00.000Z"
    RetryAfter:
      description: Seconds to wait before retrying.
      schema:
        type: integer
        example: 30
  responses:
    InvalidRequest:
      description: Invalid request
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorResponse"
    Unauthorized:
      description: Missing or invalid API key
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorResponse"
          examples:
            missing:
              value:
                error:
                  code: missing_api_key
                  message: Missing API key.
            invalid:
              value:
                error:
                  code: invalid_api_key
                  message: Invalid API key.
    Forbidden:
      description: Forbidden request
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorResponse"
    NotFound:
      description: Resource not found
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorResponse"
    InsufficientCredits:
      description: Not enough credits to start processing
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorResponse"
          example:
            error:
              code: insufficient_credits
              message: Insufficient credits.
    TranscriptionExpired:
      description: Transcription retention window has ended
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorResponse"
          example:
            error:
              code: transcription_expired
              message: This transcription has expired and is no longer available.
    RateLimited:
      description: Rate limit exceeded
      headers:
        Retry-After:
          $ref: "#/components/headers/RetryAfter"
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorResponse"
    ConcurrentJobLimitExceeded:
      description: Concurrent bulk job limit exceeded
      headers:
        Retry-After:
          $ref: "#/components/headers/RetryAfter"
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorResponse"
          example:
            error:
              code: concurrent_job_limit_exceeded
              message: Concurrent API job limit exceeded. Please wait for active jobs to finish.
    InternalError:
      description: Internal server error
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorResponse"
  schemas:
    UserResponse:
      type: object
      additionalProperties: false
      required:
        - user
      properties:
        user:
          $ref: "#/components/schemas/ApiUser"
    BalanceResponse:
      type: object
      additionalProperties: false
      required:
        - balance
      properties:
        balance:
          $ref: "#/components/schemas/CreditBalance"
    ApiUser:
      type: object
      additionalProperties: false
      required:
        - id
        - email
        - hasActiveSubscription
        - createdAt
      properties:
        id:
          type: string
          description: Media Caption user ID for the authenticated account.
          example: user_123
        email:
          type: string
          format: email
          description: Primary account email address.
          example: user@example.com
        hasActiveSubscription:
          type: boolean
          description: Whether the account currently has an active subscription.
        createdAt:
          type: string
          format: date-time
          description: Account creation timestamp.
    CreditBalance:
      type: object
      additionalProperties: false
      required:
        - credits
        - subscriptionCredits
        - bundleCredits
        - monthlyCreditLimit
        - monthlyCreditsRenewAt
      properties:
        credits:
          type: integer
          description: Total spendable credits across subscription and bundle pools.
          example: 20
        subscriptionCredits:
          type: integer
          description: Credits from the active subscription grant.
          example: 12
        bundleCredits:
          type: integer
          description: Purchased or granted credits that do not expire through subscription renewal.
          example: 8
        monthlyCreditLimit:
          type:
            - integer
            - "null"
          description: Included monthly subscription credits, or null when no active subscription limit is available.
          example: 100
        monthlyCreditsRenewAt:
          type:
            - string
            - "null"
          format: date-time
          description: Subscription credit renewal timestamp, or null when renewal details are unavailable.
          example: "2026-06-11T10:00:00.000Z"
    TranscriptRequest:
      type: object
      additionalProperties: false
      properties:
        url:
          type: string
          format: uri
          description: YouTube video URL. Required when videoId is omitted.
        videoId:
          type: string
          description: YouTube video ID. Required when url is omitted.
          example: dQw4w9WgXcQ
        language:
          $ref: "#/components/schemas/LanguageTag"
      anyOf:
        - required:
            - url
        - required:
            - videoId
    BulkTranscriptRequest:
      type: object
      additionalProperties: false
      required:
        - items
      properties:
        items:
          type: array
          minItems: 1
          maxItems: 100
          items:
            $ref: "#/components/schemas/BulkTranscriptItemRequest"
        webhookId:
          type:
            - string
            - "null"
          description: Optional active dashboard webhook ID for one final job-level event.
          example: wh_01jz7m8a3yk9pnx2g6bc8n4a8m
    BulkTranscriptItemRequest:
      type: object
      additionalProperties: false
      properties:
        externalId:
          type: string
          description: Customer-provided item ID returned in job results and webhooks.
        url:
          type: string
          format: uri
        videoId:
          type: string
        language:
          $ref: "#/components/schemas/LanguageTag"
      anyOf:
        - required:
            - url
        - required:
            - videoId
    LanguageTag:
      type: string
      pattern: ^[A-Za-z]{2,3}(?:-[A-Za-z0-9]{2,8})*$
      example: en
    TranscriptSegment:
      type: object
      additionalProperties: false
      required:
        - startSec
        - durationSec
        - text
      properties:
        startSec:
          type: number
          minimum: 0
          example: 0
        durationSec:
          type: number
          minimum: 0
          example: 2.4
        text:
          type: string
          example: Example transcript text
    TranscriptResponse:
      type: object
      additionalProperties: false
      required:
        - transcriptionId
        - source
        - language
        - durationSec
        - expiresAt
        - transcript
        - creditsFrozen
        - creditsCharged
      properties:
        transcriptionId:
          type: string
          example: tr_3f6e5c6a-0d31-4f8c-9b1d-7f3b9e6a2c11
        source:
          type: string
          enum:
            - public
        language:
          type: string
          example: en
        durationSec:
          type: number
          example: 184
        expiresAt:
          type: string
          format: date-time
        transcript:
          type: array
          items:
            $ref: "#/components/schemas/TranscriptSegment"
        creditsFrozen:
          type: integer
          example: 1
        creditsCharged:
          type: integer
          example: 1
    BulkTranscriptAcceptedResponse:
      type: object
      additionalProperties: false
      required:
        - jobId
        - type
        - status
        - itemCount
        - creditsFrozen
      properties:
        jobId:
          type: string
          example: job_01jz7m91bxkpbqha0wt8whm76r
        type:
          type: string
          enum:
            - public_transcripts_bulk
        status:
          $ref: "#/components/schemas/JobStatus"
        itemCount:
          type: integer
          example: 2
        creditsFrozen:
          type: integer
          example: 2
    Transcription:
      type: object
      additionalProperties: false
      required:
        - transcriptionId
        - source
        - status
        - language
        - sourceUrl
        - durationSec
        - expiresAt
        - createdAt
        - completedAt
        - transcript
      properties:
        transcriptionId:
          type: string
        source:
          type: string
          enum:
            - public
        status:
          type: string
          enum:
            - completed
        language:
          type: string
        sourceUrl:
          type: string
          format: uri
        durationSec:
          type: number
        expiresAt:
          type: string
          format: date-time
        createdAt:
          type: string
          format: date-time
        completedAt:
          type: string
          format: date-time
        transcript:
          type: array
          items:
            $ref: "#/components/schemas/TranscriptSegment"
    Job:
      type: object
      additionalProperties: false
      required:
        - id
        - type
        - status
        - progress
        - itemCount
        - completedCount
        - failedCount
        - creditsFrozen
        - creditsCharged
        - creditsRefunded
        - createdAt
        - startedAt
        - completedAt
        - items
        - nextCursor
      properties:
        id:
          type: string
          example: job_01jz7m91bxkpbqha0wt8whm76r
        type:
          type: string
          enum:
            - public_transcripts_bulk
        status:
          $ref: "#/components/schemas/JobStatus"
        progress:
          type: integer
          minimum: 0
          maximum: 100
        itemCount:
          type: integer
        completedCount:
          type: integer
        failedCount:
          type: integer
        creditsFrozen:
          type: integer
        creditsCharged:
          type: integer
        creditsRefunded:
          type: integer
        createdAt:
          type: string
          format: date-time
        startedAt:
          type:
            - string
            - "null"
          format: date-time
        completedAt:
          type:
            - string
            - "null"
          format: date-time
        items:
          type: array
          items:
            $ref: "#/components/schemas/JobItem"
        nextCursor:
          type:
            - string
            - "null"
    JobItem:
      type: object
      additionalProperties: false
      required:
        - externalId
        - status
        - sourceType
        - sourceUrl
        - videoId
        - language
        - transcriptionId
        - durationSec
        - creditsFrozen
        - creditsCharged
        - creditsRefunded
        - createdAt
        - completedAt
      properties:
        externalId:
          type:
            - string
            - "null"
        status:
          $ref: "#/components/schemas/JobItemStatus"
        sourceType:
          type: string
          enum:
            - youtube
        sourceUrl:
          type:
            - string
            - "null"
          format: uri
        videoId:
          type:
            - string
            - "null"
        language:
          type:
            - string
            - "null"
        transcriptionId:
          type:
            - string
            - "null"
        durationSec:
          type:
            - number
            - "null"
        creditsFrozen:
          type: integer
        creditsCharged:
          type: integer
        creditsRefunded:
          type: integer
        createdAt:
          type: string
          format: date-time
        completedAt:
          type:
            - string
            - "null"
          format: date-time
        error:
          $ref: "#/components/schemas/ItemError"
    JobStatus:
      type: string
      enum:
        - queued
        - processing
        - completed
        - completed_with_errors
        - failed
        - canceled
    JobItemStatus:
      type: string
      enum:
        - queued
        - processing
        - completed
        - failed
    WebhookEvent:
      type: object
      additionalProperties: false
      required:
        - id
        - type
        - eventId
        - eventType
        - createdAt
        - apiVersion
        - data
      properties:
        id:
          type: string
          example: evt_job_01jz7m91bxkpbqha0wt8whm76r_job.completed
        type:
          $ref: "#/components/schemas/WebhookEventType"
        eventId:
          type: string
        eventType:
          $ref: "#/components/schemas/WebhookEventType"
        createdAt:
          type: string
          format: date-time
        apiVersion:
          type: string
          example: "2026-05-11"
        livemode:
          type: boolean
          default: true
        test:
          type: boolean
          default: false
        data:
          $ref: "#/components/schemas/WebhookJobData"
    WebhookEventType:
      type: string
      enum:
        - job.completed
        - job.completed_with_errors
        - job.failed
    WebhookJobData:
      type: object
      additionalProperties: false
      required:
        - jobId
        - jobType
        - status
        - progress
        - itemCount
        - completedCount
        - failedCount
        - creditsFrozen
        - creditsCharged
        - creditsReleased
        - items
      properties:
        jobId:
          type: string
        jobType:
          type: string
          enum:
            - public_transcripts_bulk
        status:
          $ref: "#/components/schemas/JobStatus"
        progress:
          type: integer
          minimum: 0
          maximum: 100
        itemCount:
          type: integer
        completedCount:
          type: integer
        failedCount:
          type: integer
        creditsFrozen:
          type: integer
        creditsCharged:
          type: integer
        creditsReleased:
          type: integer
        items:
          type: array
          items:
            $ref: "#/components/schemas/WebhookJobItem"
    WebhookJobItem:
      type: object
      additionalProperties: false
      required:
        - itemId
        - externalId
        - status
      properties:
        itemId:
          type: integer
        externalId:
          type:
            - string
            - "null"
        status:
          $ref: "#/components/schemas/JobItemStatus"
        transcriptionId:
          type: string
        error:
          $ref: "#/components/schemas/ItemError"
    ErrorResponse:
      type: object
      additionalProperties: false
      required:
        - error
      properties:
        error:
          type: object
          additionalProperties: false
          required:
            - code
            - message
          properties:
            code:
              $ref: "#/components/schemas/ErrorCode"
            message:
              type: string
    ItemError:
      type: object
      additionalProperties: false
      required:
        - code
        - message
      properties:
        code:
          type: string
          example: transcript_unavailable
        message:
          type: string
          example: No public transcript is available for this video.
    ErrorCode:
      type: string
      enum:
        - concurrent_job_limit_exceeded
        - geo_restricted
        - invalid_api_key
        - invalid_request
        - insufficient_credits
        - internal_error
        - missing_api_key
        - not_found
        - public_api_rate_limited
        - single_transcript_rate_limited
        - transcription_expired
        - transcript_unavailable
        - video_unavailable
        - webhook_not_found
        - youtube_blocked
        - youtube_rate_limited
