API reference

The BlueMetal Music API

One JSON endpoint turns a description and a set of lyrics into a finished song. Async by default, OpenAI-shaped errors, presigned audio, and a price of $0.000115 per second of audio.

3-minute quickstart#

Base URL https://api.bluemetal.ai. JSON in, JSON out, UTF-8. Every response carries an x-request-id. Prefer to click before you type? The playground runs exactly these calls.

1

Get a key

Sign in on the dashboard and create an API key. It is shown exactly once — we store only its SHA-256 hash — so copy it straight into your secret store.

export BLUEMETAL_API_KEY="bm_live_…"
2

Ask for a song

One POST. You get a generation id back immediately, along with a queue position and an estimated wait.

curl https://api.bluemetal.ai/v1/music/generations \
  -H "Authorization: Bearer $BLUEMETAL_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
        "model": "minimax-music-3",
        "prompt": "Lo-fi jazz hip-hop, 85 BPM, mellow Rhodes piano, soft female vocal",
        "lyrics": "[Verse]\nRain on the window, coffee cold\n[Chorus]\nStay a while, stay a while",
        "duration": 90
      }'
3

Poll until it is done

Read the generation until status is succeeded. audio.url is a presigned R2 link valid for 24 hours; re-reading the generation mints a fresh one. Objects are kept for 30 days.

curl https://api.bluemetal.ai/v1/music/generations/gen_01JABCDEF \
  -H "Authorization: Bearer $BLUEMETAL_API_KEY"

Authentication#

Server-to-server calls use a bearer key: Authorization: Bearer bm_live_…. Keys are created on the dashboard and shown exactly once — we store only sha256(key), so a lost key is replaced, never recovered. Never put one in client-side code.

The dashboard and playground use a session cookie instead. There are no passwords: sign in with GitHub, or ask for an email magic link that is valid for 15 minutes and can be used once.

A key belongs to one account and carries its credit. Revoking a key on the dashboard takes effect immediately — anything still using it starts getting 401.

Generation lifecycle#

A generation moves queuedrunningsucceeded, failed or canceled. Rendering costs roughly two-fifths of the song's own length on one RTX 5090 — a 60-second track lands in about 22 seconds, a three-minute track in about 70.

StatusWhat it means
queuednon-terminalAccepted and waiting for a worker. queue_position and estimated_wait_s are best-effort hints. Cancellable.
runningnon-terminalOn a GPU. No longer cancellable — a cancel returns 409.
succeededterminalaudio and usage are populated. audio.url is a presigned R2 GET valid for 24 hours; re-read the generation to mint a fresh one. Objects are deleted after 30 days.
failedterminalerror explains why. Retryable worker failures are already retried once on a different machine before you see this.
canceledterminalYou cancelled it while it was still queued. Nothing was billed.

Poll with backoff — start at a second and grow to about five, well inside the 600 reads/minute limit. If you would rather not poll at all, pass a webhook_url and we will call you once. And if the song is short and you can hold a connection, pass mode:"sync": it returns the finished object inside 90 seconds, or a 202 with status:"running" if it needs longer, so your client must handle both.

Send an Idempotency-Key on every create. A replay within 24 hours returns the original generation instead of paying for a second one — which matters most on the retry after a timeout, when you cannot tell whether the first request landed.

POST/v1/music/generations

Create a generation#

Turn a description and optional lyrics into a full song. Returns 202 with a queued generation in async mode, or 200 with a finished one in sync mode if it lands inside 90 seconds.

Headers
HeaderDescription
AuthorizationrequiredBearer bm_live_…
Idempotency-KeyoptionalA UUID. Replays within 24 hours return the original generation instead of creating a second one.
AcceptoptionalWith mode:"sync" and audio/mpeg (or audio/wav, audio/flac), a finished generation returns raw audio bytes rather than JSON, plus x-bluemetal-generation-id and x-bluemetal-usage-usd headers.
Parameters
FieldTypeDescription
modelstringrequiredModel id. Today the only value is minimax-music-3; see GET /v1/models.
promptstringrequiredWhat the song should sound like — genre, tempo, instrumentation, vocal. Max 2,000 characters.
lyricsstringoptionalLyrics, max 6,000 characters. Section tags such as [Verse], [Chorus] and [Bridge] are honoured and shape the arrangement.
durationintegerdefault 60Seconds of audio, 10–300. Best effort: the model may end a song early, and you are billed for delivered audio.
seedintegeroptionalThe same seed with the same inputs gives the same song.
formatstringdefault mp3mp3 (192 kbps), wav or flac.
modestringdefault asyncasync returns immediately. sync holds the connection for up to 90 s, then falls back to a 202 with status running.
webhook_urlstringoptionalPOSTed once on a terminal state, HMAC-signed. See Webhooks.
Request
curl https://api.bluemetal.ai/v1/music/generations \
  -H "Authorization: Bearer $BLUEMETAL_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{
        "model": "minimax-music-3",
        "prompt": "Upbeat indie pop, 120 BPM, female vocal, bright guitars",
        "lyrics": "[Verse]\nCity lights are humming low\n[Chorus]\nHold on, hold on, the night is ours",
        "duration": 120,
        "format": "mp3"
      }'
Response
{
  "id": "gen_01JABCDEF",
  "object": "music.generation",
  "model": "minimax-music-3",
  "status": "queued",
  "created_at": 1756500000,
  "started_at": null,
  "completed_at": null,
  "audio": null,
  "usage": null,
  "error": null,
  "queue_position": 2,
  "estimated_wait_s": 25
}
GET/v1/music/generations/{id}

Retrieve a generation#

The same object, with audio and usage filled in once it succeeds. 404 if the generation is not yours. Poll this with backoff; every read mints a fresh 24-hour audio URL.

Parameters
FieldTypeDescription
idstringpathThe generation id returned by the create call.
Request
curl https://api.bluemetal.ai/v1/music/generations/gen_01JABCDEF \
  -H "Authorization: Bearer $BLUEMETAL_API_KEY"
Response
{
  "id": "gen_01JABCDEF",
  "object": "music.generation",
  "status": "succeeded",
  "created_at": 1756500000,
  "started_at": 1756500003,
  "completed_at": 1756500043,
  "audio": {
    "url": "https://…r2 presigned GET…",
    "format": "mp3",
    "duration": 178.4,
    "bytes": 2914560,
    "sample_rate": 32000,
    "expires_at": 1756586443
  },
  "usage": { "audio_seconds": 178.4, "amount_usd": 0.0107 },
  "error": null
}
GET/v1/music/generations

List generations#

Your generations, newest first, cursor-paginated.

Parameters
FieldTypeDescription
limitintegerdefault 20How many to return.
afterstringoptionalA generation id to page after.
statusstringoptionalFilter to queued, running, succeeded, failed or canceled.
Request
curl "https://api.bluemetal.ai/v1/music/generations?limit=20&status=succeeded" \
  -H "Authorization: Bearer $BLUEMETAL_API_KEY"
Response
{ "object": "list", "data": [ { … } ], "has_more": false }
POST/v1/music/generations/{id}/cancel

Cancel a generation#

Cancels a generation that is still queued. 409 once it has started — a running job is already burning GPU.

Parameters
FieldTypeDescription
idstringpathThe generation to cancel.
Request
curl -X POST https://api.bluemetal.ai/v1/music/generations/gen_01JABCDEF/cancel \
  -H "Authorization: Bearer $BLUEMETAL_API_KEY"
Response
{ "id": "gen_01JABCDEF", "status": "canceled", … }
GET/v1/models

List models#

An OpenAI-shaped model list, extended with the fields a provider listing wants: modalities, quantization, max duration, per-second pricing and supported parameters. GET /v1/models/{id} returns one.

Request
curl https://api.bluemetal.ai/v1/models -H "Authorization: Bearer $BLUEMETAL_API_KEY"
Response
{
  "object": "list",
  "data": [{
    "id": "minimax-music-3",
    "object": "model",
    "owned_by": "minimax",
    "name": "MiniMax Music 3",
    "description": "Text+lyrics → full song, up to 5 minutes, 32 kHz stereo",
    "input_modalities": ["text"],
    "output_modalities": ["audio"],
    "quantization": "int4-mixed",
    "max_duration_seconds": 300,
    "pricing": { "audio_second": "0.000115", "unit": "USD" },
    "supported_parameters": ["prompt", "lyrics", "duration", "seed", "format"],
    "endpoints": ["/v1/music/generations", "/v1/audio/speech"]
  }]
}
POST/v1/audio/speech

OpenAI-compatible alias#

Exists so an OpenAI SDK works with only a base-URL swap. input maps to lyrics and instructions to prompt. Always synchronous, and it returns raw audio bytes rather than JSON.

Parameters
FieldTypeDescription
modelstringrequiredminimax-music-3.
inputstringrequiredMaps to lyrics.
instructionsstringoptionalMaps to prompt — the style description.
response_formatstringoptionalmp3, wav or flac.
seedintegeroptionalSame meaning as on the native endpoint.
Request
curl https://api.bluemetal.ai/v1/audio/speech \
  -H "Authorization: Bearer $BLUEMETAL_API_KEY" \
  -H "Content-Type: application/json" \
  -o song.mp3 \
  -d '{
        "model": "minimax-music-3",
        "instructions": "Synthwave, 110 BPM, retro 80s synths",
        "input": "[Verse]\nNeon rivers, chrome and glass",
        "response_format": "mp3"
      }'
Response

Raw audio bytes with Content-Type: audio/mpeg | audio/wav | audio/flac.

GET/v1/account

Account#

Your account, including the remaining prepaid credit. Session cookie or an API key.

Request
curl https://api.bluemetal.ai/v1/account -H "Authorization: Bearer $BLUEMETAL_API_KEY"
Response
{ "id": "acct_…", "email": "you@example.com", "credits_usd": 0.4731, "created_at": 1756400000, "plan": "free" }
POST/v1/keys

API keys#

POST /v1/keys creates a key and returns the secret exactly once — only sha256(key) is stored. GET /v1/keys lists keys without their secrets, showing a prefix. DELETE /v1/keys/{id} revokes one and returns 204.

Parameters
FieldTypeDescription
namestringrequiredA label you will recognise later, e.g. "prod".
Request
curl https://api.bluemetal.ai/v1/keys \
  -H "Authorization: Bearer $BLUEMETAL_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"name":"prod"}'
Response
{ "id": "key_…", "name": "prod", "key": "bm_live_…", "created_at": 1756400000, "last_used_at": null }
GET/v1/usage

Usage#

Generations, audio seconds and spend, bucketed by day.

Parameters
FieldTypeDescription
fromintegeroptionalUnix timestamp, inclusive.
tointegeroptionalUnix timestamp, exclusive.
bucketstringdefault dayBucket size.
Request
curl "https://api.bluemetal.ai/v1/usage?from=1756425600&bucket=day" \
  -H "Authorization: Bearer $BLUEMETAL_API_KEY"
Response
{
  "data": [
    { "ts": 1756425600, "generations": 14, "audio_seconds": 1382.6, "amount_usd": 0.0829 }
  ],
  "total": { "generations": 143, "audio_seconds": 14204.1, "amount_usd": 0.8522 }
}

Webhooks#

Pass webhook_url on a create and we POST the whole generation object once it reaches a terminal state, as {"type":"music.generation.succeeded","data":{…}}. Non-2xx responses are retried three times, after 10 s, 60 s and 300 s.

POST https://your-app.example.com/hooks/bluemetal
bluemetal-signature: t=1756500043,v1=9f86d081884c7d659a2feaa0c55ad015…
bluemetal-delivery: 7c9e6679-7425-40de-944b-e07fc1f90ae7

{
  "type": "music.generation.succeeded",
  "data": { "id": "gen_01JABCDEF", "status": "succeeded", "audio": { … }, "usage": { … } }
}

Verify the signature before you trust the body. It is an HMAC-SHA256 of t + "." + body using your account's signing secret, which lives on the dashboard. Compare in constant time and reject timestamps more than a few minutes old.

import crypto from "node:crypto"

export function verify(rawBody, header, secret) {
  const parts = Object.fromEntries(header.split(",").map((p) => p.split("=")))
  const age = Math.abs(Date.now() / 1000 - Number(parts.t))
  if (age > 300) return false                        // reject replays
  const expected = crypto
    .createHmac("sha256", secret)
    .update(`${parts.t}.${rawBody}`)
    .digest("hex")
  return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(parts.v1))
}

Errors#

Errors are OpenAI-shaped, with the matching HTTP status. Everything 4xx and 5xx is logged against the x-request-id on the response — quote it and we can find the exact request.

{
  "error": {
    "message": "duration must be between 10 and 300",
    "type": "invalid_request_error",
    "code": "invalid_request",
    "param": "duration"
  }
}
StatusCodeWhen
400invalid_requestA field is missing or out of range — a prompt over 2,000 characters, a duration outside 10–300.
401invalid_api_keyThe key is missing, malformed, revoked or not yours. Keys start bm_live_.
402insufficient_creditsThe request would cost more than your remaining balance. Credit is checked at submission and debited at completion, so a job is never half-billed. There is no overage in v1.
404not_foundNo such generation or key — or it belongs to another account. We do not distinguish the two.
409conflictYou tried to cancel a generation that had already started.
413too_largeThe body exceeded the size limit.
429rate_limit_exceededOver the concurrency or request-rate limit. Read retry-after and x-ratelimit-reset and back off.
500server_errorOur fault. Every 4xx and 5xx is logged with the x-request-id from the response — quote it and we can find the exact request.
503no_capacityEvery GPU is busy and the queue is full. Retry with backoff; nothing was billed.

Rate limits#

LimitValueNotes
Concurrent generations10Per account, counted across queued and running jobs.
Write requests60 / minCreates and cancels.
Read requests600 / minPolling, listing, models, usage.

Every response carries x-ratelimit-limit, x-ratelimit-remaining, x-ratelimit-reset, retry-after (on 429 only). On a 429, wait for retry-after rather than retrying immediately — a tight retry loop is the fastest way to stay rate limited.

Pricing#

$0.000115 per second of delivered audio, with a 15-second minimum per request. You are billed for what the model actually produced, never for idle GPU time, and credit is debited only when a generation succeeds.

TrackPrice
60 s track$0.0069
180 s track$0.0207
300 s track$0.0345

New accounts get $0.50 of credit — about 24 three-minute tracks — with no card. When the balance hits zero, creates return 402: there is no overage and no invoice in v1.

Available now

minimax-music-3 — MiniMax Music 3, text and lyrics to a full song of up to five minutes, 32 kHz stereo. It is the only model behind this API today; the request shape is the one every model we add will use.