> ## Documentation Index
> Fetch the complete documentation index at: https://cue.vibeset.ai/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Ingest a video

> Pass a `video_url` and we fetch it for you. Or pass a `filename` to get a
presigned upload URL, then `PUT` the bytes there and call
`POST /v1/videos/{video_id}/ingest`.

This is where the per-minute analysis charge is incurred. It does not accept an
`Idempotency-Key`. The meter dedupes it on a server-generated key instead.

Only the URL's SHAPE is checked here. The fetch runs in a worker, so an oversized
source, or one that is not a video, fails long after this `202` and surfaces as
`status: failed` plus a `reason` on `GET /v1/videos/{video_id}`. There is no `413`
or `415` on this request.




## OpenAPI

````yaml /api-reference/openapi.yaml post /v1/videos
openapi: 3.1.0
info:
  title: Cue API
  version: 1.0.0
  description: >
    Match licensed music to a video, or to a script. Five things it does:
    **match** music to

    an analyzed video, **underscore** a text script with no video at all,
    **sync** a track to

    the moments in a clip, **loop** a short track across a long one, and map a
    track's

    **beats** and downbeats. Videos are ingested once and reused by match, sync
    and loop;

    underscore and beats take no video at all.


    Responses carry timings and catalog identifiers, not recordings. You resolve
    a track through

    your own catalog access and render it yourself.


    Work that is already cached answers `200`. Work that needs a first-time
    build answers `202`,

    in one of two shapes: a JOB (`job_id` plus a job URL) or a VIDEO STILL
    INGESTING (`video_id`

    and a `/v1/videos/{id}` URL, with no `job_id`). Branch on whether `job_id`
    is present.


    All requests authenticate with an API key, sent either as `Authorization:
    Bearer vbsk_...`

    or in an `x-api-key` header. Both work. Keys are created in the

    [developer portal](https://cue.vibeset.ai/developer).
servers:
  - url: https://cue.vibeset.ai
    description: Production
security:
  - bearerAuth: []
  - apiKeyAuth: []
tags:
  - name: Match
    description: Match music to a video.
  - name: Underscore
    description: Match music to a script, no video needed.
  - name: Sync
    description: Get sync points, where a track lands with a video.
  - name: Loop
    description: Loop a short track seamlessly across a long video.
  - name: Beats
    description: Map the beat and downbeat grid of any audio you can point us at.
  - name: Videos
    description: Ingest and check the status of videos.
paths:
  /v1/videos:
    post:
      tags:
        - Videos
      summary: Ingest a video
      description: >
        Pass a `video_url` and we fetch it for you. Or pass a `filename` to get
        a

        presigned upload URL, then `PUT` the bytes there and call

        `POST /v1/videos/{video_id}/ingest`.


        This is where the per-minute analysis charge is incurred. It does not
        accept an

        `Idempotency-Key`. The meter dedupes it on a server-generated key
        instead.


        Only the URL's SHAPE is checked here. The fetch runs in a worker, so an
        oversized

        source, or one that is not a video, fails long after this `202` and
        surfaces as

        `status: failed` plus a `reason` on `GET /v1/videos/{video_id}`. There
        is no `413`

        or `415` on this request.
      operationId: createVideo
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/VideoRequest'
      responses:
        '202':
          description: >
            Ingest started (`VideoJob`, poll `poll`), or an upload target was
            created

            (`UploadTarget`, PUT the bytes then call the ingest endpoint).
          content:
            application/json:
              schema:
                oneOf:
                  - $ref: '#/components/schemas/VideoJob'
                  - $ref: '#/components/schemas/UploadTarget'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '402':
          $ref: '#/components/responses/PaymentRequired'
        '429':
          $ref: '#/components/responses/RateLimited'
components:
  schemas:
    VideoRequest:
      type: object
      description: Provide either `video_url` or `filename`.
      properties:
        video_url:
          type: string
          maxLength: 2048
          description: >
            A public https video URL to fetch. Redirecting share links (Google
            Drive, Dropbox, S3, CDNs) are followed to the underlying file.
        filename:
          type: string
          maxLength: 256
          description: Ask for a presigned upload URL instead of fetching.
        content_type:
          type: string
          default: video/mp4
    VideoJob:
      type: object
      description: >-
        A video being ingested. Carries NO `job_id`. Poll `poll` until the video
        is `ready`, then call the endpoint you wanted again.
      properties:
        video_id:
          type: string
        status:
          type: string
          example: processing
        poll:
          type: string
          description: Path to poll, always `/v1/videos/{video_id}`.
        next:
          type: string
          description: >-
            What to do once the video is ready. Present on the `/v1/match`
            `video_url` branch; `POST /v1/videos` returns `video_id`, `status`
            and `poll` only.
      example:
        video_id: 69e5a58e73370adad5a10dc9
        status: processing
        poll: /v1/videos/69e5a58e73370adad5a10dc9
    UploadTarget:
      type: object
      description: A presigned upload target, returned when you pass `filename`.
      properties:
        video_id:
          type: string
        s3_key:
          type: string
        upload_url:
          type: string
        method:
          type: string
          example: PUT
        status:
          type: string
          example: awaiting_upload
        next:
          type: string
    Error:
      type: object
      description: Every `/v1` error body has this shape.
      properties:
        error:
          type: object
          properties:
            type:
              type: string
              enum:
                - invalid_request
                - authentication_error
                - payment_required
                - forbidden
                - not_found
                - conflict
                - payload_too_large
                - unsupported_media_type
                - rate_limited
                - internal_error
                - upstream_error
                - service_unavailable
                - error
              description: >-
                A stable machine-readable type. Branch on this, not on the
                message.
            message:
              type: string
              description: A human-readable explanation of what went wrong.
            request_id:
              type:
                - string
                - 'null'
              description: >-
                The same value as the `X-Request-Id` response header. Quote it
                to support.
            details:
              type: array
              description: >-
                Present only on a request-validation `422`, listing which fields
                were invalid.
              items:
                type: object
      example:
        error:
          type: invalid_request
          message: Provide either 'video_id' or 'video_url'.
          request_id: a1b2c3d4e5f6
  responses:
    BadRequest:
      description: The request was malformed.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
    Unauthorized:
      description: The key is missing, invalid, expired or revoked.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
    PaymentRequired:
      description: >
        No active payment method, the credit grant is exhausted or expired, or
        the monthly

        spend cap was reached. `error.message` names which.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
    RateLimited:
      description: Too many requests. See the `Retry-After` header.
      headers:
        Retry-After:
          description: Seconds to wait before retrying.
          schema:
            type: integer
        X-RateLimit-Limit:
          description: Requests allowed per minute.
          schema:
            type: integer
        X-RateLimit-Remaining:
          description: Requests remaining in the current window.
          schema:
            type: integer
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      description: >-
        Your API key as a Bearer token, e.g. `Authorization: Bearer
        vbsk_live_...`
    apiKeyAuth:
      type: apiKey
      in: header
      name: x-api-key
      description: 'Your API key in the x-api-key header, e.g. `x-api-key: vbsk_live_...`'

````