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

# Render loops under your own license

> Cue returns the timings. You hold the recording and render it yourself.

If you already license the catalog, Cue does not need to send you audio. It sends you the
plan: which ranges of the song to play, in what order, and how to join them. You resolve the
file through your own catalog access and render locally.

This is the same loop plan every account gets. The difference is what is *not* in it. See
[Preview audio](#preview-audio) below.

## If you already know the track

`track_id` **is** the Soundstripe song id. Pass one straight from your own catalog. There is no
need to call `/v1/match` first. Matching exists for picking a track when you do not know which one
you want. If you do, go straight to loop.

```bash theme={null}
curl https://cue.vibeset.ai/v1/loop \
  -H "x-api-key: $CUE_KEY" -H "content-type: application/json" \
  -d '{"video_id": "'$VIDEO_ID'", "track_id": "14792"}'
```

The first call for a given `(video_id, track_id)` pair builds the loop profile off the request
path. It returns `202` with a `job_id` and a `poll` of `/v1/loop/jobs/{job_id}`, and no plan. Poll
that until `status` is `complete`. Every later call for that same pair returns `200` with the plan
immediately.

The pair is what the cache is keyed on. Looping a track you have already built over a *different*
video is a cold call again, at the cold rate, so budget by pairing rather than by track.

Over MCP the same two steps are `loop_music` then `check_job(job_id, "loop")`.

## What the response contains

`POST /v1/loop` returns timings, never audio.

```json 200 theme={null}
{
  "video_id": "69e5a58e73370adad5a10dc9",
  "track_id": "14792",
  "track": {
    "soundstripe_song_id": "14792",
    "soundstripe_audio_file_id": "88031"
  },
  "applicable": true,
  "plan": {
    "music_in": 0.0,
    "music_out": 540.0,
    "music_end": 537.2,
    "segments": [
      { "song_from": 0.0,  "song_to": 41.4 },
      { "song_from": 12.6, "song_to": 41.4 },
      { "song_from": 12.6, "song_to": 41.4, "gain_db": -0.4 }
    ],
    "seam_fades":  [0.35, 0.35],
    "seam_curves": [0.5, 1.0]
  }
}
```

`track` identifies the recording. The two ids are not interchangeable: `soundstripe_song_id`
is the work, `soundstripe_audio_file_id` is the specific render to fetch. Both are returned in
every delivery mode, because identity is not content. Knowing which track this is tells you
nothing you could not read off a search result, and without it the plan is numbers with no
referent.

## Assembling the loop

`plan.segments` is the authoritative playback path. Play each range of the song back to back,
in the order given. Everything else shapes the joins.

<Steps>
  <Step title="Resolve the audio">
    Fetch the file for `track.soundstripe_audio_file_id` through your own catalog access.
  </Step>

  <Step title="Cut the segments">
    For each entry, take the song from `song_from` to `song_to`. The first segment is usually
    the song's own intro, and later ones repeat the loop body.
  </Step>

  <Step title="Place it against the video">
    The music starts at `plan.music_in`. A segment's start in video time is `music_in` plus the
    total length of every segment before it. In the example above, segment 2 starts at
    `0.0 + 41.4 = 41.4s`.
  </Step>

  <Step title="Crossfade each splice">
    `seam_fades[i]` is the crossfade length in seconds for the join between segment `i` and
    `i+1`. `seam_curves[i]` is the gain exponent for that same join, anywhere from `0.5`
    (equal power, for this song's weakest seam) to `1.0` (linear, for its best). Values in
    between are real and should be applied as given. The arrays are parallel and one shorter
    than `segments`.
  </Step>

  <Step title="Apply per-segment gain">
    `gain_db`, when present, is a small loudness correction for that segment. With
    `gain_in_db` and `gain_glide_seconds` the segment ramps between the two instead of
    stepping.
  </Step>
</Steps>

The seam settings are not cosmetic. A splice without a crossfade clicks, and the law matters
because a good seam joins near-identical material, where equal-power summing runs about 3 dB
hot at the midpoint. That is why `seam_curves` varies per splice rather than being a constant.

## Where the music ends

`music_out` is the video's end. `music_end` is where the music resolves, which can be earlier
when the plan exits the loop into a come-down outro. `tail_seconds` is the sub-bar residual
between them. Fade out across the tail rather than cutting at `music_out`.

## When there is no loop

`applicable: false` means no loop was worth making. `hint` says why in plain language and
`retryable` says whether calling again can change it. It is `true` when the analysis timed out or
came back incomplete, and `false` when the video is simply not longer than the track.

## Preview audio

**No audio is attached to any response by default.** No `preview` object on match results, no
audio URL on a loop plan. You get catalog identifiers and timings.

This is deliberate. The only audio Cue is able to serve is watermarked, which cannot go into a
finished video, so it is never a deliverable. The identifiers in `track` are how you reach the
real file, under whatever catalog access you hold.

If you are evaluating tracks and don't yet license a catalog, ask us to enable `preview` on your
account. That attaches a short-lived watermarked link for judging a track before licensing it.
It changes only what audio is attached. The plans, sync points, and metering are identical.

## The MCP server

`loop_music` returns the same plan, and its tool description carries these assembly rules so an
agent can act on the response without reading this page. One difference: over MCP the plan drops
`plan.options`, the full copies of every alternative loop region, and carries a summary of each in
`plan.alternates` instead. REST keeps `options` in full. See [MCP tools](/docs/mcp-tools).
