Recoup

Get started with Recoup.

API REFERENCE

Find song moments

On this page
POST/api/songs/moments

Finds a song's sections and its best short moments for licensing and editing, with the measurements an edit or a sync pitch needs. Times and numbers come from measuring the audio itself (the beat grid, loudness, which passages repeat), not from a language model. An audio model then listens to a clip of each moment and adds a short description.

Send audio_url (MP3, WAV or FLAC). Optionally send section_order, the section headers from a lyric sheet in order, to name the sections from the sheet, and tempo_hint, the tempo in BPM from your metadata, to correct a beat tracker that locked onto half or double time. describe chooses which moments get a description: true (the default) for every moment, false for none, or a list of moment types.

The response has the song's sections, its moments with 6, 15 and 30 second cuts on the beat grid, and a per-second energy_curve. It also carries an extended measurement: loudness and peaks (lufs, true_peak_dbtp, broadcast_ready), the stereo image, a seven-band spectrum (band_profile), a six-axis sound fingerprint, key, time_signature, valence and arousal, mute_drops, a chord_loop estimate and typed edit_points. Store measured_with with each result: it changes whenever the measurement's results would change. The fingerprint formulas and the band reference split were calibrated on a small sample of songs, so treat them as comparable numbers within a version, not as norms.

Pricing: $0.01 per song measured, plus the analyze rate ($0.05 per model call plus $0.001166 per second of inference time) for each moment described. The balance must hold $0.01 plus $0.05 for every moment that may be described before anything runs, which is $0.36 with the default describe: true, otherwise the response is 402. A moment whose description fails comes back without a description and is not charged. If the measurement itself fails, nothing is charged.

A request can take a few minutes when the measurement and model services are starting cold, up to a limit of 300 seconds.

Authentication

x-api-key in header

bearerAuth bearer

Request

cURL
curl --request POST \
  --url 'https://api.recoupable.dev/api/songs/moments' \
  --header 'x-api-key: YOUR_API_KEY' \
  --header 'Content-Type: application/json' \
  --data '{
  "audio_url": "https://example.com/song.mp3",
  "section_order": [
    "Intro",
    "Verse 1",
    "Chorus",
    "Verse 2",
    "Chorus",
    "Outro"
  ],
  "tempo_hint": 120
}'

Try it

Fill in the fields, send the request from your browser, and read the live response. The curl below updates as you type.

Kept in this browser tab only and cleared when it closes.

cURL for this request
curl --request POST \
  --url 'https://api.recoupable.dev/api/songs/moments' \
  --header 'Content-Type: application/json' \
  --data '{
  "audio_url": "https://example.com/song.mp3",
  "section_order": [
    "Intro",
    "Verse 1",
    "Chorus",
    "Verse 2",
    "Chorus",
    "Outro"
  ],
  "tempo_hint": 120
}'

Request body required

The audio to measure, with optional section names, a tempo hint and the moments to describe

application/json

audio_urlstringrequired

Public URL to an audio file (MP3, WAV or FLAC). The first 15 minutes are measured, and the file can be up to 100 MB. Verified before anything runs: the URL must answer a HEAD request (or a ranged GET) with 2xx within 10 seconds and an audio/* or application/octet-stream content type, else the request returns 422 and nothing is charged.

Example: "https://example.com/song.mp3"

format: uri

section_orderarray<string>

The song's section headers in order, as a lyric sheet lists them (Intro, Verse 1, Pre-Chorus, Chorus, ...). With it, sections are named from the sheet. If the sheet does not fit the recording, sections are named from the audio instead and names_from says audio. Headers the API does not recognize are ignored.

Example: ["Intro","Verse 1","Chorus","Verse 2","Chorus","Outro"]

maxItems: 60

Item properties for section_order

string

tempo_hintnumber

The song's tempo in BPM, from your metadata. The beat tracker often locks onto a multiple of the real tempo, so its reading, or half, double, three-quarters, four-thirds, two-thirds or one and a half times of it, is used when that lands within 8% of the hint. When none does, the hint is used. tempo_note in the response says which happened.

Example: 120

minimum: 30 · maximum: 300

describeone of

Which moments to describe. true (the default) describes every moment found, false describes none, and a list of moment types describes only those. A described moment adds description, emotion, best_for and vocals, and each description is charged at the analyze rate.

Default: true

Example: ["vocal_hook","climax"]

Properties for describe
oneOf · boolean 1

boolean

oneOf · array<string · enum> 2

string · enum · "intro", "vocal_hook", "chorus_lift", "build_up", "climax", "breakdown", "outro"

With a lyric sheet's section order and a tempo hint

lyricSheetAndTempo
{
  "audio_url": "https://example.com/song.mp3",
  "section_order": [
    "Intro",
    "Verse 1",
    "Chorus",
    "Verse 2",
    "Chorus",
    "Outro"
  ],
  "tempo_hint": 120
}

Measurements only, no descriptions

measureOnly
{
  "audio_url": "https://example.com/song.mp3",
  "describe": false
}

Describe two moment types

someMoments
{
  "audio_url": "https://example.com/song.mp3",
  "describe": [
    "vocal_hook",
    "climax"
  ]
}

Responses

200The song was measured. Every moment named by describe carries description, emotion, best_for and vocals; a moment whose description failed is returned without them.

application/json

statusstring · enumrequired

Request status.

Values: "success"

measured_withstringrequired

The version of the measurement that produced these numbers. Store it with the result: it changes whenever the measurement's results would change, so you know which stored results to rerun. A version ending +extended-<date> includes the extended fields below.

Example: "song-moments-2026-11-04+extended-2026-11-04"

duration_secondsnumberrequired

Length of the audio that was measured, in seconds (at most the first 15 minutes).

Example: 96

tempo_bpmnumberrequired

Tempo in beats per minute, reconciled to tempo_hint when one was sent.

Example: 120

loudness_lufsnumber | nullrequired

Integrated loudness in LUFS. The same figure as lufs.

Example: -9.4

endingstring · enumrequired

fade when the last seconds fade out by more than 10 dB, cold when the song stops.

Values: "fade", "cold"

names_fromstring · enumrequired

lyrics when section_order fit the audio and named the sections, audio when sections were named from repetition and loudness.

Values: "lyrics", "audio"

sectionsarray<SongSection>required

The song's sections in order, covering the whole song.

Item properties for sections
labelstringrequired

The section's name: intro, verse, pre-chorus, chorus, post-chorus, bridge, instrumental, drop, breakdown, outro, or section when nothing fits better.

Example: "chorus"

startnumberrequired

Start, in seconds from the beginning of the song.

Example: 24

endnumberrequired

End, in seconds from the beginning of the song.

Example: 44

groupstringrequired

Sections that share a letter sound alike (A, B, A, C, ...), so repeated choruses share a group.

Example: "C"

energyintegerrequired

Average loudness of the section, 0 to 100 relative to the song's own quietest and loudest seconds.

Example: 83

minimum: 0 · maximum: 100

momentsarray<SongMoment>required

The song's best short moments for licensing and editing, in order of start time.

Item properties for moments
typestring · enumrequired

What the moment is. A song has at most one moment of each type, and not every song has every type.

Values: "intro", "vocal_hook", "chorus_lift", "build_up", "climax", "breakdown", "outro"

startnumberrequired

Start, in seconds from the beginning of the song.

Example: 24

endnumberrequired

End, in seconds from the beginning of the song.

Example: 39

sectionstringrequired

The label of the section the moment starts in.

Example: "chorus"

energyintegerrequired

Average loudness of the moment, 0 to 100 relative to the song's own quietest and loudest seconds.

Example: 87

minimum: 0 · maximum: 100

cutsobjectrequired

Edit points for each standard length that fits inside the song, keyed 6, 15 and 30 (seconds). Each value is [start, end] in seconds on the beat grid. A cut starts at the moment's start, except for build_up and outro, whose cuts end at the moment's end. A cut can run past the end of a short moment.

Example: {"6":[24,30],"15":[24,39],"30":[24,54]}

alsoarray<string · enum>

Other moment types this one also covers. Moments that overlap by more than 60% are merged, and the one that is kept lists the others here. Present only when moments were merged.

Example: ["chorus_lift"]

Item properties for also

string · enum · "intro", "vocal_hook", "chorus_lift", "build_up", "climax", "breakdown", "outro"

descriptionstring | null

One sentence on what happens musically in the moment, written after listening to a clip of just that moment. Present only on described moments (see describe); null when the reply could not be read.

emotionarray<string>

Up to three lowercase mood words. Present only on described moments; empty when the reply could not be read.

Item properties for emotion

string

best_forarray<string>

Up to three uses the moment suits, such as film trailer, TV promo, ad, social video, video game, sports, documentary or end credits. Present only on described moments; empty when the reply could not be read.

Item properties for best_for

string

vocalsboolean | null

Whether the moment has vocals. Present only on described moments; null when the reply could not be read.

energy_curvearray<integer>required

Loudness for each second of the song, 0 to 100 relative to the song's own quietest and loudest seconds.

Item properties for energy_curve

integer

tempo_notestring

How tempo_bpm was decided: beat tracker alone, or reconciled to, or overruled by, tempo_hint.

Example: "beat tracker 60 reconciled to hint 120"

lufsnumber | null

Integrated loudness of the stereo file, in LUFS. When the loudness meter cannot read the file, an estimate from the mono mix is used instead.

Example: -9.4

true_peak_dbtpnumber | null

True peak in dBTP. null when the loudness meter could not read the file.

Example: -1.3

sample_peak_dbfsnumber | null

Highest sample level, in dBFS.

Example: -1.62

crest_factor_dbnumber | null

Highest sample peak minus the average (RMS) level, in decibels. A low value means a heavily compressed master.

Example: 9.8

loudness_range_lunumber | null

Loudness range in LU, the spread between the quieter and louder parts. null when the loudness meter could not read the file.

Example: 5.1

broadcast_readyboolean | null

true when the true peak is at or under -1 dBTP and the integrated loudness is between -16 and -7 LUFS. null when the true peak is unknown.

Example: true

stereoobject

The stereo image.

Properties for stereo
lr_correlationnumber | null

Correlation between the left and right channels, -1 to 1. 1 means identical channels.

Example: 0.842

side_to_midnumber | null

Side (left minus right) level over mid (left plus right) level. 0 is mono; higher is wider.

Example: 0.31

spectral_centroid_hznumber | null

Mean spectral centroid in hertz, a measure of brightness.

Example: 2410

rolloff95_hznumber | null

The frequency in hertz under which 95% of the spectral energy lies.

Example: 6931

band_profilearray<SongBand>

Seven frequency bands from sub to air, with each band's share of the energy, its side energy and its level against a typical pop split.

Item properties for band_profile
bandstring · enumrequired

Band name.

Values: "sub", "low", "low_mid", "mid", "high_mid", "high", "air"

lo_hzintegerrequired

Lower edge of the band in hertz.

Example: 60

hi_hzintegerrequired

Upper edge of the band in hertz.

Example: 250

share_pctnumberrequired

The band's share of the mix's energy, in percent. The seven shares add up to about 100.

Example: 31

side_pctnumberrequired

Side (left minus right) energy in the band, as a percent of the band's mid energy.

Example: 3.4

level_pctintegerrequired

The band's level against a typical pop energy split, 0 to 100. 50 is on the reference.

Example: 51

minimum: 0 · maximum: 100

statusstring · enumrequired

clear when the band is within 4 dB of the reference split, heavy when it is more than 4 dB above, light when it is more than 4 dB below.

Values: "clear", "heavy", "light"

fingerprintobject

Six sound axes. Each runs from 0 at its left label to 1 at its right label.

Properties for fingerprint
organic_electronicnumber

0 organic, 1 electronic.

minimum: 0 · maximum: 1

sparse_densenumber

0 sparse, 1 dense.

minimum: 0 · maximum: 1

dark_brightnumber

0 dark, 1 bright.

minimum: 0 · maximum: 1

intimate_anthemicnumber

0 intimate, 1 anthemic.

minimum: 0 · maximum: 1

smooth_texturednumber

0 smooth, 1 textured.

minimum: 0 · maximum: 1

slow_fastnumber

0 slow, 1 fast.

minimum: 0 · maximum: 1

fingerprint_inputsobject

The raw measurements the fingerprint is built from.

Properties for fingerprint_inputs
flatnessnumber

Mean spectral flatness: higher is noisier or more electronic.

Example: 0.0213

contrast_dbnumber

Mean spectral contrast in decibels.

Example: 20.4

zcr_stdnumber

Spread of the zero-crossing rate, a measure of textural change.

Example: 0.0412

perc_rationumber

Share of the energy that is percussive, 0 to 1.

Example: 0.318

onsets_per_snumber

Note and hit onsets per second.

Example: 4.11

keyobject

The song's key.

Properties for key
tonicstring · enum

The key's root note.

Values: "C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B"

Example: "A"

modestring · enum

Values: "major", "minor"

Example: "minor"

confidencenumber | null

How closely the song's pitch content matches the chosen key, as a correlation up to 1. Higher is more certain.

Example: 0.81

time_signaturestring · enum

The meter, from how beat strength repeats.

Values: "3/4", "4/4"

valencenumber

The measured half of mood, 0 (dark) to 1 (bright), from mode and brightness. An estimate, not a listening judgment.

Example: 0.37

minimum: 0 · maximum: 1

arousalnumber

How energetic the song is, 0 (calm) to 1 (intense), from loudness and tempo. An estimate, not a listening judgment.

Example: 0.72

minimum: 0 · maximum: 1

mute_dropsarray<SongDrop>

Sudden drops in level, in order of start time. Local drops are found against the preceding eight beats and whole-song breakdowns against the song's median level. None start in the first eight beats, inside the fade, or in the last three seconds.

Item properties for mute_drops
typestring · enumrequired

mute is a sudden drop in level shorter than two beats, a clean place for a zero-bleed cut. breakdown is a drop of two beats or more, the band pulling back.

Values: "mute", "breakdown"

startnumberrequired

Start, in seconds.

Example: 52

endnumberrequired

End, in seconds.

Example: 56

beatsnumberrequired

Length in beats.

Example: 8

depth_dbnumberrequired

How far the level falls below the surrounding music, in decibels.

Example: 11.3

mmssstringrequired

The start as m:ss.

Example: "0:52"

chord_loopobject

The most common four-bar chord cycle, from one chord per bar chosen among the key's own chords. Treat it as one estimate beside another source, not a transcription: confidence is always low.

Properties for chord_loop
dominant_loopstring

The cycle as four chord names, for example Am F C G.

Example: "Am F C G"

loop_sharenumber

The share of four-bar windows that match the cycle, 0 to 1.

Example: 0.52

distinct_chordsinteger

How many different chords appear.

Example: 5

barsinteger

How many bars were analyzed.

Example: 47

confidencestring · enum

Always low.

Values: "low"

edit_pointsarray<SongEditPoint>

Typed places to cut into (in) or out of (out) the song, in order of time: the cold open, chorus entries and exits, the deepest mutes, breakdowns and their re-entry, and where a fade begins.

Item properties for edit_points
typestring · enumrequired

in is a place to cut into the song, out a place to cut out of it.

Values: "in", "out"

timenumberrequired

The point, in seconds.

Example: 24

namestringrequired

What the point is, for example Chorus drop, Breakdown or Fade begins.

Example: "Chorus drop"

notestringrequired

A short note on how to use the point.

Example: "Full entry into the chorus after the verse."

mmssstringrequired

The point as m:ss.

Example: "0:24"

elapsed_secondsnumberrequired

Seconds the whole request took, descriptions included.

Example: 41.62

format: float

A 96-second song measured with the first request example above

lyricSheetAndTempo
{
  "status": "success",
  "measured_with": "song-moments-2026-11-04+extended-2026-11-04",
  "duration_seconds": 96,
  "tempo_bpm": 120,
  "loudness_lufs": -9.4,
  "ending": "fade",
  "names_from": "lyrics",
  "sections": [
    {
      "label": "intro",
      "start": 0,
      "end": 8,
      "group": "A",
      "energy": 36
    },
    {
      "label": "verse",
      "start": 8,
      "end": 24,
      "group": "B",
      "energy": 53
    },
    {
      "label": "chorus",
      "start": 24,
      "end": 44,
      "group": "C",
      "energy": 83
    },
    {
      "label": "verse",
      "start": 44,
      "end": 60,
      "group": "B",
      "energy": 47
    },
    {
      "label": "chorus",
      "start": 60,
      "end": 84,
      "group": "C",
      "energy": 91
    },
    {
      "label": "outro",
      "start": 84,
      "end": 96,
      "group": "D",
      "energy": 17
    }
  ],
  "moments": [
    {
      "type": "intro",
      "start": 0,
      "end": 8,
      "section": "intro",
      "energy": 36,
      "cuts": {
        "6": [
          0,
          6
        ],
        "15": [
          0,
          15
        ],
        "30": [
          0,
          30
        ]
      },
      "description": "Sparse opening with a filtered pad and a soft kick pattern that builds toward the first verse.",
      "emotion": [
        "calm",
        "anticipation"
      ],
      "best_for": [
        "documentary",
        "end credits"
      ],
      "vocals": false
    },
    {
      "type": "vocal_hook",
      "start": 24,
      "end": 39,
      "section": "chorus",
      "energy": 87,
      "cuts": {
        "6": [
          24,
          30
        ],
        "15": [
          24,
          39
        ],
        "30": [
          24,
          54
        ]
      },
      "description": "The first chorus lands with a full drum kit and a layered vocal hook over a rising bass line.",
      "emotion": [
        "uplifting",
        "energetic",
        "anthemic"
      ],
      "best_for": [
        "ad",
        "social video",
        "TV promo"
      ],
      "vocals": true
    },
    {
      "type": "build_up",
      "start": 46,
      "end": 64,
      "section": "verse",
      "energy": 56,
      "cuts": {
        "6": [
          58,
          64
        ],
        "15": [
          49,
          64
        ],
        "30": [
          34,
          64
        ]
      },
      "description": "Drums drop out and a riser climbs as the vocal repeats a short phrase into the second chorus.",
      "emotion": [
        "tense",
        "driving"
      ],
      "best_for": [
        "film trailer",
        "sports"
      ],
      "vocals": true
    },
    {
      "type": "climax",
      "start": 62,
      "end": 77,
      "section": "chorus",
      "energy": 91,
      "cuts": {
        "6": [
          62,
          68
        ],
        "15": [
          62,
          77
        ],
        "30": [
          62,
          92
        ]
      },
      "also": [
        "chorus_lift"
      ],
      "description": "The second chorus returns at full volume with doubled vocals and a stacked harmony on the last line.",
      "emotion": [
        "triumphant",
        "powerful",
        "euphoric"
      ],
      "best_for": [
        "film trailer",
        "sports",
        "TV promo"
      ],
      "vocals": true
    },
    {
      "type": "outro",
      "start": 84,
      "end": 96,
      "section": "outro",
      "energy": 17,
      "cuts": {
        "6": [
          90,
          96
        ],
        "15": [
          81,
          96
        ],
        "30": [
          66,
          96
        ]
      },
      "description": "The band thins to pad and a single guitar line that fades to silence.",
      "emotion": [
        "wistful",
        "gentle"
      ],
      "best_for": [
        "end credits",
        "documentary"
      ],
      "vocals": false
    }
  ],
  "energy_curve": [
    24,
    29,
    29,
    36,
    37,
    38,
    45,
    46,
    52,
    54,
    51,
    55,
    53,
    50,
    54,
    52,
    52,
    54,
    51,
    55,
    53,
    50,
    54,
    52,
    86,
    88,
    85,
    89,
    87,
    84,
    88,
    86,
    86,
    88,
    85,
    89,
    87,
    84,
    88,
    86,
    86,
    88,
    85,
    18,
    56,
    53,
    57,
    55,
    55,
    57,
    54,
    58,
    22,
    19,
    23,
    21,
    55,
    57,
    54,
    58,
    91,
    88,
    92,
    90,
    90,
    92,
    89,
    93,
    91,
    88,
    92,
    90,
    90,
    92,
    89,
    93,
    91,
    88,
    92,
    90,
    90,
    92,
    89,
    93,
    31,
    25,
    27,
    22,
    20,
    20,
    14,
    16,
    11,
    6,
    8,
    3
  ],
  "tempo_note": "beat tracker 60 reconciled to hint 120",
  "lufs": -9.4,
  "true_peak_dbtp": -1.3,
  "sample_peak_dbfs": -1.62,
  "crest_factor_db": 9.8,
  "loudness_range_lu": 5.1,
  "broadcast_ready": true,
  "stereo": {
    "lr_correlation": 0.842,
    "side_to_mid": 0.31
  },
  "spectral_centroid_hz": 2410,
  "rolloff95_hz": 6931,
  "band_profile": [
    {
      "band": "sub",
      "lo_hz": 20,
      "hi_hz": 60,
      "share_pct": 6.5,
      "side_pct": 2.1,
      "level_pct": 32,
      "status": "light"
    },
    {
      "band": "low",
      "lo_hz": 60,
      "hi_hz": 250,
      "share_pct": 31,
      "side_pct": 3.4,
      "level_pct": 49,
      "status": "clear"
    },
    {
      "band": "low_mid",
      "lo_hz": 250,
      "hi_hz": 500,
      "share_pct": 13,
      "side_pct": 9.8,
      "level_pct": 51,
      "status": "clear"
    },
    {
      "band": "mid",
      "lo_hz": 500,
      "hi_hz": 2000,
      "share_pct": 21.5,
      "side_pct": 18.6,
      "level_pct": 54,
      "status": "clear"
    },
    {
      "band": "high_mid",
      "lo_hz": 2000,
      "hi_hz": 6000,
      "share_pct": 13.5,
      "side_pct": 31.2,
      "level_pct": 55,
      "status": "clear"
    },
    {
      "band": "high",
      "lo_hz": 6000,
      "hi_hz": 12000,
      "share_pct": 10.2,
      "side_pct": 38.7,
      "level_pct": 57,
      "status": "clear"
    },
    {
      "band": "air",
      "lo_hz": 12000,
      "hi_hz": 22050,
      "share_pct": 4.3,
      "side_pct": 44,
      "level_pct": 51,
      "status": "clear"
    }
  ],
  "fingerprint": {
    "organic_electronic": 0.36,
    "sparse_dense": 0.51,
    "dark_bright": 0.38,
    "intimate_anthemic": 0.54,
    "smooth_textured": 0.33,
    "slow_fast": 0.64
  },
  "fingerprint_inputs": {
    "flatness": 0.0213,
    "contrast_db": 20.4,
    "zcr_std": 0.0412,
    "perc_ratio": 0.318,
    "onsets_per_s": 4.11
  },
  "key": {
    "tonic": "A",
    "mode": "minor",
    "confidence": 0.81
  },
  "time_signature": "4/4",
  "valence": 0.37,
  "arousal": 0.72,
  "mute_drops": [
    {
      "type": "mute",
      "start": 43.5,
      "end": 44,
      "beats": 1,
      "depth_db": 14.2,
      "mmss": "0:43"
    },
    {
      "type": "breakdown",
      "start": 52,
      "end": 56,
      "beats": 8,
      "depth_db": 11.3,
      "mmss": "0:52"
    }
  ],
  "chord_loop": {
    "dominant_loop": "Am F C G",
    "loop_share": 0.52,
    "distinct_chords": 5,
    "bars": 47,
    "confidence": "low"
  },
  "edit_points": [
    {
      "type": "in",
      "time": 0,
      "name": "Cold open (intro)",
      "note": "Starts clean from the top.",
      "mmss": "0:00"
    },
    {
      "type": "in",
      "time": 24,
      "name": "Chorus drop",
      "note": "Full entry into the chorus after the verse.",
      "mmss": "0:24"
    },
    {
      "type": "out",
      "time": 43.5,
      "name": "Mute drop",
      "note": "14.2 dB dip for 1.0 beats; a zero-bleed edit.",
      "mmss": "0:43"
    },
    {
      "type": "out",
      "time": 44,
      "name": "Chorus cut point",
      "note": "End of the chorus before the verse; cut or dip here.",
      "mmss": "0:44"
    },
    {
      "type": "out",
      "time": 52,
      "name": "Breakdown",
      "note": "Band pulls back 11.3 dB for 4.0 s.",
      "mmss": "0:52"
    },
    {
      "type": "in",
      "time": 56,
      "name": "Re-entry after the breakdown",
      "note": "Full band returns; lands as a hit.",
      "mmss": "0:56"
    },
    {
      "type": "in",
      "time": 60,
      "name": "Chorus drop",
      "note": "Full entry into the chorus after the verse.",
      "mmss": "1:00"
    },
    {
      "type": "out",
      "time": 84,
      "name": "Chorus cut point",
      "note": "End of the chorus before the outro; cut or dip here.",
      "mmss": "1:24"
    },
    {
      "type": "out",
      "time": 84,
      "name": "Fade begins",
      "note": "Natural fade; needs no tail edit.",
      "mmss": "1:24"
    }
  ],
  "elapsed_seconds": 41.62
}

400Bad request. The body is not valid JSON, or a field is invalid: audio_url missing, section_order with more than 60 labels or a label empty or over 40 characters, tempo_hint outside 30 to 300, or describe neither a boolean nor a list of moment types. missing_fields is the path to the first invalid field.

application/json

statusstring · enum

Error status

Values: "error"

missing_fieldsarray<string | integer>

Path to the first invalid or missing field when validation fails. A position inside an array appears as an integer, for example ["prompts", 1, "id"].

Item properties for missing_fields

string | integer

errorstring

Error message describing what went wrong

tempo_hint out of range

badTempo
{
  "status": "error",
  "error": "tempo_hint must be at most 300 BPM",
  "missing_fields": [
    "tempo_hint"
  ]
}

401Unauthorized — invalid or missing API key / Bearer token

application/json

statusstring · enum

Error status

Values: "error"

missing_fieldsarray<string | integer>

Path to the first invalid or missing field when validation fails. A position inside an array appears as an integer, for example ["prompts", 1, "id"].

Item properties for missing_fields

string | integer

errorstring

Error message describing what went wrong

402Insufficient credits — the balance cannot cover the measurement ($0.01) plus a model base price ($0.05) for every moment that may be described. Nothing runs, nothing is charged, and no Stripe object is created. The body includes a static billingUrl.

application/json

errorstring · enumrequired

Values: "insufficient_credits"

remaining_creditsintegerrequired

The account's balance, in credits (micro-dollars: 1,000,000 = $1.00).

Example: 0

required_creditsintegerrequired

The base price of this request, in credits. Analyze: 50000 ($0.05) for a single preset or custom prompt, 650000 ($0.65) for full_report. Analyze batch: 50000, once for the whole batch. Song moments: 10000 ($0.01) plus 50000 for every moment that may be described, which is 360000 ($0.36) with the default describe: true. The per-second inference charge is added after the model returns.

Example: 50000

billingUrlstringrequired

Static link to the Recoup app, where a human can save a card and buy credits. It is a constant, not a freshly minted Stripe Checkout Session, so a credit-gated endpoint that keeps returning 402 creates nothing. To buy credits programmatically, call POST /api/credits/sessions.

Example: "https://app.recoupable.dev"

A balance that cannot cover the default request

short
{
  "error": "insufficient_credits",
  "remaining_credits": 120000,
  "required_credits": 360000,
  "billingUrl": "https://app.recoupable.dev"
}

422Unverifiable audio. Before anything runs, audio_url is checked with a HEAD request (a ranged GET for hosts that reject HEAD). It must answer 2xx within 10 seconds with an audio/* or application/octet-stream content type. When it does not, nothing runs and nothing is charged.

application/json

statusstring · enumrequired

Values: "error"

errorstring · enumrequired

audio_url_unreachable: the URL did not answer 2xx within 10 seconds. audio_url_not_audio: it answered, but with a content type that is not audio/* or application/octet-stream.

Values: "audio_url_unreachable", "audio_url_not_audio"

Example: "audio_url_not_audio"

messagestringrequired

What the check observed, for the caller's logs.

Example: "audio_url answered 200 with content type text/html"

500The measurement failed, for example because the file is larger than 100 MB or cannot be decoded as audio, or the measurement service could not be reached. error carries the reason, such as Song measurement failed (status 422). Nothing is charged.

application/json

statusstring · enum

Error status

Values: "error"

missing_fieldsarray<string | integer>

Path to the first invalid or missing field when validation fails. A position inside an array appears as an integer, for example ["prompts", 1, "id"].

Item properties for missing_fields

string | integer

errorstring

Error message describing what went wrong

The measurement service rejected the file

measurementFailed
{
  "status": "error",
  "error": "Song measurement failed (status 422)"
}

Full specification

Download the OpenAPI file for complete schemas, constraints, and examples.

Download releases.json
View operation source
json
{
  "operationId": "postSongsMoments",
  "summary": "Find a song's sections and moments",
  "description": "Finds a song's sections and its best short moments for licensing and editing, with the measurements an edit or a sync pitch needs. Times and numbers come from measuring the audio itself (the beat grid, loudness, which passages repeat), not from a language model. An audio model then listens to a clip of each moment and adds a short description.\n\nSend `audio_url` (MP3, WAV or FLAC). Optionally send `section_order`, the section headers from a lyric sheet in order, to name the sections from the sheet, and `tempo_hint`, the tempo in BPM from your metadata, to correct a beat tracker that locked onto half or double time. `describe` chooses which moments get a description: `true` (the default) for every moment, `false` for none, or a list of moment types.\n\nThe response has the song's `sections`, its `moments` with `6`, `15` and `30` second `cuts` on the beat grid, and a per-second `energy_curve`. It also carries an extended measurement: loudness and peaks (`lufs`, `true_peak_dbtp`, `broadcast_ready`), the stereo image, a seven-band spectrum (`band_profile`), a six-axis sound `fingerprint`, `key`, `time_signature`, `valence` and `arousal`, `mute_drops`, a `chord_loop` estimate and typed `edit_points`. Store `measured_with` with each result: it changes whenever the measurement's results would change. The fingerprint formulas and the band reference split were calibrated on a small sample of songs, so treat them as comparable numbers within a version, not as norms.\n\nPricing: `$0.01` per song measured, plus the [analyze](/api-reference/songs/analyze) rate (`$0.05` per model call plus `$0.001166` per second of inference time) for each moment described. The balance must hold `$0.01` plus `$0.05` for every moment that may be described before anything runs, which is `$0.36` with the default `describe: true`, otherwise the response is `402`. A moment whose description fails comes back without a description and is not charged. If the measurement itself fails, nothing is charged.\n\nA request can take a few minutes when the measurement and model services are starting cold, up to a limit of 300 seconds.",
  "security": [
    {
      "apiKeyAuth": []
    },
    {
      "bearerAuth": []
    }
  ],
  "requestBody": {
    "description": "The audio to measure, with optional section names, a tempo hint and the moments to describe",
    "required": true,
    "content": {
      "application/json": {
        "schema": {
          "$ref": "#/components/schemas/SongMomentsRequest"
        },
        "examples": {
          "lyricSheetAndTempo": {
            "summary": "With a lyric sheet's section order and a tempo hint",
            "value": {
              "audio_url": "https://example.com/song.mp3",
              "section_order": [
                "Intro",
                "Verse 1",
                "Chorus",
                "Verse 2",
                "Chorus",
                "Outro"
              ],
              "tempo_hint": 120
            }
          },
          "measureOnly": {
            "summary": "Measurements only, no descriptions",
            "value": {
              "audio_url": "https://example.com/song.mp3",
              "describe": false
            }
          },
          "someMoments": {
            "summary": "Describe two moment types",
            "value": {
              "audio_url": "https://example.com/song.mp3",
              "describe": [
                "vocal_hook",
                "climax"
              ]
            }
          }
        }
      }
    }
  },
  "responses": {
    "200": {
      "description": "The song was measured. Every moment named by `describe` carries `description`, `emotion`, `best_for` and `vocals`; a moment whose description failed is returned without them.",
      "content": {
        "application/json": {
          "schema": {
            "$ref": "#/components/schemas/SongMomentsResponse"
          },
          "examples": {
            "lyricSheetAndTempo": {
              "summary": "A 96-second song measured with the first request example above",
              "value": {
                "status": "success",
                "measured_with": "song-moments-2026-11-04+extended-2026-11-04",
                "duration_seconds": 96,
                "tempo_bpm": 120,
                "loudness_lufs": -9.4,
                "ending": "fade",
                "names_from": "lyrics",
                "sections": [
                  {
                    "label": "intro",
                    "start": 0,
                    "end": 8,
                    "group": "A",
                    "energy": 36
                  },
                  {
                    "label": "verse",
                    "start": 8,
                    "end": 24,
                    "group": "B",
                    "energy": 53
                  },
                  {
                    "label": "chorus",
                    "start": 24,
                    "end": 44,
                    "group": "C",
                    "energy": 83
                  },
                  {
                    "label": "verse",
                    "start": 44,
                    "end": 60,
                    "group": "B",
                    "energy": 47
                  },
                  {
                    "label": "chorus",
                    "start": 60,
                    "end": 84,
                    "group": "C",
                    "energy": 91
                  },
                  {
                    "label": "outro",
                    "start": 84,
                    "end": 96,
                    "group": "D",
                    "energy": 17
                  }
                ],
                "moments": [
                  {
                    "type": "intro",
                    "start": 0,
                    "end": 8,
                    "section": "intro",
                    "energy": 36,
                    "cuts": {
                      "6": [
                        0,
                        6
                      ],
                      "15": [
                        0,
                        15
                      ],
                      "30": [
                        0,
                        30
                      ]
                    },
                    "description": "Sparse opening with a filtered pad and a soft kick pattern that builds toward the first verse.",
                    "emotion": [
                      "calm",
                      "anticipation"
                    ],
                    "best_for": [
                      "documentary",
                      "end credits"
                    ],
                    "vocals": false
                  },
                  {
                    "type": "vocal_hook",
                    "start": 24,
                    "end": 39,
                    "section": "chorus",
                    "energy": 87,
                    "cuts": {
                      "6": [
                        24,
                        30
                      ],
                      "15": [
                        24,
                        39
                      ],
                      "30": [
                        24,
                        54
                      ]
                    },
                    "description": "The first chorus lands with a full drum kit and a layered vocal hook over a rising bass line.",
                    "emotion": [
                      "uplifting",
                      "energetic",
                      "anthemic"
                    ],
                    "best_for": [
                      "ad",
                      "social video",
                      "TV promo"
                    ],
                    "vocals": true
                  },
                  {
                    "type": "build_up",
                    "start": 46,
                    "end": 64,
                    "section": "verse",
                    "energy": 56,
                    "cuts": {
                      "6": [
                        58,
                        64
                      ],
                      "15": [
                        49,
                        64
                      ],
                      "30": [
                        34,
                        64
                      ]
                    },
                    "description": "Drums drop out and a riser climbs as the vocal repeats a short phrase into the second chorus.",
                    "emotion": [
                      "tense",
                      "driving"
                    ],
                    "best_for": [
                      "film trailer",
                      "sports"
                    ],
                    "vocals": true
                  },
                  {
                    "type": "climax",
                    "start": 62,
                    "end": 77,
                    "section": "chorus",
                    "energy": 91,
                    "cuts": {
                      "6": [
                        62,
                        68
                      ],
                      "15": [
                        62,
                        77
                      ],
                      "30": [
                        62,
                        92
                      ]
                    },
                    "also": [
                      "chorus_lift"
                    ],
                    "description": "The second chorus returns at full volume with doubled vocals and a stacked harmony on the last line.",
                    "emotion": [
                      "triumphant",
                      "powerful",
                      "euphoric"
                    ],
                    "best_for": [
                      "film trailer",
                      "sports",
                      "TV promo"
                    ],
                    "vocals": true
                  },
                  {
                    "type": "outro",
                    "start": 84,
                    "end": 96,
                    "section": "outro",
                    "energy": 17,
                    "cuts": {
                      "6": [
                        90,
                        96
                      ],
                      "15": [
                        81,
                        96
                      ],
                      "30": [
                        66,
                        96
                      ]
                    },
                    "description": "The band thins to pad and a single guitar line that fades to silence.",
                    "emotion": [
                      "wistful",
                      "gentle"
                    ],
                    "best_for": [
                      "end credits",
                      "documentary"
                    ],
                    "vocals": false
                  }
                ],
                "energy_curve": [
                  24,
                  29,
                  29,
                  36,
                  37,
                  38,
                  45,
                  46,
                  52,
                  54,
                  51,
                  55,
                  53,
                  50,
                  54,
                  52,
                  52,
                  54,
                  51,
                  55,
                  53,
                  50,
                  54,
                  52,
                  86,
                  88,
                  85,
                  89,
                  87,
                  84,
                  88,
                  86,
                  86,
                  88,
                  85,
                  89,
                  87,
                  84,
                  88,
                  86,
                  86,
                  88,
                  85,
                  18,
                  56,
                  53,
                  57,
                  55,
                  55,
                  57,
                  54,
                  58,
                  22,
                  19,
                  23,
                  21,
                  55,
                  57,
                  54,
                  58,
                  91,
                  88,
                  92,
                  90,
                  90,
                  92,
                  89,
                  93,
                  91,
                  88,
                  92,
                  90,
                  90,
                  92,
                  89,
                  93,
                  91,
                  88,
                  92,
                  90,
                  90,
                  92,
                  89,
                  93,
                  31,
                  25,
                  27,
                  22,
                  20,
                  20,
                  14,
                  16,
                  11,
                  6,
                  8,
                  3
                ],
                "tempo_note": "beat tracker 60 reconciled to hint 120",
                "lufs": -9.4,
                "true_peak_dbtp": -1.3,
                "sample_peak_dbfs": -1.62,
                "crest_factor_db": 9.8,
                "loudness_range_lu": 5.1,
                "broadcast_ready": true,
                "stereo": {
                  "lr_correlation": 0.842,
                  "side_to_mid": 0.31
                },
                "spectral_centroid_hz": 2410,
                "rolloff95_hz": 6931,
                "band_profile": [
                  {
                    "band": "sub",
                    "lo_hz": 20,
                    "hi_hz": 60,
                    "share_pct": 6.5,
                    "side_pct": 2.1,
                    "level_pct": 32,
                    "status": "light"
                  },
                  {
                    "band": "low",
                    "lo_hz": 60,
                    "hi_hz": 250,
                    "share_pct": 31,
                    "side_pct": 3.4,
                    "level_pct": 49,
                    "status": "clear"
                  },
                  {
                    "band": "low_mid",
                    "lo_hz": 250,
                    "hi_hz": 500,
                    "share_pct": 13,
                    "side_pct": 9.8,
                    "level_pct": 51,
                    "status": "clear"
                  },
                  {
                    "band": "mid",
                    "lo_hz": 500,
                    "hi_hz": 2000,
                    "share_pct": 21.5,
                    "side_pct": 18.6,
                    "level_pct": 54,
                    "status": "clear"
                  },
                  {
                    "band": "high_mid",
                    "lo_hz": 2000,
                    "hi_hz": 6000,
                    "share_pct": 13.5,
                    "side_pct": 31.2,
                    "level_pct": 55,
                    "status": "clear"
                  },
                  {
                    "band": "high",
                    "lo_hz": 6000,
                    "hi_hz": 12000,
                    "share_pct": 10.2,
                    "side_pct": 38.7,
                    "level_pct": 57,
                    "status": "clear"
                  },
                  {
                    "band": "air",
                    "lo_hz": 12000,
                    "hi_hz": 22050,
                    "share_pct": 4.3,
                    "side_pct": 44,
                    "level_pct": 51,
                    "status": "clear"
                  }
                ],
                "fingerprint": {
                  "organic_electronic": 0.36,
                  "sparse_dense": 0.51,
                  "dark_bright": 0.38,
                  "intimate_anthemic": 0.54,
                  "smooth_textured": 0.33,
                  "slow_fast": 0.64
                },
                "fingerprint_inputs": {
                  "flatness": 0.0213,
                  "contrast_db": 20.4,
                  "zcr_std": 0.0412,
                  "perc_ratio": 0.318,
                  "onsets_per_s": 4.11
                },
                "key": {
                  "tonic": "A",
                  "mode": "minor",
                  "confidence": 0.81
                },
                "time_signature": "4/4",
                "valence": 0.37,
                "arousal": 0.72,
                "mute_drops": [
                  {
                    "type": "mute",
                    "start": 43.5,
                    "end": 44,
                    "beats": 1,
                    "depth_db": 14.2,
                    "mmss": "0:43"
                  },
                  {
                    "type": "breakdown",
                    "start": 52,
                    "end": 56,
                    "beats": 8,
                    "depth_db": 11.3,
                    "mmss": "0:52"
                  }
                ],
                "chord_loop": {
                  "dominant_loop": "Am F C G",
                  "loop_share": 0.52,
                  "distinct_chords": 5,
                  "bars": 47,
                  "confidence": "low"
                },
                "edit_points": [
                  {
                    "type": "in",
                    "time": 0,
                    "name": "Cold open (intro)",
                    "note": "Starts clean from the top.",
                    "mmss": "0:00"
                  },
                  {
                    "type": "in",
                    "time": 24,
                    "name": "Chorus drop",
                    "note": "Full entry into the chorus after the verse.",
                    "mmss": "0:24"
                  },
                  {
                    "type": "out",
                    "time": 43.5,
                    "name": "Mute drop",
                    "note": "14.2 dB dip for 1.0 beats; a zero-bleed edit.",
                    "mmss": "0:43"
                  },
                  {
                    "type": "out",
                    "time": 44,
                    "name": "Chorus cut point",
                    "note": "End of the chorus before the verse; cut or dip here.",
                    "mmss": "0:44"
                  },
                  {
                    "type": "out",
                    "time": 52,
                    "name": "Breakdown",
                    "note": "Band pulls back 11.3 dB for 4.0 s.",
                    "mmss": "0:52"
                  },
                  {
                    "type": "in",
                    "time": 56,
                    "name": "Re-entry after the breakdown",
                    "note": "Full band returns; lands as a hit.",
                    "mmss": "0:56"
                  },
                  {
                    "type": "in",
                    "time": 60,
                    "name": "Chorus drop",
                    "note": "Full entry into the chorus after the verse.",
                    "mmss": "1:00"
                  },
                  {
                    "type": "out",
                    "time": 84,
                    "name": "Chorus cut point",
                    "note": "End of the chorus before the outro; cut or dip here.",
                    "mmss": "1:24"
                  },
                  {
                    "type": "out",
                    "time": 84,
                    "name": "Fade begins",
                    "note": "Natural fade; needs no tail edit.",
                    "mmss": "1:24"
                  }
                ],
                "elapsed_seconds": 41.62
              }
            }
          }
        }
      }
    },
    "400": {
      "description": "Bad request. The body is not valid JSON, or a field is invalid: `audio_url` missing, `section_order` with more than 60 labels or a label empty or over 40 characters, `tempo_hint` outside 30 to 300, or `describe` neither a boolean nor a list of moment types. `missing_fields` is the path to the first invalid field.",
      "content": {
        "application/json": {
          "schema": {
            "$ref": "#/components/schemas/SongAnalyzeErrorResponse"
          },
          "examples": {
            "badTempo": {
              "summary": "tempo_hint out of range",
              "value": {
                "status": "error",
                "error": "tempo_hint must be at most 300 BPM",
                "missing_fields": [
                  "tempo_hint"
                ]
              }
            }
          }
        }
      }
    },
    "401": {
      "description": "Unauthorized — invalid or missing API key / Bearer token",
      "content": {
        "application/json": {
          "schema": {
            "$ref": "#/components/schemas/SongAnalyzeErrorResponse"
          }
        }
      }
    },
    "402": {
      "description": "Insufficient credits — the balance cannot cover the measurement (`$0.01`) plus a model base price (`$0.05`) for every moment that may be described. Nothing runs, nothing is charged, and no Stripe object is created. The body includes a static `billingUrl`.",
      "content": {
        "application/json": {
          "schema": {
            "$ref": "#/components/schemas/SongAnalyzeInsufficientCreditsResponse"
          },
          "examples": {
            "short": {
              "summary": "A balance that cannot cover the default request",
              "value": {
                "error": "insufficient_credits",
                "remaining_credits": 120000,
                "required_credits": 360000,
                "billingUrl": "https://app.recoupable.dev"
              }
            }
          }
        }
      }
    },
    "422": {
      "description": "Unverifiable audio. Before anything runs, `audio_url` is checked with a `HEAD` request (a ranged `GET` for hosts that reject `HEAD`). It must answer 2xx within 10 seconds with an `audio/*` or `application/octet-stream` content type. When it does not, nothing runs and nothing is charged.",
      "content": {
        "application/json": {
          "schema": {
            "$ref": "#/components/schemas/SongAnalyzeAudioUrlErrorResponse"
          }
        }
      }
    },
    "500": {
      "description": "The measurement failed, for example because the file is larger than 100 MB or cannot be decoded as audio, or the measurement service could not be reached. `error` carries the reason, such as `Song measurement failed (status 422)`. Nothing is charged.",
      "content": {
        "application/json": {
          "schema": {
            "$ref": "#/components/schemas/SongAnalyzeErrorResponse"
          },
          "examples": {
            "measurementFailed": {
              "summary": "The measurement service rejected the file",
              "value": {
                "status": "error",
                "error": "Song measurement failed (status 422)"
              }
            }
          }
        }
      }
    }
  }
}

Which to use

Use this endpoint when you need to know when things happen in a song and how it measures: where the sections and best moments start and end, ready-made 6, 15 and 30 second cuts on the beat grid, loudness and peaks, the spectrum, key, and where to cut in and out. Those times and numbers come from measuring the audio, so they are repeatable.

Use POST /api/songs/analyze for judgments that need a listener: genre, mood, lyrics, mix feedback, sync fit. A single preset, full_report or a custom prompt answers one analysis at a time.

Use POST /api/songs/analyze/batch to ask many short custom questions about one file in a single request.

Moment types

A song has at most one moment of each type, and not every song has every type. When two moments overlap by more than 60%, the one that is kept lists the other in also.

TypeWhat it is
introThe opening section, when it is an intro, up to 20 seconds.
vocal_hookThe start of the first chorus, up to 15 seconds.
chorus_liftThe loudest of the later choruses, up to 20 seconds. Needs more than one chorus.
build_upThe strongest rise into a section: the lead-in plus a few seconds of the payoff. Only when the rise is clear.
climaxThe loudest 15 seconds of the song.
breakdownThe longest stretch of 6 seconds or more that sits at least 4 dB under the song's median loudness, once the first chorus has started.
outroThe end of the song, up to 20 seconds.

Response fields

The first group of fields is the base measurement: measured_with, duration_seconds, tempo_bpm, loudness_lufs, ending, names_from, sections, moments, energy_curve and elapsed_seconds. Moments that were described also carry description, emotion, best_for and vocals.

The rest is the extended measurement. A result whose measured_with has no +extended- suffix was measured without these fields.

FieldMeaning
lufsIntegrated loudness of the stereo file, in LUFS. Same figure as loudness_lufs.
true_peak_dbtp, loudness_range_luTrue peak and loudness range. null when the loudness meter could not read the file.
sample_peak_dbfs, crest_factor_dbHighest sample level, and that peak minus the average level.
broadcast_readytrue when the true peak is at or under -1 dBTP and the loudness is between -16 and -7 LUFS. null when the true peak is unknown.
stereolr_correlation between the channels (1 is identical) and side_to_mid (0 is mono).
spectral_centroid_hz, rolloff95_hzBrightness: the mean centroid, and the frequency under which 95% of the energy lies.
band_profileSeven bands, sub to air. Each has share_pct of the mix's energy (the seven add up to about 100), side_pct, level_pct against a typical pop split (50 is on the reference) and status: clear within 4 dB of it, heavy above, light below.
fingerprintSix axes from 0 to 1: organic_electronic, sparse_dense, dark_bright, intimate_anthemic, smooth_textured, slow_fast. 0 is the left label, 1 the right. fingerprint_inputs holds the raw measurements.
keytonic, mode (major or minor) and confidence, a correlation up to 1.
time_signature3/4 or 4/4.
valence, arousalThe measured half of mood, 0 to 1. Estimates, not listening judgments.
mute_dropsSudden drops in level: a mute is under two beats (a zero-bleed cut), a breakdown is two beats or more. Each has start, end, beats, depth_db and mmss.
chord_loopThe most common four-bar chord cycle, dominant_loop, with loop_share, distinct_chords and bars. confidence is always low; treat it as one estimate, not a transcription.
edit_pointsTyped in and out points with a name, a note and mmss: the cold open, chorus entries and exits, the deepest mutes, breakdowns and their re-entry, and where a fade begins.
tempo_noteHow tempo_bpm was decided: the beat tracker alone, or reconciled to tempo_hint.

Store measured_with with every result. It changes whenever the measurement's results would change, so it tells you which stored results to rerun.

Pricing

$0.01 per song measured, plus the analyze rate for each moment described: $0.05 per model call plus $0.001166 per second of inference time. The balance must hold $0.01 plus $0.05 for every moment that may be described before anything runs: $0.36 with the default describe: true, $0.01 with describe: false, and $0.01 plus $0.05 per type with a list. Otherwise the response is 402 and nothing is charged. A moment whose description fails comes back without a description and is not charged, and a failed measurement is not charged. See Credits.