API REFERENCE
Analyze a song with many prompts
On this page
/api/songs/analyze/batchAsk up to 40 short questions about one audio file in a single request. The audio is downloaded once, then every prompt is answered against it, one after another, in the order sent. Use it instead of one POST /api/songs/analyze call per question: a batch pays the per-call base price once, not once per prompt.
Send audio_url and prompts, an array of { id, prompt, max_new_tokens }. id is your key for the answer (1 to 64 characters, unique within the batch), prompt is 1 to 4,000 characters, and the optional max_new_tokens (1 to 2048, default 400) bounds that one answer. temperature (default 0.2), top_p (default 1) and do_sample (default false) apply to every prompt in the batch.
Pricing: $0.05 once per batch plus $0.001166 per second of the batch's total inference time (elapsed_seconds, audio download included), charged once after the batch succeeds, as a single usage row. A batch that runs 90 seconds costs $0.15494. The balance must hold $0.05 before the model runs, otherwise the response is 402. A batch the model service rejects is not charged.
A batch is all or nothing: if the model service rejects it, the response is 502 and no answers are returned. Prompts run one after another, so a batch of many long answers can take several minutes; the request stays open until it finishes, so use a generous client timeout.
Authentication
x-api-key in header
bearerAuth bearer
Request
curl --request POST \
--url 'https://api.recoupable.dev/api/songs/analyze/batch' \
--header 'x-api-key: YOUR_API_KEY' \
--header 'Content-Type: application/json' \
--data '{
"audio_url": "https://example.com/song.mp3",
"prompts": [
{
"id": "genre",
"prompt": "What genre is this?",
"max_new_tokens": 64
},
{
"id": "tempo",
"prompt": "What is the tempo in BPM?"
}
],
"temperature": 0.2,
"top_p": 1,
"do_sample": false
}'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/batch' \
--header 'Content-Type: application/json' \
--data '{
"audio_url": "https://example.com/song.mp3",
"prompts": [
{
"id": "genre",
"prompt": "What genre is this?",
"max_new_tokens": 64
},
{
"id": "tempo",
"prompt": "What is the tempo in BPM?"
}
],
"temperature": 0.2,
"top_p": 1,
"do_sample": false
}'Request body required
One audio file and its prompts
application/json
audio_urlstringrequiredPublic URL to an audio file, with the same formats, length limit and verification as POST /api/songs/analyze: 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. The file is downloaded once for the whole batch.
Example: "https://example.com/song.mp3"
format: uri
promptsarray<SongAnalyzeBatchPrompt>requiredThe prompts to answer, 1 to 40. Each id must be unique within the batch. Answers come back in this order.
minItems: 1 · maxItems: 40
Item properties for prompts
idstringrequiredYour key for this prompt's answer. Unique within the batch.
Example: "genre"
minLength: 1 · maxLength: 64
promptstringrequiredThe question to ask about the audio.
Example: "What genre is this?"
minLength: 1 · maxLength: 4000
max_new_tokensintegerMaximum number of tokens in this prompt's answer.
Default: 400
Example: 64
minimum: 1 · maximum: 2048
temperaturenumberControls output creativity for every prompt in the batch. Lower than the single endpoint's default of 1, because batched questions are usually factual.
Default: 0.2
Example: 0.2
minimum: 0 · maximum: 2
top_pnumberNucleus sampling probability cutoff, for every prompt in the batch.
Default: 1
Example: 1
minimum: 0 · maximum: 1
do_samplebooleanEnable sampling for every prompt in the batch (set true when using temperature or top_p).
Default: false
Example: false
Two short questions about one file
{
"audio_url": "https://example.com/song.mp3",
"prompts": [
{
"id": "genre",
"prompt": "What genre is this?",
"max_new_tokens": 64
},
{
"id": "tempo",
"prompt": "What is the tempo in BPM?"
}
],
"temperature": 0.2,
"top_p": 1,
"do_sample": false
}Responses
200Every prompt was answered. answers holds one entry per prompt, in the order sent.+
application/json
statusstring · enumrequiredRequest status.
Values: "success"
answersarray<SongAnalyzeBatchAnswer>requiredOne answer per prompt, in the order the prompts were sent.
Item properties for answers
idstringrequiredThe id you gave the prompt.
responsestringrequiredThe model's text answer.
elapsed_secondsnumberrequiredInference time for this prompt alone, in seconds.
format: float
elapsed_secondsnumberrequiredWall time for the whole batch in seconds, audio download included. The charge is priced on this figure.
format: float
promptsintegerrequiredHow many prompts the batch answered.
Answers to the two questions above
{
"status": "success",
"answers": [
{
"id": "genre",
"response": "Indie pop.",
"elapsed_seconds": 1.4
},
{
"id": "tempo",
"response": "Approximately 96 BPM.",
"elapsed_seconds": 0.6
}
],
"elapsed_seconds": 3.1,
"prompts": 2
}400Bad request. The body is not valid JSON, or a field is invalid: audio_url missing, prompts empty or longer than 40, a prompt with an empty or over-long id or prompt, max_new_tokens outside 1 to 2048, or an id repeated within the batch. 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
Two prompts share an id
{
"status": "error",
"error": "prompt ids must be unique",
"missing_fields": [
"prompts"
]
}More than 40 prompts
{
"status": "error",
"error": "prompts cannot contain more than 40 prompts",
"missing_fields": [
"prompts"
]
}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 a batch ($0.05). 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 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 a batch
{
"error": "insufficient_credits",
"remaining_credits": 12000,
"required_credits": 50000,
"billingUrl": "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 rejected the batch. upstream_status is the status it answered with and detail is its error text; a 422 with audio_unreachable or audio_undecodable means it could not fetch the file or could not read it as audio. No answers are returned and nothing is charged.+
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 fetch the audio
{
"status": "error",
"error": "Music analysis failed (status 422)",
"upstream_status": 422,
"detail": "{\"detail\":\"audio_unreachable: could not download the audio file\"}"
}Full specification
Download the OpenAPI file for complete schemas, constraints, and examples.
Download releases.jsonView operation source
{
"operationId": "postSongsAnalyzeBatch",
"summary": "Ask many questions about one song",
"description": "Ask up to 40 short questions about one audio file in a single request. The audio is downloaded once, then every prompt is answered against it, one after another, in the order sent. Use it instead of one [`POST /api/songs/analyze`](/api-reference/songs/analyze) call per question: a batch pays the per-call base price once, not once per prompt.\n\nSend `audio_url` and `prompts`, an array of `{ id, prompt, max_new_tokens }`. `id` is your key for the answer (1 to 64 characters, unique within the batch), `prompt` is 1 to 4,000 characters, and the optional `max_new_tokens` (1 to 2048, default 400) bounds that one answer. `temperature` (default 0.2), `top_p` (default 1) and `do_sample` (default false) apply to every prompt in the batch.\n\nPricing: `$0.05` once per batch plus `$0.001166` per second of the batch's total inference time (`elapsed_seconds`, audio download included), charged once after the batch succeeds, as a single usage row. A batch that runs 90 seconds costs `$0.15494`. The balance must hold `$0.05` before the model runs, otherwise the response is `402`. A batch the model service rejects is not charged.\n\nA batch is all or nothing: if the model service rejects it, the response is `502` and no answers are returned. Prompts run one after another, so a batch of many long answers can take several minutes; the request stays open until it finishes, so use a generous client timeout.",
"security": [
{
"apiKeyAuth": []
},
{
"bearerAuth": []
}
],
"requestBody": {
"description": "One audio file and its prompts",
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/SongAnalyzeBatchRequest"
},
"examples": {
"twoQuestions": {
"summary": "Two short questions about one file",
"value": {
"audio_url": "https://example.com/song.mp3",
"prompts": [
{
"id": "genre",
"prompt": "What genre is this?",
"max_new_tokens": 64
},
{
"id": "tempo",
"prompt": "What is the tempo in BPM?"
}
],
"temperature": 0.2,
"top_p": 1,
"do_sample": false
}
}
}
}
}
},
"responses": {
"200": {
"description": "Every prompt was answered. `answers` holds one entry per prompt, in the order sent.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/SongAnalyzeBatchResponse"
},
"examples": {
"twoQuestions": {
"summary": "Answers to the two questions above",
"value": {
"status": "success",
"answers": [
{
"id": "genre",
"response": "Indie pop.",
"elapsed_seconds": 1.4
},
{
"id": "tempo",
"response": "Approximately 96 BPM.",
"elapsed_seconds": 0.6
}
],
"elapsed_seconds": 3.1,
"prompts": 2
}
}
}
}
}
},
"400": {
"description": "Bad request. The body is not valid JSON, or a field is invalid: `audio_url` missing, `prompts` empty or longer than 40, a prompt with an empty or over-long `id` or `prompt`, `max_new_tokens` outside 1 to 2048, or an `id` repeated within the batch. `missing_fields` is the path to the first invalid field.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/SongAnalyzeErrorResponse"
},
"examples": {
"repeatedId": {
"summary": "Two prompts share an id",
"value": {
"status": "error",
"error": "prompt ids must be unique",
"missing_fields": [
"prompts"
]
}
},
"tooManyPrompts": {
"summary": "More than 40 prompts",
"value": {
"status": "error",
"error": "prompts cannot contain more than 40 prompts",
"missing_fields": [
"prompts"
]
}
}
}
}
}
},
"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 a batch (`$0.05`). 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"
},
"examples": {
"short": {
"summary": "A balance that cannot cover a batch",
"value": {
"error": "insufficient_credits",
"remaining_credits": 12000,
"required_credits": 50000,
"billingUrl": "https://app.recoupable.dev"
}
}
}
}
}
},
"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 rejected the batch. `upstream_status` is the status it answered with and `detail` is its error text; a `422` with `audio_unreachable` or `audio_undecodable` means it could not fetch the file or could not read it as audio. No answers are returned and nothing is charged.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/SongAnalyzeUpstreamErrorResponse"
},
"examples": {
"rejectedBatch": {
"summary": "The model service could not fetch the audio",
"value": {
"status": "error",
"error": "Music analysis failed (status 422)",
"upstream_status": 422,
"detail": "{\"detail\":\"audio_unreachable: could not download the audio file\"}"
}
}
}
}
}
}
}
}Which to use
Use a batch when you have several short questions about the same song, for example genre, tempo, instruments and vocal style as four prompts. The audio is downloaded once and the base price is paid once, so a batch of 36 prompts costs one base price plus the seconds it runs, not 36 base prices.
Use POST /api/songs/analyze for a curated preset, for full_report, or for a single question. Presets are not available in a batch.
Use POST /api/songs/moments when you need the song's sections, moments and measurements. Those are measured from the audio, not answered by prompt.
Limits
| Limit | Value |
|---|---|
| Prompts per batch | 1 to 40 |
id | 1 to 64 characters, unique within the batch |
prompt | 1 to 4,000 characters |
max_new_tokens | 1 to 2048 per prompt, default 400 |
temperature | 0 to 2, default 0.2, for every prompt |
top_p | 0 to 1, default 1, for every prompt |
Answers come back in the order the prompts were sent, each under the id you gave it. A batch is all or nothing: if the model service rejects it, the response is 502 and no answers are returned.
Pricing
$0.05 once per batch plus $0.001166 per second of the batch's total inference time, which is the response's top-level elapsed_seconds. It is charged once, after the batch succeeds, as a single usage row. For example, a batch that runs 90 seconds costs $0.15494. The balance must hold $0.05 before the model runs; otherwise the response is 402 and nothing is charged. A batch the model service rejects is not charged. See Credits.