DEV Community

yan zhiyu
yan zhiyu

Posted on

How to Build an AI Background Music Generator with the Suno API (For Videos & Podcasts)

Every video creator and podcaster knows the pain: you need background music that fits the mood, isn't copyrighted, and doesn't cost $50/track from a stock library. What if you could generate a custom, royalty-free track from a single text prompt in under 30 seconds?

In this tutorial, you'll build a lightweight AI background music generator using the Suno API via TTAPI. By the end, you'll have a working Node.js app that accepts a mood description and returns a downloadable .mp3 — ready to drop into your video editor or podcast software.

What we're building:

  • A Node.js script that takes a prompt like "calm lo-fi piano, no vocals, for a study vlog"
  • Submits it to the Suno API
  • Polls for the result
  • Downloads the generated .mp3 automatically

Time to complete: ~20 minutes

Prerequisites: Node.js 18+, a free TTAPI account


Why Suno API for Background Music?

Most AI music tools are consumer-facing. The Suno API gives you programmatic access to the same generation engine — meaning you can automate it, batch it, and integrate it into your own tools.

For background music specifically, the API shines because:

  • You can set "instrumental": true to get clean, vocal-free tracks every time
  • The tags parameter gives you precise genre and mood control
  • The async workflow means you can fire off a request and do other things while it generates
  • Generated audio is yours to use — no per-track licensing fees on top of API costs

The full API reference lives at TTAPI Suno API Documentation.


Step 1 — Get Your API Key

  1. Sign up at ttapi.io
  2. Navigate to your dashboard and copy your TT-API-KEY
  3. Keep it handy — you'll add it to a .env file shortly

Step 2 — Project Setup

Create a new directory and initialize the project:

mkdir suno-bg-music && cd suno-bg-music
npm init -y
npm install node-fetch dotenv
Enter fullscreen mode Exit fullscreen mode

Create a .env file:

TTAPI_KEY=your_api_key_here
Enter fullscreen mode Exit fullscreen mode

Create the main file:

touch generate.js
Enter fullscreen mode Exit fullscreen mode

Your project structure should look like this:

suno-bg-music/
├── generate.js
├── .env
├── package.json
└── output/        ← mp3s will be saved here
Enter fullscreen mode Exit fullscreen mode

Create the output folder:

mkdir output
Enter fullscreen mode Exit fullscreen mode

Step 3 — Submit a Music Generation Request

The Suno API works asynchronously. You submit a request, get back a jobId, then poll a separate endpoint to retrieve the result when it's ready.

Here's the full generation function. Open generate.js and add:

import fetch from 'node-fetch';
import fs from 'fs';
import path from 'path';
import { createWriteStream } from 'fs';
import { pipeline } from 'stream/promises';
import 'dotenv/config';

const API_KEY = process.env.TTAPI_KEY;
const BASE_URL = 'https://api.ttapi.io';

// Step 1: Submit the generation job
async function generateMusic(prompt, tags) {
  const response = await fetch(`${BASE_URL}/suno/v1/music`, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'TT-API-KEY': API_KEY,
    },
    body: JSON.stringify({
      custom: false,        // let Suno handle the lyrics/structure
      instrumental: true,   // no vocals — clean background music
      mv: 'chirp-v5',       // latest Suno v5 model
      prompt: prompt,
      tags: tags,
    }),
  });

  const data = await response.json();

  if (data.status !== 'SUCCESS') {
    throw new Error(`Submission failed: ${JSON.stringify(data)}`);
  }

  console.log(`✅ Job submitted. jobId: ${data.data.jobId}`);
  return data.data.jobId;
}
Enter fullscreen mode Exit fullscreen mode

Key parameters explained:

  • instrumental: true — this is the most important setting for background music. It tells Suno to generate a clean track with no vocals.
  • mv: 'chirp-v5' — always use the latest model for the best output quality.
  • custom: false — since we're generating instrumental music, we don't need to supply lyrics.
  • tags — this is where you define the sound. Think of it as your style brief to the AI.

Step 4 — Poll for the Result

After submitting, the job takes 60–120 seconds to complete. We'll poll the fetch endpoint every 10 seconds until the status flips to SUCCESS:

// Step 2: Poll until the job is complete
async function pollForResult(jobId, intervalMs = 10000, maxAttempts = 24) {
  console.log('⏳ Waiting for generation to complete...');

  for (let attempt = 1; attempt <= maxAttempts; attempt++) {
    await new Promise((resolve) => setTimeout(resolve, intervalMs));

    const response = await fetch(
      `${BASE_URL}/suno/v2/fetch?jobId=${jobId}`,
      {
        headers: { 'TT-API-KEY': API_KEY },
      }
    );

    const data = await response.json();

    if (data.status === 'SUCCESS' && data.data?.musics?.length > 0) {
      console.log(`✅ Generation complete after ${attempt} poll(s).`);
      return data.data.musics[0]; // return the first track
    }

    if (data.status === 'FAILED') {
      throw new Error(`Job failed: ${JSON.stringify(data)}`);
    }

    console.log(`   Attempt ${attempt}/${maxAttempts} — status: ${data.status}`);
  }

  throw new Error('Timed out waiting for generation result.');
}
Enter fullscreen mode Exit fullscreen mode

The fetch response includes audioUrl, videoUrl, title, duration, and more. For background music, we only need audioUrl.


Step 5 — Download the MP3

// Step 3: Download the generated mp3
async function downloadMp3(audioUrl, filename) {
  const outputPath = path.join('output', filename);
  const response = await fetch(audioUrl);

  if (!response.ok) {
    throw new Error(`Failed to download audio: ${response.statusText}`);
  }

  await pipeline(response.body, createWriteStream(outputPath));
  console.log(`💾 Saved to: ${outputPath}`);
  return outputPath;
}
Enter fullscreen mode Exit fullscreen mode

Step 6 — Wire It All Together

Add the main function at the bottom of generate.js:

async function main() {
  // --- Configure your track here ---
  const prompt = 'calm background music for a study vlog, no distractions';
  const tags = 'lo-fi, piano, ambient, instrumental, slow, soft';
  // ----------------------------------

  try {
    const jobId = await generateMusic(prompt, tags);
    const track = await pollForResult(jobId);

    const safeTitle = track.title.replace(/[^a-z0-9]/gi, '_').toLowerCase();
    const filename = `${safeTitle}_${Date.now()}.mp3`;

    await downloadMp3(track.audioUrl, filename);

    console.log('\n🎵 Track details:');
    console.log(`   Title:    ${track.title}`);
    console.log(`   Duration: ${Math.round(track.duration)}s`);
    console.log(`   Tags:     ${track.tags}`);
  } catch (err) {
    console.error('❌ Error:', err.message);
    process.exit(1);
  }
}

main();
Enter fullscreen mode Exit fullscreen mode

Step 7 — Run It

Add "type": "module" to your package.json to enable ES module imports:

{
  "type": "module",
  ...
}
Enter fullscreen mode Exit fullscreen mode

Then run:

node generate.js
Enter fullscreen mode Exit fullscreen mode

Expected output:

✅ Job submitted. jobId: a3f9b2c1...
⏳ Waiting for generation to complete...
   Attempt 1/24 — status: ON_QUEUE
   Attempt 2/24 — status: ON_QUEUE
   Attempt 3/24 — status: ON_QUEUE
✅ Generation complete after 4 poll(s).
💾 Saved to: output/calm_study_vlog_1714812345678.mp3

🎵 Track details:
   Title:    Calm Study Vlog
   Duration: 187s
   Tags:     lo-fi, piano, ambient, instrumental, slow, soft
Enter fullscreen mode Exit fullscreen mode

Open the output/ folder and you'll find your generated .mp3, ready to import into DaVinci Resolve, Premiere, GarageBand, or any podcast editor.


Prompt & Tags Reference for Background Music

The tags parameter is your primary control over the sound. Here are some combinations that work well for common use cases:

Use Case prompt tags
Study / focus vlog "calm background for a study session" "lo-fi, piano, ambient, instrumental, 80bpm"
Tech / product demo "upbeat background for a software demo video" "electronic, upbeat, corporate, instrumental, clean"
Travel vlog "adventurous background music for a travel montage" "cinematic, orchestral, instrumental, epic, wide"
Podcast intro "short energetic intro for a tech podcast" "electronic, punchy, upbeat, instrumental, intro"
Meditation / wellness "peaceful background for a meditation video" "ambient, drone, nature sounds, soft, slow"
Gaming / action "intense background for a gaming highlight reel" "electronic, intense, fast, instrumental, driving"

Tips:

  • Keep tags to 4–6 descriptors. Too many can confuse the model.
  • BPM hints like "80bpm" or "120bpm" are respected but not guaranteed.
  • "instrumental" in tags reinforces the instrumental: true parameter — use both.
  • For very short clips (intros/outros), add "short" or "30 seconds" to the tags.

Going Further: Batch Generation

Need multiple options to choose from? Here's a quick wrapper to generate several variations in parallel:

async function generateBatch(variations) {
  const jobs = await Promise.all(
    variations.map(({ prompt, tags }) => generateMusic(prompt, tags))
  );

  const results = await Promise.all(jobs.map(pollForResult));

  for (const [i, track] of results.entries()) {
    const filename = `variation_${i + 1}_${Date.now()}.mp3`;
    await downloadMp3(track.audioUrl, filename);
  }
}

// Usage:
generateBatch([
  { prompt: 'calm lo-fi for a study vlog', tags: 'lo-fi, piano, ambient, instrumental' },
  { prompt: 'upbeat version of a study vlog theme', tags: 'electronic, upbeat, instrumental' },
  { prompt: 'cinematic version for a study vlog', tags: 'cinematic, orchestral, instrumental' },
]);
Enter fullscreen mode Exit fullscreen mode

This fires all three jobs simultaneously, then downloads each result as it completes.


What to Build Next

With this foundation in place, here are some natural next steps:

  • Web UI — wrap the script in an Express endpoint and build a simple frontend where creators paste a description and get a download link
  • Webhook support — instead of polling, pass a hookUrl to your server and handle the callback; better for production workloads
  • Extend a track — use the Suno Extend API to lengthen a generated track beyond its default duration
  • Stems separation — use the Suno Stems API to split a generated track into vocals and instrumentals for further editing
  • Auto-match to video — read the duration of a video file with ffprobe, then pass that duration as a target to the generation prompt

Full Code

The complete generate.js file:

import fetch from 'node-fetch';
import path from 'path';
import { createWriteStream } from 'fs';
import { pipeline } from 'stream/promises';
import 'dotenv/config';

const API_KEY = process.env.TTAPI_KEY;
const BASE_URL = 'https://api.ttapi.io';

async function generateMusic(prompt, tags) {
  const response = await fetch(`${BASE_URL}/suno/v1/music`, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'TT-API-KEY': API_KEY,
    },
    body: JSON.stringify({
      custom: false,
      instrumental: true,
      mv: 'chirp-v5',
      prompt,
      tags,
    }),
  });

  const data = await response.json();
  if (data.status !== 'SUCCESS') throw new Error(`Submission failed: ${JSON.stringify(data)}`);
  console.log(`✅ Job submitted. jobId: ${data.data.jobId}`);
  return data.data.jobId;
}

async function pollForResult(jobId, intervalMs = 5000, maxAttempts = 24) {
  console.log('⏳ Waiting for generation to complete...');
  for (let attempt = 1; attempt <= maxAttempts; attempt++) {
    await new Promise((r) => setTimeout(r, intervalMs));
    const response = await fetch(`${BASE_URL}/suno/v2/fetch?jobId=${jobId}`, {
      headers: { 'TT-API-KEY': API_KEY },
    });
    const data = await response.json();
    if (data.status === 'SUCCESS' && data.data?.musics?.length > 0) {
      console.log(`✅ Done after ${attempt} poll(s).`);
      return data.data.musics[0];
    }
    if (data.status === 'FAILED') throw new Error(`Job failed: ${JSON.stringify(data)}`);
    console.log(`   Attempt ${attempt}/${maxAttempts} — status: ${data.status}`);
  }
  throw new Error('Timed out.');
}

async function downloadMp3(audioUrl, filename) {
  const outputPath = path.join('output', filename);
  const response = await fetch(audioUrl);
  if (!response.ok) throw new Error(`Download failed: ${response.statusText}`);
  await pipeline(response.body, createWriteStream(outputPath));
  console.log(`💾 Saved to: ${outputPath}`);
  return outputPath;
}

async function main() {
  const prompt = 'calm background music for a study vlog, no distractions';
  const tags = 'lo-fi, piano, ambient, instrumental, slow, soft';

  const jobId = await generateMusic(prompt, tags);
  const track = await pollForResult(jobId);
  const filename = `${track.title.replace(/[^a-z0-9]/gi, '_').toLowerCase()}_${Date.now()}.mp3`;
  await downloadMp3(track.audioUrl, filename);

  console.log(`\n🎵 ${track.title}${Math.round(track.duration)}s`);
}

main().catch((err) => { console.error('', err.message); process.exit(1); });
Enter fullscreen mode Exit fullscreen mode

Resources


Built something with this? Drop it in the comments — always interested to see what people ship with generative audio.

Top comments (0)