Short answer: generate music with a POST to
/api/music/create, keep the returnedsong_id, poll/api/music/queryevery 3-5 seconds, and request the media URL only after the status is complete. Never put the create call inside the polling loop.
What you need
- An API key from the Suno-API console.
- Python 3.9 or newer.
- The
requestspackage.
pip install requestsThe correct request sequence
- Submit a generation to
POST /api/music/create. - Store the final
song_id; a pending placeholder is not the final ID for querying. - Poll
POST /api/music/querywith that ID. - When the status is
completedorplayable, call/api/music/download-url. - Download the file and copy it to your own storage if it must remain available.
Working Python example
import os
import time
import requests
BASE = "https://www.suno-api.io"
API_KEY = os.environ["SUNO_API_KEY"]
HEADERS = {
"Authorization": f"Bearer {API_KEY}",
"Content-Type": "application/json",
}
def post_json(path, payload):
response = requests.post(
BASE + path,
headers=HEADERS,
json=payload,
timeout=120,
)
if response.status_code == 429:
wait = int(response.headers.get("Retry-After", "15"))
time.sleep(wait)
return post_json(path, payload)
response.raise_for_status()
return response.json()
def generate_and_download(description):
submitted = post_json("/api/music/create", {
"description": description,
"model": "suno-v6",
})
clips = submitted.get("data", {}).get("clips") or []
if not clips:
raise RuntimeError("No clips in the generation response")
song_id = clips[0]["song_id"]
deadline = time.time() + 8 * 60
while time.time() < deadline:
result = post_json("/api/music/query", {"song_id": song_id})
data = result.get("data") or {}
status = str(data.get("status", "")).lower()
if status in {"complete", "completed", "playable"}:
break
if status in {"failed", "error"}:
raise RuntimeError(result)
time.sleep(4)
else:
raise TimeoutError("Still processing; query the saved song_id later")
media = post_json("/api/music/download-url", {
"song_id": song_id,
"kind": "song",
})
media_url = media.get("data", {}).get("url") or media.get("data", {}).get("audio_url")
if not media_url:
raise RuntimeError("Completed task has no media URL")
response = requests.get(media_url, timeout=180)
response.raise_for_status()
with open("output.mp3", "wb") as fh:
fh.write(response.content)
return song_id, "output.mp3"Error handling that avoids duplicate charges
| Status | Meaning | Action |
|---|---|---|
| 401 | Missing or invalid API key | Check the Authorization: Bearer header |
| 403 | Balance, account or permission problem | Read the error first; do not blindly submit again |
| 429 | Too many requests | Honor Retry-After and back off |
| 5xx | Temporary upstream or platform failure | Keep the request ID and query before retrying |
Only the create endpoint starts a billed generation. Polling and media retrieval are different operations. Persist song_id and the client request ID before waiting, so a process restart does not force a second generation.
FAQ
How long should polling wait?
Usually several minutes, but load and editing complexity vary. Use status, not an arbitrary fixed delay, to decide when the song is complete.
Why is the audio URL empty?
While the task is pending, submitted or processing, media fields may be empty. Wait for the completed status.
Can I save the media URL forever?
Do not treat an upstream media URL as permanent. Download the file and copy it to storage you control when long-term delivery is required.
Continue with the Node.js example, the polling and timeout guide, the quick start, and the query API documentation.
¥0.6 per generation, 2 tracks, unlimited downloads
Runs on the Suno V6 engine. Download MP3 or WAV as often as you like. Signing up needs an email address, not a phone number.


