> ## 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.

# Async operations

> The submit-poll-download lifecycle every endpoint follows

## 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:

| Status       | Meaning                                                                           |
| ------------ | --------------------------------------------------------------------------------- |
| `processing` | Submitted; work is in flight. `outputs` / `output` and `error` are both `null`.   |
| `ready`      | Done. The `outputs` (or `output`) field is populated with downloadable audio.     |
| `failed`     | Something 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.

```mermaid theme={null}
sequenceDiagram
  participant Client
  participant Soundpiece

  Client->>Soundpiece: PUT /v1/create.new_song { idempotency_key, lyrics, prompt }
  Soundpiece-->>Client: { id: "op_…", status: "processing", … }

  loop until status is ready or failed
    Client->>Soundpiece: GET /v1/create.new_song?id=op_…&wait=true
    Note over Soundpiece: long-polls up to 20s
    Soundpiece-->>Client: { id: "op_…", status: "processing", … }
  end

  Soundpiece-->>Client: { id: "op_…", status: "ready", outputs: [{ url, expires_at, duration }, …] }

  Client->>Soundpiece: GET <signed url>
  Soundpiece-->>Client: FLAC audio bytes
```

***

## 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.

```http theme={null}
PUT /v1/create.new_song
Authorization: Bearer <your-api-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:

```json theme={null}
{
  "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.

```http theme={null}
GET /v1/create.new_song?id=op_01h7k...&wait=true
Authorization: Bearer <your-api-key>
```

If you get back `processing` after 20 seconds, just call again — the request is cheap to repeat.

```python Python theme={null}
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...", "<your-api-key>", key="song")
```

Polling is the **canonical** source of truth. [Webhooks](/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`:

```json theme={null}
{
  "song": {
    "id": "op_01h7k...",
    "status": "failed",
    "error": {
      "code": "content_policy",
      "message": "The provided lyrics were rejected by our content policy."
    },
    ...
  }
}
```

See the [error reference](/errors) 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.

```json theme={null}
{
  "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 Python theme={null}
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](/webhooks) and only poll as a fallback.
