API REFERENCE
Find song moments
On this page
/api/songs/momentsFinds 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 --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 --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_urlstringrequiredPublic 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_hintnumberThe 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 ofWhich 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
{
"audio_url": "https://example.com/song.mp3",
"section_order": [
"Intro",
"Verse 1",
"Chorus",
"Verse 2",
"Chorus",
"Outro"
],
"tempo_hint": 120
}Measurements only, no descriptions
{
"audio_url": "https://example.com/song.mp3",
"describe": false
}Describe two moment types
{
"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 · enumrequiredRequest status.
Values: "success"
measured_withstringrequiredThe 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_secondsnumberrequiredLength of the audio that was measured, in seconds (at most the first 15 minutes).
Example: 96
tempo_bpmnumberrequiredTempo in beats per minute, reconciled to tempo_hint when one was sent.
Example: 120
loudness_lufsnumber | nullrequiredIntegrated loudness in LUFS. The same figure as lufs.
Example: -9.4
endingstring · enumrequiredfade when the last seconds fade out by more than 10 dB, cold when the song stops.
Values: "fade", "cold"
names_fromstring · enumrequiredlyrics when section_order fit the audio and named the sections, audio when sections were named from repetition and loudness.
Values: "lyrics", "audio"
sectionsarray<SongSection>requiredThe song's sections in order, covering the whole song.
Item properties for sections
labelstringrequiredThe section's name: intro, verse, pre-chorus, chorus, post-chorus, bridge, instrumental, drop, breakdown, outro, or section when nothing fits better.
Example: "chorus"
startnumberrequiredStart, in seconds from the beginning of the song.
Example: 24
endnumberrequiredEnd, in seconds from the beginning of the song.
Example: 44
groupstringrequiredSections that share a letter sound alike (A, B, A, C, ...), so repeated choruses share a group.
Example: "C"
energyintegerrequiredAverage 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>requiredThe song's best short moments for licensing and editing, in order of start time.
Item properties for moments
typestring · enumrequiredWhat 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"
startnumberrequiredStart, in seconds from the beginning of the song.
Example: 24
endnumberrequiredEnd, in seconds from the beginning of the song.
Example: 39
sectionstringrequiredThe label of the section the moment starts in.
Example: "chorus"
energyintegerrequiredAverage loudness of the moment, 0 to 100 relative to the song's own quietest and loudest seconds.
Example: 87
minimum: 0 · maximum: 100
cutsobjectrequiredEdit 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 | nullOne 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 | nullWhether the moment has vocals. Present only on described moments; null when the reply could not be read.
energy_curvearray<integer>requiredLoudness 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_notestringHow tempo_bpm was decided: beat tracker alone, or reconciled to, or overruled by, tempo_hint.
Example: "beat tracker 60 reconciled to hint 120"
lufsnumber | nullIntegrated 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 | nullTrue peak in dBTP. null when the loudness meter could not read the file.
Example: -1.3
sample_peak_dbfsnumber | nullHighest sample level, in dBFS.
Example: -1.62
crest_factor_dbnumber | nullHighest sample peak minus the average (RMS) level, in decibels. A low value means a heavily compressed master.
Example: 9.8
loudness_range_lunumber | nullLoudness 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 | nulltrue 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
stereoobjectThe stereo image.
Properties for stereo
lr_correlationnumber | nullCorrelation between the left and right channels, -1 to 1. 1 means identical channels.
Example: 0.842
side_to_midnumber | nullSide (left minus right) level over mid (left plus right) level. 0 is mono; higher is wider.
Example: 0.31
spectral_centroid_hznumber | nullMean spectral centroid in hertz, a measure of brightness.
Example: 2410
rolloff95_hznumber | nullThe 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 · enumrequiredBand name.
Values: "sub", "low", "low_mid", "mid", "high_mid", "high", "air"
lo_hzintegerrequiredLower edge of the band in hertz.
Example: 60
hi_hzintegerrequiredUpper edge of the band in hertz.
Example: 250
side_pctnumberrequiredSide (left minus right) energy in the band, as a percent of the band's mid energy.
Example: 3.4
level_pctintegerrequiredThe band's level against a typical pop energy split, 0 to 100. 50 is on the reference.
Example: 51
minimum: 0 · maximum: 100
statusstring · enumrequiredclear 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"
fingerprintobjectSix sound axes. Each runs from 0 at its left label to 1 at its right label.
Properties for fingerprint
organic_electronicnumber0 organic, 1 electronic.
minimum: 0 · maximum: 1
sparse_densenumber0 sparse, 1 dense.
minimum: 0 · maximum: 1
dark_brightnumber0 dark, 1 bright.
minimum: 0 · maximum: 1
intimate_anthemicnumber0 intimate, 1 anthemic.
minimum: 0 · maximum: 1
smooth_texturednumber0 smooth, 1 textured.
minimum: 0 · maximum: 1
slow_fastnumber0 slow, 1 fast.
minimum: 0 · maximum: 1
fingerprint_inputsobjectThe raw measurements the fingerprint is built from.
Properties for fingerprint_inputs
flatnessnumberMean spectral flatness: higher is noisier or more electronic.
Example: 0.0213
contrast_dbnumberMean spectral contrast in decibels.
Example: 20.4
zcr_stdnumberSpread of the zero-crossing rate, a measure of textural change.
Example: 0.0412
perc_rationumberShare of the energy that is percussive, 0 to 1.
Example: 0.318
onsets_per_snumberNote and hit onsets per second.
Example: 4.11
keyobjectThe song's key.
Properties for key
tonicstring · enumThe key's root note.
Values: "C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B"
Example: "A"
modestring · enumValues: "major", "minor"
Example: "minor"
confidencenumber | nullHow 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 · enumThe meter, from how beat strength repeats.
Values: "3/4", "4/4"
valencenumberThe 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
arousalnumberHow 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 · enumrequiredmute 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"
startnumberrequiredStart, in seconds.
Example: 52
endnumberrequiredEnd, in seconds.
Example: 56
beatsnumberrequiredLength in beats.
Example: 8
depth_dbnumberrequiredHow far the level falls below the surrounding music, in decibels.
Example: 11.3
mmssstringrequiredThe start as m:ss.
Example: "0:52"
chord_loopobjectThe 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_loopstringThe cycle as four chord names, for example Am F C G.
Example: "Am F C G"
distinct_chordsintegerHow many different chords appear.
Example: 5
barsintegerHow many bars were analyzed.
Example: 47
confidencestring · enumAlways 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 · enumrequiredin is a place to cut into the song, out a place to cut out of it.
Values: "in", "out"
timenumberrequiredThe point, in seconds.
Example: 24
namestringrequiredWhat the point is, for example Chorus drop, Breakdown or Fade begins.
Example: "Chorus drop"
notestringrequiredA short note on how to use the point.
Example: "Full entry into the chorus after the verse."
mmssstringrequiredThe point as m:ss.
Example: "0:24"
elapsed_secondsnumberrequiredSeconds the whole request took, descriptions included.
Example: 41.62
format: float
A 96-second song measured with the first request example above
{
"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 · enumError 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
errorstringError message describing what went wrong
tempo_hint out of range
{
"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 · enumError 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
errorstringError 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 · enumrequiredValues: "insufficient_credits"
remaining_creditsintegerrequiredThe account's balance, in credits (micro-dollars: 1,000,000 = $1.00).
Example: 0
required_creditsintegerrequiredThe 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
billingUrlstringrequiredStatic 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
{
"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 · enumrequiredValues: "error"
errorstring · enumrequiredaudio_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"
messagestringrequiredWhat 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 · enumError 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
errorstringError message describing what went wrong
The measurement service rejected the file
{
"status": "error",
"error": "Song measurement failed (status 422)"
}Full specification
Download the OpenAPI file for complete schemas, constraints, and examples.
Download releases.jsonView operation source
{
"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.
| Type | What it is |
|---|---|
intro | The opening section, when it is an intro, up to 20 seconds. |
vocal_hook | The start of the first chorus, up to 15 seconds. |
chorus_lift | The loudest of the later choruses, up to 20 seconds. Needs more than one chorus. |
build_up | The strongest rise into a section: the lead-in plus a few seconds of the payoff. Only when the rise is clear. |
climax | The loudest 15 seconds of the song. |
breakdown | The 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. |
outro | The 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.
| Field | Meaning |
|---|---|
lufs | Integrated loudness of the stereo file, in LUFS. Same figure as loudness_lufs. |
true_peak_dbtp, loudness_range_lu | True peak and loudness range. null when the loudness meter could not read the file. |
sample_peak_dbfs, crest_factor_db | Highest sample level, and that peak minus the average level. |
broadcast_ready | true 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. |
stereo | lr_correlation between the channels (1 is identical) and side_to_mid (0 is mono). |
spectral_centroid_hz, rolloff95_hz | Brightness: the mean centroid, and the frequency under which 95% of the energy lies. |
band_profile | Seven 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. |
fingerprint | Six 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. |
key | tonic, mode (major or minor) and confidence, a correlation up to 1. |
time_signature | 3/4 or 4/4. |
valence, arousal | The measured half of mood, 0 to 1. Estimates, not listening judgments. |
mute_drops | Sudden 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_loop | The 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_points | Typed 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_note | How 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.