Home / Blog / Suno API Node.js Example: Submit, Poll and Download Audio
API

Suno API Node.js Example: Submit, Poll and Download Audio

Use Node.js fetch to generate music through the Suno API, poll the task without duplicate billing, handle rate limits, and save the completed MP3 file.

Node.js developer with asynchronous task cards and an audio waveform

Photo: Pexels

Short answer: Node.js 18 and newer include fetch. Submit /api/music/create once, persist the returned song_id, query /api/music/query every 3-5 seconds, and download the file after the status reaches completed or playable.

Minimal working example

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: "bright city pop with a female vocal",
  model: "suno-v6",
});
const clips = submitted?.data?.clips ?? [];
const songId = clips[0]?.song_id;
if (!songId) throw new Error("No song_id returned");

const deadline = Date.now() + 8 * 60 * 1000;
while (Date.now() < deadline) {
  const result = await postJson("/api/music/query", { song_id: songId });
  const status = String(result?.data?.status || "").toLowerCase();
  if (["completed", "complete", "playable"].includes(status)) break;
  if (["failed", "error"].includes(status)) throw new Error(JSON.stringify(result));
  await new Promise((resolve) => setTimeout(resolve, 4000));
}

const media = await postJson("/api/music/download-url", {
  song_id: songId,
  kind: "song",
});
const url = media?.data?.url ?? media?.data?.audio_url;
if (!url) throw new Error("No media URL after completion");

const audio = await fetch(url, { signal: AbortSignal.timeout(180_000) });
if (!audio.ok) throw new Error(`Download failed: ${audio.status}`);
await fs.writeFile("output.mp3", Buffer.from(await audio.arrayBuffer()));
console.log({ songId, file: "output.mp3" });

Production rules

  • Persist the song ID and client request ID before polling.
  • Keep create and query in separate functions.
  • Limit polling concurrency across a batch.
  • Back off on 429 instead of starting another generation.
  • Copy finished audio to durable storage when the business requires persistent delivery.

Why two tracks may finish at different times

A normal generation returns two clips. Treat each song_id as an independent task. Do not assume that one completed clip means the other has finished.

FAQ

Does Node need an extra HTTP package?

No on Node 18+. Older runtimes need a fetch implementation such as undici or an upgrade.

How do I avoid duplicate charges?

Call the create endpoint once. A network timeout does not automatically prove that the task was not created, so reconcile with the request ID before attempting another generation.

What should happen after a 429?

Pause that request, honor Retry-After, and reduce the number of concurrent pollers.

Related: Python example, polling and timeout guide, query API, and media download API.

¥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.