The Suno API flow is the same in any language: submit a generation (you get a job_id back immediately), then poll until it’s done — or pass a callback_url and take a webhook. Generation takes 1–3 minutes, so it’s always async. The fastest path is the official SDK, which does submit → poll in one call; if you’d rather add no dependency, the complete raw-HTTP functions are right below.
The fast path: the official SDK
The Tunova SDK (MIT, tiny) turns the whole submit → poll flow into one generate() call and ships a built-in HMAC webhook verifier.pip install tunova or npm i tunova:
from tunova import Tunova # pip install tunova
t = Tunova("sk_live_…")
# .generate() submits AND polls for you — one call from prompt to audio URL
job = t.generate("calm rainy-night lofi", model="v5.5")
print(job["clips"][0]["audio_url"] if job["status"] == "complete" else job["error"])
# a failed render auto-refunds. Prefer async? t.submit(..., callback_url="https://you/hook")
import { Tunova } from "tunova"; // npm i tunova
const t = new Tunova(process.env.TUNOVA_API_KEY!);
// .generate() submits AND polls for you
const job = await t.generate("calm rainy-night lofi", { model: "v5.5" });
console.log(job.status === "complete" ? job.clips[0]?.audio_url : job.error);
Raw HTTP — no dependencies
Same flow, hand-rolled with a timeout and proper error handling — Python’s requests and Node 18+’s built-in fetch, nothing else.
Python
import os, time, requests # pip install requests
API = "https://api.tunova.ai"
H = {"X-API-Key": os.environ["TUNOVA_API_KEY"]}
def generate(prompt, model="v5.5", timeout=300):
r = requests.post(f"{API}/api/generate", headers=H,
json={"prompt": prompt, "model": model})
r.raise_for_status()
job_id = r.json()["job_id"] # 202 + job_id
deadline = time.time() + timeout
while time.time() < deadline:
job = requests.get(f"{API}/api/jobs/{job_id}", headers=H).json()
if job["status"] == "complete":
return job["clips"][0]["audio_url"]
if job["status"] == "failed":
raise RuntimeError(job["error"]) # tokens auto-refunded
time.sleep(3)
raise TimeoutError(job_id)
print(generate("calm rainy-night lofi"))
Node.js / TypeScript
const API = "https://api.tunova.ai";
const H = { "X-API-Key": process.env.TUNOVA_API_KEY, "Content-Type": "application/json" };
async function generate(prompt, model = "v5.5", timeoutMs = 300_000) {
const res = await fetch(`${API}/api/generate`, {
method: "POST", headers: H, body: JSON.stringify({ prompt, model }),
});
if (!res.ok) throw new Error(`${res.status} ${await res.text()}`);
const { job_id } = await res.json(); // 202 + job_id
const deadline = Date.now() + timeoutMs;
while (Date.now() < deadline) {
const job = await (await fetch(`${API}/api/jobs/${job_id}`, { headers: H })).json();
if (job.status === "complete") return job.clips[0].audio_url;
if (job.status === "failed") throw new Error(job.error); // tokens auto-refunded
await new Promise((r) => setTimeout(r, 3000));
}
throw new Error(`timeout ${job_id}`);
}
console.log(await generate("calm rainy-night lofi"));
Notes
Model: pass model = v5.5. Omit it to use the account default.
Custom / lyrics mode: POST to /api/custom_generate with prompt= your lyrics, plus optional tags (style) and title.
Billed only on success: a failed job auto-refunds the tokens — your except/catch branch is for logging, not for chasing a refund.
Idempotency: send an Idempotency-Key header and a retried submit returns the same job instead of charging twice.
Get a key (50 free tokens, no card) and the full reference on the quickstart, or browse the interactive API docs.
Tunova is an independent service, not affiliated with or endorsed by Suno.