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

# Loop a track across a long video

> Loop a short track seamlessly across a LONG video: a downbeat-aligned, integer-bar
self-loop with musical seams and fades that covers the whole clip. Pass a `track_id`
for a video you own. Soundstripe tracks only.

The `200` is served from the loop-plan cache, which is keyed on the
`(video_id, track_id)` PAIR. The first loop of a track against a given video is
therefore a `202` + `job_id`, so poll `GET /v1/loop/jobs/{job_id}`. Looping the same
track over a second video builds a second plan.

`applicable: false` when there is nothing to loop. Then `plan.reason` says why, `hint`
explains it in words, `retryable` says whether calling again can help, and nothing is
billed.

Loop needs the video to be longer than the track by at least 30 seconds. As a guideline
it is for videos over 4 minutes. Shorter clips can play the track straight. See
`/v1/align`.




## OpenAPI

````yaml /api-reference/openapi.yaml post /v1/loop
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/loop:
    post:
      tags:
        - Loop
      summary: Loop a track across a long video
      description: >
        Loop a short track seamlessly across a LONG video: a downbeat-aligned,
        integer-bar

        self-loop with musical seams and fades that covers the whole clip. Pass
        a `track_id`

        for a video you own. Soundstripe tracks only.


        The `200` is served from the loop-plan cache, which is keyed on the

        `(video_id, track_id)` PAIR. The first loop of a track against a given
        video is

        therefore a `202` + `job_id`, so poll `GET /v1/loop/jobs/{job_id}`.
        Looping the same

        track over a second video builds a second plan.


        `applicable: false` when there is nothing to loop. Then `plan.reason`
        says why, `hint`

        explains it in words, `retryable` says whether calling again can help,
        and nothing is

        billed.


        Loop needs the video to be longer than the track by at least 30 seconds.
        As a guideline

        it is for videos over 4 minutes. Shorter clips can play the track
        straight. See

        `/v1/align`.
      operationId: loopTrack
      parameters:
        - 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 loop re-running.
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/LoopRequest'
            example:
              video_id: 69e5a58e73370adad5a10dc9
              track_id: '14792'
      responses:
        '200':
          description: 'The loop plan for the track, or an `applicable: false` refusal.'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/LoopResult'
        '202':
          description: >
            Either a cold-loop job (`job_id` present) or a video still ingesting
            (`job_id`

            absent). Poll `poll` either way.
          content:
            application/json:
              schema:
                oneOf:
                  - $ref: '#/components/schemas/LoopJob'
                  - $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'
components:
  schemas:
    LoopRequest:
      type: object
      required:
        - video_id
        - track_id
      properties:
        video_id:
          type: string
          maxLength: 128
          description: A long video you own (or a shared/seed clip).
        track_id:
          type: string
          maxLength: 128
          description: The Soundstripe track to loop, as returned by `/v1/match`.
        auto_mix:
          type: boolean
          default: false
          description: >-
            When true, an APPLICABLE loop also carries a `mix` block, the
            duck-under-voiceover spec described by the `Mix` schema. There is no
            `mix` key on a not-applicable result, or when the measurement could
            not be read.
    LoopResult:
      type: object
      properties:
        video_id:
          type: string
        track_id:
          type: string
        track:
          $ref: '#/components/schemas/TrackRef'
        applicable:
          type: boolean
          description: >-
            False when there is nothing to loop. Then `plan` explains why,
            `hint` says it in words, `retryable` says whether calling again can
            help, and nothing is billed.
        plan:
          $ref: '#/components/schemas/LoopPlan'
        hint:
          type: string
          description: >-
            Present only when `applicable` is false, a plain-language reason and
            what to do.
        retryable:
          type: boolean
          description: >-
            Present only when `applicable` is false. True for `timeout`,
            `profile_error`, `missing_profile` and `audio_unavailable`: nothing
            was cached, so calling again in a few seconds normally reads back
            work that finished in the background. False means the answer is
            settled: pick another track.
        mix:
          $ref: '#/components/schemas/Mix'
      example:
        video_id: 69e5a58e73370adad5a10dc9
        track_id: '14792'
        track:
          soundstripe_song_id: '14792'
          soundstripe_audio_file_id: '60184'
        applicable: true
        plan:
          version: 15
          applicable: true
          music_in: 0
          music_out: 540
          loop_start_a: 12.6
          loop_end_b: 41.4
          bars: 16
          beats_per_bar: 4
          repeats: 18
          seam_score: 0.94
          segments:
            - song_from: 0
              song_to: 41.4
              gain_db: 0
              gain_in_db: 0
            - song_from: 12.6
              song_to: 41.4
              gain_db: -0.4
              gain_in_db: -0.1
          seam_fades:
            - 0.31
          seam_curves:
            - 0.72
          gain_glide_seconds: 1.2
          start_fade_seconds: 0.5
          end_fade_seconds: 2
    LoopJob:
      type: object
      description: >-
        A cold loop build running as a job. Poll `poll` until `status` is
        `complete`.
      properties:
        video_id:
          type: string
        track_id:
          type: string
          description: Present on the submit response.
        job_id:
          type: string
        status:
          type: string
          example: processing
        poll:
          type: string
          example: /v1/loop/jobs/run_9f2c
        next:
          type: string
    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
    TrackRef:
      type: object
      description: >-
        The catalog identifiers for the recording a loop plan describes. The
        song is the work, the audio file is the specific render you fetch. They
        are not interchangeable.
      properties:
        soundstripe_song_id:
          type:
            - string
            - 'null'
        soundstripe_audio_file_id:
          type:
            - string
            - 'null'
          description: Resolved best-effort, null when the catalog lookup fails.
    LoopPlan:
      type: object
      description: >-
        How to lay the track under the clip. `segments` is the authoritative
        playback path. `loop_start_a` / `loop_end_b` / `repeats` describe the
        same loop but cannot express a final pass that exits into a come-down
        outro. On a not-applicable result only `applicable`, `reason` and (for
        `video_not_longer`) the three duration fields are present.
      properties:
        applicable:
          type: boolean
        reason:
          type:
            - string
            - 'null'
          description: >-
            Why a loop isn't applicable, one of `video_not_longer`,
            `low_confidence`, `no_viable_loop`, `no_loop_profile`, `timeout`,
            `profile_error`, `missing_profile`, `audio_unavailable`.
        video_seconds:
          type: number
          description: Video duration. Present on a `video_not_longer` result.
        track_seconds:
          type: number
          description: Track duration. Present on a `video_not_longer` result.
        required_excess_seconds:
          type: number
          description: >-
            How much longer than the track the video must be for loop to apply
            (30s). Present on a `video_not_longer` result. Any track shorter
            than `video_seconds - required_excess_seconds` qualifies for this
            video.
        version:
          type: integer
          description: Plan-format version. Bumping it invalidates cached plans.
        segments:
          type: array
          description: >-
            THE PLAYBACK PATH: ordered song ranges to play back to back. Video
            time is `music_in` plus the cumulative length played so far.
          items:
            $ref: '#/components/schemas/LoopSegment'
        seam_fades:
          type: array
          items:
            type: number
          description: >-
            Crossfade length in seconds, one entry per internal splice, so
            `segments.length - 1` entries. Apply it or the seam clicks.
        seam_curves:
          type: array
          items:
            type: number
          description: >-
            The gain-law exponent for the same splice, parallel to `seam_fades`.
            Runs from 0.5 (equal power, used on this song's WEAKEST seams) to
            1.0 (linear, on its best).
        gain_glide_seconds:
          type: number
          description: How long a segment takes to glide from `gain_in_db` to `gain_db`.
        music_in:
          type: number
          description: >-
            Where the music starts under the clip, in seconds (0 = the very
            start).
        music_out:
          type: number
          description: The true end of the clip, in seconds. The music covers all of it.
        window_end:
          type: number
          description: The same hard ceiling as `music_out`.
        music_end:
          type: number
          description: Where the music itself resolves, at or before `window_end`.
        tail_seconds:
          type: number
          description: >-
            The sub-bar residual between `music_end` and `window_end`, absorbed
            by the end fade.
        song_start:
          type: number
        song_duration:
          type: number
        loop_start_a:
          type: number
          description: Start of the looped body within the song, in seconds.
        loop_end_b:
          type: number
          description: End of the looped body within the song, in seconds.
        bars:
          type: integer
          description: >-
            Length of the loop body in bars, always a whole number, so the beat
            grid never breaks at a seam.
        beats_per_bar:
          type: integer
        repeats:
          type: integer
          description: How many times the body repeats to cover the clip.
        seam_score:
          type: number
          description: Seam quality of the loop, 0–1 (higher is smoother).
        seam_times:
          type: array
          items:
            type: number
          description: Where the splices land in video time, in seconds.
        sections_covered:
          type: array
          items:
            type: string
          description: Which parts of the song the plan uses, e.g. Intro, Peak, Outro.
        end_label:
          type: string
          description: How the plan ends, in words.
        outro_start:
          type: number
          description: Present only when the plan resolves on a come-down outro.
        outro_end:
          type: number
          description: Present only when the plan resolves on a come-down outro.
        start_fade_seconds:
          type: number
        end_fade_seconds:
          type: number
        plan_score:
          type: number
          description: Internal ranking score for the chosen plan. Informational.
        options:
          type: array
          description: >-
            Up to four self-contained alternate plans, best first, with the
            served plan as `options[0]`. Each carries its own segments and
            window.
          items:
            type: object
        alternates:
          type: array
          description: A compact summary of the other options. Superseded by `options`.
          items:
            type: object
    Mix:
      type: object
      description: >-
        Auto-mix (duck-the-music-under-the-voiceover) spec, returned when
        `auto_mix` is true. These are the same numbers the Cue app's Loop/Sync
        preview + export use, so a render with them matches the app. Applicable
        to whatever music you lay under the clip.
      properties:
        voiceover:
          type: boolean
          description: >-
            True when the video has a sustained, measured voiceover to duck
            under.
        classification:
          type: string
          description: >-
            The video's own audio, classified, `voice` / `ambient` / `silent` /
            `none`.
        music_gain_db:
          type: number
          description: >-
            Suggested gain (dB) to sit the music under the voice. Present only
            when `voiceover` is true.
        video_gain_db:
          type: number
          description: >-
            Suggested gain (dB) to bring the footage voice up. Present only when
            `voiceover` is true.
        duck:
          type:
            - object
            - 'null'
          description: >-
            How to duck the music during speech. Null when there's no voiceover.
            Apply in this order: footage audio at `video_gain_db`, then music at
            `music_gain_db`, then `envelope_points` as further attenuation on
            the music only, then sum and limit.
          properties:
            envelope_points:
              type: array
              description: >-
                THE AUTHORITATIVE CURVE: `[[t_seconds, gain_db], ...]`, the same
                automation Cue's own renderer replays. Each `gain_db` is a
                POSITIVE amount to pull the music DOWN at that instant: `0` is
                untouched, `20` is 20 dB below where `music_gain_db` left it.
                INTERPOLATE LINEARLY between points. That reproduces the curve
                exactly. Points are emitted only where the curve moves, so held
                passages cost nothing.
              items:
                type: array
                items:
                  type: number
            envelope_hz:
              type: integer
              description: >-
                The grid the points were sampled on (the curve's resolution, not
                its length). You do not need it to render, because the points
                carry their own timestamps.
            speech_ranges:
              type: array
              description: >-
                Voiceover spans `[start, end]` in seconds where the music should
                duck.
              items:
                type: array
                items:
                  type: number
            depth_db:
              type: number
              description: >-
                NOMINAL full-depth excursion, as a POSITIVE magnitude in dB.
                Ramp lengths are computed per gap, so this describes a
                full-depth move and nothing else. A short pause gets a
                shallower, faster one. Rendering from this instead of
                `envelope_points` gives an audibly cruder mix than Cue's.
            attack_ms:
              type: integer
              description: >-
                NOMINAL fade-down time entering a speech span, in ms. See
                `depth_db`.
            release_ms:
              type: integer
              description: >-
                NOMINAL fade-up time leaving a speech span, in ms. See
                `depth_db`.
      example:
        voiceover: true
        classification: voice
        music_gain_db: -4.4
        video_gain_db: 11.4
        duck:
          envelope_points:
            - - 0
              - 20
            - - 1.2
              - 20
            - - 1.9
              - 0
            - - 4.8
              - 0
          envelope_hz: 40
          speech_ranges:
            - - 1.2
              - 4.8
            - - 9.5
              - 14.1
          depth_db: 20
          attack_ms: 375
          release_ms: 1425
    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
    LoopSegment:
      type: object
      description: One song range to play, in order.
      properties:
        song_from:
          type: number
          description: Start of the range within the song, in seconds.
        song_to:
          type: number
          description: End of the range within the song, in seconds.
        gain_db:
          type: number
          description: Loudness-match gain for this segment, so seams don't step in level.
        gain_in_db:
          type: number
          description: >-
            The gain this segment STARTS at, gliding to `gain_db` over
            `gain_glide_seconds`. Equal to `gain_db` when there is nothing to
            smooth.
  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'
  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_...`'

````