Official Node.js / TypeScript SDK for the StemSplit stem-separation API. Typed client with automatic file uploads, job-completion polling, YouTube / SoundCloud jobs, voice denoising, and webhook signature verification. Zero runtime dependencies.
npm install @stemsplit/sdkimport { StemSplit } from '@stemsplit/sdk';
const client = new StemSplit(); // reads STEMSPLIT_API_KEY
// From a URL
const job = await client.jobs.create({
sourceUrl: 'https://example.com/song.mp3',
outputType: 'FOUR_STEMS',
});
const done = await job.waitForCompletion();
await done.downloadAll('./stems/');
// → stems/vocals.mp3 stems/drums.mp3 stems/bass.mp3 stems/other.mp3
// From a local file
const job2 = await client.jobs.create({ audio: './song.mp3', outputType: 'BOTH' });
const done2 = await job2.waitForCompletion();
await done2.downloadAll('./out/');
// → out/vocals.mp3 out/instrumental.mp3- API key: stemsplit.io/app/settings/api — new accounts get 5 free minutes
- Developer guide: stemsplit.io/developers/guides/node
- API reference: stemsplit.io/developers/reference
- GitHub: github.com/StemSplit/node-stemsplit
- Changelog:
CHANGELOG.md - Python SDK:
stemsplit-python
StemSplitclass with typed sub-resources for every public endpoint- Automatic file uploads — pass a file path,
Buffer,Blob, orUint8Arrayand the SDK handles the presigned-URL dance for you job.waitForCompletion()— polls until done, returns the completed job; throws typedJobFailedError/JobExpiredErrorjob.downloadAll(dir)— downloads all stems to a directory in one calljobs.iterAll()— async generator for paginated iteration over all jobs- YouTube and SoundCloud — separate stems directly from a URL, no download step required
- Voice denoising — remove background noise from recordings via
denoiseJobs - Webhook signature verification in two lines:
webhooks.verifyAndParse(body, sig, secret) - Typed error hierarchy with documented codes —
AuthenticationError,InsufficientCreditsError,RateLimitError,JobFailedError, and more - Automatic retries with exponential backoff + jitter on transient errors and
429responses; honorsRetry-After - Zero runtime dependencies — uses Node.js built-ins only (
node:crypto,node:fs,node:stream) - Fully typed — ships
.d.tsdeclarations;strictTypeScript clean
| You want to … | Use |
|---|---|
| Call the hosted StemSplit API from Node.js / TypeScript | @stemsplit/sdk (this package) |
| Call the API from Python | stemsplit-python |
| Use the API from Claude Desktop, Cursor, Cline, or Windsurf | stemsplit-mcp |
| Wire StemSplit into n8n / Zapier / Make | The no-code guides |
| Run inference locally without an API key | demucs-onnx |
npm install @stemsplit/sdk # npm
yarn add @stemsplit/sdk # yarn
pnpm add @stemsplit/sdk # pnpmNode.js 20+ required.
import { StemSplit } from '@stemsplit/sdk';
const client = new StemSplit({ apiKey: 'sk_live_...' }); // explicit
const client2 = new StemSplit(); // reads STEMSPLIT_API_KEYGet an API key at Settings → API Keys.
import { StemSplit } from '@stemsplit/sdk';
const client = new StemSplit();
const job = await client.jobs.create({
audio: './song.mp3', // path | Buffer | Uint8Array | Blob
outputType: 'FOUR_STEMS', // VOCALS | INSTRUMENTAL | BOTH | FOUR_STEMS | SIX_STEMS
quality: 'BEST', // FAST | BALANCED | BEST
outputFormat: 'MP3', // MP3 | WAV | FLAC
});
const done = await job.waitForCompletion();
const { files } = await done.downloadAll('./stems/');
console.log(files);
// { vocals: 'stems/vocals.mp3', drums: 'stems/drums.mp3', ... }const job = await client.jobs.create({
sourceUrl: 'https://example.com/song.mp3',
outputType: 'BOTH',
});
const done = await job.waitForCompletion();const job = await client.youtubeJobs.create(
'https://youtube.com/watch?v=dQw4w9WgXcQ',
);
const done = await client.youtubeJobs.waitForCompletion(job.id);
console.log(done.videoTitle, done.outputs?.vocals?.url);const job = await client.soundcloudJobs.create(
'https://soundcloud.com/artist/track',
);
const done = await client.soundcloudJobs.waitForCompletion(job.id);const job = await client.denoiseJobs.create({ audio: './interview.mp3' });
const done = await client.denoiseJobs.waitForCompletion(job.id);
const destPath = await client.denoiseJobs.downloadResult(done, './out/');
console.log('Denoised:', destPath);// One page
const { jobs } = await client.jobs.list({ status: 'COMPLETED', limit: 20 });
// All pages (async generator)
for await (const job of client.jobs.iterAll({ status: 'COMPLETED' })) {
console.log(job.id, job.input.fileName);
}const balance = await client.account.get();
console.log(balance.balanceFormatted); // "5 minutes"Register a webhook in the dashboard, save the secret, then verify deliveries on your endpoint.
import express from 'express';
import { webhooks } from '@stemsplit/sdk';
const app = express();
const SECRET = process.env.STEMSPLIT_WEBHOOK_SECRET!;
app.post('/stemsplit-webhook', express.raw({ type: '*/*' }), (req, res) => {
try {
const event = webhooks.verifyAndParse(
req.body,
req.headers['x-webhook-signature'] as string,
SECRET,
);
if (event.event === 'job.completed') {
console.log('Stems ready:', event.data.outputs);
}
res.sendStatus(200);
} catch {
res.sendStatus(401);
}
});import { Hono } from 'hono';
import { webhooks } from '@stemsplit/sdk';
const app = new Hono();
app.post('/stemsplit-webhook', async (c) => {
const body = await c.req.arrayBuffer();
const sig = c.req.header('x-webhook-signature') ?? '';
const event = webhooks.verifyAndParse(
Buffer.from(body),
sig,
process.env.STEMSPLIT_WEBHOOK_SECRET!,
);
console.log(event.event, event.jobId);
return c.text('ok');
});Every non-2xx API response maps to a typed exception; the base class is StemSplitError.
import {
StemSplit,
AuthenticationError,
InsufficientCreditsError,
JobFailedError,
RateLimitError,
} from '@stemsplit/sdk';
const client = new StemSplit();
try {
const job = await client.jobs.create({ sourceUrl: 'https://example.com/song.mp3' });
const done = await job.waitForCompletion();
} catch (err) {
if (err instanceof AuthenticationError) {
console.error('Check your STEMSPLIT_API_KEY:', err.message);
} else if (err instanceof InsufficientCreditsError) {
console.error(`Need ${err.requiredSeconds}s of credit. Buy more at ${err.purchaseUrl}`);
} else if (err instanceof RateLimitError) {
console.error(`Rate limited. Retry after ${err.retryAfter}s`);
} else if (err instanceof JobFailedError) {
console.error(`Job ${err.jobId} failed: ${err.errorMessage}`);
}
}| Exception | HTTP | When |
|---|---|---|
BadRequestError |
400 | Invalid parameters (FILE_TOO_LARGE, AUDIO_TOO_LONG, …) |
AuthenticationError |
401 | Missing or invalid API key |
InsufficientCreditsError |
402 | Not enough credits (exposes .requiredSeconds, .purchaseUrl) |
PermissionDeniedError |
403 | API key revoked or expired |
NotFoundError |
404 | Job not found |
RateLimitError |
429 | Too many requests (exposes .retryAfter) |
InternalServerError |
5xx | Server-side errors |
JobFailedError |
— | Logical — thrown by waitForCompletion() when status is FAILED |
JobExpiredError |
— | Logical — thrown by waitForCompletion() when status is EXPIRED |
SignatureVerificationError |
— | Webhook signature mismatch |
NetworkError |
— | Transport-level failure (DNS, timeout, …) |
new StemSplit({
apiKey: 'sk_live_...', // string | undefined — env STEMSPLIT_API_KEY
baseUrl: 'https://stemsplit.io/api/v1', // override for testing / proxies
timeout: 30_000, // ms — default 30s
maxRetries: 3, // retries on transient errors — default 3
});- Max file size: 100 MB
- Max audio duration: 60 minutes
- Rate limit: 60 req/min
- Input formats: MP3, WAV, FLAC, M4A, AAC, OGG, WebM, WMA
- Output formats: MP3, WAV, FLAC
git clone https://github.com/StemSplit/node-stemsplit
cd node-stemsplit
npm install
npm run typecheck
npm run build
npm testIssues, PRs, and questions are welcome at github.com/StemSplit/node-stemsplit.
MIT — see LICENSE.