# Quick start

Split a song into vocals and instrumental and download both stems, in cURL, JavaScript or Python.

## Get a key

API keys work with bought credits only. The free daily credits apply to the web app. Buy a pack on [Credits](https://disband.io/app/credits), then create a key on [API keys](https://disband.io/app/keys). The key is shown once. Store it in an environment variable:

```bash
export DISBAND_KEY="disband_…"
```

## Split a song

The program uploads `song.mp3`, waits until the separation is ready and saves each stem as an MP3 file. It requires `curl` and `jq`, Node.js 22 or later, or Python with `requests`.

```bash
id=$(curl -sf "https://api.disband.io/v1/separations" \
  --url-query stems=vocals --url-query stems=instrumental \
  -H "Authorization: Bearer $DISBAND_KEY" \
  --data-binary @song.mp3 | jq -r .id)

while :; do
  separation=$(curl -sf "https://api.disband.io/v1/separations/$id" \
    -H "Authorization: Bearer $DISBAND_KEY" \
    -H "Prefer: wait=60")
  status=$(jq -r .status <<< "$separation")
  [ "$status" = queued ] || [ "$status" = separating ] || break
done

for stem in $(jq -r '.stems | to_entries[] | select(.value.mp3) | .key' <<< "$separation"); do
  curl -sf "https://api.disband.io/v1/separations/$id/stems/$stem/mp3" \
    -H "Authorization: Bearer $DISBAND_KEY" -o "$stem.mp3"
done
```

```javascript
import { readFile, writeFile } from "node:fs/promises";

const api = "https://api.disband.io/v1";
const headers = { Authorization: `Bearer ${process.env.DISBAND_KEY}` };

let separation = await fetch(`${api}/separations?stems=vocals&stems=instrumental`, {
  method: "POST",
  headers,
  body: await readFile("song.mp3"),
}).then((response) => response.json());

while (separation.status === "queued" || separation.status === "separating") {
  separation = await fetch(`${api}/separations/${separation.id}`, {
    headers: { ...headers, Prefer: "wait=60" },
  }).then((response) => response.json());
}
if (separation.status === "failed") throw new Error(separation.problem.title);

for (const [stem, files] of Object.entries(separation.stems)) {
  if (files.silent) continue;
  const file = await fetch(files.mp3.url, { headers });
  await writeFile(`${stem}.mp3`, Buffer.from(await file.arrayBuffer()));
}
```

```python
import os
import requests

api = "https://api.disband.io/v1"
headers = {"Authorization": f"Bearer {os.environ['DISBAND_KEY']}"}

with open("song.mp3", "rb") as song:
    separation = requests.post(
        f"{api}/separations",
        params={"stems": ["vocals", "instrumental"]},
        headers=headers,
        data=song,
    ).json()

while separation["status"] in ("queued", "separating"):
    separation = requests.get(
        f"{api}/separations/{separation['id']}",
        headers={**headers, "Prefer": "wait=60"},
    ).json()
if separation["status"] == "failed":
    raise RuntimeError(separation["problem"]["title"])

for stem, files in separation["stems"].items():
    if files.get("silent"):
        continue
    with open(f"{stem}.mp3", "wb") as out:
        out.write(requests.get(files["mp3"]["url"], headers=headers).content)
```

## What happens

1. `POST /separations` returns `202 Accepted` with the separation's `id`, `status: "queued"` and its `charge` in credits.
2. Each `GET /separations/{id}` with `Prefer: wait=60` returns when the status changes, from `queued` to `separating` to `ready`, or after 60 seconds.
3. The ready separation lists the files of each stem with their URLs and sizes. Downloads require the same key. Files are deleted one hour after the separation is ready.

A stem with no audio in the song, for example vocals in an instrumental track, is returned as `{"silent": true}` without files. The program skips these.

## Next steps

* [Separations](https://disband.io/docs/separations): all parameters, statuses and downloads.
* [Credits](https://disband.io/docs/credits): how the price is calculated.
* [Errors](https://disband.io/docs/errors): error responses and which ones to retry.
