首页 / 博客 / Suno API Node.js 调用示例:提交、轮询与下载
API

Suno API Node.js 调用示例:提交、轮询与下载

用 Node.js 调用 Suno API:原生 fetch 提交任务、轮询状态、处理 429 与超时,完成后下载 MP3,附可运行代码与防止重复提交的实践。

Node.js 开发者、异步任务卡与音频波形

图:Pexels

直接答案: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 调用示例、轮询与超时、查询接口、媒体下载接口。

0.6 元一次,固定出 2 首,下载不限次

官方原版 Suno 引擎,支持最新 V6,MP3 与 WAV 均可下载。注册不需要手机号。