> ## 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.

# Match music to a video

> Pass a `video_id` for a shortlist. This answers three ways and they carry different
bodies:

* `200`: the video's analysis is cached for your account's engine. The lane is inline.
* `202` with a `job_id`: the analysis is not cached, so the match runs as a job. Poll
  `GET /v1/match/jobs/{job_id}`. A video that has just turned `ready` normally takes
  this branch: ingest flips the row to `ready` before the engine analysis is persisted.
* `202` with **no** `job_id`: the video is still ingesting. Poll `GET /v1/videos/{id}`
  until `ready`, then call this endpoint again.

Passing a `video_url` instead of a `video_id` starts an ingest and returns that third
shape. There is no `job_id` on that path.




## OpenAPI

````yaml /api-reference/openapi.yaml post /v1/match
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/match:
    post:
      tags:
        - Match
      summary: Match music to a video
      description: >
        Pass a `video_id` for a shortlist. This answers three ways and they
        carry different

        bodies:


        * `200`: the video's analysis is cached for your account's engine. The
        lane is inline.

        * `202` with a `job_id`: the analysis is not cached, so the match runs
        as a job. Poll
          `GET /v1/match/jobs/{job_id}`. A video that has just turned `ready` normally takes
          this branch: ingest flips the row to `ready` before the engine analysis is persisted.
        * `202` with **no** `job_id`: the video is still ingesting. Poll `GET
        /v1/videos/{id}`
          until `ready`, then call this endpoint again.

        Passing a `video_url` instead of a `video_id` starts an ingest and
        returns that third

        shape. There is no `job_id` on that path.
      operationId: matchVideo
      parameters:
        - name: expand
          in: query
          required: false
          description: >-
            Set to `full` for the full track object plus the lane's candidate
            pool.
          schema:
            type: string
            enum:
              - full
        - name: Idempotency-Key
          in: header
          required: false
          description: >
            Suppresses a DUPLICATE BILLING ROW for a retry of this exact
            request. It does not

            return the earlier response and does not stop the match re-running.
            Use a new key

            per distinct request. A key pinned across different requests
            silently suppresses

            billing for all of them.
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/MatchRequest'
            examples:
              by_video_id:
                summary: Match an already-analyzed video
                value:
                  video_id: 69e5a58e73370adad5a10dc9
                  top_k: 5
              by_video_url:
                summary: Ingest a new video, then match it
                value:
                  video_url: https://yourcdn.com/clip.mp4
                  top_k: 5
      responses:
        '200':
          description: A ranked shortlist of matches.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/MatchResult'
        '202':
          description: >
            Either a match job (`job_id` present, poll `poll`) or a video still
            ingesting

            (`job_id` absent, poll `poll`, then call `/v1/match` again with the
            `video_id`).
          content:
            application/json:
              schema:
                oneOf:
                  - $ref: '#/components/schemas/MatchJob'
                  - $ref: '#/components/schemas/VideoJob'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '402':
          $ref: '#/components/responses/PaymentRequired'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '422':
          $ref: '#/components/responses/UnprocessableEntity'
        '429':
          $ref: '#/components/responses/RateLimited'
        '502':
          $ref: '#/components/responses/UpstreamError'
components:
  schemas:
    MatchRequest:
      type: object
      description: Provide either `video_id` or `video_url`.
      properties:
        video_id:
          type: string
          maxLength: 128
          description: An already-ingested video.
        video_url:
          type: string
          maxLength: 2048
          description: >
            A public https video URL. Ingested first, so the response is a 202
            pointing at the video, with no job_id. Share links that redirect
            (Google Drive, Dropbox, S3, most CDNs) work, and we follow the
            redirect to the file.
        top_k:
          type: integer
          minimum: 1
          maximum: 100
          default: 20
          description: >
            MAXIMUM number of matches to return. The lane is planned as a
            shortlist of up to 10, so a larger value returns everything there is
            rather than more.
        use_video_bpm:
          type: boolean
          default: true
          description: Weight matches toward the video's detected tempo.
        creative_direction:
          type: string
          maxLength: 500
          description: A plain-language brief, e.g. "warm, cinematic, no vocals".
        era:
          type: string
        mood:
          type: string
        vocals:
          type: string
        include_sync:
          type: boolean
          default: false
          description: >
            Attach `sync_status` and `sync_points` to every match. CACHE-ONLY:
            it never builds a profile, so an un-indexed track comes back
            `warming` with an empty `sync_points` array. On a freshly ingested
            video that is most of the shortlist. Cue starts a background index
            for those tracks, so a later call usually returns them `warm`. Use
            `/v1/align` when you need the index built now.
        sync_points_per_track:
          type: integer
          minimum: 1
          maximum: 12
          default: 4
          description: How many sync points to return per track when they are warm.
    MatchResult:
      type: object
      properties:
        video_id:
          type: string
        lane:
          $ref: '#/components/schemas/Lane'
      example:
        video_id: 69e5a58e73370adad5a10dc9
        lane:
          title: Your shortlist
          reasoning: >-
            Driving but unhurried — it carries the cuts without fighting the
            voice.
          matches:
            - track_id: '14792'
              audio_id: '60184'
              soundstripe_song_id: '14792'
              soundstripe_audio_file_id: '60184'
              score: 1
              bpm: 120
              instrumental: true
              duration_seconds: 164.01
            - track_id: '14857'
              audio_id: '60251'
              soundstripe_song_id: '14857'
              soundstripe_audio_file_id: '60251'
              score: 0.89
              bpm: 116
              instrumental: false
              duration_seconds: 235.38
    MatchJob:
      type: object
      description: A match running as a job. Poll `poll` until `status` is `complete`.
      properties:
        video_id:
          type: string
        job_id:
          type: string
        status:
          type: string
          example: processing
        poll:
          type: string
          example: /v1/match/jobs/run_9f2c
        next:
          type: string
          description: Present on the submit response, absent on later polls.
    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
    Lane:
      type: object
      properties:
        title:
          type:
            - string
            - 'null'
        reasoning:
          type:
            - string
            - 'null'
          description: Why this direction was chosen, in plain language.
        matches:
          type: array
          items:
            $ref: '#/components/schemas/Match'
        candidate_pool:
          type: array
          description: >-
            Present only with `?expand=full`, the wider pool the shortlist was
            drawn from.
          items:
            $ref: '#/components/schemas/Match'
    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
    Match:
      type: object
      description: >
        One matched track. The lean (default) shape is the first block of
        properties. With `?expand=full` the track object carries the descriptive
        fields instead.
      properties:
        track_id:
          type: string
          description: The matched track. Same value as `soundstripe_song_id`.
        audio_id:
          type:
            - string
            - 'null'
          description: >-
            The specific audio file for the track. Same value as
            `soundstripe_audio_file_id`.
        soundstripe_song_id:
          type:
            - string
            - 'null'
          description: >-
            The track's Soundstripe catalog id, named so it's unambiguous to
            resolve.
        soundstripe_audio_file_id:
          type:
            - string
            - 'null'
          description: The Soundstripe audio-file id, the specific render, not the work.
        score:
          type: number
          description: Match strength, 0–1.
        bpm:
          type:
            - number
            - 'null'
        instrumental:
          type:
            - boolean
            - 'null'
        duration_seconds:
          type:
            - number
            - 'null'
        sync_status:
          type: string
          enum:
            - warm
            - warming
            - unavailable
            - error
          description: >
            Present only when `include_sync` is true. `warm` = `sync_points` is
            populated. `warming` = this pairing isn't indexed yet, so
            `sync_points` is empty and you should call again shortly.
            `unavailable` = no points could be read. `error` = something failed
            on our side and a retry may succeed. `unavailable` and `error` mean
            different things: only the first is a fact about your track.
        sync_points:
          type: array
          description: >
            Present only when `include_sync` is true. Where to cut, best first.
            Empty unless `sync_status` is `warm`.
          items:
            $ref: '#/components/schemas/SyncPoint'
        title:
          type:
            - string
            - 'null'
          description: Only with `?expand=full`.
        artist_name:
          type:
            - string
            - 'null'
          description: Only with `?expand=full`.
        artist_image_url:
          type:
            - string
            - 'null'
          description: Only with `?expand=full`.
        description:
          type:
            - string
            - 'null'
          description: Only with `?expand=full`.
        energy:
          type:
            - number
            - string
            - 'null'
          description: Only with `?expand=full`.
        musical_key_name:
          type:
            - string
            - 'null'
          description: Only with `?expand=full`.
        musical_key_mode:
          type:
            - string
            - 'null'
          description: Only with `?expand=full`.
        explicit:
          type:
            - boolean
            - 'null'
          description: Only with `?expand=full`.
        matched_tags:
          type: array
          items:
            type: string
          description: Only with `?expand=full`.
        fit_explanation:
          type:
            - string
            - 'null'
          description: Only with `?expand=full`.
        match_map:
          type: array
          description: >-
            Only with `?expand=full` on a match. Always present on an underscore
            result.
          items:
            $ref: '#/components/schemas/MatchMapLink'
        preview:
          $ref: '#/components/schemas/TrackPreview'
    SyncPoint:
      type: object
      description: A moment where the music lands with the video.
      properties:
        offset_seconds:
          type: number
          description: Seconds into the song that sits under the start of the video.
        score:
          type: number
          description: Strength of the hit, 0–1.
        label:
          type:
            - string
            - 'null'
          description: >
            What lands here, e.g. "Drop", "Phrase", "Downbeat". Null on a track
            that could only be aligned by energy, and an empty string for a
            mid-song beat-sync point.
    MatchMapLink:
      type: object
      description: One "why it fits" link, a script phrase tied to a sonic trait.
      properties:
        video:
          type: string
          description: A short phrase from the script (the "in the script" side).
        audio:
          type: string
          description: A trait of how the track sounds (the "in the track" side).
        score:
          type: number
          description: Link strength, 0–1.
        category:
          type: string
    TrackPreview:
      type: object
      description: >
        A signed, directly playable link to the track's full watermarked MP3.
        **Only present on accounts granted `preview`**. The default is to attach
        no audio at all, because the only audio Cue can serve is watermarked and
        watermarked audio can't go into a finished video. When present it's
        minted per request with a short TTL: play it or fetch it, don't store
        the URL. Re-request to get a fresh one.
      properties:
        url:
          type: string
          format: uri
        expires_at:
          type: string
          format: date-time
          description: When THIS link stops working.
        format:
          type: string
          example: mp3
        duration_seconds:
          type:
            - number
            - 'null'
  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'
    Forbidden:
      description: >-
        The key can't access this resource, the video or job belongs to another
        account.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
    NotFound:
      description: No such video or job.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
    UnprocessableEntity:
      description: >
        A field is invalid (then `error.details` lists which), or the video
        failed to process

        and should be re-submitted.
      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'
    UpstreamError:
      description: The work itself failed. Retry, and contact us if it persists.
      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_...`'

````