直接答案:Node.js 18 及以上自带
fetch,不需要额外安装 HTTP 库。提交/api/music/create后保存song_id,用/api/music/query每 3–5 秒查询,完成后通过下载接口取回音频。
最小可运行示例
import fs from "node:fs/promises";
const BASE = "https://www.suno-api.io";
const KEY = process.env.SUNO_API_KEY;
async function postJson(path, body, retries = 2) {
const response = await fetch(BASE + path, {
method: "POST",
headers: {
Authorization: `Bearer ${KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify(body),
signal: AbortSignal.timeout(120_000),
});
if (response.status === 429 && retries > 0) {
const wait = Number(response.headers.get("retry-after") || 15);
await new Promise((resolve) => setTimeout(resolve, wait * 1000));
return postJson(path, body, retries - 1);
}
if (!response.ok) {
throw new Error(`${response.status}: ${await response.text()}`);
}
return response.json();
}
const submitted = await postJson("/api/music/create", {
description: "轻快的 city pop,夜景,女声",
model: "suno-v6",
});
const clips =
submitted?.data?.clips ?? submitted?.clips ?? [];
const songId = clips[0]?.song_id;
if (!songId) throw new Error("没有拿到 song_id");
const deadline = Date.now() + 8 * 60 * 1000;
let completed = false;
while (Date.now() < deadline) {
const result = await postJson("/api/music/query", { song_id: songId });
const status = String(result?.data?.status || "").toLowerCase();
if (["complete", "completed", "playable"].includes(status)) {
completed = true;
break;
}
if (["failed", "error"].includes(status)) {
throw new Error(JSON.stringify(result));
}
await new Promise((resolve) => setTimeout(resolve, 4000));
}
if (!completed) throw new Error("任务仍在处理中,请稍后继续查询");
const media = await postJson("/api/music/download-url", {
song_id: songId,
kind: "song",
});
const mediaUrl = media?.data?.url ?? media?.data?.audio_url;
if (!mediaUrl) throw new Error("任务完成但没有下载地址");
const audio = await fetch(mediaUrl, {
signal: AbortSignal.timeout(180_000),
});
if (!audio.ok) throw new Error(`下载失败: ${audio.status}`);
await fs.writeFile("output.mp3", Buffer.from(await audio.arrayBuffer()));
console.log({ songId, file: "output.mp3" });
为什么不要把提交放在轮询循环里
/api/music/create 是创建任务并触发生成,/api/music/query 只是读取已有任务。把提交写进 while 循环会在每次等待后新建一首歌,造成重复扣费和重复任务。
生产环境还应增加什么
- 按
song_id落库,不要只存在内存里。 - 轮询任务使用指数退避,429 时读取
Retry-After。 - 限制单进程并发,并给任务设置业务超时。
- 完成后的音频及时转存,避免依赖临时上游 URL。
- 记录 HTTP 状态、错误码和请求 ID,便于技术排查。
常见问题
Node 18 以下能运行吗?
示例使用原生 fetch 和 AbortSignal.timeout。旧版 Node 需要改用 undici 或升级运行时。
一次请求能返回几首?
普通生成一次返回 2 首;两首可能先后完成,应按每个 song_id 独立查询,而不是把第一个完成的状态套给第二个。
如何避免 429?
不要高频并发轮询。每首歌 3–5 秒查询一次,批量任务按队列串行或限并发,并在收到 429 后暂停当前请求。
继续阅读:Python 调用示例、轮询与超时、查询接口、媒体下载接口。


