API REFERENCE
Analyze songs
On this page
/api/songs/analyzeAnalyze music with an audio language model that listens directly to the audio waveform.
Send audio_url with exactly one of preset (a curated analysis, listed by GET /api/songs/analyze/presets) or prompt (a free-form question). preset: "full_report" runs all 13 sections and returns them together in report.
A full_report response has status: "success" when every section returned and status: "partial" when some did not. A partial response is still HTTP 200: failed lists the report keys of the sections that failed, and each of those keys holds { "error": "Section failed: ..." } in place of its result. If every section failed, the response is 502 instead.
Pricing: $0.05 per model request plus $0.001166 per second of inference time. full_report asks all 13 sections in one batched request, so it pays the $0.05 base once, plus the seconds of the sections that returned. A section that fails is not charged: failed sections of a partial report and requests answered with 502 cost nothing for the failed work. To ask many short questions about one file, use POST /api/songs/analyze/batch, which pays the base price once.
Authentication
x-api-key in header
bearerAuth bearer
Request
curl --request POST \
--url 'https://api.recoupable.dev/api/songs/analyze' \
--header 'x-api-key: YOUR_API_KEY' \
--header 'Content-Type: application/json' \
--data '{
"preset": "mood_tags",
"audio_url": "https://example.com/song.m4a"
}'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/analyze' \
--header 'Content-Type: application/json' \
--data '{
"preset": "mood_tags",
"audio_url": "https://example.com/song.m4a"
}'Request body required
Music analysis request
application/json
presetstring · enumName of a curated analysis preset. Use instead of prompt for structured, optimized output. The 'full_report' preset runs all 13 presets in one batched request and returns a comprehensive report. See List Analyze Presets for the full list of available presets.
Values: "catalog_metadata", "mood_tags", "lyric_transcription", "mix_feedback", "song_description", "music_theory", "similar_artists", "sample_detection", "sync_brief_match", "audience_profile", "content_advisory", "playlist_pitch", "artist_development_notes", "full_report"
Example: "catalog_metadata"
promptstringText prompt or question about the music
Example: "Describe the genre, tempo, and mood of this track."
minLength: 1 · maxLength: 24000
audio_urlstringrequiredPublic URL to an audio file (MP3, WAV, FLAC, M4A, AAC, OGG or WebM, up to 20 minutes). Required in every mode, including a custom prompt. The format is read from the file extension in the URL path, so give the URL a real extension (.m4a, .ogg, ...); a URL without one is treated as MP3. Verified before the model is called: 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
max_new_tokensintegerMaximum number of tokens to generate
Default: 512
Example: 512
minimum: 1 · maximum: 2048
temperaturenumberControls output creativity — higher values produce more varied responses
Default: 1
Example: 0.7
minimum: 0 · maximum: 2
top_pnumberNucleus sampling probability cutoff
Default: 1
Example: 0.9
minimum: 0 · maximum: 1
do_samplebooleanEnable sampling (set true when using temperature or top_p)
Default: false
Example: false
One structured analysis
{
"preset": "mood_tags",
"audio_url": "https://example.com/song.m4a"
}A free-form question
{
"prompt": "Describe the genre, tempo, and mood of this track.",
"audio_url": "https://example.com/song.mp3",
"max_new_tokens": 512
}All 13 sections
{
"preset": "full_report",
"audio_url": "https://example.com/song.mp3"
}Responses
200Music analysis completed. A single preset or custom prompt answers success with response. A full_report answers success with every section, or partial with failed naming the sections that did not return.+
application/json
statusstring · enumsuccess when the analysis completed. partial only for a full_report in which at least one section failed; the HTTP status is still 200, and failed names the sections that did not return.
Values: "success", "partial"
presetstringPreset used for analysis, when applicable
Example: "catalog_metadata"
responseanyModel output for single-preset or custom-prompt analysis. May be plain text or structured JSON depending on the preset.
reportobjectFull report payload, returned only for the full_report preset. It always has all 13 section keys: overview, mood, description, lyrics, content_advisory, music_theory, mix_feedback, similar_artists, sample_detection, audience, sync, playlist_pitch and development. A section that failed holds { "error": "Section failed: ..." } instead of its result.
elapsed_secondsnumberInference time in seconds. For full_report it is the wall time of the whole report, with its sections running one after another in one batch.
format: float
failedarray<string>Returned with full_report only. The report keys of the sections that failed, in report order; empty when status is success. A failed section is not charged. Retry it with the single preset that fills its key (see the report key table on the Analyze songs page).
Example: ["lyrics","sync"]
Item properties for failed
string
A structured preset
{
"status": "success",
"preset": "mood_tags",
"response": {
"tags": [
"dreamy",
"late-night",
"warm",
"nostalgic",
"mellow",
"hazy",
"intimate",
"slow-burn"
],
"primary_mood": "dreamy"
},
"elapsed_seconds": 2.87
}A custom prompt
{
"status": "success",
"response": "A mid-tempo indie pop track, around 96 BPM, with a warm and nostalgic mood.",
"elapsed_seconds": 4.12
}A partial full report, shown with three of its thirteen sections (every response carries all thirteen)
{
"status": "partial",
"preset": "full_report",
"report": {
"mood": {
"tags": [
"dreamy",
"late-night",
"warm",
"nostalgic",
"mellow",
"hazy",
"intimate",
"slow-burn"
],
"primary_mood": "dreamy"
},
"lyrics": {
"error": "Section failed: Music analysis failed (status 422)"
},
"sync": {
"error": "Section failed: Music analysis failed (status 422)"
}
},
"elapsed_seconds": 41.6,
"failed": [
"lyrics",
"sync"
]
}400Bad request — missing or invalid fields, including a missing audio_url (missing_fields: ["audio_url"]) or both preset and prompt in one request+
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
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 base price of this request ($0.05, full_report included). The model is not called, 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, a custom prompt or full_report. Analyze batch: 50000, once for the whole batch. Song moments: 10000 ($0.01) plus 50000 when any moment may be described, which is 60000 ($0.06) 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"
422Unverifiable audio. Before the model is called, 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, the model is not called 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"
500Server error. The request failed for a reason other than a rejection from the model service, for example the model service could not be reached or answered with an unexpected body.+
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
502The model service could not produce the analysis. Either it rejected a single preset or custom-prompt call (upstream_status and detail say why, for example 422 with audio_unreachable or audio_undecodable), or every section of a full_report failed (failed lists them). Nothing is charged for the failed work.+
application/json
statusstring · enumrequiredValues: "error"
errorstringrequiredMusic analysis failed (status N), where N is upstream_status, for a rejected single call. Music analysis failed for every section for a full_report in which no section returned.
Example: "Music analysis failed (status 422)"
upstream_statusintegerThe HTTP status the model service answered with. 422 means it could not fetch the audio (audio_unreachable) or could not read the file as audio (audio_undecodable). Present on a rejected single call only.
Example: 422
detailstringThe model service's error response, as text. For an audio problem it names the cause, beginning audio_unreachable or audio_undecodable, followed by a reason that varies. Present on a rejected single call only.
failedarray<string>All 13 report keys, in report order. Present on a full_report in which every section failed only.
Item properties for failed
string
The model service could not read the audio
{
"status": "error",
"error": "Music analysis failed (status 422)",
"upstream_status": 422,
"detail": "{\"detail\":\"audio_undecodable: could not decode the audio file\"}"
}Every section of a full report failed
{
"status": "error",
"error": "Music analysis failed for every section",
"failed": [
"overview",
"mood",
"description",
"lyrics",
"content_advisory",
"music_theory",
"mix_feedback",
"similar_artists",
"sample_detection",
"audience",
"sync",
"playlist_pitch",
"development"
]
}Full specification
Download the OpenAPI file for complete schemas, constraints, and examples.
Download releases.jsonView operation source
{
"operationId": "postSongsAnalyze",
"description": "Analyze music with an audio language model that listens directly to the audio waveform.\n\nSend `audio_url` with exactly one of `preset` (a curated analysis, listed by [`GET /api/songs/analyze/presets`](/api-reference/songs/analyze-presets)) or `prompt` (a free-form question). `preset: \"full_report\"` runs all 13 sections and returns them together in `report`.\n\nA `full_report` response has `status: \"success\"` when every section returned and `status: \"partial\"` when some did not. A `partial` response is still HTTP `200`: `failed` lists the report keys of the sections that failed, and each of those keys holds `{ \"error\": \"Section failed: ...\" }` in place of its result. If every section failed, the response is `502` instead.\n\nPricing: `$0.05` per model request plus `$0.001166` per second of inference time. `full_report` asks all 13 sections in one batched request, so it pays the `$0.05` base once, plus the seconds of the sections that returned. A section that fails is not charged: failed sections of a `partial` report and requests answered with `502` cost nothing for the failed work. To ask many short questions about one file, use [`POST /api/songs/analyze/batch`](/api-reference/songs/analyze-batch), which pays the base price once.",
"security": [
{
"apiKeyAuth": []
},
{
"bearerAuth": []
}
],
"requestBody": {
"description": "Music analysis request",
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/SongAnalyzeRequest"
},
"examples": {
"preset": {
"summary": "One structured analysis",
"value": {
"preset": "mood_tags",
"audio_url": "https://example.com/song.m4a"
}
},
"prompt": {
"summary": "A free-form question",
"value": {
"prompt": "Describe the genre, tempo, and mood of this track.",
"audio_url": "https://example.com/song.mp3",
"max_new_tokens": 512
}
},
"fullReport": {
"summary": "All 13 sections",
"value": {
"preset": "full_report",
"audio_url": "https://example.com/song.mp3"
}
}
}
}
}
},
"responses": {
"200": {
"description": "Music analysis completed. A single preset or custom prompt answers `success` with `response`. A `full_report` answers `success` with every section, or `partial` with `failed` naming the sections that did not return.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/SongAnalyzeResponse"
},
"examples": {
"preset": {
"summary": "A structured preset",
"value": {
"status": "success",
"preset": "mood_tags",
"response": {
"tags": [
"dreamy",
"late-night",
"warm",
"nostalgic",
"mellow",
"hazy",
"intimate",
"slow-burn"
],
"primary_mood": "dreamy"
},
"elapsed_seconds": 2.87
}
},
"prompt": {
"summary": "A custom prompt",
"value": {
"status": "success",
"response": "A mid-tempo indie pop track, around 96 BPM, with a warm and nostalgic mood.",
"elapsed_seconds": 4.12
}
},
"partial": {
"summary": "A partial full report, shown with three of its thirteen sections (every response carries all thirteen)",
"value": {
"status": "partial",
"preset": "full_report",
"report": {
"mood": {
"tags": [
"dreamy",
"late-night",
"warm",
"nostalgic",
"mellow",
"hazy",
"intimate",
"slow-burn"
],
"primary_mood": "dreamy"
},
"lyrics": {
"error": "Section failed: Music analysis failed (status 422)"
},
"sync": {
"error": "Section failed: Music analysis failed (status 422)"
}
},
"elapsed_seconds": 41.6,
"failed": [
"lyrics",
"sync"
]
}
}
}
}
}
},
"400": {
"description": "Bad request — missing or invalid fields, including a missing `audio_url` (`missing_fields: [\"audio_url\"]`) or both `preset` and `prompt` in one request",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/SongAnalyzeErrorResponse"
}
}
}
},
"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 base price of this request (`$0.05`, `full_report` included). The model is not called, nothing is charged, and no Stripe object is created. The body includes a static `billingUrl`.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/SongAnalyzeInsufficientCreditsResponse"
}
}
}
},
"422": {
"description": "Unverifiable audio. Before the model is called, `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, the model is not called and nothing is charged.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/SongAnalyzeAudioUrlErrorResponse"
}
}
}
},
"500": {
"description": "Server error. The request failed for a reason other than a rejection from the model service, for example the model service could not be reached or answered with an unexpected body.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/SongAnalyzeErrorResponse"
}
}
}
},
"502": {
"description": "The model service could not produce the analysis. Either it rejected a single preset or custom-prompt call (`upstream_status` and `detail` say why, for example `422` with `audio_unreachable` or `audio_undecodable`), or every section of a `full_report` failed (`failed` lists them). Nothing is charged for the failed work.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/SongAnalyzeUpstreamErrorResponse"
},
"examples": {
"rejectedCall": {
"summary": "The model service could not read the audio",
"value": {
"status": "error",
"error": "Music analysis failed (status 422)",
"upstream_status": 422,
"detail": "{\"detail\":\"audio_undecodable: could not decode the audio file\"}"
}
},
"everySectionFailed": {
"summary": "Every section of a full report failed",
"value": {
"status": "error",
"error": "Music analysis failed for every section",
"failed": [
"overview",
"mood",
"description",
"lyrics",
"content_advisory",
"music_theory",
"mix_feedback",
"similar_artists",
"sample_detection",
"audience",
"sync",
"playlist_pitch",
"development"
]
}
}
}
}
}
}
}
}Which to use
Use this endpoint for one analysis of one song: a curated preset such as mood_tags or catalog_metadata, preset: "full_report" for all 13 sections at once, or a custom prompt for a single free-form question.
Use POST /api/songs/analyze/batch when you have many short questions about the same file. The audio is downloaded once and the base price is paid once for the whole batch instead of once per question.
Use POST /api/songs/moments when you need to know when things happen and how the audio measures: the song's sections, its best short moments with 6, 15 and 30 second cuts, loudness, spectrum, key and edit points. Those times and numbers are measured from the audio, not generated.
Audio formats
audio_url accepts MP3, WAV, FLAC, M4A, AAC, OGG and WebM. The format is read from the file extension in the URL path, so give the URL a real extension; a URL without one is treated as MP3. If the model service cannot fetch the file or cannot read it as audio, a single preset or prompt call answers 502 with upstream_status 422 and a detail beginning audio_unreachable or audio_undecodable. A full_report fails every section the same way and answers 502 with failed.
Partial full reports
A full_report runs its 13 analyses, one per report section, in one batched request. When some of them fail, the response is still HTTP 200, with status: "partial" and the failed sections in failed:
{
"status": "partial",
"preset": "full_report",
"report": {
"mood": { "tags": ["dreamy", "late-night", "warm"], "primary_mood": "dreamy" },
"lyrics": { "error": "Section failed: Music analysis failed (status 422)" },
"sync": { "error": "Section failed: Music analysis failed (status 422)" }
},
"elapsed_seconds": 41.6,
"failed": ["lyrics", "sync"]
}This example shows three of the thirteen sections for brevity. Every response carries all thirteen keys, each holding its result or the { "error": ... } object. Check status or failed before you use a report; do not treat a 200 as a complete report. A failed section is not charged. To fill it in, call this endpoint again with the single preset for its key:
| Report key | Preset to retry it with |
|---|---|
overview | catalog_metadata |
mood | mood_tags |
description | song_description |
lyrics | lyric_transcription |
content_advisory | content_advisory |
music_theory | music_theory |
mix_feedback | mix_feedback |
similar_artists | similar_artists |
sample_detection | sample_detection |
audience | audience_profile |
sync | sync_brief_match |
playlist_pitch | playlist_pitch |
development | artist_development_notes |
If every section fails, the response is 502 with status: "error", error: "Music analysis failed for every section" and all thirteen keys in failed. Nothing is charged.
Pricing
$0.05 per model request plus $0.001166 per second of inference time. full_report asks all 13 sections in one batched request, so it pays the $0.05 base once, plus the seconds of the sections that returned; a failed section costs nothing. The balance must cover the base price of the request before the model runs; otherwise the response is 402 and nothing is charged. See Credits.