Recoup

Get started with Recoup.

API REFERENCE

Analyze songs

On this page
POST/api/songs/analyze

Analyze 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
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 for this request
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 · enum

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

promptstring

Text prompt or question about the music

Example: "Describe the genre, tempo, and mood of this track."

minLength: 1 · maxLength: 24000

audio_urlstringrequired

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"

format: uri

max_new_tokensinteger

Maximum number of tokens to generate

Default: 512

Example: 512

minimum: 1 · maximum: 2048

temperaturenumber

Controls output creativity — higher values produce more varied responses

Default: 1

Example: 0.7

minimum: 0 · maximum: 2

top_pnumber

Nucleus sampling probability cutoff

Default: 1

Example: 0.9

minimum: 0 · maximum: 1

do_sampleboolean

Enable sampling (set true when using temperature or top_p)

Default: false

Example: false

One structured analysis

preset
{
  "preset": "mood_tags",
  "audio_url": "https://example.com/song.m4a"
}

A free-form question

prompt
{
  "prompt": "Describe the genre, tempo, and mood of this track.",
  "audio_url": "https://example.com/song.mp3",
  "max_new_tokens": 512
}

All 13 sections

fullReport
{
  "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 · enum

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.

Values: "success", "partial"

presetstring

Preset used for analysis, when applicable

Example: "catalog_metadata"

responseany

Model output for single-preset or custom-prompt analysis. May be plain text or structured JSON depending on the preset.

reportobject

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_secondsnumber

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.

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

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

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)

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

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

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"

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 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 · 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 read the audio

rejectedCall
{
  "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

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

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 keyPreset to retry it with
overviewcatalog_metadata
moodmood_tags
descriptionsong_description
lyricslyric_transcription
content_advisorycontent_advisory
music_theorymusic_theory
mix_feedbackmix_feedback
similar_artistssimilar_artists
sample_detectionsample_detection
audienceaudience_profile
syncsync_brief_match
playlist_pitchplaylist_pitch
developmentartist_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.