Skip to main content

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.

Why async?

Audio generation and adaptation can take anywhere from a few seconds to a minute or so. Holding an HTTP connection open for that long is fragile — proxies time out, mobile networks drop, retries become ambiguous. Soundpiece returns immediately from every PUT with a handle, and you fetch the result when it’s ready. The same pattern applies to every endpoint, so once you’ve learnt it for one capability you’ve learnt it for all of them.

The lifecycle

Every operation moves through three statuses:
StatusMeaning
processingSubmitted; work is in flight. outputs / output and error are both null.
readyDone. The outputs (or output) field is populated with downloadable audio.
failedSomething went wrong. The error field is populated with a code and message.
outputs / output and error are mutually exclusive — exactly one or the other is populated when the operation terminates.

Submitting work

Every PUT accepts an idempotency_key — a UUID you generate. Sending the same request twice with the same key returns the original response without doing the work again. This is the safe way to retry over an unreliable network.
PUT /v1/create.new_song
Authorization: Bearer spk_<your-key>
Content-Type: application/json

{
  "idempotency_key": "550e8400-e29b-41d4-a716-446655440000",
  "lyrics": "I've been driving all night, my hands wet on the wheel",
  "prompt": "Synthwave, melancholic, mid-tempo"
}
You get back the operation DTO immediately:
{
  "song": {
    "id": "op_01h7k...",
    "status": "processing",
    "created_at": "2026-05-20T12:34:56Z",
    "idempotency_key": "550e8400-e29b-41d4-a716-446655440000",
    "lyrics": "I've been driving all night, my hands wet on the wheel",
    "prompt": "Synthwave, melancholic, mid-tempo",
    "reference": null,
    "outputs": null,
    "error": null
  }
}
The id is what you’ll use to fetch the result. The request fields are echoed back so the operation is self-describing without your own bookkeeping.

Polling

Call the same endpoint with GET and the operation id. Pass ?wait=true to long-poll: the server returns as soon as the operation reaches ready or failed, or after 20 seconds if it’s still running.
GET /v1/create.new_song?id=op_01h7k...&wait=true
Authorization: Bearer spk_<your-key>
If you get back processing after 20 seconds, just call again — the request is cheap to repeat.
Python
import time

def wait_for(endpoint, operation_id, api_key, key, timeout=300):
    start = time.time()
    while time.time() - start < timeout:
        res = requests.get(
            f"https://api.soundpiece.co/v1/{endpoint}",
            headers={"Authorization": f"Bearer {api_key}"},
            params={"id": operation_id, "wait": "true"},
        )
        op = res.json()[key]
        if op["status"] in ("ready", "failed"):
            return op
    raise TimeoutError("Operation did not complete in time")

result = wait_for("create.new_song", "op_01h7k...", "spk_…", key="song")
Polling is the canonical source of truth. Webhooks are a latency optimisation on top — if you don’t receive a webhook within a reasonable window, fall back to polling.

Idempotency end-to-end

Idempotency carries through more than just retries:
  • The idempotency_key is echoed on every response and webhook payload — so you can correlate work end-to-end against your own records without a request-id-to-state map.
  • 5xx responses on PUT endpoints are safely retryable — internal retry logic guarantees that the same idempotency_key lands at most once. You’ll never accidentally create two operations from one logical request.

Handling failures

When an operation itself fails (model rejection, source unreachable, content-policy violation, etc.) you get back status: "failed" with a populated error:
{
  "song": {
    "id": "op_01h7k...",
    "status": "failed",
    "error": {
      "code": "content_policy",
      "message": "The provided lyrics were rejected by our content policy."
    },
    ...
  }
}
See the error reference for the full catalogue of error.code values per endpoint.

Downloading the result

When status is ready, each entry in outputs (or output) carries a signed URL you can GET to download the audio as FLAC (44.1 kHz). The URL is time-limited; check expires_at. If a URL has expired, call the GET endpoint again — we’ll generate a fresh one.
{
  "outputs": [
    {
      "url": "https://cdn.soundpiece.co/...signed...",
      "expires_at": "2026-05-20T12:50:21Z",
      "duration": 87.4
    }
  ]
}
Don’t cache the URL itself — it’s signed against a short window. Do cache the audio bytes if you need them again.

Putting it together

A complete client looks like this:
Python
import os
import time
import uuid
import requests

API = "https://api.soundpiece.co/v1"
HEADERS = {"Authorization": f"Bearer {os.environ['SOUNDPIECE_API_KEY']}"}

def generate_song(lyrics: str, prompt: str) -> str:
    """Generate a song and return the first downloadable URL."""
    # 1. Submit
    res = requests.put(
        f"{API}/create.new_song",
        headers=HEADERS,
        json={
            "idempotency_key": str(uuid.uuid4()),
            "lyrics": lyrics,
            "prompt": prompt,
        },
    )
    res.raise_for_status()
    song = res.json()["song"]

    # 2. Poll
    while song["status"] == "processing":
        res = requests.get(
            f"{API}/create.new_song",
            headers=HEADERS,
            params={"id": song["id"], "wait": "true"},
        )
        res.raise_for_status()
        song = res.json()["song"]

    # 3. Handle terminal status
    if song["status"] == "failed":
        raise RuntimeError(f"{song['error']['code']}: {song['error']['message']}")

    return song["outputs"][0]["url"]
For production, replace the polling loop with a webhook handler and only poll as a fallback.