> ## Documentation Index
> Fetch the complete documentation index at: https://docs.soundpiece.co/llms.txt
> Use this file to discover all available pages before exploring further.

# The reference client

> One small file that submits, waits, retries and downloads for you

Every Soundpiece endpoint works the same way: you submit a request, wait for it to finish, then download the result (see [Async operations](/async-operations)). The reference client is that loop written once, carefully, in a single file you copy into your project. It is short enough to read in a few minutes, and the rest of these docs use it.

It takes care of the details that are easy to get wrong:

* your API key comes from the `SOUNDPIECE_API_KEY` environment variable, never from your code;
* every request has a timeout;
* network errors, `429` and `5xx` responses are retried, waiting as long as the `Retry-After` header asks;
* a retried submit reuses its idempotency key, so a retry never creates the work twice;
* waiting uses long polling (`wait=true`) and gives up after a deadline;
* downloads are streamed to disk, not held in memory.

## Get the file

1. Set your API key in the environment your program runs in:

   ```bash theme={null}
   export SOUNDPIECE_API_KEY="spk_..."
   ```

2. Save the file for your language next to your code:
   * **Python:** save it as `soundpiece.py`, then run `pip install requests`.
   * **JavaScript:** save it as `soundpiece.mjs`. It needs Node.js 18 or later and nothing else.

```python soundpiece.py theme={null}
"""soundpiece.py — a small client for the Soundpiece API.
Copy this file into your project. Needs: pip install requests"""
import os
import time
import uuid

import requests

API_URL = "https://api.soundpiece.co/v1"
API_KEY = os.environ["SOUNDPIECE_API_KEY"]


class SoundpieceError(Exception):
    """A request was rejected, or an operation failed."""


def run(endpoint, body, idempotency_key=None, timeout=600):
    """Submit a request, wait for it to finish, and return the finished result."""
    body = {**body, "idempotency_key": idempotency_key or str(uuid.uuid4())}
    response = send("PUT", endpoint, json=body)
    name = list(response)[0]  # the result's name, e.g. "ducked_audio"
    result = response[name]

    give_up_at = time.monotonic() + timeout
    while result["status"] == "processing":
        if time.monotonic() > give_up_at:
            raise TimeoutError(f"{endpoint} {result['id']} is still processing")
        result = send("GET", endpoint, params={"id": result["id"], "wait": "true"})[name]

    if result["status"] == "failed":
        raise SoundpieceError(f"{result['error']['code']}: {result['error']['message']}")
    return result


def download(url, path):
    """Save a result's audio to a file."""
    with requests.get(url, stream=True, timeout=60) as response:
        response.raise_for_status()
        with open(path, "wb") as file:
            for chunk in response.iter_content(chunk_size=1024 * 1024):
                file.write(chunk)


def send(method, endpoint, **options):
    """Make one API call, retrying when that is safe: network errors, 429 and 5xx."""
    for attempt in range(5):
        wait = 2 ** attempt
        try:
            response = requests.request(
                method,
                f"{API_URL}/{endpoint}",
                headers={"Authorization": f"Bearer {API_KEY}"},
                timeout=30,  # longer than the 20-second long poll
                **options,
            )
        except (requests.ConnectionError, requests.Timeout):
            time.sleep(wait)
            continue
        if response.status_code == 429 or response.status_code >= 500:
            time.sleep(float(response.headers.get("Retry-After", 0)) or wait)
            continue
        if response.status_code >= 400:
            raise SoundpieceError(f"HTTP {response.status_code}: {response.json()['detail']}")
        return response.json()
    raise SoundpieceError(f"{method} {endpoint} kept failing; try again later")
```

```javascript soundpiece.mjs theme={null}
// soundpiece.mjs — a small client for the Soundpiece API.
// Copy this file into your project. Needs Node.js 18 or later; nothing to install.
import { randomUUID } from "node:crypto";
import { createWriteStream } from "node:fs";
import { Readable } from "node:stream";
import { pipeline } from "node:stream/promises";
import { setTimeout as sleep } from "node:timers/promises";

const API_URL = "https://api.soundpiece.co/v1";
const API_KEY = process.env.SOUNDPIECE_API_KEY;
if (!API_KEY) throw new Error("Set the SOUNDPIECE_API_KEY environment variable");

// A request was rejected, or an operation failed.
export class SoundpieceError extends Error {
  name = "SoundpieceError";
}

// Submit a request, wait for it to finish, and return the finished result.
export async function run(endpoint, body, { idempotencyKey, timeout = 600 } = {}) {
  body = { ...body, idempotency_key: idempotencyKey || randomUUID() };
  const response = await send("PUT", endpoint, { json: body });
  const name = Object.keys(response)[0]; // the result's name, e.g. "ducked_audio"
  let result = response[name];

  const giveUpAt = performance.now() + timeout * 1000;
  while (result.status === "processing") {
    if (performance.now() > giveUpAt) {
      throw new Error(`${endpoint} ${result.id} is still processing`);
    }
    result = (await send("GET", endpoint, { params: { id: result.id, wait: "true" } }))[name];
  }

  if (result.status === "failed") {
    throw new SoundpieceError(`${result.error.code}: ${result.error.message}`);
  }
  return result;
}

// Save a result's audio to a file.
export async function download(url, path) {
  const response = await fetch(url, { signal: AbortSignal.timeout(300_000) });
  if (!response.ok) throw new Error(`HTTP ${response.status} downloading ${url}`);
  await pipeline(Readable.fromWeb(response.body), createWriteStream(path));
}

// Make one API call, retrying when that is safe: network errors, 429 and 5xx.
export async function send(method, endpoint, { json, params } = {}) {
  const url = new URL(`${API_URL}/${endpoint}`);
  if (params) url.search = new URLSearchParams(params);

  for (let attempt = 0; attempt < 5; attempt++) {
    const wait = 2 ** attempt;
    let response;
    try {
      response = await fetch(url, {
        method,
        headers: { Authorization: `Bearer ${API_KEY}`, "Content-Type": "application/json" },
        body: json === undefined ? undefined : JSON.stringify(json),
        signal: AbortSignal.timeout(30_000), // longer than the 20-second long poll
      });
    } catch (error) {
      // A network error (TypeError) or no answer in time (TimeoutError): try again.
      if (error.name !== "TypeError" && error.name !== "TimeoutError") throw error;
      await sleep(wait * 1000);
      continue;
    }
    if (response.status === 429 || response.status >= 500) {
      await sleep((Number(response.headers.get("Retry-After")) || wait) * 1000);
      continue;
    }
    if (response.status >= 400) {
      const { detail } = await response.json();
      const message = typeof detail === "string" ? detail : JSON.stringify(detail);
      throw new SoundpieceError(`HTTP ${response.status}: ${message}`);
    }
    return response.json();
  }
  throw new SoundpieceError(`${method} ${endpoint} kept failing; try again later`);
}
```

## What each function does

* **`run(endpoint, body)`** submits `body` to `endpoint`, waits until the work is `ready`, and returns the finished result: the object under the response's single key, such as the `song` object from `create.new_song`. It raises a `SoundpieceError` if the operation fails, and gives up after `timeout` seconds (10 minutes unless you pass another value).
* **`download(url, path)`** saves the audio at one of a result's signed URLs to a file, streaming it to disk as it arrives.
* **`send(method, endpoint, ...)`** makes one API call with your key and a 30-second timeout. It retries network errors, `429` and `5xx` up to five attempts, waiting `Retry-After` seconds when the response gives a wait, and 1, 2, 4, 8 or 16 seconds when it is missing or `0`. `run` uses it; you only call it yourself for something `run` doesn't do, such as polling an operation you submitted earlier.
* **`SoundpieceError`** is what you get when the API rejects a request (any other `4xx`; the message carries the response's `detail`), when it keeps failing after every retry, or when an operation finishes as `failed` (the message carries its `error.code` and `error.message`).

## Using it

```python sample.py theme={null}
import soundpiece

sample = soundpiece.run("create.new_sample", {
    "prompt": "rock, guitar, energetic, no drums",
    "duration": 8,
    "loopable": True,
})
soundpiece.download(sample["output"]["url"], "sample.flac")
print("Saved sample.flac")
```

```javascript sample.mjs theme={null}
import { run, download } from "./soundpiece.mjs";

const sample = await run("create.new_sample", {
  prompt: "rock, guitar, energetic, no drums",
  duration: 8,
  loopable: true,
});
await download(sample.output.url, "sample.flac");
console.log("Saved sample.flac");
```

Endpoints that make several files return them as a list, `outputs`; endpoints that make one file return `output`. Each endpoint's guide, such as [New sample](/guides/create/new_sample), describes its result and has a complete example using this client; the exact fields are in the API Reference, e.g. [`create.new_sample` (submit)](/api-reference/create/new_sample/submit).

## Keeping an idempotency key across re-runs

`run` makes a new idempotency key each time you call it, and reuses that key for every retry inside the call. That covers a dropped connection or a busy moment. It does not cover your own program stopping: if it crashes or restarts after submitting, calling `run` again sends a new key, which starts the work again and charges for it again.

To make a job survive a restart, create its key yourself, save it with your own record of the job **before** the first call, and pass it to `run` every time. Sending the same key again returns the original operation instead of starting new work, so `run` simply carries on waiting for it.

```python resume.py theme={null}
import json
import pathlib
import uuid

import soundpiece

# Save the key before submitting, so a re-run of this program finds it again.
job_file = pathlib.Path("sample-job.json")
if not job_file.exists():
    job_file.write_text(json.dumps({"idempotency_key": str(uuid.uuid4())}))
key = json.loads(job_file.read_text())["idempotency_key"]

sample = soundpiece.run(
    "create.new_sample",
    {"prompt": "rock, guitar, energetic, no drums", "duration": 8},
    idempotency_key=key,
)
soundpiece.download(sample["output"]["url"], "sample.flac")
print("Saved sample.flac")
```

```javascript resume.mjs theme={null}
import { randomUUID } from "node:crypto";
import { existsSync, readFileSync, writeFileSync } from "node:fs";
import { run, download } from "./soundpiece.mjs";

// Save the key before submitting, so a re-run of this program finds it again.
const jobFile = "sample-job.json";
if (!existsSync(jobFile)) {
  writeFileSync(jobFile, JSON.stringify({ idempotency_key: randomUUID() }));
}
const key = JSON.parse(readFileSync(jobFile, "utf8")).idempotency_key;

const sample = await run(
  "create.new_sample",
  { prompt: "rock, guitar, energetic, no drums", duration: 8 },
  { idempotencyKey: key },
);
await download(sample.output.url, "sample.flac");
console.log("Saved sample.flac");
```

In a real application the key belongs in your database, in the same row as the job it identifies. Use one key per job: a key that was already used for a different endpoint is refused with a `422`.

## Making it your own

The file is yours to change. Before you do, read the [production checklist](/best-practices): it lists what any client of the API should do, whether it is this one or one you write yourself.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.