Recoup

Get started with Recoup.

API REFERENCE

Analyze a song with many prompts

On this page
POST/api/songs/analyze/batch

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 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
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 for this request
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_urlstringrequired

Public 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>required

The 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
idstringrequired

Your key for this prompt's answer. Unique within the batch.

Example: "genre"

minLength: 1 · maxLength: 64

promptstringrequired

The question to ask about the audio.

Example: "What genre is this?"

minLength: 1 · maxLength: 4000

max_new_tokensinteger

Maximum number of tokens in this prompt's answer.

Default: 400

Example: 64

minimum: 1 · maximum: 2048

temperaturenumber

Controls 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_pnumber

Nucleus sampling probability cutoff, for every prompt in the batch.

Default: 1

Example: 1

minimum: 0 · maximum: 1

do_sampleboolean

Enable 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

twoQuestions
{
  "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 · enumrequired

Request status.

Values: "success"

answersarray<SongAnalyzeBatchAnswer>required

One answer per prompt, in the order the prompts were sent.

Item properties for answers
idstringrequired

The id you gave the prompt.

responsestringrequired

The model's text answer.

elapsed_secondsnumberrequired

Inference time for this prompt alone, in seconds.

format: float

elapsed_secondsnumberrequired

Wall time for the whole batch in seconds, audio download included. The charge is priced on this figure.

format: float

promptsintegerrequired

How many prompts the batch answered.

Answers to the two questions above

twoQuestions
{
  "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 · 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

Two prompts share an id

repeatedId
{
  "status": "error",
  "error": "prompt ids must be unique",
  "missing_fields": [
    "prompts"
  ]
}

More than 40 prompts

tooManyPrompts
{
  "status": "error",
  "error": "prompts cannot contain more than 40 prompts",
  "missing_fields": [
    "prompts"
  ]
}

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 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 · 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 a batch

short
{
  "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 · 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"

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 · 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

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 · enumrequired

Values: "error"

errorstringrequired

Music 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_statusinteger

The 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

detailstring

The 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

rejectedBatch
{
  "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.json
View operation source
json
{
  "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

LimitValue
Prompts per batch1 to 40
id1 to 64 characters, unique within the batch
prompt1 to 4,000 characters
max_new_tokens1 to 2048 per prompt, default 400
temperature0 to 2, default 0.2, for every prompt
top_p0 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.