{"openapi":"3.1.0","info":{"title":"Tunova","version":"1.0.0","description":"Suno-AI music generation API. Generation is **always async**: submit returns `202 {job_id}`; poll `GET /api/jobs/{id}` or pass `callback_url` for an HMAC-signed webhook. Billed in **tokens, on success only** (a failed render is refunded). Every response carries `X-Request-Id`. An **MCP server** for AI agents (Claude, Cursor, …) is also available at `POST /mcp` (stateless Streamable HTTP; tools `generate_song` / `wait_for_song` / `check_song`) — same `X-API-Key`, same token billing."},"servers":[{"url":"https://api.tunova.ai","description":"production"}],"security":[{"ApiKeyAuth":[]}],"tags":[{"name":"generation","description":"Create songs (async)"},{"name":"jobs","description":"Poll async jobs"},{"name":"utility","description":"Lyrics, clips, limits"}],"paths":{"/api/generate":{"post":{"tags":["generation"],"operationId":"generateSong","summary":"Generate a song from a prompt","description":"Simple mode: a text description. When generation capacity is available, returns `202` + a durable `job_id`. Keep polling the returned job; after admission, an uncertain transport outcome is reconciled to completion or failure/refund. When neither the selected session route nor a legacy BOX/account pair is dispatch-ready, returns `503` before creating a job or reserving tokens. Costs 10 tokens, settled on success.","parameters":[{"$ref":"#/components/parameters/IdempotencyKey"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/GenerateRequest"}}}},"responses":{"202":{"$ref":"#/components/responses/JobAccepted"},"400":{"$ref":"#/components/responses/Error"},"401":{"$ref":"#/components/responses/Error"},"402":{"$ref":"#/components/responses/InsufficientTokens"},"403":{"$ref":"#/components/responses/Error"},"409":{"$ref":"#/components/responses/IdempotencyConflict"},"429":{"$ref":"#/components/responses/RateLimited"},"503":{"$ref":"#/components/responses/GenerationUnavailable"}}}},"/api/custom_generate":{"post":{"tags":["generation"],"operationId":"customGenerate","summary":"Generate a song from your own lyrics + style","description":"Custom mode: supply `prompt` (your lyrics) plus `tags` (style) and `title`. When generation capacity is available, returns `202` + a durable `job_id`. Keep polling the returned job; after admission, an uncertain transport outcome is reconciled to completion or failure/refund. When neither the selected session route nor a legacy BOX/account pair is dispatch-ready, returns `503` before creating a job or reserving tokens. Costs 10 tokens, settled on success.","parameters":[{"$ref":"#/components/parameters/IdempotencyKey"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CustomGenerateRequest"}}}},"responses":{"202":{"$ref":"#/components/responses/JobAccepted"},"400":{"$ref":"#/components/responses/Error"},"401":{"$ref":"#/components/responses/Error"},"402":{"$ref":"#/components/responses/InsufficientTokens"},"403":{"$ref":"#/components/responses/Error"},"409":{"$ref":"#/components/responses/IdempotencyConflict"},"429":{"$ref":"#/components/responses/RateLimited"},"503":{"$ref":"#/components/responses/GenerationUnavailable"}}}},"/api/jobs/{id}":{"get":{"tags":["jobs"],"operationId":"getJob","summary":"Get job status + final clips","description":"Poll fallback for webhooks. While `status` remains `submitted`, `phase: streaming` and `preview_clips` may expose transient playable audiopipe URLs. They are previews only: wait for `status: complete` and `clips` for stable CDN MP3 URLs, final quality validation and billing settlement.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Job state","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Job"}}}},"401":{"$ref":"#/components/responses/Error"},"403":{"$ref":"#/components/responses/Error"},"404":{"$ref":"#/components/responses/Error"}}}},"/api/lyrics":{"post":{"tags":["utility"],"operationId":"generateLyrics","summary":"Generate song lyrics","description":"Normally completes synchronously with `200`. The paid operation is durable: its token hold, provider id, result/error and settlement survive crashes and response loss. A concurrent/recovery retry may return `202`; poll the supplied `status_url` or retry the same POST with the same Idempotency-Key. Costs 2 tokens on success and refunds atomically on failure.","parameters":[{"$ref":"#/components/parameters/IdempotencyKey"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["prompt"],"properties":{"prompt":{"type":"string","description":"Theme / brief for the lyrics"}}}}}},"responses":{"200":{"description":"Lyrics","content":{"application/json":{"schema":{"type":"object","properties":{"lyrics":{"type":"object","additionalProperties":true},"job_id":{"type":"string"},"idempotent_replay":{"type":"boolean"}}}}}},"202":{"$ref":"#/components/responses/JobAccepted"},"400":{"$ref":"#/components/responses/Error"},"401":{"$ref":"#/components/responses/Error"},"402":{"$ref":"#/components/responses/InsufficientTokens"},"403":{"$ref":"#/components/responses/Error"},"409":{"$ref":"#/components/responses/IdempotencyConflict"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/api/clips":{"get":{"tags":["utility"],"operationId":"listClips","summary":"Read clips by id (owner-only)","description":"Operator utility — requires the grandfathered owner key. Customer `sk_live_` keys get **403**; customers read clips via `GET /api/jobs/{id}` instead.","parameters":[{"name":"ids","in":"query","schema":{"type":"string"},"description":"Comma-separated clip ids"}],"responses":{"200":{"description":"Clips","content":{"application/json":{"schema":{"type":"object","properties":{"account_id":{"type":"string"},"clips":{"type":"array","items":{"$ref":"#/components/schemas/Clip"}}}}}}},"401":{"$ref":"#/components/responses/Error"},"403":{"$ref":"#/components/responses/Error"}}}},"/api/limits":{"get":{"tags":["utility"],"operationId":"getLimits","summary":"Account credit/limit snapshot (owner-only)","description":"Operator utility — requires the grandfathered owner key. Customer `sk_live_` keys get **403**; partners should use `GET /api/balance`.","responses":{"200":{"description":"Limits","content":{"application/json":{"schema":{"type":"object","properties":{"account_id":{"type":"string"},"limits":{"type":"object","additionalProperties":true}}}}}},"401":{"$ref":"#/components/responses/Error"},"403":{"$ref":"#/components/responses/Error"}}}},"/api/jobs":{"get":{"tags":["jobs"],"operationId":"listJobs","summary":"List your recent jobs","description":"The caller's own recent generations (tenant-scoped; the owner key sees all). Newest first; `?limit=` 1–100 (default 50). Jobs are retained ~7 days.","parameters":[{"name":"limit","in":"query","schema":{"type":"integer","default":50,"minimum":1,"maximum":100}}],"responses":{"200":{"description":"Recent jobs","content":{"application/json":{"schema":{"type":"object","properties":{"jobs":{"type":"array","items":{"$ref":"#/components/schemas/Job"}}}}}}},"401":{"$ref":"#/components/responses/Error"},"403":{"$ref":"#/components/responses/Error"}}}},"/api/me":{"get":{"tags":["utility"],"operationId":"getMe","deprecated":true,"summary":"Legacy alias for your token balance","description":"Backward-compatible alias of `GET /api/balance`. New integrations should use `/api/balance`.","responses":{"200":{"description":"Account","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Balance"}}}},"401":{"$ref":"#/components/responses/Error"},"403":{"$ref":"#/components/responses/Error"}}}},"/api/balance":{"get":{"tags":["utility"],"operationId":"getBalance","summary":"Get your current Tunova token balance","description":"Machine-readable balance + live engine depth so an SDK/agent can pre-flight and self-pace instead of probing 402/429 bodies. A hold is already debited from `token_balance`, so `available` equals the non-negative current balance; `reserved` discloses in-flight holds and `total_liability = max(token_balance + reserved, 0)` reconstructs the remaining entitlement. `engine_inflight` = current fleet depth (queued+processing) across all clients. `concurrency_limit` is always `null` — there is no hard admission cap; the queue absorbs bursts.","responses":{"200":{"description":"Current partner balance","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Balance"}}}},"401":{"$ref":"#/components/responses/Error"},"403":{"$ref":"#/components/responses/Error"}}}},"/api/models":{"get":{"tags":["utility"],"operationId":"listModels","summary":"Selectable models + costs","description":"Models you can pass as `model` on generate, the account default, and per-call token costs (discovery for SDKs/agents).","responses":{"200":{"description":"Models","content":{"application/json":{"schema":{"type":"object","properties":{"models":{"type":"array","items":{"type":"string"}},"default":{"type":["string","null"]},"song_cost":{"type":"integer"},"lyrics_cost":{"type":"integer"}}}}}},"401":{"$ref":"#/components/responses/Error"},"403":{"$ref":"#/components/responses/Error"}}}}},"webhooks":{"jobComplete":{"post":{"summary":"Delivered to your callback_url on terminal state","description":"Signed: `X-Webhook-Signature: sha256=<hmac>` over `<X-Webhook-Timestamp>.<body>` (key = your webhook secret, `whsec_…` — view/rotate it on the dashboard's API keys page). Retried 3× with backoff.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"job_id":{"type":"string"},"status":{"type":"string","enum":["complete","failed"]},"clips":{"type":"array","items":{"$ref":"#/components/schemas/Clip"}},"error":{"type":["string","null"]}}}}}},"responses":{"200":{"description":"Acknowledged (any 2xx)"}}}}},"components":{"securitySchemes":{"ApiKeyAuth":{"type":"apiKey","in":"header","name":"X-API-Key","description":"Per-customer key, e.g. sk_live_…"}},"parameters":{"IdempotencyKey":{"name":"Idempotency-Key","in":"header","required":false,"schema":{"type":"string","maxLength":128},"description":"Reuse for retries of one logical submission to get the same job and avoid duplicate renders/charges. Scoped to your account and bound to the accepted canonical request. Reusing a key with a different prompt, mode, model, generation options, or callback URL returns `409 IDEMPOTENCY_CONFLICT`; use a new key for new work. Maximum 128 UTF-8 bytes. While the job is retained (normally about 7 days), the key replays it even after terminal state; to start a new generation after `failed`, send a NEW Idempotency-Key. After retention expires, the old key may create a new job."}},"schemas":{"Balance":{"type":"object","required":["client_id","balance","token_balance","reserved","available","total_liability","status","song_cost","lyrics_cost"],"properties":{"client_id":{"type":"string"},"balance":{"type":"integer","description":"Current number of tokens the partner can spend"},"token_balance":{"type":"integer","description":"Current spendable balance; in-flight holds have already been debited"},"reserved":{"type":"integer","description":"Tokens held by non-terminal jobs (already debited from token_balance)"},"available":{"type":"integer","description":"Spendable now: max(token_balance, 0)"},"total_liability":{"type":"integer","description":"Remaining entitlement: max(token_balance + reserved, 0)"},"daily_token_cap":{"type":["integer","null"]},"status":{"type":"string"},"song_cost":{"type":"integer"},"lyrics_cost":{"type":"integer"},"rate_limit_rpm":{"type":"integer","description":"Per-key requests/minute limit"},"concurrency_limit":{"type":["integer","null"],"description":"Always null — no hard admission cap"},"engine_inflight":{"type":["integer","null"],"description":"Jobs currently queued+processing across all clients"}}},"GenerateRequest":{"type":"object","required":["prompt"],"example":{"prompt":"calm rainy-night lofi","model":"v5.5"},"properties":{"prompt":{"type":"string","description":"Free-text description of the song"},"make_instrumental":{"type":"boolean","default":false},"model":{"type":"string","enum":["v5.5"],"description":"Suno model version (default: account default)"},"callback_url":{"type":"string","format":"uri","description":"Public http(s) URL for the completion webhook"},"streaming_callback":{"type":"boolean","default":false,"description":"Opt in to an additional signed `status: streaming` preview webhook. Requires callback_url; it may also be enabled as an account default. Terminal complete/failed webhooks are unchanged."}}},"CustomGenerateRequest":{"type":"object","required":["prompt"],"example":{"prompt":"[Verse]\nRain taps the window, slow and low\n[Chorus]\nStay a while, let the night go","tags":"lofi hip-hop, rainy, mellow","title":"Rain On Glass","model":"v5.5"},"properties":{"prompt":{"type":"string","description":"Your lyrics. CRLF/CR line endings are normalized to LF and outer whitespace is trimmed; interior whitespace is preserved."},"tags":{"type":"string","description":"Style / genre tags"},"title":{"type":"string"},"make_instrumental":{"type":"boolean","default":false},"model":{"type":"string","enum":["v5.5"],"description":"Suno model version (default: account default)"},"callback_url":{"type":"string","format":"uri","description":"Public http(s) URL for the completion webhook"},"streaming_callback":{"type":"boolean","default":false,"description":"Opt in to an additional signed `status: streaming` preview webhook. Requires callback_url; it may also be enabled as an account default. Preview audio is transient and not yet quality-verified."}}},"JobAccepted":{"type":"object","description":"The job was recorded; for billed customer keys, its token cost was reserved. `queued` is also returned when generation-transport delivery is uncertain: poll `status_url` rather than submitting a duplicate. The job may still proceed normally; if it was truly not delivered, it is marked `failed` and refunded, normally within about 30–45 minutes.","example":{"job_id":"8f3a1c2e-7b9d-4c2a-9e5f-1a2b3c4d5e6f","status":"queued","status_url":"/api/jobs/8f3a1c2e-7b9d-4c2a-9e5f-1a2b3c4d5e6f"},"properties":{"job_id":{"type":"string"},"status":{"type":"string","example":"queued"},"status_url":{"type":"string","example":"/api/jobs/…"},"idempotent_replay":{"type":"boolean","description":"Present when an Idempotency-Key matched an existing job"}}},"Job":{"type":"object","example":{"job_id":"8f3a1c2e-7b9d-4c2a-9e5f-1a2b3c4d5e6f","status":"complete","clips":[{"id":"f81b3ad8-bc81-4135-80ac-dbddd7bf8b00","status":"complete","audio_url":"https://cdn1.suno.ai/f81b3ad8-bc81-4135-80ac-dbddd7bf8b00.mp3","image_url":"https://cdn2.suno.ai/image_f81b3ad8-bc81-4135-80ac-dbddd7bf8b00.jpeg","title":"Rain On Glass","model":"v5.5","tags":"lofi hip-hop, rainy, mellow","duration":128.16}],"error":null,"created_at":1783084482,"updated_at":1783084639},"properties":{"job_id":{"type":"string"},"status":{"type":"string","enum":["queued","processing","submitted","complete","failed"]},"phase":{"type":"string","enum":["streaming","delivering"],"description":"streaming: transient previews available; delivering: provider finished, validated audio is being stored. Status remains submitted until delivery completes."},"clips":{"type":"array","items":{"$ref":"#/components/schemas/Clip"}},"preview_clips":{"type":"array","items":{"$ref":"#/components/schemas/Clip"},"description":"Currently playable expected tracks; this may initially contain only the first track while its sibling is still rendering. Never treat previews as the authoritative final result; replace them with clips when status becomes complete."},"preview_at":{"type":"integer","description":"Unix time when the first playable expected track was observed"},"lyrics":{"type":["object","null"],"additionalProperties":true,"description":"Present for a standalone `/api/lyrics` job"},"error":{"type":["string","null"]},"created_at":{"type":"integer"},"updated_at":{"type":"integer"}}},"Clip":{"type":"object","example":{"id":"f81b3ad8-bc81-4135-80ac-dbddd7bf8b00","status":"complete","audio_url":"https://cdn1.suno.ai/f81b3ad8-bc81-4135-80ac-dbddd7bf8b00.mp3","image_url":"https://cdn2.suno.ai/image_f81b3ad8-bc81-4135-80ac-dbddd7bf8b00.jpeg","title":"Rain On Glass","model":"v5.5","tags":"lofi hip-hop, rainy, mellow","duration":128.16},"properties":{"id":{"type":"string"},"status":{"type":"string"},"audio_url":{"type":"string","format":"uri"},"image_url":{"type":"string","format":"uri","description":"Cover art"},"title":{"type":"string"},"model":{"type":"string","description":"Resolved Suno model (v5.5)"},"tags":{"type":"string","description":"Style / genre tags"},"duration":{"type":"number","description":"Seconds"}}},"Error":{"type":"object","required":["code","detail"],"properties":{"code":{"type":"string","example":"VALIDATION_ERROR"},"detail":{"type":"string"},"request_id":{"type":"string","description":"Echoed in the X-Request-Id header; quote it in support requests"},"balance":{"type":"integer","description":"On 402 (INSUFFICIENT_TOKENS / DAILY_CAP_REACHED): your current token balance"},"cost":{"type":"integer","description":"On 402: tokens this call needs"},"job_id":{"type":"string","description":"On 409 IDEMPOTENCY_CONFLICT: the retained job already bound to this key"}}}},"responses":{"JobAccepted":{"description":"Accepted — generation is async. Keep polling the returned job; a queued job may proceed normally or, if transport delivery was truly lost, fail and refund automatically (normally within about 30–45 minutes).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/JobAccepted"}}}},"Error":{"description":"Error envelope","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"InsufficientTokens":{"description":"402 — token balance too low (billed on success only)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"IdempotencyConflict":{"description":"409 — Idempotency-Key is already bound to a different canonical generation request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"RateLimited":{"description":"429 — per-key rate limit (60 requests/min, sliding 60s window). When limiter state is available, `RateLimit-Limit`/`RateLimit-Remaining`/`RateLimit-Reset` are returned on rate-checked generation/lyrics accepts and 429 responses. They are omitted during a fail-open limiter outage and are not promised on auth/validation errors, status reads, or idempotent replay. `RateLimit-Reset` is a delay in seconds (0–60), not a Unix timestamp. On 429 also honor `Retry-After`.","headers":{"Retry-After":{"description":"Seconds to wait before retrying","schema":{"type":"integer","minimum":1,"maximum":60}},"RateLimit-Limit":{"description":"Requests allowed in the sliding window","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"Requests remaining in the current sliding window","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until quota resets (delay, not Unix time)","schema":{"type":"integer","minimum":0,"maximum":60}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"GenerationUnavailable":{"description":"503 — no dispatch-ready generation route. No job was created and no tokens were reserved; retry with the same Idempotency-Key after the indicated delay.","headers":{"Retry-After":{"description":"Seconds to wait before retrying","schema":{"type":"integer","minimum":1}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}}