# Analyze a song with many prompts

Source: https://docs.recoupable.dev/api-reference/songs/analyze-batch

Ask up to 40 short questions about one audio file in a single request and pay the base price once.

## 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`](https://docs.recoupable.dev/api-reference/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`](https://docs.recoupable.dev/api-reference/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](https://docs.recoupable.dev/credits).

## POST /api/songs/analyze/batch

Full OpenAPI specification: https://docs.recoupable.dev/spec/releases.json

## Authentication

This operation requires one of the security alternatives in the specification below. Security scheme definitions are included where present in the published specification.

[Authentication guide](https://docs.recoupable.dev/authentication)

## Operation and referenced schemas

```json
{
  "openapi": "3.1.0",
  "info": {
    "title": "Recoup API - Releases",
    "description": "API documentation for the Recoup platform - an AI agent platform for the music industry",
    "license": {
      "name": "MIT"
    },
    "version": "1.0.0"
  },
  "servers": [
    {
      "url": "https://api.recoupable.dev"
    }
  ],
  "paths": {
    "/api/songs/analyze/batch": {
      "post": {
        "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\"}"
                    }
                  }
                }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "schemas": {
      "SongAnalyzeBatchRequest": {
        "type": "object",
        "description": "One audio file and up to 40 short prompts about it. Presets are not available in a batch; use [`POST /api/songs/analyze`](/api-reference/songs/analyze) for those.",
        "required": [
          "audio_url",
          "prompts"
        ],
        "properties": {
          "audio_url": {
            "type": "string",
            "format": "uri",
            "description": "Public URL to an audio file, with the same formats, length limit and verification as [`POST /api/songs/analyze`](/api-reference/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"
          },
          "prompts": {
            "type": "array",
            "minItems": 1,
            "maxItems": 40,
            "items": {
              "$ref": "#/components/schemas/SongAnalyzeBatchPrompt"
            },
            "description": "The prompts to answer, 1 to 40. Each `id` must be unique within the batch. Answers come back in this order."
          },
          "temperature": {
            "type": "number",
            "minimum": 0,
            "maximum": 2,
            "default": 0.2,
            "description": "Controls output creativity for every prompt in the batch. Lower than the single endpoint's default of 1, because batched questions are usually factual.",
            "example": 0.2
          },
          "top_p": {
            "type": "number",
            "minimum": 0,
            "maximum": 1,
            "default": 1,
            "description": "Nucleus sampling probability cutoff, for every prompt in the batch.",
            "example": 1
          },
          "do_sample": {
            "type": "boolean",
            "default": false,
            "description": "Enable sampling for every prompt in the batch (set true when using `temperature` or `top_p`).",
            "example": false
          }
        }
      },
      "SongAnalyzeBatchResponse": {
        "type": "object",
        "required": [
          "status",
          "answers",
          "elapsed_seconds",
          "prompts"
        ],
        "properties": {
          "status": {
            "type": "string",
            "enum": [
              "success"
            ],
            "description": "Request status."
          },
          "answers": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/SongAnalyzeBatchAnswer"
            },
            "description": "One answer per prompt, in the order the prompts were sent."
          },
          "elapsed_seconds": {
            "type": "number",
            "format": "float",
            "description": "Wall time for the whole batch in seconds, audio download included. The charge is priced on this figure."
          },
          "prompts": {
            "type": "integer",
            "description": "How many prompts the batch answered."
          }
        }
      },
      "SongAnalyzeErrorResponse": {
        "type": "object",
        "properties": {
          "status": {
            "type": "string",
            "enum": [
              "error"
            ],
            "description": "Error status"
          },
          "missing_fields": {
            "type": "array",
            "description": "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\"]`.",
            "items": {
              "type": [
                "string",
                "integer"
              ]
            }
          },
          "error": {
            "type": "string",
            "description": "Error message describing what went wrong"
          }
        }
      },
      "SongAnalyzeInsufficientCreditsResponse": {
        "type": "object",
        "description": "Returned (402) when the account lacks credits for the analysis. Nothing is charged and no Stripe object is created. Credits must be bought explicitly via [`POST /api/credits/sessions`](/api-reference/credits/sessions-create).",
        "properties": {
          "error": {
            "type": "string",
            "enum": [
              "insufficient_credits"
            ]
          },
          "remaining_credits": {
            "type": "integer",
            "description": "The account's balance, in credits (micro-dollars: 1,000,000 = `$1.00`).",
            "example": 0
          },
          "required_credits": {
            "type": "integer",
            "description": "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
          },
          "billingUrl": {
            "type": "string",
            "description": "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`](/api-reference/credits/sessions-create).",
            "example": "https://app.recoupable.dev"
          }
        },
        "required": [
          "error",
          "remaining_credits",
          "required_credits",
          "billingUrl"
        ]
      },
      "SongAnalyzeAudioUrlErrorResponse": {
        "type": "object",
        "description": "Returned (422) when `audio_url` could not be verified. The model is not called and nothing is charged.",
        "properties": {
          "status": {
            "type": "string",
            "enum": [
              "error"
            ]
          },
          "error": {
            "type": "string",
            "enum": [
              "audio_url_unreachable",
              "audio_url_not_audio"
            ],
            "description": "`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`.",
            "example": "audio_url_not_audio"
          },
          "message": {
            "type": "string",
            "description": "What the check observed, for the caller's logs.",
            "example": "audio_url answered 200 with content type text/html"
          }
        },
        "required": [
          "status",
          "error",
          "message"
        ]
      },
      "SongAnalyzeUpstreamErrorResponse": {
        "type": "object",
        "description": "Returned (502) when the model service could not produce the analysis. Nothing is charged for the failed work. There are two forms. A single preset or custom prompt the model service rejected carries `upstream_status` and `detail`. A `full_report` in which every section failed carries `failed` instead.",
        "properties": {
          "status": {
            "type": "string",
            "enum": [
              "error"
            ]
          },
          "error": {
            "type": "string",
            "description": "`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_status": {
            "type": "integer",
            "description": "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
          },
          "detail": {
            "type": "string",
            "description": "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."
          },
          "failed": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "All 13 report keys, in report order. Present on a `full_report` in which every section failed only."
          }
        },
        "required": [
          "status",
          "error"
        ]
      },
      "SongAnalyzeBatchPrompt": {
        "type": "object",
        "required": [
          "id",
          "prompt"
        ],
        "properties": {
          "id": {
            "type": "string",
            "minLength": 1,
            "maxLength": 64,
            "description": "Your key for this prompt's answer. Unique within the batch.",
            "example": "genre"
          },
          "prompt": {
            "type": "string",
            "minLength": 1,
            "maxLength": 4000,
            "description": "The question to ask about the audio.",
            "example": "What genre is this?"
          },
          "max_new_tokens": {
            "type": "integer",
            "minimum": 1,
            "maximum": 2048,
            "default": 400,
            "description": "Maximum number of tokens in this prompt's answer.",
            "example": 64
          }
        }
      },
      "SongAnalyzeBatchAnswer": {
        "type": "object",
        "required": [
          "id",
          "response",
          "elapsed_seconds"
        ],
        "properties": {
          "id": {
            "type": "string",
            "description": "The `id` you gave the prompt."
          },
          "response": {
            "type": "string",
            "description": "The model's text answer."
          },
          "elapsed_seconds": {
            "type": "number",
            "format": "float",
            "description": "Inference time for this prompt alone, in seconds."
          }
        }
      }
    },
    "securitySchemes": {
      "apiKeyAuth": {
        "type": "apiKey",
        "in": "header",
        "name": "x-api-key",
        "description": "Your Recoup API key. [Learn more](/quickstart#api-keys)."
      },
      "bearerAuth": {
        "type": "http",
        "scheme": "bearer"
      }
    }
  }
}
```
