Docs
Music generation
Text-to-music on Suno V5 / V5.5 — vocals, lyrics and style control. Two API shapes: a one-shot synchronous endpoint that blocks until the tracks are ready, and a submit-and-poll async pair (with signed webhooks) for production.
Endpoints: POST /v1/audio/music (synchronous, covered first), and POST /v1/audio/music/jobs + GET /v1/audio/music/jobs/{id} (async, recommended for production). Each request generates two tracks; generation runs about 1–3 minutes.
Quick start (synchronous)
Pass a prompt and a Suno model. The request holds open until both tracks are ready, then returns their permanent URLs.
import requests
# Synchronous: one request blocks until the tracks are ready (1-3 min).
resp = requests.post(
"https://api.kunavo.com/v1/audio/music",
headers={"Authorization": f"Bearer {API_KEY}"},
json={
"model": "suno-v5",
"prompt": "upbeat lofi hip hop for late-night coding, mellow piano",
"instrumental": True,
},
timeout=600, # generation can take a few minutes
)
# Suno returns ~2 tracks per request.
for track in resp.json()["data"]:
print(track["url"], track.get("image_url"))Parameters
| Param | Type | Notes |
|---|---|---|
model | string (required) | A music model slug — suno-v5 or suno-v5-5. |
prompt | string (required) | Description of the music (or the lyrics, in custom mode). |
instrumental | bool | true = no vocals. Default false. |
customMode | bool | Advanced Suno mode — drive structure / lyrics / style explicitly. |
style | string | Style / genre hint, e.g. "lofi, jazz, chill". |
title | string | Track title. |
webhook_url | string (https) | Async only — push a signed terminal event here. See Webhooks. |
Music models
| Slug | Provider | Notes |
|---|---|---|
suno-v5 | Suno | Suno V5 — vocals, lyrics, style blending. |
suno-v5-5 | Suno | Suno V5.5 — newest, higher fidelity and longer generations. |
Check /models for the live list and per-call pricing.
Async API (submit + poll)
Endpoints: POST /v1/audio/music/jobs + GET /v1/audio/music/jobs/{id}. Mirrors the async video API, so the same polling / webhook code works across modalities.
The sync endpoint holds an HTTP connection open for minutes — convenient for scripts but unreliable on mobile. The async pair returns immediately with a msc_* id; you poll GET /v1/audio/music/jobs/{id} until status is completed or failed. Same models, same pricing, same permanent CDN URLs.
import requests, time
# 1. Submit — returns immediately with a msc_ task id. No long-lived connection.
submit = requests.post(
"https://api.kunavo.com/v1/audio/music/jobs",
headers={
"Authorization": f"Bearer {API_KEY}",
# Optional: retrying with the same key inside ~24h returns the
# original task rather than submitting again.
"Idempotency-Key": "my-song-uuid",
},
json={
"model": "suno-v5",
"prompt": "dreamy synthwave with a driving bassline",
"title": "Midnight Drive",
},
timeout=60,
).json()
task_id = submit["id"] # "msc_abc..."
status = submit["status"] # "queued"
# 2. Poll until terminal. Recommended cadence: 5s, backing off to 30s.
while status not in ("completed", "failed"):
time.sleep(5)
r = requests.get(
f"https://api.kunavo.com/v1/audio/music/jobs/{task_id}",
headers={"Authorization": f"Bearer {API_KEY}"},
timeout=30,
).json()
status = r["status"]
if status == "failed":
raise RuntimeError(r["error"]["message"])
for track in r["output"]["tracks"]:
print(track["url"])Response shape
| Field | Type | Notes |
|---|---|---|
id | string | msc_-prefixed task id. |
object | string | "music". |
status | string | queued | in_progress | completed | failed. |
model | string | The model slug you submitted. |
created_at | int | Unix seconds when we accepted the submit. |
completed_at | int | null | Unix seconds when terminal — null while pending. |
expires_at | int | Unix seconds after which the result is pruned (~30 days). |
progress | int | 0 until terminal, then 100. |
output | object | null | {tracks[], archived} on completed. |
output.tracks | array | Each: {url, stream_url, image_url}. url / image_url are permanent CDN links. |
error | object | null | {code, message} on failed. |
Headers & conventions
Idempotency-Key(optional, ≤128 chars) — submitting twice with the same key on the same account returns the original task instead of creating a duplicate.POST /v1/audio/music/jobsreturns 202 Accepted on a fresh submit and 200 OK when an idempotency key replays a result.GETis owner-scoped: querying a task that belongs to another account returns 404.- Recommended polling cadence: 5 s, backing off to 30 s.
Webhooks
Pass webhook_url (https) when you submit, and Kunavo POSTs a signed music.completed / music.failed event the moment the task terminates — delivered with retry-and-backoff. The event's data field is exactly the GET payload above.
# Pass webhook_url on submit to be pushed a signed event on terminal —
# instead of (or alongside) polling:
# {"model": "suno-v5", "prompt": "...", "webhook_url": "https://you.com/hook"}
#
# Verify the HMAC signature on your receiver (Flask shown):
import hmac, hashlib
from flask import request, abort
def verify(secret: str) -> dict:
ts = request.headers["X-Kunavo-Webhook-Timestamp"]
sig = request.headers["X-Kunavo-Webhook-Signature"] # "sha256=<hex>"
raw = request.get_data(as_text=True)
expected = "sha256=" + hmac.new(
secret.encode(), f"{ts}.{raw}".encode(), hashlib.sha256
).hexdigest()
if not hmac.compare_digest(expected, sig):
abort(401)
return request.get_json()
# Event body:
# { "id": "evt_...", "object": "event",
# "type": "music.completed" | "music.failed",
# "created_at": 1700000000,
# "data": { ...the GET /v1/audio/music/jobs/{id} payload... } }HMAC-SHA256 over {timestamp}.{rawBody}, sent as X-Kunavo-Webhook-Signature: sha256=<hex>. Same recipe as the video webhooks, so one verifier handles both.When to use which
| Sync /v1/audio/music | Async /v1/audio/music/jobs | |
|---|---|---|
| Best for | One-off scripts, notebooks | Production, mobile, webhooks |
| Network | Holds connection 1-3 min | Submit in seconds; poll or get pushed |
| Failure recovery | Lose the response = lose the result | Re-query the id any time before expires_at |
| Webhooks | — | Signed music.completed / music.failed |
Response & storage
Each track's url (audio) and image_url (cover art) are permanent files.kunavo.com links — Kunavo archives every result to its own CDN, so unlike the raw Suno URLs they never expire. stream_url is Suno's original streaming link (handy for instant playback). Every track also shows up in /app/assets.
output.archived is false and the URLs fall back to Suno's temporary links (which expire after ~24h) — re-host them yourself in that case.