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
.mp3automatically
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": trueto get clean, vocal-free tracks every time - The
tagsparameter 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
- Sign up at ttapi.io
- Navigate to your dashboard and copy your
TT-API-KEY - Keep it handy — you'll add it to a
.envfile 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
Create a .env file:
TTAPI_KEY=your_api_key_here
Create the main file:
touch generate.js
Your project structure should look like this:
suno-bg-music/
├── generate.js
├── .env
├── package.json
└── output/ ← mp3s will be saved here
Create the output folder:
mkdir output
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;
}
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.');
}
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;
}
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();
Step 7 — Run It
Add "type": "module" to your package.json to enable ES module imports:
{
"type": "module",
...
}
Then run:
node generate.js
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
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
tagsto 4–6 descriptors. Too many can confuse the model. - BPM hints like
"80bpm"or"120bpm"are respected but not guaranteed. -
"instrumental"intagsreinforces theinstrumental: trueparameter — 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' },
]);
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
hookUrlto 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); });
Resources
- Suno API Full Documentation — complete endpoint reference, parameters, and workflow guides
- Suno API Quickstart — get your first track in 5 minutes
- Suno API Async Workflow — deep dive on polling vs webhooks
- Suno Extend API — lengthen any generated track
- Suno Stems API — separate vocals and instrumentals
Built something with this? Drop it in the comments — always interested to see what people ship with generative audio.
Top comments (0)