# Analyze songs

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

Analyze music with an audio language model that listens directly to the audio waveform.

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

```json
{
  "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](https://docs.recoupable.dev/credits).

## POST /api/songs/analyze

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": {
      "post": {
        "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"
                      ]
                    }
                  }
                }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "schemas": {
      "SongAnalyzeRequest": {
        "type": "object",
        "description": "Provide exactly one of `preset` or `prompt`, plus `audio_url`. Use `preset` for structured analysis workflows, or `prompt` for free-form questions about the audio.",
        "required": [
          "audio_url"
        ],
        "properties": {
          "preset": {
            "type": "string",
            "enum": [
              "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"
            ],
            "description": "Name 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](/api-reference/songs/analyze-presets) for the full list of available presets.",
            "example": "catalog_metadata"
          },
          "prompt": {
            "type": "string",
            "minLength": 1,
            "maxLength": 24000,
            "description": "Text prompt or question about the music",
            "example": "Describe the genre, tempo, and mood of this track."
          },
          "audio_url": {
            "type": "string",
            "format": "uri",
            "description": "Public 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"
          },
          "max_new_tokens": {
            "type": "integer",
            "minimum": 1,
            "maximum": 2048,
            "default": 512,
            "description": "Maximum number of tokens to generate",
            "example": 512
          },
          "temperature": {
            "type": "number",
            "minimum": 0,
            "maximum": 2,
            "default": 1,
            "description": "Controls output creativity — higher values produce more varied responses",
            "example": 0.7
          },
          "top_p": {
            "type": "number",
            "minimum": 0,
            "maximum": 1,
            "default": 1,
            "description": "Nucleus sampling probability cutoff",
            "example": 0.9
          },
          "do_sample": {
            "type": "boolean",
            "default": false,
            "description": "Enable sampling (set true when using temperature or top_p)",
            "example": false
          }
        }
      },
      "SongAnalyzeResponse": {
        "type": "object",
        "properties": {
          "status": {
            "type": "string",
            "enum": [
              "success",
              "partial"
            ],
            "description": "`success` 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."
          },
          "preset": {
            "type": "string",
            "description": "Preset used for analysis, when applicable",
            "example": "catalog_metadata"
          },
          "response": {
            "description": "Model output for single-preset or custom-prompt analysis. May be plain text or structured JSON depending on the preset."
          },
          "report": {
            "type": "object",
            "description": "Full 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_seconds": {
            "type": "number",
            "format": "float",
            "description": "Inference 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."
          },
          "failed": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "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](/api-reference/songs/analyze) page).",
            "example": [
              "lyrics",
              "sync"
            ]
          }
        }
      },
      "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, 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
          },
          "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"
        ]
      }
    },
    "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"
      }
    }
  }
}
```
